Files
turbostarter/_bmad-output/implementation-artifacts/3-1-chat-panel-ui-with-streaming-ai-responses.md
Alejandro Gutiérrez 26215d9060 feat: implement Story 3.1 — chat panel UI with streaming AI responses
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>
2026-02-28 10:03:43 +00:00

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

  1. 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).

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

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

  4. 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.ts with POST / endpoint
    • 1.2 Wire enforceAuthvalidate("json", copilotMessageSchema) → handler
    • 1.3 Create copilotMessageSchema in same file or adjacent schema file — extends chat message with diagramId: z.string(), diagramType: z.string(), optional graphContext: z.string()
    • 1.4 Handler calls streamText() with diagram-specific system prompt, returns result.toUIMessageStreamResponse()
    • 1.5 Register route in packages/api/src/modules/ai/router.ts as .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
  • 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 useChat hook with api pointing to /api/ai/copilot endpoint
    • 2.3 Pass body: { diagramId, diagramType } to useChat for 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(...))
  • 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
  • Task 4: Wire ChatPanel into RightPanel (AC: 1)

    • 4.1 Replace the placeholder content in RightPanel.tsx Chat tab with <ChatPanel diagramId={...} diagramType={...} />
    • 4.2 Pass diagramId and diagramType as props from DiagramEditor → RightPanel → ChatPanel
    • 4.3 Ensure Chat tab is active by default (already implemented in RightPanel)
  • Task 5: Error handling (AC: 4)

    • 5.1 Use useChat's onError callback to show toast via sonner
    • 5.2 Display inline error message in chat when error is 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 calls stop()
  • Task 6: Persist chat per diagram (AC: 1, 2)

    • 6.1 Add diagramId column to existing chat.chat table (nullable text, indexed) — or create a new copilot_chat table in a copilot pgSchema
    • 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
  • 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:

  1. page.tsx fetches diagram data (already does this)
  2. DiagramEditor receives diagram prop (already does this)
  3. Pass diagram.id and diagram.diagramType to RightPanel
  4. RightPanel passes to ChatPanel

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-chat or 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 Map for 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 from vitest
  • Co-located test files: ChatPanel.test.ts next to ChatPanel.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 test or pnpm test

Critical Don't-Miss Rules

  • ESM-only: No require(). All import statements.
  • 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 exports field exactly (e.g., @turbostarter/ai/credits/server)
  • pgSchema: If creating new schema, wrap with prefix() in schema/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 useChat body option — not a top-level option in latest AI SDK; used prepareSendMessagesRequest in DefaultChatTransport instead
  • Fixed toChatMessage type error — DB text() column typed as string but UIMessagePart needs literal "text" type; used "text" as const

Code Review Fixes Applied

  • H1: Added GET /messages endpoint to copilot router + getCopilotHistory in api.ts; CopilotPanel fetches history on mount via useQuery and seeds via setMessages
  • H2: Tasks 7.3-7.5 marked as deferred (not [x]) with explanation notes
  • M1: Removed unused generateId import from CopilotPanel
  • M2: Wrapped DefaultChatTransport in useMemo to prevent recreation on every render
  • M3: Added graphContext: z.string().optional() to copilot message schema
  • M4: Rewrote types.ts to derive DiagramType from DB diagramTypeEnum (single source of truth)
  • Cascading fix: Changed PDF module Chat imports from @turbostarter/ai/chat/types to SelectPdfChat from @turbostarter/db/schema/pdf (type broke when diagramId was added to chat.chat)

Completion Notes List

  • Copilot API route created at /api/ai/copilot with own system prompt, schema, and streaming
  • Reuses existing chat.chat, chat.message, chat.part DB tables (no new schema)
  • Added diagramId column (nullable, indexed) to chat.chat table with migration
  • CopilotPanel uses useChat directly (simpler than useComposer hook)
  • Chat history persists and loads on page refresh via GET /messages endpoint
  • 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.ts
  • packages/ai/src/modules/copilot/schema.ts
  • packages/ai/src/modules/copilot/schema.test.ts
  • packages/ai/src/modules/copilot/system-prompt.ts
  • packages/ai/src/modules/copilot/system-prompt.test.ts
  • packages/ai/src/modules/copilot/api.ts
  • packages/api/src/modules/ai/copilot/router.ts
  • apps/web/src/modules/copilot/components/CopilotPanel.tsx
  • packages/db/migrations/0002_numerous_siren.sql

Modified:

  • packages/ai/package.json — added ./copilot/* export
  • packages/api/src/modules/ai/router.ts — registered copilot route
  • packages/db/src/schema/chat.ts — added diagramId column + index
  • apps/web/src/modules/diagram/components/editor/RightPanel.tsx — wired CopilotPanel
  • apps/web/src/modules/diagram/components/editor/DiagramEditor.tsx — passed diagramId/diagramType props
  • apps/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)