diff options
| author | Adam Malczewski <[email protected]> | 2026-06-27 20:20:02 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-27 20:20:02 +0900 |
| commit | f5dc22f7c14d6c0dd4bcedee5a85b21ecd294aed (patch) | |
| tree | 8ba78efaefc25148dfb550b4332204822f5acf5d /src/features/chat/ui/ChatView.svelte | |
| parent | fa0bd9c0e433b1abddc814b48a358c94954c7d36 (diff) | |
| download | dispatch-web-f5dc22f7c14d6c0dd4bcedee5a85b21ecd294aed.tar.gz dispatch-web-f5dc22f7c14d6c0dd4bcedee5a85b21ecd294aed.zip | |
feat(vision): resolve persisted image URLs against the API base
Images are now stored on disk under tmp (not SQLite) and served via
GET /images/:conversationId/:imageId. Persisted ImageChunk.url is a compact
relative HTTP path (/images/<conv>/<uuid>.png) instead of a base64 data URL.
No wire/transport-contract type change (behavior only) — re-mirrored the
delta notes.
- New pure resolveImageUrl(url, apiBase) helper (core/chunks/image-url.ts,
+8 tests): data/absolute URLs pass through; relative paths are prepended
with the API base (no double slash; empty base -> root-relative). Exported
from core/chunks + re-exported from features/chat.
- ChatView: new apiBaseUrl prop; <img src> uses resolveImageUrl. The
optimistic echo's data URL passes through; persisted relative paths
resolve against the base. +3 tests.
- AppStore exposes httpBase (getter); App.svelte passes apiBaseUrl into
ChatView and the heartbeat RunModal (also renders image chunks).
Verification: svelte-check 0/0; vitest 959/959 (run twice, +11); biome
clean; vite build OK. See backend-handoff.md §2j-update-2.
Not merged or pushed.
Diffstat (limited to 'src/features/chat/ui/ChatView.svelte')
| -rw-r--r-- | src/features/chat/ui/ChatView.svelte | 19 |
1 files changed, 15 insertions, 4 deletions
diff --git a/src/features/chat/ui/ChatView.svelte b/src/features/chat/ui/ChatView.svelte index 8081951..cd69071 100644 --- a/src/features/chat/ui/ChatView.svelte +++ b/src/features/chat/ui/ChatView.svelte @@ -1,6 +1,6 @@ <script lang="ts"> import type { TurnProviderRetryEvent } from "@dispatch/wire"; - import { groupRenderedChunks, type RenderedChunk, viewProviderRetry } from "../index"; + import { groupRenderedChunks, resolveImageUrl, type RenderedChunk, viewProviderRetry } from "../index"; import { interleaveTurnMetrics, viewCacheRate, @@ -24,6 +24,7 @@ onShowEarlier, thinkingKeyBase = 0, providerRetry = null, + apiBaseUrl = "", }: { chunks: readonly RenderedChunk[]; turnMetrics?: readonly TurnMetricsEntry[]; @@ -44,6 +45,14 @@ * newest attempt + delay, and is cleared when content resumes / turn ends. */ providerRetry?: TurnProviderRetryEvent | null; + /** + * The HTTP API base URL (e.g. `http://localhost:24203`). Persisted image + * chunks carry a compact relative path (`/images/<conv>/<uuid>.png`); this + * base is prepended to render them. The optimistic echo's data URL and any + * absolute URL pass through unchanged (see `resolveImageUrl`). Defaults to + * "" (root-relative — a browser resolves `/images/…` against its origin). + */ + apiBaseUrl?: string; } = $props(); // True while a show-earlier page-in is awaited (disables the button). @@ -96,15 +105,17 @@ {#snippet chunkRow(rendered: RenderedChunk)} {#if rendered.role === "user"} <!-- User: a speech bubble, left-aligned. A user message may be multi-chunk - ([text, image, image, …]); each chunk renders in its own bubble (the - image's url is a base64 data URL or an https URL — render it directly). --> + ([text, image, image, …]); each chunk renders in its own bubble. A + persisted image chunk's url is a compact relative path (`/images/…`) + served by the backend — resolve it against the API base. The + optimistic echo's data URL (and any absolute URL) passes through. --> <div class="chat chat-start"> <div class="chat-bubble chat-bubble-primary"> {#if rendered.chunk.type === "text"} <p>{rendered.chunk.text}</p> {:else if rendered.chunk.type === "image"} <img - src={rendered.chunk.url} + src={resolveImageUrl(rendered.chunk.url, apiBaseUrl)} alt={rendered.chunk.mimeType ?? "pasted image"} loading="lazy" decoding="async" |
