import { useEffect, useMemo } from 'react' import { Select } from './ui/select' import { videoGenerationResolutionLabel } from '../lib/video-resolution' import { areVideoGenerationSettingsEquivalent, resolveVideoGenerationOptions, sanitizeVideoGenerationSettings, type VideoGenerationModelSpecItem, type VideoGenerationPipeline, } from '../lib/video-generation-model-specs' export type GenerationMode = 'text-to-video' | 'image-to-video' | 'text-to-image' export interface LoraSelection { ref: string name: string scale: number // Catalog id (e.g. "cozy-felt-style"), when this LoRA was picked from the library — // absent for a manually-added/custom LoRA not in the catalog. Lets the prompt enhancer // look up trigger/instructions without re-matching on filename or display name. catalogId?: string } export interface GenerationSettings { model: VideoGenerationPipeline duration: number | null videoResolution: string fps: number audio: boolean cameraMotion: string aspectRatio?: string loras?: LoraSelection[] // Image-specific settings imageResolution: string imageAspectRatio: string imageSteps: number variations?: number // Number of image variations to generate imageEditStrength?: number // Denoising strength when editing an existing image } interface SettingsPanelProps { settings: GenerationSettings onSettingsChange: (settings: GenerationSettings) => void disabled?: boolean mode?: GenerationMode hasAudio?: boolean videoModelSpecs?: VideoGenerationModelSpecItem[] | null minimumDuration?: number hideDuration?: boolean videoSettingsMessage?: string | null } export function SettingsPanel({ settings, onSettingsChange, disabled, mode = 'text-to-video', hasAudio = false, videoModelSpecs, minimumDuration, hideDuration = false, videoSettingsMessage, }: SettingsPanelProps) { const isImageMode = mode === 'text-to-image' const resolvedVideoOptions = useMemo(() => { if (isImageMode || !videoModelSpecs || videoModelSpecs.length === 0) return null return resolveVideoGenerationOptions({ settings, modelSpecs: videoModelSpecs, hasAudio, minimumDuration, durationSelection: hideDuration ? 'smallest_valid' : 'preserve', }) }, [hasAudio, hideDuration, isImageMode, minimumDuration, settings, videoModelSpecs]) useEffect(() => { if (isImageMode || !videoModelSpecs || videoModelSpecs.length === 0) return const sanitized = sanitizeVideoGenerationSettings(settings, videoModelSpecs, { hasAudio, minimumDuration, durationSelection: hideDuration ? 'smallest_valid' : 'preserve', }) if (!sanitized) return if (!areVideoGenerationSettingsEquivalent(settings, sanitized)) { onSettingsChange(sanitized) } }, [hasAudio, hideDuration, isImageMode, minimumDuration, onSettingsChange, settings, videoModelSpecs]) const handleChange = (key: keyof GenerationSettings, value: string | number | boolean | null) => { if (isImageMode) { onSettingsChange({ ...settings, [key]: value } as GenerationSettings) return } if (!videoModelSpecs || videoModelSpecs.length === 0) return const nextSettings = { ...settings, [key]: value } as GenerationSettings const sanitized = sanitizeVideoGenerationSettings(nextSettings, videoModelSpecs, { hasAudio, minimumDuration, durationSelection: hideDuration ? 'smallest_valid' : 'preserve', }) if (sanitized) { onSettingsChange(sanitized) } } // Image mode settings if (isImageMode) { return (