# Chat Response Placeholder Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Render an accessible assistant loading placeholder immediately after every newly submitted user turn and reconcile it with the authoritative model response. **Architecture:** `useChat` will create a client user message and a client assistant placeholder in one optimistic state update. Response reconciliation will replace both client IDs with server IDs and update the existing assistant object instead of appending a second message. `ChatMessage` will render an empty streaming assistant as an accessible animated-dot indicator. **Tech Stack:** React 19, Next.js 16 App Router, TypeScript, Tailwind CSS, Vitest, Testing Library, minikube/Kubernetes. ## Global Constraints - No API, database, Gateway, FinOps, model-adapter, or persisted-message schema changes. - The accessible pending label is exactly `Модель формирует ответ`. - The placeholder must exist before `/api/chat` resolves. - Authoritative response reconciliation must leave exactly one user message and one assistant message. - Pre-header errors and stops must update the placeholder in place and remain non-retryable. - No new runtime dependencies. --- ### Task 1: Optimistic assistant lifecycle **Files:** - Modify: `hooks/use-chat.ts` - Test: `hooks/use-chat.test.tsx` **Interfaces:** - Consumes: existing `ChatMessage`, `Conversation`, `authFetch`, and client-prefixed optimistic IDs. - Produces: `OptimisticTurn.assistantMessageId: string` and conversations containing `[user, assistant(streaming)]` immediately after submission. - [ ] **Step 1: Write failing optimistic lifecycle tests** Extend the deferred-response test to assert the immediate two-message state: ```tsx expect(result.current.activeConversation?.messages).toMatchObject([ { role: "user", content: "Покажи сразу", status: "complete" }, { role: "assistant", content: "", status: "streaming" }, ]); ``` After resolving the response, retain the exact authoritative IDs assertion: ```tsx expect(result.current.activeConversation?.messages.map(({ id }) => id)).toEqual([ "user-1", "assistant-1", ]); expect(result.current.activeConversation?.messages).toHaveLength(2); ``` Strengthen the pre-header error test to capture the client assistant ID before rejection and assert the same ID becomes `error` rather than a third message being appended. - [ ] **Step 2: Run the hook tests and verify RED** Run: ```bash npm test -- hooks/use-chat.test.tsx ``` Expected: FAIL because the immediate state contains only the user message, and the pre-header error creates a new assistant object after rejection. - [ ] **Step 3: Implement the optimistic assistant and reconciliation** Add `assistantMessageId` to `OptimisticTurn`, create an empty streaming assistant alongside the user message, and append both in the same `commitConversations` update: ```ts const assistantMessage: ChatMessage = { id: optimistic.assistantMessageId, role: "assistant", content: "", createdAt: optimistic.createdAt, status: "streaming", }; messages: [...conversation.messages, userMessage, assistantMessage] ``` In `commitResponseMessage`, replace the optimistic assistant in place: ```ts messages: conversation.messages.map((message) => { if (message.id === optimistic?.userMessageId) { return { ...message, id: userMessageId as string }; } if (message.id === optimistic?.assistantMessageId) { return assistantMessage; } return message; }) ``` For pre-header errors or stops, call `updateMessage` with `optimistic.assistantMessageId`; do not construct or append a second assistant message. - [ ] **Step 4: Run hook tests and verify GREEN** Run: ```bash npm test -- hooks/use-chat.test.tsx ``` Expected: all hook tests PASS, including immediate placeholder, reconciliation, attachments, errors, stops, switching, and preference lifecycle. - [ ] **Step 5: Commit the hook lifecycle** ```bash git add hooks/use-chat.ts hooks/use-chat.test.tsx git commit -m "feat: add optimistic assistant placeholder" ``` --- ### Task 2: Accessible animated pending state **Files:** - Modify: `components/chat/chat-message.tsx` - Test: `components/chat/chat-message.test.tsx` **Interfaces:** - Consumes: an assistant `ChatMessage` with `status: "streaming"` and empty `content`. - Produces: a visible three-dot pending indicator with accessible status text, hidden as soon as content exists. - [ ] **Step 1: Write failing rendering tests** Render an empty streaming assistant and assert: ```tsx expect(screen.getByRole("status", { name: "Модель формирует ответ" })).toBeVisible(); expect(screen.getAllByTestId("response-placeholder-dot")).toHaveLength(3); ``` Render a streaming assistant with `content: "Первый фрагмент"` and assert the label is absent and the text is visible. - [ ] **Step 2: Run the component test and verify RED** Run: ```bash npm test -- components/chat/chat-message.test.tsx ``` Expected: FAIL because the current empty streaming state renders the text `Думаю…` and no status element or dots. - [ ] **Step 3: Implement the loading indicator** Replace the empty-streaming fallback with: ```tsx {message.status === "streaming" && !message.content ? ( {[0, 1, 2].map((index) => ( ))} ) : (
{message.content}
)} ``` Keep copy, retry, stopped, attachment, and download behavior unchanged. - [ ] **Step 4: Run focused chat tests and verify GREEN** Run: ```bash npm test -- components/chat/chat-message.test.tsx components/chat/chat-app.test.tsx hooks/use-chat.test.tsx ``` Expected: all focused tests PASS. - [ ] **Step 5: Commit the rendering change** ```bash git add components/chat/chat-message.tsx components/chat/chat-message.test.tsx git commit -m "feat: show model response placeholder" ``` --- ### Task 3: Verification and minikube rollout **Files:** - Verify only: entire `corp-ui` workspace - Deploy: Kubernetes Deployment `aegida-services/ai-control-chat-ui` **Interfaces:** - Consumes: the committed corp-ui image. - Produces: a Ready minikube pod serving the placeholder behavior through `https://chat.aegida.test:8443`. - [ ] **Step 1: Run full local verification** ```bash npm test npm run lint npx tsc --noEmit npm run build git diff --check ``` Expected: all commands exit 0. If Turbopack cannot bind its sandbox worker port, rerun only `npm run build` with the approved local-worker permission. - [ ] **Step 2: Build and deploy the minikube image** ```bash CHAT_UI_IMAGE="ai-control-chat-ui:minikube-$(git rev-parse --short=7 HEAD)" minikube image build -t "$CHAT_UI_IMAGE" . kubectl -n aegida-services set image deployment/ai-control-chat-ui app="$CHAT_UI_IMAGE" kubectl -n aegida-services rollout status deployment/ai-control-chat-ui --timeout=240s ``` Expected: rollout succeeds with one Ready replica and zero restarts. - [ ] **Step 3: Verify health and browser behavior** ```bash curl -ksS --max-time 10 -w '\nHTTP %{http_code}\n' https://chat.aegida.test:8443/api/health ``` In the in-app browser, submit a unique short prompt and immediately verify the user article, `Модель формирует ответ` status, and stop button are simultaneously visible. Wait for completion and verify the status disappears, a single assistant response remains, and the conversation persists after reload. - [ ] **Step 4: Record final evidence** Report the image tag and digest, pod Ready/restart state, health HTTP status, exact test count, and browser assertions. Leave the verified application tab open for the user.