import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { Download, ExternalLink, Check, Loader2, Sparkles } from 'lucide-react' import { formatBytes } from '@/lib/format' import { catalogVariantKey, preferredVariantId } from '@/lib/lora-library' export type LibraryItemStatus = | { kind: 'available' } | { kind: 'downloading'; progress: number } | { kind: 'downloaded' } | { kind: 'error'; gated?: boolean; message: string } export interface LibraryItemVariant { id: string label: string sizeBytes: number downloaded?: boolean } export interface LibraryItem { id: string title: string description?: string sizeBytes?: number /** Item-level fallback when there are no variants (any file present). */ downloaded: boolean thumbnailUrl?: string demoVideoUrl?: string author?: { name: string; url?: string | null } license?: { name: string; url?: string | null } variants?: LibraryItemVariant[] defaultVariantId?: string requiresHfLogin?: boolean } interface LibraryItemCardProps { item: LibraryItem selected?: boolean // The variant actually active for this entry (when selected). Undefined/null means // "don't check" (e.g. no variant picker) — badge follows `selected` alone. selectedVariantId?: string | null downloadingKey?: string | null progress?: number downloadError?: { key: string; message: string; gated?: boolean } | null onDownload?: (id: string, variantId?: string) => void onUse?: (id: string, variantId?: string) => void onRequestAccess?: (id: string) => void onRetry?: (id: string, variantId?: string) => void infoSlot?: ReactNode } const openUrl = (url?: string | null) => { if (url) void window.electronAPI.openExternalUrl({ url }) } // Generic media grid card for one downloadable asset. No domain knowledge — the consumer // maps its type into a LibraryItem. Media is optional: a demo video shows its first frame // (preload=metadata) and plays on hover; without media the card shows the thumbnail or a placeholder. export function LibraryItemCard({ item, selected, selectedVariantId, downloadingKey, progress = 0, downloadError, onDownload, onUse, onRequestAccess, onRetry, infoSlot, }: LibraryItemCardProps) { const hasVariants = (item.variants?.length ?? 0) > 1 const downloadedVariantIds = useMemo( () => (item.variants ?? []).filter(v => v.downloaded).map(v => v.id), [item.variants], ) const downloadedKey = downloadedVariantIds.join(',') const fallbackVariantId = item.defaultVariantId ?? item.variants?.[0]?.id const initialVariantId = preferredVariantId(item.variants, item.defaultVariantId, downloadedVariantIds) ?? fallbackVariantId const [variantId, setVariantId] = useState(initialVariantId) // Keep selection on an installed checkpoint when install state arrives after first paint // (e.g. listModels refresh) and the catalog default still isn't on disk. useEffect(() => { if (!hasVariants) return const preferred = preferredVariantId(item.variants, item.defaultVariantId, downloadedVariantIds) setVariantId(prev => { if (prev && downloadedVariantIds.includes(prev)) return prev if (prev && item.variants?.some(v => v.id === prev)) return prev return preferred ?? fallbackVariantId }) }, [hasVariants, item.defaultVariantId, item.variants, downloadedKey, fallbackVariantId]) // eslint-disable-line react-hooks/exhaustive-deps -- downloadedKey proxies ids const selectedVariant = item.variants?.find(v => v.id === variantId) const sizeBytes = selectedVariant?.sizeBytes ?? item.sizeBytes const size = sizeBytes ? formatBytes(sizeBytes) : null const activeKey = catalogVariantKey(item.id, hasVariants ? variantId : undefined) const status: LibraryItemStatus = (() => { if (downloadingKey === activeKey) return { kind: 'downloading', progress } if (downloadError?.key === activeKey) { return { kind: 'error', gated: downloadError.gated ?? Boolean(item.requiresHfLogin), message: downloadError.message, } } if (hasVariants) { return selectedVariant?.downloaded ? { kind: 'downloaded' } : { kind: 'available' } } return item.downloaded ? { kind: 'downloaded' } : { kind: 'available' } })() const selectedVariantReady = status.kind === 'downloaded' // With a variant picker, only badge "Selected" when the dropdown is on the variant // that's actually active — not just this catalog entry (avoids marking the wrong checkpoint). const isSelected = Boolean(selected) && (!hasVariants || selectedVariantId == null || selectedVariantId === variantId) const [hovered, setHovered] = useState(false) // Lazy media: don't fetch/decode the demo video until the card scrolls near the viewport. const [near, setNear] = useState(false) const videoRef = useRef(null) const cardRef = useRef(null) useEffect(() => { const el = cardRef.current if (!el || near) return const observer = new IntersectionObserver((entries) => { if (entries.some((e) => e.isIntersecting)) setNear(true) }, { rootMargin: '200px' }) observer.observe(el) return () => observer.disconnect() }, [near]) useEffect(() => { const v = videoRef.current if (!v) return if (hovered) void v.play().catch(() => {}) else { v.pause(); v.currentTime = 0 } }, [hovered]) // Keep the variant picker visible while another variant downloads. const showVariantPicker = hasVariants && status.kind !== 'downloading' return (
setHovered(true)} onMouseLeave={() => setHovered(false)} > {/* Media */}
{item.demoVideoUrl ? (
{/* Body */}
{item.title} {infoSlot}
{item.description &&
{item.description}
} {(item.author || item.license) && (
{item.author && ( item.author.url ? : by {item.author.name} )} {item.license && ( item.license.url ? : {item.license.name} )}
)} {/* Footer: status / action */}
{showVariantPicker && ( )} {status.kind === 'downloading' && (
{Math.round(status.progress)}%
)} {status.kind === 'error' && (
{status.gated ? 'This model is gated — request access, then retry.' : status.message}
{status.gated && ( )}
)} {status.kind !== 'downloading' && status.kind !== 'error' && selectedVariantReady && ( )} {status.kind !== 'downloading' && status.kind !== 'error' && !selectedVariantReady && ( )}
) }