Files
brain-of-reese/.agent/user_stories/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

3.2 KiB
Raw Blame History

Story: Loading Feedback & Progress

Phase: 06_story_loading_feedback.md · E2E: tests/e2e/test_loading_feedback.py

Narrative

As a user, local LLM answers can take 10–30+ seconds. I want to always know Brain is working — a clear "thinking" state, live progress as tokens arrive, and a definitive end — so I never stare at a stale Send button wondering if it's stuck.

  • Given I submit a question
  • When the answer is in flight (pre-token, streaming, or erroring)
  • Then the UI shows an unambiguous in-progress state, transitions cleanly to done/error, and the send button is never left in a zombie state.

Acceptance criteria

  1. Pre-token: typing-indicator bubble (3 animated dots, role="status", aria-label="Brain of Reese is thinking") + send button disabled with spinner and label "Thinking…".
  2. Streaming: first delta replaces the typing indicator; text appends live; button stays busy until done.
  3. Done: button re-enabled, label "Send", input focused back.
  4. Error paths: (a) LLM/DB error → red banner role="alert" with retry hint, button re-enabled; (b) 120s client timeout → same error state (guard against a hung stream); (c) page reload mid-stream loses the stream but the composer is usable again (state is turn-local).
  5. Slow-model E2E: the mock LLM's 3s warm-up (message containing "pretend to think slowly") must show the typing indicator for ≥2s before any text appears.
  6. Server side: per-turn log includes embed_ms / total total_ms (PLAN §9) so "slow" is diagnosable.
  7. prefers-reduced-motion: dots/spinner still visible (slower/static) — feedback is never removed, only calmed.

UI Visualization & Structure

  • State machine (single source of truth in app.js): idle → thinking → streaming → done | error → idle.
  • Typing indicator: 8px dots, --ink-soft, staggered 1.2s bounce; inside a normal brain bubble (same geometry as answers) so the layout doesn't jump.
  • Send button busy style: background: #a5b4fc (disabled contrast still fine — it's a disabled state), 16px spinner (2.5px ring, white top arc), label swap "Send" ↔ "Thinking…".
  • Error banner: --err-bg/--err-ink/--err-line, top of chat shell, role="alert", includes the actionable hint ("Try again — if this persists, check the LLM is reachable").
  • Elapsed-time hint: after 10s still pre-token, the typing bubble's aria label becomes "…still thinking (12s)" — SR users are never left guessing.

Playwright Mapping Rule

Test Scenario → tests/e2e/test_loading_feedback.py (mock LLM):

  1. test_typing_indicator_during_slow_think — ask "pretend to think slowly then tell me about kubernetes"; assert #typing-indicator visible within 500ms of submit, still visible at ~2s, gone by the time the answer text is present.
  2. test_button_state_machine — during the in-flight turn: #send-btn disabled + label "Thinking…"; after done: enabled + "Send".
  3. test_streaming_appends_live — capture bubble text at two timestamps during the stream; second length > first (progress is visible).
  4. test_error_banner_on_llm_down (fixture stops the mock) — submit; assert role=alert banner visible and button re-enabled within timeout.