Spaces:
Build error
Build error
| 'use client'; | |
| import { useState, useEffect, useRef, useCallback, memo } from 'react'; | |
| import { Play, Pause, RotateCcw, Sliders, Zap } 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; | |
| category: string; | |
| is_fraud: number; | |
| prediction: string; | |
| final_score: number; | |
| classical_score: number; | |
| quantum_score: number; | |
| quantum_details: { | |
| vqc: number; | |
| qaoa: number; | |
| qnn: number; | |
| }; | |
| } | |
| interface Metrics { | |
| total: number; | |
| flagged: number; | |
| actual_fraud: number; | |
| accuracy: number; | |
| precision: number; | |
| recall: number; | |
| f1: number; | |
| tp: number; | |
| fp: number; | |
| tn: number; | |
| fn: number; | |
| } | |
| interface SimulationEngineProps { | |
| onTransactionUpdate: (transaction: Transaction, metrics: Metrics) => void; | |
| onRunningChange: (isRunning: boolean) => void; | |
| onThresholdChange?: (threshold: number) => void; | |
| } | |
| // Completely isolated simulation engine | |
| const SimulationEngine = memo(function SimulationEngine({ | |
| onTransactionUpdate, | |
| onRunningChange, | |
| onThresholdChange | |
| }: SimulationEngineProps) { | |
| const [isRunning, setIsRunning] = useState(false); | |
| const [speed, setSpeed] = useState(1000); | |
| const [threshold, setThreshold] = useState(0.5); | |
| const [showSettings, setShowSettings] = useState(false); | |
| // All refs to prevent any re-render issues | |
| const intervalRef = useRef<NodeJS.Timeout | null>(null); | |
| const isRunningRef = useRef(false); | |
| const speedRef = useRef(speed); | |
| const thresholdRef = useRef(threshold); | |
| const isMountedRef = useRef(true); | |
| // Stable callback refs | |
| const onTransactionUpdateRef = useRef(onTransactionUpdate); | |
| const onRunningChangeRef = useRef(onRunningChange); | |
| useEffect(() => { | |
| onTransactionUpdateRef.current = onTransactionUpdate; | |
| onRunningChangeRef.current = onRunningChange; | |
| }, [onTransactionUpdate, onRunningChange]); | |
| useEffect(() => { | |
| speedRef.current = speed; | |
| }, [speed]); | |
| useEffect(() => { | |
| thresholdRef.current = threshold; | |
| onThresholdChange?.(threshold); | |
| }, [threshold, onThresholdChange]); | |
| useEffect(() => { | |
| isMountedRef.current = true; | |
| return () => { | |
| isMountedRef.current = false; | |
| if (intervalRef.current) { | |
| clearInterval(intervalRef.current); | |
| intervalRef.current = null; | |
| } | |
| }; | |
| }, []); | |
| const processTransaction = useCallback(async () => { | |
| if (!isMountedRef.current) return; | |
| try { | |
| const response = await fetch(`${API_URL}/api/process-random?threshold=${thresholdRef.current}`); | |
| if (response.ok && isMountedRef.current) { | |
| const data = await response.json(); | |
| onTransactionUpdateRef.current(data.transaction, data.metrics); | |
| } | |
| } catch (error) { | |
| console.error('Failed to process transaction:', error); | |
| } | |
| }, []); | |
| const clearIntervalSafely = useCallback(() => { | |
| if (intervalRef.current) { | |
| clearInterval(intervalRef.current); | |
| intervalRef.current = null; | |
| } | |
| }, []); | |
| const start = useCallback(() => { | |
| clearIntervalSafely(); | |
| isRunningRef.current = true; | |
| setIsRunning(true); | |
| onRunningChangeRef.current(true); | |
| // Process first one immediately | |
| processTransaction(); | |
| // Start interval | |
| intervalRef.current = setInterval(() => { | |
| if (isRunningRef.current && isMountedRef.current) { | |
| processTransaction(); | |
| } | |
| }, speedRef.current); | |
| }, [processTransaction, clearIntervalSafely]); | |
| const stop = useCallback(() => { | |
| isRunningRef.current = false; | |
| setIsRunning(false); | |
| onRunningChangeRef.current(false); | |
| clearIntervalSafely(); | |
| }, [clearIntervalSafely]); | |
| const reset = useCallback(async () => { | |
| stop(); | |
| try { | |
| await fetch(`${API_URL}/api/reset`, { method: 'POST' }); | |
| } catch (error) { | |
| console.error('Failed to reset:', error); | |
| } | |
| }, [stop]); | |
| const handleSpeedChange = useCallback((newSpeed: number) => { | |
| setSpeed(newSpeed); | |
| speedRef.current = newSpeed; | |
| if (isRunningRef.current) { | |
| clearIntervalSafely(); | |
| intervalRef.current = setInterval(() => { | |
| if (isRunningRef.current && isMountedRef.current) { | |
| processTransaction(); | |
| } | |
| }, newSpeed); | |
| } | |
| }, [processTransaction, clearIntervalSafely]); | |
| return ( | |
| <div className="bg-white rounded-xl p-3 mb-4 border border-gray-200 shadow-sm"> | |
| <div className="flex flex-wrap items-center justify-between gap-3"> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={isRunning ? stop : start} | |
| className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium text-sm transition-all ${ | |
| isRunning | |
| ? 'bg-red-500 hover:bg-red-600 text-white' | |
| : 'bg-green-600 hover:bg-green-700 text-white' | |
| }`} | |
| > | |
| {isRunning ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />} | |
| {isRunning ? 'Pause' : 'Start'} | |
| </button> | |
| <button | |
| onClick={reset} | |
| className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-gray-700 text-sm transition-all border border-gray-200" | |
| > | |
| <RotateCcw className="w-4 h-4" /> | |
| Reset | |
| </button> | |
| <button | |
| onClick={() => setShowSettings(!showSettings)} | |
| className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all border ${ | |
| showSettings | |
| ? 'bg-indigo-100 text-indigo-700 border-indigo-200' | |
| : 'bg-gray-100 hover:bg-gray-200 text-gray-700 border-gray-200' | |
| }`} | |
| > | |
| <Sliders className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <span className="text-gray-500 text-sm">Speed:</span> | |
| <select | |
| value={speed} | |
| onChange={(e) => handleSpeedChange(Number(e.target.value))} | |
| className="bg-gray-100 border border-gray-200 rounded-lg px-3 py-1.5 text-gray-700 text-sm focus:ring-1 focus:ring-gray-400 focus:outline-none" | |
| > | |
| <option value={2000}>Slow</option> | |
| <option value={1000}>Normal</option> | |
| <option value={500}>Fast</option> | |
| <option value={200}>Very Fast</option> | |
| </select> | |
| <div className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm ${ | |
| isRunning ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500' | |
| }`}> | |
| <div className={`w-2 h-2 rounded-full ${ | |
| isRunning ? 'bg-green-500 animate-pulse' : 'bg-gray-400' | |
| }`} /> | |
| {isRunning ? 'Running' : 'Stopped'} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Settings Panel */} | |
| {showSettings && ( | |
| <div className="mt-3 pt-3 border-t border-gray-200"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| {/* Threshold Control */} | |
| <div className="bg-gray-50 rounded-lg p-3"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <label className="text-sm font-medium text-gray-700 flex items-center gap-2"> | |
| <Zap className="w-4 h-4 text-amber-500" /> | |
| Fraud Threshold | |
| </label> | |
| <span className={`text-sm font-bold ${ | |
| threshold < 0.4 ? 'text-red-600' : threshold > 0.6 ? 'text-green-600' : 'text-amber-600' | |
| }`}> | |
| {(threshold * 100).toFixed(0)}% | |
| </span> | |
| </div> | |
| <input | |
| type="range" | |
| min="0.1" | |
| max="0.9" | |
| step="0.05" | |
| value={threshold} | |
| onChange={(e) => setThreshold(Number(e.target.value))} | |
| className="w-full h-2 bg-gradient-to-r from-green-400 via-amber-400 to-red-400 rounded-lg appearance-none cursor-pointer" | |
| /> | |
| <div className="flex justify-between text-[10px] text-gray-500 mt-1"> | |
| <span>More Sensitive</span> | |
| <span>Less Sensitive</span> | |
| </div> | |
| <p className="text-[10px] text-gray-500 mt-2"> | |
| {threshold < 0.4 | |
| ? '⚠️ Low threshold: More fraud flagged, higher false positives' | |
| : threshold > 0.6 | |
| ? '✅ High threshold: Fewer flags, may miss some fraud' | |
| : '⚖️ Balanced threshold: Good precision/recall trade-off'} | |
| </p> | |
| </div> | |
| {/* Model Weights Info */} | |
| <div className="bg-gradient-to-br from-indigo-50 to-purple-50 rounded-lg p-3 border border-indigo-100"> | |
| <label className="text-sm font-medium text-gray-700 mb-2 block">Hybrid Model Weights</label> | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-xs text-gray-600">Classical (XGBoost)</span> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-24 h-2 bg-gray-200 rounded-full overflow-hidden"> | |
| <div className="h-full bg-gray-700 rounded-full" style={{ width: '80%' }} /> | |
| </div> | |
| <span className="text-xs font-medium text-gray-700">80%</span> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-xs text-gray-600">Quantum Ensemble</span> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-24 h-2 bg-gray-200 rounded-full overflow-hidden"> | |
| <div className="h-full bg-indigo-500 rounded-full" style={{ width: '20%' }} /> | |
| </div> | |
| <span className="text-xs font-medium text-indigo-600">20%</span> | |
| </div> | |
| </div> | |
| </div> | |
| <p className="text-[10px] text-indigo-600 mt-2"> | |
| ⚛️ VQC (40%) + QAOA (30%) + QNN (30%) | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }); | |
| export default SimulationEngine; | |