import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import clsx from 'clsx'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { MessageSquare, Bot, Users, Pin, Search, Plus, Send, Paperclip, Smile, MoreVertical, ChevronDown, ChevronRight, Hash, Bell, ExternalLink, Loader2, X, Sparkles, } from 'lucide-react'; import { ResizablePanel } from '@/components/ui/ResizablePanel'; import { usePluginToolbarStore } from '@/store/pluginToolbarStore'; import { apiClient } from '@/api/client'; import { streamChat, fetchAgents, type AIAgent } from '@/api/ai'; // ─── Types ─── interface Conversation { id: string; title: string | null; is_pinned: boolean; is_direct: boolean; is_archived: boolean; is_locked: boolean; last_msg_at: string | null; last_msg_preview: string | null; last_msg_sender_type: string | null; unread_count: number; participants: Participant[]; metadata: Record; } interface Participant { id: string; participant_id: string | null; participant_type: string; display_name: string | null; role: string; } interface Message { id: string; conversation_id: string; sender_id: string | null; sender_type: string; content: string; content_format: string; created_at: string | null; edited_at: string | null; is_pinned: boolean; blocks: MessageBlock[]; attachments: MessageAttachment[]; reactions: MessageReaction[]; reply_to_id: string | null; } interface MessageBlock { id: string; block_type: string; block_data: Record; sort_order: number; } interface MessageAttachment { id: string; file_id: string | null; file_name: string; file_type: string; file_size: number | null; } interface MessageReaction { id: string; message_id: string; user_id: string; emoji: string; } interface MiniApp { app_id: string; name: string; icon: string; description: string; plugin_name: string; } type ConversationCategory = 'system' | 'ai' | 'colleague'; // ─── Helpers ─── function categorizeConversation(conv: Conversation): ConversationCategory { // System messages: created by system participant type if (conv.participants.some(p => p.participant_type === 'system')) return 'system'; // AI conversations: have an AI participant if (conv.participants.some(p => p.participant_type === 'ai')) return 'ai'; // Everything else is colleague chat return 'colleague'; } function formatTime(iso: string | null): string { if (!iso) return ''; const d = new Date(iso); const now = new Date(); if (d.toDateString() === now.toDateString()) { return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); } function getConversationDisplayName(conv: Conversation, currentUserId?: string): string { if (conv.title) return conv.title; const others = conv.participants.filter(p => p.participant_id !== currentUserId); if (others.length === 0) return 'Neue Konversation'; if (others.length === 1) return others[0].display_name || 'Unbekannt'; return others.map(p => p.display_name || '?').join(', '); } // ─── API calls ─── async function fetchConversations(): Promise { const r = await apiClient.get('/comm/conversations'); return r.data.items || []; } async function fetchMessages(convId: string): Promise { const r = await apiClient.get(`/comm/conversations/${convId}/messages`); return r.data.items || []; } async function sendMessage(convId: string, content: string, replyToId?: string): Promise { const r = await apiClient.post(`/comm/conversations/${convId}/messages`, { content, content_format: 'text', reply_to_id: replyToId, }); return r.data; } async function createConversation(title: string | null, participantIds: string[], isDirect: boolean, initialMessage?: string): Promise { const r = await apiClient.post('/comm/conversations', { title, participant_ids: participantIds, is_direct: isDirect, initial_message: initialMessage, }); return r.data; } async function pinConversation(convId: string): Promise { await apiClient.post(`/comm/conversations/${convId}/pin`); } async function unpinConversation(convId: string): Promise { await apiClient.delete(`/comm/conversations/${convId}/pin`); } async function markConversationRead(convId: string): Promise { await apiClient.post(`/comm/conversations/${convId}/read`, {}); } async function fetchMiniApps(): Promise { const r = await apiClient.get('/comm/miniapps'); return r.data.items || []; } async function fetchUsers(): Promise<{id: string; name: string; email: string}[]> { const r = await apiClient.get('/users'); return r.data.items || r.data || []; } // ─── Tree Node ─── interface TreeSection { category: ConversationCategory; label: string; icon: React.ReactNode; conversations: Conversation[]; } function buildTree(conversations: Conversation[], currentUserId?: string): TreeSection[] { const system = conversations.filter(c => categorizeConversation(c) === 'system'); const ai = conversations.filter(c => categorizeConversation(c) === 'ai'); const colleague = conversations.filter(c => categorizeConversation(c) === 'colleague'); // Sort: pinned first, then by last_msg_at const sortFn = (a: Conversation, b: Conversation) => { if (a.is_pinned !== b.is_pinned) return a.is_pinned ? -1 : 1; const aTime = a.last_msg_at || ''; const bTime = b.last_msg_at || ''; return bTime.localeCompare(aTime); }; system.sort(sortFn); ai.sort(sortFn); colleague.sort(sortFn); return [ { category: 'system', label: 'Systemnachrichten', icon: , conversations: system }, { category: 'ai', label: 'KI Chats', icon: , conversations: ai }, { category: 'colleague', label: 'Kollegen', icon: , conversations: colleague }, ]; } // ─── Conversation Tree ─── interface ConversationTreeProps { conversations: Conversation[]; activeConvId: string | null; onSelect: (id: string) => void; onNewChat: (category: ConversationCategory) => void; loading: boolean; } function ConversationTree({ conversations, activeConvId, onSelect, onNewChat, loading }: ConversationTreeProps) { const [expanded, setExpanded] = useState>({ system: true, ai: true, colleague: true, }); const tree = useMemo(() => buildTree(conversations), [conversations]); const toggle = (cat: ConversationCategory) => setExpanded(s => ({ ...s, [cat]: !s[cat] })); return (

Kommunikation

{loading && (
)} {!loading && tree.map(section => (
toggle(section.category)} > {expanded[section.category] ? : } {section.icon} {section.label} {section.conversations.length}
{expanded[section.category] && section.conversations.map(conv => ( ))} {expanded[section.category] && section.conversations.length === 0 && (
Keine Konversationen
)}
))}
); } // ─── Chat Window ─── interface ChatWindowProps { convId: string; category: ConversationCategory; onBack?: () => void; } function ChatWindow({ convId, category, onBack }: ChatWindowProps) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [loading, setLoading] = useState(true); const [sending, setSending] = useState(false); const [error, setError] = useState(null); const [aiStreaming, setAiStreaming] = useState(false); const [streamingContent, setStreamingContent] = useState(''); const [showMiniApps, setShowMiniApps] = useState(false); const [miniApps, setMiniApps] = useState([]); const [agents, setAgents] = useState([]); const [selectedAgentId, setSelectedAgentId] = useState(null); const [aiSessionId, setAiSessionId] = useState(null); const scrollRef = useRef(null); const wsRef = useRef(null); // Load messages useEffect(() => { if (!convId) return; setLoading(true); setError(null); fetchMessages(convId) .then(msgs => { setMessages(msgs); setLoading(false); }) .catch(e => { setError(e?.message || 'Fehler beim Laden'); setLoading(false); }); markConversationRead(convId).catch(() => {}); }, [convId]); // Load agents and mini-apps useEffect(() => { fetchAgents().then(setAgents).catch(() => {}); fetchMiniApps().then(setMiniApps).catch(() => {}); }, []); // Auto-select default agent for AI chats useEffect(() => { if (category === 'ai' && agents.length > 0 && !selectedAgentId) { const def = agents.find(a => a.is_default) || agents[0]; setSelectedAgentId(def.id); } }, [category, agents, selectedAgentId]); // WebSocket connection useEffect(() => { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/v1/comm/ws`; try { const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { ws.send(JSON.stringify({ type: 'subscribe', conversation_id: convId })); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'message' && data.conversation_id === convId) { setMessages(prev => [...prev, data.message]); } } catch {} }; return () => { ws.close(); wsRef.current = null; }; } catch {} }, [convId]); // Auto-scroll useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [messages, streamingContent]); const handleSend = async () => { if (!input.trim() || sending) return; const content = input.trim(); setInput(''); setSending(true); setError(null); try { if (category === 'ai' && aiSessionId) { // AI streaming chat setAiStreaming(true); setStreamingContent(''); let accumulated = ''; for await (const event of streamChat(aiSessionId, content, selectedAgentId || undefined)) { if (event.type === 'token' && event.content) { accumulated += event.content; setStreamingContent(accumulated); } else if (event.type === 'done') { break; } else if (event.type === 'error') { setError(event.content || 'KI Fehler'); break; } } setAiStreaming(false); setStreamingContent(''); // Reload messages to get the AI response const msgs = await fetchMessages(convId); setMessages(msgs); } else { // Regular message const msg = await sendMessage(convId, content); setMessages(prev => [...prev, msg]); } } catch (e: any) { setError(e?.message || 'Senden fehlgeschlagen'); } finally { setSending(false); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const startAiSession = async () => { try { const r = await apiClient.post('/ai/sessions', { title: 'KI Chat', agent_id: selectedAgentId, is_sidebar: false, }); setAiSessionId(r.data.id); } catch (e: any) { setError('KI Session konnte nicht gestartet werden'); } }; // Auto-start AI session for AI conversations useEffect(() => { if (category === 'ai' && !aiSessionId && selectedAgentId) { startAiSession(); } }, [category, aiSessionId, selectedAgentId]); return (
{/* Header */}
{onBack && ( )} {messages[0]?.conversation_id === convId ? 'Konversation' : 'Chat'} {category === 'ai' && agents.length > 0 && ( )}
{/* Mini-Apps panel */} {showMiniApps && miniApps.length > 0 && (
{miniApps.map(app => ( ))}
)} {/* Messages */}
{loading && (
)} {error && (
{error}
)} {!loading && messages.length === 0 && !aiStreaming && (

Keine Nachrichten. Schreibe die erste!

)} {messages.map(msg => ( ))} {aiStreaming && (
KI antwortet...
{streamingContent || '...'}
)}
{/* Input */}