import { sendToAgentChat } from "@agent-native/core/client/agent-chat"; import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { IconArrowLeft, IconClipboard, IconCopy, IconDownload, IconTrash, IconVideo, } from "@tabler/icons-react"; import { useEffect, useState, type ComponentProps, type ReactNode, } from "react"; import { Link, useNavigate, useParams } from "react-router"; import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { assetPreviewSources } from "@/lib/asset-preview-sources"; import { assetMediaUrl } from "@/lib/asset-urls"; import { cn } from "@/lib/utils"; export default function AssetDetailPage() { const t = useT(); const { id } = useParams(); const navigate = useNavigate(); const assetQuery = useActionQuery("get-asset", { id: id! }) as any; const exportAsset = useActionMutation("export-asset"); const deleteAsset = useActionMutation("delete-asset"); const asset = assetQuery.data; if (!asset) { if (assetQuery.isLoading || assetQuery.isPending || assetQuery.isFetching) { return (
{t("assetDetail.loading")}
); } return (

{t("assetDetail.unavailableTitle")}

{t("assetDetail.unavailableDescription")}

); } const isVideo = asset.mediaType === "video" || asset.mimeType?.startsWith("video/"); const previewSources = assetPreviewSources(asset); const previewUrl = previewSources[0]; const categoryLabel = assetCategoryLabel(asset, t); const isStarterAsset = asset.metadata?.isStarterAsset === true || String(asset.libraryId || "").startsWith("starter:"); const libraryBackPath = isStarterAsset ? "/library" : `/library/${asset.libraryId}`; function refine() { sendToAgentChat({ message: isVideo ? `Create a new video variation inspired by asset ${asset.id}. Ask me what should change, then call generate-video with this libraryId and folderId when ready.` : `Refine image ${asset.id}. Ask me what to change, then call refine-image with this assetId and show the new preview.`, context: `Asset: ${asset.id}\nLibrary: ${asset.libraryId}\nFolder: ${asset.folderId || "none"}\nPrompt: ${asset.prompt || ""}`, submit: true, newTab: true, }); } async function copyTextToClipboard(text: string, successMessage: string) { try { await navigator.clipboard.writeText(text); toast.success(successMessage); } catch { toast.error(t("assetDetail.copyFailed")); } } function assetUrlForClipboard(url: string | undefined) { if (!url) return ""; return new URL(url, window.location.origin).toString(); } function handoffPrompt() { const previewLine = previewUrl ? `Preview URL: ${assetUrlForClipboard(previewUrl)}` : null; return [ `Handoff asset ${asset.id}.`, `Library ID: ${asset.libraryId}`, asset.collectionId ? `Collection ID: ${asset.collectionId}` : null, asset.metadata?.presetId ? `Preset ID: ${asset.metadata.presetId}` : null, asset.generationRunId ? `Run ID: ${asset.generationRunId}` : null, previewLine, `Prompt: ${asset.prompt || asset.description || t("assetDetail.continueRefining")}`, t("assetDetail.refineInstruction"), ] .filter(Boolean) .join("\n"); } return (
{isVideo ? (
); } function AssetActionButton({ label, children, className, ...props }: ComponentProps & { label: string; children: ReactNode; }) { return ( {label} ); } function assetCategoryLabel( asset: any, t: (key: string, options?: Record) => string, ): string | null { if ( asset?.metadata?.intent === "subject" || asset?.role === "subject_reference" ) { return t("assetDetail.contentOnly"); } const category = asset?.metadata?.category ?? asset?.category; if (typeof category !== "string") return null; if (category === "style-only") return t("assetDetail.styleReference"); if (category === "skeleton") return t("assetDetail.skeletonPlate"); return category.replace(/-/g, " "); } function AssetImagePreview({ sources, alt, previewUnavailableLabel, }: { sources: string[]; alt: string; previewUnavailableLabel: string; }) { const [sourceIndex, setSourceIndex] = useState(0); const [unavailable, setUnavailable] = useState(false); const sourcesKey = sources.join("\n"); useEffect(() => { setSourceIndex(0); setUnavailable(false); }, [sourcesKey]); const src = sources[sourceIndex]; if (!src || unavailable) { return (
{previewUnavailableLabel}
); } return ( {alt} { const nextIndex = sourceIndex + 1; if (nextIndex < sources.length) { setSourceIndex(nextIndex); } else { setUnavailable(true); } }} /> ); } function Field({ label, value, multiline, }: { label: string; value: ReactNode; multiline?: boolean; }) { return (
{label}
{value}
); } function formatDimensions(width?: number | null, height?: number | null) { const dimensions = `${width || "?"} x ${height || "?"}`; if (!width || !height) return dimensions; const divisor = gcd(width, height); return ( {dimensions} {`${width / divisor}:${height / divisor}`} ); } function gcd(a: number, b: number): number { return b === 0 ? a : gcd(b, a % b); }