import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { Check, ChevronLeft, ChevronRight, Copy, X } from 'lucide-react' import type { Asset } from '../types/project-model' import { pathToFileUrl } from '../lib/file-url' import { formatPipelineDisplayName } from '../lib/video-generation-model-specs' const mediaClassName = 'mx-auto max-h-[calc(100dvh-18vh-7rem)] max-w-full rounded-xl object-contain' // Duration can be a raw float (e.g. extend output: 12.041667s). Show a clean value: // integers as-is, otherwise at most 2 decimals with trailing zeros trimmed. function formatSeconds(seconds: number): string { return Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(2).replace(/\.?0+$/, '') } function formatDurationLabel(asset: Asset): string | null { if (asset.type === 'image') return 'Image' // Requested Auto is persisted as generationParams.duration === null. Do not fall through // to omitting the chip — that made Auto clips look like they had no duration at all. if (asset.generationParams?.duration === null) return 'Auto duration' if (asset.duration) return `${formatSeconds(asset.duration)}s` return null } export interface AssetPreviewModalProps { asset: Asset /** 0-based index into the current filtered gallery. */ index: number total: number canGoPrev: boolean canGoNext: boolean onPrev: () => void onNext: () => void onClose: () => void } export function AssetPreviewModal({ asset, index, total, canGoPrev, canGoNext, onPrev, onNext, onClose, }: AssetPreviewModalProps) { const [copiedPrompt, setCopiedPrompt] = useState(false) useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === 'ArrowLeft') { e.preventDefault(); onPrev() } else if (e.key === 'ArrowRight') { e.preventDefault(); onNext() } else if (e.key === 'Escape') onClose() } window.addEventListener('keydown', handleKey) return () => window.removeEventListener('keydown', handleKey) }, [onPrev, onNext, onClose]) // Reset copy affordance when paging between assets. useEffect(() => { setCopiedPrompt(false) }, [asset.id]) // Image assets also store a placeholder `model` (e.g. "fast" for Z-Image) that is not an // LTX video pipeline — only label known video pipelines on video assets. Prefer the label // captured at generation time: the local "fast" pipeline id is shared by every LTX version, // so mapping it here would report the wrong version. const modelLabel = asset.type === 'video' ? asset.generationParams?.modelLabel ?? formatPipelineDisplayName(asset.generationParams?.model) : null const metaParts = [ modelLabel, asset.resolution || null, formatDurationLabel(asset), ].filter(Boolean) return createPortal(
{asset.prompt}
{asset.prompt && ( )}{metaParts.join(' • ')}
)}