---
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"'
---

<objective>
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.
</objective>

<execution_context>
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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

<interfaces>
<!-- From Plan 01 — backend contracts the frontend connects to -->

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<string, string>;
const api = { get<T>, post<T>, put<T>, delete };
```

From packages/portal/lib/queries.ts:
```typescript
export const queryKeys = { tenants, agents, ... };
export function useAgents(tenantId: string): UseQueryResult<Agent[]>;
export function useTenants(page?: number): UseQueryResult<TenantsListResponse>;
// 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")
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Install dependencies, add API types/hooks, create WebSocket hook</name>
  <files>
    packages/portal/package.json,
    packages/portal/lib/api.ts,
    packages/portal/lib/queries.ts,
    packages/portal/lib/use-chat-socket.ts
  </files>
  <action>
    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).
  </action>
  <verify>
    <automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
  </verify>
  <done>
    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.
  </done>
</task>

<task type="auto">
  <name>Task 2: Chat page, components, nav link, and styling</name>
  <files>
    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
  </files>
  <action>
    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).
  </action>
  <verify>
    <automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
  </verify>
  <done>
    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.
  </done>
</task>

</tasks>

<verification>
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
</verification>

<success_criteria>
- /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
</success_criteria>

<output>
After completion, create `.planning/phases/06-web-chat/06-02-SUMMARY.md`
</output>
