// TODO: P3-F5 — Replace inline styles with Tailwind classes import React, { useState, useRef, useEffect } from 'react'; import clsx from 'clsx'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { streamChat, fetchMessages, fetchAttachments, uploadAttachment, getAttachmentDownloadUrl, type ChatMessage, type ChatAttachment, } from '@/api/ai'; interface ChatWindowProps { sessionId: string; agentId?: string | null; className?: string; compact?: boolean; } function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function MessageContent({ content, role }: { content: string; role: string }) { if (role === 'user') { return
{content}
; } return (
{content}
); } function ActivityIndicator({ status }: { status: string | null }) { if (!status) return null; return (
{status}
); } export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindowProps) { const [messages, setMessages] = useState([]); const [attachments, setAttachments] = useState([]); const [input, setInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); const [streamingContent, setStreamingContent] = useState(''); const [activity, setActivity] = useState(null); const [error, setError] = useState(null); const [uploadingFile, setUploadingFile] = useState(false); const scrollRef = useRef(null); const inputRef = useRef(null); const fileInputRef = useRef(null); useEffect(() => { if (!sessionId) return; setMessages([]); setAttachments([]); setError(null); fetchMessages(sessionId).then(setMessages).catch((e) => setError(e?.message || 'Failed to load')); fetchAttachments(sessionId).then(setAttachments).catch(() => {}); }, [sessionId]); useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [messages, streamingContent, activity]); const handleFileSelect = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length === 0) return; setUploadingFile(true); try { for (const file of files) { const att = await uploadAttachment(sessionId, file); setAttachments((prev) => [...prev, att]); } } catch (e) { setError(e instanceof Error ? e.message : 'Upload failed'); } finally { setUploadingFile(false); if (fileInputRef.current) fileInputRef.current.value = ''; } }; const handleSend = async () => { const content = input.trim(); if (!content || isStreaming) return; setInput(''); setIsStreaming(true); setStreamingContent(''); setActivity('Denke nach...'); setError(null); const userMsg: ChatMessage = { id: 'temp-' + Date.now(), session_id: sessionId, role: 'user', content, tokens: 0, model_used: '' }; setMessages((prev) => [...prev, userMsg]); try { const stream = streamChat(sessionId, content, agentId || undefined); let accumulated = ''; for await (const event of stream) { if (event.type === 'token' && event.content) { accumulated += event.content; setStreamingContent(accumulated); setActivity(null); } else if (event.type === 'tool_calls' && event.tools) { setActivity(`Rufe Tool auf: ${event.tools.join(', ')}`); } else if (event.type === 'tool_result' && event.tool) { setActivity(`Tool '${event.tool}' ausgeführt`); } else if (event.type === 'done') { setActivity(null); const msgs = await fetchMessages(sessionId); setMessages(msgs); setStreamingContent(''); } else if (event.type === 'error') { setError(event.content || 'Unknown error'); setActivity(null); } } } catch (e) { setError(e instanceof Error ? e.message : 'Stream failed'); } finally { setIsStreaming(false); setStreamingContent(''); setActivity(null); inputRef.current?.focus(); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; return (
{attachments.length > 0 && (
{attachments.map((att) => ( {att.filename} {formatSize(att.size_bytes)} ))}
)}
{messages.length === 0 && !streamingContent && !error && (
Starte eine Konversation...
)} {messages.map((msg) => (
{msg.role === 'user' ? 'Du' : 'KI'}
{msg.tool_calls && msg.tool_calls.length > 0 && (
⚙️ Tools: {msg.tool_calls.map((tc: any) => tc.function?.name).join(', ')}
)}
))} {streamingContent && (
KI
)} {error &&
{error}
}