返回 presentation-ai
route.ts
根目录 / src / app / api / presentation / edit-diagram / route.ts
1 import "server-only";
2
3 import { toUIMessageStream } from "@ai-sdk/langchain";
4 import { PromptTemplate } 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 { auth } from "@/server/auth";
13
14 const INFOGRAPHIC_MODEL = "google/gemini-3-flash-preview";
15
16 // Organize templates by category for the prompt
17 function organizeTemplates(templateList: string[]): string {
18 const categories: Record<string, string[]> = {
19 wordCloud: [],
20 compare: [],
21 hierarchy: [],
22 list: [],
23 quadrant: [],
24 relation: [],
25 sequence: [],
26 };
27
28 for (const t of templateList) {
29 if (t.startsWith("chart-wordcloud")) categories.wordCloud!.push(t);
30 else if (t.startsWith("compare-")) categories.compare!.push(t);
31 else if (t.startsWith("hierarchy-")) categories.hierarchy!.push(t);
32 else if (t.startsWith("list-")) categories.list!.push(t);
33 else if (t.startsWith("quadrant-")) categories.quadrant!.push(t);
34 else if (t.startsWith("relation-")) categories.relation!.push(t);
35 else if (t.startsWith("sequence-")) categories.sequence!.push(t);
36 }
37
38 let result = "";
39 for (const [category, items] of Object.entries(categories)) {
40 if (items.length > 0) {
41 const title =
42 category === "wordCloud"
43 ? "Word Cloud"
44 : category.charAt(0).toUpperCase() + category.slice(1);
45 result += `\n### ${title} Templates\n`;
46 result += items.map((t) => `- ${t}`).join("\n");
47 result += "\n";
48 }
49 }
50 return result;
51 }
52
53 const SYSTEM_PROMPT = `You are an expert Information Designer and AntV Infographic Syntax Specialist. Your task is to edit an existing AntV Infographic DSL code based on user instructions.
54
55 ## Response Format
56
57 You must output ONLY the complete modified infographic syntax code. Do not provide conversational filler, explanations, or preambles. Do NOT wrap your output in a markdown code block.
58
59 ## The AntV Infographic Syntax
60
61 The syntax is strict, case-sensitive, and indentation-based (2 spaces). It follows this structure:
62
63 \`\`\`
64 infographic <template-id>
65 theme
66 colorBg transparent
67 data
68 title <Main Title>
69 desc <Subtitle or Description>
70 items
71 - label <Item Title>
72 desc <Item Description>
73 value <Optional Numeric Value>
74 icon <Icon ID>
75 \`\`\`
76
77 **IMPORTANT**: The theme block MUST come immediately after the infographic line, BEFORE the data block. Make sure \`colorBg\` is always set to \`transparent\`.
78
79 ## Template Library
80
81 If the user wants to change the template, select the most appropriate template-id.
82 {templateList}
83
84 ## Icon Selection Rules
85
86 You must assign an icon to every item in the items list. Use one of these methods:
87
88 **Option A: Material Design Icons (Recommended)**
89 Use mdi/ prefix. Examples: mdi/rocket-launch, mdi/account-group, mdi/lightbulb, mdi/source-branch
90
91 **Option B: Font Awesome**
92 Use fa/ prefix. Examples: fa/check-circle, fa/users, fa/cog
93
94 **Option C: Semantic Search (Auto-Select)**
95 If unsure of the exact icon ID, use: ref:search:svg:<keyword>
96 Example: ref:search:svg:artificial intelligence
97
98 ## Styling & Theme Options
99
100 Add a stylize property inside the theme block for special effects:
101
102 - **Hand-Drawn/Sketchy**: stylize rough (adds pencil sketch effect)
103 - **Gradient**: stylize linear-gradient or stylize radial-gradient
104 - **Pattern**: stylize pattern (fills with geometric textures)
105
106 ## Syntax Rules
107
108 1. Entry starts with: infographic <template-name>
109 2. Key-value pairs use spaces for separation
110 3. Indentation uses 2 spaces
111 4. Object arrays use - on new lines (e.g., items)
112 5. Simple arrays stay inline (e.g., palette #ff5a5f #1fb6ff #13ce66)
113
114 ## Data Field Mapping (choose ONE main field)
115
116 - compare-* => compares
117 - chart-wordcloud* => items
118 - hierarchy-* => root
119 - list-* => items
120 - quadrant-* => items
121 - relation-* => items
122 - sequence-* => items
123
124 ## Binary / Hierarchy Constraints
125
126 - compare-binary-* and compare-hierarchy-left-right-* require exactly two root nodes; all compare items must live under those two roots.
127 - hierarchy-* uses a single root; do not repeat root.
128 ## Relation Guidance
129
130 - For relation-* templates, model relationships explicitly.
131 - Prefer relations with arrows (A -> B) when the template supports it.
132 - If only items are allowed, express connections via concise item labels and descriptions.
133
134 ## Relations (for relation-* templates)
135
136 For graph templates, use relations to describe connections:
137
138 YAML-style:
139 \`\`\`
140 relations
141 - from Node A
142 to Node B
143 \`\`\`
144
145 Mermaid-style:
146 \`\`\`
147 relations
148 A -> B
149 B -> C
150 A <-> D
151 \`\`\`
152
153 ## Editing Guidelines
154
155 1. Preserve the structure and format of the original syntax
156 2. Only modify what the user explicitly requests
157 3. Keep all existing data unless told to change it
158 4. Maintain proper 2-space indentation
159 5. Ensure the output is valid AntV infographic syntax
160 6. Always keep colorBg as transparent
161 7. When asked to change or expand content, use the user text as inspiration only
162 8. Do NOT paste or quote the user text verbatim in labels or descriptions
163 9. Avoid using more than 3 consecutive words from the user's text
164 10. Rephrase and synthesize: derive core ideas, then express them freshly and concisely
165 11. Keep each item label at 20 characters or fewer
166 12. Keep each item description at 60 characters or fewer
167 13. Expand outward from the seed text: add helpful supporting nodes, contrasts, examples, or implications
168
169 ---
170
171 ## Current Infographic Syntax:
172
173 {currentSyntax}
174
175 ---
176
177 ## User's Edit Request:
178
179 {prompt}
180
181 ---
182
183 Apply the user's requested changes to the infographic and output the complete modified syntax.`;
184
185 export async function POST(req: Request) {
186 let endSpanOnReturn = true;
187 const actionName = "presentation.edit_diagram.post";
188 const span = logger.startSpan(`allweone.api.${actionName}`, {
189 attributes: {
190 "allweone.scope": "api",
191 "allweone.action.type": "api_route",
192 "allweone.action.name": actionName,
193 "http.method": "POST",
194 "http.route": "/api/presentation/edit-diagram",
195 },
196 });
197
198 try {
199 const session = await auth();
200 if (!session) {
201 span.event("allweone.api.request_rejected", {
202 "allweone.validation.error": "unauthorized",
203 });
204 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
205 }
206
207 const { currentSyntax, prompt } = (await req.json()) as {
208 currentSyntax: string;
209 prompt: string;
210 };
211
212 if (!currentSyntax || currentSyntax.trim().length === 0) {
213 span.event("allweone.api.request_rejected", {
214 "allweone.validation.error": "missing_current_syntax",
215 });
216 return NextResponse.json(
217 { error: "No current syntax provided" },
218 { status: 400 },
219 );
220 }
221
222 if (!prompt || prompt.trim().length === 0) {
223 span.event("allweone.api.request_rejected", {
224 "allweone.validation.error": "missing_prompt",
225 });
226 return NextResponse.json(
227 { error: "No edit prompt provided" },
228 { status: 400 },
229 );
230 }
231
232 const templateList = organizeTemplates(templates);
233 const editDiagramChain = RunnableSequence.from([
234 PromptTemplate.fromTemplate(SYSTEM_PROMPT),
235 modelPicker(INFOGRAPHIC_MODEL),
236 ]);
237
238 const stream = await editDiagramChain.stream({
239 currentSyntax,
240 prompt,
241 templateList,
242 });
243 span.event("allweone.api.response_stream_created");
244 endSpanOnReturn = false;
245
246 return createUIMessageStreamResponse({
247 stream: toUIMessageStream(stream),
248 consumeSseStream: ({ stream: sseStream }) => {
249 void consumeStream({
250 stream: sseStream,
251 onError: (error) => {
252 span.error(error);
253 },
254 }).finally(() => {
255 span.end();
256 });
257 },
258 });
259 } catch (error) {
260 span.error(error);
261 return NextResponse.json(
262 { error: "Failed to edit diagram" },
263 { status: 500 },
264 );
265 } finally {
266 if (endSpanOnReturn) {
267 span.end();
268 }
269 }
270 }
271
271 lines TYPESCRIPT