# Story 3.2: AI Diagram Generation from Natural Language Status: done ## Story As a user, I want to describe what I need in natural language and have the AI generate a diagram, so that I can go from idea to visual diagram without manual drawing. ## Acceptance Criteria 1. **Given** I have a new empty diagram open, **When** the chat panel loads, **Then** the AI greets me with "What are you designing today?" and the canvas shows the dot grid background (no blank canvas paralysis) 2. **Given** I type a description like "Customer checkout flow with payment, address confirmation, and error handling", **When** the AI processes my message, **Then** the AI determines the best diagram type (BPMN for this example), generates a JSON graph patch with appropriate nodes and edges, and the diagram materializes on the canvas with nodes animating into position via ELK.js. Full mutation applied and rendered in < 3 seconds (NFR4) 3. **Given** no elements are selected on the canvas (FR3), **When** I send a message to the AI, **Then** the AI operates in whole-diagram scope and can create new diagrams, restructure existing ones, or analyze the entire graph 4. **Given** the AI generates a diagram, **When** the graph data is created, **Then** nodes use the correct diagram-type-specific node types (e.g., BPMN gateways, E-R entities), edges have proper routing metadata, and the graph data is saved to the database 5. **Given** the AI is generating a diagram, **When** streaming is in progress, **Then** the chat shows the AI reasoning/response streaming, and once complete, the diagram renders on canvas 6. **Given** the AI service fails or times out, **When** an error occurs during generation, **Then** the user sees a friendly error message, the chat remains functional, and no partial/corrupt graph data is applied ## Tasks / Subtasks - [x] Task 1: Extend system prompt for diagram generation (AC: #2, #3, #4) - [x] 1.1 Update `buildCopilotSystemPrompt()` in `packages/ai/src/modules/copilot/system-prompt.ts` to include mutation instructions - [x] 1.2 Add JSON output format specification matching `GraphData` interface (nodes, edges, meta) - [x] 1.3 Add diagram-type-specific schemas and best practices for each of 6 types (BPMN, E-R, orgchart, architecture, sequence, flowchart) - [x] 1.4 Add type inference instructions so AI determines diagram type from natural language - [x] 1.5 Add few-shot examples for each diagram type showing expected JSON output - [x] 1.6 Update system prompt tests in `system-prompt.test.ts` (19 tests) - [x] Task 2: Create mutation schema and types (AC: #4, #6) - [x] 2.1 Create `packages/ai/src/modules/copilot/mutation-schema.ts` with Zod schema for AI-generated `GraphData` patches - [x] 2.2 Schema must validate: `nodes[]` (id, type, label required), `edges[]` (id, from, to required), `meta` (diagramType required) - [x] 2.3 Add type-specific node type validation (e.g., BPMN nodes must use `bpmn:*` prefix types) - [x] 2.4 Create `mutation-schema.test.ts` with valid/invalid patch tests (30 tests) - [x] 2.5 Export types from `packages/ai/src/modules/copilot/types.ts` - [x] Task 3: Implement AI diagram generation handler (AC: #2, #5, #6) - [x] 3.1 Added `generateDiagramTool` using AI SDK `tool()` with `inputSchema: graphPatchSchema` and server-side validation - [x] 3.2 `graphContext` field already existed in `copilotMessageSchema` (from Story 3.1 M3 fix) - [x] 3.3 Tool `execute` function validates node/edge types and referential integrity before returning - [x] 3.4 Streaming handled via `createUIMessageStream` — text streams first, tool results emit as structured parts - [x] 3.5 `stopWhen: stepCountIs(2)` prevents infinite loops; validation errors returned as `{ success: false, errors }` - [x] Task 4: Implement graph patch application on canvas (AC: #2, #4) - [x] 4.1 Create `apps/web/src/modules/copilot/hooks/useGraphMutation.ts` hook - [x] 4.2 Tool invocation detection via `isGenerateDiagramTool` type guard in CopilotPanel - [x] 4.3 Patch applied to Zustand store via `setNodes`/`setEdges` after `graphToFlow()` conversion - [x] 4.4 Layout triggered via `requestLayout()` → `layoutRequestId` counter → `useAutoLayout` watcher - [x] 4.5 Animation uses existing CSS transition on `.react-flow__node.layouting` class from Stories 2.x - [x] Task 5: Wire CopilotPanel to generation flow (AC: #1, #2, #3, #5) - [x] 5.1 `graphContext` serialized in `prepareSendMessagesRequest` via `useGraphStore.getState()` + `flowToGraph()` - [x] 5.2 Tool invocations detected via `part.type === "tool-generateDiagram"` (AI SDK v4 pattern) - [x] 5.3 `useGraphMutation.applyGraphPatch()` called on `output-available` state, tracked by `appliedToolCallIds` ref - [x] 5.4 "Generating diagram..." indicator shown during `input-streaming`/`input-available` states - [x] 5.5 Empty state shows "What are you designing today?" greeting - [x] Task 6: Implement diagram type inference (AC: #2) - [x] 6.1 Type inference rules added to system prompt with keyword → diagram type mapping - [x] 6.2 System prompt receives `diagramType` param — AI uses existing type for consistency - [x] 6.3 AI sets `meta.diagramType` in the patch for empty diagrams - [x] 6.4 Extended `updateDiagramBodySchema` to accept `graphData`; persistence via fire-and-forget PATCH in `useGraphMutation` - [x] Task 7: Write tests (AC: all) - [x] 7.1 Mutation schema validation tests: 30 tests covering all diagram types, node/edge type validation, referential integrity - [x] 7.2 System prompt tests: 19 tests covering generation instructions, type-specific schemas, few-shot examples, type inference rules - [x] 7.3 Graph patch application: tested indirectly via graph-converter (39 tests) and graph store (25 tests) — hook is glue code - [x] 7.4 Type inference prompt tests: covered in system-prompt tests (type inference rules, keyword presence) - [x] 7.5 Component rendering tests deferred to E2E (per project standards from Story 3.1) ## Dev Notes ### Architecture Compliance - **AI mutations through CRDT**: Per Winston architecture decision, all graph mutations flow through the Zustand store (which syncs to Liveblocks CRDT in future Epic 4). Do NOT create side-channel API calls that directly modify the graph. - **ELK.js in Web Worker**: Layout runs in Web Worker. Use the existing layout trigger mechanism from Stories 2.x. Do NOT run ELK on the main thread. - **200-node soft cap**: AI should not generate graphs with more than ~200 nodes per request. Add instruction in system prompt. - **Unified graph model**: All diagram types use the same `GraphData` interface with type discriminators. The AI output MUST match this exact interface. ### Critical Implementation Patterns (from Story 3.1) **Transport pattern for sending graph context:** ```typescript const transport = useMemo( () => new DefaultChatTransport({ api: api.ai.copilot.$url().toString(), prepareSendMessagesRequest: ({ messages, id }) => { const lastMessage = messages.at(-1); return { body: { ...lastMessage, chatId: id, diagramId, diagramType, graphContext: JSON.stringify(currentGraphData), // NEW: serialize graph state }, }; }, }), [diagramId, diagramType, currentGraphData], ); ``` **Server-side streaming pattern:** ```typescript // In copilot router handler: const result = streamText({ model: registry.languageModel(modelId), system: getDiagramSystemPrompt(diagramType, { mode: 'generate' }), messages: convertToModelMessages(messages), // ... }); return result.toUIMessageStreamResponse(); ``` **`graphContext` schema field already exists** in `copilotMessageSchema` (added during Story 3.1 code review fix M3). Use this to pass current graph state to the AI. ### Unified Graph Model (Source of Truth) Location: `apps/web/src/modules/diagram/types/graph.ts` ```typescript type DiagramType = "bpmn" | "er" | "orgchart" | "architecture" | "sequence" | "flowchart"; interface DiagramNode { id: string; // Required type: string; // Required - diagram-type-specific (e.g., "activity", "gateway", "entity") tag?: string; // Optional subtype label: string; // Required - display text icon?: string; color?: string; w?: number; // Width override position?: { x: number; y: number }; manuallyPositioned?: boolean; lane?: string; // BPMN lane assignment group?: string; // Group containment columns?: Column[]; // E-R entity columns lifeline?: boolean; // Sequence diagram lifeline parentId?: string; // Nesting / subprocess containment } interface DiagramEdge { id: string; // Required from: string; // Required - source node ID to: string; // Required - target node ID label?: string; color?: string; type?: string; // Edge type (e.g., "association", "dependency") cardinality?: string; // E-R cardinality (e.g., "1:N") } interface GraphData { meta?: DiagramMeta; // { version, title, description, diagramType, layoutDirection, edgeRouting } nodes: DiagramNode[]; edges: DiagramEdge[]; pools?: Array<{ id, label, lanes: Array<{ id, label }> }>; // BPMN pools/lanes groups?: Array<{ id, label, color? }>; // Grouping containers } ``` ### Diagram Type Node Types (from codebase) Each diagram type has specific node/edge types defined in `apps/web/src/modules/diagram/types//constants.ts`: | Diagram Type | Node Types | Edge Types | |---|---|---| | **BPMN** | activity, gateway, timerEvent, dataObject, pool, subprocess, group | messageFlow, flow | | **E-R** | entity (with columns[]) | relationship (with cardinality) | | **Orgchart** | person | hierarchyEdge | | **Architecture** | service, database, queue, loadBalancer, external | connection | | **Sequence** | actor (with lifeline) | message | | **Flowchart** | process, decision, terminal, io, subprocess | flow | The AI system prompt MUST include these type mappings so the AI generates valid node types. ### File Structure **Files to CREATE:** ``` packages/ai/src/modules/copilot/ ├── mutation-schema.ts # Zod schema for AI-generated GraphData patches ├── mutation-schema.test.ts # Mutation schema validation tests apps/web/src/modules/copilot/hooks/ └── useGraphMutation.ts # Hook to apply AI patches to canvas ``` **Files to MODIFY:** ``` packages/ai/src/modules/copilot/ ├── system-prompt.ts # Add generation mode instructions, type schemas, JSON format ├── system-prompt.test.ts # Add generation prompt tests ├── schema.ts # Extend copilotMessageSchema if needed ├── types.ts # Add mutation-related types packages/api/src/modules/ai/copilot/ └── router.ts # Extend handler for generation mode apps/web/src/modules/copilot/components/ └── CopilotPanel.tsx # Wire graphContext, detect patches, apply to canvas ``` **Files to REFERENCE (read-only):** ``` apps/web/src/modules/diagram/types/graph.ts # GraphData interface (source of truth) apps/web/src/modules/diagram/types/*/constants.ts # Node/edge type definitions per diagram type apps/web/src/modules/diagram/components/editor/*.tsx # DiagramEditor, RightPanel (wiring context) apps/web/src/modules/diagram/hooks/useLayout.ts # ELK.js layout trigger (if exists) ``` ### Project Structure Notes - Copilot AI logic lives in `packages/ai/src/modules/copilot/` — do NOT create a separate module - Copilot API routes in `packages/api/src/modules/ai/copilot/` — extend existing router - Copilot UI in `apps/web/src/modules/copilot/` — add hooks subdirectory for new hooks - Feature code in `~/modules//` — do NOT co-locate in route directories - Hooks go in feature module, not in `apps/web/src/hooks/` ### Anti-Patterns to AVOID - **Do NOT create a new API endpoint** for generation. Extend the existing copilot POST endpoint. The AI response should include graph patches as structured parts alongside chat text. - **Do NOT use `require()` or CommonJS** — all packages are ESM-only - **Do NOT inline `.parse()` in Hono handlers** — use `validate()` middleware - **Do NOT put business logic in API routers** — handlers call domain package functions - **Do NOT use `uuid()` column type** — always `text().primaryKey().$defaultFn(generateId)` - **Do NOT create a separate "generation" mode** that bypasses chat. Generation IS chat — the AI responds to messages with both text explanations AND graph patches. - **Do NOT modify graph data outside Zustand store** — mutations flow through the store - **Do NOT run ELK.js on main thread** — use existing Web Worker - **Do NOT generate more than 200 nodes** per AI request ### Performance Requirements - First token < 1 second (NFR3) — already achieved in Story 3.1 - Full mutation applied and rendered in < 3 seconds (NFR4) - ELK.js layout < 500ms (already benchmarked in Story 2.2) - Zero-to-diagram < 30 seconds (UX spec: greeting + description + generation) - Rate limit: 30 requests/min per user on AI endpoints ### Security Requirements - `enforceAuth` middleware on all endpoints (already in place) - `deductCredits` middleware before AI generation calls (already in place) - Validate AI output against mutation schema before applying to graph (prevent injection of malformed data) - Do NOT expose raw LLM output to graph store — always validate through Zod schema first ### Testing Standards - Test runner: Vitest with explicit imports (`import { describe, it, expect } from 'vitest'`) - Test location: co-located with source files (e.g., `mutation-schema.test.ts` next to `mutation-schema.ts`) - Factory pattern for test data (e.g., `createTestGraphPatch(overrides)`) - Component rendering tests deferred to E2E per project standards - Workspace command: `pnpm --filter @turbostarter/ai test` and `pnpm --filter @turbostarter/web test` - Expected new tests: ~15-20 (mutation schema validation + generation prompt tests + graph patch application) ### Previous Story Intelligence (Story 3.1) **What was built:** - CopilotPanel UI with streaming, auto-scroll, typing indicator, stop button, markdown rendering - Copilot API route (POST for chat, GET for history) with SSE streaming - System prompt generator (`getDiagramSystemPrompt`) — needs extension for generation mode - Chat persistence via existing `chat.chat`, `chat.message`, `chat.part` tables with `diagramId` column - `graphContext` field in copilotMessageSchema (ready for graph state) - `DiagramType` sourced from DB `diagramTypeEnum` (single source of truth) **Key learnings:** - Use `prepareSendMessagesRequest` in `DefaultChatTransport` for custom body params (NOT `body` option on `useChat`) - DB `text()` columns need `"text" as const` casting for literal types in `UIMessagePart` - Wrap `DefaultChatTransport` in `useMemo` to prevent recreation on every render - AI SDK uses `message.parts: Part[]` (NOT `message.content`) for structured responses - `streamText()` → `convertToModelMessages()` → `result.toUIMessageStreamResponse()` for server-side streaming **Files created in 3.1 (do NOT recreate):** - `packages/ai/src/modules/copilot/types.ts` — extend with mutation types - `packages/ai/src/modules/copilot/schema.ts` — already has `graphContext` field - `packages/ai/src/modules/copilot/system-prompt.ts` — extend for generation - `packages/ai/src/modules/copilot/api.ts` — `getCopilotHistory` query - `packages/api/src/modules/ai/copilot/router.ts` — extend handler - `apps/web/src/modules/copilot/components/CopilotPanel.tsx` — extend for patches **No new dependencies needed** — everything required is already in the workspace (AI SDK, Zod, React Query, Motion, etc.) ### References - [Source: _bmad-output/planning-artifacts/epics.md#Story 3.2] — Acceptance criteria and technical notes - [Source: _bmad-output/planning-artifacts/architecture.md] — AI mutation architecture, CRDT decisions, ELK.js Web Worker - [Source: _bmad-output/planning-artifacts/ux-design-specification.md] — Chat-first UX, conversational design loop, performance requirements - [Source: _bmad-output/project-context.md] — Framework rules, coding standards, anti-patterns - [Source: apps/web/src/modules/diagram/types/graph.ts] — Unified graph data model (GraphData, DiagramNode, DiagramEdge) - [Source: _bmad-output/implementation-artifacts/3-1-chat-panel-ui-with-streaming-ai-responses.md] — Previous story learnings and established patterns ## Dev Agent Record ### Agent Model Used Claude Opus 4.6 ### Debug Log References - AI SDK v4 uses `tool-${toolName}` type pattern (e.g., `tool-generateDiagram`), not `tool-invocation` with `toolName` property - AI SDK v4 tool part states: `input-streaming`, `input-available`, `output-available`, `output-error` — not `partial-call`/`call`/`result` - AI SDK v4 uses `part.output` not `part.result` for tool invocation output - `z.record(z.unknown())` fails in this Zod version — must use `z.record(z.string(), z.unknown())` - Cross-component layout triggering: `CopilotPanel` is outside `ReactFlowProvider`, solved via `layoutRequestId` counter in Zustand store ### Completion Notes List - 49 new tests added (19 system-prompt + 30 mutation-schema), exceeding 15-20 target - All 261 tests pass (75 AI + 186 web) - TypeScript compiles clean (pre-existing errors in bpmn-layout.ts/bpmn constants.ts are unrelated) - `graphContext` serialized at send-time via `useGraphStore.getState()` in transport callback — avoids transport re-creation - Graph persistence is fire-and-forget PATCH call after store update — will be replaced by Liveblocks CRDT in Epic 4 - `stopWhen: stepCountIs(2)` limits AI to one tool call + one follow-up response ### Senior Developer Review (AI) **Reviewer:** Mou on 2026-02-28 **Issues Found:** 2 High, 3 Medium, 3 Low — **All Fixed** **Fixes Applied:** - **H1** (FIXED): `diagramType` in `metaSchema` changed from `z.string()` to `z.enum(VALID_DIAGRAM_TYPES)` — prevents unknown diagram types from bypassing node/edge type validation - **H2** (FIXED): `toChatMessage()` now preserves tool invocation parts (type `tool-*`) instead of converting all to empty text — AI retains diagram generation context across reloads - **M1** (FIXED): Fire-and-forget PATCH in `useGraphMutation` now has `.then()/.catch()` error handling with `toast.error()` — prevents silent data loss - **M2** (FIXED): Added `validateUniqueIds()` function detecting duplicate node/edge IDs — prevents React Flow silent node dropping - **M3** (FIXED): `graphData` in `updateDiagramBodySchema` now validates structural shape (meta, nodes[], edges[], pools?, groups?) instead of accepting any record - **L1** (FIXED): Fixed missing blank line before `## Example` in system prompt template when typeSpecificSections is empty - **L2** (FIXED): `meta.title` now requires `z.string().min(1)` — prevents untitled diagrams - **L3** (FIXED): Added `description?` to system prompt GraphData format spec **Tests added:** 8 new tests (validateUniqueIds: 4, validateGraphPatch: 4 for diagramType enum, empty title, duplicate node/edge IDs) **Total tests after review:** 269 (83 AI + 186 web) — all passing ### File List **Created:** - `packages/ai/src/modules/copilot/mutation-schema.ts` — Zod schema + validation for AI-generated graph patches - `packages/ai/src/modules/copilot/mutation-schema.test.ts` — 38 tests for mutation schema validation (30 original + 8 review fixes) - `apps/web/src/modules/copilot/hooks/useGraphMutation.ts` — Hook to apply AI patches to Zustand store + persist to DB **Modified:** - `packages/ai/src/modules/copilot/system-prompt.ts` — Full rewrite: generation instructions, node/edge type references, type inference rules, few-shot examples per diagram type, 200-node cap, graphContext section - `packages/ai/src/modules/copilot/system-prompt.test.ts` — Rewritten: 19 tests (up from 6), covers generation mode, type-specific schemas, type inference - `packages/ai/src/modules/copilot/api.ts` — Added `generateDiagramTool` with `graphPatchSchema`, validation in `execute`, `tools` + `stopWhen` in `streamText`, `graphContext` passed to system prompt. [Review: preserved tool parts in `toChatMessage`, added `validateUniqueIds` to tool execute] - `packages/ai/src/modules/copilot/types.ts` — Re-exports `GraphPatch`, `graphPatchSchema`, `validateGraphPatch`, `validateUniqueIds` - `packages/api/src/modules/diagram/router.ts` — Extended `updateDiagramBodySchema` with structurally validated `graphData` field - `apps/web/src/modules/diagram/stores/useGraphStore.ts` — Added `layoutRequestId` counter + `requestLayout()` action - `apps/web/src/modules/diagram/hooks/useAutoLayout.ts` — Added `layoutRequestId` watcher to trigger layout from outside ReactFlowProvider - `apps/web/src/modules/copilot/components/CopilotPanel.tsx` — Rewired: graphContext serialization, tool invocation detection, graph patch application, "Generating diagram..." indicator, "Diagram updated" confirmation, updated greeting/placeholder