| 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 |