Files
brain-of-reese/.agent/phases/todo/06_story_loading_feedback.md
T
ducoterra 022da8e2bc feat: scaffold Brain of Reese — FastAPI RAG chat over Postgres 17 + pgvector
Foundation (phase 01, verified):
- FastAPI app: /api/health, /api/suggestions, /api/chat (placeholder),
  static frontend served locally (no CDN)
- Postgres 17 + pgvector via db/Containerfile + compose.yaml
  (podman compose up -d db), Alembic initial migration (documents,
  chunks with vector(768), query_log)
- LLM client targeting https://aipi.reeseapps.com/v1 (turbo/embed);
  scripts/llm_probe.py verified models + 768-dim embeddings live
- Conditional debugpy: imported only when DEBUGPY=1 (attach on demand,
  :5678); logging config for clean single-line logs
- Frontend shell: mobile-first chat + Sources pages, tokens, a11y baselines
- Tests: 24 unit+integration (99% coverage on app/), ruff + pyright clean,
  Playwright smoke E2E (3 tests) against a deterministic mock LLM
- Planning: .agent/PLAN.md (architecture + LOCKED decisions), AGENTS.md,
  6 user stories, 7 phase files (one story / one phase / one Playwright
  suite each)
2026-08-21 13:42:21 -04:00

2.7 KiB

Phase 06 — Story: Loading Feedback & Progress

Story: .agent/user_stories/loading-feedback.md Context: .agent/PLAN.md §7.4 ("never stale" contract), §9

Goal

An unambiguous state machine — idle → thinking → streaming → done | error → idle — so the user always knows what's happening, and a stale Send button is impossible.

Implementation steps

  1. app.js — formalize the state machine (single setUiState(state) function driving: typing indicator, send button disabled/spinner/label, #send-status live text). Replace ad-hoc busy handling from Phase 03.
  2. Pre-token: typing indicator (role="status", aria-label="Brain of Reese is thinking"); after 10s pre-token, update the label with elapsed seconds (setInterval, cleared on state change).
  3. Streaming: first delta removes the typing indicator and starts appending to the answer bubble; button stays busy.
  4. Error paths: {"type":"error"} SSE event, non-2xx response, or 120s client-side timeout (clear on first delta) → red banner role="alert" ("Try again — if this persists, check the LLM is reachable") + state → idle.
  5. prefers-reduced-motion: CSS already slows animations — verify; add a static fallback for the dots if needed.
  6. Server: confirm the per-turn log line includes embed_ms and total_ms (add if Phase 03 omitted it).

UI Verification

Walk the full state machine by hand (dev server + mock LLM slow path): submit → indicator + "Thinking…" disabled button → live tokens → done (enabled, focused input). Kill the mock mid-stream → banner + recovery. Contrast of disabled button + spinner OK; reduced-motion pass.

Testing & Quality

  • Unit/integration: SSE error event serialization; timeout constant exported/testable; (JS logic is E2E-covered).
  • Coverage: >90% on app/.

Playwright Execution Phase

Run ONLY this story's suite:

uv run pytest tests/e2e/test_loading_feedback.py -v --no-cov

Implements the story mapping (mock LLM's 3s "pretend to think slowly" warm-up + a fixture that stops the mock): typing indicator visible during pre-token and gone by answer; button disabled→"Thinking…"→enabled "Send"; streaming appends (two-timestamp length check); LLM-down ⇒ role=alert banner + button recovered.

Success criteria

  • every in-flight state has a visible indicator; button never zombies
  • error + 120s timeout paths both recover cleanly
  • reduced-motion respected
  • unit + integration green, coverage >90%
  • story E2E green in isolation
  • committed

Commit

git add -A && git commit --no-gpg-sign -m "feat(ui): explicit chat state machine — typing indicator, streaming progress, timeout and error recovery"