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)
3.2 KiB
3.2 KiB
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
- 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…". - Streaming: first delta replaces the typing indicator; text appends
live; button stays busy until
done. - Done: button re-enabled, label "Send", input focused back.
- 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). - 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.
- Server side: per-turn log includes
embed_ms/ totaltotal_ms(PLAN §9) so "slow" is diagnosable. 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):
test_typing_indicator_during_slow_think— ask "pretend to think slowly then tell me about kubernetes"; assert#typing-indicatorvisible within 500ms of submit, still visible at ~2s, gone by the time the answer text is present.test_button_state_machine— during the in-flight turn:#send-btndisabled + label "Thinking…"; after done: enabled + "Send".test_streaming_appends_live— capture bubble text at two timestamps during the stream; second length > first (progress is visible).test_error_banner_on_llm_down(fixture stops the mock) — submit; assertrole=alertbanner visible and button re-enabled within timeout.