abbe7a18fc
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner - P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var - P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup - P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns - P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs) - P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import - P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default - P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed - P2: 28 frontend TODOs (hardcoded constants, deprecated notification API) - P3: dead code, duplicates, deprecated imports, private attr, __import__ inline - P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n) - ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix) - F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String) - Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
220 lines
8.9 KiB
TypeScript
220 lines
8.9 KiB
TypeScript
import { useProactiveSettings } from '@/api/aiProactive';
|
|
// TODO: P2-T20 — Replace hardcoded categoryLabels/modelOptions with backend config
|
|
|
|
const categoryLabels: Record<string, string> = {
|
|
mail: 'E-Mails',
|
|
tasks: 'Aufgaben',
|
|
contacts: 'Kontakte',
|
|
companies: 'Firmen',
|
|
insights: 'Erkenntnisse',
|
|
};
|
|
|
|
const modelOptions = [
|
|
{ value: 'ollama/deepseek-v4', label: 'DeepSeek V4 (empfohlen)' },
|
|
{ value: 'ollama/deepseek-v4-pro', label: 'DeepSeek V4 Pro (präzise)' },
|
|
{ value: 'ollama/llama3.2', label: 'Llama 3.2 (Alternative)' },
|
|
{ value: 'ollama/gpt-4o-mini', label: 'GPT-4o mini via Ollama' },
|
|
{ value: 'gpt-4o-mini', label: 'GPT-4o mini (Direct OpenAI)' },
|
|
];
|
|
|
|
export function ProactiveAISettings() {
|
|
const { settings, update } = useProactiveSettings();
|
|
|
|
if (!settings) {
|
|
return (
|
|
<div className="flex items-center justify-center p-8 text-gray-400">
|
|
<div className="animate-pulse">Lade Einstellungen...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const toggleCategory = async (cat: string) => {
|
|
const current = settings.suggestion_categories || [];
|
|
const newCats = current.includes(cat)
|
|
? current.filter((c: string) => c !== cat)
|
|
: [...current, cat];
|
|
await update({ suggestion_categories: newCats });
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-2xl space-y-6">
|
|
{/* Enable/Disable */}
|
|
<div className="flex items-center justify-between p-4 bg-white rounded-lg border border-gray-200">
|
|
<div>
|
|
<h3 className="font-semibold text-gray-800">Proaktive KI</h3>
|
|
<p className="text-sm text-gray-500">Aktiviert oder deaktiviert alle Vorschläge</p>
|
|
</div>
|
|
<button
|
|
onClick={() => update({ enabled: !settings.enabled })}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
settings.enabled ? 'bg-blue-500' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
settings.enabled ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Categories */}
|
|
<div className="p-4 bg-white rounded-lg border border-gray-200">
|
|
<h3 className="font-semibold text-gray-800 mb-3">Kategorien</h3>
|
|
<p className="text-sm text-gray-500 mb-4">Für welche Bereiche sollen Vorschläge generiert werden?</p>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{Object.entries(categoryLabels).map(([key, label]) => {
|
|
const checked = settings.suggestion_categories?.includes(key);
|
|
return (
|
|
<label
|
|
key={key}
|
|
className={`flex items-center gap-2 p-3 rounded-lg border cursor-pointer transition-colors ${
|
|
checked ? 'bg-blue-50 border-blue-300' : 'bg-gray-50 border-gray-200 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={checked || false}
|
|
onChange={() => toggleCategory(key)}
|
|
className="w-4 h-4 rounded text-blue-500 focus:ring-blue-400"
|
|
/>
|
|
<span className="text-sm text-gray-700">{label}</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Confidence Threshold */}
|
|
<div className="p-4 bg-white rounded-lg border border-gray-200">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="font-semibold text-gray-800">Confidence Schwellwert</h3>
|
|
<span className="text-sm font-mono text-blue-600">
|
|
{Math.round((settings.confidence_threshold || 0.5) * 100)}%
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-500 mb-3">
|
|
Nur Vorschläge mit höherer Confidence anzeigen
|
|
</p>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.05"
|
|
value={settings.confidence_threshold || 0.5}
|
|
onChange={(e) => update({ confidence_threshold: parseFloat(e.target.value) })}
|
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
|
/>
|
|
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
|
<span>Alle</span>
|
|
<span>Sehr sicher</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Rate Limit */}
|
|
<div className="p-4 bg-white rounded-lg border border-gray-200">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="font-semibold text-gray-800">Rate Limit</h3>
|
|
<span className="text-sm font-mono text-blue-600">
|
|
{settings.rate_limit_seconds || 10}s
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-500 mb-3">
|
|
Mindestabstand zwischen Vorschlägen
|
|
</p>
|
|
<input
|
|
type="range"
|
|
min="5"
|
|
max="60"
|
|
step="5"
|
|
value={settings.rate_limit_seconds || 10}
|
|
onChange={(e) => update({ rate_limit_seconds: parseInt(e.target.value) })}
|
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
|
/>
|
|
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
|
<span>5s</span>
|
|
<span>60s</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Model Selection */}
|
|
<div className="p-4 bg-white rounded-lg border border-gray-200">
|
|
<h3 className="font-semibold text-gray-800 mb-2">KI Modell (Ollama Cloud)</h3>
|
|
<p className="text-sm text-gray-500 mb-3">Wähle das Modell für Vorschläge. DeepSeek V4 ist empfohlen für beste Performance/Kosten.</p>
|
|
<select
|
|
value={settings.model || 'ollama/deepseek-v4'}
|
|
onChange={(e) => update({ model: e.target.value })}
|
|
className="w-full px-3 py-2 rounded-lg border border-gray-200 bg-white text-sm text-gray-700 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none"
|
|
>
|
|
{modelOptions.map(opt => (
|
|
<option key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Heartbeat Configuration */}
|
|
<div className="p-4 bg-white rounded-lg border border-gray-200">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div>
|
|
<h3 className="font-semibold text-gray-800">Heartbeat</h3>
|
|
<p className="text-sm text-gray-500">Regelmäßige Status-Meldung an KI-Raum</p>
|
|
</div>
|
|
<button
|
|
onClick={() => update({ heartbeat_enabled: !settings.heartbeat_enabled })}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
settings.heartbeat_enabled ? 'bg-blue-500' : 'bg-gray-300'
|
|
}`}
|
|
role="switch"
|
|
aria-checked={settings.heartbeat_enabled}
|
|
aria-label="Heartbeat aktivieren"
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
settings.heartbeat_enabled ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
{settings.heartbeat_enabled && (
|
|
<>
|
|
<div className="mb-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium text-gray-700">Intervall</span>
|
|
<span className="text-sm font-mono text-blue-600">
|
|
{settings.heartbeat_interval_seconds || 300}s
|
|
</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="60"
|
|
max="1800"
|
|
step="60"
|
|
value={settings.heartbeat_interval_seconds || 300}
|
|
onChange={(e) => update({ heartbeat_interval_seconds: parseInt(e.target.value) })}
|
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
|
/>
|
|
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
|
<span>1min</span>
|
|
<span>30min</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Ziel-Raum</label>
|
|
<input
|
|
type="text"
|
|
value={settings.heartbeat_target_room || 'Live KI'}
|
|
onChange={(e) => update({ heartbeat_target_room: e.target.value })}
|
|
className="w-full px-3 py-2 rounded-lg border border-gray-200 bg-white text-sm text-gray-700 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none"
|
|
placeholder="Live KI"
|
|
/>
|
|
<p className="text-xs text-gray-400 mt-1">Name des Raums in der Kommunikation, an den Status-Meldungen gesendet werden.</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|