diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 61b10b9..b7a4cbb 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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| | --- diff --git a/.planning/STATE.md b/.planning/STATE.md index 094d232..f6f8be4 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -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 diff --git a/.planning/phases/06-web-chat/06-02-SUMMARY.md b/.planning/phases/06-web-chat/06-02-SUMMARY.md new file mode 100644 index 0000000..02332f4 --- /dev/null +++ b/.planning/phases/06-web-chat/06-02-SUMMARY.md @@ -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 `` 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