phase: 123_chat_image_questions
All gates green. Verification complete. **Phase 123 — final verification pass (all 4 tasks already in `complete/`)** - Verified the full implementation is in the working tree: `app/api/chat_images.py` (upload/serve pair), `ChatRequest.image`/`ChatMessage.image` (path-validated, omitted-when-None), toggle-off + stale-file hinted error frames, `build_user_content` multimodal build at both sites (chat.py deflected branch + `run_agent`), config-gated composer attach/preview/upload-then-send, restore + shared rendering, CSP `img-src 'self' data:` carve-out, mock-LLM capture buffer. - `uv run pytest` → **2796 passed**, exit 0 (unit + integration). - `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (29/4615 missed; phase-123 modules 99–100%). - `uv run pytest tests/e2e/test_chat_image_questions.py -v --no-cov` → **5 passed** in isolation. - `uv run ruff check . && uv run pyright` → clean (0 errors). **Completion criteria:** (1) attach→send→multimodal text+image to the model, bubble/reload/shared all render it, saved chat stores the PATH with `"base64" not in json.dumps(stored)` — **verified** (E2E tests 1–4 + integration round-trip); (2) `BOR_IMAGES=false` — control hidden, exact hinted error frame, zero model calls / no query_log row — **verified** (E2E test 5 + integration); (3) text-only byte-identical (`content` stays a plain `str`) — **verified** (unit + integration); (4) all gates green — **verified**; (5) commit + phase move — left to the harness per pipeline rules (no `git add`/`commit` run). No defects found; no live-infrastructure changes (repo + local dev DB only). **Next pending phase: none** — 123 is the last phase in `todo/`.
This commit is contained in:
@@ -318,9 +318,42 @@ function addStoppedNote(wrap) {
|
||||
meta.appendChild(note);
|
||||
}
|
||||
|
||||
/* The question's attached image (phase 123, task 03, TODO L6) — the
|
||||
* local copy of the chat page's attachBubbleImage (the per-page
|
||||
* duplication house style: this file keeps its own small copies of
|
||||
* the chat page's message-fragment builders). The SAME .msg-image
|
||||
* treatment the chat page uses — styles.css is shared by both pages,
|
||||
* so the rule needs no second copy: the img at the TOP of the user
|
||||
* bubble (the attachment is part of the question), lazy, alt = the
|
||||
* record's text or the fallback. The load failure degrades IDENTICALLY
|
||||
* to the chat page: the img is replaced by the small "image
|
||||
* unavailable" line (the stored file was deleted out-of-band — the
|
||||
* record keeps its path, the render degrades; never a broken icon).
|
||||
* The serve route is public (the token is the shared chat's
|
||||
* credential, like saved-chat content), so the img loads for guests
|
||||
* exactly as it does for the owner. */
|
||||
function addBubbleImage(wrap, src, alt) {
|
||||
const bubble = wrap?.querySelector?.(".bubble");
|
||||
if (!bubble) return;
|
||||
const img = document.createElement("img");
|
||||
img.className = "msg-image";
|
||||
img.src = src;
|
||||
img.alt = alt || "attached image";
|
||||
img.loading = "lazy";
|
||||
img.onerror = () => {
|
||||
const note = document.createElement("span");
|
||||
note.className = "msg-image-unavailable";
|
||||
note.textContent = "image unavailable";
|
||||
img.replaceWith(note);
|
||||
};
|
||||
bubble.prepend(img);
|
||||
}
|
||||
|
||||
/* 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; brain → the .msg.brain bubble with the optional
|
||||
* .msg.user bubble (with the question's attached image when the
|
||||
* record carries the stored path — phase 123, task 03); brain → the
|
||||
* .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
|
||||
@@ -331,7 +364,16 @@ function addStoppedNote(wrap) {
|
||||
* unchanged. */
|
||||
function renderSharedMessage(m) {
|
||||
if (m.who === "user") {
|
||||
addSharedMessage("user", renderMarkdown(m.text));
|
||||
const wrap = addSharedMessage("user", renderMarkdown(m.text));
|
||||
// Phase 123 (task 03, TODO L6): the question's attached image —
|
||||
// the record's `m.image` carries the STORED PATH (A5: never
|
||||
// base64; a pre-phase record has no key at all, so it renders
|
||||
// byte-identically — no img). The public image route makes the
|
||||
// shared view faithful: the SAME bubble treatment, alt, and
|
||||
// load-failure degradation as the chat page's restore.
|
||||
if (typeof m.image === "string" && m.image) {
|
||||
addBubbleImage(wrap, m.image, m.text || "attached image");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const wrap = addSharedMessage("brain", renderMarkdown(m.text));
|
||||
|
||||
Reference in New Issue
Block a user