Phase 4: KI-UI-Steuerung — AI agent UI control via WebSocket
- New ai_ui_control plugin: WS endpoint /ws/ai-ui-control, REST API (POST /command, GET /command/{id}/status, GET /online-users)
- UI-Command-Protocol: 6 command types (navigate, filter, open_contact, modal, tab, settings) with Pydantic schemas
- WebSocket manager: per-user connections, command delivery, feedback storage, stale cleanup
- Frontend useAIUIControl hook: WS client with auto-reconnect, command dispatch, feedback sending
- aiUIControlStore: Zustand store for command state, active modal/tab, pending filter/settings
- AIUIControlIndicator: visual KI indication (Bot icon, toast, pulse animation)
- ContactDetail integration: syncs activeTab and personModalOpen from AI control store
- AppShell integration: useAIUIControl hook + AIUIControlIndicator
- i18n keys for DE/EN
- 18 Vitest tests: command protocol, store actions, feedback, visual indication
- TSC: 0 new errors (only 2 pre-existing Dms.tsx errors)
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* AIUIControlIndicator — Visual indication when AI is controlling the UI.
|
||||
*
|
||||
* Task 4.11: Visuelle KI-Indikation
|
||||
* When AI executes an action: highlight effect + toast "KI führt Aktion aus...".
|
||||
* User sees that AI is acting.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { Bot, X } from 'lucide-react';
|
||||
import { useAIUIControlStore } from '@/store/aiUIControlStore';
|
||||
import { useUIStore } from '@/store/uiStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function AIUIControlIndicator() {
|
||||
const { t } = useTranslation();
|
||||
const aiActive = useAIUIControlStore((s) => s.aiActive);
|
||||
const activeCommand = useAIUIControlStore((s) => s.activeCommand);
|
||||
const clearPending = useAIUIControlStore((s) => s.clearPending);
|
||||
const addToast = useUIStore((s) => s.addToast);
|
||||
|
||||
// Show toast when a new command starts
|
||||
useEffect(() => {
|
||||
if (aiActive && activeCommand) {
|
||||
const desc = activeCommand.description || t(`ai.uiControl.${actionToKey(activeCommand.action)}`, getCommandFallback(activeCommand.action));
|
||||
addToast({
|
||||
type: 'info',
|
||||
message: `${t('ai.uiControl.executing', 'KI führt Aktion aus')}: ${desc}`,
|
||||
duration: 4000,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [aiActive, activeCommand?.command_id]);
|
||||
|
||||
if (!aiActive) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed top-4 right-4 z-50 flex items-center gap-2 rounded-lg border border-primary-300 bg-primary-50 px-4 py-2 shadow-lg animate-pulse"
|
||||
data-testid="ai-ui-control-indicator"
|
||||
role="status"
|
||||
aria-label={t('ai.uiControl.active', 'KI steuert die UI')}
|
||||
>
|
||||
<Bot className="h-5 w-5 text-primary-600 animate-pulse" aria-hidden="true" />
|
||||
<span className="text-sm font-medium text-primary-700">
|
||||
{t('ai.uiControl.active', 'KI steuert die UI')}
|
||||
</span>
|
||||
{activeCommand?.description && (
|
||||
<span className="text-xs text-primary-500">
|
||||
— {activeCommand.description}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => clearPending()}
|
||||
className="ml-2 text-primary-400 hover:text-primary-600"
|
||||
aria-label={t('common.dismiss', 'Schließen')}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function actionToKey(action: string): string {
|
||||
const keyMap: Record<string, string> = {
|
||||
navigate: 'navigate',
|
||||
filter: 'filter',
|
||||
open_contact: 'openContact',
|
||||
modal: 'modal',
|
||||
tab: 'tab',
|
||||
settings: 'settings',
|
||||
};
|
||||
return keyMap[action] || action;
|
||||
}
|
||||
|
||||
function getCommandFallback(action: string): string {
|
||||
const fallbacks: Record<string, string> = {
|
||||
navigate: 'Navigation',
|
||||
filter: 'Filter setzen',
|
||||
open_contact: 'Kontakt öffnen',
|
||||
modal: 'Dialog öffnen',
|
||||
tab: 'Tab wechseln',
|
||||
settings: 'Einstellungen ändern',
|
||||
};
|
||||
return fallbacks[action] || action;
|
||||
}
|
||||
Reference in New Issue
Block a user