diff --git a/src/components/generation/EnhancePanel.tsx b/src/components/generation/EnhancePanel.tsx index d02a0019a..f256bb2e2 100644 --- a/src/components/generation/EnhancePanel.tsx +++ b/src/components/generation/EnhancePanel.tsx @@ -50,6 +50,8 @@ interface ResultEntry { durationSec: number; peaks: number[]; timestamp: number; + status: 'generating' | 'ready' | 'error'; + error?: string; } type ABSide = 'A' | 'B'; @@ -160,6 +162,9 @@ export function EnhancePanel() { // Quick Styles section const [quickStylesOpen, setQuickStylesOpen] = useState(false); + // Local guard against rapid Generate clicks (supplements store-level isGenerating) + const [isSubmitting, setIsSubmitting] = useState(false); + // Playback const playback = useEnhancePlayback(); @@ -331,17 +336,27 @@ export function EnhancePanel() { } } } - if (!audioKey) return; + if (!audioKey) { + setResults((prev) => prev.map((r) => + r.id === resultId ? { ...r, status: 'error' as const, error: 'No audio key found for result' } : r, + )); + return; + } try { const buffer = await playback.loadBuffer(audioKey); - if (!buffer) return; + if (!buffer) { + setResults((prev) => prev.map((r) => + r.id === resultId ? { ...r, status: 'error' as const, error: 'Failed to load audio buffer' } : r, + )); + return; + } const peaks = computeWaveformPeaks(buffer, 60); const dur = buffer.duration; const finalClipId = updatedClip?.id ?? originalClipId; setResults((prev) => prev.map((r) => r.id === resultId - ? { ...r, clipId: finalClipId, audioKey, peaks, duration: formatDuration(dur), durationSec: dur } + ? { ...r, clipId: finalClipId, audioKey, peaks, duration: formatDuration(dur), durationSec: dur, status: 'ready' as const } : r, )); // Auto-select first result @@ -350,14 +365,18 @@ export function EnhancePanel() { if (prev === 0) return Math.max(0, results.length); // point to new entry return prev; }); - } catch { - // Audio decode failed — leave duration as --:-- + } catch (err) { + const message = err instanceof Error ? err.message : 'Audio decode failed'; + setResults((prev) => prev.map((r) => + r.id === resultId ? { ...r, status: 'error' as const, error: message } : r, + )); } }, [playback, results.length, enhancerTarget]); // Cover generation const handleCoverGenerate = useCallback(async () => { - if (!enhancerTarget || isGenerating) return; + if (!enhancerTarget || isGenerating || isSubmitting) return; + setIsSubmitting(true); const coverStrength = CONSISTENCY_VALUES[consistency]; const resultId = `result-${Date.now()}`; setResults((prev) => [...prev, { @@ -369,22 +388,33 @@ export function EnhancePanel() { durationSec: 0, peaks: [], timestamp: Date.now(), + status: 'generating', }]); - const newClipId = await generateCoverClip({ - clipId: enhancerTarget.clipId, - caption, - lyrics, - coverStrength, - createNew, - sourceAudioOverride: chainedSourceAudioKey || undefined, - }); - // After generation, try to load the result audio to get peaks/duration - await finalizeResult(resultId, enhancerTarget.clipId, newClipId); - }, [enhancerTarget, caption, lyrics, consistency, createNew, isGenerating, chainedSourceAudioKey, finalizeResult]); + try { + const newClipId = await generateCoverClip({ + clipId: enhancerTarget.clipId, + caption, + lyrics, + coverStrength, + createNew, + sourceAudioOverride: chainedSourceAudioKey || undefined, + }); + // After generation, try to load the result audio to get peaks/duration + await finalizeResult(resultId, enhancerTarget.clipId, newClipId); + } catch (err) { + const message = err instanceof Error ? err.message : 'Enhancement failed'; + setResults((prev) => prev.map((r) => + r.id === resultId ? { ...r, status: 'error' as const, error: message } : r, + )); + } finally { + setIsSubmitting(false); + } + }, [enhancerTarget, caption, lyrics, consistency, createNew, isGenerating, isSubmitting, chainedSourceAudioKey, finalizeResult]); // Repaint generation const handleRepaintGenerate = useCallback(async () => { - if (!enhancerTarget || isGenerating) return; + if (!enhancerTarget || isGenerating || isSubmitting) return; + setIsSubmitting(true); const resultId = `result-${Date.now()}`; setResults((prev) => [...prev, { id: resultId, @@ -395,19 +425,29 @@ export function EnhancePanel() { durationSec: 0, peaks: [], timestamp: Date.now(), + status: 'generating', }]); - const newClipId = await generateRepaintClip({ - clipId: enhancerTarget.clipId, - repaintStart: selStart, - repaintEnd: selEnd, - prompt, - globalCaption: globalCaption || undefined, - repaintMode, - repaintStrength, - sourceAudioOverride: chainedSourceAudioKey || undefined, - }); - await finalizeResult(resultId, enhancerTarget.clipId, newClipId); - }, [enhancerTarget, selStart, selEnd, prompt, globalCaption, repaintMode, repaintStrength, isGenerating, chainedSourceAudioKey, finalizeResult]); + try { + const newClipId = await generateRepaintClip({ + clipId: enhancerTarget.clipId, + repaintStart: selStart, + repaintEnd: selEnd, + prompt, + globalCaption: globalCaption || undefined, + repaintMode, + repaintStrength, + sourceAudioOverride: chainedSourceAudioKey || undefined, + }); + await finalizeResult(resultId, enhancerTarget.clipId, newClipId); + } catch (err) { + const message = err instanceof Error ? err.message : 'Repaint failed'; + setResults((prev) => prev.map((r) => + r.id === resultId ? { ...r, status: 'error' as const, error: message } : r, + )); + } finally { + setIsSubmitting(false); + } + }, [enhancerTarget, selStart, selEnd, prompt, globalCaption, repaintMode, repaintStrength, isGenerating, isSubmitting, chainedSourceAudioKey, finalizeResult]); const handleGenerate = mode === 'cover' ? handleCoverGenerate : handleRepaintGenerate; @@ -560,7 +600,7 @@ export function EnhancePanel() { const coverSupported = modelSupportsTaskType('cover'); const repaintSupported = modelSupportsTaskType('repaint'); const modeSupported = mode === 'cover' ? coverSupported : repaintSupported; - const canGenerate = hasAudio && modeSupported && !isGenerating && !!(clip && track); + const canGenerate = hasAudio && modeSupported && !isGenerating && !isSubmitting && !!(clip && track); const clipStart = clip?.startTime ?? 0; const clipEnd = (clip?.startTime ?? 0) + (clip?.duration ?? 0); @@ -1055,7 +1095,7 @@ export function EnhancePanel() { : 'bg-[#2a2a2e] text-zinc-500 cursor-not-allowed' }`} > - {isGenerating + {isGenerating || isSubmitting ? (mode === 'cover' ? 'Enhancing...' : 'Repainting...') : (mode === 'cover' ? 'Enhance' : 'Repaint Selection') } @@ -1094,6 +1134,15 @@ export function EnhancePanel() { }`} >
+
{r.title} {canAB && isSelected && ( B )}
-{r.duration}
++ {r.status === 'generating' ? 'Generating...' : r.status === 'error' ? (r.error ?? 'Failed') : r.duration} +