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:
@@ -140,7 +140,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6
|
||||
| 3. Operator Experience | 5/5 | Complete | 2026-03-24 |
|
||||
| 4. RBAC | 3/3 | Complete | 2026-03-24 |
|
||||
| 5. Employee Design | 4/4 | Complete | 2026-03-25 |
|
||||
| 6. Web Chat | 1/3 | In Progress| |
|
||||
| 6. Web Chat | 2/3 | In Progress| |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ gsd_state_version: 1.0
|
||||
milestone: v1.0
|
||||
milestone_name: milestone
|
||||
status: completed
|
||||
stopped_at: Completed 06-01-PLAN.md
|
||||
last_updated: "2026-03-25T16:28:34.002Z"
|
||||
stopped_at: Completed 06-02-PLAN.md
|
||||
last_updated: "2026-03-25T16:36:09.493Z"
|
||||
last_activity: 2026-03-23 — Completed 03-02 onboarding wizard, Slack OAuth, BYO API keys
|
||||
progress:
|
||||
total_phases: 6
|
||||
completed_phases: 5
|
||||
total_plans: 25
|
||||
completed_plans: 23
|
||||
completed_plans: 24
|
||||
percent: 100
|
||||
---
|
||||
|
||||
@@ -75,6 +75,7 @@ Progress: [██████████] 100%
|
||||
| Phase 05-employee-design P03 | 2min | 1 tasks | 0 files |
|
||||
| Phase 05-employee-design P04 | 1min | 2 tasks | 3 files |
|
||||
| Phase 06-web-chat P01 | 8min | 2 tasks | 11 files |
|
||||
| Phase 06-web-chat PP02 | 6min | 2 tasks | 10 files |
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
@@ -163,6 +164,8 @@ Recent decisions affecting current work:
|
||||
- [Phase 06-web-chat]: WebSocket auth via first JSON message after connection — browser WebSocket API cannot send custom HTTP headers
|
||||
- [Phase 06-web-chat]: thread_id = conversation_id in web channel normalizer — scopes agent memory to one web conversation per conversation ID
|
||||
- [Phase 06-web-chat]: Redis pub-sub delivery: orchestrator publishes to webchat_response_key, WebSocket subscribes with 60s timeout before sending to client
|
||||
- [Phase 06-web-chat]: useSearchParams wrapped in Suspense boundary — Next.js 16 static prerendering requires Suspense for pages using URL params
|
||||
- [Phase 06-web-chat]: Stable callback refs in useChatSocket — onMessage/onTyping held in refs so WebSocket effect re-runs only when conversationId or auth changes
|
||||
|
||||
### Roadmap Evolution
|
||||
|
||||
@@ -178,6 +181,6 @@ None — all phases complete.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-25T16:28:33.999Z
|
||||
Stopped at: Completed 06-01-PLAN.md
|
||||
Last session: 2026-03-25T16:36:09.490Z
|
||||
Stopped at: Completed 06-02-PLAN.md
|
||||
Resume file: None
|
||||
|
||||
144
.planning/phases/06-web-chat/06-02-SUMMARY.md
Normal file
144
.planning/phases/06-web-chat/06-02-SUMMARY.md
Normal 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
|
||||
Reference in New Issue
Block a user