feat(T07): KI-Copilot with OpenRouter chat, voice input, action system, chat history
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { MessageList } from './MessageList';
|
||||
import { ChatInput } from './ChatInput';
|
||||
import { VoiceInput } from './VoiceInput';
|
||||
import { ActionPreview } from './ActionPreview';
|
||||
import { ChatHistory } from './ChatHistory';
|
||||
import {
|
||||
sendChatMessage,
|
||||
executeAction,
|
||||
sendVoiceMessage,
|
||||
type ChatMessage,
|
||||
type ActionItem,
|
||||
} from '@/lib/copilot';
|
||||
|
||||
export function ChatInterface() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||
const [pendingActions, setPendingActions] = useState<ActionItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
|
||||
const handleSend = useCallback(async (text: string) => {
|
||||
if (!text.trim() || loading) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const userMsg: ChatMessage = {
|
||||
id: `temp-${Date.now()}`,
|
||||
session_id: sessionId || '',
|
||||
role: 'user',
|
||||
content: text,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
setMessages((prev) => [...prev, userMsg]);
|
||||
try {
|
||||
const result = await sendChatMessage(text, sessionId);
|
||||
setSessionId(result.session_id);
|
||||
const assistantMsg: ChatMessage = {
|
||||
id: result.message_id,
|
||||
session_id: result.session_id,
|
||||
role: 'assistant',
|
||||
content: result.response,
|
||||
actions: result.actions,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
setMessages((prev) => [...prev, assistantMsg]);
|
||||
if (result.actions && result.actions.length > 0) {
|
||||
setPendingActions(result.actions);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [sessionId, loading]);
|
||||
|
||||
const handleVoice = useCallback(async (audioBase64: string, mimeType: string) => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await sendVoiceMessage(audioBase64, mimeType, sessionId);
|
||||
setSessionId(result.session_id);
|
||||
const userMsg: ChatMessage = {
|
||||
id: `voice-${Date.now()}`,
|
||||
session_id: result.session_id,
|
||||
role: 'user',
|
||||
content: `🎤 ${result.transcription}`,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
const assistantMsg: ChatMessage = {
|
||||
id: result.message_id,
|
||||
session_id: result.session_id,
|
||||
role: 'assistant',
|
||||
content: result.response,
|
||||
actions: result.actions,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
setMessages((prev) => [...prev, userMsg, assistantMsg]);
|
||||
if (result.actions && result.actions.length > 0) {
|
||||
setPendingActions(result.actions);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Spracherkennung fehlgeschlagen');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [sessionId, loading]);
|
||||
|
||||
const handleConfirmAction = useCallback(async (action: ActionItem) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await executeAction(action.type, action.params, sessionId);
|
||||
const resultMsg: ChatMessage = {
|
||||
id: `action-${Date.now()}`,
|
||||
session_id: sessionId || '',
|
||||
role: 'assistant',
|
||||
content: `Aktion "${result.action}" ausgefuehrt. ${result.success ? 'Erfolgreich.' : 'Fehlgeschlagen.'}`,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
setMessages((prev) => [...prev, resultMsg]);
|
||||
setPendingActions((prev) => prev.filter((a) => a !== action));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Aktion fehlgeschlagen');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
const handleDismissAction = useCallback((action: ActionItem) => {
|
||||
setPendingActions((prev) => prev.filter((a) => a !== action));
|
||||
}, []);
|
||||
|
||||
const handleSelectSession = useCallback((selectedSessionId: string) => {
|
||||
setSessionId(selectedSessionId);
|
||||
setMessages([]);
|
||||
setPendingActions([]);
|
||||
setShowHistory(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 h-[calc(100vh-12rem)]">
|
||||
{showHistory && (
|
||||
<ChatHistory
|
||||
onSelectSession={handleSelectSession}
|
||||
onClose={() => setShowHistory(false)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<button
|
||||
onClick={() => setShowHistory(!showHistory)}
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
data-testid="toggle-history"
|
||||
>
|
||||
{showHistory ? 'Verlauf ausblenden' : 'Verlauf anzeigen'}
|
||||
</button>
|
||||
</div>
|
||||
<MessageList messages={messages} loading={loading} />
|
||||
{error && (
|
||||
<div className="text-red-600 text-sm p-2" data-testid="chat-error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{pendingActions.length > 0 && (
|
||||
<ActionPreview
|
||||
actions={pendingActions}
|
||||
onConfirm={handleConfirmAction}
|
||||
onDismiss={handleDismissAction}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2 items-end pt-2">
|
||||
<ChatInput onSend={handleSend} disabled={loading} />
|
||||
<VoiceInput onVoice={handleVoice} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user