Implement the full AI UX state matrix for the chat feature — thinking, streamed, refusal, error/retry, empty, and feedback capture — and pin each as a Storybook state. The deliverable is the matrix plus a rationale doc that justifies every state against a pattern rubric, not vibes.
Sunil's chat feature technically works: it streams answers. But the support tickets are all about the gaps — "it just froze" (no thinking state), "it said no and I didn't know why" (bare refusal), "I couldn't tell it the answer was wrong" (no feedback path), "the empty screen looked broken" (no empty state). A working happy path is maybe a third of an AI surface; the other two-thirds is the state matrix, and it's where AI UX quality actually lives. This capstone makes you enumerate every state, build each one, and defend each against an established pattern rubric.
~75 minutes hands-on · Ship stage (maps to module M7, playbook PB-51) · $0 — extends the chat build; Storybook is the primary deliverable surface.
From your chat-lab repo. The mock server gains a refusal trigger and a thinking delay — add a route server/index.mjs:
app.post("/api/chat3", async (req, res) => {
const prompt = (req.body?.prompt ?? "").toLowerCase();
res.setHeader("Content-Type", "text/event-stream");
res.flushHeaders();
const emit = (o) => res.write(`data: ${JSON.stringify(o)}\n\n`);
emit({ type: "status", value: "thinking" });
await new Promise((r) => setTimeout(r, 1500)); // visible thinking window
if (prompt.includes("password") || prompt.includes("bomb")) {
emit({ type: "refusal", reason: "safety", message: "I can't help with that request." });
return res.end(emit({ type: "done" }) || undefined);
}
emit({ type: "status", value: "streaming" });
for (const t of "Here is a considered answer to your question.".match(/.{1,3}/g)) {
await new Promise((r) => setTimeout(r, 40));
emit({ type: "text", delta: t });
}
emit({ type: "done" });
res.end();
});
The pattern rubric you audit against, rubric.md (distilled from NN/g AI-UX guidance and common design-system practice):
R1 System status is always visible — the user never wonders "is it working?"
R2 Set expectations — thinking/long-wait states signal that work is happening.
R3 Explain, don't just refuse — a refusal states that it declined and (safely) why,
plus a path forward.
R4 Make errors recoverable — every failure offers a concrete next action (retry/edit).
R5 Respect the empty state — first-run has guidance, not a void.
R6 Close the feedback loop — the user can signal good/bad, and it's captured.
R7 Preserve user effort — partial output, draft input, and history survive interruption.
The state contract, src/chat/chatMachine.ts — a typed enumeration you implement against (skeleton given; you fill the transitions):
export type ChatState =
| { kind: "empty" } // no messages yet
| { kind: "thinking" } // request sent, no tokens yet
| { kind: "streaming"; partial: string } // tokens arriving
| { kind: "complete"; text: string; msgId: string } // done, feedback-able
| { kind: "refusal"; reason: string; message: string }// model declined
| { kind: "error"; message: string; canRetry: true }; // transport/other failure
export type Feedback = { msgId: string; vote: "up" | "down"; note?: string };
state-matrix.md, list all six states and, for each, the three questions the rubric forces: what does the user see, what can they do, and which rubric line(s) does it satisfy? This is a 15-minute writing task done before code — enumerating is the skill; the components are downstream.Empty.Thinking.done, transition to complete with a stable msgId. Complete-state messages must expose the feedback affordances (next step). Stories: Streaming (mid-partial), Complete.Refusal.ErrorRetry.complete message: thumbs up/down; a down-vote opens an optional note; the vote is captured (POST to a /api/feedback stub that logs it, or persist to sessionStorage and show it in the network/console as proof). This is the state most teams skip and the one that feeds the eval loop. Story: Complete with feedback given.StateMatrix.stories.tsx (or a documentation page) showing all seven stories together. Then rationale.md: one paragraph per state mapping it to its rubric line(s), plus a note on the two most-skipped-in-industry states (thinking and feedback) and why their absence generates the support tickets from the scenario. Verify every rubric line R1–R7 is satisfied by at least one state, and every state cites at least one rubric line.Refusal and error look superficially similar — both interrupt the happy path — but conflating them is a real UX failure: an error invites Retry ("try again, it might work"), a refusal must not ("trying again won't change the answer"). Distinct treatments, distinct affordances. This distinction is a common exam and interview probe.
state-matrix.md enumerates all six states with the see/do/rubric triple, written before the components.rationale.md.An AI surface is its state matrix — the happy path is a third of the work, and quality lives in the thinking wait, the humane refusal, the recoverable error, the guided empty screen, and the feedback loop; enumerate every state against a pattern rubric first, because the states you don't design are the ones your users file tickets about.