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