2025-09-09 14:37:32 +05:30
|
|
|
"use client";
|
|
|
|
|
|
2026-03-10 15:17:40 +05:30
|
|
|
import { Plus, X } from "lucide-react";
|
2025-09-09 14:37:32 +05:30
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import { useForm } from "react-hook-form";
|
|
|
|
|
|
|
|
|
|
import { getDefaultConfigurationsApiV1UserConfigurationsDefaultsGet } from '@/client/sdk.gen';
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2025-11-29 15:39:57 +05:30
|
|
|
import { Card, CardContent } from "@/components/ui/card";
|
2025-12-25 15:05:53 +05:30
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
2025-09-09 14:37:32 +05:30
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
2025-11-29 15:39:57 +05:30
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
2025-12-25 15:05:53 +05:30
|
|
|
import { VoiceSelector } from "@/components/VoiceSelector";
|
2025-09-09 14:37:32 +05:30
|
|
|
import { useUserConfig } from "@/context/UserConfigContext";
|
|
|
|
|
|
2026-01-17 14:37:03 +05:30
|
|
|
type ServiceSegment = "llm" | "tts" | "stt" | "embeddings";
|
2025-09-09 14:37:32 +05:30
|
|
|
|
|
|
|
|
interface SchemaProperty {
|
|
|
|
|
type?: string;
|
|
|
|
|
default?: string | number | boolean;
|
|
|
|
|
enum?: string[];
|
2025-12-25 15:05:53 +05:30
|
|
|
examples?: string[];
|
2026-02-13 10:11:48 +05:30
|
|
|
model_options?: Record<string, string[]>;
|
2025-09-09 14:37:32 +05:30
|
|
|
$ref?: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
format?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ProviderSchema {
|
|
|
|
|
properties: Record<string, SchemaProperty>;
|
|
|
|
|
required?: string[];
|
|
|
|
|
$defs?: Record<string, SchemaProperty>;
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FormValues {
|
|
|
|
|
[key: string]: string | number | boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 15:39:57 +05:30
|
|
|
const TAB_CONFIG: { key: ServiceSegment; label: string }[] = [
|
|
|
|
|
{ key: "llm", label: "LLM" },
|
|
|
|
|
{ key: "tts", label: "Voice" },
|
|
|
|
|
{ key: "stt", label: "Transcriber" },
|
2026-01-17 14:37:03 +05:30
|
|
|
{ key: "embeddings", label: "Embedding" },
|
2025-11-29 15:39:57 +05:30
|
|
|
];
|
|
|
|
|
|
2025-12-25 15:05:53 +05:30
|
|
|
// Display names for language codes (Deepgram + Sarvam)
|
|
|
|
|
const LANGUAGE_DISPLAY_NAMES: Record<string, string> = {
|
|
|
|
|
"multi": "Multilingual (Auto-detect)",
|
2026-02-13 11:44:57 +05:30
|
|
|
// 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",
|
2025-12-25 15:05:53 +05:30
|
|
|
"en": "English",
|
|
|
|
|
"en-US": "English (US)",
|
|
|
|
|
"en-AU": "English (Australia)",
|
2026-02-13 11:44:57 +05:30
|
|
|
"en-GB": "English (UK)",
|
2025-12-25 15:05:53 +05:30
|
|
|
"en-IN": "English (India)",
|
2026-02-13 11:44:57 +05:30
|
|
|
"en-NZ": "English (New Zealand)",
|
2025-12-25 15:05:53 +05:30
|
|
|
"es": "Spanish",
|
|
|
|
|
"es-419": "Spanish (Latin America)",
|
2026-02-13 11:44:57 +05:30
|
|
|
"et": "Estonian",
|
|
|
|
|
"fa": "Persian",
|
|
|
|
|
"fi": "Finnish",
|
2025-12-25 15:05:53 +05:30
|
|
|
"fr": "French",
|
|
|
|
|
"fr-CA": "French (Canada)",
|
2026-02-13 11:44:57 +05:30
|
|
|
"he": "Hebrew",
|
2025-12-25 15:05:53 +05:30
|
|
|
"hi": "Hindi",
|
2026-02-13 11:44:57 +05:30
|
|
|
"hr": "Croatian",
|
|
|
|
|
"hu": "Hungarian",
|
|
|
|
|
"id": "Indonesian",
|
|
|
|
|
"it": "Italian",
|
2025-12-25 15:05:53 +05:30
|
|
|
"ja": "Japanese",
|
2026-02-13 11:44:57 +05:30
|
|
|
"kn": "Kannada",
|
2025-12-25 15:05:53 +05:30
|
|
|
"ko": "Korean",
|
2026-02-13 11:44:57 +05:30
|
|
|
"ko-KR": "Korean (South Korea)",
|
|
|
|
|
"lt": "Lithuanian",
|
|
|
|
|
"lv": "Latvian",
|
|
|
|
|
"mk": "Macedonian",
|
|
|
|
|
"mr": "Marathi",
|
|
|
|
|
"ms": "Malay",
|
|
|
|
|
"nl": "Dutch",
|
|
|
|
|
"nl-BE": "Flemish",
|
|
|
|
|
"no": "Norwegian",
|
2025-12-25 15:05:53 +05:30
|
|
|
"pl": "Polish",
|
2026-02-13 11:44:57 +05:30
|
|
|
"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",
|
2025-12-25 15:05:53 +05:30
|
|
|
"tr": "Turkish",
|
|
|
|
|
"uk": "Ukrainian",
|
2026-02-13 11:44:57 +05:30
|
|
|
"ur": "Urdu",
|
2025-12-25 15:05:53 +05:30
|
|
|
"vi": "Vietnamese",
|
2026-02-13 11:44:57 +05:30
|
|
|
"zh-CN": "Chinese (Simplified)",
|
|
|
|
|
"zh-TW": "Chinese (Traditional)",
|
2025-12-25 15:05:53 +05:30
|
|
|
// 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<string, string> = {
|
|
|
|
|
"anushka": "Anushka (Female)",
|
|
|
|
|
"manisha": "Manisha (Female)",
|
|
|
|
|
"vidya": "Vidya (Female)",
|
|
|
|
|
"arya": "Arya (Female)",
|
|
|
|
|
"abhilash": "Abhilash (Male)",
|
|
|
|
|
"karun": "Karun (Male)",
|
|
|
|
|
"hitesh": "Hitesh (Male)",
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-09 14:37:32 +05:30
|
|
|
export default function ServiceConfiguration() {
|
|
|
|
|
const [apiError, setApiError] = useState<string | null>(null);
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
const { userConfig, saveUserConfig } = useUserConfig();
|
|
|
|
|
const [schemas, setSchemas] = useState<Record<ServiceSegment, Record<string, ProviderSchema>>>({
|
|
|
|
|
llm: {},
|
|
|
|
|
tts: {},
|
2026-01-17 14:37:03 +05:30
|
|
|
stt: {},
|
|
|
|
|
embeddings: {}
|
2025-09-09 14:37:32 +05:30
|
|
|
});
|
|
|
|
|
const [serviceProviders, setServiceProviders] = useState<Record<ServiceSegment, string>>({
|
|
|
|
|
llm: "",
|
|
|
|
|
tts: "",
|
2026-01-17 14:37:03 +05:30
|
|
|
stt: "",
|
|
|
|
|
embeddings: ""
|
2025-09-09 14:37:32 +05:30
|
|
|
});
|
2026-03-10 15:17:40 +05:30
|
|
|
const [apiKeys, setApiKeys] = useState<Record<ServiceSegment, string[]>>({
|
|
|
|
|
llm: [""],
|
|
|
|
|
tts: [""],
|
|
|
|
|
stt: [""],
|
|
|
|
|
embeddings: [""],
|
|
|
|
|
});
|
2025-12-25 15:05:53 +05:30
|
|
|
const [isManualModelInput, setIsManualModelInput] = useState(false);
|
|
|
|
|
const [hasCheckedManualMode, setHasCheckedManualMode] = useState(false);
|
2025-09-09 14:37:32 +05:30
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
register,
|
|
|
|
|
handleSubmit,
|
2026-03-10 15:17:40 +05:30
|
|
|
formState: { },
|
2025-09-09 14:37:32 +05:30
|
|
|
reset,
|
|
|
|
|
getValues,
|
|
|
|
|
setValue,
|
|
|
|
|
watch
|
|
|
|
|
} = useForm();
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchConfigurations = async () => {
|
|
|
|
|
const response = await getDefaultConfigurationsApiV1UserConfigurationsDefaultsGet();
|
|
|
|
|
if (response.data) {
|
|
|
|
|
setSchemas({
|
|
|
|
|
llm: response.data.llm as Record<string, ProviderSchema>,
|
|
|
|
|
tts: response.data.tts as Record<string, ProviderSchema>,
|
2026-01-17 14:37:03 +05:30
|
|
|
stt: response.data.stt as Record<string, ProviderSchema>,
|
|
|
|
|
embeddings: response.data.embeddings as Record<string, ProviderSchema>
|
2025-09-09 14:37:32 +05:30
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
console.error("Failed to fetch configurations");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultValues: Record<string, string | number | boolean> = {};
|
|
|
|
|
const selectedProviders: Record<ServiceSegment, string> = {
|
|
|
|
|
llm: response.data.default_providers.llm,
|
|
|
|
|
tts: response.data.default_providers.tts,
|
2026-01-17 14:37:03 +05:30
|
|
|
stt: response.data.default_providers.stt,
|
|
|
|
|
embeddings: response.data.default_providers.embeddings
|
2025-09-09 14:37:32 +05:30
|
|
|
};
|
|
|
|
|
|
2026-03-10 15:17:40 +05:30
|
|
|
const loadedApiKeys: Record<ServiceSegment, string[]> = {
|
|
|
|
|
llm: [""],
|
|
|
|
|
tts: [""],
|
|
|
|
|
stt: [""],
|
|
|
|
|
embeddings: [""],
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-09 14:37:32 +05:30
|
|
|
const setServicePropertyValues = (service: ServiceSegment) => {
|
|
|
|
|
if (userConfig?.[service]?.provider) {
|
|
|
|
|
Object.entries(userConfig?.[service]).forEach(([field, value]) => {
|
2026-03-10 15:17:40 +05:30
|
|
|
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") {
|
2026-03-10 15:26:39 +05:30
|
|
|
defaultValues[`${service}_${field}`] = value as string | number | boolean;
|
2025-09-09 14:37:32 +05:30
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
selectedProviders[service] = userConfig?.[service]?.provider as string;
|
2026-03-24 17:50:45 +05:30
|
|
|
// Fill in schema defaults for fields not present in userConfig
|
|
|
|
|
const properties = response.data[service]?.[selectedProviders[service]]?.properties as Record<string, SchemaProperty>;
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-09 14:37:32 +05:30
|
|
|
} else {
|
|
|
|
|
const properties = response.data[service]?.[selectedProviders[service]]?.properties as Record<string, SchemaProperty>;
|
|
|
|
|
if (properties) {
|
|
|
|
|
Object.entries(properties).forEach(([field, schema]) => {
|
2026-03-24 17:50:45 +05:30
|
|
|
if (field !== "provider" && schema.default !== undefined) {
|
2025-09-09 14:37:32 +05:30
|
|
|
defaultValues[`${service}_${field}`] = schema.default;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setServicePropertyValues("llm");
|
|
|
|
|
setServicePropertyValues("tts");
|
|
|
|
|
setServicePropertyValues("stt");
|
2026-01-17 14:37:03 +05:30
|
|
|
setServicePropertyValues("embeddings");
|
2025-09-09 14:37:32 +05:30
|
|
|
|
2025-12-17 14:56:25 +07:00
|
|
|
// 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
|
2025-09-09 14:37:32 +05:30
|
|
|
reset(defaultValues);
|
2026-03-10 15:17:40 +05:30
|
|
|
setApiKeys(loadedApiKeys);
|
2025-12-17 14:56:25 +07:00
|
|
|
setServiceProviders(selectedProviders);
|
2025-09-09 14:37:32 +05:30
|
|
|
};
|
|
|
|
|
fetchConfigurations();
|
|
|
|
|
}, [reset, userConfig]);
|
|
|
|
|
|
2025-12-25 15:05:53 +05:30
|
|
|
// 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]);
|
|
|
|
|
|
2026-02-13 10:11:48 +05:30
|
|
|
// 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]);
|
|
|
|
|
|
2026-02-13 11:44:57 +05:30
|
|
|
// 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]);
|
|
|
|
|
|
2025-09-09 14:37:32 +05:30
|
|
|
const handleProviderChange = (service: ServiceSegment, providerName: string) => {
|
|
|
|
|
if (!providerName) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentValues = getValues();
|
|
|
|
|
const preservedValues: Record<string, string | number | boolean> = {};
|
|
|
|
|
|
|
|
|
|
// 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 }));
|
2026-03-10 15:17:40 +05:30
|
|
|
setApiKeys(prev => ({ ...prev, [service]: [""] }));
|
2025-12-25 15:05:53 +05:30
|
|
|
|
|
|
|
|
// Reset manual model input when LLM provider changes
|
|
|
|
|
if (service === "llm") {
|
|
|
|
|
setIsManualModelInput(false);
|
|
|
|
|
}
|
2025-09-09 14:37:32 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onSubmit = async (data: FormValues) => {
|
|
|
|
|
setApiError(null);
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
|
2026-03-10 15:17:40 +05:30
|
|
|
// 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<ServiceSegment, Record<string, string | number | string[]>> = {
|
2025-09-09 14:37:32 +05:30
|
|
|
llm: {
|
|
|
|
|
provider: serviceProviders.llm,
|
2026-03-19 15:07:41 +05:30
|
|
|
...(getServiceApiKeys("llm").length > 0 && { api_key: getServiceApiKeys("llm") }),
|
2025-09-09 14:37:32 +05:30
|
|
|
model: data.llm_model as string
|
|
|
|
|
},
|
|
|
|
|
tts: {
|
|
|
|
|
provider: serviceProviders.tts,
|
2026-03-19 15:07:41 +05:30
|
|
|
...(getServiceApiKeys("tts").length > 0 && { api_key: getServiceApiKeys("tts") }),
|
2025-09-09 14:37:32 +05:30
|
|
|
},
|
|
|
|
|
stt: {
|
|
|
|
|
provider: serviceProviders.stt,
|
2026-03-19 15:07:41 +05:30
|
|
|
...(getServiceApiKeys("stt").length > 0 && { api_key: getServiceApiKeys("stt") }),
|
2026-01-17 14:37:03 +05:30
|
|
|
},
|
|
|
|
|
embeddings: {
|
|
|
|
|
provider: serviceProviders.embeddings,
|
2026-03-19 15:07:41 +05:30
|
|
|
...(getServiceApiKeys("embeddings").length > 0 && { api_key: getServiceApiKeys("embeddings") }),
|
2026-01-17 14:37:03 +05:30
|
|
|
model: data.embeddings_model as string
|
2025-09-09 14:37:32 +05:30
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Add any extra properties in the payload
|
|
|
|
|
Object.entries(data).forEach(([property, value]) => {
|
|
|
|
|
const parts = property.split('_');
|
|
|
|
|
const service = parts[0] as ServiceSegment;
|
2025-11-29 15:39:57 +05:30
|
|
|
const field = parts.slice(1).join('_');
|
2025-09-09 14:37:32 +05:30
|
|
|
|
2026-03-10 15:17:40 +05:30
|
|
|
if (field === "api_key") return; // handled via apiKeys state
|
2025-09-09 14:37:32 +05:30
|
|
|
if (userConfig[service] && !(field in userConfig[service])) {
|
|
|
|
|
(userConfig[service] as Record<string, string>)[field] = value as string;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-17 14:37:03 +05:30
|
|
|
// Build save config - only include embeddings if api_key is provided
|
|
|
|
|
const saveConfig: {
|
2026-03-10 15:17:40 +05:30
|
|
|
llm: Record<string, string | number | string[]>;
|
|
|
|
|
tts: Record<string, string | number | string[]>;
|
|
|
|
|
stt: Record<string, string | number | string[]>;
|
|
|
|
|
embeddings?: Record<string, string | number | string[]>;
|
2026-01-17 14:37:03 +05:30
|
|
|
} = {
|
|
|
|
|
llm: userConfig.llm,
|
|
|
|
|
tts: userConfig.tts,
|
|
|
|
|
stt: userConfig.stt
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Only include embeddings if user has configured it (has api_key)
|
2026-03-10 15:17:40 +05:30
|
|
|
const embeddingsKeys = getServiceApiKeys("embeddings");
|
|
|
|
|
if (embeddingsKeys.length > 0) {
|
2026-01-17 14:37:03 +05:30
|
|
|
saveConfig.embeddings = userConfig.embeddings;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 14:37:32 +05:30
|
|
|
try {
|
2026-01-17 14:37:03 +05:30
|
|
|
await saveUserConfig(saveConfig);
|
2025-09-09 14:37:32 +05:30
|
|
|
setApiError(null);
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
setApiError(error.message);
|
|
|
|
|
} else {
|
|
|
|
|
setApiError('An unknown error occurred');
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-29 15:39:57 +05:30
|
|
|
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)
|
2026-01-23 18:53:59 +05:30
|
|
|
const fields = Object.keys(providerSchema.properties).filter(
|
2025-11-29 15:39:57 +05:30
|
|
|
field => field !== "provider" && field !== "api_key"
|
|
|
|
|
);
|
2026-01-23 18:53:59 +05:30
|
|
|
|
|
|
|
|
return fields;
|
2025-11-29 15:39:57 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderServiceFields = (service: ServiceSegment) => {
|
2025-09-09 14:37:32 +05:30
|
|
|
const currentProvider = serviceProviders[service];
|
|
|
|
|
const providerSchema = schemas?.[service]?.[currentProvider];
|
|
|
|
|
const availableProviders = schemas?.[service] ? Object.keys(schemas[service]) : [];
|
2025-11-29 15:39:57 +05:30
|
|
|
const configFields = getConfigFields(service);
|
2025-09-09 14:37:32 +05:30
|
|
|
|
|
|
|
|
return (
|
2025-11-29 15:39:57 +05:30
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Provider and first config field in one row */}
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label>Provider</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={currentProvider}
|
|
|
|
|
onValueChange={(providerName) => {
|
|
|
|
|
handleProviderChange(service, providerName);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-full">
|
|
|
|
|
<SelectValue placeholder="Select provider" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{availableProviders.map((provider) => (
|
|
|
|
|
<SelectItem key={provider} value={provider}>
|
|
|
|
|
{provider}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{currentProvider && providerSchema && configFields[0] && (
|
2025-09-09 14:37:32 +05:30
|
|
|
<div className="space-y-2">
|
2025-11-29 15:39:57 +05:30
|
|
|
<Label className="capitalize">{configFields[0].replace(/_/g, ' ')}</Label>
|
|
|
|
|
{renderField(service, configFields[0], providerSchema)}
|
2025-09-09 14:37:32 +05:30
|
|
|
</div>
|
2025-11-29 15:39:57 +05:30
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Additional config fields (like voice for TTS) */}
|
|
|
|
|
{currentProvider && providerSchema && configFields.length > 1 && (
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
{configFields.slice(1).map((field) => (
|
|
|
|
|
<div key={field} className="space-y-2">
|
|
|
|
|
<Label className="capitalize">{field.replace(/_/g, ' ')}</Label>
|
|
|
|
|
{renderField(service, field, providerSchema)}
|
2025-09-09 14:37:32 +05:30
|
|
|
</div>
|
2025-11-29 15:39:57 +05:30
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-10 15:17:40 +05:30
|
|
|
{/* API Key(s) */}
|
2025-11-29 15:39:57 +05:30
|
|
|
{currentProvider && providerSchema && providerSchema.properties.api_key && (
|
|
|
|
|
<div className="space-y-2">
|
2026-03-10 15:17:40 +05:30
|
|
|
<Label>API Key(s)</Label>
|
|
|
|
|
{apiKeys[service].map((key, index) => (
|
|
|
|
|
<div key={index} className="flex gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Enter API key"
|
|
|
|
|
value={key}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const newKeys = [...apiKeys[service]];
|
|
|
|
|
newKeys[index] = e.target.value;
|
|
|
|
|
setApiKeys(prev => ({ ...prev, [service]: newKeys }));
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
{apiKeys[service].length > 1 && (
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="shrink-0"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setApiKeys(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[service]: prev[service].filter((_, i) => i !== index),
|
|
|
|
|
}));
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<X className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setApiKeys(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[service]: [...prev[service], ""],
|
|
|
|
|
}));
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-4 w-4 mr-1" /> Add API Key
|
|
|
|
|
</Button>
|
2025-09-09 14:37:32 +05:30
|
|
|
</div>
|
2025-11-29 15:39:57 +05:30
|
|
|
)}
|
|
|
|
|
</div>
|
2025-09-09 14:37:32 +05:30
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-29 15:39:57 +05:30
|
|
|
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;
|
|
|
|
|
|
2025-12-26 16:00:02 +05:30
|
|
|
// Use VoiceSelector for voice field in TTS service (except Sarvam which uses predefined options)
|
2025-12-25 15:05:53 +05:30
|
|
|
if (service === "tts" && field === "voice") {
|
|
|
|
|
const currentProvider = serviceProviders.tts;
|
2025-12-26 16:00:02 +05:30
|
|
|
// Sarvam uses predefined voice options, not VoiceSelector
|
|
|
|
|
const hasVoiceOptions = actualSchema?.enum || actualSchema?.examples;
|
|
|
|
|
if (currentProvider !== "sarvam" && !hasVoiceOptions) {
|
2025-12-25 15:05:53 +05:30
|
|
|
return (
|
|
|
|
|
<VoiceSelector
|
|
|
|
|
provider={currentProvider}
|
|
|
|
|
value={watch(`${service}_${field}`) as string || ""}
|
|
|
|
|
onChange={(voiceId) => {
|
|
|
|
|
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 (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Enter model name"
|
|
|
|
|
value={currentValue}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
setValue(`${service}_${field}`, e.target.value, { shouldDirty: true });
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="manual-model-input"
|
|
|
|
|
checked={isManualModelInput}
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
setIsManualModelInput(checked as boolean);
|
|
|
|
|
if (!checked && modelOptions.length > 0) {
|
|
|
|
|
// Reset to first option when switching back
|
|
|
|
|
setValue(`${service}_${field}`, modelOptions[0], { shouldDirty: true });
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="manual-model-input"
|
|
|
|
|
className="text-sm font-normal cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
Add Model Manually
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Select
|
|
|
|
|
value={currentValue}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
if (!value) return;
|
|
|
|
|
setValue(`${service}_${field}`, value, { shouldDirty: true });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-full">
|
|
|
|
|
<SelectValue placeholder="Select model" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{modelOptions.map((value: string) => (
|
|
|
|
|
<SelectItem key={value} value={value}>
|
|
|
|
|
{value}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="manual-model-input-dropdown"
|
|
|
|
|
checked={isManualModelInput}
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
setIsManualModelInput(checked as boolean);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="manual-model-input-dropdown"
|
|
|
|
|
className="text-sm font-normal cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
Add Model Manually
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 16:00:02 +05:30
|
|
|
// Handle fields with enum or examples (dropdown options)
|
2026-02-13 10:11:48 +05:30
|
|
|
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];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 16:00:02 +05:30
|
|
|
if (dropdownOptions && dropdownOptions.length > 0) {
|
2025-12-25 15:05:53 +05:30
|
|
|
// 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") {
|
2026-02-13 10:11:48 +05:30
|
|
|
return VOICE_DISPLAY_NAMES[value] || value.charAt(0).toUpperCase() + value.slice(1);
|
2025-12-25 15:05:53 +05:30
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-29 15:39:57 +05:30
|
|
|
return (
|
|
|
|
|
<Select
|
|
|
|
|
value={watch(`${service}_${field}`) as string || ""}
|
|
|
|
|
onValueChange={(value) => {
|
2025-12-17 14:56:25 +07:00
|
|
|
// Ignore empty string - Radix Select sometimes calls onValueChange('')
|
|
|
|
|
// when options change, even if current value is valid
|
|
|
|
|
if (!value) return;
|
2025-11-29 15:39:57 +05:30
|
|
|
setValue(`${service}_${field}`, value, { shouldDirty: true });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-full">
|
|
|
|
|
<SelectValue placeholder={`Select ${field}`} />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
2025-12-26 16:00:02 +05:30
|
|
|
{dropdownOptions.map((value: string) => (
|
2025-11-29 15:39:57 +05:30
|
|
|
<SelectItem key={value} value={value}>
|
2025-12-25 15:05:53 +05:30
|
|
|
{getDisplayName(value)}
|
2025-11-29 15:39:57 +05:30
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-09-09 14:37:32 +05:30
|
|
|
|
2025-11-29 15:39:57 +05:30
|
|
|
return (
|
|
|
|
|
<Input
|
|
|
|
|
type={actualSchema?.type === "number" ? "number" : "text"}
|
|
|
|
|
{...(actualSchema?.type === "number" && { step: "any" })}
|
|
|
|
|
placeholder={`Enter ${field}`}
|
|
|
|
|
{...register(`${service}_${field}`, {
|
2026-01-18 14:48:53 +05:30
|
|
|
// Embeddings is optional, so don't require its fields
|
|
|
|
|
required: service !== "embeddings" && providerSchema.required?.includes(field),
|
2025-11-29 15:39:57 +05:30
|
|
|
valueAsNumber: actualSchema?.type === "number"
|
|
|
|
|
})}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
2025-09-09 14:37:32 +05:30
|
|
|
|
2025-11-29 15:39:57 +05:30
|
|
|
return (
|
|
|
|
|
<div className="w-full max-w-2xl mx-auto">
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<h1 className="text-3xl font-bold mb-2">AI Models Configuration</h1>
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
Configure your AI model, voice, and transcription services.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="pt-6">
|
|
|
|
|
<Tabs defaultValue="llm" className="w-full">
|
2026-01-17 14:37:03 +05:30
|
|
|
<TabsList className="grid w-full grid-cols-4 mb-6">
|
2025-11-29 15:39:57 +05:30
|
|
|
{TAB_CONFIG.map(({ key, label }) => (
|
|
|
|
|
<TabsTrigger key={key} value={key}>
|
|
|
|
|
{label}
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
))}
|
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
{TAB_CONFIG.map(({ key }) => (
|
|
|
|
|
<TabsContent key={key} value={key} className="mt-0">
|
|
|
|
|
{renderServiceFields(key)}
|
|
|
|
|
</TabsContent>
|
|
|
|
|
))}
|
|
|
|
|
</Tabs>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{apiError && <p className="text-red-500 mt-4">{apiError}</p>}
|
|
|
|
|
|
|
|
|
|
<Button type="submit" className="w-full mt-6" disabled={isSaving}>
|
2025-09-09 14:37:32 +05:30
|
|
|
{isSaving ? "Saving..." : "Save Configuration"}
|
|
|
|
|
</Button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|