Spaces:
Build error
Build error
| 'use client'; | |
| import { useState, useEffect, useRef, memo } from 'react'; | |
| import { Send, Bot, User, Sparkles, Loader2 } from 'lucide-react'; | |
| // Use environment variable or relative URL for production | |
| const API_URL = process.env.NEXT_PUBLIC_API_URL || ''; | |
| interface Transaction { | |
| id: number; | |
| amount: number; | |
| merchant: string; | |
| prediction: string; | |
| final_score: number; | |
| } | |
| interface Message { | |
| id: string; | |
| role: 'user' | 'assistant'; | |
| content: string; | |
| timestamp: Date; | |
| } | |
| interface ChatPanelProps { | |
| transactions: Transaction[]; | |
| metrics: { | |
| total: number; | |
| flagged: number; | |
| accuracy: number; | |
| }; | |
| } | |
| // Memoized to prevent re-renders from parent | |
| const ChatPanel = memo(function ChatPanel({ transactions, metrics }: ChatPanelProps) { | |
| const [messages, setMessages] = useState<Message[]>([ | |
| { | |
| id: '1', | |
| role: 'assistant', | |
| content: "Hello! I'm the QuantumShield AI Assistant. I can help you analyze transactions and explain our quantum detection. What would you like to know?", | |
| timestamp: new Date() | |
| } | |
| ]); | |
| const [input, setInput] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const messagesEndRef = useRef<HTMLDivElement>(null); | |
| const abortControllerRef = useRef<AbortController | null>(null); | |
| // Store latest data in refs to avoid stale closures | |
| const transactionsRef = useRef(transactions); | |
| const metricsRef = useRef(metrics); | |
| useEffect(() => { | |
| transactionsRef.current = transactions; | |
| metricsRef.current = metrics; | |
| }, [transactions, metrics]); | |
| // Scroll to bottom on new messages | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }, [messages]); | |
| const sendMessage = async () => { | |
| if (!input.trim() || isLoading) return; | |
| const userInput = input.trim(); | |
| const userMessage: Message = { | |
| id: Date.now().toString(), | |
| role: 'user', | |
| content: userInput, | |
| timestamp: new Date() | |
| }; | |
| setMessages(prev => [...prev, userMessage]); | |
| setInput(''); | |
| setIsLoading(true); | |
| // Cancel any pending request | |
| if (abortControllerRef.current) { | |
| abortControllerRef.current.abort(); | |
| } | |
| abortControllerRef.current = new AbortController(); | |
| try { | |
| // Include current transaction context in the message | |
| const contextMessage = ` | |
| Current System Status: | |
| - Total Transactions Processed: ${metricsRef.current.total} | |
| - Flagged as Fraud: ${metricsRef.current.flagged} | |
| - Model Accuracy: ${(metricsRef.current.accuracy * 100).toFixed(1)}% | |
| - Recent Transactions: ${transactionsRef.current.slice(0, 5).map(t => | |
| `#${t.id}: $${t.amount.toFixed(2)} at ${t.merchant} (${t.prediction}, score: ${(t.final_score * 100).toFixed(0)}%)` | |
| ).join('; ')} | |
| User Question: ${userInput}`; | |
| const response = await fetch(`${API_URL}/api/chat`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| message: contextMessage, | |
| conversation_history: messages.slice(-10).map(m => ({ role: m.role, content: m.content })) | |
| }), | |
| signal: abortControllerRef.current.signal | |
| }); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| setMessages(prev => [...prev, { | |
| id: (Date.now() + 1).toString(), | |
| role: 'assistant', | |
| content: data.response, | |
| timestamp: new Date() | |
| }]); | |
| } else { | |
| throw new Error('Request failed'); | |
| } | |
| } catch (error: unknown) { | |
| if (error instanceof Error && error.name === 'AbortError') return; | |
| setMessages(prev => [...prev, { | |
| id: (Date.now() + 1).toString(), | |
| role: 'assistant', | |
| content: "Connection error. Please check if the backend is running.", | |
| timestamp: new Date() | |
| }]); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleKeyPress = (e: React.KeyboardEvent) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }; | |
| return ( | |
| <div className="bg-white rounded-xl border border-gray-200 flex flex-col h-[450px] shadow-sm"> | |
| {/* Header */} | |
| <div className="px-4 py-3 border-b border-gray-200 flex items-center gap-3"> | |
| <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-sm"> | |
| <Sparkles className="w-4 h-4 text-white" /> | |
| </div> | |
| <div> | |
| <h3 className="text-gray-900 font-bold text-sm uppercase tracking-wide">AI Operations Lead</h3> | |
| <p className="text-[10px] text-gray-500">Quantum Analysis Expert</p> | |
| </div> | |
| </div> | |
| {/* Messages */} | |
| <div className="flex-1 overflow-y-auto p-3 space-y-3 bg-gray-50"> | |
| {messages.map((msg) => ( | |
| <div key={msg.id} className={`flex gap-2 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}> | |
| <div className={`w-6 h-6 rounded-md flex items-center justify-center flex-shrink-0 ${ | |
| msg.role === 'user' ? 'bg-gray-900' : 'bg-gradient-to-br from-indigo-500 to-purple-600' | |
| }`}> | |
| {msg.role === 'user' ? ( | |
| <User className="w-3 h-3 text-white" /> | |
| ) : ( | |
| <Bot className="w-3 h-3 text-white" /> | |
| )} | |
| </div> | |
| <div className={`max-w-[85%] rounded-xl px-3 py-2 text-xs ${ | |
| msg.role === 'user' | |
| ? 'bg-gray-900 text-white' | |
| : 'bg-white text-gray-800 border border-gray-200 shadow-sm' | |
| }`}> | |
| {msg.content} | |
| </div> | |
| </div> | |
| ))} | |
| {isLoading && ( | |
| <div className="flex gap-2"> | |
| <div className="w-6 h-6 rounded-md bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center"> | |
| <Bot className="w-3 h-3 text-white" /> | |
| </div> | |
| <div className="bg-white rounded-xl px-3 py-2 border border-gray-200 shadow-sm"> | |
| <Loader2 className="w-4 h-4 text-indigo-500 animate-spin" /> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| {/* Input */} | |
| <div className="p-3 border-t border-gray-200 bg-white"> | |
| <div className="flex gap-2"> | |
| <input | |
| type="text" | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| onKeyPress={handleKeyPress} | |
| placeholder="Ask about fraud detection..." | |
| className="flex-1 bg-gray-100 rounded-lg px-3 py-2 text-gray-800 text-xs placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 border border-gray-200" | |
| disabled={isLoading} | |
| /> | |
| <button | |
| onClick={sendMessage} | |
| disabled={isLoading || !input.trim()} | |
| className="px-3 py-2 bg-gray-900 hover:bg-gray-800 rounded-lg disabled:opacity-50 transition-colors" | |
| > | |
| <Send className="w-4 h-4 text-white" /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }); | |
| export default ChatPanel; | |