116 lines
5.9 KiB
TypeScript
116 lines
5.9 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import { ModalBase } from './ModalBase'
|
|
import { apiFetch } from '../_api'
|
|
import type { LLMPolicy } from '../_types'
|
|
|
|
export function PolicyModal({ existing, onClose, onSaved }: { existing: LLMPolicy | null; onClose: () => void; onSaved: () => void }) {
|
|
const [form, setForm] = useState({
|
|
name: existing?.name || '',
|
|
description: existing?.description || '',
|
|
allowed_models: (existing?.allowed_models || []).join(', '),
|
|
blocked_models: (existing?.blocked_models || []).join(', '),
|
|
rate_limit_rpm: existing?.rate_limit_rpm ?? 60,
|
|
rate_limit_tpd: existing?.rate_limit_tpd ?? 1000000,
|
|
max_tokens_per_request: existing?.max_tokens_per_request ?? 4096,
|
|
pii_detection_required: existing?.pii_detection_required ?? true,
|
|
pii_redaction_required: existing?.pii_redaction_required ?? false,
|
|
is_active: existing?.is_active ?? true,
|
|
})
|
|
const [saving, setSaving] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const handleSubmit = async () => {
|
|
if (!form.name) { setError('Name ist Pflichtfeld'); return }
|
|
setSaving(true)
|
|
try {
|
|
const body = {
|
|
...form,
|
|
allowed_models: form.allowed_models.split(',').map(s => s.trim()).filter(Boolean),
|
|
blocked_models: form.blocked_models.split(',').map(s => s.trim()).filter(Boolean),
|
|
}
|
|
if (existing) {
|
|
await apiFetch(`llm/policies/${existing.id}`, { method: 'PUT', body: JSON.stringify(body) })
|
|
} else {
|
|
await apiFetch('llm/policies', { method: 'POST', body: JSON.stringify(body) })
|
|
}
|
|
onSaved()
|
|
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
|
|
finally { setSaving(false) }
|
|
}
|
|
|
|
return (
|
|
<ModalBase title={existing ? 'Policy bearbeiten' : 'Policy erstellen'} onClose={onClose}>
|
|
{error && <div className="mb-3 p-2 bg-red-50 text-red-700 rounded text-sm">{error}</div>}
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
<input type="text" value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
|
|
<input type="text" value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Erlaubte Models (kommagetrennt)</label>
|
|
<input type="text" value={form.allowed_models} onChange={e => setForm(f => ({ ...f, allowed_models: e.target.value }))}
|
|
placeholder="qwen3:30b-a3b, claude-sonnet-4-5"
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Blockierte Models (kommagetrennt)</label>
|
|
<input type="text" value={form.blocked_models} onChange={e => setForm(f => ({ ...f, blocked_models: e.target.value }))}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono" />
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Rate (req/min)</label>
|
|
<input type="number" value={form.rate_limit_rpm} onChange={e => setForm(f => ({ ...f, rate_limit_rpm: +e.target.value }))}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Tokens/Tag</label>
|
|
<input type="number" value={form.rate_limit_tpd} onChange={e => setForm(f => ({ ...f, rate_limit_tpd: +e.target.value }))}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Max Tok/Req</label>
|
|
<input type="number" value={form.max_tokens_per_request} onChange={e => setForm(f => ({ ...f, max_tokens_per_request: +e.target.value }))}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" />
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input type="checkbox" checked={form.pii_detection_required}
|
|
onChange={e => setForm(f => ({ ...f, pii_detection_required: e.target.checked }))}
|
|
className="rounded border-gray-300" />
|
|
PII-Erkennung
|
|
</label>
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input type="checkbox" checked={form.pii_redaction_required}
|
|
onChange={e => setForm(f => ({ ...f, pii_redaction_required: e.target.checked }))}
|
|
className="rounded border-gray-300" />
|
|
PII-Redaktion
|
|
</label>
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input type="checkbox" checked={form.is_active}
|
|
onChange={e => setForm(f => ({ ...f, is_active: e.target.checked }))}
|
|
className="rounded border-gray-300" />
|
|
Aktiv
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end gap-2 mt-5">
|
|
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800">Abbrechen</button>
|
|
<button onClick={handleSubmit} disabled={saving}
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 disabled:opacity-50">
|
|
{saving ? 'Speichern...' : (existing ? 'Aktualisieren' : 'Erstellen')}
|
|
</button>
|
|
</div>
|
|
</ModalBase>
|
|
)
|
|
}
|