phase: 124_image_chips_and_answers
All green. Final report: **Phase 124 final verification pass — all criteria verified, no defects found (nothing to fix)** - Verified the implemented phase (all 7 tasks done, changes in working tree): `BOR_ANSWER_IMAGES` toggle, `show_image` tool + refusals + `holder.shown_images`, toggle-gated prompt clause, `answer_image` frame field (None-omitting serializer) + `ChatMessage` persistence, `merge_cited_docs` auto-cite bug fix, answer-figure UI (live/restore/shared/CSS), mock triggers + E2E suite. Also confirmed the bytes route → public change matches the phase-123 shared-chat precedent (task 06 design). - `uv run pytest --cov=app --cov-report=term-missing` → **2848 passed**, TOTAL **99%** (>90% gate) - `bash .agents/validate.sh` → **validation OK** (tests + coverage + ruff + pyright) - `uv run ruff check . && uv run pyright` → clean, 0 errors - `uv run pytest tests/e2e/test_answer_images.py -v --no-cov` (isolated) → **5/5 passed** - Mock-inert spot checks: `tests/e2e/test_image_documents.py` 4/4, `tests/e2e/test_agent_document_tools.py` 4/4 **Completion criteria:** ① zero-read suggested-tier image doc chips with figure — verified (unit matrix, integration `test_zero_read_grounded_turn_auto_cites_suggested_image_doc`, E2E bug-fix flows); ② show_image → answer figure below text/above chips, second call refused, persists + restore + shared — verified (unit handler matrix, integration round-trip, E2E feature flow); ③ default-off byte-identical (3 tools, no clause, no key) — verified (unit + integration + E2E toggle-off); ④ full suite green / >90% / lint clean — verified above; ⑤ commit + phase move — harness responsibility (changes left uncommitted in working tree, no live-infrastructure changes made). **Next pending phase:** none — `todo/` holds only this phase; the pipeline is clear once the harness moves it.
This commit is contained in:
@@ -349,6 +349,50 @@ function addBubbleImage(wrap, src, alt) {
|
||||
bubble.prepend(img);
|
||||
}
|
||||
|
||||
/* The brain answer's image (phase 124, task 06, TODO L3) — the local
|
||||
* copy of the chat page's appendAnswerImage (the per-page duplication
|
||||
* house style: this file keeps its own small copies of the chat
|
||||
* page's message-fragment builders). NON-INTERACTIVE like every other
|
||||
* shared-page fragment (owner-locked zero controls: no link, no
|
||||
* modal, no click handler — a plain <figure>, unlike the chat page's
|
||||
* anchor). The bytes route is PUBLIC (the token is the shared chat's
|
||||
* credential, like the phase-123 user-image contract), so the img
|
||||
* loads for guests exactly as it does for the owner. The visible
|
||||
* caption keeps the document's TITLE: the content endpoint the chat
|
||||
* page fetches for the summary is user-gated (a guest fetch would
|
||||
* just 401), and a caption is ALWAYS visible either way (the WCAG
|
||||
* alt contract holds with the title standing in). A failed load
|
||||
* REMOVES the figure (the collapse rule — never a broken-image
|
||||
* icon). */
|
||||
function addAnswerImage(wrap, ref) {
|
||||
if (
|
||||
!ref ||
|
||||
typeof ref.source !== "string" ||
|
||||
typeof ref.path !== "string" ||
|
||||
typeof ref.image_url !== "string" ||
|
||||
!ref.image_url
|
||||
) {
|
||||
return; // no ref (or a malformed one) → no figure DOM at all
|
||||
}
|
||||
const body = wrap?.querySelector?.(".msg-body");
|
||||
if (!body) return;
|
||||
const title =
|
||||
typeof ref.title === "string" && ref.title ? ref.title : "image";
|
||||
const fig = document.createElement("figure");
|
||||
fig.className = "answer-image";
|
||||
const img = document.createElement("img");
|
||||
img.className = "answer-image-img";
|
||||
img.src = ref.image_url; // the public bytes route
|
||||
img.loading = "lazy";
|
||||
img.alt = title;
|
||||
img.addEventListener("error", () => fig.remove()); // collapse: never a broken icon
|
||||
const caption = document.createElement("span");
|
||||
caption.className = "answer-image-caption";
|
||||
caption.textContent = title; // the visible caption (always)
|
||||
fig.append(img, caption);
|
||||
body.appendChild(fig);
|
||||
}
|
||||
|
||||
/* One stored record through the SAME .msg structure the chat page
|
||||
* uses (pixel-parity with the chat page's restore path): user → the
|
||||
* .msg.user bubble (with the question's attached image when the
|
||||
@@ -356,9 +400,11 @@ function addBubbleImage(wrap, src, alt) {
|
||||
* .msg.brain bubble with the optional
|
||||
* thinking block (restored COLLAPSED — phase 17), the tool lines,
|
||||
* the deflection treatment + the plain-text "Maybe try" chips, the
|
||||
* plain-text source chips, and the stopped note. NO interactive
|
||||
* markup is ever created here — no buttons, no forms, no links, no
|
||||
* click handlers (owner-locked: zero controls). Markdown goes
|
||||
* brain answer's image figure (phase 124, task 06 — a plain
|
||||
* non-interactive <figure>), the plain-text source chips, and the
|
||||
* stopped note. NO interactive markup is ever created here — no
|
||||
* buttons, no forms, no links, no click handlers (owner-locked:
|
||||
* zero controls). Markdown goes
|
||||
* through the global escape-first renderMarkdown (markdown.js): the
|
||||
* stored payloads are raw text, so the renderer's XSS safety applies
|
||||
* unchanged. */
|
||||
@@ -385,6 +431,11 @@ function renderSharedMessage(m) {
|
||||
wrap.classList.add("is-deflected");
|
||||
addMaybeTry(wrap, m.suggestions);
|
||||
}
|
||||
// Phase 124 (task 06, TODO L3): the answer image renders with the
|
||||
// brain bubble — the SAME A4 position as the chat page's render
|
||||
// (below the text, above the chip row). A record without the key
|
||||
// renders byte-identically (no figure DOM).
|
||||
addAnswerImage(wrap, m.answer_image);
|
||||
addSources(wrap, m.sources);
|
||||
if (m.stopped) addStoppedNote(wrap);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user