import { useState, useEffect } from 'react' import Generator from './components/Generator' import Gallery from './components/Gallery' import Player from './components/Player' import DetailPanel from './components/DetailPanel' import SettingsModal from './components/SettingsModal' import ArtworkGenerationModal from './components/ArtworkGenerationModal' interface Clip { id: string title?: string prompt: string model: string duration_seconds: number status: string error_message?: string file_size_bytes?: number artwork_filename?: string | null bpm?: number | null key?: string | null created_at: string } function App() { const [clips, setClips] = useState([]) const [currentClip, setCurrentClip] = useState(null) const [isPlaying, setIsPlaying] = useState(false) const [selectedClip, setSelectedClip] = useState(null) const [settingsOpen, setSettingsOpen] = useState(false) const [artworkGenOpen, setArtworkGenOpen] = useState(false) const [artworkGenClip, setArtworkGenClip] = useState(null) const fetchClips = async () => { try { const res = await fetch('/api/clips') const data = await res.json() setClips(data.clips) // Fetch analysis for clips that don't have it const missing = data.clips.filter((c: Clip) => c.status === 'ready' && (!c.bpm || !c.key)) missing.forEach(async (clip: Clip) => { try { const res = await fetch(`/api/clips/${clip.id}/analysis`) if (res.ok) { const analysis = await res.json() setClips(prev => prev.map(c => c.id === clip.id ? { ...c, bpm: analysis.bpm, key: analysis.key } : c)) } } catch (err) { // Silently fail — analysis is optional } }) } catch (err) { console.error('Failed to fetch clips:', err) } } useEffect(() => { fetchClips() const interval = setInterval(fetchClips, 5000) return () => clearInterval(interval) }, []) const handlePlay = (clip: Clip) => { setCurrentClip(clip) setIsPlaying(true) } const handleDelete = async (id: string) => { try { await fetch(`/api/clips/${id}`, { method: 'DELETE' }) if (currentClip?.id === id) { setCurrentClip(null) setIsPlaying(false) } fetchClips() } catch (err) { console.error('Failed to delete clip:', err) } } const handleRename = async (id: string, title: string) => { try { await fetch(`/api/clips/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }), }) setClips(prev => prev.map(c => c.id === id ? { ...c, title } : c)) if (currentClip?.id === id) { setCurrentClip({ ...currentClip, title }) } if (selectedClip?.id === id) { setSelectedClip({ ...selectedClip, title }) } } catch (err) { console.error('Failed to rename clip:', err) } } const handleUploadArtwork = async (id: string, file: File) => { try { const formData = new FormData() formData.append('file', file) const res = await fetch(`/api/clips/${id}/artwork`, { method: 'PATCH', body: formData, }) if (!res.ok) { const err = await res.json() console.error('Artwork upload failed:', err.detail) return } const updated = await res.json() setClips(prev => prev.map(c => c.id === id ? { ...c, artwork_filename: updated.artwork_filename } : c)) if (currentClip?.id === id) { setCurrentClip({ ...currentClip, artwork_filename: updated.artwork_filename }) } if (selectedClip?.id === id) { setSelectedClip({ ...selectedClip, artwork_filename: updated.artwork_filename }) } } catch (err) { console.error('Failed to upload artwork:', err) } } const handleRemoveArtwork = async (id: string) => { try { await fetch(`/api/clips/${id}/artwork`, { method: 'DELETE' }) setClips(prev => prev.map(c => c.id === id ? { ...c, artwork_filename: null } : c)) if (currentClip?.id === id) { setCurrentClip({ ...currentClip, artwork_filename: null }) } if (selectedClip?.id === id) { setSelectedClip({ ...selectedClip, artwork_filename: null }) } } catch (err) { console.error('Failed to remove artwork:', err) } } const handleGenerateArtwork = async (clip: Clip) => { setArtworkGenClip(clip) setArtworkGenOpen(true) } const handleArtworkGenSubmit = async (prompt: string) => { if (!artworkGenClip) return try { const res = await fetch(`/api/clips/${artworkGenClip.id}/generate-artwork`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }), }) if (res.ok) { const updated = await res.json() setClips(prev => prev.map(c => c.id === updated.id ? updated : c)) if (currentClip?.id === updated.id) { setCurrentClip(updated) } if (selectedClip?.id === updated.id) { setSelectedClip(updated) } } else { const err = await res.json() alert(err.detail || 'Failed to generate artwork') } } catch (err) { console.error('Failed to generate artwork:', err) alert('Failed to generate artwork') } } const handleSelectClip = async (clip: Clip) => { setSelectedClip(clip) // Fetch analysis if not already cached if (!clip.bpm || !clip.key) { try { const res = await fetch(`/api/clips/${clip.id}/analysis`) if (res.ok) { const analysis = await res.json() setClips(prev => prev.map(c => c.id === clip.id ? { ...c, bpm: analysis.bpm, key: analysis.key } : c)) setSelectedClip({ ...clip, bpm: analysis.bpm, key: analysis.key }) } } catch (err) { console.error('Failed to fetch analysis:', err) } } } const handleClearWaveforms = async () => { try { await fetch('/api/admin/clear-waveforms', { method: 'POST' }) } catch (err) { console.error('Failed to clear waveforms:', err) } } const handleDeleteAll = async () => { try { await fetch('/api/admin/delete-all-clips', { method: 'POST' }) setClips([]) setCurrentClip(null) setIsPlaying(false) setSelectedClip(null) } catch (err) { console.error('Failed to delete all clips:', err) } } return (
L

Lyria Studio

v0.1
{currentClip && ( setIsPlaying(!isPlaying)} onStop={() => setIsPlaying(false)} onClose={() => { setCurrentClip(null) setIsPlaying(false) }} /> )} {selectedClip && ( setSelectedClip(null)} onRename={handleRename} onDelete={handleDelete} onUploadArtwork={handleUploadArtwork} onRemoveArtwork={handleRemoveArtwork} onPlay={handlePlay} onTogglePlay={() => setIsPlaying(!isPlaying)} onGenerateArtwork={handleGenerateArtwork} /> )} {artworkGenOpen && artworkGenClip && ( { setArtworkGenOpen(false) setArtworkGenClip(null) }} onGenerate={handleArtworkGenSubmit} trackName={artworkGenClip.title || artworkGenClip.prompt} /> )} setSettingsOpen(false)} onClearWaveforms={handleClearWaveforms} onDeleteAll={handleDeleteAll} clipCount={clips.length} />
) } export default App