172 lines
6.0 KiB
React
172 lines
6.0 KiB
React
|
|
import { useEffect, useState } from 'react'
|
||
|
|
import { api } from '../api.js'
|
||
|
|
import { useAuth } from '../context/AuthContext.jsx'
|
||
|
|
|
||
|
|
const emptyRole = () => ({
|
||
|
|
role: '',
|
||
|
|
name: '',
|
||
|
|
url: '',
|
||
|
|
model: '',
|
||
|
|
zdr: true,
|
||
|
|
no_train: true,
|
||
|
|
key: ''
|
||
|
|
})
|
||
|
|
|
||
|
|
function statusLabel(role) {
|
||
|
|
if (role.role === 'detect' && !role.url) return 'Muster, kein LLM'
|
||
|
|
if (role.role === 'detect' && role.url && !role.local && role.ready) {
|
||
|
|
return role.remote_plaintext_allowed === false
|
||
|
|
? 'extern konfiguriert, Produktiv blockiert'
|
||
|
|
: 'bereit (nur Development/Test, extern)'
|
||
|
|
}
|
||
|
|
if (role.ready) return 'bereit'
|
||
|
|
if (!role.key_present && !role.local) return 'Key fehlt'
|
||
|
|
if (!role.url) return 'URL fehlt'
|
||
|
|
return 'nicht bereit'
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function AdminProvidersPage() {
|
||
|
|
const { session } = useAuth()
|
||
|
|
const [data, setData] = useState(null)
|
||
|
|
const [forms, setForms] = useState({})
|
||
|
|
const [error, setError] = useState('')
|
||
|
|
const [notice, setNotice] = useState('')
|
||
|
|
const [saving, setSaving] = useState('')
|
||
|
|
|
||
|
|
const load = () =>
|
||
|
|
api('/api/admin/providers', { token: session.token })
|
||
|
|
.then((payload) => {
|
||
|
|
setData(payload)
|
||
|
|
const next = {}
|
||
|
|
for (const role of payload.roles) {
|
||
|
|
next[role.role] = {
|
||
|
|
role: role.role,
|
||
|
|
name: role.name || '',
|
||
|
|
url: role.url || '',
|
||
|
|
model: role.model || '',
|
||
|
|
zdr: Boolean(role.zdr),
|
||
|
|
no_train: Boolean(role.no_train),
|
||
|
|
key: ''
|
||
|
|
}
|
||
|
|
}
|
||
|
|
setForms(next)
|
||
|
|
})
|
||
|
|
.catch((e) => setError(e.message))
|
||
|
|
|
||
|
|
useEffect(() => { load() }, [session.token])
|
||
|
|
|
||
|
|
const update = (role, field, value) => {
|
||
|
|
setForms((prev) => ({ ...prev, [role]: { ...(prev[role] || emptyRole()), [field]: value } }))
|
||
|
|
}
|
||
|
|
|
||
|
|
const save = async (role) => {
|
||
|
|
const form = forms[role]
|
||
|
|
if (!form) return
|
||
|
|
setError('')
|
||
|
|
setNotice('')
|
||
|
|
setSaving(role)
|
||
|
|
try {
|
||
|
|
const body = {
|
||
|
|
name: form.name,
|
||
|
|
url: form.url,
|
||
|
|
model: form.model,
|
||
|
|
zdr: form.zdr,
|
||
|
|
no_train: form.no_train
|
||
|
|
}
|
||
|
|
if (form.key.trim()) body.key = form.key.trim()
|
||
|
|
const payload = await api(`/api/admin/providers/${role}`, {
|
||
|
|
token: session.token,
|
||
|
|
method: 'PUT',
|
||
|
|
body
|
||
|
|
})
|
||
|
|
setData(payload)
|
||
|
|
setForms((prev) => ({ ...prev, [role]: { ...prev[role], key: '' } }))
|
||
|
|
setNotice(form.key.trim() ? `${role}: gespeichert, Key in .env geschrieben.` : `${role}: gespeichert.`)
|
||
|
|
} catch (err) {
|
||
|
|
setError(err.message)
|
||
|
|
} finally {
|
||
|
|
setSaving('')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<section className="card">
|
||
|
|
<h1>Schnittstellen</h1>
|
||
|
|
<p className="muted">
|
||
|
|
Zwei eigene Einträge: Maskierung (Detect-Call) und Sprachmodell (Dialogzug und Journalentwurf).
|
||
|
|
Externes Klartext-Detect ist nur in Development/Test zulässig. Produktiv bleibt Detect lokal oder bei Mustern.
|
||
|
|
Der Secret-Key wird nie angezeigt und nie in der Datenbank gespeichert.
|
||
|
|
</p>
|
||
|
|
{error && <p className="error">{error}</p>}
|
||
|
|
{notice && <p>{notice}</p>}
|
||
|
|
{data && (
|
||
|
|
<>
|
||
|
|
{data.roles.map((role) => {
|
||
|
|
const form = forms[role.role] || emptyRole()
|
||
|
|
return (
|
||
|
|
<form
|
||
|
|
key={role.role}
|
||
|
|
className="stack"
|
||
|
|
onSubmit={(e) => { e.preventDefault(); save(role.role) }}
|
||
|
|
>
|
||
|
|
<h2>{role.title || role.role}</h2>
|
||
|
|
{role.task && <p className="muted">{role.task}</p>}
|
||
|
|
<dl className="meta">
|
||
|
|
<dt>Status</dt>
|
||
|
|
<dd>{statusLabel(role)}</dd>
|
||
|
|
<dt>Key</dt>
|
||
|
|
<dd>{role.key_present ? (role.key_source === 'generate' ? 'über Sprachmodell' : 'gesetzt') : 'fehlt'}</dd>
|
||
|
|
<dt>Modus</dt>
|
||
|
|
<dd>{role.local ? 'lokal' : 'extern'} · {role.mode}</dd>
|
||
|
|
</dl>
|
||
|
|
<label>
|
||
|
|
Name
|
||
|
|
<input value={form.name} onChange={(e) => update(role.role, 'name', e.target.value)} />
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
URL
|
||
|
|
<input
|
||
|
|
value={form.url}
|
||
|
|
onChange={(e) => update(role.role, 'url', e.target.value)}
|
||
|
|
placeholder={role.role === 'detect' ? 'http://127.0.0.1:11434/v1/chat/completions' : ''}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
Modell
|
||
|
|
<input value={form.model} onChange={(e) => update(role.role, 'model', e.target.value)} />
|
||
|
|
</label>
|
||
|
|
<label className="check">
|
||
|
|
<input type="checkbox" checked={form.zdr} onChange={(e) => update(role.role, 'zdr', e.target.checked)} />
|
||
|
|
Zero Data Retention
|
||
|
|
</label>
|
||
|
|
<label className="check">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
checked={form.no_train}
|
||
|
|
onChange={(e) => update(role.role, 'no_train', e.target.checked)}
|
||
|
|
/>
|
||
|
|
kein Provider-Training
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
API-Key {role.key_present ? '(leer lassen, um den bestehenden zu behalten)' : '(pflichtig für externe URL)'}
|
||
|
|
<input
|
||
|
|
type="password"
|
||
|
|
autoComplete="off"
|
||
|
|
value={form.key}
|
||
|
|
onChange={(e) => update(role.role, 'key', e.target.value)}
|
||
|
|
placeholder={role.role === 'detect' ? 'leer = Key des Sprachmodells' : ''}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
<button type="submit" disabled={saving === role.role}>
|
||
|
|
{saving === role.role ? 'Speichert …' : 'Speichern'}
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
)
|
||
|
|
})}
|
||
|
|
<p className="muted">Ohne ZDR und ohne Trainingsverbot bleibt das Sprachmodell fail-closed. Maskierung wählt keine Operation. Gateway lässt sich darüber nicht umgehen.</p>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
)
|
||
|
|
}
|