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 |
|
| 3. Operator Experience | 5/5 | Complete | 2026-03-24 |
|
||||||
| 4. RBAC | 3/3 | Complete | 2026-03-24 |
|
| 4. RBAC | 3/3 | Complete | 2026-03-24 |
|
||||||
| 5. Employee Design | 4/4 | Complete | 2026-03-25 |
|
| 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: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: completed
|
status: completed
|
||||||
stopped_at: Completed 06-01-PLAN.md
|
stopped_at: Completed 06-02-PLAN.md
|
||||||
last_updated: "2026-03-25T16:28:34.002Z"
|
last_updated: "2026-03-25T16:36:09.493Z"
|
||||||
last_activity: 2026-03-23 — Completed 03-02 onboarding wizard, Slack OAuth, BYO API keys
|
last_activity: 2026-03-23 — Completed 03-02 onboarding wizard, Slack OAuth, BYO API keys
|
||||||
progress:
|
progress:
|
||||||
total_phases: 6
|
total_phases: 6
|
||||||
completed_phases: 5
|
completed_phases: 5
|
||||||
total_plans: 25
|
total_plans: 25
|
||||||
completed_plans: 23
|
completed_plans: 24
|
||||||
percent: 100
|
percent: 100
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ Progress: [██████████] 100%
|
|||||||
| Phase 05-employee-design P03 | 2min | 1 tasks | 0 files |
|
| Phase 05-employee-design P03 | 2min | 1 tasks | 0 files |
|
||||||
| Phase 05-employee-design P04 | 1min | 2 tasks | 3 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 P01 | 8min | 2 tasks | 11 files |
|
||||||
|
| Phase 06-web-chat PP02 | 6min | 2 tasks | 10 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## 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]: 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]: 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]: 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
|
### Roadmap Evolution
|
||||||
|
|
||||||
@@ -178,6 +181,6 @@ None — all phases complete.
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-25T16:28:33.999Z
|
Last session: 2026-03-25T16:36:09.490Z
|
||||||
Stopped at: Completed 06-01-PLAN.md
|
Stopped at: Completed 06-02-PLAN.md
|
||||||
Resume file: None
|
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