返回 presentation-ai
GenerateControls.tsx
1 "use client";
2
3 import Image from "next/image";
4 import { type TElement } from "platejs";
5 import { useEditorRef } from "platejs/react";
6
7 import { type Image as GeneratedImage } from "@/app/_actions/apps/image-studio/fetch";
8 import { SharedGenerateControls } from "@/components/presentation/shared/SharedGenerateControls";
9 import { useDebouncedSave } from "@/hooks/presentation/useDebouncedSave";
10 import { usePresentationState } from "@/states/presentation-state";
11 import { type RootImage as RootImageType } from "../../../utils/parser";
12 import { ActionButtons } from "./ActionButtons";
13 import { ImagePreview } from "./ImagePreview";
14 import { type ImageDimensions } from "./useImageDimensions";
15
16 interface GenerateControlsProps {
17 element: TElement & RootImageType;
18 slideId?: string;
19 isRootImage: boolean;
20 onImageSelect?: (url: string, prompt: string) => void;
21 onImagesGenerated?: (images: GeneratedImage[]) => void;
22 onOpenCrop?: () => void;
23 imageDimensions?: ImageDimensions;
24 }
25
26 export function GenerateControls({
27 element,
28 slideId,
29 isRootImage,
30 onImageSelect,
31 onImagesGenerated,
32 onOpenCrop,
33 imageDimensions,
34 }: GenerateControlsProps) {
35 const editor = useEditorRef(slideId);
36 const { saveImmediately } = useDebouncedSave();
37
38 const handleImageSelect = (url: string, prompt: string) => {
39 const { setSlides, clearRootImageGeneration } =
40 usePresentationState.getState();
41 if (isRootImage) {
42 if (slideId) clearRootImageGeneration(slideId);
43 setSlides((slides) =>
44 slides.map((slide) =>
45 slide.id === slideId
46 ? {
47 ...slide,
48 rootImage: {
49 ...slide.rootImage!,
50 url: url,
51 query: prompt,
52 imageSource: "generate",
53 embedType: undefined,
54 chartType: undefined,
55 chartData: undefined,
56 },
57 }
58 : slide,
59 ),
60 );
61 void saveImmediately();
62 } else {
63 editor.tf.setNodes(
64 { url: url, query: prompt, embedType: undefined },
65 { at: editor.api.findPath(element) },
66 );
67 void saveImmediately();
68 }
69 };
70
71 const selectImage = onImageSelect ?? handleImageSelect;
72
73 // Calculate thumbnail scale to fit in 90x90 box
74 const thumbnailDimensions = imageDimensions
75 ? {
76 ...imageDimensions,
77 scale: Math.min(
78 360 / imageDimensions.width,
79 360 / imageDimensions.height,
80 ),
81 }
82 : { width: 360, height: 360, scale: 1 };
83
84 const handleCropSettingsChange = (settings: typeof element.cropSettings) => {
85 const { setSlides } = usePresentationState.getState();
86 if (isRootImage) {
87 setSlides((slides) =>
88 slides.map((slide) =>
89 slide.id === slideId
90 ? {
91 ...slide,
92 rootImage: {
93 ...slide.rootImage!,
94 cropSettings: settings,
95 },
96 }
97 : slide,
98 ),
99 );
100 void saveImmediately();
101 } else {
102 editor.tf.setNodes(
103 { cropSettings: settings },
104 { at: editor.api.findPath(element) },
105 );
106 void saveImmediately();
107 }
108 };
109
110 return (
111 <div className="flex flex-col gap-4">
112 {/* Mini Preview */}
113
114 <div className="flex flex-col gap-2">
115 <div className="flex justify-center">
116 <div className="relative flex size-90 items-center justify-center overflow-hidden rounded-md border bg-muted shadow">
117 {imageDimensions ? (
118 <ImagePreview
119 element={element}
120 currentMode="generate"
121 localCropSettings={{
122 objectFit: element.cropSettings?.objectFit ?? "cover",
123 objectPosition: {
124 x: element.cropSettings?.objectPosition.x ?? 50,
125 y: element.cropSettings?.objectPosition.y ?? 50,
126 },
127 zoom: element.cropSettings?.zoom ?? 1,
128 }}
129 imageDimensions={thumbnailDimensions}
130 onCropSettingsChange={() => {}}
131 hideControls={true}
132 />
133 ) : element.url ? (
134 <Image
135 unoptimized
136 width={400}
137 height={300}
138 src={element.url}
139 alt="Preview"
140 className="h-full w-full object-cover"
141 />
142 ) : (
143 <div className="h-full w-full bg-muted" />
144 )}
145 </div>
146 </div>
147
148 {/* Action Buttons - Always Visible */}
149 <div className="flex justify-center">
150 <ActionButtons
151 element={element}
152 slideId={slideId}
153 isRootImage={isRootImage}
154 imageUrl={element.url}
155 onOpenCrop={onOpenCrop}
156 cropSettings={{
157 objectFit: element.cropSettings?.objectFit ?? "cover",
158 objectPosition: {
159 x: element.cropSettings?.objectPosition.x ?? 50,
160 y: element.cropSettings?.objectPosition.y ?? 50,
161 },
162 zoom: element.cropSettings?.zoom ?? 1,
163 }}
164 onCropSettingsChange={handleCropSettingsChange}
165 showInOverlay={true}
166 />
167 </div>
168 </div>
169
170 <SharedGenerateControls
171 onImageSelect={selectImage}
172 initialPrompt={element.query ?? ""}
173 onImagesGenerated={onImagesGenerated}
174 />
175 </div>
176 );
177 }
178
178 lines Plain Text