87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|