返回 presentation-ai
EditSlideProperties.tsx
根目录 / src / components / presentation / agent / tools / EditSlideProperties.tsx
1 "use client";
2
3 import { Loader2 } from "lucide-react";
4 import { useEffect, useMemo, useState } from "react";
5
6 import { Button } from "@/components/ui/button";
7 import ColorPicker from "@/components/ui/color-picker";
8 import { Label } from "@/components/ui/label";
9 import {
10 Select,
11 SelectContent,
12 SelectItem,
13 SelectTrigger,
14 SelectValue,
15 } from "@/components/ui/select";
16 import {
17 executeToolAction,
18 getSlidesToUpdate,
19 } from "@/hooks/presentation/agentTools";
20
21 type Scope = "all" | undefined;
22
23 export function PresentationEditSlidePropertiesCall({
24 scope,
25 slideIds,
26 bgColor,
27 alignment,
28 layoutType,
29 width,
30 loading,
31 }: {
32 scope?: Scope;
33 slideIds?: string[];
34 bgColor?: string;
35 alignment?: "start" | "center" | "end";
36 layoutType?: "left" | "right" | "vertical" | "background";
37 width?: "S" | "M" | "L";
38 loading?: boolean;
39 }) {
40 const [form, setForm] = useState({
41 bgColor: bgColor ?? "",
42 alignment: alignment ?? undefined,
43 layoutType: layoutType ?? undefined,
44 width: width ?? undefined,
45 } as {
46 bgColor: string;
47 alignment?: "start" | "center" | "end";
48 layoutType?: "left" | "right" | "vertical" | "background";
49 width?: "S" | "M" | "L";
50 });
51
52 useEffect(() => {
53 if (!bgColor && !alignment && !layoutType && !width) return;
54 setForm({
55 bgColor: bgColor ?? "",
56 alignment: alignment ?? undefined,
57 layoutType: layoutType ?? undefined,
58 width: width ?? undefined,
59 });
60 }, [bgColor, alignment, layoutType, width]);
61
62 const targetSlides = useMemo(
63 () => getSlidesToUpdate(scope, slideIds),
64 [scope, slideIds],
65 );
66
67 const [isEditing, setIsEditing] = useState(false);
68
69 const apply = () => {
70 try {
71 executeToolAction({
72 action: "edit_slide_properties",
73 scope,
74 slideIds: scope === "all" ? undefined : targetSlides,
75 ...(form.bgColor ? { bgColor: form.bgColor } : {}),
76 ...(form.alignment ? { alignment: form.alignment } : {}),
77 ...(form.layoutType ? { layoutType: form.layoutType } : {}),
78 ...(form.width ? { width: form.width } : {}),
79 });
80 setIsEditing(false);
81 } catch (error) {
82 console.error("Error applying slide properties:", error);
83 }
84 };
85
86 const slideCount =
87 scope === "all" || (!scope && (!targetSlides || targetSlides.length === 0))
88 ? "all"
89 : (targetSlides?.length ?? 0);
90
91 const summaryText = useMemo(() => {
92 const parts: string[] = [];
93 if (form.bgColor) parts.push(`background to ${form.bgColor}`);
94 if (form.layoutType) parts.push(`layout to ${form.layoutType}`);
95 if (form.width) {
96 const widthWord =
97 form.width === "S" ? "small" : form.width === "M" ? "medium" : "large";
98 parts.push(`width to ${widthWord}`);
99 }
100 if (form.alignment) {
101 const alignmentWord =
102 form.alignment === "center"
103 ? "aligned content to centered"
104 : form.alignment === "start"
105 ? "aligned content to left"
106 : "aligned content to right";
107 parts.push(alignmentWord);
108 }
109
110 if (parts.length === 0) return "No changes selected";
111 const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
112 if (parts.length === 1) return capitalize(parts[0] ?? "");
113 const last = parts[parts.length - 1] ?? "";
114 const body = `Updated ${parts.slice(0, -1).join(", ")} and ${last}`;
115 return capitalize(body);
116 }, [form]);
117
118 // Show loading state when no properties are available
119 if (
120 !bgColor &&
121 !alignment &&
122 !layoutType &&
123 !width &&
124 !form.bgColor &&
125 !form.alignment &&
126 !form.layoutType &&
127 !form.width
128 )
129 return (
130 <div className="w-full rounded-lg border bg-card p-3">
131 <div className="flex items-center gap-2">
132 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
133 <span className="text-sm text-muted-foreground">
134 Updating presentation...
135 </span>
136 </div>
137 </div>
138 );
139
140 if (isEditing) {
141 return (
142 <div className="space-y-3 rounded-lg border bg-card p-4">
143 <div className="flex items-center justify-between">
144 <h3 className="text-sm font-medium">Edit Slide Properties</h3>
145 <Button
146 size="sm"
147 variant="ghost"
148 onClick={() => setIsEditing(false)}
149 className="h-7 text-xs"
150 >
151 Cancel
152 </Button>
153 </div>
154
155 <div className="grid gap-3">
156 <div className="space-y-1.5">
157 <Label className="text-xs">Background</Label>
158 <ColorPicker
159 value={form.bgColor || "#ffffff"}
160 onChange={(v) => setForm((f) => ({ ...f, bgColor: v || "" }))}
161 >
162 <Button
163 variant="outline"
164 className="h-9 w-full justify-start gap-2 bg-transparent font-mono text-xs"
165 >
166 <div
167 className="h-4 w-4 rounded border"
168 style={{ backgroundColor: form.bgColor || "#ffffff" }}
169 />
170 {form.bgColor || "Select color"}
171 </Button>
172 </ColorPicker>
173 </div>
174
175 <div className="grid grid-cols-2 gap-3">
176 <div className="space-y-1.5">
177 <Label className="text-xs">Layout</Label>
178 <Select
179 value={form.layoutType}
180 onValueChange={(v) =>
181 setForm((f) => ({
182 ...f,
183 layoutType: v as typeof f.layoutType,
184 }))
185 }
186 >
187 <SelectTrigger className="h-9">
188 <SelectValue placeholder="Select" />
189 </SelectTrigger>
190 <SelectContent>
191 <SelectItem value="left">Left</SelectItem>
192 <SelectItem value="right">Right</SelectItem>
193 <SelectItem value="vertical">Vertical</SelectItem>
194 <SelectItem value="background">Background</SelectItem>
195 </SelectContent>
196 </Select>
197 </div>
198
199 <div className="space-y-1.5">
200 <Label className="text-xs">Width</Label>
201 <Select
202 value={form.width}
203 onValueChange={(v) =>
204 setForm((f) => ({ ...f, width: v as typeof f.width }))
205 }
206 >
207 <SelectTrigger className="h-9">
208 <SelectValue placeholder="Select" />
209 </SelectTrigger>
210 <SelectContent>
211 <SelectItem value="S">S</SelectItem>
212 <SelectItem value="M">M</SelectItem>
213 <SelectItem value="L">L</SelectItem>
214 </SelectContent>
215 </Select>
216 </div>
217 </div>
218
219 <div className="space-y-1.5">
220 <Label className="text-xs">Alignment</Label>
221 <Select
222 value={form.alignment}
223 onValueChange={(v) =>
224 setForm((f) => ({
225 ...f,
226 alignment: v as typeof f.alignment,
227 }))
228 }
229 >
230 <SelectTrigger className="h-9">
231 <SelectValue placeholder="Select" />
232 </SelectTrigger>
233 <SelectContent>
234 <SelectItem value="start">Start</SelectItem>
235 <SelectItem value="center">Center</SelectItem>
236 <SelectItem value="end">End</SelectItem>
237 </SelectContent>
238 </Select>
239 </div>
240
241 <Button size="sm" onClick={apply} disabled={loading} className="mt-1">
242 Apply to {slideCount} {slideCount === 1 ? "slide" : "slides"}
243 </Button>
244 </div>
245 </div>
246 );
247 }
248
249 return (
250 <button
251 onClick={() => setIsEditing(true)}
252 disabled={loading}
253 className="w-full rounded-lg border bg-card p-3 text-left transition-colors hover:bg-accent/50 disabled:opacity-60"
254 >
255 <div className="flex items-center justify-between gap-2">
256 <div className="flex min-w-0 items-center gap-2">
257 {form.bgColor && (
258 <div
259 className="h-4 w-4 rounded border shadow-xs"
260 style={{ backgroundColor: form.bgColor }}
261 />
262 )}
263 <span className="truncate text-sm">{summaryText}</span>
264 <span className="shrink-0 text-xs text-muted-foreground">
265 ({slideCount} {slideCount === 1 ? "slide" : "slides"})
266 </span>
267 </div>
268 </div>
269 </button>
270 );
271 }
272
273 export function PresentationEditSlidePropertiesResult({
274 message,
275 }: {
276 message?: string;
277 }) {
278 return (
279 <div className="rounded-lg border border-green-200 bg-green-50 px-3 py-2 dark:border-green-900 dark:bg-green-950/20">
280 <span className="text-sm text-green-900 dark:text-green-100">
281 {message ?? "Slide properties updated"}
282 </span>
283 </div>
284 );
285 }
286
286 lines Plain Text