ai-chatbot/components/canvas/chat-panel/canvas-image-result-sync.ts

71 lines
2.6 KiB
TypeScript

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<string>();
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<ArchivedImageUrlMap>;
}): Promise<ArchivedImageUrlMap> {
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<T>(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();
}