import { normalizeImageIdentityUrl } from "./canvas-chat-media"; import type { ArchivedImageUrlMap } from "@/lib/storage/image-archive-urls"; type ImageResultNode = { type: string; data?: unknown; }; function imageUrls(node: ImageResultNode): string[] { if (node.type !== "image") return []; const data = node.data as { src?: string; metadata?: { sourceImageKey?: string } } | undefined; return [data?.src, data?.metadata?.sourceImageKey].filter( (url): url is string => typeof url === "string" && Boolean(url.trim()), ); } export function getCanvasImageResultUrlKeys( nodes: readonly ImageResultNode[], aliases: ArchivedImageUrlMap = {}, ) { const keys = new Set(); for (const node of nodes) { for (const url of imageUrls(node)) { keys.add(normalizeImageIdentityUrl(url)); if (aliases[url]) keys.add(normalizeImageIdentityUrl(aliases[url])); } } keys.delete(""); return keys; } // A history refresh may already contain bucket URLs while this tab still shows // provider previews. Resolve aliases only for an existing result of this message; // a new generation never waits for storage before appearing on the canvas. export async function resolveCanvasImageResultAliases(params: { nodes: readonly ImageResultNode[]; messageId?: string; imageUrls: string[]; resolveAliases: (urls: string[]) => Promise; }): Promise { const keys = getCanvasImageResultUrlKeys(params.nodes); if (!params.messageId || params.imageUrls.every((url) => keys.has(normalizeImageIdentityUrl(url)))) { return {}; } const urls = [...new Set(params.nodes.flatMap((node) => { const data = node.data as { metadata?: { sourceMessageId?: string } } | undefined; return data?.metadata?.sourceMessageId === params.messageId ? imageUrls(node) : []; }))].filter((url) => /^https?:\/\//i.test(url)); if (!urls.length) return {}; const aliases: ArchivedImageUrlMap = {}; for (let index = 0; index < urls.length; index += 100) { Object.assign(aliases, await params.resolveAliases(urls.slice(index, index + 100))); } return aliases; } // Call synchronously immediately before changing the store, after all image // loading/network awaits. Both stream and history writers share this check. export function commitCanvasImageResultIfMissing(params: { getNodes: () => readonly ImageResultNode[]; url: string; aliases: ArchivedImageUrlMap; commit: () => T; }): T | undefined { const key = normalizeImageIdentityUrl(params.url); if (!key || getCanvasImageResultUrlKeys(params.getNodes(), params.aliases).has(key)) { return undefined; } return params.commit(); }