| import React, { useState, useCallback, useEffect, useRef } from 'react' |
| import ModelSelector from './ModelSelector' |
| import SeedRegionInfo from './SeedRegionInfo' |
| import TaskTypeSelector from './TaskTypeSelector' |
| import MusicGenParams from './MusicGenParams' |
| import ACEStepParams from './ACEStepParams' |
| import GenerationProgress from './GenerationProgress' |
| import GenerationOutput from './GenerationOutput' |
|
|
| |
| function getTaskTypeKey(model) { |
| if (model === 'ace-step') return 'task_type' |
| return null |
| } |
|
|
| export default function GenerationPanel({ |
| sessionId, |
| detection, |
| regionStart, |
| regionEnd, |
| hasRegion, |
| onPlaySection, |
| generation, // all state from useGeneration hook |
| }) { |
| const [isCollapsed, setIsCollapsed] = useState(false) |
| const [isExtracting, setIsExtracting] = useState(false) |
|
|
| const { |
| selectedModel, |
| setSelectedModel, |
| modelParams, |
| setModelParam, |
| isGenerating, |
| generationResult, |
| generationProgress, |
| modelAvailability, |
| seedData, |
| doExtractSeed, |
| startGeneration, |
| setGenerationResult, |
| } = generation |
|
|
| const currentParams = modelParams[selectedModel] || {} |
|
|
| |
| const taskTypeKey = getTaskTypeKey(selectedModel) |
| const taskType = taskTypeKey ? currentParams[taskTypeKey] : 'continuation' |
|
|
| const handleTaskTypeChange = useCallback((newType) => { |
| if (taskTypeKey) { |
| setModelParam(selectedModel, taskTypeKey, newType) |
| } |
| }, [selectedModel, taskTypeKey, setModelParam]) |
|
|
| const handleParamChange = useCallback((key, value) => { |
| setModelParam(selectedModel, key, value) |
| }, [selectedModel, setModelParam]) |
|
|
| |
| const regionRef = useRef({ start: regionStart, end: regionEnd }) |
| useEffect(() => { |
| regionRef.current = { start: regionStart, end: regionEnd } |
| }, [regionStart, regionEnd]) |
|
|
| useEffect(() => { |
| if (!hasRegion || !sessionId) return |
| if (regionEnd - regionStart < 3) return |
|
|
| const timer = setTimeout(async () => { |
| |
| if (regionRef.current.start === regionStart && regionRef.current.end === regionEnd) { |
| setIsExtracting(true) |
| await doExtractSeed(regionStart, regionEnd) |
| setIsExtracting(false) |
| } |
| }, 1000) |
|
|
| return () => clearTimeout(timer) |
| |
| }, [regionStart, regionEnd, hasRegion, sessionId]) |
|
|
| const handleGenerate = useCallback(async () => { |
| if (!hasRegion) return |
| await startGeneration(regionStart, regionEnd) |
| }, [hasRegion, regionStart, regionEnd, startGeneration]) |
|
|
| const handleRegenerate = useCallback(async () => { |
| |
| if (selectedModel === 'musicgen') { |
| setModelParam('musicgen', 'seed', Math.floor(Math.random() * 999999)) |
| } else if (selectedModel === 'ace-step') { |
| setModelParam('ace-step', 'seed', Math.floor(Math.random() * 999999)) |
| } |
| |
| setTimeout(() => handleGenerate(), 50) |
| }, [selectedModel, setModelParam, handleGenerate]) |
|
|
| const handleUseAsSource = useCallback(() => { |
| |
| |
| setGenerationResult(null) |
| }, [setGenerationResult]) |
|
|
| const canGenerate = hasRegion && !isGenerating && (regionEnd - regionStart >= 3) |
|
|
| return ( |
| <div className="card rounded-xl p-4 animate-fade-in"> |
| {/* Header */} |
| <button |
| onClick={() => setIsCollapsed(!isCollapsed)} |
| className="w-full flex items-center justify-between mb-3" |
| > |
| <div className="flex items-center gap-2"> |
| <span className="text-[10px] uppercase tracking-widest text-accent-500 font-medium"> |
| AI Generation |
| </span> |
| </div> |
| <span className={`text-gray-500 text-xs transition-transform ${isCollapsed ? '' : 'rotate-180'}`}> |
| ▼ |
| </span> |
| </button> |
| |
| {!isCollapsed && ( |
| <div className="space-y-4"> |
| {/* Model selector tabs */} |
| <ModelSelector |
| selectedModel={selectedModel} |
| onModelChange={setSelectedModel} |
| availability={modelAvailability} |
| /> |
| |
| {/* Seed region info */} |
| <SeedRegionInfo |
| seedData={seedData} |
| regionStart={regionStart} |
| regionEnd={regionEnd} |
| hasRegion={hasRegion} |
| onPreviewRegion={onPlaySection} |
| isExtracting={isExtracting} |
| /> |
| |
| {/* Task type selector (per model) */} |
| <TaskTypeSelector |
| model={selectedModel} |
| taskType={taskType} |
| onTaskTypeChange={handleTaskTypeChange} |
| /> |
| |
| {/* Model-specific parameters */} |
| <div className="transition-all"> |
| {selectedModel === 'musicgen' && ( |
| <MusicGenParams |
| params={currentParams} |
| onParamChange={handleParamChange} |
| /> |
| )} |
| {selectedModel === 'ace-step' && ( |
| <ACEStepParams |
| params={currentParams} |
| onParamChange={handleParamChange} |
| detection={detection} |
| /> |
| )} |
| </div> |
| |
| {/* Generate button */} |
| <button |
| onClick={handleGenerate} |
| disabled={!canGenerate} |
| className={`w-full py-2.5 rounded-lg text-sm font-medium transition-all ${ |
| canGenerate |
| ? 'bg-accent-500 hover:bg-accent-400 text-white' |
| : 'bg-white/[0.04] text-gray-600 cursor-not-allowed' |
| }`} |
| > |
| {isGenerating |
| ? 'Generating...' |
| : `\u25B6 Generate with ${selectedModel === 'musicgen' ? 'MusicGen' : 'ACE-Step'}` |
| } |
| </button> |
| |
| {/* Progress */} |
| <GenerationProgress |
| model={selectedModel} |
| progress={generationProgress} |
| isGenerating={isGenerating} |
| /> |
| |
| {/* Output */} |
| <GenerationOutput |
| sessionId={sessionId} |
| generationResult={generationResult} |
| onRegenerate={handleRegenerate} |
| onUseAsSource={handleUseAsSource} |
| onPlaySection={onPlaySection} |
| /> |
| </div> |
| )} |
| </div> |
| ) |
| } |
|
|