返回 presentation-ai
route.ts
1 import { createUIMessageStreamResponse } from "ai";
2 import { assertModelIsConfigured, modelPicker } from "@/lib/modelPicker";
3 import { createLogger } from "@/lib/observability/logger";
4 import { toUIMessageStream } from "@ai-sdk/langchain";
5 import { auth } from "@/server/auth";
6 import { PromptTemplate } from "@langchain/core/prompts";
7 import { RunnableSequence } from "@langchain/core/runnables";
8 import { NextResponse } from "next/server";
9 import { LAYOUT_REFERENCE } from "@/lib/presentation/layout-catalog";
10
11 interface GenerateSlideRequest {
12 prompt: string;
13 currentSlide?: string; // Serialized XML of current slide (optional context)
14 theme?: string;
15 language?: string;
16 slideType?: "standard" | "image";
17 imageStyle?: "3D" | "Sketch" | "Flat";
18 textDensity?: "Minimal" | "Balanced" | "Detailed";
19 }
20
21 const singleSlideTemplate = `You are an expert presentation designer. Create an engaging presentation in XML format.
22 Your output is consumed directly by a slide rendering engine that parses a custom XML schema -- it is NOT rendered as HTML or displayed as raw text. Every tag name, attribute, and structural rule described below is part of a strict schema.
23
24 Your task: Create a SINGLE engaging slide in XML format based on the user's request.
25
26 ---
27
28 # TASK CONTEXT
29
30 ## User Request
31 {PROMPT}
32
33 ## Current Slide Context (if provided)
34 {CURRENT_SLIDE}
35
36 ## Language
37 {LANGUAGE}
38
39 ---
40
41 # XML OUTPUT SCHEMA
42
43 Return ONLY the XML for a single slide. No explanation, no wrapper tags.
44
45 \`\`\`xml
46 <SECTION layout="left|right|vertical|background">
47 <!-- Choose ONE layout component -->
48 <!-- Optional: include an IMG tag with query after the content if relevant. Use layout="background" as a normal full-slide image layout when the image leaves enough contrast for readable foreground content. -->
49 </SECTION>
50 \`\`\`
51
52 ---
53
54 ${LAYOUT_REFERENCE}
55
56 ---
57
58 # IMAGE HANDLING
59
60 Include an image query in most slides:
61 \`\`\`xml
62
63 <IMG query="abstract background, minimalist design" />
64 \`\`\`
65
66 If you include an \`<IMG query="...">\` tag, the query text MUST always be in English for stock or web image-provider compatibility, even if {LANGUAGE} is not English. Keep all other slide copy in {LANGUAGE}; only the image search query should be in English.
67 For standard slides, generate the slide content before the root image: put headings/body/layout components first and place any direct child root \`<IMG ... />\` as the final child of \`<SECTION>\`.
68
69 ---
70
71 # INFOGRAPHIC BLOCKS
72
73 Use an infographic block when the slide needs a custom visual explanation: process map, hierarchy, lifecycle, relationship diagram, matrix, framework, or cause-and-effect flow.
74
75 <SECTION layout="vertical">
76 <H2>Operating Model</H2>
77 <INFOGRAPHIC>Operating model with five connected parts: Inputs = customer data and market signals; Workflows = intake, prioritization, delivery; Governance = decision rights and risk checks; Metrics = cycle time, quality, adoption; Outcomes = faster launches and higher retention.</INFOGRAPHIC>
78 </SECTION>
79
80 Place the \`<INFOGRAPHIC>\` element as slide content inside \`<SECTION>\`. Its text must be fully self-contained: include the exact labels, entities, values, steps, sequence, relationships, and takeaway the infographic should show.
81 For item-level content inside the infographic prompt, use labels of 20 characters or fewer and descriptions of 60 characters or fewer.
82 For layout-based infographic prompts such as pyramids, quadrants, lists, hierarchies, sequences, matrices, and relationship diagrams, include only the strongest 4 to 5 visible items. Synthesize extra detail into those items. Word clouds and chart-style visuals may include more items when useful.
83 When an \`<INFOGRAPHIC>\` is the main/root slide component, do not generate any other layout component on that slide. Only simple \`<H1>\`, \`<H2>\`, \`<H3>\`, or \`<P>\` text may accompany it.
84 The infographic prompt must state the required visual orientation: \`layout="vertical"\` or \`layout="background"\` requires a horizontal/landscape infographic because the content area is wide; \`layout="left"\` or \`layout="right"\` requires a vertical/stacked infographic because the content area is a narrow side column.
85
86 **CRITICAL INFOGRAPHIC RULE**: If the user's request explicitly mentions an "infographic", "diagram", "process map", or similar visual component, you MUST include an \`<INFOGRAPHIC>...</INFOGRAPHIC>\` element.
87
88 ---
89
90 # HARD CONSTRAINTS
91
92 These rules are non-negotiable. Violating any **MUST** rule will break the parser.
93
94 ### MUST (parsing will break)
95 1. Generate exactly ONE \`<SECTION>\` with ONE main layout component.
96 2. Use ONLY layout tags from the AVAILABLE LAYOUTS section -- unlisted tags cause parsing errors.
97 3. Do NOT use CYCLE with a vertical root image layout. Give CYCLE enough horizontal room or omit the root image.
98
99 ### SHOULD (quality)
100 4. Use supported attributes instead of plain defaults when they improve the slide: alignment, bulletType, orientation, sidedness, svgType, boxType, statstype, numbered, showLine, and isFunnel.
101 5. Match nested layout orientation to the root image layout: vertical/background root image pairs with horizontal timelines; left/right root image pairs with vertical timelines.
102 6. Do not force all visuals into the root image. Add nested \`<IMG query="..." />\` inside layout items when item-level imagery improves comprehension.
103 7. For direct child root images, place \`<IMG ... />\` after the slide's content/layout component so the content is produced before the root image.
104 8. Use an \`<INFOGRAPHIC>\` element when it communicates the idea better than another list, chart, or image.
105 9. If the root slide component is \`<INFOGRAPHIC>\`, do not add COLUMNS, BULLETS, ICONS, CYCLE, ARROWS, TIMELINE, PYRAMID, BOXES, STEPS, COMPARE, TABLE, CHART, or any other layout component to the same slide.
106
107 Now generate a single slide based on the user's request.
108 `;
109
110 const singleImageSlideTemplate = `# ROLE
111
112 You are a visual presentation-to-XML compiler. Your output is consumed directly by a slide rendering engine -- it is NOT rendered as HTML. You produce a single full-bleed image slide where ALL text is rendered inside the generated image itself (no separate text overlays).
113
114 ---
115
116 # TASK CONTEXT
117
118 ## User Request
119 {PROMPT}
120
121 ## Current Slide Context (if provided)
122 {CURRENT_SLIDE}
123
124 ## Language
125 {LANGUAGE}
126
127 ---
128
129 # XML OUTPUT SCHEMA
130
131 Return ONLY the XML for a single image slide. No explanation, no wrapper tags.
132
133 \`\`\`xml
134 <SECTION isImageSlide="true">
135 <IMG query="..." />
136 </SECTION>
137 \`\`\`
138
139 Requirements:
140 - The SECTION MUST have \`isImageSlide="true"\`
141 - Output ONLY an \`<IMG query="...">\` tag inside the SECTION -- no H1/H2/H3/P or other elements
142 - The IMG query must be 60-120 words, highly descriptive, and include the exact on-image text in quotes
143
144 ---
145
146 # IMAGE GENERATION GUIDELINES
147
148 ## Visual Style
149 - **Style**: {IMAGE_STYLE}
150 - {IMAGE_STYLE_GUIDANCE}
151
152 ## Text Density
153 - **Density**: {TEXT_DENSITY}
154 - {TEXT_DENSITY_GUIDANCE}
155
156 ## Prompt Construction
157
158 Create a detailed, artistic prompt that:
159 - Describes the visual scene, composition, mood, color palette, and lighting
160 - Includes typography guidance (font style, size, placement, contrast)
161 - Expands the prompt into the final copy for the slide (titles, subtitles, bullets, labels)
162 - Does NOT use placeholders, brackets, or vague references
163
164 ---
165
166 # HARD CONSTRAINTS
167
168 1. Generate exactly ONE \`<SECTION isImageSlide="true">\` containing exactly ONE \`<IMG query="...">\`
169 2. Do NOT include any other tags -- no H1, H2, H3, P, COLUMNS, or any layout component
170 3. The IMG query MUST be 60-120 words
171 4. The IMG query MUST include the exact on-image text in quotes
172
173 Now generate the single image slide.
174 `;
175
176 const model = modelPicker("gpt-4o-mini");
177
178 function getImageStyleGuidance(style?: string): string {
179 switch (style) {
180 case "3D":
181 return "Use a cinematic 3D render with depth, realistic shadows, and depth of field.";
182 case "Sketch":
183 return "Use hand-drawn sketch textures, ink lines, and paper grain.";
184 case "Flat":
185 return "Use flat design with minimal shading and strong color blocks.";
186 default:
187 return "Use a cinematic 3D render with depth, realistic shadows, and depth of field.";
188 }
189 }
190
191 function getTextDensityGuidance(density?: string): string {
192 switch (density) {
193 case "Minimal":
194 return "Text should be minimal: a short title and one short supporting line.";
195 case "Detailed":
196 return "Text should be detailed: title, subtitle, and 4-6 concise bullet lines or labels.";
197 default:
198 return "Text should be balanced: title, subtitle, and 2-3 concise supporting lines.";
199 }
200 }
201
202 export async function POST(req: Request) {
203 const requestId = crypto.randomUUID();
204 const routeLogger = createLogger("api:presentation-generate-slide");
205
206 try {
207 routeLogger.info("Single slide generation request received", { requestId });
208 const session = await auth();
209 if (!session) {
210 routeLogger.warn("Single slide generation request rejected: unauthorized", {
211 requestId,
212 });
213 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
214 }
215
216 const {
217 prompt,
218 currentSlide,
219 language,
220 slideType,
221 imageStyle,
222 textDensity,
223 } = (await req.json()) as GenerateSlideRequest;
224
225 if (!prompt) {
226 routeLogger.warn("Single slide generation request rejected: missing prompt", {
227 requestId,
228 });
229 return NextResponse.json(
230 { error: "Missing required prompt field" },
231 { status: 400 },
232 );
233 }
234 routeLogger.info("Validated single slide generation request", {
235 requestId,
236 slideType: slideType || "standard",
237 language: language || "en-US",
238 imageStyle: imageStyle || "3D",
239 textDensity: textDensity || "Balanced",
240 promptLength: prompt.length,
241 modelProvider: "openai",
242 modelId: "gpt-4o-mini",
243 });
244 try {
245 assertModelIsConfigured("gpt-4o-mini");
246 } catch (error) {
247 routeLogger.error(
248 "Single slide generation request rejected: invalid model configuration",
249 error,
250 {
251 requestId,
252 modelProvider: "openai",
253 modelId: "gpt-4o-mini",
254 },
255 );
256 return NextResponse.json(
257 {
258 error:
259 error instanceof Error
260 ? error.message
261 : "Invalid model configuration",
262 },
263 { status: 400 },
264 );
265 }
266
267 const isImageSlide = slideType === "image";
268 const promptTemplate = PromptTemplate.fromTemplate(
269 isImageSlide ? singleImageSlideTemplate : singleSlideTemplate,
270 );
271 const chain = RunnableSequence.from([promptTemplate, model]);
272
273 const input = isImageSlide
274 ? {
275 PROMPT: prompt,
276 CURRENT_SLIDE: currentSlide || "No current slide context provided.",
277 LANGUAGE: language || "en-US",
278 IMAGE_STYLE: imageStyle || "3D",
279 IMAGE_STYLE_GUIDANCE: getImageStyleGuidance(imageStyle),
280 TEXT_DENSITY: textDensity || "Balanced",
281 TEXT_DENSITY_GUIDANCE: getTextDensityGuidance(textDensity),
282 }
283 : {
284 PROMPT: prompt,
285 CURRENT_SLIDE: currentSlide || "No current slide context provided.",
286 LANGUAGE: language || "en-US",
287 };
288 routeLogger.info("Single slide generation started", {
289 requestId,
290 slideType: isImageSlide ? "image" : "standard",
291 });
292 // @ts-expect-error types are incorrectly inferred
293 const stream = await chain.stream(input);
294
295 routeLogger.info("Single slide generation stream created", {
296 requestId,
297 slideType: isImageSlide ? "image" : "standard",
298 });
299 return createUIMessageStreamResponse({
300 stream: toUIMessageStream(stream),
301 });
302 } catch (error) {
303 routeLogger.error("Single slide generation failed", error, { requestId });
304 return NextResponse.json(
305 { error: "Failed to generate slide" },
306 { status: 500 },
307 );
308 }
309 }
310
310 lines TYPESCRIPT