phase: 130_chat_answer_clusters
Build and Push Containers / build-and-push-app (push) Successful in 2m27s
Build and Push Containers / build-and-push-db (push) Successful in 12s

**Phase 130 — final verification pass: 100% green, complete.**

- **Verified:** chips-only row (no chip for image docs) → images-only row (`.msg-images`) → related row → `.msg-actions` last; `getActionsRow` used by all 3 buttons + stopped/failed notes; `markLastRetryable` save-re-append hack gone; in-place chip fallback on image-load failure; CSS right-align with per-button auto margins removed — implementation matches the design in every detail.
- **Completion criteria:** (1) mixed citation renders chips→images→actions-last, DOM-asserted ✅ (2) all 3 buttons in one right-aligned `.msg-actions`; non-admin sees Retry-only; restore path identical ✅ (3) 404-bytes figure → plain chip, unit-pinned ✅ (4) E2E isolated ✅ (5) no behavior change (modal affordances, related tier, ARIA names) ✅
- **Tests/lint/coverage:** `uv run pytest tests/e2e/test_chat_answer_clusters.py -v --no-cov` → 4 passed; `uv run pytest --cov=app --cov-report=term-missing` → 3061 passed, **99%** app coverage; the 4 E2E files this phase modified (test_answer_images, test_image_documents, test_pdf_support, test_responsive_polish) → 22 passed in isolation; `uv run ruff check .` clean, `uv run pyright` → 0 errors.
- **Docs:** README.md — verified consistent in full (page table vs routes, env-var coverage vs `.env.example`, phase-130 cluster wording already in tree); no changes needed. AGENTS.md — fixed one pre-existing inconsistency: rule 5 still cited the retired "centered 46rem chat column"; now states the 72rem container contract (phase 100).
- **Decisions:** PLAN.md §7.1 still says 46rem, but it's owner-managed (phase 100 records the supersession pending owner sign-off) and out of my editable scope — AGENTS.md now cross-references the supersession.
- **Next pending phase:** `131_read_line_ranges` (todo/).
This commit is contained in:
2026-09-28 07:49:37 -04:00
parent 2d0f608246
commit c88c65641a
46 changed files with 3266 additions and 351 deletions
@@ -0,0 +1,36 @@
# Phase 130 — Chat answer clusters: chips row, images row, bottom-right actions cluster
**Source:** `TODO.md` L6 — "The chat output is getting messy. I think it's because the `source-chip`, `source-image`, `tune-btn`, `retry-btn`, and `save-as-doc-btn` end up in the same div which causes them to push each other in weird ways. Those three buttons should be in a button cluster in the bottom right. We don't need to display the `source-chip` for images, just the image itself is fine. `source-chips` should show up before images in their own cluster, then images in their own cluster to keep things organized."
**Story:** n/a (TODO-derived phase, Protocol B append).
**Context:** `frontend/assets/app.js` — `appendSources` (~L1561: builds the FIRST `.msg-meta` row, interleaving a `.source-image` figure after each chip for refs carrying `image_url`), `appendRelated` (~L1618: the "Nearby docs" row, appended LAST among meta rows), `appendSourceImageFigure` (~L1660: the compact figure; on `img` error it removes the figure and the chip remains), `appendAnswerImage` (~L1735: the `.answer-image` figure — appended by callers right BEFORE `appendSources`), `appendTuneButton` (~L484, admin-gated), `appendRetryButton` (~L598), `appendSaveAsDocButton` (~L793, admin + docs-repo gated; `margin-inline-start: auto` pushes it right), `markLastRetryable` (~L620: re-appends the Retry button on the last bubble and re-appends the save button after it — "the auto margins split the free space"), `appendStoppedNote` / `appendFailedNote` (~L520/~L560: join the first `.msg-meta` row); `frontend/assets/styles.css` — the `.msg-meta`, `.source-chip`, `.source-image`, `.tune-btn`, `.retry-btn`, `.save-as-doc-btn` rules (grep each — the auto-margin rules live here); `frontend/index.html` — no chat markup changes expected (the bubble DOM is JS-built); E2E image-KB fixture patterns in `tests/e2e/test_chat_image_questions.py` / `test_image_documents.py` (the mock's `IMAGE_DESCRIPTION_MODE` canned vision reply makes image docs retrievable).
## Objective
A completed brain bubble's metadata reads as organized clusters, top to bottom: the answer text → the answer figure (phase 124, unchanged) → the **source chips row** (text documents only — an image document gets NO chip, just its figure) → the **source images row** (all retrieved image figures) → the "Nearby docs" row (unchanged) → the **actions row** (a right-aligned cluster holding Tune / Retry / Save-as-doc and the Stopped/Failed notes) — never one soup div where buttons push chips around.
## Dependencies
- `129_sync_chunk_progress` (todo) — pipeline order only; code dependencies are the completed chat-render contract (phases 26/49/59/113/120/122/124).
## Design (shared by all tasks — the executor reads this, not the chat)
- **Row structure per brain bubble (top → bottom):** (1) answer text; (2) `.answer-image` figure — the phase-124 position is UNCHANGED (below the text, before the source rows — A3); (3) `.msg-meta` chips row (`role=list`, `aria-label="Sources"`) — one `.source-chip` per NON-image ref; refs with `image_url` contribute NO chip (ASSUMPTION A3 — the figure is the citation for image docs); (4) `.msg-meta.msg-images` images row (`role=list`, `aria-label="Source images"`) — one `.source-image` figure per image ref, in ref order; the row is absent when there are no image refs; (5) `.msg-meta.related-docs` — the phase-113 row, UNCHANGED in content, placed after the images row (ASSUMPTION A4); (6) `.msg-meta.msg-actions` actions row (`role=list`, `aria-label="Answer actions"`) — the ONLY row the action buttons + the stopped/failed notes join (ASSUMPTION A7).
- **Call order at every render site** (the `done` branch, the empty-answer fallback, the stop finalize, the phase-14 restore path): answer image → `appendSources` (now builds the chips row AND the images row) → `appendRelated` → the action buttons/notes. The current "buttons claim the FIRST `.msg-meta` row" behavior is REPLACED — the buttons get-or-create `.msg-actions` instead.
- **Image-load failure (ASSUMPTION A3):** `appendSourceImageFigure`'s `img` error handler replaces the figure IN PLACE with a plain `.source-chip` anchor for that document (same `documentUrl` href, same modal click behavior, the label as text) — the citation survives a lost byte copy; no broken-image icon.
- **Actions row:** one get-or-create `.msg-actions` per bubble (the house pattern). DOM order = Tune, Retry, Save-as-doc (each present per its gate — non-admins see only Retry; a stopped bubble sees the note + Retry). The note (`appendStoppedNote` / `appendFailedNote`) joins this row. `markLastRetryable`'s re-append logic moves the Retry button into `.msg-actions` and DROPS the save-button re-append hack (row-level alignment replaces the per-button `margin-inline-start: auto` — CSS task). ARIA: `role=list` + `role=listitem` on every child (the existing convention).
- **CSS:** `.msg-actions { justify-content: flex-end; }` (the right-aligned cluster — "bottom right" = the bubble's last row, right edge); remove the per-button auto margins (`.save-as-doc-btn`'s `margin-inline-start: auto` + the tune/retry auto margins the `markLastRetryable` comment names); the chips row keeps its wrap; `.msg-images` wraps with the `.msg-meta` gap; the row ORDER is DOM order (no positioning tricks); mobile: rows stack naturally, the actions row stays right-aligned.
## Tasks
1. `01_sources_clusters.md` — `appendSources`/figure split: chips-only row + images-only row, no chip for image docs, the in-place chip fallback.
2. `02_actions_cluster.md` — the `.msg-actions` row: the three buttons + the notes, `markLastRetryable` rework, ARIA kept.
3. `03_css_clusters.md` — the cluster CSS (actions right-aligned, auto margins removed, images row).
4. `04_e2e_clusters.md` — dedicated Playwright E2E, run in isolation.
## Testing & Quality
- Unit: the frontend unit suite — the pinned behaviors that move (the button-row target, the no-chip-for-images rule, the fallback) — inspect `tests/unit/test_frontend_*.py` for the chat-render pins and update them.
- E2E: one dedicated Playwright file, run in isolation, image-KB fixture (text doc + image doc both cited).
- Coverage: **>90%** on new/modified code (frontend: unit pins + E2E; `app/` untouched this phase).
## Completion Criteria
- [ ] a bubble citing a text doc + an image doc renders: chips row (text chip only, NO chip for the image) → images row (the figure) → actions row (bottom right) — asserted in the DOM, not the pixels
- [ ] Tune/Retry/Save-as-doc sit in ONE `.msg-actions` row, right-aligned; non-admin sees only Retry; the restore path renders the identical structure
- [ ] an image whose bytes 404 renders as its plain chip (the citation survives)
- [ ] `uv run pytest tests/e2e/test_chat_answer_clusters.py -v --no-cov` green in isolation
- [ ] no behavior change in completed phases (chips still open the modal, the related row, the answer figure's position, the ARIA names)