**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/).
6.9 KiB
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-imagefigure — the phase-124 position is UNCHANGED (below the text, before the source rows — A3); (3).msg-metachips row (role=list,aria-label="Sources") — one.source-chipper NON-image ref; refs withimage_urlcontribute NO chip (ASSUMPTION A3 — the figure is the citation for image docs); (4).msg-meta.msg-imagesimages row (role=list,aria-label="Source images") — one.source-imagefigure 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-actionsactions 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
donebranch, 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-metarow" behavior is REPLACED — the buttons get-or-create.msg-actionsinstead. - Image-load failure (ASSUMPTION A3):
appendSourceImageFigure'simgerror handler replaces the figure IN PLACE with a plain.source-chipanchor for that document (samedocumentUrlhref, 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-actionsper 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-actionsand DROPS the save-button re-append hack (row-level alignment replaces the per-buttonmargin-inline-start: auto— CSS task). ARIA:role=list+role=listitemon 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'smargin-inline-start: auto+ the tune/retry auto margins themarkLastRetryablecomment names); the chips row keeps its wrap;.msg-imageswraps with the.msg-metagap; the row ORDER is DOM order (no positioning tricks); mobile: rows stack naturally, the actions row stays right-aligned.
Tasks
01_sources_clusters.md—appendSources/figure split: chips-only row + images-only row, no chip for image docs, the in-place chip fallback.02_actions_cluster.md— the.msg-actionsrow: the three buttons + the notes,markLastRetryablerework, ARIA kept.03_css_clusters.md— the cluster CSS (actions right-aligned, auto margins removed, images row).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_*.pyfor 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-actionsrow, 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-covgreen in isolation- no behavior change in completed phases (chips still open the modal, the related row, the answer figure's position, the ARIA names)