返回 presentation-ai
agentTools.ts
根目录 / src / hooks / presentation / agentTools.ts
1 import { type PresentationTool } from "@/ai/tools/presentation/tools";
2 import {
3 parseSlideXml,
4 type PlateSlide,
5 } from "@/components/notebook/presentation/utils/parser";
6 import { usePresentationState } from "@/states/presentation-state";
7
8 interface ToolArgs {
9 action: PresentationTool;
10 scope?: "all";
11 slideIds?: string[];
12 [key: string]: unknown;
13 }
14
15 async function executeSearchTool(args: Record<string, unknown>) {
16 const query = typeof args.query === "string" ? args.query.trim() : "";
17
18 if (!query) {
19 return "Missing search query";
20 }
21
22 const response = await fetch("/api/agent/presentation/search", {
23 method: "POST",
24 headers: {
25 "Content-Type": "application/json",
26 },
27 body: JSON.stringify({ query }),
28 });
29
30 if (!response.ok) {
31 throw new Error("Search tool request failed");
32 }
33
34 const data = (await response.json()) as {
35 result?: string;
36 };
37
38 return data.result ?? "Search completed";
39 }
40
41 function editSlideProperties(
42 slidesToUpdate: string[],
43 rest: Record<string, unknown>,
44 ) {
45 const { slides, setSlides } = usePresentationState.getState();
46 const { bgColor, alignment, layoutType, width } = rest as {
47 bgColor?: string;
48 alignment?: "start" | "center" | "end" | "reset";
49 layoutType?: "left" | "right" | "vertical" | "background" | "reset";
50 width?: "S" | "M" | "L" | "reset";
51 };
52
53 if (!bgColor && !alignment && !layoutType && !width) {
54 return "No properties provided";
55 }
56 const updatedSlides = slides.map((slide) => {
57 if (slidesToUpdate.includes(slide.id)) {
58 return {
59 ...slide,
60 ...(bgColor
61 ? { bgColor: bgColor === "reset" ? undefined : bgColor }
62 : {}),
63 ...(alignment
64 ? { alignment: alignment === "reset" ? undefined : alignment }
65 : {}),
66 ...(layoutType
67 ? { layoutType: layoutType === "reset" ? "left" : layoutType }
68 : {}),
69 ...(width ? { width: width === "reset" ? "M" : width } : {}),
70 };
71 }
72 return slide;
73 });
74 setSlides(updatedSlides);
75 return `Updated ${Object.keys(rest).join(", ")} successfully to ${Object.values(rest).join(", ")}`;
76 }
77
78 function replaceImage(slidesToUpdate: string[], rest: Record<string, unknown>) {
79 const { slides, setSlides } = usePresentationState.getState();
80 const { imageUrl, imagePrompt } = rest;
81
82 if (
83 !imageUrl &&
84 !imagePrompt &&
85 typeof imageUrl !== "string" &&
86 typeof imagePrompt !== "string"
87 ) {
88 return "No image URL or image prompt provided";
89 }
90
91 if (imageUrl) {
92 const updatedSlides = slides.map((slide) => {
93 if (slidesToUpdate.includes(slide.id)) {
94 return {
95 ...slide,
96 rootImage: {
97 ...slide.rootImage,
98 url: imageUrl as string,
99 embedType: undefined, // Clear embed type when replacing image
100 },
101 };
102 }
103 return slide;
104 });
105 setSlides(updatedSlides as PlateSlide[]);
106 return "Image url replaced successfully";
107 } else if (imagePrompt) {
108 const updatedSlides = slides.map((slide) => {
109 if (slidesToUpdate.includes(slide.id)) {
110 const { startRootImageGeneration } = usePresentationState.getState();
111 startRootImageGeneration(slide.id as string, imagePrompt as string);
112 return {
113 ...slide,
114 rootImage: {
115 ...slide.rootImage,
116 query: imagePrompt as string,
117 embedType: undefined, // Clear embed type when generating image
118 },
119 };
120 }
121 return slide;
122 });
123 setSlides(updatedSlides as PlateSlide[]);
124 return "Image successfully generated from the given prompt";
125 }
126
127 return "No image url or image prompt provided";
128 }
129
130 function changeTheme(rest: Record<string, unknown>) {
131 const { setTheme } = usePresentationState.getState();
132 const { theme } = rest;
133 if (!theme && typeof theme !== "string") {
134 return "No theme provided";
135 }
136 setTheme(theme as string);
137 return "Theme changed successfully";
138 }
139
140 export function regenerateSlide(rest: Record<string, unknown>) {
141 const { slides, setSlides } = usePresentationState.getState();
142 const { slides: slidesString, slideIds } = rest as {
143 slides: string[];
144 slideIds: string[];
145 };
146
147 if (!slidesString) {
148 return "No slides provided";
149 }
150
151 const updatedSlides = slides.map((slide) => {
152 console.log("Slide", slide);
153 if (slideIds.includes(slide.id)) {
154 const slideString = slidesString[slideIds.indexOf(slide.id)] as string;
155 const parsedSlide = parseSlideXml(slideString as string);
156 console.log("Parsed slide", parsedSlide);
157 return {
158 ...parsedSlide[0],
159 id: slide.id, // Preserve the original slide ID
160 rootImage: slide.rootImage,
161 };
162 }
163
164 return slide;
165 });
166 setSlides(updatedSlides as PlateSlide[]);
167 return `Slides ${slideIds.join(", ")} regenerated successfully`;
168 }
169
170 export function createSlide(rest: Record<string, unknown>) {
171 const { slides, setSlides } = usePresentationState.getState();
172 const { slides: newSlidesXml, afterSlideId } = rest as {
173 slides?: string[];
174 afterSlideId?: string;
175 };
176
177 if (!Array.isArray(newSlidesXml) || newSlidesXml.length === 0) {
178 return "No slides provided";
179 }
180
181 const parsedNewSlides: PlateSlide[] = [];
182 for (const xml of newSlidesXml) {
183 try {
184 const parsed = parseSlideXml(xml ?? "");
185 if (parsed?.[0]) parsedNewSlides.push(parsed[0]);
186 } catch (e) {
187 console.error("Failed to parse new slide:", e);
188 }
189 }
190
191 if (parsedNewSlides.length === 0) {
192 return "No valid slides to insert";
193 }
194
195 const updated = [...slides];
196 if (afterSlideId) {
197 const idx = updated.findIndex((s) => s.id === afterSlideId);
198 if (idx >= 0) updated.splice(idx + 1, 0, ...parsedNewSlides);
199 else updated.push(...parsedNewSlides);
200 } else {
201 updated.push(...parsedNewSlides);
202 }
203
204 setSlides(updated);
205 return `Added ${parsedNewSlides.length} slide${parsedNewSlides.length !== 1 ? "s" : ""}`;
206 }
207
208 export function deleteSlide(rest: Record<string, unknown>) {
209 const { slides, setSlides } = usePresentationState.getState();
210 const { slideIds } = rest as { slideIds?: string[] };
211
212 if (!Array.isArray(slideIds) || slideIds.length === 0) {
213 return "No slide ids provided";
214 }
215
216 const toDelete = new Set(slideIds);
217 const updated = slides.filter((s) => !toDelete.has(s.id));
218 setSlides(updated);
219 return `Deleted ${slideIds.length} slide${slideIds.length !== 1 ? "s" : ""}`;
220 }
221
222 export function getSlidesToUpdate(scope?: "all", slideIds?: string[]) {
223 const { slides } = usePresentationState.getState();
224 return scope === "all" || (!scope && (!slideIds || slideIds?.length === 0))
225 ? slides.map((slide) => slide.id)
226 : slideIds;
227 }
228 /**
229 * Execute a tool action on the presentation
230 */
231 export function executeToolAction(args: ToolArgs) {
232 const { action, slideIds, scope, ...rest } = args;
233
234 const slidesToUpdate = getSlidesToUpdate(scope, slideIds);
235
236 if (!slidesToUpdate) {
237 return "No slides to update";
238 }
239
240 switch (action) {
241 case "edit_slide_properties":
242 return editSlideProperties(slidesToUpdate, rest);
243 case "replace_image":
244 return replaceImage(slidesToUpdate, rest);
245 case "change_theme":
246 return changeTheme(args);
247 case "regenerate_slide":
248 return regenerateSlide(args);
249 case "create_slide":
250 return createSlide(args);
251 case "delete_slide":
252 return deleteSlide(args);
253 default:
254 console.warn(`Unknown tool action: ${action}`);
255 return "Unknown tool action";
256 }
257 }
258
259 export async function executeToolCall({
260 name,
261 args,
262 }: {
263 name: string;
264 args: unknown;
265 }): Promise<string> {
266 try {
267 const parsedArgs = typeof args === "string" ? JSON.parse(args) : args;
268
269 if (name === "webSearch") {
270 return executeSearchTool((parsedArgs as Record<string, unknown>) ?? {});
271 }
272
273 if (name === "respond_to_user") {
274 return typeof (parsedArgs as { message?: unknown })?.message === "string"
275 ? ((parsedArgs as { message: string }).message ?? "").trim()
276 : "";
277 }
278
279 const result = executeToolAction({
280 action: name,
281 ...parsedArgs,
282 } as ToolArgs);
283 return result;
284 } catch (error) {
285 console.error("Failed to handle tool call:", name, args, error);
286 return "Unable to perform the task: " + name;
287 }
288 }
289
289 lines TYPESCRIPT