返回 presentation-ai
route.ts
1 import { toUIMessageStream } from "@ai-sdk/langchain";
2 import { ChatPromptTemplate } from "@langchain/core/prompts";
3 import { RunnableSequence } from "@langchain/core/runnables";
4 import { consumeStream, createUIMessageStreamResponse } from "ai";
5 import { NextResponse } from "next/server";
6
7 import { templates } from "@/constants/antv-templates";
8 import { modelPicker } from "@/lib/modelPicker";
9 import { logger } from "@/lib/observability/server/logger";
10 import {
11 buildInfographicLayoutInstruction,
12 filterInfographicTemplatesForOrientation,
13 getInfographicOrientationForSlideLayout,
14 type InfographicOrientation,
15 type InfographicSlideLayout,
16 } from "@/lib/presentation/infographic-layout";
17 import { auth } from "@/server/auth";
18
19 const INFOGRAPHIC_MODEL = "google/gemini-3-flash-preview";
20
21 type TextToDiagramRequest = {
22 prompt: string;
23 slideLayoutType?: InfographicSlideLayout;
24 requestedOrientation?: InfographicOrientation;
25 layoutInstruction?: string;
26 };
27
28 function isTextToDiagramRequest(value: unknown): value is TextToDiagramRequest {
29 if (!value || typeof value !== "object") return false;
30
31 const candidate = value as Partial<TextToDiagramRequest>;
32 return (
33 typeof candidate.prompt === "string" &&
34 (candidate.slideLayoutType === undefined ||
35 typeof candidate.slideLayoutType === "string") &&
36 (candidate.requestedOrientation === undefined ||
37 typeof candidate.requestedOrientation === "string") &&
38 (candidate.layoutInstruction === undefined ||
39 typeof candidate.layoutInstruction === "string")
40 );
41 }
42
43 // Organize templates by category for the prompt
44 function organizeTemplates(templateList: string[]): string {
45 const categories: Record<string, string[]> = {
46 wordCloud: [],
47 compare: [],
48 hierarchy: [],
49 list: [],
50 quadrant: [],
51 relation: [],
52 sequence: [],
53 };
54
55 for (const t of templateList) {
56 if (t.startsWith("chart-wordcloud")) categories.wordCloud!.push(t);
57 else if (t.startsWith("compare-")) categories.compare!.push(t);
58 else if (t.startsWith("hierarchy-")) categories.hierarchy!.push(t);
59 else if (t.startsWith("list-")) categories.list!.push(t);
60 else if (t.startsWith("quadrant-")) categories.quadrant!.push(t);
61 else if (t.startsWith("relation-")) categories.relation!.push(t);
62 else if (t.startsWith("sequence-")) categories.sequence!.push(t);
63 }
64
65 let result = "";
66 for (const [category, items] of Object.entries(categories)) {
67 if (items.length > 0) {
68 const title =
69 category === "wordCloud"
70 ? "Word Cloud"
71 : category.charAt(0).toUpperCase() + category.slice(1);
72 result += `\n### ${title} Templates\n`;
73 result += items.map((t) => `- ${t}`).join("\n");
74 result += "\n";
75 }
76 }
77 return result;
78 }
79
80 const SYSTEM_PROMPT = `You are an expert Information Designer and AntV Infographic Syntax Specialist. Your sole purpose is to translate natural language user requests into valid AntV Infographic DSL (Domain Specific Language) code.
81
82 ## Response Format
83
84 You must output ONLY the infographic syntax code. Do not provide conversational filler, explanations, or preambles. Do NOT wrap your output in a markdown code block.
85
86 ## The AntV Infographic Syntax
87
88 The syntax is strict, case-sensitive, and indentation-based (2 spaces). It follows this structure:
89
90 \`\`\`
91 infographic <template-id>
92 theme
93 colorBg transparent
94 data
95 title <Main Title>
96 desc <Subtitle or Description>
97 items
98 - label <Item Title>
99 desc <Item Description>
100 value <Optional Numeric Value>
101 icon <Icon ID>
102 \`\`\`
103
104 **IMPORTANT**: The theme block MUST come immediately after the infographic line, BEFORE the data block. and make sure \`colorBg\` is always set to \`transparent\`.
105
106 ## Template Library
107
108 Select the most appropriate template-id based on the data structure implied by the user's request.
109 {templateList}
110
111 Do not use chart-* templates except chart-wordcloud and chart-wordcloud-rotate. Standard charts are not part of the infographic conversion flow.
112
113 ## Layout Fit Contract
114
115 {layoutInstruction}
116
117 - The selected template MUST match the required infographic orientation. Treat orientation as a hard requirement.
118 - Horizontal means landscape/wide: left-to-right flow, rows, grids, quadrants, or compact wide diagrams.
119 - Vertical means portrait/stacked: top-to-bottom flow, columns, vertical roadmaps, or compact stacked hierarchy.
120 - Do not choose a template just because it matches the topic; choose one that also fits the required orientation.
121 - Do not mention the slide layout, orientation rule, or fitting instructions in the generated infographic text.
122
123 ## Icon Selection Rules
124
125 You must assign an icon to every item in the items list. Use one of these methods:
126
127 **Option A: Material Design Icons (Recommended)**
128 Use mdi/ prefix. Examples: mdi/rocket-launch, mdi/account-group, mdi/lightbulb, mdi/source-branch
129
130 **Option B: Font Awesome**
131 Use fa/ prefix. Examples: fa/check-circle, fa/users, fa/cog
132
133 **Option C: Semantic Search (Auto-Select)**
134 If unsure of the exact icon ID, use: ref:search:svg:<keyword>
135 Example: ref:search:svg:artificial intelligence
136
137 ## Styling & Theme Options
138
139 Add a stylize property inside the theme block for special effects:
140
141 - **Hand-Drawn/Sketchy**: stylize rough (adds pencil sketch effect)
142 - **Gradient**: stylize linear-gradient or stylize radial-gradient
143 - **Pattern**: stylize pattern (fills with geometric textures)
144
145 ## Syntax Rules
146
147 1. Entry starts with: infographic <template-name>
148 2. Key-value pairs use spaces for separation
149 3. Indentation uses 2 spaces
150 4. Object arrays use - on new lines (e.g., items)
151 5. Simple arrays stay inline (e.g., palette #ff5a5f #1fb6ff #13ce66)
152
153 ## Data Field Mapping (choose ONE main field)
154
155 - compare-* => compares
156 - chart-wordcloud* => items
157 - hierarchy-* => root
158 - list-* => items
159 - quadrant-* => items
160 - relation-* => items
161 - sequence-* => items
162
163 ## Binary / Hierarchy Constraints
164
165 - compare-binary-* and compare-hierarchy-left-right-* require exactly two root nodes; all compare items must live under those two roots.
166 - hierarchy-* uses a single root; do not repeat root.
167
168 ## Item Count Limits
169
170 - Never generate more than 5 visible content items. This is a hard cap across top-level items, direct root children, comparison points, relation nodes, and word-cloud terms.
171 - For list-*, quadrant-*, sequence-*, relation-*, and comparable layout templates, use 3 to 5 top-level items.
172 - For hierarchy-* templates, use 3 to 5 direct child nodes under the single root unless the selected template strictly requires fewer.
173 - For compare-* templates, use exactly two sides and keep the combined comparison points to 4 or 5 visible points total.
174 - If the source text contains more details than the chosen layout can fit, synthesize and merge related ideas into the strongest 5 or fewer items instead of listing everything.
175 - Avoid nested child nodes unless the selected template requires them; when nesting is required, keep the total visible content items at 5 or fewer.
176
177 ## Relation Guidance
178
179 - For relation-* templates, model relationships explicitly.
180 - Prefer relations with arrows (A -> B) when the template supports it.
181 - If only items are allowed, express connections via concise item labels and descriptions.
182
183 ## Relations (for relation-* templates)
184
185 For graph templates, use relations to describe connections:
186
187 YAML-style:
188 \`\`\`
189 relations
190 - from Node A
191 to Node B
192 \`\`\`
193
194 Mermaid-style:
195 \`\`\`
196 relations
197 A -> B
198 B -> C
199 A <-> D
200 \`\`\`
201
202 ## Content Mapping Rules
203
204 - Generate all text in the same language as the user's input
205 - Use the input text only as inspiration, not as direct copy
206 - Do NOT paste or quote the input text verbatim in labels or descriptions
207 - Avoid using more than 3 consecutive words from the source text
208 - Rephrase and synthesize: derive core ideas, then express them freshly and concisely
209 - Keep each item label at 20 characters or fewer
210 - Keep each item description at 60 characters or fewer
211 - Expand outward from the seed text: add helpful supporting nodes, contrasts, examples, or implications
212 - Favor clear, high-level abstractions over literal sentences from the source
213 - Identify a strong title and brief description that reframe the topic
214 - Break down content into logical items that radiate in multiple directions (not a single linear restatement)
215 - Choose the template that best matches the inferred structure (sequence, hierarchy, relation, etc.)
216
217 ## Example
218
219 User Input: "Create a 3-step process: Research, Design, Build"
220
221 Your Output:
222 infographic sequence-steps-simple
223 theme light
224 colorBg transparent
225 data
226 title Development Process
227 desc A streamlined approach to building products
228 items
229 - label Research
230 desc Understand user needs and market
231 icon mdi/magnify
232 - label Design
233 desc Create wireframes and prototypes
234 icon mdi/palette
235 - label Build
236 desc Develop and test the solution
237 icon mdi/hammer-wrench
238
239 The following user message will be a selected excerpt from a presentation or document. Your task is to analyze that content and convert it into a clear, visually appealing diagram using the AntV Infographic syntax. Choose the most appropriate template that best represents the structure and meaning of the content.`;
240
241 const USER_PROMPT = `{prompt}
242
243 Convert the above content into an AntV infographic diagram.`;
244
245 export async function POST(req: Request) {
246 let endSpanOnReturn = true;
247 const actionName = "presentation.text_to_diagram.post";
248 const span = logger.startSpan(`allweone.api.${actionName}`, {
249 attributes: {
250 "allweone.scope": "api",
251 "allweone.action.type": "api_route",
252 "allweone.action.name": actionName,
253 "http.method": "POST",
254 "http.route": "/api/presentation/text-to-diagram",
255 },
256 });
257
258 try {
259 const session = await auth();
260 if (!session) {
261 span.event("allweone.api.request_rejected", {
262 "allweone.validation.error": "unauthorized",
263 });
264 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
265 }
266
267 const body: unknown = await req.json();
268
269 if (!isTextToDiagramRequest(body) || body.prompt.trim().length === 0) {
270 span.event("allweone.api.request_rejected", {
271 "allweone.validation.error": "missing_prompt",
272 });
273 return NextResponse.json(
274 { error: "No text provided for diagram generation" },
275 { status: 400 },
276 );
277 }
278
279 const prompt = body.prompt;
280 const requestedOrientation =
281 body.requestedOrientation ??
282 getInfographicOrientationForSlideLayout(body.slideLayoutType);
283 const layoutInstruction =
284 body.layoutInstruction ??
285 buildInfographicLayoutInstruction(body.slideLayoutType);
286 const templateList = organizeTemplates(
287 filterInfographicTemplatesForOrientation(templates, requestedOrientation),
288 );
289 const diagramChain = RunnableSequence.from([
290 ChatPromptTemplate.fromMessages([
291 ["system", SYSTEM_PROMPT],
292 ["user", USER_PROMPT],
293 ]),
294 modelPicker(INFOGRAPHIC_MODEL),
295 ]);
296
297 const stream = await diagramChain.stream({
298 prompt,
299 templateList,
300 layoutInstruction,
301 });
302 span.event("allweone.api.response_stream_created");
303 endSpanOnReturn = false;
304
305 return createUIMessageStreamResponse({
306 stream: toUIMessageStream(stream),
307 consumeSseStream: ({ stream: sseStream }) => {
308 void consumeStream({
309 stream: sseStream,
310 onError: (error) => {
311 span.error(error);
312 },
313 }).finally(() => {
314 span.end();
315 });
316 },
317 });
318 } catch (error) {
319 span.error(error);
320 return NextResponse.json(
321 { error: "Failed to generate diagram" },
322 { status: 500 },
323 );
324 } finally {
325 if (endSpanOnReturn) {
326 span.end();
327 }
328 }
329 }
330
330 lines TYPESCRIPT