--- phase: 06-web-chat plan: 02 type: execute wave: 2 depends_on: ["06-01"] files_modified: - packages/portal/app/(dashboard)/chat/page.tsx - 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/lib/use-chat-socket.ts - packages/portal/lib/queries.ts - packages/portal/lib/api.ts - packages/portal/components/nav.tsx - packages/portal/package.json autonomous: true requirements: - CHAT-01 - CHAT-03 - CHAT-04 - CHAT-05 must_haves: truths: - "User can navigate to /chat from the sidebar and see a conversation list" - "User can select an agent and start a new conversation" - "User can type a message and see it appear as a right-aligned bubble" - "Agent response appears as a left-aligned bubble with markdown rendering" - "Typing indicator (animated dots) shows while waiting for agent response" - "Conversation history loads when user returns to a previous conversation" - "Operator, customer admin, and platform admin can all access /chat" artifacts: - path: "packages/portal/app/(dashboard)/chat/page.tsx" provides: "Main chat page with sidebar + active conversation" min_lines: 50 - path: "packages/portal/components/chat-sidebar.tsx" provides: "Conversation list with agent names and timestamps" contains: "ChatSidebar" - path: "packages/portal/components/chat-window.tsx" provides: "Active conversation with message list, input, and send button" contains: "ChatWindow" - path: "packages/portal/components/chat-message.tsx" provides: "Message bubble with markdown rendering and role-based alignment" contains: "ChatMessage" - path: "packages/portal/components/typing-indicator.tsx" provides: "Animated typing dots component" contains: "TypingIndicator" - path: "packages/portal/lib/use-chat-socket.ts" provides: "React hook managing WebSocket lifecycle" contains: "useChatSocket" key_links: - from: "packages/portal/lib/use-chat-socket.ts" to: "packages/gateway/gateway/channels/web.py" via: "WebSocket connection to /chat/ws/{conversationId}" pattern: "new WebSocket" - from: "packages/portal/app/(dashboard)/chat/page.tsx" to: "packages/portal/lib/queries.ts" via: "useConversations + useConversationHistory hooks" pattern: "useConversations|useConversationHistory" - from: "packages/portal/components/nav.tsx" to: "packages/portal/app/(dashboard)/chat/page.tsx" via: "Nav link to /chat" pattern: 'href.*"/chat"' --- Build the complete portal chat UI: a dedicated /chat page with conversation sidebar, message window with markdown rendering, typing indicators, and WebSocket integration. Users can start conversations with AI Employees, see real-time responses, and browse conversation history. Purpose: Delivers the user-facing chat experience that connects to the backend infrastructure from Plan 01. Output: Fully interactive chat page in the portal with all CHAT requirements addressed. @/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md @/home/adelorenzo/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/06-web-chat/06-CONTEXT.md @.planning/phases/06-web-chat/06-RESEARCH.md @.planning/phases/06-web-chat/06-01-SUMMARY.md WebSocket endpoint: ws://localhost:8001/chat/ws/{conversationId} Protocol: 1. Client connects 2. Client sends: {"type": "auth", "userId": "uuid", "role": "role_string", "tenantId": "uuid|null"} 3. Client sends: {"type": "message", "text": "user message"} 4. Server sends: {"type": "typing"} (immediate) 5. Server sends: {"type": "response", "text": "agent reply", "conversation_id": "uuid"} REST API: GET /api/portal/chat/conversations?tenant_id={id} -> [{id, agent_id, agent_name, updated_at, last_message_preview}] GET /api/portal/chat/conversations/{id}/messages?limit=50&before={cursor} -> [{id, role, content, created_at}] POST /api/portal/chat/conversations Body: {tenant_id, agent_id} -> {id, tenant_id, agent_id, user_id, created_at, updated_at} DELETE /api/portal/chat/conversations/{id} -> 204 From packages/portal/lib/api.ts: ```typescript export function setPortalSession(session: {...}): void; function getAuthHeaders(): Record; const api = { get, post, put, delete }; ``` From packages/portal/lib/queries.ts: ```typescript export const queryKeys = { tenants, agents, ... }; export function useAgents(tenantId: string): UseQueryResult; export function useTenants(page?: number): UseQueryResult; // ADD: useConversations, useConversationHistory, useCreateConversation, useDeleteConversation ``` From packages/portal/components/nav.tsx: ```typescript const navItems: NavItem[] = [ { href: "/dashboard", ... }, { href: "/agents", label: "Employees", ... }, // ADD: { href: "/chat", label: "Chat", icon: MessageSquare } // Visible to ALL roles (no allowedRoles restriction) ]; ``` From packages/portal/proxy.ts: ```typescript const CUSTOMER_OPERATOR_RESTRICTED = ["/billing", "/settings/api-keys", "/users", "/admin", "/agents/new"]; // /chat is NOT in this list — operators CAN access chat (per CONTEXT.md: "chatting IS the product") ``` Task 1: Install dependencies, add API types/hooks, create WebSocket hook packages/portal/package.json, packages/portal/lib/api.ts, packages/portal/lib/queries.ts, packages/portal/lib/use-chat-socket.ts 1. Install react-markdown and remark-gfm: `cd packages/portal && npm install react-markdown remark-gfm` 2. Add chat types to packages/portal/lib/api.ts (at the bottom, after existing types): ```typescript // Chat types export interface Conversation { id: string; agent_id: string; agent_name: string; updated_at: string; last_message_preview: string | null; } export interface ConversationMessage { id: string; role: "user" | "assistant"; content: string; created_at: string; } export interface CreateConversationRequest { tenant_id: string; agent_id: string; } export interface ConversationDetail { id: string; tenant_id: string; agent_id: string; user_id: string; created_at: string; updated_at: string; } ``` 3. Add chat hooks to packages/portal/lib/queries.ts: - Add to queryKeys: conversations(tenantId) and conversationHistory(conversationId) - useConversations(tenantId: string) — GET /api/portal/chat/conversations?tenant_id={tenantId}, returns Conversation[], enabled: !!tenantId - useConversationHistory(conversationId: string) — GET /api/portal/chat/conversations/{conversationId}/messages, returns ConversationMessage[], enabled: !!conversationId - useCreateConversation() — POST mutation to /api/portal/chat/conversations, invalidates conversations query on success - useDeleteConversation() — DELETE mutation, invalidates conversations + history queries Follow the exact same pattern as useAgents, useCreateAgent, etc. 4. Create packages/portal/lib/use-chat-socket.ts: - "use client" directive at top - useChatSocket({ conversationId, onMessage, onTyping, authHeaders }) hook - authHeaders: { userId: string; role: string; tenantId: string | null } - On mount: create WebSocket to `${NEXT_PUBLIC_WS_URL ?? "ws://localhost:8001"}/chat/ws/${conversationId}` - On open: send auth JSON message immediately - On message: parse JSON, if type="typing" call onTyping(true), if type="response" call onTyping(false) then onMessage(data.text) - send(text: string) function: sends {"type": "message", "text": text} if connected - Return { send, isConnected } - On unmount/conversationId change: close WebSocket (useEffect cleanup) - Simple reconnect: on close, attempt reconnect after 3s (limit to 3 retries, then show error) - Use useRef for WebSocket instance, useState for isConnected - Use useCallback for send to keep stable reference IMPORTANT: Read packages/portal/node_modules/next/dist/docs/ for any relevant Next.js 16 patterns before writing code. IMPORTANT: Use NEXT_PUBLIC_WS_URL env var (not NEXT_PUBLIC_API_URL) — WebSocket URL may differ from REST API URL. IMPORTANT: Auth message sent as first JSON payload after connection (browser WebSocket cannot send custom headers). cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20 react-markdown and remark-gfm installed. Chat types exported from api.ts. Four query hooks (useConversations, useConversationHistory, useCreateConversation, useDeleteConversation) added to queries.ts. useChatSocket hook manages WebSocket lifecycle with auth and reconnection. Portal builds without errors. Task 2: Chat page, components, nav link, and styling packages/portal/app/(dashboard)/chat/page.tsx, 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/components/nav.tsx 1. Create packages/portal/components/typing-indicator.tsx: - "use client" component - Three animated dots with CSS animation (scale/opacity pulsing with staggered delays) - Wrapped in a message-bubble-style container (left-aligned, muted background) - Use Tailwind animate classes or inline keyframes 2. Create packages/portal/components/chat-message.tsx: - "use client" component - Props: { role: "user" | "assistant"; content: string; createdAt: string } - User messages: right-aligned, primary color background, white text - Assistant messages: left-aligned, muted background, with agent avatar icon (Bot from lucide-react) - Render content with react-markdown + remark-gfm for assistant messages (code blocks, lists, bold, links) - User messages: plain text (no markdown rendering needed) - Show timestamp in relative format (e.g., "2m ago") on hover or below message - Inline image display for any markdown image links in agent responses 3. Create packages/portal/components/chat-sidebar.tsx: - "use client" component - Props: { conversations: Conversation[]; activeId: string | null; onSelect: (id: string) => void; onNewChat: () => void } - "New Conversation" button at top (Plus icon from lucide-react) - Scrollable list of conversations showing: agent name (bold), last message preview (truncated, muted), relative timestamp - Active conversation highlighted with accent background - Empty state: "No conversations yet" 4. Create packages/portal/components/chat-window.tsx: - "use client" component - Props: { conversationId: string; authHeaders: { userId, role, tenantId } } - Uses useConversationHistory(conversationId) for initial load - Uses useChatSocket for real-time messaging - State: messages array (merged from history + new), isTyping boolean, inputText string - On history load: populate messages from query data - On WebSocket message: append to messages array, scroll to bottom - On typing indicator: show TypingIndicator below last message - Input area at bottom: textarea (auto-growing, max 4 lines) + Send button (SendHorizontal icon from lucide-react) - Send on Enter (Shift+Enter for newline), clear input after send - Auto-scroll to bottom on new messages (use ref + scrollIntoView) - Show "Connecting..." state when WebSocket not connected - Empty state when no conversationId selected: "Select a conversation or start a new one" 5. Create packages/portal/app/(dashboard)/chat/page.tsx: - "use client" component - Layout: flex row, full height (h-[calc(100vh-4rem)] or similar to fill dashboard area) - Left: ChatSidebar (w-80, border-right) - Right: ChatWindow (flex-1) - State: activeConversationId (string | null), showAgentPicker (boolean) - On mount: load conversations via useConversations(activeTenantId) - For platform admin: use tenant switcher pattern — show all tenants, load agents per tenant - "New Conversation" flow: show agent picker dialog (Dialog from shadcn base-ui). List agents from useAgents(tenantId). On agent select: call useCreateConversation, set activeConversationId to result.id - URL state: sync activeConversationId to URL search param ?id={conversationId} for bookmark/refresh support - Get auth headers from session (useSession from next-auth/react) — userId, role, activeTenantId 6. Update packages/portal/components/nav.tsx: - Import MessageSquare from lucide-react - Add { href: "/chat", label: "Chat", icon: MessageSquare } to navItems array - Position after "Employees" and before "Usage" - No allowedRoles restriction (all roles can chat per CONTEXT.md) The chat should feel like a modern messaging app (Slack DMs / iMessage style) — not a clinical chatbot widget. Clean spacing, smooth scrolling, readable typography. IMPORTANT: Use standardSchemaResolver (not zodResolver) if any forms are needed (per STATE.md convention). IMPORTANT: use(searchParams) pattern for reading URL params in client components (Next.js 15/16 convention). IMPORTANT: base-ui DialogTrigger uses render prop not asChild (per Phase 4 STATE.md decision). cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20 Chat page renders at /chat with sidebar (conversation list) and main panel (active conversation). New Conversation button opens agent picker dialog. Messages display with role-based alignment and markdown rendering. Typing indicator animates during response wait. Nav sidebar includes Chat link visible to all roles. Portal builds without errors. 1. Portal builds: `cd packages/portal && npx next build` 2. Chat page accessible at /chat after login 3. Nav shows "Chat" link for all roles 4. No TypeScript errors in new files - /chat page renders with left sidebar and right conversation panel - New Conversation flow: agent picker -> create conversation -> WebSocket connect - Messages render with markdown (assistant) and plain text (user) - Typing indicator shows animated dots during response generation - Conversation history loads from REST API on page visit - WebSocket connects and authenticates via JSON auth message - Nav includes Chat link visible to all three roles - Portal builds successfully After completion, create `.planning/phases/06-web-chat/06-02-SUMMARY.md`