/** * ProvidersSection — BYOK provider CRUD (user-scoped) */ const { html } = window; const { useState, useEffect, useCallback } = hooks; function esc(s) { return s == null ? '' : String(s); } export function ProvidersSection() { const [providers, setProviders] = useState(null); const [showForm, setShowForm] = useState(false); const [editId, setEditId] = useState(null); const [form, setForm] = useState({ name: '', provider: 'openai', endpoint: '', api_key: '', model_default: '' }); const [saving, setSaving] = useState(false); const load = useCallback(async () => { try { const resp = await sw.api.providers.list(); setProviders(resp?.configs || resp?.data || resp || []); } catch (e) { setProviders([]); } }, []); useEffect(() => { load(); }, [load]); const openAdd = useCallback(() => { setEditId(null); setForm({ name: '', provider: 'openai', endpoint: '', api_key: '', model_default: '' }); setShowForm(true); }, []); const openEdit = useCallback(async (prov) => { try { const cfg = await sw.api.providers.get(prov.id); setEditId(cfg.id); setForm({ name: cfg.name || '', provider: cfg.provider || 'openai', endpoint: cfg.endpoint || '', api_key: '', model_default: cfg.model_default || '', }); setShowForm(true); } catch (e) { sw.emit('toast', { message: e.message, variant: 'error' }); } }, []); const cancel = useCallback(() => { setShowForm(false); setEditId(null); }, []); const save = useCallback(async () => { if (!form.name || !form.endpoint) { sw.emit('toast', { message: 'Name and endpoint are required', variant: 'error' }); return; } if (!editId && !form.api_key) { sw.emit('toast', { message: 'API key is required', variant: 'error' }); return; } setSaving(true); try { if (editId) { const patch = { name: form.name, provider: form.provider, endpoint: form.endpoint, model_default: form.model_default }; if (form.api_key) patch.api_key = form.api_key; await sw.api.providers.update(editId, patch); sw.emit('toast', { message: 'Provider updated', variant: 'success' }); } else { const body = { name: form.name, provider: form.provider, endpoint: form.endpoint, api_key: form.api_key, model_default: form.model_default }; const result = await sw.api.providers.create(body); const count = result?.models_fetched || 0; sw.emit('toast', { message: result?.warning || `Provider added \u2014 ${count} model${count !== 1 ? 's' : ''} synced`, variant: result?.warning ? 'warning' : 'success', }); } setShowForm(false); setEditId(null); load(); } catch (e) { sw.emit('toast', { message: e.message, variant: 'error' }); } finally { setSaving(false); } }, [form, editId, load]); const del = useCallback(async (prov) => { const ok = await sw.confirm(`Remove provider "${prov.name}"?`); if (!ok) return; try { await sw.api.providers.del(prov.id); sw.emit('toast', { message: 'Provider removed', variant: 'success' }); load(); } catch (e) { sw.emit('toast', { message: e.message, variant: 'error' }); } }, [load]); const sync = useCallback(async (prov) => { sw.emit('toast', { message: `Fetching models for ${prov.name}\u2026`, variant: 'info' }); try { const result = await sw.api.providers.fetchModels(prov.id); const total = result?.total || 0; sw.emit('toast', { message: `${prov.name}: ${total} model${total !== 1 ? 's' : ''} synced`, variant: 'success' }); } catch (e) { sw.emit('toast', { message: e.message, variant: 'error' }); } }, []); const setField = useCallback((key, val) => { setForm(f => ({ ...f, [key]: val })); }, []); if (providers === null) { return html`
| Provider | Type | Endpoint | |
|---|---|---|---|
| ${esc(p.name)} | ${esc(p.provider)} |