import React, { useState, useEffect, useCallback } from 'react'; import { motion } from 'framer-motion'; import { useUserStore } from '@/store/userStore'; import apiService from '@/services/api'; import VariableControl from './VariableControl'; import { Play, RefreshCcw, Save, Upload, Copy, Undo2, Redo2, Search, Sliders, ChevronDown, ChevronRight, Zap } from 'lucide-react'; const ScenarioBuilderTab: React.FC = () => { const { isDark } = useUserStore(); const [variables, setVariables] = useState([]); const [values, setValues] = useState>({}); const [history, setHistory] = useState[]>([]); const [historyIndex, setHistoryIndex] = useState(-1); const [loading, setLoading] = useState(true); const [simulating, setSimulating] = useState(false); const [result, setResult] = useState(null); const [autoUpdate, setAutoUpdate] = useState(true); const [search, setSearch] = useState(''); const [lockedVars, setLockedVars] = useState>(new Set()); const [pinnedVars, setPinnedVars] = useState>(new Set()); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); const [scenarioName, setScenarioName] = useState(''); const [showSaveModal, setShowSaveModal] = useState(false); const cardBg = isDark ? 'rgba(255,255,255,0.03)' : '#ffffff'; const cardBorder = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)'; const textPrimary = isDark ? '#f8fafc' : '#0f172a'; const textMuted = isDark ? '#94a3b8' : '#64748b'; useEffect(() => { const fetchVars = async () => { try { const res = await apiService.getSimulatorVariables(); if (res.data) { setVariables(res.data); const init: Record = {}; res.data.forEach((v: any) => { init[v.name] = v.current_value; }); setValues(init); setHistory([init]); setHistoryIndex(0); } } catch (err) { console.error(err); } finally { setLoading(false); } }; fetchVars(); }, []); const handleChange = useCallback((name: string, val: any) => { if (lockedVars.has(name)) return; setValues(prev => { const next = { ...prev, [name]: val }; setHistory(h => [...h.slice(0, historyIndex + 1), next]); setHistoryIndex(i => i + 1); return next; }); }, [lockedVars, historyIndex]); const handleUndo = () => { if (historyIndex > 0) { setHistoryIndex(i => i - 1); setValues(history[historyIndex - 1]); } }; const handleRedo = () => { if (historyIndex < history.length - 1) { setHistoryIndex(i => i + 1); setValues(history[historyIndex + 1]); } }; const handleReset = () => { const init: Record = {}; variables.forEach((v: any) => { init[v.name] = v.current_value; }); setValues(init); setHistory(h => [...h, init]); setHistoryIndex(h => h + 1); }; const handleSimulate = async () => { setSimulating(true); try { const res = await apiService.runSimulation(values, scenarioName || undefined); setResult(res.data); } catch (err) { console.error(err); } finally { setSimulating(false); } }; const handleSave = async () => { try { await apiService.saveScenario({ name: scenarioName || `Scenario ${new Date().toLocaleTimeString()}`, variables: values, prediction: result?.prediction, confidence: result?.confidence, metrics: result?.secondary_metrics, }); setShowSaveModal(false); setScenarioName(''); } catch (err) { console.error(err); } }; useEffect(() => { if (!autoUpdate || Object.keys(values).length === 0) return; const timer = setTimeout(handleSimulate, 600); return () => clearTimeout(timer); }, [values, autoUpdate]); const groups: Record = {}; const filteredVars = variables.filter(v => v.display_name?.toLowerCase().includes(search.toLowerCase()) || v.name?.toLowerCase().includes(search.toLowerCase()) ); const pinned = filteredVars.filter(v => pinnedVars.has(v.name)); const unpinned = filteredVars.filter(v => !pinnedVars.has(v.name)); unpinned.forEach(v => { const g = v.group || 'General'; if (!groups[g]) groups[g] = []; groups[g].push(v); }); if (loading) { return
; } if (variables.length === 0) { return (

No Variables to Configure

Variables are automatically generated from your dataset columns. Upload data in the Data Hub first.

); } const targetName = result?.target_name || 'Target Metric'; return (
{/* Left: Controls */}
setSearch(e.target.value)} className="w-full pl-9 pr-3 py-2 rounded-xl text-xs border outline-none focus:ring-2 focus:ring-indigo-500/30" style={{ background: cardBg, borderColor: cardBorder, color: textPrimary }} />
{/* Variable controls */}
{pinned.length > 0 && (

📌 Pinned Variables

{pinned.map(v => ( handleChange(v.name, val)} minValue={v.min_value} maxValue={v.max_value} step={v.step} unit={v.unit} options={v.options} locked={lockedVars.has(v.name)} pinned={true} description={v.description} onLock={() => setLockedVars(prev => { const n = new Set(prev); n.has(v.name) ? n.delete(v.name) : n.add(v.name); return n; })} onPin={() => setPinnedVars(prev => { const n = new Set(prev); n.delete(v.name); return n; })} /> ))}
)} {Object.entries(groups).map(([group, vars]) => (
{!collapsedGroups.has(group) && (
{vars.map(v => ( handleChange(v.name, val)} minValue={v.min_value} maxValue={v.max_value} step={v.step} unit={v.unit} options={v.options} locked={lockedVars.has(v.name)} pinned={pinnedVars.has(v.name)} description={v.description} onLock={() => setLockedVars(prev => { const n = new Set(prev); n.has(v.name) ? n.delete(v.name) : n.add(v.name); return n; })} onPin={() => setPinnedVars(prev => { const n = new Set(prev); pinnedVars.has(v.name) ? n.delete(v.name) : n.add(v.name); return n; })} /> ))}
)}
))}
{/* Actions */}
{/* Right: Results */}
{/* Live Prediction */}

Live Prediction ({targetName})

{result && ● Live}
{result ? (

Prediction

{result.formatted_prediction || result.prediction?.toLocaleString()}

Baseline

{result.formatted_baseline || result.baseline_prediction?.toLocaleString()}

Impact

= 0 ? 'text-emerald-500' : 'text-red-500'}`}> {result.impact_percentage >= 0 ? '+' : ''}{result.impact_percentage?.toFixed(1)}%

Confidence

{result.confidence?.toFixed(1)}%

{/* Feature contributions */} {result.feature_contributions?.length > 0 && (

Feature Impact Shift

{result.feature_contributions.slice(0, 6).map((fc: any, i: number) => (
{fc.feature}
= 0 ? '#22c55e' : '#ef4444' }} />
= 0 ? 'text-emerald-500' : 'text-red-500'}`}> {fc.contribution >= 0 ? '+' : ''}{fc.contribution.toFixed(1)}%
))}
)}

Computed in {result.duration_ms}ms • {new Date().toLocaleTimeString()}

) : (

Adjust variables and run a simulation

)}
{/* Secondary Metrics */} {result?.secondary_metrics && Object.keys(result.secondary_metrics).length > 0 && (

Secondary Metrics

{Object.entries(result.secondary_metrics).map(([key, val]: [string, any]) => (

{key}

{val.formatted_simulated || val.simulated?.toLocaleString()}

= 0 ? 'text-emerald-500' : 'text-red-500'}`}> {val.impact >= 0 ? '↑' : '↓'} {Math.abs(val.impact).toFixed(1)}% vs baseline

))}
)}
{/* Save Modal */} {showSaveModal && (
setShowSaveModal(false)} />

Save Scenario

setScenarioName(e.target.value)} className="w-full px-4 py-3 rounded-xl border text-sm outline-none focus:ring-2 focus:ring-indigo-500/30 mb-4" style={{ background: cardBg, borderColor: cardBorder, color: textPrimary }} autoFocus />
)}
); }; export default ScenarioBuilderTab;