import React, { useMemo, useEffect, useState, useRef } from 'react' import { X, Upload, Video, Image, Loader2, Sparkles, RefreshCw, Info } from 'lucide-react' import { pathToFileUrl } from '../../lib/file-url' import { SettingsPanel } from '../../components/SettingsPanel' import type { GenerationSettings } from '../../components/SettingsPanel' import type { GenerationMode } from '../../components/SettingsPanel' import type { VideoGenerationModelSpecItem } from '../../lib/video-generation-model-specs' interface TimelineGap { trackIndex: number startTime: number endTime: number } type GapGenerateMode = 'text-to-video' | 'image-to-video' | 'text-to-image' interface GapGenerationModalProps { selectedGap: TimelineGap | null anchorPosition?: { x: number; gapTop: number; gapBottom: number } | null gapGenerateMode: GapGenerateMode | null setGapGenerateMode: (mode: GapGenerateMode | null) => void gapPrompt: string setGapPrompt: (prompt: string) => void gapSuggesting: boolean gapSuggestion: string | null gapBeforeFrame: string | null gapAfterFrame: string | null gapSettings: GenerationSettings setGapSettings: (settings: GenerationSettings) => void gapVideoModelSpecs: VideoGenerationModelSpecItem[] gapVideoSettingsMessage?: string | null gapCanGenerateVideo: boolean gapImageFile: File | null setGapImageFile: (file: File | null) => void gapImageInputRef: React.RefObject isRegenerating: boolean regenStatusMessage: string regenProgress: number regenReset: () => void handleGapGenerate: () => void handleCloseGap: () => void setSelectedGap: (gap: TimelineGap | null) => void gapApplyAudioToTrack: boolean setGapApplyAudioToTrack: (v: boolean) => void regenerateSuggestion: () => void gapSuggestionError?: boolean gapSuggestionNoApiKey?: boolean } export function GapGenerationModal({ selectedGap, gapGenerateMode, setGapGenerateMode, gapPrompt, setGapPrompt, gapSuggesting, gapSuggestion, gapBeforeFrame, gapAfterFrame, gapSettings, setGapSettings, gapVideoModelSpecs, gapVideoSettingsMessage, gapCanGenerateVideo, gapImageFile, setGapImageFile, gapImageInputRef, isRegenerating, regenStatusMessage, regenProgress, regenReset, handleGapGenerate, handleCloseGap, setSelectedGap, gapApplyAudioToTrack, setGapApplyAudioToTrack, regenerateSuggestion, gapSuggestionError, gapSuggestionNoApiKey, anchorPosition, }: GapGenerationModalProps) { if (!selectedGap) return null const isVideoMode = gapGenerateMode === 'text-to-video' || gapGenerateMode === 'image-to-video' const isImageMode = gapGenerateMode === 'text-to-image' const gapImageUrl = useMemo(() => { if (!gapImageFile) return null return URL.createObjectURL(gapImageFile) }, [gapImageFile]) const modalTitle = isVideoMode ? (gapImageFile ? 'Image to Video' : 'Generate Video') : 'Generate Image' const settingsMode: GenerationMode = isVideoMode ? (gapImageFile ? 'image-to-video' : 'text-to-video') : 'text-to-image' useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.stopPropagation() if (gapGenerateMode) { setGapGenerateMode(null) regenReset() } else { setSelectedGap(null) } } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [gapGenerateMode, setGapGenerateMode, regenReset, setSelectedGap]) const [startFrameEnabled, setStartFrameEnabled] = useState(true) const [endFrameEnabled, setEndFrameEnabled] = useState(false) const [startFrameOverride, setStartFrameOverride] = useState(null) const [endFrameOverride, setEndFrameOverride] = useState(null) const startFrameInputRef = useRef(null) const endFrameInputRef = useRef(null) useEffect(() => { setStartFrameEnabled(true) setEndFrameEnabled(false) setStartFrameOverride(null) setEndFrameOverride(null) }, [gapGenerateMode]) const displayedBeforeFrame = startFrameOverride ?? gapBeforeFrame const displayedAfterFrame = endFrameOverride ?? gapAfterFrame const handleFrameFileChange = ( e: React.ChangeEvent, setter: (v: string | null) => void, onSelect: () => void ) => { const file = e.target.files?.[0] if (!file) return const reader = new FileReader() reader.onload = (ev) => { setter(ev.target?.result as string); onSelect() } reader.readAsDataURL(file) e.target.value = '' } return ( <> {gapGenerateMode && (
{/* Header */}
{isVideoMode ?

{modalTitle}

Fill {(selectedGap.endTime - selectedGap.startTime).toFixed(1)}s gap on Track {selectedGap.trackIndex + 1}

{/* Timeline visualization */}
{/* Label + toggle row */}
{isVideoMode ? 'Generate from' : 'Context frames'}
{isVideoMode ? ( <>

Only one conditioning frame can be used at a time.

If End frame is selected, it will be treated as the start frame, since the model does not currently support generating from an end frame. The video will then be generated from that frame and played in reverse.

) : (

Select frames from adjacent clips to provide context for the prompt.

)}
{/* Segmented toggle */}
{/* Frame strip */}
{/* Before frame */} {displayedBeforeFrame ? (
{ if (startFrameEnabled) { setStartFrameEnabled(false) } else { setStartFrameEnabled(true); setEndFrameEnabled(false) } }} > {/* Replace button */}
Replace image
{/* Selection border */} {startFrameEnabled && (
)}
) : (
No clip
)} {/* Center gap area */}
{isVideoMode && gapImageFile && gapImageUrl ? (
Source frame
) : (
AI fills this gap
)}
{/* After frame */} {displayedAfterFrame ? (
{ if (endFrameEnabled) { setEndFrameEnabled(false) } else { setEndFrameEnabled(true); setStartFrameEnabled(false) } }} > {/* Replace button */}
Replace image
{/* Selection border */} {endFrameEnabled && (
)}
) : (
No clip
)}
{/* Body */}
{/* Prompt */}
{gapSuggesting && (
Analyzing...
)} {!gapSuggesting && gapSuggestion && gapPrompt === gapSuggestion && (
AI-suggested
)} {!gapSuggesting && !gapSuggestionNoApiKey && ( )}