"use client"; import { Plus, X } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { getDefaultConfigurationsApiV1UserConfigurationsDefaultsGet } from '@/client/sdk.gen'; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { VoiceSelector } from "@/components/VoiceSelector"; import { LANGUAGE_DISPLAY_NAMES } from "@/constants/languages"; import { useUserConfig } from "@/context/UserConfigContext"; import type { ModelOverrides } from "@/types/workflow-configurations"; type ServiceSegment = "llm" | "tts" | "stt" | "embeddings" | "realtime"; interface SchemaProperty { type?: string; default?: string | number | boolean; enum?: string[]; examples?: string[]; model_options?: Record; allow_custom_input?: boolean; $ref?: string; description?: string; format?: string; } interface ProviderSchema { properties: Record; required?: string[]; $defs?: Record; [key: string]: unknown; } interface FormValues { [key: string]: string | number | boolean; } const STANDARD_TABS: { key: ServiceSegment; label: string }[] = [ { key: "llm", label: "LLM" }, { key: "tts", label: "Voice" }, { key: "stt", label: "Transcriber" }, { key: "embeddings", label: "Embedding" }, ]; const REALTIME_TABS: { key: ServiceSegment; label: string }[] = [ { key: "realtime", label: "Realtime Model" }, { key: "embeddings", label: "Embedding" }, ]; const OVERRIDE_STANDARD_TABS: { key: ServiceSegment; label: string }[] = [ { key: "llm", label: "LLM" }, { key: "tts", label: "Voice" }, { key: "stt", label: "Transcriber" }, ]; const OVERRIDE_REALTIME_TABS: { key: ServiceSegment; label: string }[] = [ { key: "realtime", label: "Realtime Model" }, ]; // Display names for Sarvam voices const VOICE_DISPLAY_NAMES: Record = { "anushka": "Anushka (Female)", "manisha": "Manisha (Female)", "vidya": "Vidya (Female)", "arya": "Arya (Female)", "abhilash": "Abhilash (Male)", "karun": "Karun (Male)", "hitesh": "Hitesh (Male)", }; export interface ServiceConfigurationFormProps { mode: 'global' | 'override'; currentOverrides?: ModelOverrides; onSave: (config: Record) => Promise; /** Text for the submit button. Defaults to "Save Configuration". */ submitLabel?: string; } function getGlobalSummary(config: Record | null | undefined): string { if (!config) return "Not configured"; const provider = config.provider as string | undefined; const model = config.model as string | undefined; if (!provider) return "Not configured"; return model ? `${provider} / ${model}` : provider; } export function ServiceConfigurationForm({ mode, currentOverrides, onSave, submitLabel, }: ServiceConfigurationFormProps) { const [apiError, setApiError] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isRealtime, setIsRealtime] = useState(false); const { userConfig } = useUserConfig(); const [schemas, setSchemas] = useState>>({ llm: {}, tts: {}, stt: {}, embeddings: {}, realtime: {}, }); const [serviceProviders, setServiceProviders] = useState>({ llm: "", tts: "", stt: "", embeddings: "", realtime: "", }); const [apiKeys, setApiKeys] = useState>({ llm: [""], tts: [""], stt: [""], embeddings: [""], realtime: [""], }); const [isCustomInput, setIsCustomInput] = useState>({}); // Override-specific state: which services have the override toggle enabled const [enabledOverrides, setEnabledOverrides] = useState>({ llm: false, tts: false, stt: false, realtime: false, }); const { register, handleSubmit, formState: { }, reset, getValues, setValue, watch } = useForm(); // Build effective config source: overlay overrides onto global config const configSource = useMemo(() => { if (mode === 'global' || !currentOverrides) return userConfig; // Merge overrides onto global config for form initialization const merged = { ...userConfig } as Record; const overrideServices: (keyof ModelOverrides)[] = ["llm", "tts", "stt", "realtime"]; for (const svc of overrideServices) { if (svc === "is_realtime") continue; const overrideVal = currentOverrides[svc]; if (overrideVal && typeof overrideVal === "object") { const globalVal = (userConfig as Record | null)?.[svc] as Record | undefined; merged[svc] = { ...globalVal, ...overrideVal }; } } if (currentOverrides.is_realtime !== undefined) { merged.is_realtime = currentOverrides.is_realtime; } return merged as typeof userConfig; }, [mode, userConfig, currentOverrides]); useEffect(() => { const fetchConfigurations = async () => { const response = await getDefaultConfigurationsApiV1UserConfigurationsDefaultsGet(); if (!response.data) { console.error("Failed to fetch configurations"); return; } const data = response.data as Record; const realtimeSchemas = (data.realtime || {}) as Record; setSchemas({ llm: response.data.llm as Record, tts: response.data.tts as Record, stt: response.data.stt as Record, embeddings: response.data.embeddings as Record, realtime: realtimeSchemas, }); // Restore realtime toggle const configData = configSource as Record | null; if (configData?.is_realtime) { setIsRealtime(true); } const defaultValues: Record = {}; const selectedProviders: Record = { llm: response.data.default_providers.llm, tts: response.data.default_providers.tts, stt: response.data.default_providers.stt, embeddings: response.data.default_providers.embeddings, realtime: "", }; const realtimeProviderKeys = Object.keys(realtimeSchemas); if (realtimeProviderKeys.length > 0) { selectedProviders.realtime = realtimeProviderKeys[0]; } const loadedApiKeys: Record = { llm: [""], tts: [""], stt: [""], embeddings: [""], realtime: [""], }; const setServicePropertyValues = (service: ServiceSegment) => { const src = service === "realtime" ? (configSource as Record | null)?.realtime as Record | undefined : (configSource as Record | null)?.[service] as Record | undefined; const schemaSource = service === "realtime" ? realtimeSchemas : response.data![service as "llm" | "tts" | "stt" | "embeddings"] as Record | undefined; if (src?.provider) { Object.entries(src).forEach(([field, value]) => { if (field === "api_key") { if (mode === 'override') { // In override mode, only load API keys from the override itself const overrideVal = currentOverrides?.[service as keyof ModelOverrides]; const overrideApiKey = overrideVal && typeof overrideVal === "object" ? (overrideVal as Record).api_key : undefined; if (overrideApiKey) { loadedApiKeys[service] = Array.isArray(overrideApiKey) ? overrideApiKey as string[] : [overrideApiKey as string]; } else { loadedApiKeys[service] = [""]; } } else { if (Array.isArray(value)) { loadedApiKeys[service] = (value as string[]).length > 0 ? value as string[] : [""]; } else { loadedApiKeys[service] = value ? [value as string] : [""]; } } } else if (field !== "provider") { defaultValues[`${service}_${field}`] = value as string | number | boolean; } }); selectedProviders[service] = src.provider as string; const properties = schemaSource?.[selectedProviders[service]]?.properties as Record; if (properties) { Object.entries(properties).forEach(([field, schema]) => { const key = `${service}_${field}`; if (field !== "provider" && field !== "api_key" && schema.default !== undefined && !(key in defaultValues)) { defaultValues[key] = schema.default; } }); } } else { const properties = schemaSource?.[selectedProviders[service]]?.properties as Record; if (properties) { Object.entries(properties).forEach(([field, schema]) => { if (field !== "provider" && schema.default !== undefined) { defaultValues[`${service}_${field}`] = schema.default; } }); } } }; setServicePropertyValues("llm"); setServicePropertyValues("tts"); setServicePropertyValues("stt"); setServicePropertyValues("embeddings"); setServicePropertyValues("realtime"); // Detect custom inputs const detectedCustomInput: Record = {}; const allSchemas = { ...response.data, realtime: realtimeSchemas } as unknown as Record>; (["llm", "tts", "stt", "embeddings", "realtime"] as ServiceSegment[]).forEach(service => { const provider = selectedProviders[service]; const providerSchema = allSchemas[service]?.[provider]; if (!providerSchema) return; const src = service === "realtime" ? (configSource as Record | null)?.realtime as Record | undefined : (configSource as Record | null)?.[service] as Record | undefined; Object.entries(providerSchema.properties).forEach(([field, schema]) => { const actualSchema = (schema as SchemaProperty).$ref && providerSchema.$defs ? providerSchema.$defs[(schema as SchemaProperty).$ref!.split('/').pop() || ''] : schema as SchemaProperty; if (!actualSchema?.allow_custom_input || !actualSchema?.examples) return; const savedValue = src?.[field] as string | undefined; if (savedValue && !actualSchema.examples.includes(savedValue)) { detectedCustomInput[`${service}_${field}`] = true; } }); }); // Initialize override toggles if (mode === 'override') { setEnabledOverrides({ llm: !!currentOverrides?.llm, tts: !!currentOverrides?.tts, stt: !!currentOverrides?.stt, realtime: !!currentOverrides?.realtime, }); } reset(defaultValues); setApiKeys(loadedApiKeys); setServiceProviders(selectedProviders); setIsCustomInput(detectedCustomInput); }; fetchConfigurations(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [reset, configSource]); // Reset voice when TTS model changes if the provider has model-dependent voice options const ttsModel = watch("tts_model"); useEffect(() => { const voiceSchema = schemas?.tts?.[serviceProviders.tts]?.properties?.voice; const modelOptions = voiceSchema?.model_options; if (!modelOptions || !ttsModel) return; const validVoices = modelOptions[ttsModel as string]; const currentVoice = getValues("tts_voice") as string; if (validVoices && currentVoice && !validVoices.includes(currentVoice)) { setValue("tts_voice", validVoices[0], { shouldDirty: true }); } }, [ttsModel, serviceProviders.tts, setValue, getValues, schemas]); // Reset language when STT model changes if the provider has model-dependent language options const sttModel = watch("stt_model"); useEffect(() => { const languageSchema = schemas?.stt?.[serviceProviders.stt]?.properties?.language; const modelOptions = languageSchema?.model_options; if (!modelOptions || !sttModel) return; const validLanguages = modelOptions[sttModel as string]; const currentLanguage = getValues("stt_language") as string; if (validLanguages && currentLanguage && !validLanguages.includes(currentLanguage)) { setValue("stt_language", validLanguages[0], { shouldDirty: true }); } }, [sttModel, serviceProviders.stt, setValue, getValues, schemas]); const handleProviderChange = (service: ServiceSegment, providerName: string) => { if (!providerName) return; const currentValues = getValues(); const preservedValues: Record = {}; Object.keys(currentValues).forEach(key => { if (!key.startsWith(`${service}_`)) { preservedValues[key] = currentValues[key]; } }); if (schemas?.[service]?.[providerName]) { const providerSchema = schemas[service][providerName]; Object.entries(providerSchema.properties).forEach(([field, schema]: [string, SchemaProperty]) => { if (field !== "provider" && schema.default !== undefined) { preservedValues[`${service}_${field}`] = schema.default; } }); } preservedValues[`${service}_provider`] = providerName; reset(preservedValues); setServiceProviders(prev => ({ ...prev, [service]: providerName })); setApiKeys(prev => ({ ...prev, [service]: [""] })); setIsCustomInput(prev => { const next = { ...prev }; Object.keys(next).forEach(key => { if (key.startsWith(`${service}_`)) delete next[key]; }); return next; }); }; const buildServiceConfig = (service: ServiceSegment, data: FormValues) => { const config: Record = { provider: serviceProviders[service], }; const keys = apiKeys[service].map(k => k.trim()).filter(k => k.length > 0); if (keys.length > 0) { config.api_key = mode === 'override' ? keys[0] : keys; } Object.entries(data).forEach(([property, value]) => { if (!property.startsWith(`${service}_`)) return; const field = property.slice(service.length + 1); if (field === "api_key" || field === "provider") return; config[field] = value as string | number; }); return config; }; const onSubmit = async (data: FormValues) => { setApiError(null); setIsSaving(true); try { if (mode === 'override') { // Build model_overrides for enabled services only const modelOverrides: Record = {}; const services = isRealtime ? ["realtime"] : ["llm", "tts", "stt"]; for (const svc of services) { if (enabledOverrides[svc]) { modelOverrides[svc] = buildServiceConfig(svc as ServiceSegment, data); } } // Include is_realtime if it differs from global const globalIsRealtime = !!(userConfig as Record | null)?.is_realtime; if (isRealtime !== globalIsRealtime) { modelOverrides.is_realtime = isRealtime; } await onSave({ model_overrides: Object.keys(modelOverrides).length > 0 ? modelOverrides : undefined, }); } else { // Global mode: save all services const saveConfig: Record = { llm: buildServiceConfig("llm", data), tts: buildServiceConfig("tts", data), stt: buildServiceConfig("stt", data), is_realtime: isRealtime, }; if (serviceProviders.realtime) { saveConfig.realtime = buildServiceConfig("realtime", data); } const embeddingsKeys = apiKeys.embeddings.map(k => k.trim()).filter(k => k.length > 0); if (embeddingsKeys.length > 0) { saveConfig.embeddings = buildServiceConfig("embeddings", data); } await onSave(saveConfig); } setApiError(null); } catch (error: unknown) { if (error instanceof Error) { setApiError(error.message); } else { setApiError('An unknown error occurred'); } } finally { setIsSaving(false); } }; const getConfigFields = (service: ServiceSegment): string[] => { const currentProvider = serviceProviders[service]; const providerSchema = schemas?.[service]?.[currentProvider]; if (!providerSchema) return []; return Object.keys(providerSchema.properties).filter( field => field !== "provider" && field !== "api_key" ); }; const renderServiceFields = (service: ServiceSegment) => { const currentProvider = serviceProviders[service]; const providerSchema = schemas?.[service]?.[currentProvider]; const availableProviders = schemas?.[service] ? Object.keys(schemas[service]) : []; const configFields = getConfigFields(service); return (
{currentProvider && providerSchema && configFields[0] && (
{renderField(service, configFields[0], providerSchema)}
)}
{currentProvider && providerSchema && configFields.length > 1 && (
{configFields.slice(1).map((field) => (
{renderField(service, field, providerSchema)}
))}
)} {currentProvider && providerSchema && providerSchema.properties.api_key && (
{apiKeys[service].map((key, index) => (
{ const newKeys = [...apiKeys[service]]; newKeys[index] = e.target.value; setApiKeys(prev => ({ ...prev, [service]: newKeys })); }} /> {apiKeys[service].length > 1 && ( )}
))} {mode !== 'override' && ( )}
)}
); }; const renderField = (service: ServiceSegment, field: string, providerSchema: ProviderSchema) => { const schema = providerSchema.properties[field]; const actualSchema = schema.$ref && providerSchema.$defs ? providerSchema.$defs[schema.$ref.split('/').pop() || ''] : schema; if (service === "tts" && field === "voice" && !actualSchema?.allow_custom_input) { const hasVoiceOptions = actualSchema?.enum || actualSchema?.examples; if (!hasVoiceOptions) { return ( { setValue(`${service}_${field}`, voiceId, { shouldDirty: true }); }} model={watch("tts_model") as string || undefined} /> ); } } if (actualSchema?.allow_custom_input && actualSchema?.examples) { const fieldKey = `${service}_${field}`; const currentValue = watch(fieldKey) as string || ""; const options = actualSchema.examples; if (isCustomInput[fieldKey]) { return (
{ setValue(fieldKey, e.target.value, { shouldDirty: true }); }} />
{ setIsCustomInput(prev => ({ ...prev, [fieldKey]: checked as boolean })); if (!checked && options.length > 0) { setValue(fieldKey, options[0], { shouldDirty: true }); } }} />
); } return (
{ setIsCustomInput(prev => ({ ...prev, [fieldKey]: checked as boolean })); }} />
); } let dropdownOptions = actualSchema?.enum || actualSchema?.examples; if (actualSchema?.model_options) { const modelValue = watch(`${service}_model`) as string; if (modelValue && actualSchema.model_options[modelValue]) { dropdownOptions = actualSchema.model_options[modelValue]; } } if (dropdownOptions && dropdownOptions.length > 0) { const getDisplayName = (value: string) => { if (field === "language") { return LANGUAGE_DISPLAY_NAMES[value] || value; } if (field === "voice") { return VOICE_DISPLAY_NAMES[value] || value.charAt(0).toUpperCase() + value.slice(1); } return value; }; return ( ); } return ( ); }; const handleOverrideToggle = (service: string, enabled: boolean) => { setEnabledOverrides(prev => ({ ...prev, [service]: enabled })); }; const renderOverrideToggle = (service: ServiceSegment, label: string) => { const globalVal = (userConfig as Record | null)?.[service] as Record | null | undefined; const isEnabled = enabledOverrides[service]; return (
{!isEnabled && (

Using global: {getGlobalSummary(globalVal)}

)}
handleOverrideToggle(service, checked)} />
); }; const getVisibleTabs = () => { if (mode === 'override') { return isRealtime ? OVERRIDE_REALTIME_TABS : OVERRIDE_STANDARD_TABS; } return isRealtime ? REALTIME_TABS : STANDARD_TABS; }; const visibleTabs = getVisibleTabs(); const defaultTab = isRealtime ? "realtime" : "llm"; return (
{/* Realtime toggle */}

Uses a single speech-to-speech model (no separate STT/TTS)

{visibleTabs.map(({ key, label }) => ( {label} ))} {visibleTabs.map(({ key, label }) => ( {mode === 'override' && renderOverrideToggle(key, label)} {(mode === 'global' || enabledOverrides[key]) && renderServiceFields(key)} ))} {apiError &&

{apiError}

}
); }