// src/components/InferencePanel.tsx import React, { useState, useEffect, useRef } from 'react'; import axios from 'axios'; const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; // Define message types interface Message { id: string; sender: 'user' | 'ai'; text: string; domain?: string; confidence?: number; modelUsed?: string; sourceType?: string; thinkingProcess?: string; thinkingSteps?: string[]; isEditing?: boolean; editedText?: string; } interface InferencePanelProps { activeDomain: string; currentSessionId?: string; } const InferencePanel: React.FC = ({ activeDomain }) => { const [sessionId] = useState(`ws-session-${Date.now()}-${Math.random()}`); const [socket, setSocket] = useState(null); const [isConnected, setIsConnected] = useState(false); const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [ragMode, setRagMode] = useState<'direct' | 'rag'>('rag'); const [currentThinkingSteps, setCurrentThinkingSteps] = useState([]); const [availableDomains, setAvailableDomains] = useState([]); const [editingMessageId, setEditingMessageId] = useState(null); const [editingText, setEditingText] = useState(''); const messagesEndRef = useRef(null); // Load available domains useEffect(() => { const fetchDomains = async () => { try { const response = await axios.get(`${API_URL}/api/config/domains`); setAvailableDomains(response.data); } catch (error) { console.error('Failed to fetch domains:', error); } }; fetchDomains(); }, []); // Effect to manage WebSocket connection useEffect(() => { const wsUrl = `ws://localhost:8000/api/questions/ws/${sessionId}`; const ws = new WebSocket(wsUrl); ws.onopen = () => { console.log('WebSocket connected'); setIsConnected(true); setError(null); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); handleSocketMessage(data); }; ws.onclose = () => { console.log('WebSocket disconnected'); setIsConnected(false); }; ws.onerror = (error) => { console.error('WebSocket error:', error); setIsConnected(false); setIsLoading(false); setError('Could not connect to the inference server.'); }; setSocket(ws); // Cleanup on unmount return () => { ws.close(); }; }, [sessionId]); // Effect to scroll to the bottom of messages useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, currentThinkingSteps]); const handleSocketMessage = (data: any) => { switch (data.type) { case 'processing': setCurrentThinkingSteps(prev => [...prev, 'Processing...']); break; case 'thinking': // Update current thinking steps for real-time display setCurrentThinkingSteps(prev => [...prev, data.step]); break; case 'token': setMessages(prev => { const lastMessage = prev[prev.length - 1]; if (lastMessage && lastMessage.sender === 'ai' && !lastMessage.sourceType) { // Ensure it's the current AI response // Append token to the last AI message const updatedMessages = [...prev]; updatedMessages[prev.length - 1] = { ...lastMessage, text: lastMessage.text + data.content }; return updatedMessages; } else { // Start a new AI message (should be rare, but good for robustness) return [...prev, { id: `ai-${Date.now()}-${Math.random()}`, sender: 'ai', text: data.content || '', domain: activeDomain }]; } }); break; case 'meta': // Metadata sent before or during streaming setMessages(prev => { const lastMessage = prev[prev.length - 1]; if (lastMessage && lastMessage.sender === 'ai') { const updatedMessages = [...prev]; updatedMessages[prev.length - 1] = { ...lastMessage, confidence: data.confidence, modelUsed: data.model_used, sourceType: data.source_type, thinkingProcess: data.thinking_process // Changed from data.thinking }; return updatedMessages; } return prev; }); break; case 'response': // Final complete answer setIsLoading(false); setCurrentThinkingSteps([]); // Clear real-time thinking steps setMessages(prev => { const lastMessage = prev[prev.length - 1]; if (lastMessage && lastMessage.sender === 'ai') { const updatedMessages = [...prev]; // Ensure final metadata is set updatedMessages[prev.length - 1] = { ...lastMessage, text: data.response, // Final complete response confidence: data.confidence || lastMessage.confidence, modelUsed: data.model_used || lastMessage.modelUsed, sourceType: data.source_type || lastMessage.sourceType, thinkingProcess: data.thinking_process || lastMessage.thinkingProcess, // Changed from data.thinking thinkingSteps: currentThinkingSteps // Attach all thinking steps to the final message }; return updatedMessages; } // If no streaming tokens received, create a new message return [...prev, { id: `ai-${Date.now()}-${Math.random()}`, sender: 'ai', text: data.response, domain: activeDomain, confidence: data.confidence, modelUsed: data.model_used, sourceType: data.source_type, thinkingProcess: data.thinking_process, // Changed from data.thinking thinkingSteps: currentThinkingSteps }]; }); break; case 'error': setIsLoading(false); setCurrentThinkingSteps([]); setError(`WebSocket Error: ${data.error}`); // Set error state for prominent display break; } }; const handleDomainChange = (messageId: string, newDomain: string) => { setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, domain: newDomain } : msg )); }; const handleStartEdit = (msg: Message) => { setEditingMessageId(msg.id); setEditingText(msg.text); }; const handleSaveEdit = (msgId: string) => { setMessages(prev => prev.map(msg => msg.id === msgId ? { ...msg, text: editingText } : msg )); setEditingMessageId(null); }; const handleCancelEdit = () => { setEditingMessageId(null); setEditingText(''); }; const handleSendMessage = () => { if (inputValue.trim() && socket && isConnected && !isLoading) { // Add user message to chat const newMessage: Message = { id: `msg-${Date.now()}-${Math.random()}`, sender: 'user', text: inputValue, domain: activeDomain }; setMessages(prev => [...prev, newMessage]); // Send question to WebSocket server socket.send(JSON.stringify({ type: 'question', question: inputValue, stream: true, // Always stream domain_id: activeDomain, // Use the active domain from props rag_mode: ragMode // Add the selected RAG mode })); setIsLoading(true); setInputValue(''); setCurrentThinkingSteps([]); // Clear previous thinking steps display } }; const getSourceTypeColor = (sourceType?: string) => { switch (sourceType) { case 'db_augmented': return 'bg-blue-600'; case 'ai_internal_weights': return 'bg-purple-600'; case 'web_search': return 'bg-yellow-600'; default: return 'bg-gray-600'; } }; return (

Inference

{isConnected ? '● Connected' : '○ Disconnected'}
{error && (
{error}
)}
{messages.map((msg) => (
{editingMessageId === msg.id ? (