返回 presentation-ai
EditingSlide.tsx
根目录 / src / components / presentation / agent / tools / EditingSlide.tsx
1 import { useEffect, useMemo, useRef, useState } from "react";
2
3 import PresentationEditorStaticView from "@/components/notebook/presentation/editor/presentation-editor-static";
4 import { parseSlideXml } from "@/components/notebook/presentation/utils/parser";
5 import { baseWidths } from "@/hooks/presentation/scaling";
6 import { cn } from "@/lib/utils";
7
8 function getStablePreviewId(input: string, index: number): string {
9 let hash = 0;
10
11 for (let position = 0; position < input.length; position += 1) {
12 hash = (hash << 5) - hash + input.charCodeAt(position);
13 hash |= 0;
14 }
15
16 return `preview-${index}-${Math.abs(hash).toString(36)}`;
17 }
18
19 export default function EditingSlide({ slides = [] }: { slides: string[] }) {
20 return (
21 <div
22 className={cn(
23 "scrollbar-thumb-rounded-full scrollbar-thin flex max-w-80 flex-col gap-2 overflow-y-auto scrollbar-thumb-muted-foreground scrollbar-track-transparent",
24 slides?.length > 1 ? "h-80" : "h-55",
25 )}
26 >
27 <h3 className="text-lg font-bold">
28 Editing {slides?.length > 1 ? "slides" : "slide"}
29 </h3>
30 <div className="flex h-max w-full flex-col gap-2">
31 {slides?.map((slideString, index) => (
32 <MemoizedEditingSlide
33 key={getStablePreviewId(slideString, index)}
34 slideString={slideString}
35 index={index}
36 />
37 ))}
38 </div>
39 </div>
40 );
41 }
42
43 export function MemoizedEditingSlide({
44 slideString,
45 index,
46 }: {
47 slideString: string;
48 index: number;
49 }) {
50 const previewId = useMemo(
51 () => getStablePreviewId(slideString, index),
52 [index, slideString],
53 );
54 const slide = useMemo(() => {
55 const parsed = parseSlideXml(slideString);
56 return parsed[0];
57 }, [slideString]);
58
59 // Static, one-time scaling to fit available width
60 const containerRef = useRef<HTMLDivElement | null>(null);
61 const contentRef = useRef<HTMLDivElement | null>(null);
62 const [scale, setScale] = useState(1);
63 const [height, setHeight] = useState<number | undefined>(undefined);
64
65 useEffect(() => {
66 const container = containerRef.current;
67 const content = contentRef.current;
68 if (!container || !content) return;
69
70 // Compute scale once based on current container width and a fixed base width
71 const baseWidth = baseWidths.M;
72 const containerWidth = container.clientWidth || baseWidth;
73 const staticScale = Math.max(0.1, containerWidth / baseWidth);
74 setScale(staticScale);
75
76 // Measure unscaled height once and set the container's layout height
77 const unscaledHeight = content.offsetHeight || 0;
78 setHeight(
79 Math.max(0, Math.ceil(unscaledHeight * staticScale)) || undefined,
80 );
81 }, []);
82
83 return (
84 <div className="group relative overflow-hidden rounded-md border border-muted bg-card">
85 <div className="absolute top-1 left-2 z-10 rounded-sm bg-muted px-1 py-0.5 text-xs font-medium text-muted-foreground">
86 {index + 1}
87 </div>
88 <div
89 ref={containerRef}
90 className="pointer-events-none w-full overflow-hidden"
91 style={{
92 height: height ?? undefined,
93 aspectRatio: height === undefined ? "16/9" : undefined,
94 }}
95 >
96 <div
97 ref={contentRef}
98 style={{
99 transform: `scale(${scale})`,
100 transformOrigin: "top left",
101 width: baseWidths.M,
102 }}
103 >
104 <PresentationEditorStaticView
105 initialContent={slide}
106 className="min-h-55 w-full border"
107 id={previewId}
108 />
109 </div>
110 </div>
111 </div>
112 );
113 }
114
114 lines Plain Text