- 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)
145 lines
7.7 KiB
Markdown
145 lines
7.7 KiB
Markdown
---
|
|
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
|