Add AI copilot chat panel to the diagram editor with streaming responses, chat history persistence, and markdown rendering. Includes copilot API route, diagram-aware system prompt, and schema with 15 passing tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
17 KiB
Story 3.1: Chat Panel UI with Streaming AI Responses
Status: done
Story
As a user, I want a chat panel where I can converse with an AI about my diagram, So that I can describe what I want and see the AI's responses stream in real-time.
Acceptance Criteria
-
Given I am in the diagram editor, When I look at the right panel, Then I see a Chat tab (active by default) with a message input area at the bottom, a scrollable message history above, and a mic button placeholder (disabled, for Epic 5).
-
Given I type a message and press Enter (or click Send), When the message is sent to the AI backend, Then my message appears in the chat history immediately And the AI response streams token-by-token into a new message bubble And first token appears in < 1 second.
-
Given the AI is streaming a response, When I view the chat panel, Then I see a typing indicator while tokens stream And the chat auto-scrolls to show the latest content And the message progressively renders with markdown formatting.
-
Given the AI service is unavailable, When I send a message, Then I see a user-friendly error message within 5 seconds And the chat remains functional for viewing history.
Tasks / Subtasks
-
Task 1: Create diagram copilot API route (AC: 2, 4)
- 1.1 Create
packages/api/src/modules/ai/copilot/router.tswith POST/endpoint - 1.2 Wire
enforceAuth→validate("json", copilotMessageSchema)→ handler - 1.3 Create
copilotMessageSchemain same file or adjacent schema file — extends chat message withdiagramId: z.string(),diagramType: z.string(), optionalgraphContext: z.string() - 1.4 Handler calls
streamText()with diagram-specific system prompt, returnsresult.toUIMessageStreamResponse() - 1.5 Register route in
packages/api/src/modules/ai/router.tsas.route("/copilot", copilotRouter) - 1.6 Create diagram system prompt in
packages/ai/src/modules/copilot/system-prompt.ts— instructs AI about diagram types, graph model, and conversation scope
- 1.1 Create
-
Task 2: Create ChatPanel component (AC: 1, 2, 3)
- 2.1 Create
apps/web/src/modules/chat/components/ChatPanel.tsx— main copilot panel - 2.2 Use AI SDK
useChathook withapipointing to/api/ai/copilotendpoint - 2.3 Pass
body: { diagramId, diagramType }touseChatfor diagram context - 2.4 Message list: scrollable container, auto-scroll on new content via
useRef+scrollIntoView - 2.5 Input area: text input with Enter-to-send, Send button, disabled mic button placeholder
- 2.6 Render user messages and assistant messages with parts-based rendering (
message.parts.map(...))
- 2.1 Create
-
Task 3: Implement streaming UX (AC: 2, 3)
- 3.1 Typing indicator: show animated dots while
status === "submitted"(waiting for first token) - 3.2 Streaming indicator: progressive text rendering while
status === "streaming" - 3.3 Auto-scroll: scroll to bottom on each streaming update, respect user scroll-up (pause auto-scroll if user scrolled up)
- 3.4 Markdown rendering: render assistant text parts with basic markdown (bold, italic, code blocks, lists) — use a lightweight markdown renderer or dangerouslySetInnerHTML with sanitization
- 3.1 Typing indicator: show animated dots while
-
Task 4: Wire ChatPanel into RightPanel (AC: 1)
- 4.1 Replace the placeholder content in
RightPanel.tsxChat tab with<ChatPanel diagramId={...} diagramType={...} /> - 4.2 Pass
diagramIdanddiagramTypeas props from DiagramEditor → RightPanel → ChatPanel - 4.3 Ensure Chat tab is active by default (already implemented in RightPanel)
- 4.1 Replace the placeholder content in
-
Task 5: Error handling (AC: 4)
- 5.1 Use
useChat'sonErrorcallback to show toast via sonner - 5.2 Display inline error message in chat when
erroris set — "Something went wrong. Please try again." - 5.3 Keep message history visible and input functional during error state
- 5.4 Add Stop button visible during streaming (
status === "submitted" || status === "streaming") that callsstop()
- 5.1 Use
-
Task 6: Persist chat per diagram (AC: 1, 2)
- 6.1 Add
diagramIdcolumn to existingchat.chattable (nullable text, indexed) — or create a newcopilot_chattable in acopilotpgSchema - 6.2 Generate and apply Drizzle migration
- 6.3 On ChatPanel mount: query for existing chat by diagramId, load history if exists
- 6.4 On first message: create chat record linked to diagramId, then stream
- 6.5 Register any new table exports in
packages/db/src/schema/index.ts
- 6.1 Add
-
Task 7: Tests (all ACs)
- 7.1 Unit test copilot message schema validation (valid/invalid payloads)
- 7.2 Unit test system prompt generation (correct diagram type context)
- 7.3 Unit test ChatPanel message rendering logic — deferred: React component rendering tests go to E2E per project testing standards
- 7.4 Unit test auto-scroll behavior — deferred: DOM interaction tests go to E2E per project testing standards
- 7.5 Unit test status-based UI states — deferred: React component rendering tests go to E2E per project testing standards
Dev Notes
Existing Infrastructure — DO NOT Reinvent
The TurboStarter template already has a complete generic chat system. Reuse patterns, do NOT duplicate:
| What Exists | Location | How to Reuse |
|---|---|---|
| Chat DB schema (chat, message, part tables) | packages/db/src/schema/chat.ts |
Extend with diagramId column or create parallel copilot schema |
| Generic chat API (CRUD + streaming) | packages/api/src/modules/ai/chat.ts |
Reference patterns; copilot gets its OWN route |
streamChat() with AI SDK |
packages/ai/src/modules/chat/api.ts |
Study createUIMessageStream + streamText + smoothStream patterns |
| Chat composer hook | apps/web/src/modules/chat/composer/hooks/use-composer.tsx |
Study getChatInstance + DefaultChatTransport pattern |
| Assistant message renderer | apps/web/src/modules/chat/thread/message/assistant/index.tsx |
Study parts-based rendering with status-driven streaming UX |
| Model definitions (12 models, 5 providers) | packages/ai/src/modules/chat/constants.ts |
Reuse model registry; copilot picks a default model |
| React Query chat hooks | apps/web/src/modules/chat/lib/api.ts |
Reference patterns for chat CRUD queries |
| Hono API client setup | apps/web/src/lib/api/client.tsx + server.ts |
Use existing type-safe Hono RPC client |
AI SDK Latest API (February 2026)
The AI SDK has evolved from its earlier API. Key differences:
| Old API | Current API | Notes |
|---|---|---|
handleSubmit() |
sendMessage() |
No longer form-bound |
input / handleInputChange |
Manage your own input state | Use useState for input |
isLoading |
status: 'submitted' | 'streaming' | 'ready' | 'error' |
Granular status |
message.content (string) |
message.parts: Part[] |
Parts-based rendering |
append() |
sendMessage({ text }) |
Message submission |
Critical useChat pattern:
const { messages, sendMessage, status, error, stop } = useChat({
api: '/api/ai/copilot', // Custom endpoint
body: { diagramId, diagramType }, // Extra body params sent with every request
onError: (err) => toast.error('Failed to get AI response'),
onFinish: () => { /* invalidate queries if needed */ },
});
Critical streamText server pattern:
import { streamText, convertToModelMessages } from 'ai';
const result = streamText({
model: registry.languageModel('openai:gpt-4o'),
system: diagramSystemPrompt,
messages: convertToModelMessages(messages), // Convert UIMessage[] to ModelMessage[]
});
return result.toUIMessageStreamResponse(); // SSE format for useChat
Diagram System Prompt
Create a diagram-aware system prompt that tells the AI:
- It is a diagram design copilot for domaingraph
- The current diagram type (BPMN, E-R, Org Chart, Architecture, Sequence, Flowchart)
- It should discuss diagram design, explain concepts, suggest improvements
- Story 3.1 is chat-only — the AI does NOT modify the diagram yet (that's Story 3.2)
- Keep responses concise and diagram-focused
Component Architecture
DiagramEditor
├── DiagramCanvas (left, existing)
└── RightPanel (right, existing)
├── Tab: Chat (active) → <ChatPanel diagramId={id} diagramType={type} />
├── Tab: Inspector (placeholder)
└── Tab: Annotations (placeholder)
ChatPanel
├── MessageList (scrollable)
│ ├── UserMessage (text bubble)
│ └── AssistantMessage (parts-based, streaming)
├── TypingIndicator (shown when status === "submitted")
├── ErrorMessage (shown when error !== undefined)
└── InputArea
├── TextInput (Enter to send)
├── SendButton (disabled when status !== "ready")
├── StopButton (shown during streaming)
└── MicButton (placeholder, disabled)
Prop Threading
The diagramId and diagramType must flow from the page down to ChatPanel:
page.tsxfetches diagram data (already does this)DiagramEditorreceivesdiagramprop (already does this)- Pass
diagram.idanddiagram.diagramTypetoRightPanel RightPanelpasses toChatPanel
Check the current DiagramEditor component to see how diagram prop is structured and what fields are available.
Styling
- Use Tailwind CSS classes (no CSS modules)
- Use shadcn/ui components from
@turbostarter/ui-web/for Button, Input, ScrollArea - Follow existing diagram accent pattern: consider using
--diagram-chator a neutral accent for the copilot - Support light/dark mode via CSS variables (already system-wide via
colorMode="system") - Use
color-mix()for computed tints following Epic 2 patterns
Project Structure Notes
New files to create:
packages/api/src/modules/ai/copilot/
└── router.ts # Copilot streaming endpoint
packages/ai/src/modules/copilot/
└── system-prompt.ts # Diagram system prompt generator
apps/web/src/modules/chat/components/
└── ChatPanel.tsx # Main copilot chat panel
└── ChatPanel.test.ts # Unit tests
└── MessageList.tsx # Scrollable message container (optional split)
└── ChatInput.tsx # Input area with send/mic (optional split)
Existing files to modify:
apps/web/src/modules/diagram/components/editor/RightPanel.tsx # Replace placeholder
apps/web/src/modules/diagram/components/editor/DiagramEditor.tsx # Thread diagramId/type props
packages/api/src/modules/ai/router.ts # Register copilot route
packages/db/src/schema/chat.ts # Add diagramId column (or new copilot schema)
packages/db/src/schema/index.ts # Register new exports if schema changes
Naming conventions: PascalCase component files in kebab-case dirs. Co-located tests (ChatPanel.test.ts beside ChatPanel.tsx).
References
- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.1]
- [Source: _bmad-output/project-context.md#Critical Implementation Rules]
- [Source: packages/db/src/schema/chat.ts — existing chat DB schema]
- [Source: packages/api/src/modules/ai/chat.ts — existing chat API router]
- [Source: packages/ai/src/modules/chat/api.ts — streamChat() implementation]
- [Source: packages/ai/src/modules/chat/constants.ts — model definitions]
- [Source: apps/web/src/modules/chat/composer/hooks/use-composer.tsx — useChat transport pattern]
- [Source: apps/web/src/modules/chat/thread/message/assistant/index.tsx — streaming message rendering]
- [Source: apps/web/src/modules/diagram/components/editor/RightPanel.tsx — current placeholder]
- [Source: apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx — existing canvas]
- [Source: AI SDK docs — https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat]
- [Source: AI SDK docs — https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text]
Previous Story Intelligence (Story 2.9)
- Pattern:
useGraphStore.getState()for accessing store state in callbacks (avoids stale closures) - Map over Set+find: Use
Mapfor O(1) lookups when you need both membership test and value retrieval - Review themes: Dead code removal, CSS variable discipline, performance-aware selectors
- Web test count at end of 2.9: 186 tests in apps/web
Testing Standards
- Vitest with
describe/it/expect— explicit imports fromvitest - Co-located test files:
ChatPanel.test.tsnext toChatPanel.tsx - Factory functions for test data (e.g.,
createTestMessage(overrides)) - Test schema validation, prompt generation, UI state logic
- Do NOT test React component rendering (deferred to E2E) — test logic/hooks/utilities
- Run:
pnpm --filter @turbostarter/web testorpnpm test
Critical Don't-Miss Rules
- ESM-only: No
require(). Allimportstatements. - Path alias:
~/for app-internal imports in apps/web - IDs:
generateId()from@turbostarter/shared/utils— 32-char text columns - Validation:
validate("json", schema)middleware on Hono — never inline.parse() - Error handling:
HttpException(HttpStatusCode.XXX, {...})— never raw status numbers - No business logic in routers: Handler delegates to domain function
- Workspace imports: Must match
exportsfield exactly (e.g.,@turbostarter/ai/credits/server) - pgSchema: If creating new schema, wrap with
prefix()inschema/index.ts - Conventional commits:
feat: implement chat panel UI with streaming AI responses - Soft-delete guards:
isNull(deletedAt)on every query touching soft-deleted tables
Dev Agent Record
Agent Model Used
Claude Opus 4.6
Debug Log References
- Fixed
useChatbodyoption — not a top-level option in latest AI SDK; usedprepareSendMessagesRequestinDefaultChatTransportinstead - Fixed
toChatMessagetype error — DBtext()column typed asstringbutUIMessagePartneeds literal"text"type; used"text" as const
Code Review Fixes Applied
- H1: Added GET
/messagesendpoint to copilot router +getCopilotHistoryin api.ts; CopilotPanel fetches history on mount viauseQueryand seeds viasetMessages - H2: Tasks 7.3-7.5 marked as deferred (not [x]) with explanation notes
- M1: Removed unused
generateIdimport from CopilotPanel - M2: Wrapped
DefaultChatTransportinuseMemoto prevent recreation on every render - M3: Added
graphContext: z.string().optional()to copilot message schema - M4: Rewrote
types.tsto deriveDiagramTypefrom DBdiagramTypeEnum(single source of truth) - Cascading fix: Changed PDF module
Chatimports from@turbostarter/ai/chat/typestoSelectPdfChatfrom@turbostarter/db/schema/pdf(type broke whendiagramIdwas added tochat.chat)
Completion Notes List
- Copilot API route created at
/api/ai/copilotwith own system prompt, schema, and streaming - Reuses existing
chat.chat,chat.message,chat.partDB tables (no new schema) - Added
diagramIdcolumn (nullable, indexed) tochat.chattable with migration - CopilotPanel uses
useChatdirectly (simpler thanuseComposerhook) - Chat history persists and loads on page refresh via GET
/messagesendpoint - Auto-scroll with user-scroll-pause detection
- Typing indicator, error display, stop button, markdown rendering all functional
- 15 copilot tests (9 schema + 6 system prompt) passing
- 186 web tests passing (unchanged)
- Task 7.3-7.5 (component rendering tests) deferred per testing standards: "Do NOT test React component rendering — deferred to E2E"
File List
Created:
packages/ai/src/modules/copilot/types.tspackages/ai/src/modules/copilot/schema.tspackages/ai/src/modules/copilot/schema.test.tspackages/ai/src/modules/copilot/system-prompt.tspackages/ai/src/modules/copilot/system-prompt.test.tspackages/ai/src/modules/copilot/api.tspackages/api/src/modules/ai/copilot/router.tsapps/web/src/modules/copilot/components/CopilotPanel.tsxpackages/db/migrations/0002_numerous_siren.sql
Modified:
packages/ai/package.json— added./copilot/*exportpackages/api/src/modules/ai/router.ts— registered copilot routepackages/db/src/schema/chat.ts— addeddiagramIdcolumn + indexapps/web/src/modules/diagram/components/editor/RightPanel.tsx— wired CopilotPanelapps/web/src/modules/diagram/components/editor/DiagramEditor.tsx— passed diagramId/diagramType propsapps/web/src/modules/pdf/components/recent-chats.tsx— fixed Chat type import (cascading fix)apps/web/src/modules/pdf/history/list/item.tsx— fixed Chat type import (cascading fix)