feat(T07): KI-Copilot with OpenRouter chat, voice input, action system, chat history

This commit is contained in:
2026-07-17 02:28:41 +02:00
parent cb89e3ff1e
commit 5cc9f8e9a9
24 changed files with 2878 additions and 188 deletions
@@ -0,0 +1,91 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { getChatHistory, type ChatMessage } from '@/lib/copilot';
interface ChatHistoryProps {
onSelectSession: (sessionId: string) => void;
onClose: () => void;
}
interface SessionGroup {
sessionId: string;
firstMessage: string;
messageCount: number;
lastActivity: string;
}
export function ChatHistory({ onSelectSession, onClose }: ChatHistoryProps) {
const [sessions, setSessions] = useState<SessionGroup[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadHistory = async () => {
setLoading(true);
setError(null);
try {
const result = await getChatHistory(1, 100);
const sessionMap = new Map<string, ChatMessage[]>();
for (const msg of result.items) {
const existing = sessionMap.get(msg.session_id) || [];
existing.push(msg);
sessionMap.set(msg.session_id, existing);
}
const groups: SessionGroup[] = [];
for (const [sessionId, msgs] of sessionMap) {
const userMsg = msgs.find((m) => m.role === 'user');
groups.push({
sessionId,
firstMessage: userMsg?.content || 'Unbekannt',
messageCount: msgs.length,
lastActivity: msgs[msgs.length - 1]?.created_at || '',
});
}
groups.sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
setSessions(groups);
} catch (err) {
setError(err instanceof Error ? err.message : 'Verlauf konnte nicht geladen werden');
} finally {
setLoading(false);
}
};
loadHistory();
}, []);
const handleSelect = useCallback((sessionId: string) => {
onSelectSession(sessionId);
}, [onSelectSession]);
return (
<div className="w-64 border-r border-gray-200 pr-4" data-testid="chat-history">
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold text-sm">Konversationen</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-sm" data-testid="close-history">
</button>
</div>
{loading && <p className="text-sm text-gray-400" data-testid="history-loading">Wird geladen...</p>}
{error && <p className="text-sm text-red-600" data-testid="history-error">{error}</p>}
{!loading && !error && sessions.length === 0 && (
<p className="text-sm text-gray-400" data-testid="history-empty">Keine Konversationen vorhanden.</p>
)}
{!loading && !error && sessions.length > 0 && (
<ul className="space-y-1">
{sessions.map((session) => (
<li key={session.sessionId}>
<button
onClick={() => handleSelect(session.sessionId)}
className="w-full text-left p-2 rounded hover:bg-gray-100 text-sm"
data-testid={`history-session-${session.sessionId}`}
>
<p className="font-medium truncate">{session.firstMessage}</p>
<p className="text-xs text-gray-400">{session.messageCount} Nachrichten</p>
</button>
</li>
))}
</ul>
)}
</div>
);
}