diff --git a/web/frontend/src/components/transcription/FormHelpers.tsx b/web/frontend/src/components/transcription/FormHelpers.tsx index 94d96500..e9bf87f8 100644 --- a/web/frontend/src/components/transcription/FormHelpers.tsx +++ b/web/frontend/src/components/transcription/FormHelpers.tsx @@ -1,8 +1,50 @@ import type { ReactNode } from "react"; import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Slider } from "@/components/ui/slider"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import { Info } from "lucide-react"; +// ============================================================================ +// Shared CSS class constants for form inputs +// ============================================================================ + +export const inputClassName = ` + h-11 bg-[var(--bg-main)] border border-[var(--border-subtle)] rounded-xl + text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] + focus:border-[var(--brand-solid)] focus:ring-2 focus:ring-[var(--brand-solid)]/20 + transition-all duration-200 + [color-scheme:light] dark:[color-scheme:dark] +`; + +const selectTriggerClassName = ` + h-11 bg-[var(--bg-main)] border border-[var(--border-subtle)] rounded-xl + text-[var(--text-primary)] shadow-none + focus:border-[var(--brand-solid)] focus:ring-2 focus:ring-[var(--brand-solid)]/20 +`; + +const selectContentClassName = ` + bg-[var(--bg-card)] border border-[var(--border-subtle)] rounded-xl +`; + +const selectItemClassName = ` + text-[var(--text-primary)] rounded-lg mx-1 cursor-pointer + focus:bg-[var(--brand-light)] focus:text-[var(--brand-solid)] +`; + interface FormFieldProps { label: string; htmlFor?: string; @@ -117,3 +159,106 @@ export function InfoBanner({ variant, title, children }: InfoBannerProps) { ); } + +// ============================================================================ +// Composite Field Helpers +// ============================================================================ + +/** + * SelectField - FormField + Select boilerplate in one component. + * Accepts either {value, label}[] objects or plain string[] arrays. + */ +export function SelectField({ label, description, optional, value, onValueChange, options }: { + label: string; + description?: string; + optional?: boolean; + value: string; + onValueChange: (value: string) => void; + options: readonly { value: string; label: string }[] | string[]; +}) { + return ( + + + + ); +} + +/** + * SwitchField - Switch toggle with label in a consistent layout. + */ +export function SwitchField({ id, label, checked, onCheckedChange }: { + id: string; + label: string; + checked: boolean; + onCheckedChange: (checked: boolean) => void; +}) { + return ( +
+ + +
+ ); +} + +/** + * SliderField - FormField + Slider with min/max range labels and current value display. + */ +export function SliderField({ label, value, onValueChange, min, max, step }: { + label: string; + value: number; + onValueChange: (value: number) => void; + min: number; + max: number; + step: number; +}) { + return ( +
+ + onValueChange(v[0])} + min={min} + max={max} + step={step} + className="w-full" + /> +
+ {min} + {value} + {max} +
+
+
+ ); +} + +/** + * AdvancedAccordion - Collapsible "Advanced Settings" section. + */ +export function AdvancedAccordion({ children }: { children: ReactNode }) { + return ( + + + + Advanced Settings + + + {children} + + + + ); +} diff --git a/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx b/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx index 01f5da56..4f46aa33 100644 --- a/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx +++ b/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx @@ -10,24 +10,12 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { Switch } from "@/components/ui/switch"; -import { Slider } from "@/components/ui/slider"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; import { Loader2, Check, XCircle } from "lucide-react"; import { useAuth } from "@/features/auth/hooks/useAuth"; -import { FormField, Section, InfoBanner } from "@/components/transcription/FormHelpers"; +import { + FormField, Section, InfoBanner, SelectField, SwitchField, SliderField, AdvancedAccordion, + inputClassName, +} from "@/components/transcription/FormHelpers"; // ============================================================================ // Types & Constants @@ -234,33 +222,6 @@ const PARAM_DESCRIPTIONS = { vad_offset: "Speech ending sensitivity. Lower values detect speech endings more precisely.", }; -// ============================================================================ -// Styled Input/Select Components -// ============================================================================ - -const inputClassName = ` - h-11 bg-[var(--bg-main)] border border-[var(--border-subtle)] rounded-xl - text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] - focus:border-[var(--brand-solid)] focus:ring-2 focus:ring-[var(--brand-solid)]/20 - transition-all duration-200 - [color-scheme:light] dark:[color-scheme:dark] -`; - -const selectTriggerClassName = ` - h-11 bg-[var(--bg-main)] border border-[var(--border-subtle)] rounded-xl - text-[var(--text-primary)] shadow-none - focus:border-[var(--brand-solid)] focus:ring-2 focus:ring-[var(--brand-solid)]/20 -`; - -const selectContentClassName = ` - bg-[var(--bg-card)] border border-[var(--border-subtle)] rounded-xl -`; - -const selectItemClassName = ` - text-[var(--text-primary)] rounded-lg mx-1 cursor-pointer - focus:bg-[var(--brand-light)] focus:text-[var(--brand-solid)] -`; - // ============================================================================ // Main Component // ============================================================================ @@ -400,36 +361,19 @@ export const TranscriptionConfigDialog = memo(function TranscriptionConfigDialog )} {/* Model Family Selection */} - - - + value={params.model_family} + onValueChange={(v) => updateParam('model_family', v)} + options={[ + { value: "whisper", label: "Whisper" }, + { value: "nvidia_parakeet", label: "NVIDIA Parakeet" }, + { value: "nvidia_canary", label: "NVIDIA Canary" }, + { value: "mistral_voxtral", label: "Mistral Voxtral" }, + { value: "openai", label: "OpenAI" }, + ]} + /> {/* Multi-track notice */} {isMultiTrack && ( @@ -440,46 +384,24 @@ export const TranscriptionConfigDialog = memo(function TranscriptionConfigDialog {/* Model-Specific Configuration */} {params.model_family === "whisper" && ( - + )} - {params.model_family === "nvidia_parakeet" && ( - + )} - {params.model_family === "nvidia_canary" && ( - + )} - {params.model_family === "openai" && ( )} - {params.model_family === "mistral_voxtral" && ( - + )} @@ -513,119 +435,56 @@ export const TranscriptionConfigDialog = memo(function TranscriptionConfigDialog }); // ============================================================================ -// Model-Specific Configuration Components +// Shared Diarization Section // ============================================================================ -interface ConfigProps { +function DiarizationSection({ id, params, updateParam, description }: { + id: string; params: WhisperXParams; updateParam: (key: K, value: WhisperXParams[K]) => void; - isMultiTrack?: boolean; -} - -function WhisperConfig({ params, updateParam, isMultiTrack }: ConfigProps) { + description?: string; +}) { return ( -
- {/* Essential Settings */} -
-
- - - - - - - - - - - - - - - -
-
+
+
+ updateParam('diarize', v)} /> + + {params.diarize && ( +
+ updateParam('diarize_model', v)} + options={[ + { value: "pyannote", label: "Pyannote" }, + { value: "nvidia_sortformer", label: "NVIDIA Sortformer" }, + ]} + /> - {/* Speaker Diarization */} - {!isMultiTrack && ( -
-
-
- updateParam('diarize', v)} - /> - +
+ + updateParam('min_speakers', e.target.value ? parseInt(e.target.value) : undefined)} + className={inputClassName} + /> + + + updateParam('max_speakers', e.target.value ? parseInt(e.target.value) : undefined)} + className={inputClassName} + /> +
- {params.diarize && ( -
-
- - updateParam('min_speakers', e.target.value ? parseInt(e.target.value) : undefined)} - className={inputClassName} - /> - - - updateParam('max_speakers', e.target.value ? parseInt(e.target.value) : undefined)} - className={inputClassName} - /> - -
- + {params.diarize_model === "pyannote" && ( + <> updateParam('hf_token', e.target.value || undefined)} className={inputClassName} @@ -637,10 +496,7 @@ function WhisperConfig({ params, updateParam, isMultiTrack }: ConfigProps) {
updateParam('vad_onset', parseFloat(e.target.value) || 0.5)} className={inputClassName} @@ -648,10 +504,7 @@ function WhisperConfig({ params, updateParam, isMultiTrack }: ConfigProps) { updateParam('vad_offset', parseFloat(e.target.value) || 0.363)} className={inputClassName} @@ -659,116 +512,82 @@ function WhisperConfig({ params, updateParam, isMultiTrack }: ConfigProps) {
-
+ )}
-
- )} + )} +
+
+ ); +} - {/* Advanced Settings (Accordion) */} - - - - Advanced Settings - - -
- - - +// ============================================================================ +// Model-Specific Configuration Components +// ============================================================================ - - updateParam('batch_size', parseInt(e.target.value) || 8)} - className={inputClassName} - /> - +interface ConfigProps { + params: WhisperXParams; + updateParam: (key: K, value: WhisperXParams[K]) => void; + isMultiTrack?: boolean; +} - - updateParam('beam_size', parseInt(e.target.value) || 5)} - className={inputClassName} - /> - +function WhisperConfig({ params, updateParam, isMultiTrack }: ConfigProps) { + return ( +
+
+
+ updateParam('model', v)} options={WHISPER_MODELS} /> + updateParam('language', v === "auto" ? undefined : v)} options={LANGUAGES} /> + updateParam('task', v)} options={[{ value: "transcribe", label: "Transcribe" }, { value: "translate", label: "Translate to English" }]} /> + updateParam('device', v)} options={[{ value: "cpu", label: "CPU" }, { value: "cuda", label: "GPU (CUDA)" }]} /> +
+
- - updateParam('temperature', parseFloat(e.target.value) || 0)} - className={inputClassName} - /> - -
+ {!isMultiTrack && ( + + )} - -