summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--src/features/chat/ui/ChatView.svelte24
-rw-r--r--src/features/chat/ui/ChatView.test.ts17
2 files changed, 33 insertions, 8 deletions
diff --git a/src/features/chat/ui/ChatView.svelte b/src/features/chat/ui/ChatView.svelte
index f34de9a..b473766 100644
--- a/src/features/chat/ui/ChatView.svelte
+++ b/src/features/chat/ui/ChatView.svelte
@@ -78,6 +78,7 @@
// True while a show-earlier page-in is awaited (disables the button).
let loadingEarlier = $state(false);
+ let failedImages = $state<string[]>([]);
async function showEarlier() {
if (!onShowEarlier || loadingEarlier) return;
@@ -161,13 +162,22 @@
{#if rendered.chunk.type === "text"}
<p>{rendered.chunk.text}</p>
{:else if rendered.chunk.type === "image"}
- <img
- src={resolveImageUrl(rendered.chunk.url, apiBaseUrl)}
- alt={rendered.chunk.mimeType ?? "pasted image"}
- loading="lazy"
- decoding="async"
- class="max-h-80 max-w-full rounded"
- />
+ {#if failedImages.includes(rendered.chunk.url)}
+ <span class="text-sm opacity-70">image expired</span>
+ {:else}
+ <img
+ src={resolveImageUrl(rendered.chunk.url, apiBaseUrl)}
+ alt={rendered.chunk.mimeType ?? "pasted image"}
+ loading="lazy"
+ decoding="async"
+ onerror={() => {
+ if (rendered.chunk.type === "image" && rendered.chunk.url.startsWith("/images/")) {
+ failedImages = [...failedImages, rendered.chunk.url];
+ }
+ }}
+ class="max-h-80 max-w-full rounded"
+ />
+ {/if}
{/if}
</div>
</div>
diff --git a/src/features/chat/ui/ChatView.test.ts b/src/features/chat/ui/ChatView.test.ts
index 77f832d..ad50784 100644
--- a/src/features/chat/ui/ChatView.test.ts
+++ b/src/features/chat/ui/ChatView.test.ts
@@ -1,5 +1,5 @@
import type { StepId } from "@dispatch/wire";
-import { render, screen } from "@testing-library/svelte";
+import { fireEvent, render, screen } from "@testing-library/svelte";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import type { RenderedChunk } from "../../../core/chunks";
@@ -497,3 +497,18 @@ it("renders a per-tool Phosphor icon in the header", () => {
// Different tools → different glyphs.
expect(icons[0]?.innerHTML).not.toBe(icons[1]?.innerHTML);
});
+
+it("replaces failed persisted images with an expired placeholder, without marking inline images expired", async () => {
+ const chunks: RenderedChunk[] = ["/images/conv/gone.png", "data:image/png;base64,AAAA"].map(
+ (url, i) => ({ seq: i + 1, role: "user", chunk: { type: "image", url }, provisional: false }),
+ );
+ render(ChatView, { chunks, apiBaseUrl: "https://backend.test" });
+ const [stored, inline] = screen.getAllByRole("img");
+ if (!stored || !inline) throw new Error("expected both images");
+ expect(stored).toHaveAttribute("src", "https://backend.test/images/conv/gone.png");
+ await fireEvent.error(inline);
+ expect(screen.queryByText("image expired")).toBeNull();
+ await fireEvent.error(stored);
+ expect(screen.getByText("image expired")).toBeInTheDocument();
+ expect(screen.getAllByRole("img")).toEqual([inline]);
+});