Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
142 lines
6.6 KiB
JavaScript
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>
|
|
`;
|
|
}
|