"use client"; import { Plus, X } from "lucide-react"; import { useEffect, 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { VoiceSelector } from "@/components/VoiceSelector"; import { useUserConfig } from "@/context/UserConfigContext"; type ServiceSegment = "llm" | "tts" | "stt" | "embeddings"; interface SchemaProperty { type?: string; default?: string | number | boolean; enum?: string[]; examples?: string[]; model_options?: Record; $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 TAB_CONFIG: { key: ServiceSegment; label: string }[] = [ { key: "llm", label: "LLM" }, { key: "tts", label: "Voice" }, { key: "stt", label: "Transcriber" }, { key: "embeddings", label: "Embedding" }, ]; // Display names for language codes (Deepgram + Sarvam) const LANGUAGE_DISPLAY_NAMES: Record = { "multi": "Multilingual (Auto-detect)", // Arabic "ar": "Arabic", "ar-AE": "Arabic (UAE)", "ar-SA": "Arabic (Saudi Arabia)", "ar-QA": "Arabic (Qatar)", "ar-KW": "Arabic (Kuwait)", "ar-SY": "Arabic (Syria)", "ar-LB": "Arabic (Lebanon)", "ar-PS": "Arabic (Palestine)", "ar-JO": "Arabic (Jordan)", "ar-EG": "Arabic (Egypt)", "ar-SD": "Arabic (Sudan)", "ar-TD": "Arabic (Chad)", "ar-MA": "Arabic (Morocco)", "ar-DZ": "Arabic (Algeria)", "ar-TN": "Arabic (Tunisia)", "ar-IQ": "Arabic (Iraq)", "ar-IR": "Arabic (Iran)", // Other languages "be": "Belarusian", "bn": "Bengali", "bs": "Bosnian", "bg": "Bulgarian", "ca": "Catalan", "cs": "Czech", "da": "Danish", "da-DK": "Danish (Denmark)", "de": "German", "de-CH": "German (Switzerland)", "el": "Greek", "en": "English", "en-US": "English (US)", "en-AU": "English (Australia)", "en-GB": "English (UK)", "en-IN": "English (India)", "en-NZ": "English (New Zealand)", "es": "Spanish", "es-419": "Spanish (Latin America)", "et": "Estonian", "fa": "Persian", "fi": "Finnish", "fr": "French", "fr-CA": "French (Canada)", "he": "Hebrew", "hi": "Hindi", "hr": "Croatian", "hu": "Hungarian", "id": "Indonesian", "it": "Italian", "ja": "Japanese", "kn": "Kannada", "ko": "Korean", "ko-KR": "Korean (South Korea)", "lt": "Lithuanian", "lv": "Latvian", "mk": "Macedonian", "mr": "Marathi", "ms": "Malay", "nl": "Dutch", "nl-BE": "Flemish", "no": "Norwegian", "pl": "Polish", "pt": "Portuguese", "pt-BR": "Portuguese (Brazil)", "pt-PT": "Portuguese (Portugal)", "ro": "Romanian", "ru": "Russian", "sk": "Slovak", "sl": "Slovenian", "sr": "Serbian", "sv": "Swedish", "sv-SE": "Swedish (Sweden)", "ta": "Tamil", "te": "Telugu", "th": "Thai", "tl": "Tagalog", "tr": "Turkish", "uk": "Ukrainian", "ur": "Urdu", "vi": "Vietnamese", "zh-CN": "Chinese (Simplified)", "zh-TW": "Chinese (Traditional)", // Sarvam Indian languages "bn-IN": "Bengali", "gu-IN": "Gujarati", "hi-IN": "Hindi", "kn-IN": "Kannada", "ml-IN": "Malayalam", "mr-IN": "Marathi", "od-IN": "Odia", "pa-IN": "Punjabi", "ta-IN": "Tamil", "te-IN": "Telugu", "as-IN": "Assamese", }; // 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 default function ServiceConfiguration() { const [apiError, setApiError] = useState(null); const [isSaving, setIsSaving] = useState(false); const { userConfig, saveUserConfig } = useUserConfig(); const [schemas, setSchemas] = useState>>({ llm: {}, tts: {}, stt: {}, embeddings: {} }); const [serviceProviders, setServiceProviders] = useState>({ llm: "", tts: "", stt: "", embeddings: "" }); const [apiKeys, setApiKeys] = useState>({ llm: [""], tts: [""], stt: [""], embeddings: [""], }); const [isManualModelInput, setIsManualModelInput] = useState(false); const [hasCheckedManualMode, setHasCheckedManualMode] = useState(false); const { register, handleSubmit, formState: { }, reset, getValues, setValue, watch } = useForm(); useEffect(() => { const fetchConfigurations = async () => { const response = await getDefaultConfigurationsApiV1UserConfigurationsDefaultsGet(); if (response.data) { 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 }); } else { console.error("Failed to fetch configurations"); return; } 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 }; const loadedApiKeys: Record = { llm: [""], tts: [""], stt: [""], embeddings: [""], }; const setServicePropertyValues = (service: ServiceSegment) => { if (userConfig?.[service]?.provider) { Object.entries(userConfig?.[service]).forEach(([field, value]) => { if (field === "api_key") { // Handle api_key separately — it can be string or string[] if (Array.isArray(value)) { loadedApiKeys[service] = value.length > 0 ? value : [""]; } else { loadedApiKeys[service] = value ? [value as string] : [""]; } } else if (field !== "provider") { defaultValues[`${service}_${field}`] = value as string | number | boolean; } }); selectedProviders[service] = userConfig?.[service]?.provider as string; // Fill in schema defaults for fields not present in userConfig const properties = response.data[service]?.[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 = response.data[service]?.[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"); // IMPORTANT: Reset form values BEFORE changing providers // Otherwise, Radix Select sees old values that don't match new provider's enum // and calls onValueChange('') to clear "invalid" values reset(defaultValues); setApiKeys(loadedApiKeys); setServiceProviders(selectedProviders); }; fetchConfigurations(); }, [reset, userConfig]); // Check if the saved LLM model is not in the suggested options (custom model) useEffect(() => { if (hasCheckedManualMode) return; const currentProvider = serviceProviders.llm; const providerSchema = schemas?.llm?.[currentProvider]; if (!providerSchema) return; const modelSchema = providerSchema.properties.model; const actualModelSchema = modelSchema?.$ref && providerSchema.$defs ? providerSchema.$defs[modelSchema.$ref.split('/').pop() || ''] : modelSchema; if (actualModelSchema?.examples && userConfig?.llm?.model) { const savedModel = userConfig.llm.model as string; const isInOptions = actualModelSchema.examples.includes(savedModel); if (!isInOptions) { setIsManualModelInput(true); } setHasCheckedManualMode(true); } }, [schemas, serviceProviders.llm, userConfig?.llm?.model, hasCheckedManualMode]); // 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 = {}; // Preserve values from other services Object.keys(currentValues).forEach(key => { if (!key.startsWith(`${service}_`)) { preservedValues[key] = currentValues[key]; } }); // Set default values from schema 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]: [""] })); // Reset manual model input when LLM provider changes if (service === "llm") { setIsManualModelInput(false); } } const onSubmit = async (data: FormValues) => { setApiError(null); setIsSaving(true); // Collect non-empty API keys per service const getServiceApiKeys = (service: ServiceSegment): string[] => apiKeys[service].map(k => k.trim()).filter(k => k.length > 0); const userConfig: Record> = { llm: { provider: serviceProviders.llm, ...(getServiceApiKeys("llm").length > 0 && { api_key: getServiceApiKeys("llm") }), model: data.llm_model as string }, tts: { provider: serviceProviders.tts, ...(getServiceApiKeys("tts").length > 0 && { api_key: getServiceApiKeys("tts") }), }, stt: { provider: serviceProviders.stt, ...(getServiceApiKeys("stt").length > 0 && { api_key: getServiceApiKeys("stt") }), }, embeddings: { provider: serviceProviders.embeddings, ...(getServiceApiKeys("embeddings").length > 0 && { api_key: getServiceApiKeys("embeddings") }), model: data.embeddings_model as string } }; // Add any extra properties in the payload Object.entries(data).forEach(([property, value]) => { const parts = property.split('_'); const service = parts[0] as ServiceSegment; const field = parts.slice(1).join('_'); if (field === "api_key") return; // handled via apiKeys state if (userConfig[service] && !(field in userConfig[service])) { (userConfig[service] as Record)[field] = value as string; } }); // Build save config - only include embeddings if api_key is provided const saveConfig: { llm: Record; tts: Record; stt: Record; embeddings?: Record; } = { llm: userConfig.llm, tts: userConfig.tts, stt: userConfig.stt }; // Only include embeddings if user has configured it (has api_key) const embeddingsKeys = getServiceApiKeys("embeddings"); if (embeddingsKeys.length > 0) { saveConfig.embeddings = userConfig.embeddings; } try { await saveUserConfig(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 []; // Find all config fields (not provider, not api_key) const fields = Object.keys(providerSchema.properties).filter( field => field !== "provider" && field !== "api_key" ); return fields; }; 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 (
{/* Provider and first config field in one row */}
{currentProvider && providerSchema && configFields[0] && (
{renderField(service, configFields[0], providerSchema)}
)}
{/* Additional config fields (like voice for TTS) */} {currentProvider && providerSchema && configFields.length > 1 && (
{configFields.slice(1).map((field) => (
{renderField(service, field, providerSchema)}
))}
)} {/* API Key(s) */} {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 && ( )}
))}
)}
); }; 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; // Use VoiceSelector for voice field in TTS service (except Sarvam which uses predefined options) if (service === "tts" && field === "voice") { const currentProvider = serviceProviders.tts; // Sarvam uses predefined voice options, not VoiceSelector const hasVoiceOptions = actualSchema?.enum || actualSchema?.examples; if (currentProvider !== "sarvam" && !hasVoiceOptions) { return ( { setValue(`${service}_${field}`, voiceId, { shouldDirty: true }); }} /> ); } } // Handle LLM model field with manual input toggle (uses examples from schema) if (service === "llm" && field === "model" && actualSchema?.examples) { const currentValue = watch(`${service}_${field}`) as string || ""; const modelOptions = actualSchema.examples; if (isManualModelInput) { return (
{ setValue(`${service}_${field}`, e.target.value, { shouldDirty: true }); }} />
{ setIsManualModelInput(checked as boolean); if (!checked && modelOptions.length > 0) { // Reset to first option when switching back setValue(`${service}_${field}`, modelOptions[0], { shouldDirty: true }); } }} />
); } return (
{ setIsManualModelInput(checked as boolean); }} />
); } // Handle fields with enum or examples (dropdown options) let dropdownOptions = actualSchema?.enum || actualSchema?.examples; // Use model-dependent options when available (e.g., Sarvam voices per model) 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) { // Use friendly display names for language and voice fields 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 ( ); }; return (

AI Models Configuration

Configure your AI model, voice, and transcription services.

{TAB_CONFIG.map(({ key, label }) => ( {label} ))} {TAB_CONFIG.map(({ key }) => ( {renderServiceFields(key)} ))} {apiError &&

{apiError}

}
); }