| 1 | import { tool } from "@langchain/core/tools"; |
| 2 | import * as z from "zod"; |
| 3 | |
| 4 | import { LAYOUT_REFERENCE } from "@/lib/presentation/layout-catalog"; |
| 5 | import { presentationAiThemePropertiesSchema } from "@/lib/presentation/theme-schema"; |
| 6 | import { themes } from "@/lib/presentation/themes"; |
| 7 | import { search_tool } from "../search"; |
| 8 | |
| 9 | const COMPONENT_INSTRUCTIONS = `Component instructions: |
| 10 | - Match component geometry to SECTION layout: vertical root images need horizontal/wide components, and left/right root images need vertical or compact components. |
| 11 | - Do not pair CYCLE with layout="vertical". |
| 12 | - Use compact text in dense visual components. SNAKE, CIRCULAR-GRID, CONNECTED-CIRCLES, and SLOPE items need very short labels. |
| 13 | - SLOPE items must use <H4> only and must not include <P>. |
| 14 | - Use <TITLE> only for the first slide, a newly created title slide, or an introduction slide. |
| 15 | - For most first/title slides, include <TITLE>, <CONTRIBUTOR />, and a supporting visual image. The contributor block self-populates with the creator name; omit it only when a stronger creative concept needs the space. |
| 16 | - Treat <LABEL>, <BLOCKQUOTE>, <QUOTE>, <CALLOUT>, and <CODE> as normal content blocks that can be used anywhere headings and paragraphs can be used, including inside COLUMNS. But don't overuse them. |
| 17 | - Use COLUMNS only for balanced lanes. Every column item must have parallel content, similar text length, and the same heading level; do not mix an H1-style item with H3/H4-style items in sibling columns. |
| 18 | - Keep columns visually balanced even when they include images, charts, infographics, or nested supported content.`; |
| 19 | |
| 20 | // Schema for scope specification |
| 21 | const ScopeSchema = z |
| 22 | .enum(["all"]) |
| 23 | .optional() |
| 24 | .describe( |
| 25 | "Scope of the action: 'all' for all slides. Defaults to 'all' if not specified. This property and slideIds property are mutually exclusive. If you provide both, the slideIds property will be ignored.", |
| 26 | ); |
| 27 | |
| 28 | const slideIdsSchema = z |
| 29 | .array(z.string()) |
| 30 | .optional() |
| 31 | .describe( |
| 32 | "Specific slide ids to apply the action to. If provided, overrides scope. This property and scope property are mutually exclusive. If you provide both, this property will be ignored. So be very careful.", |
| 33 | ); |
| 34 | |
| 35 | const builtInThemeSchema = z.enum( |
| 36 | Object.keys(themes) as [keyof typeof themes, ...(keyof typeof themes)[]], |
| 37 | ); |
| 38 | |
| 39 | const edit_slide_properties = tool( |
| 40 | async (props) => { |
| 41 | const { slideIds: _slideIds, scope: _scope, ...rest } = props; |
| 42 | return `Updated ${Object.keys(rest).join(", ")} successfully to ${Object.values(rest).join(", ")}`; |
| 43 | }, |
| 44 | { |
| 45 | name: "edit_slide_properties", |
| 46 | description: "You can use this tool to edit the properties of a slide", |
| 47 | schema: z.object({ |
| 48 | scope: ScopeSchema, |
| 49 | slideIds: slideIdsSchema, |
| 50 | bgColor: z |
| 51 | .string() |
| 52 | .describe( |
| 53 | "The background color of the slide, use 'reset' to reset the background color", |
| 54 | ) |
| 55 | .optional(), |
| 56 | alignment: z |
| 57 | .enum(["start", "center", "end", "reset"]) |
| 58 | .describe("The content alignment of the slide") |
| 59 | .optional(), |
| 60 | layoutType: z |
| 61 | .enum(["left", "right", "vertical", "background", "reset"]) |
| 62 | .describe( |
| 63 | "Determines where the accent / root image appears in the slide, left means the image is on the left, right means the image is on the right, vertical means the image is on the top, background means the image is the background of the slide", |
| 64 | ) |
| 65 | .optional(), |
| 66 | width: z |
| 67 | .enum(["S", "M", "L", "reset"]) |
| 68 | .describe("The width of the slide") |
| 69 | .optional(), |
| 70 | }), |
| 71 | }, |
| 72 | ); |
| 73 | |
| 74 | const replace_image = tool( |
| 75 | async (props) => { |
| 76 | const { slideIds: _slideIds, scope: _scope, ...rest } = props; |
| 77 | if (rest.imageUrl) { |
| 78 | return `Image url replaced successfully`; |
| 79 | } else if (rest.imagePrompt) { |
| 80 | return `Image successfully generated from the given prompt`; |
| 81 | } |
| 82 | return `No image url or image prompt provided`; |
| 83 | }, |
| 84 | { |
| 85 | name: "replace_image", |
| 86 | description: |
| 87 | "You can use this tool to replace the root image of a slide. If the user also asked for slide text, layout, or content changes, call create_slide or regenerate_slide first, wait for that content tool to complete, and only then call replace_image.", |
| 88 | schema: z.object({ |
| 89 | slideIds: slideIdsSchema, |
| 90 | scope: ScopeSchema, |
| 91 | imageUrl: z |
| 92 | .string() |
| 93 | .describe("The URL of the image to replace") |
| 94 | .optional(), |
| 95 | imagePrompt: z |
| 96 | .string() |
| 97 | .describe( |
| 98 | "Image request for the replacement. Use a detailed descriptive prompt only when the selected image source is AI generation. For Unsplash, Pixabay, Google, web, or stock image search, use a short English keyword query with 2-5 concrete words, such as 'team collaboration' or 'solar panels roof'.", |
| 99 | ) |
| 100 | .optional(), |
| 101 | imageSource: z |
| 102 | .enum(["ai", "stock", "gif"]) |
| 103 | .describe( |
| 104 | "How the imagePrompt should be resolved. Use 'stock' for Unsplash/Pixabay/Google/web image search, 'gif' for animated GIFs, and 'ai' for detailed generated-image prompts.", |
| 105 | ) |
| 106 | .optional(), |
| 107 | stockImageProvider: z |
| 108 | .enum(["unsplash", "pixabay", "google"]) |
| 109 | .describe("Preferred stock provider when imageSource is 'stock'.") |
| 110 | .optional(), |
| 111 | }), |
| 112 | }, |
| 113 | ); |
| 114 | |
| 115 | const change_theme = tool( |
| 116 | async (props) => { |
| 117 | const { theme } = props; |
| 118 | return `Theme changed successfully to ${theme}`; |
| 119 | }, |
| 120 | { |
| 121 | name: "change_theme", |
| 122 | description: |
| 123 | "Apply an existing built-in presentation theme. Use create_custom_theme when the user asks for custom fonts, custom colors, brand styling, or a new theme.", |
| 124 | schema: z.object({ |
| 125 | theme: builtInThemeSchema.describe("The built-in theme id to apply"), |
| 126 | }), |
| 127 | }, |
| 128 | ); |
| 129 | |
| 130 | const create_custom_theme = tool( |
| 131 | async (props) => { |
| 132 | return `Custom theme "${props.themeData.name ?? "Custom theme"}" created and applied successfully`; |
| 133 | }, |
| 134 | { |
| 135 | name: "create_custom_theme", |
| 136 | description: |
| 137 | "Create and apply a custom presentation theme. Use this for custom visual identity, brand styling, font changes, palettes, or backgrounds. Only provide colors, fonts, and background values that are relevant to the request; omitted values are kept from the current theme.", |
| 138 | schema: z.object({ |
| 139 | isPublic: z |
| 140 | .boolean() |
| 141 | .optional() |
| 142 | .default(false) |
| 143 | .describe("Whether the new custom theme should be public"), |
| 144 | themeData: presentationAiThemePropertiesSchema.describe( |
| 145 | "Partial custom theme data. Only include colors, fonts, and background values. Use real, well-known font family names that fit the brand and requirement. Do not invent font names. smartLayout is the fill color for SVG layout elements such as pyramids, pie charts, staircases, cycles, timelines, and diagrams; choose it as a close companion or deliberate variant of primary, not as the cardBackground. cardBackground is the readable text container surface. Do not include animation, transitions, shadows, border radius, or masks.", |
| 146 | ), |
| 147 | }), |
| 148 | }, |
| 149 | ); |
| 150 | |
| 151 | const update_custom_theme = tool( |
| 152 | async (props) => { |
| 153 | return `Custom theme "${props.themeData.name ?? "Custom theme"}" updated and applied successfully`; |
| 154 | }, |
| 155 | { |
| 156 | name: "update_custom_theme", |
| 157 | description: |
| 158 | "Update the currently selected custom presentation theme and apply it. If the current theme is built-in, the app will create a new custom theme from this data instead. Only provide colors, fonts, and background values that should change; omitted values are kept from the current theme.", |
| 159 | schema: z.object({ |
| 160 | isPublic: z |
| 161 | .boolean() |
| 162 | .optional() |
| 163 | .default(false) |
| 164 | .describe("Whether the custom theme should be public"), |
| 165 | themeData: presentationAiThemePropertiesSchema.describe( |
| 166 | "Partial replacement theme data. Only include colors, fonts, and background values. Use real, well-known font family names that fit the brand and requirement. Do not invent font names. smartLayout is the fill color for SVG layout elements such as pyramids, pie charts, staircases, cycles, timelines, and diagrams; choose it as a close companion or deliberate variant of primary, not as the cardBackground. cardBackground is the readable text container surface. Do not include animation, transitions, shadows, border radius, or masks.", |
| 167 | ), |
| 168 | }), |
| 169 | }, |
| 170 | ); |
| 171 | |
| 172 | const REGENERATE_SLIDE_DESCRIPTION = `You are a presentation XML expert. Regenerate one or more existing slides from the user's request. |
| 173 | |
| 174 | Your task is to return exactly two arrays: \`slideIds\` and \`slides\`, with the same length and same order so that \`slides[i]\` replaces \`slideIds[i]\`. |
| 175 | |
| 176 | Use this tool for slide content, layout, text, chart, infographic, and structure changes. If the user also wants a new root image, regenerate the XML content here first; replace/generate the root image afterward with replace_image. |
| 177 | |
| 178 | Return only valid XML strings in \`slides\`. Each item must be one <SECTION>...</SECTION> block, not a full <PRESENTATION>. Use only supported tags and attributes. Put headings, body, and layout content before any direct child root <IMG ... />. If a root image is included, it must be the final direct child of <SECTION>. Keep <IMG /> tags self-closing. |
| 179 | |
| 180 | Preserve structure when the request is text-only. Keep the same SECTION layout, component type, item count when reasonable, root image placement, and existing image URLs unless the user asks to change them. When the user asks for a structural change, choose the component that fits the content shape: list for grouped points, sequence for process or progression, comparison for trade-offs or states, relationship for connected concepts, data for evidence, infographic for custom diagrams, and columns for balanced mixed-content lanes. |
| 181 | |
| 182 | ${LAYOUT_REFERENCE} |
| 183 | |
| 184 | ${COMPONENT_INSTRUCTIONS} |
| 185 | |
| 186 | Use images deliberately. A direct child <IMG /> is the root slide image and must stay last. Use short English keyword queries for stock, web, Unsplash, Pixabay, Google, or GIF search. Use detailed visual prompts for AI image generation. Keep existing image urls exactly when the user did not request image regeneration. |
| 187 | |
| 188 | Use icon attributes as search hints only. Each icon value must be exactly one broad lowercase English keyword with no spaces, punctuation, hyphens, underscores, or react-icons component names. Good examples: security, analytics, team, growth, upload, idea, automation, calendar, money, network, settings, document, message. Do not default to home unless the content is actually about home. For icon lists, use <ICONS variant="icon"> with DIV icon attributes, or <ICONS variant="image"> with DIV prompt attributes for generated item images. Use orientation="side" for visual beside text and orientation="top" for visual above text. |
| 189 | |
| 190 | Do not choose background layouts or full-slide image backgrounds by default. Only use <SECTION layout="background"> when the user explicitly asks to make an image the slide background, or when preserving an existing/template structure that already uses it. |
| 191 | |
| 192 | Use infographics when a process map, lifecycle, hierarchy, relationship diagram, matrix, framework, funnel, or cause-and-effect flow communicates better than a list, chart, or image. If the user explicitly asks for an infographic, diagram, process map, framework, or similar visual on a slide, include exactly one <INFOGRAPHIC> element on that slide. The infographic text must include only the information needed to generate the diagram: exact labels, entities, values, steps, sequence, relationships, the required visual orientation, and the takeaway. Do not include unrelated slide state. For <SECTION layout="vertical">, request a horizontal/landscape infographic because the infographic will sit in the wide content area. For <SECTION layout="left"> or <SECTION layout="right">, request a vertical/stacked infographic because it must fit beside the side root image. Limit layout-based infographic prompts to 5 or fewer visible items by merging lower-priority details. If INFOGRAPHIC is the main/root component, do not add another layout component; only simple headings or paragraphs may accompany it. |
| 193 | |
| 194 | Mandatory rules: |
| 195 | - Include boolean-style attributes only when enabled; omit false attributes. |
| 196 | - Write compact real slide copy, not placeholders. |
| 197 | - Convert markdown into XML text content; do not include markdown markers inside headings or paragraphs.`; |
| 198 | |
| 199 | const regenerate_slide = tool( |
| 200 | async (props) => { |
| 201 | const { slideIds: _slideIds } = props; |
| 202 | return `Slides regenerated successfully`; |
| 203 | }, |
| 204 | { |
| 205 | name: "regenerate_slide", |
| 206 | description: REGENERATE_SLIDE_DESCRIPTION, |
| 207 | schema: z |
| 208 | .object({ |
| 209 | slideIds: z |
| 210 | .array(z.string()) |
| 211 | .min(1) |
| 212 | .describe( |
| 213 | "Array of slide ids to regenerate. Order must match the `slides` array.", |
| 214 | ), |
| 215 | slides: z |
| 216 | .array(z.string()) |
| 217 | .min(1) |
| 218 | .describe( |
| 219 | "Array of XML <SECTION> strings. Each item is a single slide's content.", |
| 220 | ), |
| 221 | }) |
| 222 | .superRefine((data, ctx) => { |
| 223 | if (data.slideIds.length !== data.slides.length) { |
| 224 | ctx.addIssue({ |
| 225 | code: z.ZodIssueCode.custom, |
| 226 | message: |
| 227 | "`slideIds` and `slides` must have the same length and matching order.", |
| 228 | path: ["slides"], |
| 229 | }); |
| 230 | } |
| 231 | }), |
| 232 | }, |
| 233 | ); |
| 234 | |
| 235 | // Create new slides and insert them after a given slide id (if provided), else append |
| 236 | const create_slide = tool( |
| 237 | async (props) => { |
| 238 | const { afterSlideId: _afterSlideId, slides: _slides } = props as { |
| 239 | afterSlideId?: string; |
| 240 | slides: string[]; |
| 241 | }; |
| 242 | return `Slides created successfully`; |
| 243 | }, |
| 244 | { |
| 245 | name: "create_slide", |
| 246 | description: |
| 247 | "Create one or more slides. Return an array of XML <SECTION> strings and optionally the slide id to insert after. Generate headings/body/layout content first; when adding a direct child root <IMG ... />, place it as the final child of <SECTION> so the content appears before root image generation.", |
| 248 | schema: z.object({ |
| 249 | slides: z |
| 250 | .array(z.string()) |
| 251 | .min(1) |
| 252 | .describe( |
| 253 | 'Array of XML <SECTION> strings. Each item is a single slide\'s content. Supported default layouts are left, right, and vertical. Use layout="background" only when the user explicitly asks for a full-slide image background or when preserving an existing/template structure that already uses it. For the root image only provide the query and not url. Include an <INFOGRAPHIC> XML element inside SECTION when a custom visual explanation improves clarity. The element text must include labels, values, entities, sequence, relationships, takeaway, and required orientation, without unrelated slide state. For layout="vertical", request a horizontal/landscape infographic for the wide content area. For layout="left" or layout="right", request a vertical/stacked infographic for the narrow side-by-side content area. If INFOGRAPHIC is the main slide component, only simple headings or paragraphs may accompany it; do not add another layout component. For layout-based infographic prompts, cap visible items at 5 or fewer by combining lower-priority details.', |
| 254 | ), |
| 255 | afterSlideId: z |
| 256 | .string() |
| 257 | .optional() |
| 258 | .describe( |
| 259 | "Insert new slides immediately after this slide id. If omitted or not found, append to the end.", |
| 260 | ), |
| 261 | }), |
| 262 | }, |
| 263 | ); |
| 264 | |
| 265 | // Delete slides by ids |
| 266 | const delete_slide = tool( |
| 267 | async (props) => { |
| 268 | const { slideIds: _slideIds } = props as { slideIds: string[] }; |
| 269 | return `Slides deleted successfully`; |
| 270 | }, |
| 271 | { |
| 272 | name: "delete_slide", |
| 273 | description: "Delete one or more slides by id.", |
| 274 | schema: z.object({ |
| 275 | slideIds: z |
| 276 | .array(z.string()) |
| 277 | .min(1) |
| 278 | .describe("Array of slide ids to delete."), |
| 279 | }), |
| 280 | }, |
| 281 | ); |
| 282 | // Export all tools as an array |
| 283 | export const presentationTools = [ |
| 284 | edit_slide_properties, |
| 285 | replace_image, |
| 286 | change_theme, |
| 287 | create_custom_theme, |
| 288 | update_custom_theme, |
| 289 | regenerate_slide, |
| 290 | create_slide, |
| 291 | delete_slide, |
| 292 | search_tool, |
| 293 | ] as const; |
| 294 | |
| 295 | export type PresentationTool = |
| 296 | | "edit_slide_properties" |
| 297 | | "replace_image" |
| 298 | | "change_theme" |
| 299 | | "create_custom_theme" |
| 300 | | "update_custom_theme" |
| 301 | | "regenerate_slide" |
| 302 | | "create_slide" |
| 303 | | "delete_slide" |
| 304 | | "exa_search_results_json"; |
| 305 |