feat(assistant): Fragen an die Daten im Kopf der Anwendung
Management und Administration koennen oben rechts eine Frage in Worten stellen - "wie viele Stunden im August", "welches Projekt hat noch Budget", "wie ist die Auslastung". Aufbau: das Modell bekommt eine Uebersicht (Kunden, laufende Projekte, Eckdaten) und vier Werkzeuge - auswertung, projekte, buchungen, personen. Damit fragt es gezielt nach, statt dass ihm der ganze Bestand vorgelegt wird; 19.000 Buchungen passen in keinen Kontext. Die Werkzeuge rechnen mit loadEntries und loadProjects, also mit denselben Funktionen wie die Reports - der Assistent kann so keine zweite Wahrheit erzeugen. Was einfliesst, begrenzt visibleUserIds wie ueberall sonst. Benutzt wird die Responses-API von OpenAI: die neueren Modelle nehmen Werkzeuge in /v1/chat/completions nur ohne Nachdenken an. Mit "store: false" bleibt nichts bei OpenAI liegen, der Verlauf geht deshalb je Runde komplett mit. Der Schluessel liegt verschluesselt in den Einstellungen (AES-256-GCM wie das SSO-Secret) und wird nie ausgeliefert; das Modell ist einstellbar, Vorgabe gpt-5.6-luna. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c3aed8a3d3
commit
e908db2788
@@ -10,12 +10,13 @@ import { useLoader } from '../../lib/hooks';
|
||||
import { useRouter } from '../../lib/router';
|
||||
import { useToast } from '../Toast';
|
||||
|
||||
type Tab = 'allgemein' | 'export' | 'sso' | 'sso-verwaltung' | 'hrworks' | 'benutzer';
|
||||
type Tab = 'allgemein' | 'export' | 'sso' | 'sso-verwaltung' | 'hrworks' | 'ki' | 'benutzer';
|
||||
const TABS: { key: Tab; label: string; icon: IconName; adminOnly: boolean }[] = [
|
||||
{ key: 'allgemein', label: 'Allgemein', icon: 'settings', adminOnly: true },
|
||||
{ key: 'export', label: 'Export: Kopf & Fuß', icon: 'document', adminOnly: true },
|
||||
{ key: 'sso', label: 'SSO / Entra ID', icon: 'key', adminOnly: true },
|
||||
{ key: 'hrworks', label: 'Abwesenheiten (HRworks)', icon: 'calendar', adminOnly: true },
|
||||
{ key: 'ki', label: 'KI-Assistent', icon: 'spark', adminOnly: true },
|
||||
// Management sieht in den Einstellungen nur diesen Reiter.
|
||||
{ key: 'sso-verwaltung', label: 'SSO-Verwaltung', icon: 'globe', adminOnly: false },
|
||||
{ key: 'benutzer', label: 'Benutzerverwaltung', icon: 'team', adminOnly: true },
|
||||
@@ -60,6 +61,7 @@ export const Settings: React.FC = () => {
|
||||
{tab === 'export' && <ExportSettings settings={data} onSaved={(s) => setData(s)} />}
|
||||
{tab === 'sso' && <SsoSettings settings={data} onSaved={reload} />}
|
||||
{tab === 'hrworks' && <HrworksSettings settings={data} onSaved={reload} />}
|
||||
{tab === 'ki' && <AiSettings settings={data} onSaved={reload} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -438,3 +440,93 @@ const SsoSettings: React.FC<{ settings: AdminSettings; onSaved: () => void }> =
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* KI-Assistent: Schluessel und Modell. Der Schluessel wird verschluesselt
|
||||
* abgelegt und nie wieder ausgeliefert - wie das SSO-Secret.
|
||||
*/
|
||||
const AiSettings: React.FC<{ settings: AdminSettings; onSaved: () => void }> = ({ settings, onSaved }) => {
|
||||
const [ai, setAi] = useState({ ...settings.ai, api_key: '' });
|
||||
const { busy, save } = useSaver();
|
||||
const { addToast } = useToast();
|
||||
const [testing, setTesting] = useState(false);
|
||||
const set = (patch: Partial<typeof ai>) => setAi({ ...ai, ...patch });
|
||||
|
||||
useEffect(() => setAi({ ...settings.ai, api_key: '' }), [settings.ai]);
|
||||
|
||||
const test = async () => {
|
||||
setTesting(true);
|
||||
try {
|
||||
const r = await settingsApi.aiTest();
|
||||
addToast(`Verbindung ok – ${r.model} antwortet: ${r.answer || 'OK'}`, 'success');
|
||||
} catch (err) {
|
||||
addToast(err instanceof Error ? err.message : 'Test fehlgeschlagen.', 'error');
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Section
|
||||
title="KI-Assistent"
|
||||
hint="Fragen zu Kunden, Projekten, Budgets und Auslastung – im Kopf der Anwendung, für Management und Administration"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="secondary" icon="refresh" busy={testing} onClick={test} disabled={!settings.ai.key_set && !ai.api_key}>
|
||||
Verbindung testen
|
||||
</Button>
|
||||
<Button icon="check" busy={busy} onClick={() => save({ ai: { ...ai, api_key: ai.api_key || undefined } }, onSaved)}>
|
||||
Speichern
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={`flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-semibold ${settings.ai.enabled ? 'bg-brand-50 text-brand-800' : 'bg-ink-50 text-ink-600'}`}>
|
||||
<Icon name={settings.ai.enabled ? 'check' : 'info'} className="h-5 w-5" />
|
||||
{settings.ai.enabled
|
||||
? 'Der Assistent ist aktiv – Management und Administration sehen oben rechts „Fragen“.'
|
||||
: settings.ai.key_set
|
||||
? 'Schlüssel hinterlegt, der Assistent ist noch nicht eingeschaltet.'
|
||||
: 'Noch kein Schlüssel hinterlegt.'}
|
||||
</div>
|
||||
|
||||
<Field
|
||||
label="OpenAI API-Schlüssel"
|
||||
hint={settings.ai.key_set ? 'Hinterlegt (verschlüsselt). Leer lassen = unverändert.' : 'Wird verschlüsselt gespeichert und nie wieder angezeigt.'}
|
||||
>
|
||||
{/* Kein type="password" - Passwortmanager sollen hier nicht anspringen. */}
|
||||
<input
|
||||
name="pulse-openai-credential"
|
||||
autoComplete="off"
|
||||
data-lpignore="true"
|
||||
data-1p-ignore="true"
|
||||
className={`${inputClass} font-mono text-xs`}
|
||||
style={{ WebkitTextSecurity: 'disc' } as React.CSSProperties}
|
||||
value={ai.api_key}
|
||||
onChange={(e) => set({ api_key: e.target.value })}
|
||||
placeholder={settings.ai.key_set ? '••••••••••••' : 'sk-…'}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Modell" hint="Voreingestellt gpt-5.6-luna. Ein anderes Modell muss Werkzeuge über die Responses-API unterstützen.">
|
||||
<input className={`${inputClass} font-mono text-xs`} value={ai.model} onChange={(e) => set({ model: e.target.value })} />
|
||||
</Field>
|
||||
|
||||
<Toggle
|
||||
checked={ai.enabled}
|
||||
onChange={(v) => set({ enabled: v })}
|
||||
label="Assistent einschalten"
|
||||
hint="Zeigt den Knopf „Fragen“ im Kopf – nur für Management und Administration."
|
||||
/>
|
||||
|
||||
<div className="rounded-xl border border-ink-200 bg-ink-50/60 px-4 py-3 text-xs leading-relaxed text-ink-500">
|
||||
Jede Frage geht zusammen mit den dafür nötigen Zahlen aus Pulse an OpenAI. Übertragen werden Kunden-,
|
||||
Projekt- und Mitarbeiternamen sowie Stunden und Beträge – keine Zugangsdaten. OpenAI speichert die Anfragen
|
||||
nicht (die Schnittstelle wird mit „store: false“ aufgerufen). Der Assistent liest nur und ändert nichts.
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user