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 && (
+
+ )}
-
-
+
+
+ updateParam('compute_type', v)} options={[{ value: "float32", label: "Float32 (Accurate)" }, { value: "float16", label: "Float16 (Fast)" }, { value: "int8", label: "Int8 (Fastest)" }]} />
+
+ updateParam('batch_size', parseInt(e.target.value) || 8)} className={inputClassName} />
+
+
+ updateParam('beam_size', parseInt(e.target.value) || 5)} className={inputClassName} />
+
+
+ updateParam('temperature', parseFloat(e.target.value) || 0)} className={inputClassName} />
+
+
-
- updateParam('suppress_numerals', v)}
- />
-
-
+
+
- {/* Alignment Settings */}
-
-
- updateParam('no_align', v)}
- />
-
-
+
updateParam('suppress_numerals', v)} />
- {!params.no_align && (
-
- updateParam('align_model', e.target.value || undefined)}
- className={inputClassName}
- />
-
- )}
-
-
-
-
+
+ updateParam('no_align', v)} />
+
+ {!params.no_align && (
+
+ updateParam('align_model', e.target.value || undefined)}
+ className={inputClassName}
+ />
+
+ )}
+
+
);
}
@@ -776,146 +595,15 @@ function WhisperConfig({ params, updateParam, isMultiTrack }: ConfigProps) {
function ParakeetConfig({ params, updateParam, isMultiTrack }: ConfigProps) {
return (
- {/* Long-form Audio Settings */}
-
-
- updateParam('attention_context_left', v[0])}
- max={512}
- min={64}
- step={64}
- className="w-full"
- />
-
- 64
- {params.attention_context_left}
- 512
-
-
-
-
-
-
- updateParam('attention_context_right', v[0])}
- max={512}
- min={64}
- step={64}
- className="w-full"
- />
-
- 64
- {params.attention_context_right}
- 512
-
-
-
+
updateParam('attention_context_left', v)} min={64} max={512} step={64} />
+ updateParam('attention_context_right', v)} min={64} max={512} step={64} />
- {/* Diarization for Parakeet */}
{!isMultiTrack && (
-
-
-
- updateParam('diarize', v)}
- />
-
-
-
- {params.diarize && (
-
- )}
-
-
+
)}
);
@@ -925,119 +613,11 @@ function CanaryConfig({ params, updateParam, isMultiTrack }: ConfigProps) {
return (
-
-
-
+ updateParam('language', v)} options={CANARY_LANGUAGES} />
- {/* Diarization for Canary */}
{!isMultiTrack && (
-
-
-
- updateParam('diarize', v)}
- />
-
-
-
- {params.diarize && (
-
- )}
-
-
+
)}
);
@@ -1052,13 +632,8 @@ interface OpenAIConfigProps extends ConfigProps {
}
function OpenAIConfig({
- params,
- updateParam,
- isValidating,
- validationStatus,
- validationMessage,
- availableModels,
- onValidate
+ params, updateParam,
+ isValidating, validationStatus, validationMessage, availableModels, onValidate
}: OpenAIConfigProps) {
return (
@@ -1067,57 +642,28 @@ function OpenAIConfig({
{
- updateParam('api_key', e.target.value);
- }}
+ onChange={(e) => updateParam('api_key', e.target.value)}
className={`${inputClassName} flex-1`}
/>
{validationStatus !== 'idle' && (
-
+
{validationStatus === 'valid' ? : }
{validationMessage}
)}
-
-
-
-
-
-
-
+
updateParam('model', v)} options={availableModels} />
+ updateParam('language', v === "auto" ? undefined : v)} options={LANGUAGES} />
@@ -1133,46 +679,24 @@ function OpenAIConfig({
function VoxtralConfig({ params, updateParam }: ConfigProps) {
return (
);
}
diff --git a/web/frontend/src/features/settings/components/SummaryTemplateDialog.tsx b/web/frontend/src/features/settings/components/SummaryTemplateDialog.tsx
index 6ab2eb42..8594380f 100644
--- a/web/frontend/src/features/settings/components/SummaryTemplateDialog.tsx
+++ b/web/frontend/src/features/settings/components/SummaryTemplateDialog.tsx
@@ -3,10 +3,8 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-import { Switch } from "@/components/ui/switch";
import { useAuth } from "@/features/auth/hooks/useAuth";
-import { FormField } from "@/components/transcription/FormHelpers";
+import { FormField, SelectField, SwitchField, inputClassName } from "@/components/transcription/FormHelpers";
import { Loader2 } from "lucide-react";
export interface SummaryTemplate {
@@ -27,30 +25,6 @@ interface SummaryTemplateDialogProps {
initial?: SummaryTemplate | null;
}
-// Styled class constants
-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)]
-`;
-
export function SummaryTemplateDialog({ open, onOpenChange, onSave, initial }: SummaryTemplateDialogProps) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
@@ -134,23 +108,13 @@ export function SummaryTemplateDialog({ open, onOpenChange, onSave, initial }: S
{/* Model Selection */}
-
-
-
+ value={model}
+ onValueChange={setModel}
+ options={models}
+ />
{/* Description Field */}
@@ -183,15 +147,12 @@ Summarize the following transcript into concise bullet points. Focus on key deci
{/* Include Speaker Info Toggle */}
-
-
-
+
{/* Footer */}