/** * Global AI Chat - Floating chat widget connected to backend AI * Uses backend MCP chat_completion tool via REST API * Supports Smart UI visualization rendering via ```ui:ComponentName``` format */ import { useState, useRef, useEffect, useCallback } from 'react'; import { MessageSquare, X, Send, Loader2, Minimize2, Maximize2, Bot, User, Sparkles, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; import { API_URL } from '@/config/api'; import { SmartComponentRenderer, parseMessageForComponents, UIComponentMessage, getAvailableComponents } from '@/components/smart-ui/SmartComponentRenderer'; interface ChatMessage { id: string; role: 'user' | 'assistant' | 'system'; content: string; timestamp: Date; toolUsed?: string; } interface GlobalAIChatProps { defaultOpen?: boolean; position?: 'bottom-right' | 'bottom-left'; } export default function GlobalAIChat({ defaultOpen = false, position = 'bottom-right' }: GlobalAIChatProps) { const [isOpen, setIsOpen] = useState(defaultOpen); const [isMinimized, setIsMinimized] = useState(false); const [messages, setMessages] = useState([ { id: '1', role: 'assistant', content: `Hej! Jeg er din AI assistent med adgang til ${getAvailableComponents().length} Smart UI visualiseringskomponenter og systemets MCP tools. Spørg mig om hvad som helst - jeg kan generere diagrammer, grafer, metrics og meget mere!`, timestamp: new Date() } ]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isConnected, setIsConnected] = useState(false); const scrollRef = useRef(null); const inputRef = useRef(null); // Check backend connection const checkConnection = useCallback(async () => { try { const response = await fetch(`${API_URL}/health`); setIsConnected(response.ok); } catch { setIsConnected(false); } }, []); useEffect(() => { checkConnection(); const interval = setInterval(checkConnection, 30000); return () => clearInterval(interval); }, [checkConnection]); // Auto-scroll to bottom useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages]); // Focus input when opened useEffect(() => { if (isOpen && !isMinimized && inputRef.current) { inputRef.current.focus(); } }, [isOpen, isMinimized]); const sendMessage = async () => { if (!input.trim() || isLoading) return; const userMessage: ChatMessage = { id: Date.now().toString(), role: 'user', content: input.trim(), timestamp: new Date() }; setMessages(prev => [...prev, userMessage]); setInput(''); setIsLoading(true); try { // Try Autonomous Agent first (AI-driven source selection) const response = await fetch(`${API_URL}/api/mcp/autonomous/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'natural_language', params: { query: userMessage.content } }) }); const data = await response.json(); if (data.success && data.data) { // Autonomous agent found a result const assistantMessage: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: typeof data.data === 'string' ? data.data : JSON.stringify(data.data, null, 2), timestamp: new Date(), toolUsed: `autonomous (${data.meta?.source || 'unknown'})` }; setMessages(prev => [...prev, assistantMessage]); } else { // Fallback: Use MCP route to call chat_completion tool const chatResponse = await fetch(`${API_URL}/api/mcp/route`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tool: 'chat_completion', params: { messages: messages .filter(m => m.role !== 'system') .concat(userMessage) .map(m => ({ role: m.role, content: m.content })), max_tokens: 1000, temperature: 0.7 } }) }); const chatData = await chatResponse.json(); // Extract response from various possible formats let responseText: string | null = null; let toolUsed = 'chat_completion'; // Format 1: DeepSeek response { result: { content: [{ text: "..." }] } } if (chatData.result?.content?.[0]?.text) { responseText = chatData.result.content[0].text; } // Format 2: SRAG response { result: { response: "..." } or { answer: "..." } } else if (chatData.result?.response || chatData.result?.answer) { responseText = chatData.result.answer || chatData.result.response; } // Format 3: success with response else if (chatData.result?.success && chatData.result?.response) { responseText = chatData.result.response; } if (responseText) { const assistantMessage: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: responseText, timestamp: new Date(), toolUsed }; setMessages(prev => [...prev, assistantMessage]); } else { // Try fallback to srag.query for knowledge-based questions const fallbackResponse = await fetch(`${API_URL}/api/mcp/route`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tool: 'srag.query', params: { naturalLanguageQuery: userMessage.content, model: 'deepseek' } }) }); const fallbackData = await fallbackResponse.json(); // Extract from fallback response const fallbackText = fallbackData.result?.answer || fallbackData.result?.response || fallbackData.result?.content?.[0]?.text || 'Beklager, jeg kunne ikke behandle din forespørgsel. Prøv igen.'; const assistantMessage: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: fallbackText, timestamp: new Date(), toolUsed: 'srag.query' }; setMessages(prev => [...prev, assistantMessage]); } } } catch (error) { console.error('Chat error:', error); const errorMessage: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: 'Der opstod en fejl. Tjek at backend kører på port 3001.', timestamp: new Date() }; setMessages(prev => [...prev, errorMessage]); } finally { setIsLoading(false); } }; const clearChat = () => { setMessages([{ id: '1', role: 'assistant', content: `Chat ryddet. Jeg kan generere visualiseringer med ${getAvailableComponents().length} komponenter. Hvordan kan jeg hjælpe?`, timestamp: new Date() }]); }; const positionClasses = position === 'bottom-right' ? 'right-4 bottom-4' : 'left-4 bottom-4'; if (!isOpen) { return ( ); } return (
{/* Header */}
AI Assistent
{!isMinimized && ( <> {/* Messages */}
{messages.map((message) => (
{message.role === 'user' ? ( ) : ( )}
{/* Parse and render Smart UI components from message content */} {message.role === 'assistant' ? (
{parseMessageForComponents(message.content).map((part, idx) => ( typeof part === 'string' ? (

{part}

) : ( ) ))}
) : (

{message.content}

)}
{message.timestamp.toLocaleTimeString('da-DK', { hour: '2-digit', minute: '2-digit' })} {message.toolUsed && ( {message.toolUsed} )}
))} {isLoading && (
)}
{/* Input */}
setInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }} placeholder="Skriv din besked..." className="flex-1" disabled={isLoading || !isConnected} />
{!isConnected && (

Ikke forbundet til backend. Tjek at serveren kører.

)}
)}
); }