Files
konstruct/.planning/phases/06-web-chat/06-02-SUMMARY.md
Adolfo Delorenzo 7281285b13 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)
2026-03-25 10:36:22 -06:00

7.7 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
06-web-chat 02 frontend
web-chat
websocket
react-markdown
tanstack-query
portal-ui
requires provides affects
packages/gateway/gateway/channels/web.py (WebSocket endpoint /chat/ws/{conversationId})
packages/shared/shared/api/chat.py (REST API /api/portal/chat/*)
/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
packages/portal/components/nav.tsx (Chat link added)
packages/portal/lib/api.ts (Conversation types added)
packages/portal/lib/queries.ts (chat hooks added)
added patterns
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
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
created modified
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
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)
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
duration completed_date tasks_completed files_created files_modified
~6 minutes 2026-03-25 2 6 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 ChatWindowuseChatSocket
  • 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