jam-tracks / frontend /src /components /generation /GenerationOutput.jsx
Mina Emadi
added the music generation functionality
59dcfdf
Raw History Blame Contribute Delete
7.19 kB
import React, { useRef, useState, useEffect } from 'react'
export default function GenerationOutput({
sessionId,
generationResult,
onRegenerate,
onUseAsSource,
onPlaySection,
}) {
const audioRef = useRef(null)
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [abSource, setAbSource] = useState('generated')
const [downloadFormat, setDownloadFormat] = useState('wav')
const [audioSrc, setAudioSrc] = useState(null)
const [loadError, setLoadError] = useState(false)
// Only set the audio src when we get a NEW generation result
useEffect(() => {
if (generationResult && sessionId) {
setAudioSrc(`/api/stem/${sessionId}/_continuation?format=wav&v=${Date.now()}`)
setIsPlaying(false)
setCurrentTime(0)
setDuration(0)
setAbSource('generated')
setLoadError(false)
} else {
setAudioSrc(null)
}
}, [generationResult, sessionId])
const handleTimeUpdate = () => {
if (audioRef.current) setCurrentTime(audioRef.current.currentTime)
}
const handleLoadedMetadata = () => {
if (audioRef.current) setDuration(audioRef.current.duration)
}
const handleEnded = () => setIsPlaying(false)
// Stop retrying on error — clear the src so the browser doesn't loop
const handleError = () => {
setLoadError(true)
setIsPlaying(false)
}
const togglePlay = () => {
if (!audioRef.current || loadError) return
if (isPlaying) {
audioRef.current.pause()
setIsPlaying(false)
} else {
audioRef.current.play().catch(() => setIsPlaying(false))
setIsPlaying(true)
}
}
const handleSeek = (e) => {
if (!audioRef.current || !duration) return
const rect = e.currentTarget.getBoundingClientRect()
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
audioRef.current.currentTime = pct * duration
}
const formatTime = (t) => {
const m = Math.floor(t / 60)
const s = Math.floor(t % 60).toString().padStart(2, '0')
return `${m}:${s}`
}
const handleDownload = () => {
if (!sessionId) return
const url = `/api/stem/${sessionId}/_continuation?format=${downloadFormat}`
const a = document.createElement('a')
a.href = url
a.download = `generation.${downloadFormat}`
a.click()
}
// Don't render anything until we have a generation result
if (!generationResult) return null
return (
<div className="space-y-3 pt-3 border-t border-white/[0.06]">
{/* Only mount <audio> when we have a valid src */}
{audioSrc && !loadError && (
<audio
ref={audioRef}
src={audioSrc}
preload="auto"
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onEnded={handleEnded}
onError={handleError}
/>
)}
{loadError && (
<div className="text-xs text-red-400 bg-red-500/10 rounded px-3 py-2">
Audio playback unavailable — session may have expired. Try generating again.
</div>
)}
{/* A/B Toggle */}
<div className="flex gap-1.5">
<button
onClick={() => setAbSource('generated')}
className={`px-3 py-1 rounded text-xs font-medium transition-all ${
abSource === 'generated'
? 'bg-accent-500/20 text-accent-400 border border-accent-500/30'
: 'bg-white/[0.04] text-gray-500 border border-transparent hover:bg-white/[0.06]'
}`}
>
Seed + Generated
</button>
<button
onClick={() => {
setAbSource('seed')
if (audioRef.current) { audioRef.current.pause(); setIsPlaying(false) }
if (onPlaySection) onPlaySection()
}}
className={`px-3 py-1 rounded text-xs font-medium transition-all ${
abSource === 'seed'
? 'bg-accent-500/20 text-accent-400 border border-accent-500/30'
: 'bg-white/[0.04] text-gray-500 border border-transparent hover:bg-white/[0.06]'
}`}
>
Seed Only
</button>
</div>
{/* Playback controls */}
{!loadError && (
<div className="space-y-2">
<div className="flex items-center gap-3">
<button
onClick={togglePlay}
disabled={abSource === 'seed'}
className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors text-white text-sm ${
abSource === 'seed' ? 'bg-gray-700 cursor-not-allowed' : 'bg-accent-500 hover:bg-accent-400'
}`}
>
{isPlaying ? '⏸' : '▶'}
</button>
<div
className="flex-1 h-1.5 rounded-full bg-white/[0.06] cursor-pointer"
onClick={handleSeek}
>
<div
className="h-full bg-accent-500 rounded-full transition-all"
style={{ width: duration ? `${(currentTime / duration) * 100}%` : '0%' }}
/>
</div>
<span className="text-xs font-mono text-gray-400 w-16 text-right">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
</div>
</div>
)}
{/* Metadata */}
<div className="flex flex-wrap gap-2 text-[10px]">
<span className="px-2 py-0.5 rounded bg-white/[0.04] text-gray-500">
Model: {generationResult.model || 'unknown'}
</span>
<span className="px-2 py-0.5 rounded bg-white/[0.04] text-gray-500">
Duration: {generationResult.duration_seconds?.toFixed(1)}s
</span>
<span className="px-2 py-0.5 rounded bg-white/[0.04] text-gray-500">
Generated in {generationResult.generation_time_seconds?.toFixed(1)}s
</span>
</div>
{/* Action buttons */}
<div className="flex items-center gap-2">
<button
onClick={onRegenerate}
className="px-3 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1] text-xs text-gray-300 transition-colors"
>
Regenerate
</button>
<button
onClick={onUseAsSource}
className="px-3 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1] text-xs text-gray-300 transition-colors"
>
Use as Source
</button>
{/* Download */}
<div className="ml-auto flex items-center gap-1">
<select
value={downloadFormat}
onChange={e => setDownloadFormat(e.target.value)}
className="bg-surface-elevated border border-white/[0.06] rounded px-1.5 py-1 text-[10px] text-gray-400 focus:outline-none"
>
<option value="wav">.wav</option>
<option value="flac">.flac</option>
</select>
<button
onClick={handleDownload}
className="px-2 py-1 rounded bg-white/[0.06] hover:bg-white/[0.1] text-[10px] text-gray-400 transition-colors"
>
Download
</button>
</div>
</div>
</div>
)
}