This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/surfaces/admin/providers.js
gobha 3a4afea7f2 Changeset 0.37.18 (#230)
Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
2026-03-24 19:55:14 +00:00

142 lines
6.6 KiB
JavaScript

/**
* Admin > AI > Providers
*/
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
export default function ProvidersSection() {
const [configs, setConfigs] = useState([]);
const [providerTypes, setProviderTypes] = useState([]);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(null); // null | 'new' | config object
const load = useCallback(async () => {
try {
const [c, t] = await Promise.all([
sw.api.admin.configs.list(),
sw.api.admin.providers.types(),
]);
setConfigs(c || []);
setProviderTypes(t || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, []);
async function saveProvider(e) {
e.preventDefault();
const form = e.target;
const data = {
name: form.pname.value.trim(),
provider: form.provider.value,
endpoint: form.endpoint.value.trim(),
api_key: form.api_key.value || undefined,
is_private: form.is_private.checked,
};
try {
if (editing === 'new') {
await sw.api.admin.configs.create(data);
sw.toast('Provider created', 'success');
} else {
await sw.api.admin.configs.update(editing.id, data);
sw.toast('Provider updated', 'success');
}
setEditing(null);
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
async function syncModels(id) {
try {
const result = await sw.api.admin.models.fetch();
sw.toast(`Synced: ${result.added || 0} added, ${result.updated || 0} updated`, 'success');
} catch (e) { sw.toast(e.message, 'error'); }
}
async function testProvider(cfg) {
sw.toast(`Testing ${cfg.name}\u2026`, 'info');
try {
const result = await sw.api.admin.models.fetch();
sw.toast(`${cfg.name}: connection OK`, 'success');
} catch (e) {
sw.toast(`${cfg.name}: connection failed \u2014 ${e.message}`, 'error');
}
}
async function deleteProvider(id) {
const ok = await sw.confirm('Delete this provider config?', true);
if (!ok) return;
try {
await sw.api.admin.configs.del(id);
sw.toast('Provider deleted', 'success');
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
return html`
<div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<div style="flex:1"></div>
<button class="btn-md btn-primary" onClick=${() => setEditing('new')}>+ Add</button>
</div>
${editing && html`
<form class="admin-inline-form" style="margin-bottom:16px;padding:12px;border:1px solid var(--border);border-radius:var(--radius);" onSubmit=${saveProvider}>
<h4 style="margin:0 0 12px;">${editing === 'new' ? 'Add Provider' : 'Edit Provider'}</h4>
<div class="form-row">
<div class="form-group"><label>Name</label>
<input name="pname" placeholder="My OpenAI" value=${editing !== 'new' ? editing.name || '' : ''} />
</div>
<div class="form-group"><label>Type</label>
<select name="provider">
${providerTypes.map(t => html`<option key=${t.id} value=${t.id} selected=${editing !== 'new' && editing.provider === t.id}>${t.display_name || t.name}</option>`)}
</select>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex:2;"><label>Endpoint</label>
<input name="endpoint" placeholder="https://api.openai.com/v1" value=${editing !== 'new' ? editing.endpoint || '' : ''} />
</div>
<div class="form-group" style="flex:1;"><label>API Key</label>
<input name="api_key" type="password" placeholder="${editing !== 'new' ? '(unchanged)' : 'sk-...'}" autocomplete="off" />
</div>
</div>
<label class="toggle-label" style="margin:8px 0;">
<input type="checkbox" name="is_private" checked=${editing !== 'new' && editing.is_private} />
<span class="toggle-track"></span>
<span>Private (admin personas only)</span>
</label>
<div class="form-row" style="margin-top:12px;gap:8px;">
<button type="submit" class="btn-small btn-primary">${editing === 'new' ? 'Create' : 'Save'}</button>
<button type="button" class="btn-small" onClick=${() => setEditing(null)}>Cancel</button>
</div>
</form>
`}
<div class="provider-cards">
${configs.length === 0 && html`<div class="empty-hint">No provider configs</div>`}
${configs.map(c => html`
<div class="provider-card" key=${c.id}>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<strong>${c.name}</strong>
<span class="badge">${c.provider}</span>
${c.is_private && html`<span class="badge badge-inactive">private</span>`}
${c.has_key ? html`<span class="badge badge-active">key set</span>` : html`<span class="badge badge-pending">no key</span>`}
</div>
<div class="text-muted" style="font-size:12px;margin-bottom:8px;word-break:break-all;">${c.endpoint || '(default)'}</div>
<div style="display:flex;gap:6px;">
<button class="btn-small" onClick=${() => testProvider(c)}>Test</button>
<button class="btn-small" onClick=${() => setEditing(c)}>Edit</button>
<button class="btn-small" onClick=${() => syncModels(c.id)}>Sync Models</button>
<button class="btn-small btn-danger" onClick=${() => deleteProvider(c.id)}>Delete</button>
</div>
</div>
`)}
</div>
</div>
`;
}