# 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.