SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
<nav class="settings-tabs">
|
||||
<a href="/settings?tab=email" class="settings-tab {% if active_tab == 'email' %}active{% endif %}">Email</a>
|
||||
<a href="/settings?tab=social" class="settings-tab {% if active_tab == 'social' %}active{% endif %}">Social API's</a>
|
||||
<a href="/settings?tab=ai" class="settings-tab {% if active_tab == 'ai' %}active{% endif %}">AI / LLM</a>
|
||||
<a href="/settings?tab=permissions" class="settings-tab {% if active_tab == 'permissions' %}active{% endif %}">Rechten Herman</a>
|
||||
<a href="/settings?tab=general" class="settings-tab {% if active_tab == 'general' %}active{% endif %}">General</a>
|
||||
</nav>
|
||||
@@ -113,6 +114,109 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif active_tab == 'ai' %}
|
||||
<section class="panel settings-panel" x-data="llmSettingsPage()" x-init="load()">
|
||||
<div class="settings-panel-head">
|
||||
<div>
|
||||
<h2>AI / LLM providers</h2>
|
||||
<p class="page-tip">Koppel cloud API's (DeepSeek, Gemini, Groq, OpenRouter) naast lokale Ollama. Standaard provider wordt gebruikt in chat, Herman en documenten.</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" @click="openForm()">+ Nieuwe provider</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-active-banner" x-show="defaultProvider" x-cloak>
|
||||
<span class="badge badge-positive">Standaard</span>
|
||||
<span x-text="defaultProvider ? defaultProvider.label + ' · ' + defaultProvider.model : ''"></span>
|
||||
</div>
|
||||
|
||||
<div class="llm-preset-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:0.65rem;margin-bottom:1.25rem">
|
||||
<template x-for="pr in presets" :key="pr.id">
|
||||
<div class="perm-card" style="cursor:pointer" @click="quickAdd(pr)">
|
||||
<strong x-text="pr.label"></strong>
|
||||
<p class="page-tip" style="margin:0.35rem 0" x-text="pr.hint"></p>
|
||||
<small x-text="(pr.models||[]).slice(0,2).join(', ')"></small>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="email-account-list">
|
||||
<template x-for="p in providers" :key="p.id">
|
||||
<article class="email-account-card" :class="{ 'is-active': p.is_default }">
|
||||
<div class="email-account-card-head">
|
||||
<div>
|
||||
<strong x-text="p.label"></strong>
|
||||
<span class="muted" x-text="p.preset_label + ' · ' + p.model"></span>
|
||||
</div>
|
||||
<span class="badge" :class="p.is_default ? 'badge-positive' : (p.is_active ? 'badge-neutral' : 'badge-muted')"
|
||||
x-text="p.is_default ? 'Standaard' : (p.is_active ? 'Actief' : 'Inactief')"></span>
|
||||
</div>
|
||||
<div class="email-account-meta">
|
||||
<span x-text="p.api_base_url || 'Ollama lokaal'"></span>
|
||||
<span x-text="p.api_key_set ? '🔒 API key opgeslagen' : (p.needs_key ? '⚠️ geen API key' : 'Geen key nodig')"></span>
|
||||
<span x-show="p.last_test_status" x-text="'Test: ' + p.last_test_status"></span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="edit(p)">Bewerken</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" @click="activate(p.id)" x-show="!p.is_default">Maak standaard</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="testProvider(p.id)">Test</button>
|
||||
<button type="button" class="btn btn-reject btn-sm" @click="remove(p.id)" x-show="p.provider_type !== 'ollama' || providers.length > 1">Verwijder</button>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
<p class="empty-state" x-show="!providers.length && !loading">Nog geen LLM provider — voeg DeepSeek, Gemini of Groq toe.</p>
|
||||
</div>
|
||||
|
||||
<div class="modal" :class="{ open: formOpen }" @click.self="formOpen=false">
|
||||
<div class="modal-card settings-modal">
|
||||
<h3 x-text="form.id ? 'Provider bewerken' : 'Nieuwe LLM provider'"></h3>
|
||||
|
||||
<label class="form-label">Label
|
||||
<input class="form-input" x-model="form.label" placeholder="bv. DeepSeek productie" />
|
||||
</label>
|
||||
|
||||
<label class="form-label">Type
|
||||
<select class="form-input" x-model="form.provider_type" @change="applyPreset()">
|
||||
<template x-for="pr in presets" :key="'fp'+pr.id">
|
||||
<option :value="pr.id" x-text="pr.label"></option>
|
||||
</template>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="form-label">Model
|
||||
<input class="form-input" x-model="form.model" list="llm-model-list" placeholder="deepseek-chat" />
|
||||
<datalist id="llm-model-list">
|
||||
<template x-for="m in currentPresetModels()" :key="m">
|
||||
<option :value="m"></option>
|
||||
</template>
|
||||
</datalist>
|
||||
</label>
|
||||
|
||||
<label class="form-label" x-show="formNeedsKey()">API base URL
|
||||
<input class="form-input" x-model="form.api_base_url" placeholder="https://api.deepseek.com/v1" />
|
||||
</label>
|
||||
|
||||
<label class="form-label" x-show="formNeedsKey()">API key
|
||||
<input class="form-input" type="password" x-model="form.api_key"
|
||||
:placeholder="form.api_key_set ? '•••••••• (laat leeg om te behouden)' : 'sk-...'" />
|
||||
</label>
|
||||
|
||||
<p class="page-tip" x-show="presetHint()" x-text="presetHint()"></p>
|
||||
<p class="page-tip" x-show="presetSignup()" >
|
||||
<a :href="presetSignup()" target="_blank" rel="noopener">→ Gratis API key aanmaken</a>
|
||||
</p>
|
||||
|
||||
<label class="form-check">
|
||||
<input type="checkbox" x-model="form.is_default" /> Instellen als standaard provider
|
||||
</label>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" @click="save()" :disabled="saving">Opslaan</button>
|
||||
<button type="button" class="btn btn-secondary" @click="testForm()" :disabled="saving">Test</button>
|
||||
<button type="button" class="btn btn-secondary" @click="formOpen=false">Annuleren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% elif active_tab == 'permissions' %}
|
||||
<section class="panel settings-panel hm-neo" x-data="permissionsPage()" x-init="loadPerms()">
|
||||
<div class="settings-panel-head">
|
||||
@@ -428,5 +532,145 @@ function defaultForm() {
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% elif active_tab == 'ai' %}
|
||||
<script>
|
||||
function llmDefaultForm() {
|
||||
return {
|
||||
id: null, label: '', provider_type: 'deepseek', api_base_url: '', api_key: '',
|
||||
model: 'deepseek-chat', is_active: true, is_default: false, api_key_set: false,
|
||||
};
|
||||
}
|
||||
|
||||
function llmSettingsPage() {
|
||||
return {
|
||||
providers: [],
|
||||
presets: [],
|
||||
defaultProvider: null,
|
||||
loading: true,
|
||||
formOpen: false,
|
||||
saving: false,
|
||||
form: llmDefaultForm(),
|
||||
|
||||
async load() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const r = await Cockpit.api('/api/settings/llm');
|
||||
this.providers = r.providers || [];
|
||||
this.presets = r.presets || [];
|
||||
this.defaultProvider = r.default || this.providers.find(p => p.is_default) || null;
|
||||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
openForm() {
|
||||
this.form = llmDefaultForm();
|
||||
this.applyPreset();
|
||||
this.formOpen = true;
|
||||
},
|
||||
|
||||
quickAdd(pr) {
|
||||
this.form = llmDefaultForm();
|
||||
this.form.provider_type = pr.id;
|
||||
this.form.label = pr.label;
|
||||
this.applyPreset();
|
||||
this.formOpen = true;
|
||||
},
|
||||
|
||||
edit(p) {
|
||||
this.form = Object.assign(llmDefaultForm(), p, { api_key: '' });
|
||||
this.formOpen = true;
|
||||
},
|
||||
|
||||
applyPreset() {
|
||||
const pr = this.presets.find(x => x.id === this.form.provider_type) || {};
|
||||
if (pr.api_base_url) this.form.api_base_url = pr.api_base_url;
|
||||
if ((pr.models || []).length && !this.form.id) this.form.model = pr.models[0];
|
||||
if (!this.form.label) this.form.label = pr.label || this.form.provider_type;
|
||||
},
|
||||
|
||||
currentPresetModels() {
|
||||
const pr = this.presets.find(x => x.id === this.form.provider_type);
|
||||
return (pr && pr.models) ? pr.models : [];
|
||||
},
|
||||
|
||||
formNeedsKey() {
|
||||
const pr = this.presets.find(x => x.id === this.form.provider_type);
|
||||
return pr ? !!pr.needs_key : true;
|
||||
},
|
||||
|
||||
presetHint() {
|
||||
const pr = this.presets.find(x => x.id === this.form.provider_type);
|
||||
return pr ? (pr.hint || '') : '';
|
||||
},
|
||||
|
||||
presetSignup() {
|
||||
const pr = this.presets.find(x => x.id === this.form.provider_type);
|
||||
return pr ? (pr.signup_url || '') : '';
|
||||
},
|
||||
|
||||
bodyFromForm() {
|
||||
const b = {
|
||||
label: this.form.label,
|
||||
provider_type: this.form.provider_type,
|
||||
api_base_url: this.form.api_base_url,
|
||||
model: this.form.model,
|
||||
is_active: true,
|
||||
is_default: this.form.is_default,
|
||||
extra_config: {},
|
||||
};
|
||||
if (this.form.api_key) b.api_key = this.form.api_key;
|
||||
return b;
|
||||
},
|
||||
|
||||
async save() {
|
||||
this.saving = true;
|
||||
try {
|
||||
const body = this.bodyFromForm();
|
||||
if (this.form.id) {
|
||||
await Cockpit.api('/api/settings/llm/' + this.form.id, { method: 'PUT', body: JSON.stringify(body) });
|
||||
} else {
|
||||
await Cockpit.api('/api/settings/llm', { method: 'POST', body: JSON.stringify(body) });
|
||||
}
|
||||
Cockpit.toast('LLM provider opgeslagen', 'success');
|
||||
this.formOpen = false;
|
||||
await this.load();
|
||||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||||
this.saving = false;
|
||||
},
|
||||
|
||||
async testForm() {
|
||||
if (!this.form.id) {
|
||||
Cockpit.toast('Sla eerst op, daarna testen', 'info');
|
||||
return;
|
||||
}
|
||||
await this.testProvider(this.form.id);
|
||||
},
|
||||
|
||||
async testProvider(id) {
|
||||
try {
|
||||
const r = await Cockpit.api('/api/settings/llm/' + id + '/test', { method: 'POST' });
|
||||
Cockpit.toast(r.message || 'OK', 'success');
|
||||
await this.load();
|
||||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||||
},
|
||||
|
||||
async activate(id) {
|
||||
try {
|
||||
await Cockpit.api('/api/settings/llm/' + id + '/activate', { method: 'POST' });
|
||||
Cockpit.toast('Standaard LLM bijgewerkt', 'success');
|
||||
await this.load();
|
||||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||||
},
|
||||
|
||||
async remove(id) {
|
||||
if (!confirm('Deze LLM provider verwijderen?')) return;
|
||||
try {
|
||||
await Cockpit.api('/api/settings/llm/' + id, { method: 'DELETE' });
|
||||
await this.load();
|
||||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user