Files

1 line
30 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "ahmadali0071/va-cd", "files": {"admin-panel/src/pages/ApiSettings.jsx": {"aa55182da05c2078587a920716769716698f1f94": {"t": 1785746562.0892217, "n": 27250, "h": "1309be46eec6ffed", "c": "import { useState, useEffect, useRef } from \"react\";\n\nconst API = \"/api/admin\";\n\nfunction call(path, options = {}) {\n const token = localStorage.getItem(\"admin_token\");\n return fetch(`${API}${path}`, {\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\", ...options.headers },\n ...options,\n }).then(r => r.ok ? r.json() : r.json().then(e => { throw new Error(e.error || \"Failed\"); }));\n}\n\nconst LLM_PROVIDERS = [\n { key: \"deepseek\", label: \"DeepSeek\", baseURL: \"https://api.deepseek.com/v1\" },\n { key: \"openai\", label: \"OpenAI\", baseURL: \"https://api.openai.com/v1\" },\n { key: \"groq\", label: \"Groq\", baseURL: \"https://api.groq.com/openai/v1\" },\n { key: \"anthropic\", label: \"Anthropic Claude\", baseURL: \"https://api.anthropic.com/v1\" },\n { key: \"gemini\", label: \"Google Gemini\", baseURL: \"https://generativelanguage.googleapis.com/v1beta\" },\n { key: \"mistral\", label: \"Mistral AI\", baseURL: \"https://api.mistral.ai/v1\" },\n { key: \"perplexity\", label: \"Perplexity\", baseURL: \"https://api.perplexity.ai\" },\n { key: \"xai\", label: \"xAI Grok\", baseURL: \"https://api.x.ai/v1\" },\n { key: \"together\", label: \"Together AI\", baseURL: \"https://api.together.xyz/v1\" },\n { key: \"fireworks\", label: \"Fireworks AI\", baseURL: \"https://api.fireworks.ai/inference/v1\" },\n { key: \"replicate\", label: \"Replicate\", baseURL: \"https://api.replicate.com/v1\" },\n { key: \"cohere\", label: \"Cohere\", baseURL: \"https://api.cohere.ai/v1\" },\n { key: \"custom\", label: \"Custom Provider\", baseURL: \"\" },\n];\n\nconst ALL_MODELS = [\n { name: \"deepseek-reasoner\", provider: \"deepseek\", label: \"DeepSeek R1\" },\n { name: \"deepseek-chat\", provider: \"deepseek\", label: \"DeepSeek V3\" },\n { name: \"gpt-4o\", provider: \"openai\", label: \"GPT-4o\" },\n { name: \"gpt-4o-mini\", provider: \"openai\", label: \"GPT-4o Mini\" },\n { name: \"gpt-4-turbo\", provider: \"openai\", label: \"GPT-4 Turbo\" },\n { name: \"gpt-4\", provider: \"openai\", label: \"GPT-4\" },\n { name: \"gpt-3.5-turbo\", provider: \"openai\", label: \"GPT-3.5 Turbo\" },\n { name: \"o1\", provider: \"openai\", label: \"OpenAI o1\" },\n { name: \"o1-mini\", provider: \"openai\", label: \"OpenAI o1 Mini\" },\n { name: \"o3-mini\", provider: \"openai\", label: \"OpenAI o3 Mini\" },\n { name: \"llama-3.3-70b-versatile\", provider: \"groq\", label: \"Llama 3.3 70B\" },\n { name: \"llama-3.1-8b-instant\", provider: \"groq\", label: \"Llama 3.1 8B\" },\n { name: \"mixtral-8x7b-32768\", provider: \"groq\", label: \"Mixtral 8x7B\" },\n { name: \"gemma2-9b-it\", provider: \"groq\", label: \"Gemma 2 9B\" },\n { name: \"claude-3-opus-20240229\", provider: \"anthropic\", label: \"Claude 3 Opus\" },\n { name: \"claude-3-sonnet-20240229\", provider: \"anthropic\", label: \"Claude 3 Sonnet\" },\n { name: \"claude-3-haiku-20240307\", provider: \"anthropic\", label: \"Claude 3 Haiku\" },\n { name: \"claude-3-5-sonnet-20240620\", provider: \"anthropic\", label: \"Claude 3.5 Sonnet\" },\n { name: \"claude-3-5-haiku-20241022\", provider: \"anthropic\", label: \"Claude 3.5 Haiku\" },\n { name: \"gemini-2.0-flash\", provider: \"gemini\", label: \"Gemini 2.0 Flash\" },\n { name: \"gemini-1.5-pro\", provider: \"gemini\", label: \"Gemini 1.5 Pro\" },\n { name: \"gemini-1.5-flash\", provider: \"gemini\", label: \"Gemini 1.5 Flash\" },\n { name: \"gemini-1.0-pro\", provider: \"gemini\", label: \"Gemini 1.0 Pro\" },\n { name: \"mistral-large-latest\", provider: \"mistral\", label: \"Mistral Large\" },\n { name: \"mistral-small-latest\", provider: \"mistral\", label: \"Mistral Small\" },\n { name: \"mistral-medium\", provider: \"mistral\", label: \"Mistral Medium\" },\n { name: \"codestral-latest\", provider: \"mistral\", label: \"Codestral\" },\n { name: \"sonar-reasoning\", provider: \"perplexity\", label: \"Sonar Reasoning\" },\n { name: \"sonar-pro\", provider: \"perplexity\", label: \"Sonar Pro\" },\n { name: \"sonar\", provider: \"perplexity\", label: \"Sonar\" },\n { name: \"grok-2-1212\", provider: \"xai\", label: \"Grok 2\" },\n { name: \"grok-2-vision-1212\", provider: \"xai\", label: \"Grok 2 Vision\" },\n { name: \"grok-beta\", provider: \"xai\", label: \"Grok Beta\" },\n { name: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\", provider: \"together\", label: \"Llama 3.3 70B Turbo\" },\n { name: \"meta-llama/Llama-3.1-8B-Instruct-Turbo\", provider: \"together\", label: \"Llama 3.1 8B Turbo\" },\n { name: \"Qwen/Qwen2.5-72B-Instruct-Turbo\", provider: \"together\", label: \"Qwen 2.5 72B\" },\n { name: \"mistralai/Mixtral-8x22B-Instruct-v0.1\", provider: \"together\", label: \"Mixtral 8x22B\" },\n { name: \"deepseek-ai/DeepSeek-R1\", provider: \"together\", label: \"DeepSeek R1 (Together)\" },\n { name: \"accounts/fireworks/models/llama-v3p3-70b-instruct\", provider: \"fireworks\", label: \"Llama 3.3 70B (FW)\" },\n { name: \"accounts/fireworks/models/mixtral-8x22b-instruct\", provider: \"fireworks\", label: \"Mixtral 8x22B (FW)\" },\n { name: \"meta/meta-llama-3-70b-instruct\", provider: \"replicate\", label: \"Llama 3 70B (Rep)\" },\n { name: \"command-r-plus\", provider: \"cohere\", label: \"Command R+\" },\n { name: \"command-r\", provider: \"cohere\", label: \"Command R\" },\n { name: \"command\", provider: \"cohere\", label: \"Command\" },\n];\n\nconst TTS_PROVIDERS = [\n { key: \"polly\", label: \"Amazon Polly (Free)\" },\n { key: \"elevenlabs\", label: \"ElevenLabs (11 Labs)\" },\n { key: \"openai_tts\", label: \"OpenAI TTS\" },\n { key: \"deepgram_tts\", label: \"Deepgram Aura\" },\n { key: \"playht\", label: \"Play.ht\" },\n { key: \"google_tts\", label: \"Google Cloud TTS\" },\n { key: \"azure_tts\", label: \"Azure TTS\" },\n { key: \"custom\", label: \"Custom TTS\" },\n];\n\nconst ALL_VOICES = [\n { name: \"Polly.Joanna-Neural\", provider: \"polly\", label: \"Joanna (Female, Warm)\" },\n { name: \"Polly.Matthew-Neural\", provider: \"polly\", label: \"Matthew (Male, Friendly)\" },\n { name: \"Polly.Ruth-Neural\", provider: \"polly\", label: \"Ruth (Female, Professional)\" },\n { name: \"Polly.Amy-Neural\", provider: \"polly\", label: \"Amy (Female, British)\" },\n { name: \"Polly.Stephen-Neural\", provider: \"polly\", label: \"Stephen (Male, Deep)\" },\n { name: \"Polly.Salli-Neural\", provider: \"polly\", label: \"Salli (Female, US)\" },\n { name: \"Polly.Kimberly-Neural\", provider: \"polly\", label: \"Kimberly (Female, US)\" },\n { name: \"Polly.Kendra-Neural\", provider: \"polly\", label: \"Kendra (Female, US)\" },\n { name: \"Rachel\", provider: \"elevenlabs\", label: \"Rachel (Female, Warm)\" },\n { name: \"Adam\", provider: \"elevenlabs\", label: \"Adam (Male, Deep)\" },\n { name: \"Antoni\", provider: \"elevenlabs\", label: \"Antoni (Male, British)\" },\n { name: \"Bella\", provider: \"elevenlabs\", label: \"Bella (Female, Young)\" },\n { name: \"Domi\", provider: \"elevenlabs\", label: \"Domi (Female, Energetic)\" },\n { name: \"Elli\", provider: \"elevenlabs\", label: \"Elli (Female, Soft)\" },\n { name: \"Josh\", provider: \"elevenlabs\", label: \"Josh (Male, Casual)\" },\n { name: \"Arnold\", provider: \"elevenlabs\", label: \"Arnold (Male, Authoritative)\" },\n { name: \"alloy\", provider: \"openai_tts\", label: \"Alloy (Neutral)\" },\n { name: \"echo\", provider: \"openai_tts\", label: \"Echo (Male)\" },\n { name: \"fable\", provider: \"openai_tts\", label: \"Fable (British)\" },\n { name: \"onyx\", provider: \"openai_tts\", label: \"Onyx (Deep Male)\" },\n { name: \"nova\", provider: \"openai_tts\", label: \"Nova (Female)\" },\n { name: \"shimmer\", provider: \"openai_tts\", label: \"Shimmer (Soft Female)\" },\n { name: \"aura-asteria-en\", provider: \"deepgram_tts\", label: \"Asteria (Female, US)\" },\n { name: \"aura-luna-en\", provider: \"deepgram_tts\", label: \"Luna (Female, US)\" },\n { name: \"aura-stella-en\", provider: \"deepgram_tts\", label: \"Stella (Female, US)\" },\n { name: \"aura-athena-en\", provider: \"deepgram_tts\", label: \"Athena (Female, UK)\" },\n { name: \"aura-hera-en\", provider: \"deepgram_tts\", label: \"Hera (Female, US)\" },\n { name: \"aura-orion-en\", provider: \"deepgram_tts\", label: \"Orion (Male, US)\" },\n { name: \"aura-arcas-en\", provider: \"deepgram_tts\", label: \"Arcas (Male, US)\" },\n { name: \"en-US-Standard-C\", provider: \"google_tts\", label: \"Standard C (Female, US)\" },\n { name: \"en-US-Standard-D\", provider: \"google_tts\", label: \"Standard D (Male, US)\" },\n { name: \"en-US-Wavenet-C\", provider: \"google_tts\", label: \"Wavenet C (Female, US)\" },\n { name: \"en-US-Wavenet-D\", provider: \"google_tts\", label: \"Wavenet D (Male, US)\" },\n { name: \"en-US-Studio-O\", provider: \"google_tts\", label: \"Studio O (Female, US)\" },\n { name: \"en-GB-Wavenet-B\", provider: \"google_tts\", label: \"Wavenet B (Male, UK)\" },\n { name: \"en-US-AriaNeural\", provider: \"azure_tts\", label: \"Aria (Female, US)\" },\n { name: \"en-US-GuyNeural\", provider: \"azure_tts\", label: \"Guy (Male, US)\" },\n { name: \"en-US-JennyNeural\", provider: \"azure_tts\", label: \"Jenny (Female, US)\" },\n { name: \"en-GB-SoniaNeural\", provider: \"azure_tts\", label: \"Sonia (Female, UK)\" },\n { name: \"en-GB-RyanNeural\", provider: \"azure_tts\", label: \"Ryan (Male, UK)\" },\n];\n\nconst API_KEY_DEFS = [\n { key: \"deepseek_api_key\", label: \"DeepSeek\", category: \"llm\", provider: \"deepseek\" },\n { key: \"openai_api_key\", label: \"OpenAI\", category: \"llm\", provider: \"openai\" },\n { key: \"groq_api_key\", label: \"Groq\", category: \"llm\", provider: \"groq\" },\n { key: \"anthropic_api_key\", label: \"Claude\", category: \"llm\", provider: \"anthropic\" },\n { key: \"gemini_api_key\", label: \"Gemini\", category: \"llm\", provider: \"gemini\" },\n { key: \"mistral_api_key\", label: \"Mistral\", category: \"llm\", provider: \"mistral\" },\n { key: \"perplexity_api_key\", label: \"Perplexity\", category: \"llm\", provider: \"perplexity\" },\n { key: \"xai_api_key\", label: \"xAI Grok\", category: \"llm\", provider: \"xai\" },\n { key: \"together_api_key\", label: \"Together AI\", category: \"llm\", provider: \"together\" },\n { key: \"fireworks_api_key\", label: \"Fireworks AI\", category: \"llm\", provider: \"fireworks\" },\n { key: \"replicate_api_key\", label: \"Replicate\", category: \"llm\", provider: \"replicate\" },\n { key: \"cohere_api_key\", label: \"Cohere\", category: \"llm\", provider: \"cohere\" },\n { key: \"elevenlabs_api_key\", label: \"ElevenLabs\", category: \"voice\", provider: \"elevenlabs\" },\n { key: \"deepgram_api_key\", label: \"Deepgram\", category: \"stt\", provider: \"deepgram\" },\n { key: \"playht_api_key\", label: \"Play.ht\", category: \"voice\", provider: \"playht\" },\n { key: \"google_tts_api_key\", label: \"Google Cloud TTS\", category: \"voice\", provider: \"google_tts\" },\n { key: \"azure_tts_api_key\", label: \"Azure TTS\", category: \"voice\", provider: \"azure_tts\" },\n];\n\nfunction SearchInput({ value, onChange, items, placeholder, providerFilter }) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(value || \"\");\n const ref = useRef(null);\n\n useEffect(() => { setSearch(value || \"\"); }, [value]);\n\n const filtered = (search?.length > 0)\n ? items.filter(m => {\n if (providerFilter && m.provider !== providerFilter) return false;\n return m.name.toLowerCase().includes(search.toLowerCase()) || m.label.toLowerCase().includes(search.toLowerCase());\n }).slice(0, 8)\n : (providerFilter \n ? items.filter(m => m.provider === providerFilter).slice(0, 6) \n : items.slice(0, 8));\n\n const exactMatch = filtered.find(m => m.name === search);\n\n return (\n <div style={{ position: \"relative\" }} ref={ref}>\n <input\n style={{ width: \"100%\", padding: \"9px 14px\", borderRadius: 10, border: \"1.5px solid #e5e7eb\", fontSize: 13, outline: \"none\", background: \"#fafafa\" }}\n placeholder={placeholder}\n value={search}\n onFocus={() => setOpen(true)}\n onBlur={() => setTimeout(() => setOpen(false), 200)}\n onChange={e => { setSearch(e.target.value); setOpen(true); onChange?.(e.target.value); }}\n onKeyDown={e => { if (e.key === \"Enter\") { onChange?.(search); setOpen(false); } }}\n />\n {open && (\n <div style={{ position: \"absolute\", top: \"100%\", left: 0, right: 0, background: \"#fff\", borderRadius: 10, boxShadow: \"0 10px 40px rgba(0,0,0,0.12)\", border: \"1px solid #e5e7eb\", zIndex: 50, maxHeight: 240, overflow: \"auto\", marginTop: 4 }}>\n {search?.length > 0 && filtered.length > 0 && filtered.map(m => (\n <div key={m.name} onMouseDown={() => { setSearch(m.name); onChange?.(m.name); setOpen(false); }}\n style={{ padding: \"8px 14px\", cursor: \"pointer\", fontSize: 13, background: m.name === search ? \"#f0f0ff\" : \"transparent\", borderBottom: \"1px solid #f3f4f6\" }}>\n <div style={{ fontWeight: 600, color: \"#111827\" }}>{m.label}</div>\n <div style={{ fontSize: 11, color: \"#9ca3af\" }}>{m.name}</div>\n </div>\n ))}\n {!search?.length && filtered.map(m => (\n <div key={m.name} onMouseDown={() => { setSearch(m.name); onChange?.(m.name); setOpen(false); }}\n style={{ padding: \"8px 14px\", cursor: \"pointer\", fontSize: 13, borderBottom: \"1px solid #f3f4f6\" }}>\n <div style={{ fontWeight: 600, color: \"#111827\" }}>{m.label}</div>\n <div style={{ fontSize: 11, color: \"#9ca3af\" }}>{m.name}</div>\n </div>\n ))}\n {!search?.length && (\n <div style={{ padding: \"10px 14px\", fontSize: 12, color: \"#9ca3af\" }}>Type to search or pick from above</div>\n )}\n {!exactMatch && search?.length > 1 && (\n <div onMouseDown={() => { setSearch(search); onChange?.(search); setOpen(false); }}\n style={{ padding: \"10px 14px\", cursor: \"pointer\", fontSize: 12, background: \"#f0fdf4\", color: \"#16a34a\", borderTop: \"1px solid #bbf7d0\" }}>\n + Use \"<strong>{search}</strong>\" as custom\n </div>\n )}\n {search?.length > 0 && filtered.length === 0 && (\n <div style={{ padding: \"10px 14px\", fontSize: 12, color: \"#9ca3af\" }}>No matches found</div>\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport default function ApiSettings() {\n const [settings, setSettings] = useState({});\n const [loading, setLoading] = useState(true);\n const [saving, setSaving] = useState(false);\n const [msg, setMsg] = useState(\"\");\n const [err, setErr] = useState(\"\");\n const [editingKey, setEditingKey] = useState(null);\n const [editValue, setEditValue] = useState(\"\");\n\n useEffect(() => { load(); }, []);\n\n async function load() {\n setLoading(true);\n try { setSettings(await call(\"/apisettings\")); } catch (e) { setErr(e.message); }\n finally { setLoading(false); }\n }\n\n async function save(silent) {\n setSaving(true);\n if (!silent) { setMsg(\"\"); setErr(\"\"); }\n try {\n await call(\"/apisettings\", { method: \"PUT\", body: JSON.stringify(settings) });\n if (!silent) setMsg(\"Saved!\");\n } catch (e) { if (!silent) setErr(e.message); }\n finally { setSaving(false); }\n }\n\n const setVal = (k, v) => {\n const next = { ...settings, [k]: v };\n if (k === \"llm_provider\" && v !== \"custom\") {\n const defaultModel = ALL_MODELS.find(m => m.provider === v);\n if (defaultModel) next.llm_model = defaultModel.name;\n }\n if (k === \"tts_provider\" && v !== \"custom\") {\n const defaultVoice = ALL_VOICES.find(m => m.provider === v);\n if (defaultVoice) next.tts_voice = defaultVoice.name;\n }\n setSettings(next);\n };\n\n async function deleteKey(key) {\n if (!confirm(`Remove ${key}?`)) return;\n setSettings(s => ({ ...s, [key]: \"\" }));\n await save(true);\n load();\n }\n\n async function editKey(key) {\n setEditingKey(key);\n setEditValue(settings[key] || \"\");\n }\n\n async function saveKey() {\n setSettings(s => ({ ...s, [editingKey]: editValue }));\n setEditingKey(null);\n await save(true);\n load();\n }\n\n const activeKeys = API_KEY_DEFS.filter(d => (settings[d.key] || \"\").length > 8);\n const prov = settings.llm_provider || \"deepseek\";\n\n const s = {\n page: { padding: 32, maxWidth: 900 },\n title: { fontSize: 26, fontWeight: 800, color: \"#111827\", marginBottom: 4 },\n sub: { fontSize: 13, color: \"#6b7280\", marginBottom: 28 },\n card: { background: \"#fff\", borderRadius: 14, padding: 24, marginBottom: 18, boxShadow: \"0 1px 3px rgba(0,0,0,0.05)\", border: \"1px solid #f3f4f6\" },\n cardTitle: { fontSize: 15, fontWeight: 700, color: \"#111827\", marginBottom: 18, display: \"flex\", alignItems: \"center\", gap: 8 },\n sectionIcon: (bg) => ({ width: 32, height: 32, borderRadius: 10, background: bg, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", fontSize: 16 }),\n grid: { display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 16 },\n label: { fontSize: 12, fontWeight: 600, color: \"#4b5563\", marginBottom: 5, display: \"block\" },\n input: { width: \"100%\", padding: \"9px 14px\", borderRadius: 10, border: \"1.5px solid #e5e7eb\", fontSize: 13, outline: \"none\", background: \"#fafafa\" },\n select: { width: \"100%\", padding: \"9px 14px\", borderRadius: 10, border: \"1.5px solid #e5e7eb\", fontSize: 13, background: \"#fff\", cursor: \"pointer\" },\n btn: { padding: \"10px 22px\", borderRadius: 10, border: \"none\", cursor: \"pointer\", fontWeight: 600, fontSize: 13, transition: \"all 0.15s\" },\n btnPrimary: { background: \"#6366f1\", color: \"#fff\" },\n btnGreen: { background: \"#f0fdf4\", color: \"#16a34a\", border: \"1px solid #bbf7d0\" },\n btnRed: { background: \"#fef2f2\", color: \"#ef4444\", border: \"1px solid #fecaca\" },\n keyRow: { display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", padding: \"12px 16px\", borderRadius: 10, background: \"#f9fafb\", marginBottom: 8, border: \"1px solid #f3f4f6\" },\n badge: (active) => ({ display: \"inline-block\", padding: \"3px 10px\", borderRadius: 20, fontSize: 11, fontWeight: 700, background: active ? \"#d1fae5\" : \"#fee2e2\", color: active ? \"#065f46\" : \"#991b1b\" }),\n };\n\n if (loading) return <div style={{ padding: 40, color: \"#9ca3af\" }}>Loading...</div>;\n\n return (\n <div style={s.page}>\n <h1 style={s.title}>API Settings</h1>\n <p style={s.sub}>Configure LLM, voice, and API keys for the AI agent</p>\n\n {(msg || err) && (\n <div style={{ padding: \"12px 18px\", background: msg ? \"#f0fdf4\" : \"#fef2f2\", color: msg ? \"#065f46\" : \"#dc2626\", borderRadius: 12, marginBottom: 20, fontSize: 13, fontWeight: 500 }}>\n {msg || err}\n </div>\n )}\n\n {/* LLM Provider */}\n <div style={s.card}>\n <div style={s.cardTitle}>\n <span style={s.sectionIcon(\"#e0e7ff\")}>🧠</span> LLM Provider\n </div>\n <div style={s.grid}>\n <div>\n <label style={s.label}>Provider (Search)</label>\n <SearchInput\n value={LLM_PROVIDERS.find(p => p.key === prov)?.label || prov}\n onChange={v => {\n const match = LLM_PROVIDERS.find(p => p.label.toLowerCase() === v.toLowerCase() || p.key === v.toLowerCase());\n if (match) setVal(\"llm_provider\", match.key);\n else setVal(\"llm_provider\", v);\n }}\n items={LLM_PROVIDERS.map(p => ({ name: p.key, provider: p.key, label: p.label }))}\n providerFilter={null}\n placeholder=\"Search LLM provider...\"\n />\n </div>\n <div>\n <label style={s.label}>Model (Search)</label>\n <SearchInput\n value={settings.llm_model || \"\"}\n onChange={v => setVal(\"llm_model\", v)}\n items={ALL_MODELS}\n providerFilter={prov !== \"custom\" ? prov : null}\n placeholder=\"Type to search models...\"\n />\n </div>\n {prov !== \"custom\" && (\n <div style={{ gridColumn: \"1 / -1\" }}>\n <label style={s.label}>{LLM_PROVIDERS.find(p => p.key === prov)?.label || prov} API Key</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <input style={{ ...s.input, flex: 1 }} type=\"password\" value={settings[`${prov}_api_key`] || \"\"} onChange={e => setVal(`${prov}_api_key`, e.target.value)} placeholder=\"sk-...\" />\n <button onClick={async () => { await save(true); load(); }} disabled={saving} style={{ ...s.btn, padding: \"8px 16px\", fontSize: 12, background: \"#d1fae5\", color: \"#065f46\", flexShrink: 0 }}>Save Key</button>\n </div>\n </div>\n )}\n {prov === \"custom\" && (\n <div style={{ gridColumn: \"1 / -1\", display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 16 }}>\n <div>\n <label style={s.label}>Custom API Key</label>\n <input style={s.input} type=\"password\" value={settings.custom_llm_api_key || \"\"} onChange={e => setVal(\"custom_llm_api_key\", e.target.value)} placeholder=\"sk-...\" />\n </div>\n <div>\n <label style={s.label}>Custom Base URL</label>\n <input style={s.input} value={settings.custom_llm_base_url || \"\"} onChange={e => setVal(\"custom_llm_base_url\", e.target.value)} placeholder=\"https://api.together.xyz/v1\" />\n </div>\n </div>\n )}\n </div>\n </div>\n\n {/* TTS / Voice */}\n <div style={s.card}>\n <div style={s.cardTitle}>\n <span style={s.sectionIcon(\"#fef3c7\")}>🔊</span> Voice (TTS)\n </div>\n <div style={s.grid}>\n <div>\n <label style={s.label}>TTS Provider (Search)</label>\n <SearchInput\n value={TTS_PROVIDERS.find(p => p.key === (settings.tts_provider || \"polly\"))?.label || settings.tts_provider || \"Amazon Polly (Free)\"}\n onChange={v => {\n const match = TTS_PROVIDERS.find(p => p.label.toLowerCase() === v.toLowerCase() || p.key === v.toLowerCase());\n if (match) setVal(\"tts_provider\", match.key);\n else setVal(\"tts_provider\", v);\n }}\n items={TTS_PROVIDERS.map(p => ({ name: p.key, provider: p.key, label: p.label }))}\n providerFilter={null}\n placeholder=\"Search TTS provider...\"\n />\n </div>\n <div>\n <label style={s.label}>Voice (Search)</label>\n <SearchInput\n value={settings.tts_voice || \"\"}\n onChange={v => setVal(\"tts_voice\", v)}\n items={ALL_VOICES}\n providerFilter={(settings.tts_provider || \"polly\") !== \"custom\" ? (settings.tts_provider || \"polly\") : null}\n placeholder=\"Type to search voices...\"\n />\n </div>\n {(settings.tts_provider || \"polly\") !== \"polly\" && (\n <div style={{ gridColumn: \"1 / -1\" }}>\n <label style={s.label}>{TTS_PROVIDERS.find(p => p.key === (settings.tts_provider || \"polly\"))?.label || \"TTS\"} API Key</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n {(() => {\n const ttsProv = settings.tts_provider || \"polly\";\n const keyMap = { elevenlabs: \"elevenlabs_api_key\", openai_tts: \"openai_api_key\", deepgram_tts: \"deepgram_api_key\", playht: \"playht_api_key\", google_tts: \"google_tts_api_key\", azure_tts: \"azure_tts_api_key\", custom: \"custom_tts_api_key\" };\n const key = keyMap[ttsProv] || \"custom_tts_api_key\";\n return (\n <>\n <input style={{ ...s.input, flex: 1 }} type=\"password\" value={settings[key] || \"\"} onChange={e => setVal(key, e.target.value)} placeholder=\"API key...\" />\n <button onClick={async () => { await save(true); load(); }} disabled={saving} style={{ ...s.btn, padding: \"8px 16px\", fontSize: 12, background: \"#d1fae5\", color: \"#065f46\", flexShrink: 0 }}>Save</button>\n </>\n );\n })()}\n </div>\n </div>\n )}\n </div>\n </div>\n\n {/* Active API Keys */}\n <div style={s.card}>\n <div style={s.cardTitle}>\n <span style={s.sectionIcon(\"#d1fae5\")}>🔑</span> Active API Keys\n <span style={{ fontSize: 12, fontWeight: 400, color: \"#9ca3af\", marginLeft: 4 }}>({activeKeys.length} configured)</span>\n </div>\n {activeKeys.length === 0 ? (\n <div style={{ color: \"#d1d5db\", fontSize: 13, padding: \"16px 0\" }}>No API keys configured.</div>\n ) : (\n activeKeys.map(def => {\n const val = settings[def.key] || \"\";\n const last4 = `...${val.slice(-4)}`;\n return (\n <div key={def.key} style={s.keyRow}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 12 }}>\n <span style={{ fontWeight: 600, fontSize: 14, color: \"#111827\" }}>{def.label}</span>\n <span style={s.badge(true)}>Active</span>\n <span style={{ fontSize: 12, color: \"#6b7280\", fontFamily: \"monospace\" }}>{last4}</span>\n </div>\n <div style={{ display: \"flex\", gap: 6 }}>\n <button onClick={() => editKey(def.key)} style={{ ...s.btn, ...s.btnGreen, padding: \"5px 14px\", fontSize: 11 }}>Edit</button>\n <button onClick={() => deleteKey(def.key)} style={{ ...s.btn, ...s.btnRed, padding: \"5px 14px\", fontSize: 11 }}>Delete</button>\n </div>\n </div>\n );\n })\n )}\n </div>\n\n {/* Add Keys */}\n <div style={s.card}>\n <div style={s.cardTitle}>\n <span style={s.sectionIcon(\"#ede9fe\")}>➕</span> Add API Key\n </div>\n <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: \"10px 20px\" }}>\n {API_KEY_DEFS.filter(d => !activeKeys.find(a => a.key === d.key)).map(def => (\n <div key={def.key} style={{ display: \"flex\", gap: 8, alignItems: \"center\" }}>\n <input style={{ ...s.input, flex: 1 }} type=\"password\" placeholder={`${def.label} key...`} value={settings[def.key] || \"\"} onChange={e => setVal(def.key, e.target.value)} />\n <button onClick={async () => { await save(true); load(); }} disabled={saving || !settings[def.key]} style={{ ...s.btn, padding: \"8px 14px\", fontSize: 11, background: settings[def.key] ? \"#f0fdf4\" : \"#f3f4f6\", color: settings[def.key] ? \"#16a34a\" : \"#9ca3af\", flexShrink: 0 }}>Add</button>\n </div>\n ))}\n </div>\n </div>\n\n <button onClick={() => save(false)} disabled={saving} style={{ ...s.btn, ...s.btnPrimary }}>\n {saving ? \"Saving...\" : \"Save All Settings\"}\n </button>\n\n {editingKey && (\n <div style={{ position: \"fixed\", inset: 0, background: \"rgba(0,0,0,0.4)\", display: \"flex\", justifyContent: \"center\", alignItems: \"center\", zIndex: 100 }} onClick={e => { if (e.target === e.currentTarget) setEditingKey(null); }}>\n <div style={{ background: \"#fff\", borderRadius: 16, padding: 32, width: \"100%\", maxWidth: 440, boxShadow: \"0 20px 60px rgba(0,0,0,0.15)\" }}>\n <h3 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16 }}>Edit {API_KEY_DEFS.find(d => d.key === editingKey)?.label || editingKey}</h3>\n <input style={s.input} type=\"password\" value={editValue} onChange={e => setEditValue(e.target.value)} placeholder=\"Enter new API key\" />\n <div style={{ display: \"flex\", gap: 10, justifyContent: \"flex-end\", marginTop: 16 }}>\n <button onClick={() => setEditingKey(null)} style={{ ...s.btn, background: \"#f3f4f6\", color: \"#374151\" }}>Cancel</button>\n <button onClick={saveKey} style={{ ...s.btn, ...s.btnPrimary }}>Save Key</button>\n </div>\n </div>\n </div>\n )}\n </div>\n );\n}\n"}}, "saas-backend/.env.example": {"aa55182da05c2078587a920716769716698f1f94": {"t": 1785747116.8103685, "n": 1055, "h": "85d6c8f1ded4d246", "c": "# Server\nPORT=3000\nMONGODB_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/saas-appointment-agent\nJWT_SECRET=your_super_secret_jwt_key_change_this\nSTREAM_HOST=va.crossdigi.com\nPUBLIC_HOST=va.crossdigi.com\n\n# Admin Seed\nADMIN_SEED_EMAIL=admin@yourapp.com\nADMIN_SEED_PASSWORD=admin123change\n\n# Twilio\nTWILIO_ACCOUNT_SID=your_twilio_sid\nTWILIO_AUTH_TOKEN=your_twilio_auth_token\nTWILIO_PHONE_NUMBER=+13362217315\n\n# LLM API Keys (configure via Admin Panel)\nDEEPSEEK_API_KEY=sk-your-deepseek-key\nDEEPSEEK_MODEL=deepseek-reasoner\nOPENAI_API_KEY=sk-your-openai-key\nGROQ_API_KEY=gsk-your-groq-key\nANTHROPIC_API_KEY=sk-ant-your-anthropic-key\nGEMINI_API_KEY=AIza-your-gemini-key\nMISTRAL_API_KEY=your-mistral-key\nPERPLEXITY_API_KEY=pplx-your-perplexity-key\nXAI_API_KEY=xai-your-xai-key\n\n# STT / Voice\nDEEPGRAM_API_KEY=your-deepgram-key\nGOOGLE_STT_KEY=AIza-your-google-stt-key\nELEVENLABS_API_KEY=your-elevenlabs-key\n\n# Email\nRESEND_API_KEY=re_your_resend_key\nOWNER_EMAIL=admin@yourapp.com\nEMAIL_USER=your-email@gmail.com\nEMAIL_PASS=your-gmail-app-password\n"}}}}