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 (
);
}
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 (
{
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 (
);
}
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);
}