1 line
30 KiB
JSON
1 line
30 KiB
JSON
{"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"}}}} |