BXUP Track progress & test yourself — free
Frontend & Mobile · Hands-on lab · ~4 min read
FRONTEND & MOBILE · HANDS-ON LAB

Lab 14 · Capstone 5 — AI UX State Matrix

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.

Time & tier

~75 minutes hands-on · Ship stage (maps to module M7, playbook PB-51) · $0 — extends the chat build; Storybook is the primary deliverable surface.

Setup

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 };

Guided steps

  1. Enumerate before building. In 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.
  2. Build the empty state (R5). First render, no messages: a purposeful empty state — a one-line explanation of what the assistant does and 2–3 example prompts the user can click to send. Not a blank panel, not a lone input. Story: Empty.
  3. Build the thinking state (R1, R2). Between send and first token (the mock gives you a 1500ms window), show an explicit working indicator that is distinct from streaming — an animated "Thinking…" affordance, not a bare spinner that could mean anything, and not silence. Story: Thinking.
  4. Wire the streaming and complete states (R1, R7). Stream text (reuse Lab 10/11 transport). On done, transition to complete with a stable msgId. Complete-state messages must expose the feedback affordances (next step). Stories: Streaming (mid-partial), Complete.
  5. Build the refusal state (R3). Send "reset my password" to trigger the refusal event. A refusal is not an error and must not look like one: render it as a calm, distinct treatment that says the assistant declined, gives a safe reason ("safety"), and offers a path forward ("try rephrasing" / a help link). Log the design distinction between refusal and error in the rationale doc. Story: Refusal.
  6. Build the error + retry state (R4, R7). Force a transport failure (stop the server mid-request). Show a designed error with a Retry that re-sends the same prompt, and confirm the user's typed input isn't lost if they were mid-compose. Story: ErrorRetry.
  7. Build the feedback capture (R6). On every 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.
  8. Assemble the matrix and write the rationale. A 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.
Note

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.

Verify

  • state-matrix.md enumerates all six states with the see/do/rubric triple, written before the components.
  • Seven Storybook stories render distinctly: Empty, Thinking, Streaming, Complete, Refusal, ErrorRetry, and Complete-with-feedback.
  • Live: empty state on load; thinking visible for the 1500ms window; refusal on the trigger prompt looks distinct from the error state; error offers working Retry; feedback down-vote captures a note (provable in console/network/storage).
  • Every rubric line R1–R7 is covered by at least one state, and the coverage is explicit in rationale.md.
  • Refusal and error are visually and behaviorally distinct (refusal has no Retry; error does).

Self-grading rubric (pass = 7/10)

  • Enumeration-first (0–2): matrix written before code with the see/do/rubric triple per state = 2.
  • State completeness (0–4): all six states built and pinned in Storybook, each visually distinct = 4 (−1 per missing/indistinct state).
  • Refusal ≠ error (0–2): the two are distinct in look and affordances, with the distinction justified = 2.
  • Feedback loop (0–2): capture works and is provably persisted/sent, with the eval-loop rationale stated = 2.
▸Answer key — the state matrix and the two commonly-skipped statesoptional deep dive — click to expand

There is no seeded-bug hunt here — this capstone grades completeness against the rubric, and the answer key is the reference matrix plus the two states that separate a real AI surface from a demo.

Reference matrix (state → user sees → user does → rubric):

  • Empty (R5): purpose line + clickable example prompts. Does: pick an example or type. The absence of this is the "looked broken on first load" ticket.
  • Thinking (R1, R2): distinct "Thinking…" affordance during the pre-token wait. Does: wait, or cancel. Must be distinguishable from streaming and from a generic spinner.
  • Streaming (R1, R7): progressive text, a Stop control, preserved partial on interrupt. Does: read, stop.
  • Complete (R1, R6): finished text with feedback affordances and a stable msgId. Does: read, copy, vote, regenerate.
  • Refusal (R3): calm distinct treatment — declined + safe reason + path forward, no Retry. Does: rephrase, get help. Must not be styled as an error.
  • Error/Retry (R4, R7): designed failure + Retry that re-sends + preserved composer input. Does: retry, edit. Must offer Retry (unlike refusal).

The two most-skipped states — and why the scenario's tickets map to them:

  • Thinking is skipped because on a fast dev machine the pre-token wait is invisible; under real latency it's seconds of apparent freeze → the "it just froze" tickets. R1/R2 exist precisely for the case developers don't feel locally.
  • Feedback capture is skipped because it's invisible to the happy path and "not a feature" — but it's the input to the eval loop that improves the model surface over time, and its absence is both a UX dead-end ("couldn't tell it it was wrong") and a data gap. R6 is the state with the highest long-term leverage and the lowest first-day salience.

Grading: full marks require (1) all six states distinct in Storybook, (2) the refusal/error distinction implemented and justified (no Retry on refusal is the objective tell), and (3) feedback provably captured — a thumbs button that does nothing is a false state. A rationale doc that maps each state to a rubric line and calls out thinking + feedback as the leverage states demonstrates the judgment this whole track is certifying.

Key Takeaway:

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.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in