From c0fa0cefee7d32a94bbf28eb67ea6b8e06f4232d Mon Sep 17 00:00:00 2001 From: Adolfo Delorenzo Date: Wed, 25 Mar 2026 10:08:44 -0600 Subject: [PATCH] docs(06-web-chat): create phase plan --- .planning/ROADMAP.md | 36 +-- .planning/phases/06-web-chat/06-01-PLAN.md | 329 +++++++++++++++++++++ .planning/phases/06-web-chat/06-02-PLAN.md | 325 ++++++++++++++++++++ .planning/phases/06-web-chat/06-03-PLAN.md | 119 ++++++++ 4 files changed, 792 insertions(+), 17 deletions(-) create mode 100644 .planning/phases/06-web-chat/06-01-PLAN.md create mode 100644 .planning/phases/06-web-chat/06-02-PLAN.md create mode 100644 .planning/phases/06-web-chat/06-03-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index f601557..60dd584 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -111,6 +111,23 @@ Plans: - [ ] 05-03-PLAN.md — Human verification: test all three creation paths, RBAC enforcement, system prompt auto-generation - [ ] 05-04-PLAN.md — Gap closure: add /agents/new to proxy RBAC restrictions, hide New Employee button for operators, fix wizard deploy error handling +### Phase 6: Web Chat +**Goal**: Users can chat with AI Employees directly in the portal through a real-time web chat interface — no external messaging platform required +**Depends on**: Phase 5 +**Requirements**: CHAT-01, CHAT-02, CHAT-03, CHAT-04, CHAT-05 +**Success Criteria** (what must be TRUE): + 1. A user can open a chat window with any AI Employee and have a real-time conversation within the portal + 2. The chat interface supports the full agent pipeline — memory, tools, escalation, and media (same capabilities as Slack/WhatsApp) + 3. Conversation history persists and is visible when the user returns to the chat + 4. The chat respects RBAC — users can only chat with agents belonging to tenants they have access to + 5. The chat interface feels responsive — typing indicators, message streaming or fast response display +**Plans**: 3 plans + +Plans: +- [ ] 06-01-PLAN.md — Backend: DB migration (web_conversations + web_conversation_messages), ORM models, ChannelType.WEB, Redis pub-sub key, WebSocket endpoint, web channel adapter, chat REST API with RBAC, orchestrator _send_response wiring, unit tests +- [ ] 06-02-PLAN.md — Frontend: /chat page with conversation sidebar, message window with markdown rendering, typing indicators, WebSocket hook, agent picker dialog, nav link, react-markdown install +- [ ] 06-03-PLAN.md — Human verification: end-to-end chat flow, conversation persistence, RBAC enforcement, markdown rendering, all roles can chat + ## Progress **Execution Order:** @@ -123,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 | 0/0 | Not started | - | +| 6. Web Chat | 0/3 | Not started | - | --- @@ -131,21 +148,6 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 **LLM-03 conflict resolved:** BYO API keys confirmed in v1 scope per user decision during Phase 3 context gathering. Implemented via Fernet encryption in Phase 3. -### Phase 6: Web Chat -**Goal**: Users can chat with AI Employees directly in the portal through a real-time web chat interface — no external messaging platform required -**Depends on**: Phase 5 -**Requirements**: CHAT-01, CHAT-02, CHAT-03, CHAT-04, CHAT-05 -**Success Criteria** (what must be TRUE): - 1. A user can open a chat window with any AI Employee and have a real-time conversation within the portal - 2. The chat interface supports the full agent pipeline — memory, tools, escalation, and media (same capabilities as Slack/WhatsApp) - 3. Conversation history persists and is visible when the user returns to the chat - 4. The chat respects RBAC — users can only chat with agents belonging to tenants they have access to - 5. The chat interface feels responsive — typing indicators, message streaming or fast response display -**Plans**: 0 plans - -Plans: -- [ ] TBD (run /gsd:plan-phase 6 to break down) - --- *Roadmap created: 2026-03-23* -*Coverage: 25/25 v1 requirements + 6 RBAC requirements + 5 Employee Design requirements mapped* +*Coverage: 25/25 v1 requirements + 6 RBAC requirements + 5 Employee Design requirements + 5 Web Chat requirements mapped* diff --git a/.planning/phases/06-web-chat/06-01-PLAN.md b/.planning/phases/06-web-chat/06-01-PLAN.md new file mode 100644 index 0000000..422940d --- /dev/null +++ b/.planning/phases/06-web-chat/06-01-PLAN.md @@ -0,0 +1,329 @@ +--- +phase: 06-web-chat +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages/shared/shared/models/message.py + - packages/shared/shared/redis_keys.py + - packages/shared/shared/models/chat.py + - packages/shared/shared/api/chat.py + - packages/shared/shared/api/__init__.py + - packages/gateway/gateway/channels/web.py + - packages/gateway/gateway/main.py + - packages/orchestrator/orchestrator/tasks.py + - migrations/versions/008_web_chat.py + - tests/unit/test_web_channel.py + - tests/unit/test_chat_api.py +autonomous: true +requirements: + - CHAT-01 + - CHAT-02 + - CHAT-03 + - CHAT-04 + - CHAT-05 + +must_haves: + truths: + - "Web channel messages normalize into valid KonstructMessage with channel='web'" + - "Celery _send_response publishes web channel responses to Redis pub-sub" + - "WebSocket endpoint accepts connections and dispatches messages to Celery pipeline" + - "Typing indicator event is sent immediately after receiving a user message" + - "Chat REST API enforces RBAC — non-members get 403" + - "Platform admin can access conversations for any tenant" + - "Conversation history persists in DB and is loadable via REST" + artifacts: + - path: "packages/shared/shared/models/chat.py" + provides: "WebConversation and WebConversationMessage ORM models" + contains: "class WebConversation" + - path: "packages/gateway/gateway/channels/web.py" + provides: "WebSocket endpoint and web channel normalizer" + contains: "async def chat_websocket" + - path: "packages/shared/shared/api/chat.py" + provides: "REST API for conversation CRUD" + exports: ["chat_router"] + - path: "migrations/versions/008_web_chat.py" + provides: "DB migration for web_conversations and web_conversation_messages tables" + contains: "web_conversations" + - path: "tests/unit/test_web_channel.py" + provides: "Unit tests for web channel adapter" + contains: "test_normalize_web_event" + - path: "tests/unit/test_chat_api.py" + provides: "Unit tests for chat REST API with RBAC" + contains: "test_chat_rbac_enforcement" + key_links: + - from: "packages/gateway/gateway/channels/web.py" + to: "packages/orchestrator/orchestrator/tasks.py" + via: "handle_message.delay() Celery dispatch" + pattern: "handle_message\\.delay" + - from: "packages/orchestrator/orchestrator/tasks.py" + to: "packages/shared/shared/redis_keys.py" + via: "Redis pub-sub publish for web channel" + pattern: "webchat_response_key" + - from: "packages/gateway/gateway/channels/web.py" + to: "packages/shared/shared/redis_keys.py" + via: "Redis pub-sub subscribe for response delivery" + pattern: "webchat_response_key" + - from: "packages/shared/shared/api/chat.py" + to: "packages/shared/shared/api/rbac.py" + via: "require_tenant_member RBAC guard" + pattern: "require_tenant_member" + +user_setup: [] +--- + + +Build the complete backend infrastructure for web chat: DB schema, ORM models, web channel adapter with WebSocket endpoint, Redis pub-sub response bridge, chat REST API with RBAC, and orchestrator integration. After this plan, the portal can send messages via WebSocket and receive responses through the full agent pipeline. + +Purpose: Enables the portal to use the same agent pipeline as Slack/WhatsApp via a new "web" channel — the foundational plumbing that the frontend chat UI (Plan 02) connects to. +Output: Working WebSocket endpoint, conversation persistence, RBAC-enforced REST API, and unit tests. + + + +@/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 + + + + +From packages/shared/shared/models/message.py: +```python +class ChannelType(StrEnum): + SLACK = "slack" + WHATSAPP = "whatsapp" + MATTERMOST = "mattermost" + ROCKETCHAT = "rocketchat" + TEAMS = "teams" + TELEGRAM = "telegram" + SIGNAL = "signal" + # WEB = "web" <-- ADD THIS + +class KonstructMessage(BaseModel): + id: str + tenant_id: str | None + channel: ChannelType + channel_metadata: dict[str, Any] + sender: SenderInfo + content: MessageContent + timestamp: datetime + thread_id: str | None + reply_to: str | None + context: dict[str, Any] +``` + +From packages/shared/shared/redis_keys.py: +```python +# All keys follow: {tenant_id}:{key_type}:{discriminator} +def memory_short_key(tenant_id: str, agent_id: str, user_id: str) -> str +def escalation_status_key(tenant_id: str, thread_id: str) -> str +# ADD: webchat_response_key(tenant_id, conversation_id) +``` + +From packages/shared/shared/api/rbac.py: +```python +@dataclass +class PortalCaller: + user_id: uuid.UUID + role: str + tenant_id: uuid.UUID | None = None + +async def get_portal_caller(...) -> PortalCaller +async def require_tenant_member(tenant_id: UUID, caller: PortalCaller, session: AsyncSession) -> None +async def require_tenant_admin(tenant_id: UUID, caller: PortalCaller, session: AsyncSession) -> None +``` + +From packages/orchestrator/orchestrator/tasks.py: +```python +# handle_message pops extras before model_validate: +# placeholder_ts, channel_id, phone_number_id, bot_token +# ADD: conversation_id, portal_user_id, tenant_id (for web) + +# _send_response routes by channel_str: +# "slack" -> _update_slack_placeholder +# "whatsapp" -> send_whatsapp_message +# ADD: "web" -> Redis pub-sub publish + +# _build_response_extras builds channel-specific extras dict +# ADD: "web" case returning conversation_id + tenant_id +``` + +From packages/shared/shared/api/__init__.py: +```python +# Current routers mounted on gateway: +# portal_router, billing_router, channels_router, llm_keys_router, +# usage_router, webhook_router, invitations_router, templates_router +# ADD: chat_router +``` + +From packages/gateway/gateway/main.py: +```python +# CORS allows: localhost:3000, 127.0.0.1:3000, 100.64.0.10:3000 +# WebSocket doesn't use CORS (browser doesn't enforce) but same origin rules apply +# Include chat_router and WebSocket router here +``` + + + + + + + Task 1: Backend models, migration, channel type, Redis key, and unit tests + + packages/shared/shared/models/message.py, + packages/shared/shared/redis_keys.py, + packages/shared/shared/models/chat.py, + migrations/versions/008_web_chat.py, + tests/unit/test_web_channel.py, + tests/unit/test_chat_api.py + + + - test_normalize_web_event: normalize_web_event({text, tenant_id, agent_id, user_id, conversation_id}) -> KonstructMessage with channel=WEB, thread_id=conversation_id, sender.user_id=portal_user_id + - test_send_response_web_publishes_to_redis: _send_response("web", "hello", {conversation_id, tenant_id}) publishes JSON to Redis channel matching webchat_response_key(tenant_id, conversation_id) + - test_typing_indicator_sent: WebSocket handler sends {"type": "typing"} immediately after receiving user message, before Celery dispatch + - test_chat_rbac_enforcement: GET /api/portal/chat/conversations?tenant_id=X returns 403 when caller is not a member of tenant X + - test_platform_admin_cross_tenant: GET /api/portal/chat/conversations?tenant_id=X returns 200 when caller is platform_admin (bypasses membership) + - test_list_conversation_history: GET /api/portal/chat/conversations/{id}/messages returns paginated messages ordered by created_at + - test_create_conversation: POST /api/portal/chat/conversations with {tenant_id, agent_id} creates or returns existing conversation for user+agent pair + + + 1. Add WEB = "web" to ChannelType in packages/shared/shared/models/message.py + + 2. Add webchat_response_key(tenant_id, conversation_id) to packages/shared/shared/redis_keys.py following existing pattern: return f"{tenant_id}:webchat:response:{conversation_id}" + + 3. Create packages/shared/shared/models/chat.py with ORM models: + - WebConversation: id (UUID PK), tenant_id (UUID, FK tenants.id), agent_id (UUID, FK agents.id), user_id (UUID, FK portal_users.id), created_at, updated_at. UniqueConstraint on (tenant_id, agent_id, user_id). RLS via tenant_id. + - WebConversationMessage: id (UUID PK), conversation_id (UUID, FK web_conversations.id ON DELETE CASCADE), tenant_id (UUID), role (TEXT, CHECK "user"/"assistant"), content (TEXT), created_at. RLS via tenant_id. + Use mapped_column() + Mapped[] (SQLAlchemy 2.0 pattern, not Column()). + + 4. Create migration 008_web_chat.py: + - Create web_conversations table with columns matching ORM model + - Create web_conversation_messages table with FK to web_conversations + - Enable RLS on both tables (FORCE ROW LEVEL SECURITY) + - Create RLS policies matching existing pattern (current_setting('app.current_tenant')::uuid) + - ALTER CHECK constraint on channel_connections.channel_type to include 'web' (see Pitfall 5 in RESEARCH.md — the existing CHECK must be replaced, not just added to) + - Add index on web_conversation_messages(conversation_id, created_at) + + 5. Write test files FIRST (RED phase): + - tests/unit/test_web_channel.py: test normalize_web_event, test _send_response web publishes to Redis (mock aioredis), test typing indicator + - tests/unit/test_chat_api.py: test RBAC enforcement (403 for non-member), platform admin cross-tenant (200), list history (paginated), create conversation (get-or-create) + Use httpx AsyncClient with app fixture pattern from existing tests. Mock DB sessions and Redis. + + IMPORTANT: Celery tasks MUST be sync def with asyncio.run() — never async def (hard architectural constraint). + IMPORTANT: Use TEXT+CHECK for role column (not sa.Enum) per Phase 1 convention. + IMPORTANT: _send_response "web" case must use try/finally around aioredis.from_url() to avoid connection leaks (Pitfall 2 from RESEARCH.md). + + + cd /home/adelorenzo/repos/konstruct && python -m pytest tests/unit/test_web_channel.py tests/unit/test_chat_api.py -x -v + + + ChannelType.WEB exists. webchat_response_key function exists. ORM models define web_conversations and web_conversation_messages. Migration 008 creates both tables with RLS and updates channel_type CHECK constraint. All test assertions pass (RED then GREEN). + + + + + Task 2: WebSocket endpoint, web channel adapter, REST API, orchestrator wiring + + packages/gateway/gateway/channels/web.py, + packages/shared/shared/api/chat.py, + packages/shared/shared/api/__init__.py, + packages/gateway/gateway/main.py, + packages/orchestrator/orchestrator/tasks.py + + + 1. Create packages/gateway/gateway/channels/web.py with: + a. normalize_web_event() function: takes dict with {text, tenant_id, agent_id, user_id, display_name, conversation_id} and returns KonstructMessage with channel=ChannelType.WEB, thread_id=conversation_id, sender.user_id=user_id (portal user UUID string), channel_metadata={portal_user_id, tenant_id, conversation_id} + b. WebSocket endpoint at /chat/ws/{conversation_id}: + - Accept connection + - Wait for first JSON message with type="auth" containing {userId, role, tenantId} (browser cannot send custom headers — Pitfall 1 from RESEARCH.md) + - Validate auth: userId must be non-empty UUID string, role must be valid + - For each subsequent message (type="message"): + * Immediately send {"type": "typing"} back to client (CHAT-05) + * Normalize message to KonstructMessage via normalize_web_event + * Save user message to web_conversation_messages table + * Build extras dict: conversation_id, portal_user_id, tenant_id + * Dispatch handle_message.delay(msg.model_dump() | extras) + * Subscribe to Redis pub-sub channel webchat_response_key(tenant_id, conversation_id) with 60s timeout + * When response arrives: save assistant message to web_conversation_messages, send {"type": "response", "text": ..., "conversation_id": ...} to WebSocket + - On disconnect: unsubscribe and close Redis connections + c. Create an APIRouter with the WebSocket route for mounting + + 2. Create packages/shared/shared/api/chat.py with REST endpoints: + a. GET /api/portal/chat/conversations?tenant_id={id} — list conversations for the authenticated user within a tenant. For platform_admin: returns conversations across all tenants if no tenant_id. Uses require_tenant_member for RBAC. Returns [{id, agent_id, agent_name, updated_at, last_message_preview}] sorted by updated_at DESC. + b. GET /api/portal/chat/conversations/{id}/messages?limit=50&before={cursor} — paginated message history. Verify caller owns the conversation (same user_id) OR is platform_admin. Returns [{id, role, content, created_at}] ordered by created_at ASC. + c. POST /api/portal/chat/conversations — create or get-or-create conversation. Body: {tenant_id, agent_id}. Uses require_tenant_member. Returns conversation object with id. + d. DELETE /api/portal/chat/conversations/{id} — reset conversation (delete messages, keep row). Updates updated_at. Verify ownership. + All endpoints use Depends(get_portal_caller) and Depends(get_session). Set RLS context var (configure_rls_hook + current_tenant_id.set) before DB queries. + + 3. Update packages/shared/shared/api/__init__.py: add chat_router to imports and __all__ + + 4. Update packages/gateway/gateway/main.py: + - Import chat_router from shared.api and web channel router from gateway.channels.web + - app.include_router(chat_router) for REST endpoints + - app.include_router(web_chat_router) for WebSocket endpoint + - Add comment block "Phase 6 Web Chat routers" + + 5. Update packages/orchestrator/orchestrator/tasks.py: + a. In handle_message: pop "conversation_id" and "portal_user_id" before model_validate (same pattern as placeholder_ts, channel_id). Add to extras dict. + b. In _build_response_extras: add "web" case returning {"conversation_id": extras.get("conversation_id"), "tenant_id": extras.get("tenant_id")}. Note: tenant_id for web comes from extras, not from channel_metadata like Slack. + c. In _send_response: add "web" case that publishes to Redis pub-sub: + ```python + elif channel_str == "web": + conversation_id = extras.get("conversation_id", "") + tenant_id = extras.get("tenant_id", "") + if not conversation_id or not tenant_id: + logger.warning("_send_response: web channel missing conversation_id or tenant_id") + return + response_channel = webchat_response_key(tenant_id, conversation_id) + publish_redis = aioredis.from_url(settings.redis_url) + try: + await publish_redis.publish(response_channel, json.dumps({ + "type": "response", "text": text, "conversation_id": conversation_id, + })) + finally: + await publish_redis.aclose() + ``` + d. Import webchat_response_key from shared.redis_keys at module level (matches existing import pattern for other keys) + + IMPORTANT: WebSocket auth via JSON message after connection (NOT URL params or headers — browser limitation). + IMPORTANT: Redis pub-sub subscribe in WebSocket handler must use try/finally for cleanup (Pitfall 2). + IMPORTANT: The web normalizer must set thread_id = conversation_id (Pitfall 3 — conversation ID scopes memory correctly). + IMPORTANT: For DB access in WebSocket handler, use configure_rls_hook + current_tenant_id context var per existing pattern. + + + cd /home/adelorenzo/repos/konstruct && python -m pytest tests/unit/test_web_channel.py tests/unit/test_chat_api.py -x -v + + + WebSocket endpoint at /chat/ws/{conversation_id} accepts connections, authenticates via JSON message, dispatches to Celery, subscribes to Redis for response. REST API provides conversation CRUD with RBAC. Orchestrator _send_response handles "web" channel via Redis pub-sub publish. All unit tests pass. Gateway mounts both routers. + + + + + + +1. All unit tests pass: `pytest tests/unit/test_web_channel.py tests/unit/test_chat_api.py -x` +2. Migration 008 applies cleanly: `cd /home/adelorenzo/repos/konstruct && alembic upgrade head` +3. Gateway starts without errors: `cd /home/adelorenzo/repos/konstruct/packages/gateway && python -c "from gateway.main import app; print('OK')"` +4. Full test suite still green: `pytest tests/unit -x` + + + +- ChannelType includes WEB +- WebSocket endpoint exists at /chat/ws/{conversation_id} +- REST API at /api/portal/chat/* provides conversation CRUD with RBAC +- _send_response in tasks.py handles "web" channel via Redis pub-sub +- web_conversations and web_conversation_messages tables created with RLS +- All 7+ unit tests pass covering CHAT-01 through CHAT-05 + + + +After completion, create `.planning/phases/06-web-chat/06-01-SUMMARY.md` + diff --git a/.planning/phases/06-web-chat/06-02-PLAN.md b/.planning/phases/06-web-chat/06-02-PLAN.md new file mode 100644 index 0000000..6b0d521 --- /dev/null +++ b/.planning/phases/06-web-chat/06-02-PLAN.md @@ -0,0 +1,325 @@ +--- +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` + diff --git a/.planning/phases/06-web-chat/06-03-PLAN.md b/.planning/phases/06-web-chat/06-03-PLAN.md new file mode 100644 index 0000000..d049b91 --- /dev/null +++ b/.planning/phases/06-web-chat/06-03-PLAN.md @@ -0,0 +1,119 @@ +--- +phase: 06-web-chat +plan: 03 +type: execute +wave: 2 +depends_on: ["06-01", "06-02"] +files_modified: [] +autonomous: false +requirements: + - CHAT-01 + - CHAT-02 + - CHAT-03 + - CHAT-04 + - CHAT-05 + +must_haves: + truths: + - "End-to-end chat works: user sends message via WebSocket, receives LLM response" + - "Conversation history persists and loads on page revisit" + - "Typing indicator appears during response generation" + - "Markdown renders correctly in agent responses" + - "RBAC enforced: operator can chat, but cannot see admin-only nav items" + - "Platform admin can chat with agents across tenants" + artifacts: [] + key_links: [] +--- + + +Human verification of the complete web chat feature. Test end-to-end flow, RBAC enforcement, conversation persistence, and UX quality. + +Purpose: Confirm all CHAT requirements are met before marking Phase 6 complete. +Output: Verified working chat feature. + + + +@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md +@/home/adelorenzo/.claude/get-shit-done/templates/summary.md + + + +@.planning/phases/06-web-chat/06-01-SUMMARY.md +@.planning/phases/06-web-chat/06-02-SUMMARY.md + + + + + + Task 1: Verify end-to-end web chat feature + + + Present the following verification checklist to the user. This is a human verification checkpoint — no code changes needed. + + What was built: + - WebSocket-based real-time chat in the portal at /chat + - Conversation sidebar with agent list, timestamps, message previews + - Message bubbles with markdown rendering and typing indicators + - Full agent pipeline integration (memory, tools, escalation, audit) + - Conversation history persistence in PostgreSQL + - RBAC enforcement (all roles can chat, scoped to accessible tenants) + + Prerequisites: + - Docker Compose stack running (gateway, orchestrator, portal, postgres, redis) + - At least one active agent configured for a tenant + - Migration applied: `alembic upgrade head` + + Test 1 — Basic Chat (CHAT-01, CHAT-05): + 1. Log in to portal as customer_admin + 2. Click "Chat" in the sidebar navigation + 3. Click "New Conversation" and select an AI Employee + 4. Type a message and press Enter + 5. Verify: typing indicator (animated dots) appears immediately + 6. Verify: agent response appears as a left-aligned message bubble + 7. Verify: your message appears right-aligned + + Test 2 — Markdown Rendering (CHAT-05): + 1. Send a message that triggers a formatted response (e.g., "Give me a bulleted list of 3 tips") + 2. Verify: response renders with proper markdown (bold, lists, code blocks) + + Test 3 — Conversation History (CHAT-03): + 1. After sending a few messages, navigate away from /chat (e.g., go to /dashboard) + 2. Navigate back to /chat + 3. Verify: previous conversation appears in sidebar with last message preview + 4. Click the conversation + 5. Verify: full message history loads (all previous messages visible) + + Test 4 — RBAC (CHAT-04): + 1. Log in as customer_operator + 2. Verify: "Chat" link visible in sidebar + 3. Navigate to /chat, start a conversation with an agent + 4. Verify: chat works (operators can chat) + 5. Verify: admin-only nav items (Billing, API Keys, Users) are still hidden + + Test 5 — Full Pipeline (CHAT-02): + 1. If the agent has tools configured, send a message that triggers tool use + 2. Verify: agent invokes the tool and incorporates the result + 3. (Optional) If escalation rules are configured, trigger one and verify handoff message + + Human confirms all 5 test scenarios pass + User types "approved" confirming end-to-end web chat works correctly across all CHAT requirements + + + + + +All 5 test scenarios pass as described above. + + + +- Human confirms end-to-end chat works with real LLM responses +- Conversation history persists across page navigations +- Typing indicator visible during response generation +- Markdown renders correctly +- RBAC correctly scopes agent access +- All three roles (platform_admin, customer_admin, customer_operator) can chat + + + +After completion, create `.planning/phases/06-web-chat/06-03-SUMMARY.md` +