SantoshKumar1310's picture
Upload folder using huggingface_hub
49e53ae verified
'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;