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:
@@ -267,6 +267,24 @@
|
||||
(role=alert). -->
|
||||
<p class="char-count" id="char-count" hidden></p>
|
||||
|
||||
<!-- Phase 123 (task 02, TODO L6): the attach PREVIEW STRIP —
|
||||
the selected image (thumbnail ≤48px + the filename + a
|
||||
remove X) ABOVE the input row: the attachment is part of
|
||||
the question being typed. `hidden` by default — app.js
|
||||
reveals it only while a file is attached (attachedImage)
|
||||
and clears it with the send (the strip must not linger
|
||||
into the turn). With no pick — and with the images flag
|
||||
off — it is never visible (A5's default-off contract).
|
||||
The thumbnail is decorative (alt="" — the filename is
|
||||
the visible, readable label beside it). -->
|
||||
<div class="attach-preview" id="attach-preview" hidden>
|
||||
<img class="attach-preview-img" alt="">
|
||||
<span class="attach-preview-name"></span>
|
||||
<button type="button" class="attach-preview-remove" id="attach-remove" aria-label="Remove the attached image">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Composer (phase 48, 2026-08-29, TODO.md L3): one button, two
|
||||
roles — #send-btn reads "Send" when idle and morphs into the
|
||||
enabled "Stop" control (.is-stop, rose treatment) while a turn
|
||||
@@ -284,6 +302,24 @@
|
||||
`!text` guard in app.js is the real empty-input check (same
|
||||
precedent as the tuning form's noValidate). -->
|
||||
<form class="composer" id="composer" novalidate>
|
||||
<!-- Phase 123 (task 02, TODO L6): the attach control — the
|
||||
paperclip glyph button LEFT of the input (the icon style
|
||||
of the other composer glyphs; aria-label is its
|
||||
accessible name, the SVG is decorative). HIDDEN BY
|
||||
DEFAULT — app.js reveals it only when the boot
|
||||
/api/config says `images: true` (the brand boot's ONE
|
||||
config request — no second round-trip); flag off (the
|
||||
default) keeps the button hidden for good, so the
|
||||
flag-off DOM is byte-identical to pre-phase (A5's
|
||||
default-off contract). The hidden #attach-file input is
|
||||
its backend (a programmatic .click() — the native
|
||||
picker); accept mirrors the server's six-extension gate
|
||||
(the client re-checks the name's extension before any
|
||||
state change; the upload endpoint is the authority). -->
|
||||
<button type="button" class="attach-btn" id="attach-btn" hidden aria-label="Attach an image">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
||||
</button>
|
||||
<input type="file" id="attach-file" accept="image/png,image/jpeg,image/webp,image/gif,image/bmp" hidden>
|
||||
<label class="visually-hidden" for="message-input">Ask Brain of Reese a question</label>
|
||||
<!-- maxlength=4000 mirrors ChatRequest.message max_length=4000
|
||||
(app/schemas.py) — the server 422s beyond; the #char-count
|
||||
|
||||
Reference in New Issue
Block a user