From 2f677fcfb08e1701201bc77bda7715ce4a2e7b2c Mon Sep 17 00:00:00 2001 From: Zhifei Li Date: Thu, 4 Jun 2026 23:46:26 -0700 Subject: [PATCH] fix(web): don't show broken tiles in the chat gallery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent pages through long articles by guessing chunk coordinates (0/0, 0/1, ... 1/5, 2/0), so some pixelrag_tile reads 404 past the end of an article — normal exploration. But the agent backend emitted viewing_tile BEFORE fetching, so failed reads still landed in the chat tile gallery as broken images (seen live on /api/tile/290066/1/5 while answering the Inter shots-on-target example). - agent-server.mjs + route.ts: emit viewing_tile only after a successful fetch - TileGallery: belt-and-braces onError — drop tiles whose image fails to load --- web/agent-server.mjs | 5 ++++- web/app/api/chat/route.ts | 15 +++++++++------ web/app/chat/page.tsx | 11 ++++++++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/web/agent-server.mjs b/web/agent-server.mjs index 322ec91..066fffb 100644 --- a/web/agent-server.mjs +++ b/web/agent-server.mjs @@ -131,10 +131,13 @@ function createTools(onEvent, uploadedImage) { }, async (args) => { const tileUrl = `${SEARCH_URL}/tile/${args.article_id}/${args.tile_index}/${args.chunk_index}` - onEvent("viewing_tile", { article_id: args.article_id, tile_index: args.tile_index, chunk_index: args.chunk_index }) try { const resp = await fetch(tileUrl) + // The agent pages through articles by guessing chunk coordinates, so + // 404s are normal exploration — only surface tiles that actually load, + // otherwise the chat gallery renders broken images. if (!resp.ok) return { content: [{ type: "text", text: `Tile not found: ${resp.status}` }] } + onEvent("viewing_tile", { article_id: args.article_id, tile_index: args.tile_index, chunk_index: args.chunk_index }) const buffer = await resp.arrayBuffer() const base64 = Buffer.from(buffer).toString("base64") const mimeType = resp.headers.get("content-type") || "image/png" diff --git a/web/app/api/chat/route.ts b/web/app/api/chat/route.ts index 998f203..08731f8 100644 --- a/web/app/api/chat/route.ts +++ b/web/app/api/chat/route.ts @@ -158,14 +158,17 @@ function createTools( async (args) => { const tileUrl = `${SEARCH_URL}/tile/${args.article_id}/${args.tile_index}/${args.chunk_index}` - onEvent("viewing_tile", { - article_id: args.article_id, - tile_index: args.tile_index, - chunk_index: args.chunk_index, - }) - try { const resp = await fetch(tileUrl) + // Only surface successfully fetched tiles — the agent pages through + // articles by guessing chunk coordinates, so 404s are normal. + if (resp.ok) { + onEvent("viewing_tile", { + article_id: args.article_id, + tile_index: args.tile_index, + chunk_index: args.chunk_index, + }) + } if (!resp.ok) { return { content: [ diff --git a/web/app/chat/page.tsx b/web/app/chat/page.tsx index 5a61957..d2dca39 100644 --- a/web/app/chat/page.tsx +++ b/web/app/chat/page.tsx @@ -565,7 +565,12 @@ const READING_VERBS = [ ] function TileGallery({ tiles, loading }: { tiles: TileView[]; loading?: boolean }) { - const n = tiles.length + // Belt-and-braces: if a tile image 404s anyway, drop it from the gallery + // instead of rendering a broken image. + const [failed, setFailed] = React.useState>(new Set()) + const key = (t: TileView) => `${t.article_id}/${t.tile_index}/${t.chunk_index}` + const shown = tiles.filter((t) => !failed.has(key(t))) + const n = shown.length // Seed by the first tile so the verb stays put as more tiles stream in, // but differs from answer to answer. const verb = READING_VERBS[(tiles[0]?.article_id ?? n) % READING_VERBS.length] @@ -579,13 +584,13 @@ function TileGallery({ tiles, loading }: { tiles: TileView[]; loading?: boolean {loading && }
- {tiles.map((t, i) => ( + {shown.map((t, i) => ( {/* eslint-disable-next-line @next/next/no-img-element */} - {`Tile + {`Tile setFailed((prev) => new Set(prev).add(key(t)))} />
Open