import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { Upload, Trash2, RefreshCw, ChevronDown, ChevronRight, Library } from 'lucide-react' import { useDropzone } from 'react-dropzone' import { listAssetLibraries, createAssetLibrary, refreshAssetLibraryCatalog, deleteAssetLibrary, } from '../api/assetLibraries' import type { AssetLibrary } from '../api/assetLibraries' import api from '../api/client' // ── UploadModal ──────────────────────────────────────────────────────────── function UploadModal({ onClose }: { onClose: () => void }) { const qc = useQueryClient() const [name, setName] = useState('') const [description, setDescription] = useState('') const [file, setFile] = useState(null) const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept: { 'application/octet-stream': ['.blend'] }, multiple: false, onDrop: (files) => { if (files[0]) setFile(files[0]) }, }) const uploadMut = useMutation({ mutationFn: () => { if (!file || !name.trim()) throw new Error('Name and file required') return createAssetLibrary({ name: name.trim(), description: description.trim() || undefined, blend_file: file }) }, onSuccess: () => { toast.success('Asset library uploaded') qc.invalidateQueries({ queryKey: ['asset-libraries'] }) onClose() }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Upload failed'), }) return (

Upload Asset Library

setName(e.target.value)} />
setDescription(e.target.value)} />
{file ? (

{file.name}

) : ( <>

{isDragActive ? 'Drop the .blend file here' : 'Drag & drop a .blend file, or click to browse'}

)}
) } // ── LibraryCard ──────────────────────────────────────────────────────────── function LibraryCard({ lib }: { lib: AssetLibrary }) { const qc = useQueryClient() const [expanded, setExpanded] = useState(false) const refreshMut = useMutation({ mutationFn: () => refreshAssetLibraryCatalog(lib.id), onSuccess: () => { toast.success('Catalog updated') qc.invalidateQueries({ queryKey: ['asset-libraries'] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Refresh failed'), }) const toggleMut = useMutation({ mutationFn: () => api.patch(`/asset-libraries/${lib.id}`, { is_active: !lib.is_active }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['asset-libraries'] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Toggle failed'), }) const deleteMut = useMutation({ mutationFn: () => deleteAssetLibrary(lib.id), onSuccess: () => { toast.success('Library deleted') qc.invalidateQueries({ queryKey: ['asset-libraries'] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Delete failed'), }) const materialCount = lib.catalog?.materials?.length ?? 0 const nodeGroupCount = lib.catalog?.node_groups?.length ?? 0 const MAX_VISIBLE = 10 return (
{/* Header row */}

{lib.name}

{lib.is_active ? 'active' : 'inactive'}
{lib.description && (

{lib.description}

)} {lib.original_filename && (

{lib.original_filename}

)}
{/* Actions */}
{/* Active toggle */}
{/* Catalog badges */}
{materialCount} material{materialCount !== 1 ? 's' : ''} {nodeGroupCount > 0 && ( {nodeGroupCount} node group{nodeGroupCount !== 1 ? 's' : ''} )}
{/* Expandable material list */} {materialCount > 0 && (
{expanded && (
{lib.catalog.materials.slice(0, MAX_VISIBLE).map((m) => ( {m} ))} {materialCount > MAX_VISIBLE && ( ... and {materialCount - MAX_VISIBLE} more )}
)}
)}
) } // ── AssetLibraryPage ─────────────────────────────────────────────────────── export default function AssetLibraryPage() { const [showUpload, setShowUpload] = useState(false) const { data: libraries, isLoading, isError } = useQuery({ queryKey: ['asset-libraries'], queryFn: listAssetLibraries, }) return (
{/* Header */}

Asset Libraries

Manage .blend material libraries used for Blender rendering.

{/* States */} {isLoading && (
Loading libraries...
)} {isError && (
Failed to load asset libraries. Please try again.
)} {!isLoading && !isError && libraries && libraries.length === 0 && (

No asset libraries.

Upload a .blend file to get started.

)} {!isLoading && !isError && libraries && libraries.length > 0 && (
{libraries.map((lib) => ( ))}
)} {showUpload && setShowUpload(false)} />}
) }