docs(06-02): complete web chat portal UI plan

- Add 06-02-SUMMARY.md with full execution record
- Update STATE.md: progress 96%, decisions recorded, session updated
- Update ROADMAP.md: phase 6 plan progress (2/3 summaries)
This commit is contained in:
2026-03-25 10:36:22 -06:00
parent 3c10bceba7
commit 7281285b13
3 changed files with 153 additions and 6 deletions

View File

@@ -0,0 +1,144 @@
---
phase: 06-web-chat
plan: 02
subsystem: frontend
tags: [web-chat, websocket, react-markdown, tanstack-query, portal-ui]
dependency_graph:
requires:
- packages/gateway/gateway/channels/web.py (WebSocket endpoint /chat/ws/{conversationId})
- packages/shared/shared/api/chat.py (REST API /api/portal/chat/*)
provides:
- /chat page accessible to all roles
- ChatSidebar, ChatWindow, ChatMessage, TypingIndicator components
- useChatSocket hook with auth handshake and reconnection
- useConversations, useConversationHistory, useCreateConversation, useDeleteConversation hooks
- Chat nav link visible to all roles
affects:
- packages/portal/components/nav.tsx (Chat link added)
- packages/portal/lib/api.ts (Conversation types added)
- packages/portal/lib/queries.ts (chat hooks added)
tech_stack:
added:
- react-markdown@^10.x (markdown rendering for assistant messages)
- remark-gfm (GitHub Flavored Markdown support)
- packages/portal/lib/use-chat-socket.ts (WebSocket lifecycle hook)
- packages/portal/components/chat-sidebar.tsx
- packages/portal/components/chat-window.tsx
- packages/portal/components/chat-message.tsx
- packages/portal/components/typing-indicator.tsx
- packages/portal/app/(dashboard)/chat/page.tsx
patterns:
- Suspense wrapper required for useSearchParams in Next.js 16 static prerendering
- Stable callback refs in useChatSocket to prevent WebSocket reconnect on re-renders
- Optimistic user message append before WebSocket send completes
- DialogTrigger with render prop (base-ui pattern, not asChild)
- crypto.randomUUID() for local message IDs before server assignment
key_files:
created:
- packages/portal/lib/use-chat-socket.ts
- packages/portal/components/chat-sidebar.tsx
- packages/portal/components/chat-window.tsx
- packages/portal/components/chat-message.tsx
- packages/portal/components/typing-indicator.tsx
- packages/portal/app/(dashboard)/chat/page.tsx
modified:
- packages/portal/lib/api.ts (Conversation, ConversationMessage, CreateConversationRequest, ConversationDetail types)
- packages/portal/lib/queries.ts (conversations/conversationHistory queryKeys + 4 hooks)
- packages/portal/components/nav.tsx (Chat nav item added)
- packages/portal/package.json (react-markdown, remark-gfm added)
decisions:
- "useSearchParams wrapped in Suspense boundary — Next.js 16 requires this for static prerendering of pages using URL params"
- "Stable callback refs in useChatSocket — onMessage/onTyping held in refs so WebSocket effect re-runs only when conversationId or auth changes, not on every render"
- "Optimistic user message appended locally before server echo — avoids waiting for WebSocket roundtrip to show the user's own message"
- "ChatPageInner + ChatPage split — useSearchParams must be inside Suspense; outer page provides fallback"
metrics:
duration: "~6 minutes"
completed_date: "2026-03-25"
tasks_completed: 2
files_created: 6
files_modified: 4
---
# Phase 6 Plan 02: Web Chat Portal UI Summary
**One-liner:** Full portal chat UI with WebSocket hook, markdown-rendering message bubbles, animated typing indicator, and conversation sidebar connecting to the Plan 01 gateway backend.
## What Was Built
This plan delivers the user-facing chat experience on top of the backend infrastructure from Plan 01.
### useChatSocket Hook (`lib/use-chat-socket.ts`)
WebSocket lifecycle management for browser clients:
- Connects to `${NEXT_PUBLIC_WS_URL}/chat/ws/{conversationId}`
- Sends JSON auth message immediately on open (browser WebSocket cannot send custom HTTP headers — established in Plan 01)
- Parses `{"type": "typing"}` and `{"type": "response", "text": "..."}` server messages
- Reconnects up to 3 times with 3-second delay after unexpected close
- Uses `useRef` for the WebSocket instance and callback refs for stable event handlers
- Intentional cleanup (unmount/conversationId change) sets `onclose = null` before closing to prevent spurious reconnect
### Chat Types and Query Hooks
Four new types in `api.ts`: `Conversation`, `ConversationMessage`, `CreateConversationRequest`, `ConversationDetail`.
Four new hooks in `queries.ts`:
- `useConversations(tenantId)` — lists all conversations for a tenant
- `useConversationHistory(conversationId)` — fetches last 50 messages
- `useCreateConversation()` — POST to create/get-or-create, invalidates conversations list
- `useDeleteConversation()` — DELETE with conversation + history invalidation
### Components
**TypingIndicator** — Three CSS `animate-bounce` dots with staggered `animationDelay` values (0ms, 150ms, 300ms) wrapped in a left-aligned muted bubble matching the assistant message style.
**ChatMessage** — Role-based bubble rendering:
- User: right-aligned, `bg-primary text-primary-foreground`, plain text
- Assistant: left-aligned, `bg-muted`, `Bot` icon avatar, full `react-markdown` with `remark-gfm` for code blocks, lists, links, tables
- Relative timestamp visible on hover via `opacity-0 group-hover:opacity-100`
**ChatSidebar** — Scrollable conversation list showing agent name, last message preview (truncated), and relative time. Active conversation highlighted with `bg-accent`. "New Conversation" button (Plus icon) triggers agent picker.
**ChatWindow** — Full-height conversation panel:
- Loads history via `useConversationHistory` on mount
- WebSocket via `useChatSocket` for real-time exchange
- Optimistically appends user message before server acknowledgement
- Auto-scrolls with `scrollIntoView({ behavior: "smooth" })` on new messages or typing changes
- Auto-growing textarea (capped at 96px / ~4 lines), Enter to send, Shift+Enter for newline
- Amber "Connecting..." banner when WebSocket disconnected
**ChatPage (`/chat`)** — Two-column layout (w-72 sidebar + flex-1 main):
- Reads `?id=` from URL via `useSearchParams` for bookmark/refresh support
- Agent picker dialog (base-ui `Dialog` with `render` prop on `DialogTrigger`) lists agents and calls `useCreateConversation`
- Session-derived auth headers passed to `ChatWindow``useChatSocket`
- Wrapped in `Suspense` (required for `useSearchParams` in Next.js 16)
### Nav Update
`MessageSquare` icon added to `nav.tsx` with `{ href: "/chat", label: "Chat" }` — no `allowedRoles` restriction, visible to operator, customer_admin, and platform_admin.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Suspense boundary required for useSearchParams**
- **Found during:** Task 2 build verification
- **Issue:** Next.js 16 static prerendering throws at build time when `useSearchParams()` is called outside a Suspense boundary: "useSearchParams() should be wrapped in a suspense boundary at page /chat"
- **Fix:** Extracted all page logic into `ChatPageInner` and wrapped it with `<Suspense fallback={...}>` in the `ChatPage` default export
- **Files modified:** `packages/portal/app/(dashboard)/chat/page.tsx`
- **Commit:** f9e67f9
## Self-Check: PASSED
All key artifacts verified:
- `packages/portal/app/(dashboard)/chat/page.tsx` — FOUND (235 lines, >50 min_lines)
- `packages/portal/components/chat-sidebar.tsx` — FOUND (contains ChatSidebar)
- `packages/portal/components/chat-window.tsx` — FOUND (contains ChatWindow)
- `packages/portal/components/chat-message.tsx` — FOUND (contains ChatMessage)
- `packages/portal/components/typing-indicator.tsx` — FOUND (contains TypingIndicator)
- `packages/portal/lib/use-chat-socket.ts` — FOUND (contains useChatSocket)
- WebSocket `new WebSocket` in use-chat-socket.ts — FOUND
- Nav href="/chat" in nav.tsx — FOUND
- useConversations/useConversationHistory in chat/page.tsx — FOUND
- Commits `7e21420` and `f9e67f9` — FOUND in git log
- Portal build: passes with `/chat` route listed