QuantumShield / frontend /components /SimulationEngine.tsx
SantoshKumar1310's picture
Upload folder using huggingface_hub
49e53ae verified
'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;