import { AgentToggleButton, insertAgentComposerReference, sendMcpAppHostMessage, updateMcpAppModelContext, useAgentChatGenerating, } from "@agent-native/core/client/agent-chat"; import { appPath } from "@agent-native/core/client/api-path"; import { getBrowserTabId, readClientAppState, useActionMutation, useActionQuery, writeClientAppState, } from "@agent-native/core/client/hooks"; import { getEmbedAuthToken, isEmbedAuthActive, isEmbedMcpChatBridgeActive, } from "@agent-native/core/client/host"; import { useT } from "@agent-native/core/client/i18n"; import { AGENT_NATIVE_EMBED_MESSAGE_TYPES, createAgentNativeEmbedEnvelope, createEmbeddedAppBridge, type EmbeddedAppBridge, } from "@agent-native/core/embedding"; import { EMBED_MODE_QUERY_PARAM, EMBED_TOKEN_QUERY_PARAM, } from "@agent-native/core/shared"; import { IconAlertTriangle, IconArrowUpRight, IconCheck, IconChevronDown, IconClipboard, IconLibraryPhoto, IconPhotoPlus, IconSearch, IconX, } from "@tabler/icons-react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { Link, useSearchParams, useLocation, useNavigate, useParams, } from "react-router"; import { toast } from "sonner"; import { AssetPreviewDialog as SharedAssetPreviewDialog } from "@/components/asset/AssetPreviewDialog"; import { LibraryPresetGrid } from "@/components/library/LibraryPresetGrid"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { sortLibrariesByUsage, type ImageLibrarySummary, } from "@/lib/libraries"; import { buildPickerChatHandoffPrompt } from "@/lib/picker-chat-handoff"; import { cn } from "@/lib/utils"; import type { AssetVariantState, ImageModel, ImageQualityTier, StyleStrength, } from "../../shared/api"; import { MODEL_ASPECT_RATIOS } from "../../shared/api"; import { DEFAULT_LIBRARY_PRESETS, LibraryPreset, } from "../../shared/library-presets"; import { BrandKitDetailRoute, LiveCandidatesStage, type VariantSlot, } from "./brand-kits.$id"; type AssetTab = "all" | "generated" | "drafts" | "references"; function isGeneratedAsset(asset: Asset) { const role = asset.role ?? ""; return role === "generated" || role === "active" || role === "candidate"; } function isDraftAsset(asset: Asset) { return asset.role === "generated" && asset.status === "candidate"; } function assetMatchesTab(asset: Asset, tab: AssetTab) { if (tab === "all") return true; if (tab === "drafts") return isDraftAsset(asset); if (tab === "generated") return isGeneratedAsset(asset) && !isDraftAsset(asset); return !isGeneratedAsset(asset); } const ASPECT_RATIOS = ["16:9", "1:1", "9:16", "4:3", "3:4", "21:9"] as const; const GENERATION_COUNTS = [1, 2, 3, 4, 6] as const; const STARTER_PRESET = DEFAULT_LIBRARY_PRESETS[0]; const STARTER_LIBRARY_ID = `starter:${STARTER_PRESET.id}`; const MCP_APP_CHAT_BRIDGE_QUERY_PARAM = "__an_mcp_chat_bridge"; const PICKER_INLINE_SELECT_CLASS = "h-7 w-auto min-w-0 max-w-full rounded-md border-0 bg-transparent px-1.5 py-1 text-xs font-medium text-muted-foreground shadow-none ring-offset-transparent transition hover:bg-accent/50 hover:text-foreground focus:ring-0 focus:ring-offset-0 sm:px-2 [&>svg]:ms-1 [&>svg]:size-3.5 [&>svg]:opacity-60"; type PickerMediaType = "image" | "video"; type PickerLayout = "default" | "vertical"; type Asset = { id: string; libraryId: string; role?: string | null; status?: string | null; title?: string | null; description?: string | null; altText?: string | null; prompt?: string | null; mediaType?: string | null; mimeType?: string | null; width?: number | null; height?: number | null; url?: string; previewUrl?: string; thumbnailUrl?: string; downloadUrl?: string; embedUrl?: string; embedPath?: string; folderId?: string | null; category?: string | null; model?: string | null; aspectRatio?: string | null; durationSeconds?: number | null; metadata?: Record | null; libraryTitle?: string | null; lineage?: { label?: string | null; kind?: string | null; sourceLabel?: string | null; } | null; }; type Library = { id: string; title: string; description?: string | null; }; type GenerationConfig = { builderEnabled?: boolean; builderConnected?: boolean; geminiConfigured?: boolean; configured?: boolean; lastIssue?: { message?: unknown } | null; }; type GenerationPreset = { id: string; title: string; mediaType?: string | null; aspectRatio?: string | null; imageSize?: string | null; model?: string | null; }; type HostConfig = { mediaType?: PickerMediaType; prompt?: string; query?: string; libraryId?: string; libraryHint?: string; aspectRatio?: string; presetId?: string; count?: number; tier?: ImageQualityTier; styleStrength?: StyleStrength; includeLogo?: boolean; callerAppId?: string; creativeContextRequestId?: string; layout?: PickerLayout; autoGenerate?: boolean; candidateRunIds?: string[]; }; // Preselect the library whose title/description best matches a free-text brand // or use-case hint. Falls back to no match (caller uses the first library). function matchLibraryByHint( libraries: Library[], hint: string | undefined, ): string | undefined { const needle = hint?.trim().toLowerCase(); if (!needle) return undefined; const terms = needle.split(/\s+/).filter(Boolean); let best: { id: string; score: number } | null = null; for (const library of libraries) { const haystack = `${library.title ?? ""} ${library.description ?? ""}`.toLowerCase(); if (!haystack.trim()) continue; let score = 0; if (haystack.includes(needle)) score += 10; for (const term of terms) { if (haystack.includes(term)) score += 1; } if (score > 0 && (!best || score > best.score)) { best = { id: library.id, score }; } } return best?.id; } function isEmbeddedWindow() { if (typeof window === "undefined") return false; try { return window.self !== window.top; } catch { return true; } } interface StandalonePickerHandoff { handoffId: string; returnOrigin: string; } function standalonePickerHandoff(): StandalonePickerHandoff | null { if (typeof window === "undefined") return null; const params = new URLSearchParams(window.location.search); const handoffId = params.get("__an_asset_picker_handoff")?.trim(); const rawReturnOrigin = params.get("__an_asset_picker_return_origin")?.trim(); if (!handoffId || handoffId.length > 128 || !rawReturnOrigin) return null; try { const parsed = new URL(rawReturnOrigin); if ( (parsed.protocol !== "http:" && parsed.protocol !== "https:") || parsed.origin !== rawReturnOrigin ) { return null; } return { handoffId, returnOrigin: parsed.origin }; } catch { return null; } } function normalizeCount(value: unknown): number { if (value === null || value === undefined || value === "") return 3; const count = Number(value); if (!Number.isFinite(count)) return 3; const rounded = Math.round(count); return Math.min(6, Math.max(1, rounded)); } function normalizeMediaType(value: unknown): PickerMediaType { return value === "video" ? "video" : "image"; } function normalizePickerLayout(value: unknown): PickerLayout | undefined { return value === "vertical" ? "vertical" : undefined; } function normalizeBoolean(value: unknown): boolean | undefined { if (typeof value === "boolean") return value; if (typeof value !== "string") return undefined; const normalized = value.trim().toLowerCase(); if (["1", "true", "yes", "on"].includes(normalized)) return true; if (["", "0", "false", "no", "off"].includes(normalized)) return false; return undefined; } function normalizeTier(value: unknown): ImageQualityTier | undefined { return value === "auto" || value === "fast" || value === "best" ? value : undefined; } function normalizeStyleStrength(value: unknown): StyleStrength | undefined { return value === "subtle" || value === "balanced" || value === "strong" ? value : undefined; } function normalizeCandidateRunIds(value: unknown): string[] | undefined { if (value === null || value === undefined) return undefined; const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : []; const ids = raw .map((item) => (typeof item === "string" ? item.trim() : "")) .filter(Boolean); return ids; } function searchParamsEnableEmbeddedLibrary(params: URLSearchParams): boolean { const embedMode = params.get(EMBED_MODE_QUERY_PARAM); return ( params.has(EMBED_TOKEN_QUERY_PARAM) || embedMode === "1" || embedMode === "true" ); } function searchParamsRequestPicker(params: URLSearchParams): boolean { const mcpChatBridge = params.get(MCP_APP_CHAT_BRIDGE_QUERY_PARAM); return ( params.get("__an_picker") === "1" || mcpChatBridge === "1" || mcpChatBridge === "true" ); } function normalizeHostConfig(value: unknown): HostConfig { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; const record = value as Record; const config: HostConfig = { mediaType: record.mediaType === "image" || record.mediaType === "video" ? record.mediaType : undefined, prompt: typeof record.prompt === "string" ? record.prompt : undefined, query: typeof record.query === "string" ? record.query : undefined, libraryId: typeof record.libraryId === "string" ? record.libraryId : undefined, libraryHint: typeof record.libraryHint === "string" ? record.libraryHint : undefined, aspectRatio: typeof record.aspectRatio === "string" ? record.aspectRatio : undefined, presetId: typeof record.presetId === "string" ? record.presetId : undefined, count: record.count === undefined ? undefined : normalizeCount(record.count), tier: normalizeTier(record.tier), styleStrength: normalizeStyleStrength(record.styleStrength), includeLogo: normalizeBoolean(record.includeLogo), callerAppId: typeof record.callerAppId === "string" ? record.callerAppId : undefined, creativeContextRequestId: typeof record.creativeContextRequestId === "string" ? record.creativeContextRequestId : undefined, layout: normalizePickerLayout(record.layout), candidateRunIds: normalizeCandidateRunIds(record.candidateRunIds), }; if (Object.prototype.hasOwnProperty.call(record, "autoGenerate")) { config.autoGenerate = normalizeBoolean(record.autoGenerate) ?? false; } return config; } function embeddedAppOrigin() { if (typeof window === "undefined") return undefined; const externalOrigin = typeof (window as any).__AGENT_NATIVE_EXTERNAL_EMBED?.origin === "string" ? (window as any).__AGENT_NATIVE_EXTERNAL_EMBED.origin : undefined; if (externalOrigin) return externalOrigin; if (typeof document === "undefined") return undefined; const baseHref = document.querySelector("base")?.getAttribute("href") ?? document.baseURI; try { const baseOrigin = new URL(baseHref).origin; return baseOrigin === window.location.origin ? undefined : baseOrigin; } catch { return undefined; } } function absoluteAppUrl(value: string) { if (typeof window === "undefined") return value; const path = value.startsWith("/") ? appPath(value) : value; try { return new URL( path, embeddedAppOrigin() ?? window.location.origin, ).toString(); } catch { return value; } } function absoluteAssetUrl(value: string | undefined) { if (!value) return undefined; try { const path = value.startsWith("/") && !value.startsWith("//") ? appPath(value) : value; return new URL(path, absoluteAppUrl("/")).toString(); } catch { return value; } } function shouldUseContentProxyForPreview(asset: Asset) { if (typeof window === "undefined") return false; if (!isEmbeddedWindow()) return false; return ( asset.libraryId !== STARTER_LIBRARY_ID && !asset.id.startsWith("starter-") ); } function embedTokenParam() { if (typeof window === "undefined") return null; const externalToken = typeof (window as any).__AGENT_NATIVE_EXTERNAL_EMBED?.token === "string" ? (window as any).__AGENT_NATIVE_EXTERNAL_EMBED.token : null; if (externalToken) return externalToken; return ( getEmbedAuthToken() ?? new URLSearchParams(window.location.search).get("__an_embed_token") ); } function assetContentUrl(asset: Asset, variant?: "thumb") { const params = new URLSearchParams(); if (variant === "thumb") params.set("variant", "thumb"); const embedToken = embedTokenParam(); if (embedToken) params.set("__an_embed_token", embedToken); const query = params.toString(); return absoluteAssetUrl( `/api/assets/${asset.id}/content${query ? `?${query}` : ""}`, ); } function uniqueSources(sources: Array) { return sources.filter( (source, index, all): source is string => typeof source === "string" && source.length > 0 && all.indexOf(source) === index, ); } function assetThumbnailSources(asset: Asset) { if (shouldUseContentProxyForPreview(asset)) { return uniqueSources([ assetContentUrl(asset, asset.thumbnailUrl ? "thumb" : undefined), assetContentUrl(asset), ]); } return uniqueSources( [asset.thumbnailUrl, asset.previewUrl, asset.downloadUrl].map((source) => absoluteAssetUrl(source), ), ); } function assetOverlaySources(asset: Asset) { if (shouldUseContentProxyForPreview(asset)) { return uniqueSources([assetContentUrl(asset)]); } return uniqueSources( [asset.previewUrl, asset.downloadUrl, asset.url, asset.thumbnailUrl].map( (source) => absoluteAssetUrl(source), ), ); } function previewFetchCredentials( source: string | undefined, ): RequestCredentials { if (!source || typeof window === "undefined") return "omit"; try { return new URL(source, window.location.href).origin === window.location.origin ? "same-origin" : "omit"; } catch { return "omit"; } } /** * True when `url` points at a different origin than the current document. * Inline embeds load under `Cross-Origin-Embedder-Policy: require-corp`, which * blocks cross-origin `` subresources unless they opt in via CORS. Marking * cross-origin previews `crossOrigin="anonymous"` makes the browser CORS-fetch * them (the asset CDN sends `Access-Control-Allow-Origin: *`), satisfying COEP. * Same-origin and `data:`/`blob:` URLs return false so their cookies / inline * bytes are untouched. */ function isCrossOriginPreview(url: string | undefined): boolean { if (!url || typeof window === "undefined") return false; if (url.startsWith("data:") || url.startsWith("blob:")) return false; try { return new URL(url, window.location.href).origin !== window.location.origin; } catch { return false; } } function assetPayload(asset: Asset, requestedMediaType: PickerMediaType) { const mediaType = asset.mediaType === "video" || asset.mimeType?.startsWith("video/") ? "video" : requestedMediaType; const assetTitle = asset.title?.trim() ?? ""; const assetAltText = asset.altText?.trim() ?? ""; const assetPrompt = asset.prompt?.trim() ?? ""; const displayTitle = assetDisplayTitle(asset); const fallbackLabel = assetTitle || assetPrompt || displayTitle || asset.id; const embeddedContentUrl = shouldUseContentProxyForPreview(asset) ? assetContentUrl(asset) : undefined; const previewUrl = absoluteAssetUrl(embeddedContentUrl ?? asset.previewUrl); const url = absoluteAssetUrl( embeddedContentUrl ?? asset.previewUrl ?? asset.downloadUrl ?? asset.url, ); const thumbnailUrl = absoluteAssetUrl(asset.thumbnailUrl); const downloadUrl = absoluteAssetUrl(asset.downloadUrl); const embedUrl = absoluteAssetUrl(asset.embedUrl); return { id: asset.id, assetId: asset.id, libraryId: asset.libraryId, mediaType, url, previewUrl, thumbnailUrl, downloadUrl, embedUrl, embedPath: asset.embedPath, altText: assetAltText || fallbackLabel, title: assetTitle || fallbackLabel, prompt: asset.prompt ?? null, width: asset.width ?? null, height: asset.height ?? null, mimeType: asset.mimeType ?? null, }; } function selectedAssetText(payload: ReturnType) { const url = payload.url ?? payload.downloadUrl ?? payload.previewUrl; return `Selected ${payload.mediaType} asset ${payload.assetId}${url ? `: ${url}` : ""}`; } function selectedAssetLabel(payload: ReturnType) { const title = payload.title?.trim() ?? ""; const altText = payload.altText?.trim() ?? ""; const prompt = payload.prompt?.trim() ?? ""; const machineLikeTitle = title === payload.assetId || /^[A-Za-z0-9_-]{12,}$/.test(title); const genericGeneratedTitle = /^Original \d+$/i.test(title); if (prompt && (!title || machineLikeTitle || genericGeneratedTitle)) { return prompt; } if (altText && altText !== title) return altText; if (title && !machineLikeTitle) return title; return prompt || altText || title || payload.assetId; } function selectedAssetFollowUpMessage( payload: ReturnType, ) { const url = payload.url ?? payload.downloadUrl ?? payload.previewUrl; const label = selectedAssetLabel(payload); return [ `Use this selected ${payload.mediaType} in the current work: ${label}`, url ? `URL: ${url}` : null, ] .filter(Boolean) .join("\n"); } /** Compact, agent-usable context — not the full internal payload. */ function selectedAssetContext(payload: ReturnType) { const url = payload.url ?? payload.downloadUrl ?? payload.previewUrl; const width = Number(payload.width); const height = Number(payload.height); return { assetId: payload.assetId, title: payload.title, mediaType: payload.mediaType, url, ...(Number.isFinite(width) && Number.isFinite(height) && width && height ? { width, height } : {}), }; } function selectedAssetClipboardText(payload: ReturnType) { const url = payload.url ?? payload.downloadUrl ?? payload.previewUrl; const previewTip = payload.mediaType === "image" && url ? [ `Markdown preview: ![Selected asset](${url})`, "If this remote preview does not render in Codex or Claude Code, download the image locally and embed the absolute local file path.", ] : []; return [ selectedAssetFollowUpMessage(payload), ...previewTip, "", JSON.stringify(selectedAssetContext(payload), null, 2), ].join("\n"); } function blobToBase64(blob: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const value = typeof reader.result === "string" ? reader.result : ""; resolve(value.includes(",") ? value.split(",")[1] : value); }; reader.onerror = () => reject(reader.error ?? new Error("Read failed")); reader.readAsDataURL(blob); }); } const MCP_IMAGE_CONTENT_MAX_BYTES = 2.5 * 1024 * 1024; function base64ByteLength(data: string) { const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0; return Math.floor((data.length * 3) / 4) - padding; } async function fetchImageContent(url: string, fallbackMimeType: string) { const credentials = typeof window !== "undefined" && new URL(url, window.location.href).origin === window.location.origin ? "same-origin" : "omit"; const response = await fetch(url, { credentials }); if (!response.ok) return null; const blob = await response.blob(); const detectedMimeType = blob.type.startsWith("image/") ? blob.type : fallbackMimeType; return { type: "image", data: await blobToBase64(blob), mimeType: detectedMimeType, }; } async function imageContentForAsset(payload: ReturnType) { const url = payload.url ?? payload.downloadUrl ?? payload.previewUrl; const mimeType = payload.mimeType?.startsWith("image/") ? payload.mimeType : "image/png"; if (!url || payload.mediaType !== "image") return null; const sources = uniqueSources([url, payload.thumbnailUrl]); for (const source of sources) { try { const content = await fetchImageContent(source, mimeType); if ( content && base64ByteLength(content.data) <= MCP_IMAGE_CONTENT_MAX_BYTES ) { return content; } } catch { // Try the next smaller source. } } return null; } function notifyMcpHost(payload: ReturnType) { return Promise.resolve(imageContentForAsset(payload)) .catch(() => null) .then((imageContent) => { const context = { selectedAsset: payload }; const message = selectedAssetFollowUpMessage(payload); const modelContent = [ { type: "text", text: selectedAssetText(payload) }, ...(imageContent ? [imageContent] : []), ]; const chatContent = [ { type: "text", text: message }, ...(imageContent ? [imageContent] : []), ]; return Promise.resolve( updateMcpAppModelContext({ structuredContent: context, content: modelContent, }) || false, ) .catch(() => false) .then((contextOk) => { return Promise.resolve( sendMcpAppHostMessage({ message, context: JSON.stringify(context, null, 2), content: chatContent, structuredContent: context, }) || false, ) .catch(() => false) .then((chatOk) => contextOk || chatOk); }); }); } function assetDisplayTitle(asset: Asset) { return ( asset.lineage?.label || asset.title || asset.prompt || "Untitled asset" ); } function AssetThumbnail({ asset }: { asset: Asset }) { const sources = assetThumbnailSources(asset); const [sourceIndex, setSourceIndex] = useState(0); const [unavailable, setUnavailable] = useState(false); const source = sources[sourceIndex]; const proxiedPreview = shouldUseContentProxyForPreview(asset); const [displayUrl, setDisplayUrl] = useState( proxiedPreview ? undefined : source, ); const sourcesKey = sources.join("\n"); useEffect(() => { setSourceIndex(0); setUnavailable(false); }, [sourcesKey]); function tryNextSource() { const nextIndex = sourceIndex + 1; if (nextIndex < sources.length) { setSourceIndex(nextIndex); } else { setDisplayUrl(undefined); setUnavailable(true); } } useEffect(() => { let cancelled = false; const proxied = shouldUseContentProxyForPreview(asset); if (!source || unavailable) { setDisplayUrl(undefined); return; } if (!proxied) { setDisplayUrl(source); return; } setDisplayUrl(undefined); fetch(source, { cache: "no-store", credentials: previewFetchCredentials(source), }) .then(async (response) => { if (!response.ok) throw new Error("Preview fetch failed"); const blob = await response.blob(); const base64 = await blobToBase64(blob); return `data:${blob.type || asset.mimeType || "image/png"};base64,${base64}`; }) .then((dataUrl) => { if (!cancelled) setDisplayUrl(dataUrl); }) .catch(() => { if (!cancelled) tryNextSource(); }); return () => { cancelled = true; }; }, [asset.mimeType, source, sourceIndex, unavailable]); if (!displayUrl) { return
; } return ( {asset.altText ); } function AssetOverlayImage({ asset }: { asset: Asset }) { const t = useT(); const sources = assetOverlaySources(asset); const sourcesKey = sources.join("\n"); const [sourceIndex, setSourceIndex] = useState(0); const source = sources[sourceIndex]; useEffect(() => { setSourceIndex(0); }, [sourcesKey]); if (!source) { return (
{t("brandKitDetail.previewUnavailable")}
); } return ( {asset.altText setSourceIndex((index) => index + 1 < sources.length ? index + 1 : index, ) } /> ); } function EmptyLibraryStarter({ onCreateBlank }: { onCreateBlank: () => void }) { const t = useT(); const navigate = useNavigate(); const { data: presetData } = useActionQuery("list-library-presets", {}); const createFromPreset = useActionMutation("create-library-from-preset"); const [creatingPresetId, setCreatingPresetId] = useState(null); const presets = ((presetData as any)?.presets ?? []) as LibraryPreset[]; function createPresetLibrary(presetId: string) { setCreatingPresetId(presetId); createFromPreset.mutate( { presetId } as any, { onSuccess: (library: any) => { setCreatingPresetId(null); navigate(`/library/${library.id}`); }, onError: (error: Error) => { setCreatingPresetId(null); toast.error(error.message || t("brandKits.presetCreateFailed")); }, } as any, ); } return (

{t("library.buildYourFirstKit")}

{t("library.buildYourFirstKitDescription")}

); } function LibraryShellHeader({ selectedLibraryId = null, libraries, isLoading, onCreateKit, }: { selectedLibraryId?: string | null; libraries: ImageLibrarySummary[]; isLoading?: boolean; onCreateKit: () => void; }) { const t = useT(); const selectedLibrary = selectedLibraryId ? libraries.find((library) => library.id === selectedLibraryId) : null; const title = selectedLibrary?.title ?? (selectedLibraryId ? t("library.library") : t("library.allAssets")); const visibility = (selectedLibrary as any)?.visibility; return (
{visibility ? ( {visibility} ) : null}
{selectedLibraryId ? (
) : null} {selectedLibraryId ? (
) : null}
); } function LibraryKitSelector({ selectedLibraryId = null, libraries, isLoading, triggerLabel, triggerStyle = "button", onCreateKit, }: { selectedLibraryId?: string | null; libraries: ImageLibrarySummary[]; isLoading?: boolean; triggerLabel?: string; triggerStyle?: "button" | "title"; onCreateKit: () => void; }) { const t = useT(); const navigate = useNavigate(); const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const selectedLibrary = selectedLibraryId ? libraries.find((library) => library.id === selectedLibraryId) : null; const filteredLibraries = useMemo(() => { const items = sortLibrariesByUsage(libraries.filter(Boolean)); const q = query.trim().toLowerCase(); if (!q) return items; return items.filter((library) => [library.title, library.description] .filter(Boolean) .join(" ") .toLowerCase() .includes(q), ); }, [libraries, query]); function selectLibrary(libraryId: string | null) { setOpen(false); navigate(libraryId ? `/library/${libraryId}` : "/library"); } const titleTrigger = triggerStyle === "title"; return ( { setOpen(next); if (!next) setQuery(""); }} > {titleTrigger ? ( ) : ( )}
setQuery(event.target.value)} placeholder={t("library.searchKits")} className="h-full min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" />
{isLoading ? (
{Array.from({ length: 3 }).map((_, index) => ( ))}
) : filteredLibraries.length ? (
{filteredLibraries.map((library) => { const selected = selectedLibraryId === library.id; return ( ); })}
) : (
{t("library.noKitsMatch")}
)}
); } function AllAssetsBrowser({ foldersByLibraryId = {}, }: { foldersByLibraryId?: Record; }) { const t = useT(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [searchParams, setSearchParams] = useSearchParams(); const searchParamsKey = searchParams.toString(); // The root Library view keeps its tab/search in the URL so deep links, // refreshes, and agent `navigate` commands are honored (the framework's // useNavigationState reads the same `?tab=`/`?q=` params). Absent a tab param, // default to Drafts. const urlAssetTab = useMemo(() => { const tab = new URLSearchParams(searchParamsKey).get("tab"); return tab === "drafts" || tab === "generated" || tab === "references" ? tab : "drafts"; }, [searchParamsKey]); const urlQuery = useMemo( () => new URLSearchParams(searchParamsKey).get("q") ?? "", [searchParamsKey], ); const [query, setQuery] = useState(urlQuery); const [assetTab, setAssetTab] = useState(urlAssetTab); const [previewAsset, setPreviewAsset] = useState(null); const [standaloneSelection, setStandaloneSelection] = useState | null>(null); const [standaloneCopyOk, setStandaloneCopyOk] = useState(false); const isDraftsTab = assetTab === "drafts"; // The Drafts tab renders its own candidate queries via LibraryCandidateStage, // so skip the cross-library asset scan while it is the active tab. const { data: assetData, isLoading, isError, isFetching, refetch, } = useActionQuery( "list-assets", { query: query.trim() || undefined, } as any, { enabled: !isDraftsTab } as any, ) as { data?: { assets?: Asset[] }; isLoading: boolean; isError: boolean; isFetching: boolean; refetch: () => Promise; }; const allAssets = assetData?.assets ?? []; const assets = useMemo( () => allAssets.filter((asset) => assetMatchesTab(asset, assetTab)), [allAssets, assetTab], ); const visibleAssetCount = assets.length; // The badge only renders on the Generated/References tabs, which are always a // filtered subset, so report the shown count rather than the library total. const assetCountLabel = isLoading ? t("library.loading") : t("library.shownCount", { count: visibleAssetCount }); const standaloneSelectionText = useMemo( () => standaloneSelection ? selectedAssetClipboardText(standaloneSelection) : "", [standaloneSelection], ); const standaloneSelectionUrl = standaloneSelection?.url ?? standaloneSelection?.downloadUrl ?? standaloneSelection?.previewUrl; const copyStandaloneSelection = useCallback( async (payload: ReturnType) => { const text = selectedAssetClipboardText(payload); try { await navigator.clipboard.writeText(text); setStandaloneCopyOk(true); toast.success(t("library.selectionCopied")); } catch { setStandaloneCopyOk(false); toast.info(t("library.selectionReady")); } }, [], ); function chooseAsset(asset: Asset) { const payload = assetPayload(asset, "image"); setStandaloneSelection(payload); setStandaloneCopyOk(false); void copyStandaloneSelection(payload); } // Keep local state in sync when the URL changes externally (back/forward, // agent navigation, deep links) since the component stays mounted. useEffect(() => { setAssetTab(urlAssetTab); }, [urlAssetTab]); useEffect(() => { setQuery(urlQuery); }, [urlQuery]); const handleAssetTabChange = useCallback( (value: AssetTab) => { setAssetTab(value); setSearchParams( (prev) => { const next = new URLSearchParams(prev); // Drafts is the default, so keep it out of the URL for clean links. if (value === "drafts") next.delete("tab"); else next.set("tab", value); return next; }, { replace: true }, ); }, [setSearchParams], ); const handleQueryChange = useCallback( (value: string) => { setQuery(value); setSearchParams( (prev) => { const next = new URLSearchParams(prev); if (value.trim()) next.set("q", value); else next.delete("q"); return next; }, { replace: true }, ); }, [setSearchParams], ); // The Drafts tab's candidate queries live inside LibraryCandidateStage; // refetch them by key so the error state offers a working retry. const retryDrafts = useCallback(() => { void queryClient.refetchQueries({ queryKey: ["app-state", assetVariantStateKey(null)], }); void queryClient.refetchQueries({ queryKey: ["action", "list-assets"] }); }, [queryClient]); return (
handleAssetTabChange(value as AssetTab)} > {t("library.drafts")} {t("library.generated")} {t("library.references")} {!isDraftsTab && ( {assetCountLabel} )}
{!isDraftsTab && (
handleQueryChange(event.currentTarget.value) } onChange={(event) => handleQueryChange(event.target.value)} placeholder={t("library.searchAssets")} className="h-full min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" />
)}
{standaloneSelection && (
{selectedAssetLabel(standaloneSelection)}
{standaloneSelectionUrl && (
{standaloneSelectionUrl}
)}
{t("library.showPasteText")}