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