import React, { useState, useEffect } from "react"; import { Plus, Pencil, Trash2, CheckCircle, XCircle, Zap } from "lucide-react"; import { Card, CardHeader, CardTitle, CardContent, Button, Input, Label, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Separator, } from "@/components/ui"; import { useSettingsStore } from "@/stores/settingsStore"; import { testProviderConnectionCmd, saveAiProviderCmd, loadAiProvidersCmd, deleteAiProviderCmd, type ProviderConfig, } from "@/lib/tauriCommands"; export const CUSTOM_REST_MODELS = [ "ChatGPT4o", "ChatGPT4o-mini", "ChatGPT-o3-mini", "Gemini-2_0-Flash-001", "Gemini-2_5-Flash", "Claude-Sonnet-3_7", "Openai-gpt-4_1-mini", "Openai-o4-mini", "Claude-Sonnet-4", "ChatGPT-o3-pro", "OpenAI-ChatGPT-4_1", "OpenAI-GPT-4_1-Nano", "ChatGPT-5", "VertexGemini", "ChatGPT-5_1", "ChatGPT-5_1-chat", "ChatGPT-5_2-Chat", "Gemini-3_Pro-Preview", "Gemini-3_1-flash-lite-preview", ] as const; export const CUSTOM_MODEL_OPTION = "__custom_model__"; export const CUSTOM_REST_FORMAT = "custom_rest"; const emptyProvider: ProviderConfig = { name: "", provider_type: "openai", api_url: "", api_key: "", model: "", max_tokens: 4096, temperature: 0.7, custom_endpoint_path: undefined, custom_auth_header: undefined, custom_auth_prefix: undefined, api_format: undefined, session_id: undefined, user_id: undefined, }; export default function AIProviders() { const { ai_providers, active_provider, addProvider, updateProvider, removeProvider, setActiveProvider, setProviders, } = useSettingsStore(); const [editIndex, setEditIndex] = useState(null); const [isAdding, setIsAdding] = useState(false); const [form, setForm] = useState({ ...emptyProvider }); const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); const [isTesting, setIsTesting] = useState(false); const [isCustomModel, setIsCustomModel] = useState(false); const [customModelInput, setCustomModelInput] = useState(""); // Load providers from database on mount // Note: Auto-testing of active provider is handled in App.tsx on startup useEffect(() => { const loadProviders = async () => { try { const providers = await loadAiProvidersCmd(); setProviders(providers); } catch (err) { console.error("Failed to load AI providers:", err); } }; loadProviders(); }, [setProviders]); const startAdd = () => { setForm({ ...emptyProvider }); setEditIndex(null); setIsAdding(true); setTestResult(null); setIsCustomModel(false); setCustomModelInput(""); }; const startEdit = (index: number) => { const provider = ai_providers[index]; const apiFormat = normalizeApiFormat(provider.api_format); const nextForm = { ...provider, api_format: apiFormat }; setForm(nextForm); setEditIndex(index); setIsAdding(true); setTestResult(null); const isCustomRestProvider = nextForm.provider_type === "custom" && apiFormat === CUSTOM_REST_FORMAT; const knownModel = CUSTOM_REST_MODELS.includes(nextForm.model as (typeof CUSTOM_REST_MODELS)[number]); if (isCustomRestProvider && !knownModel) { setIsCustomModel(true); setCustomModelInput(nextForm.model); } else { setIsCustomModel(false); setCustomModelInput(""); } }; const handleSave = async () => { if (!form.name || !form.api_url || !form.model) return; try { // Save to database await saveAiProviderCmd(form); // Update local state if (editIndex != null) { updateProvider(editIndex, form); } else { addProvider(form); } setIsAdding(false); setEditIndex(null); setForm({ ...emptyProvider }); } catch (err) { console.error("Failed to save provider:", err); setTestResult({ success: false, message: `Failed to save: ${err}` }); } }; const handleCancel = () => { setIsAdding(false); setEditIndex(null); setForm({ ...emptyProvider }); setTestResult(null); }; const handleRemove = async (index: number) => { const provider = ai_providers[index]; try { await deleteAiProviderCmd(provider.name); removeProvider(index); } catch (err) { console.error("Failed to delete provider:", err); } }; const handleTest = async () => { setIsTesting(true); setTestResult(null); try { const response = await testProviderConnectionCmd(form); setTestResult({ success: true, message: `OK: ${response.content.slice(0, 100)}` }); } catch (err) { setTestResult({ success: false, message: String(err) }); } finally { setIsTesting(false); } }; const maskApiKey = (key?: string) => { if (!key) return "Not set"; if (key.length <= 8) return "****"; return key.slice(0, 4) + "..." + key.slice(-4); }; return (

AI Providers

Configure AI model providers for triage and document generation.

{!isAdding && ( )}
{/* Provider list */} {ai_providers.length === 0 && !isAdding && (

No providers configured yet.

)} {ai_providers.map((provider, idx) => (
{provider.name} {provider.provider_type} {active_provider === provider.name && ( Active )}

{provider.api_url} | Model: {provider.model} | Key: {maskApiKey(provider.api_key)}

{active_provider !== provider.name && ( )}
))} {/* Add/Edit form */} {isAdding && ( {editIndex != null ? "Edit Provider" : "Add Provider"}
setForm({ ...form, name: e.target.value })} placeholder="My OpenAI" />
setForm({ ...form, api_url: e.target.value })} placeholder="https://api.openai.com/v1" />
setForm({ ...form, api_key: e.target.value })} placeholder="sk-..." />
{!(form.provider_type === "custom" && normalizeApiFormat(form.api_format) === CUSTOM_REST_FORMAT) && (
setForm({ ...form, model: e.target.value })} placeholder="gpt-4o" />
)}
setForm({ ...form, max_tokens: Number(e.target.value) })} />
setForm({ ...form, temperature: Number(e.target.value) })} />
{/* Custom provider format options */} {form.provider_type === "custom" && ( <>

Select the API format. Custom REST uses a non-OpenAI request/response structure.

setForm({ ...form, custom_endpoint_path: e.target.value }) } placeholder="/chat/completions" />

Path appended to API URL. Leave empty if URL includes full path.

setForm({ ...form, custom_auth_header: e.target.value }) } placeholder="Authorization" />

Header name for authentication (e.g., "Authorization" or "x-api-key")

setForm({ ...form, custom_auth_prefix: e.target.value })} placeholder="Bearer " />

Prefix added before API key (e.g., "Bearer " for OpenAI, empty for Custom REST)

{/* Custom REST specific: User ID field */} {normalizeApiFormat(form.api_format) === CUSTOM_REST_FORMAT && (
setForm({ ...form, user_id: e.target.value })} placeholder="user@example.com" />

Optional: Email address for usage tracking. If omitted, costs are attributed to the API key owner.

)} {/* Custom REST specific: model dropdown with custom option */} {normalizeApiFormat(form.api_format) === CUSTOM_REST_FORMAT && (
{isCustomModel && ( { const value = e.target.value; setCustomModelInput(value); setForm({ ...form, model: value }); }} placeholder="Enter custom model ID" /> )}
)}
)} {/* Test result */} {testResult && (
{testResult.success ? ( ) : ( )} {testResult.message}
)}
)}
); }