MuazTPM commited on
Commit
c865cd8
Β·
1 Parent(s): f37e9bf

fix(ui): patch trim typeerror, globe invisible arcs, and leaflet latency

Browse files
frontend/app/page.module.css CHANGED
@@ -28,6 +28,7 @@
28
  display: flex;
29
  align-items: center;
30
  gap: 10px;
 
31
  }
32
 
33
  .headerSep {
@@ -44,7 +45,6 @@
44
  }
45
 
46
  .headerCenter {
47
- flex: 1;
48
  display: flex;
49
  justify-content: center;
50
  padding: 0 20px;
@@ -53,7 +53,9 @@
53
  .headerRight {
54
  display: flex;
55
  align-items: center;
 
56
  gap: 12px;
 
57
  }
58
 
59
  /* ── Main Content ─────────────────────────── */
@@ -86,13 +88,23 @@
86
  }
87
 
88
  @keyframes pageFadeIn {
89
- from { opacity: 0; }
90
- to { opacity: 1; }
 
 
 
 
 
91
  }
92
 
93
  @keyframes pageFadeOut {
94
- from { opacity: 1; }
95
- to { opacity: 0; }
 
 
 
 
 
96
  }
97
 
98
  @keyframes pageSlideIn {
@@ -101,10 +113,12 @@
101
  transform: scale(1.05);
102
  filter: brightness(2);
103
  }
 
104
  30% {
105
  opacity: 1;
106
  filter: brightness(1.5);
107
  }
 
108
  to {
109
  opacity: 1;
110
  transform: scale(1);
@@ -123,4 +137,4 @@
123
  color: var(--nx-muted);
124
  font-size: 12px;
125
  letter-spacing: 0.06em;
126
- }
 
28
  display: flex;
29
  align-items: center;
30
  gap: 10px;
31
+ flex: 1;
32
  }
33
 
34
  .headerSep {
 
45
  }
46
 
47
  .headerCenter {
 
48
  display: flex;
49
  justify-content: center;
50
  padding: 0 20px;
 
53
  .headerRight {
54
  display: flex;
55
  align-items: center;
56
+ justify-content: flex-end;
57
  gap: 12px;
58
+ flex: 1;
59
  }
60
 
61
  /* ── Main Content ─────────────────────────── */
 
88
  }
89
 
90
  @keyframes pageFadeIn {
91
+ from {
92
+ opacity: 0;
93
+ }
94
+
95
+ to {
96
+ opacity: 1;
97
+ }
98
  }
99
 
100
  @keyframes pageFadeOut {
101
+ from {
102
+ opacity: 1;
103
+ }
104
+
105
+ to {
106
+ opacity: 0;
107
+ }
108
  }
109
 
110
  @keyframes pageSlideIn {
 
113
  transform: scale(1.05);
114
  filter: brightness(2);
115
  }
116
+
117
  30% {
118
  opacity: 1;
119
  filter: brightness(1.5);
120
  }
121
+
122
  to {
123
  opacity: 1;
124
  transform: scale(1);
 
137
  color: var(--nx-muted);
138
  font-size: 12px;
139
  letter-spacing: 0.06em;
140
+ }
frontend/app/page.tsx CHANGED
@@ -13,183 +13,180 @@ import type { ViewState, ReplayStep } from '@/lib/types';
13
 
14
  // Dynamic import for map (no SSR)
15
  const GlobalThreatMap = dynamic(
16
- () => import('@/components/globe/GlobalThreatMap'),
17
- { ssr: false, loading: () => <div className={styles.mapLoading}>&gt; INITIALIZING MAP ENGINE...</div> }
18
  );
19
 
20
  export default function Home() {
21
- const [view, setView] = useState<ViewState>('globe');
22
- const [mode, setMode] = useState<'live' | 'replay'>('replay');
23
- const [backendStatus, setBackendStatus] = useState<'connected' | 'replay' | 'disconnected'>('replay');
24
-
25
- // Replay state
26
- const replayRef = useRef<ReplayEngine | null>(null);
27
- const [replaySteps, setReplaySteps] = useState<ReplayStep[]>([]);
28
- const [currentStepIndex, setCurrentStepIndex] = useState(-1);
29
- const [isPlaying, setIsPlaying] = useState(false);
30
- const [speed, setSpeed] = useState(1);
31
- const [replayLoaded, setReplayLoaded] = useState(false);
32
- const [allSteps, setAllSteps] = useState<ReplayStep[]>([]);
33
-
34
- // Transition state
35
- const [transitioning, setTransitioning] = useState(false);
36
-
37
- // Check backend availability
38
- useEffect(() => {
39
- isBackendAvailable().then(available => {
40
- setBackendStatus(available ? 'connected' : 'replay');
41
- setMode(available ? 'live' : 'replay');
42
- });
43
- }, []);
44
-
45
- // Initialize replay engine
46
- useEffect(() => {
47
- const engine = new ReplayEngine();
48
- replayRef.current = engine;
49
-
50
- engine.load().then(() => {
51
- setAllSteps(engine.getSteps());
52
- setReplayLoaded(true);
53
- });
54
-
55
- const unsub = engine.subscribe((step, index) => {
56
- setCurrentStepIndex(index);
57
- setReplaySteps(engine.getSteps().slice(0, index + 1));
58
- });
59
-
60
- return () => unsub();
61
- }, []);
62
-
63
- // Handle corridor/hub clicks
64
- const handleCorridorSelect = useCallback((corridorId: string) => {
65
- setTransitioning(true);
66
- setTimeout(() => {
67
- setView('case');
68
- setTransitioning(false);
69
- // Start replay
70
- if (replayRef.current && allSteps.length > 0) {
71
- replayRef.current.seekTo(0);
72
- setCurrentStepIndex(0);
73
- setReplaySteps(allSteps.slice(0, 1));
74
- }
75
- }, 800);
76
- }, [allSteps]);
77
-
78
- const handleHubSelect = useCallback((hubId: string) => {
79
- handleCorridorSelect(hubId);
80
- }, [handleCorridorSelect]);
81
-
82
- // Replay controls
83
- const handlePlay = useCallback(() => {
84
- replayRef.current?.play();
85
- setIsPlaying(true);
86
- }, []);
87
-
88
- const handlePause = useCallback(() => {
89
- replayRef.current?.pause();
90
- setIsPlaying(false);
91
- }, []);
92
-
93
- const handleStepForward = useCallback(() => {
94
- replayRef.current?.stepForward();
95
- }, []);
96
-
97
- const handleStepBack = useCallback(() => {
98
- replayRef.current?.stepBack();
99
- }, []);
100
-
101
- const handleSeek = useCallback((step: number) => {
102
- replayRef.current?.seekTo(step);
103
- }, []);
104
-
105
- const handleSpeedChange = useCallback((newSpeed: number) => {
106
- replayRef.current?.setSpeed(newSpeed);
107
- setSpeed(newSpeed);
108
- }, []);
109
-
110
- const handleBackToMap = useCallback(() => {
111
- replayRef.current?.pause();
112
- setIsPlaying(false);
113
- setTransitioning(true);
114
- setTimeout(() => {
115
- setView('globe');
116
- setTransitioning(false);
117
- }, 600);
118
- }, []);
119
-
120
- return (
121
- <div className={styles.app}>
122
- {/* Global Header */}
123
- <header className={styles.header}>
124
- <div className={styles.headerLeft}>
125
- <MemexLogo size="sm" />
126
- <span className={styles.headerSep}>β”‚</span>
127
- {view === 'case' ? (
128
- <>
129
- <button className="nx-btn nx-btn-icon" onClick={handleBackToMap} style={{ padding: '4px 8px', fontSize: '10px' }}>
130
- ← MAP
131
- </button>
132
- <span className={styles.headerSep}>β”‚</span>
133
- <div style={{ display: 'flex', flexDirection: 'column', cursor: 'pointer' }}>
134
- <span style={{ fontSize: '11px', fontWeight: 700, color: 'var(--nx-orange)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
135
- {replayRef.current?.getMeta()?.alert?.alert_id || 'CASE-1MDB'}
136
- </span>
137
- <span style={{ fontSize: '9px', color: 'var(--nx-muted)', textTransform: 'uppercase', letterSpacing: '0.12em' }}>
138
- Financial Crime Intelligence
139
- </span>
140
- </div>
141
- </>
142
- ) : (
143
- <span className={styles.headerSubtitle}>FINANCIAL CRIME INTELLIGENCE</span>
144
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  </div>
146
- <div className={styles.headerCenter}>
147
- {view === 'case' && replayLoaded && (
148
- <ReplayControls
149
- currentStep={Math.max(0, currentStepIndex)}
150
- totalSteps={allSteps.length}
151
- isPlaying={isPlaying}
152
- speed={speed}
153
- onPlay={handlePlay}
154
- onPause={handlePause}
155
- onStepForward={handleStepForward}
156
- onStepBack={handleStepBack}
157
- onSeek={handleSeek}
158
- onSpeedChange={handleSpeedChange}
159
- />
160
- )}
161
- </div>
162
- <div className={styles.headerRight}>
163
- <StatusIndicator
164
- status={backendStatus}
165
- label={backendStatus === 'connected' ? 'LIVE' : backendStatus === 'replay' ? 'REPLAY' : 'OFFLINE'}
166
- />
167
- </div>
168
- </header>
169
-
170
- {/* View Content */}
171
- <main className={`${styles.main} ${transitioning ? styles.transitioning : ''}`}>
172
- {view === 'globe' && (
173
- <div className={`${styles.viewContainer} ${transitioning ? styles.viewFadeOut : styles.viewFadeIn}`}>
174
- <GlobalThreatMap
175
- onCorridorSelect={handleCorridorSelect}
176
- onHubSelect={handleHubSelect}
177
- />
178
- </div>
179
- )}
180
-
181
- {view === 'case' && (
182
- <div className={`${styles.viewContainer} ${transitioning ? styles.viewFadeOut : styles.viewSlideIn}`}>
183
- <CaseTerminal
184
- steps={replaySteps}
185
- currentStepIndex={Math.max(0, currentStepIndex)}
186
- onBack={handleBackToMap}
187
- alertSummary={replayRef.current?.getMeta()?.alert?.summary}
188
- caseId={replayRef.current?.getMeta()?.alert?.alert_id}
189
- />
190
- </div>
191
- )}
192
- </main>
193
- </div>
194
- );
195
  }
 
13
 
14
  // Dynamic import for map (no SSR)
15
  const GlobalThreatMap = dynamic(
16
+ () => import('@/components/globe/GlobalThreatMap'),
17
+ { ssr: false, loading: () => <div className={styles.mapLoading}>&gt; INITIALIZING MAP ENGINE...</div> }
18
  );
19
 
20
  export default function Home() {
21
+ const [view, setView] = useState<ViewState>('globe');
22
+ const [mode, setMode] = useState<'live' | 'replay'>('replay');
23
+ const [backendStatus, setBackendStatus] = useState<'connected' | 'replay' | 'disconnected'>('replay');
24
+
25
+ // Replay state
26
+ const replayRef = useRef<ReplayEngine | null>(null);
27
+ const [replaySteps, setReplaySteps] = useState<ReplayStep[]>([]);
28
+ const [currentStepIndex, setCurrentStepIndex] = useState(-1);
29
+ const [isPlaying, setIsPlaying] = useState(false);
30
+ const [speed, setSpeed] = useState(1);
31
+ const [replayLoaded, setReplayLoaded] = useState(false);
32
+ const [allSteps, setAllSteps] = useState<ReplayStep[]>([]);
33
+
34
+ // Transition state
35
+ const [transitioning, setTransitioning] = useState(false);
36
+
37
+ // Check backend availability
38
+ useEffect(() => {
39
+ isBackendAvailable().then(available => {
40
+ setBackendStatus(available ? 'connected' : 'replay');
41
+ setMode(available ? 'live' : 'replay');
42
+ });
43
+ }, []);
44
+
45
+ // Initialize replay engine
46
+ useEffect(() => {
47
+ const engine = new ReplayEngine();
48
+ replayRef.current = engine;
49
+
50
+ engine.load().then(() => {
51
+ setAllSteps(engine.getSteps());
52
+ setReplayLoaded(true);
53
+ });
54
+
55
+ const unsub = engine.subscribe((step, index) => {
56
+ setCurrentStepIndex(index);
57
+ setReplaySteps(engine.getSteps().slice(0, index + 1));
58
+ });
59
+
60
+ return () => unsub();
61
+ }, []);
62
+
63
+ // Handle corridor/hub clicks
64
+ const handleCorridorSelect = useCallback((corridorId: string) => {
65
+ setTransitioning(true);
66
+ setTimeout(() => {
67
+ setView('case');
68
+ setTransitioning(false);
69
+ // Start replay
70
+ if (replayRef.current && allSteps.length > 0) {
71
+ replayRef.current.seekTo(0);
72
+ setCurrentStepIndex(0);
73
+ setReplaySteps(allSteps.slice(0, 1));
74
+ }
75
+ }, 800);
76
+ }, [allSteps]);
77
+
78
+ const handleHubSelect = useCallback((hubId: string) => {
79
+ handleCorridorSelect(hubId);
80
+ }, [handleCorridorSelect]);
81
+
82
+ // Replay controls
83
+ const handlePlay = useCallback(() => {
84
+ replayRef.current?.play();
85
+ setIsPlaying(true);
86
+ }, []);
87
+
88
+ const handlePause = useCallback(() => {
89
+ replayRef.current?.pause();
90
+ setIsPlaying(false);
91
+ }, []);
92
+
93
+ const handleStepForward = useCallback(() => {
94
+ replayRef.current?.stepForward();
95
+ }, []);
96
+
97
+ const handleStepBack = useCallback(() => {
98
+ replayRef.current?.stepBack();
99
+ }, []);
100
+
101
+ const handleSeek = useCallback((step: number) => {
102
+ replayRef.current?.seekTo(step);
103
+ }, []);
104
+
105
+ const handleSpeedChange = useCallback((newSpeed: number) => {
106
+ replayRef.current?.setSpeed(newSpeed);
107
+ setSpeed(newSpeed);
108
+ }, []);
109
+
110
+ const handleBackToMap = useCallback(() => {
111
+ replayRef.current?.pause();
112
+ setIsPlaying(false);
113
+ setTransitioning(true);
114
+ setTimeout(() => {
115
+ setView('globe');
116
+ setTransitioning(false);
117
+ }, 600);
118
+ }, []);
119
+
120
+ return (
121
+ <div className={styles.app}>
122
+ {/* Global Header */}
123
+ <header className={styles.header}>
124
+ <div className={styles.headerLeft}>
125
+ <MemexLogo size="sm" />
126
+ <span className={styles.headerSep}>β”‚</span>
127
+ {view === 'case' ? (
128
+ <>
129
+ <button className="nx-btn nx-btn-icon" onClick={handleBackToMap} style={{ padding: '4px 8px', fontSize: '10px' }}>
130
+ ← MAP
131
+ </button>
132
+ <span className={styles.headerSep}>β”‚</span>
133
+ <div style={{ display: 'flex', flexDirection: 'column', cursor: 'pointer' }}>
134
+ <span style={{ fontSize: '11px', fontWeight: 700, color: 'var(--nx-orange)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
135
+ {replayRef.current?.getMeta()?.alert?.alert_id || 'CASE-1MDB'}
136
+ </span>
137
+ </div>
138
+ </>
139
+ ) : (
140
+ <span className={styles.headerSubtitle}>FINANCIAL CRIME INTELLIGENCE</span>
141
+ )}
142
+ </div>
143
+ <div className={styles.headerCenter}>
144
+ {view === 'case' && replayLoaded && (
145
+ <ReplayControls
146
+ currentStep={Math.max(0, currentStepIndex)}
147
+ totalSteps={allSteps.length}
148
+ isPlaying={isPlaying}
149
+ speed={speed}
150
+ onPlay={handlePlay}
151
+ onPause={handlePause}
152
+ onStepForward={handleStepForward}
153
+ onStepBack={handleStepBack}
154
+ onSeek={handleSeek}
155
+ onSpeedChange={handleSpeedChange}
156
+ />
157
+ )}
158
+ </div>
159
+ <div className={styles.headerRight}>
160
+ <StatusIndicator
161
+ status={backendStatus}
162
+ label={backendStatus === 'connected' ? 'LIVE' : backendStatus === 'replay' ? 'REPLAY' : 'OFFLINE'}
163
+ />
164
+ </div>
165
+ </header>
166
+
167
+ {/* View Content */}
168
+ <main className={`${styles.main} ${transitioning ? styles.transitioning : ''}`}>
169
+ {view === 'globe' && (
170
+ <div className={`${styles.viewContainer} ${transitioning ? styles.viewFadeOut : styles.viewFadeIn}`}>
171
+ <GlobalThreatMap
172
+ onCorridorSelect={handleCorridorSelect}
173
+ onHubSelect={handleHubSelect}
174
+ />
175
+ </div>
176
+ )}
177
+
178
+ {view === 'case' && (
179
+ <div className={`${styles.viewContainer} ${transitioning ? styles.viewFadeOut : styles.viewSlideIn}`}>
180
+ <CaseTerminal
181
+ steps={replaySteps}
182
+ currentStepIndex={Math.max(0, currentStepIndex)}
183
+ onBack={handleBackToMap}
184
+ alertSummary={replayRef.current?.getMeta()?.alert?.summary}
185
+ caseId={replayRef.current?.getMeta()?.alert?.alert_id}
186
+ />
187
+ </div>
188
+ )}
189
+ </main>
190
  </div>
191
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  }
frontend/components/case/AgentTerminal.tsx CHANGED
@@ -5,122 +5,117 @@ import styles from './case.module.css';
5
  import type { ReplayStep } from '@/lib/types';
6
 
7
  interface Props {
8
- steps: ReplayStep[];
9
  }
10
 
11
  const TOOL_ICONS: Record<string, string> = {
12
- review_alert: 'πŸ””',
13
- get_customer_profile: 'πŸ‘€',
14
- query_transactions: 'πŸ’³',
15
- check_watchlist: 'πŸ”',
16
- trace_network: 'πŸ•Έ',
17
- check_source_of_funds: 'πŸ’°',
18
- write_to_case_file: 'πŸ’Ύ',
19
- update_system_prompt: 'βš™',
20
- search_compliance_manual: 'πŸ“‹',
21
- request_wire_trace: 'πŸ“‘',
22
- retrieve_async_result: 'πŸ“₯',
23
- file_sar: '🚨',
24
- close_alert: 'βœ…',
25
  };
26
 
27
  export default function AgentTerminal({ steps }: Props) {
28
- const scrollRef = useRef<HTMLDivElement>(null);
29
 
30
- useEffect(() => {
31
- if (scrollRef.current) {
32
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
33
- }
34
- }, [steps.length]);
35
 
36
- return (
37
- <div className={styles.terminalContainer}>
38
- <div className="nx-panel-header">
39
- <span className="nx-panel-title">AGENT TERMINAL</span>
40
- <span className="nx-panel-badge nx-tag--orange">{steps.length} ACTIONS</span>
41
- </div>
 
 
 
 
 
42
 
43
- <div className={styles.terminalBody} ref={scrollRef}>
44
- {/* Initial prompt */}
45
- <div className={styles.termEntry}>
46
- <div className={styles.termPrompt}>
47
- <span className={styles.termPS1}>memex@case:~$</span>
48
- <span className={styles.termCmd}>init_investigation</span>
49
- </div>
50
- <div className={styles.termOutput}>&gt; Environment loaded. Awaiting agent actions.</div>
51
- </div>
52
 
53
- {/* Step entries */}
54
- {steps.map((step) => {
55
- const icon = TOOL_ICONS[step.action.tool] || 'β–Έ';
56
- const reward = step.observation.reward ?? 0;
57
- const rewardColor = reward >= 0.05 ? '#22C55E' : reward < 0 ? '#D4334A' : '#505055';
 
 
 
 
 
 
 
58
 
59
- return (
60
- <div
61
- key={step.step_number}
62
- className={`${styles.termEntry} ${step.observation.done ? styles.termEntryFinal : ''}`}
63
- >
64
- {/* Step header */}
65
- <div className={styles.termStepHeader}>
66
- <span className={styles.termStepNum}>STEP {String(step.step_number).padStart(2, '0')}</span>
67
- <span className={styles.termReward} style={{ color: rewardColor }}>
68
- {reward >= 0 ? '+' : ''}{reward.toFixed(2)}
69
- </span>
70
- </div>
71
 
72
- {/* Reasoning */}
73
- {step.reasoning && (
74
- <div className={styles.termReasoning}>
75
- <span className={styles.termReasonLabel}>REASONING</span>
76
- <p className={styles.termReasonText}>{step.reasoning}</p>
77
- </div>
78
- )}
79
 
80
- {/* Tool call */}
81
- <div className={styles.termPrompt}>
82
- <span className={styles.termPS1}>memex@case:~$</span>
83
- <span className={styles.termCmd}>
84
- {icon} {step.action.tool}
85
- </span>
86
- </div>
 
 
 
 
 
 
87
 
88
- {/* Parameters */}
89
- {Object.keys(step.action.parameters).length > 0 && (
90
- <div className={styles.termParams}>
91
- {Object.entries(step.action.parameters).map(([key, val]) => (
92
- <div key={key} className={styles.termParam}>
93
- <span className={styles.termParamKey}>{key}:</span>
94
- <span className={styles.termParamVal}>
95
- {typeof val === 'object' ? JSON.stringify(val) : String(val)}
96
- </span>
97
- </div>
98
- ))}
99
- </div>
100
- )}
101
 
102
- {/* Output */}
103
- <div className={styles.termOutput}>
104
- &gt; {step.observation.message}
105
- </div>
 
 
 
 
 
 
106
 
107
- {/* Terminal marker for final step */}
108
- {step.observation.done && (
109
- <div className={styles.termFinal}>
110
- <span className={styles.termFinalIcon}>β–ˆ</span>
111
- EPISODE COMPLETE β€” FINAL SCORE: {(step.observation.reward ?? 0) >= 0 ? '+' : ''}{(step.observation.reward ?? 0).toFixed(4)}
112
  </div>
113
- )}
114
  </div>
115
- );
116
- })}
117
-
118
- {/* Cursor blink */}
119
- <div className={styles.termCursor}>
120
- <span className={styles.termPS1}>memex@case:~$</span>
121
- <span className={styles.termBlink}>β–ˆ</span>
122
  </div>
123
- </div>
124
- </div>
125
- );
126
  }
 
5
  import type { ReplayStep } from '@/lib/types';
6
 
7
  interface Props {
8
+ steps: ReplayStep[];
9
  }
10
 
11
  const TOOL_ICONS: Record<string, string> = {
12
+ review_alert: 'πŸ””',
13
+ get_customer_profile: 'πŸ‘€',
14
+ query_transactions: 'πŸ’³',
15
+ check_watchlist: 'πŸ”',
16
+ trace_network: 'πŸ•Έ',
17
+ check_source_of_funds: 'πŸ’°',
18
+ write_to_case_file: 'πŸ’Ύ',
19
+ update_system_prompt: 'βš™',
20
+ search_compliance_manual: 'πŸ“‹',
21
+ request_wire_trace: 'πŸ“‘',
22
+ retrieve_async_result: 'πŸ“₯',
23
+ file_sar: '🚨',
24
+ close_alert: 'βœ…',
25
  };
26
 
27
  export default function AgentTerminal({ steps }: Props) {
28
+ const scrollRef = useRef<HTMLDivElement>(null);
29
 
30
+ useEffect(() => {
31
+ if (scrollRef.current) {
32
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
33
+ }
34
+ }, [steps.length]);
35
 
36
+ return (
37
+ <div className={styles.terminalContainer}>
38
+ <div className={styles.terminalBody} ref={scrollRef}>
39
+ {/* Initial prompt */}
40
+ <div className={styles.termEntry}>
41
+ <div className={styles.termPrompt}>
42
+ <span className={styles.termPS1}>memex@case:~$</span>
43
+ <span className={styles.termCmd}>init_investigation</span>
44
+ </div>
45
+ <div className={styles.termOutput}>&gt; Environment loaded. Awaiting agent actions.</div>
46
+ </div>
47
 
48
+ {/* Step entries */}
49
+ {steps.map((step) => {
50
+ const icon = TOOL_ICONS[step.action.tool] || 'β–Έ';
51
+ const reward = step.observation.reward ?? 0;
52
+ const rewardColor = reward >= 0.05 ? '#22C55E' : reward < 0 ? '#D4334A' : '#505055';
 
 
 
 
53
 
54
+ return (
55
+ <div
56
+ key={step.step_number}
57
+ className={`${styles.termEntry} ${step.observation.done ? styles.termEntryFinal : ''}`}
58
+ >
59
+ {/* Step header */}
60
+ <div className={styles.termStepHeader}>
61
+ <span className={styles.termStepNum}>STEP {String(step.step_number).padStart(2, '0')}</span>
62
+ <span className={styles.termReward} style={{ color: rewardColor }}>
63
+ {reward >= 0 ? '+' : ''}{reward.toFixed(2)}
64
+ </span>
65
+ </div>
66
 
67
+ {/* Reasoning */}
68
+ {step.reasoning && (
69
+ <div className={styles.termReasoning}>
70
+ <span className={styles.termReasonLabel}>REASONING</span>
71
+ <p className={styles.termReasonText}>{step.reasoning}</p>
72
+ </div>
73
+ )}
 
 
 
 
 
74
 
75
+ {/* Tool call */}
76
+ <div className={styles.termPrompt}>
77
+ <span className={styles.termPS1}>memex@case:~$</span>
78
+ <span className={styles.termCmd}>
79
+ {icon} {step.action.tool}
80
+ </span>
81
+ </div>
82
 
83
+ {/* Parameters */}
84
+ {Object.keys(step.action.parameters).length > 0 && (
85
+ <div className={styles.termParams}>
86
+ {Object.entries(step.action.parameters).map(([key, val]) => (
87
+ <div key={key} className={styles.termParam}>
88
+ <span className={styles.termParamKey}>{key}:</span>
89
+ <span className={styles.termParamVal}>
90
+ {typeof val === 'object' ? JSON.stringify(val) : String(val)}
91
+ </span>
92
+ </div>
93
+ ))}
94
+ </div>
95
+ )}
96
 
97
+ {/* Output */}
98
+ <div className={styles.termOutput}>
99
+ &gt; {step.observation.message}
100
+ </div>
 
 
 
 
 
 
 
 
 
101
 
102
+ {/* Terminal marker for final step */}
103
+ {step.observation.done && (
104
+ <div className={styles.termFinal}>
105
+ <span className={styles.termFinalIcon}>β–ˆ</span>
106
+ EPISODE COMPLETE β€” FINAL SCORE: {(step.observation.reward ?? 0) >= 0 ? '+' : ''}{(step.observation.reward ?? 0).toFixed(4)}
107
+ </div>
108
+ )}
109
+ </div>
110
+ );
111
+ })}
112
 
113
+ {/* Cursor blink */}
114
+ <div className={styles.termCursor}>
115
+ <span className={styles.termPS1}>memex@case:~$</span>
116
+ <span className={styles.termBlink}>β–ˆ</span>
 
117
  </div>
 
118
  </div>
 
 
 
 
 
 
 
119
  </div>
120
+ );
 
 
121
  }
frontend/components/case/EntityGraph.tsx CHANGED
@@ -104,7 +104,7 @@ export default function EntityGraph({ graphData }: Props) {
104
  'shape': (ele: any) => NODE_SHAPES[ele.data('type')] || 'ellipse',
105
  'color': '#D4D4D4',
106
  'font-size': '9px',
107
- 'font-family': '"JetBrains Mono", monospace',
108
  'text-valign': 'bottom',
109
  'text-margin-y': 6,
110
  'text-outline-width': 4,
 
104
  'shape': (ele: any) => NODE_SHAPES[ele.data('type')] || 'ellipse',
105
  'color': '#D4D4D4',
106
  'font-size': '9px',
107
+ 'font-family': 'JetBrains Mono, monospace',
108
  'text-valign': 'bottom',
109
  'text-margin-y': 6,
110
  'text-outline-width': 4,
frontend/components/globe/GlobalThreatMap.tsx CHANGED
@@ -138,13 +138,10 @@ export default function GlobalThreatMap({ onCorridorSelect, onHubSelect }: Props
138
  const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
139
  svgEl.setAttribute('class', styles.arcOverlay);
140
  svgEl.style.position = 'absolute';
141
- svgEl.style.top = '0';
142
- svgEl.style.left = '0';
143
- svgEl.style.width = '100%';
144
- svgEl.style.height = '100%';
145
  svgEl.style.pointerEvents = 'none';
146
  svgEl.style.zIndex = '400';
147
- map.getContainer().appendChild(svgEl);
148
  svgOverlay.current = svgEl;
149
 
150
  // Pre-create glow filter defs
@@ -200,8 +197,7 @@ export default function GlobalThreatMap({ onCorridorSelect, onHubSelect }: Props
200
  const drawArcs = () => {
201
  if (!svgOverlay.current || !mapInstance.current) return;
202
  const svg = svgOverlay.current;
203
- const mapSize = mapInstance.current.getSize();
204
- svg.setAttribute('viewBox', `0 0 ${mapSize.x} ${mapSize.y}`);
205
 
206
  const defsEl = svg.querySelector('defs');
207
  while (svg.lastChild && svg.lastChild !== defsEl) {
@@ -213,8 +209,8 @@ export default function GlobalThreatMap({ onCorridorSelect, onHubSelect }: Props
213
  const tgt = getHubById(corridor.target);
214
  if (!src || !tgt) return;
215
 
216
- const p1 = mapInstance.current!.latLngToContainerPoint([src.lat, src.lng]);
217
- const p2 = mapInstance.current!.latLngToContainerPoint([tgt.lat, tgt.lng]);
218
 
219
  const midX = (p1.x + p2.x) / 2;
220
  const midY = (p1.y + p2.y) / 2;
@@ -278,10 +274,10 @@ export default function GlobalThreatMap({ onCorridorSelect, onHubSelect }: Props
278
  rafId.current = requestAnimationFrame(drawArcs);
279
  };
280
 
281
- map.on('move', scheduleRedraw);
282
- map.on('zoom', scheduleRedraw);
283
- map.on('moveend', scheduleRedraw);
284
  map.on('zoomend', scheduleRedraw);
 
285
  setTimeout(drawArcs, 200);
286
  setMapReady(true);
287
  };
 
138
  const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
139
  svgEl.setAttribute('class', styles.arcOverlay);
140
  svgEl.style.position = 'absolute';
141
+ svgEl.style.overflow = 'visible';
 
 
 
142
  svgEl.style.pointerEvents = 'none';
143
  svgEl.style.zIndex = '400';
144
+ map.getPanes().overlayPane.appendChild(svgEl);
145
  svgOverlay.current = svgEl;
146
 
147
  // Pre-create glow filter defs
 
197
  const drawArcs = () => {
198
  if (!svgOverlay.current || !mapInstance.current) return;
199
  const svg = svgOverlay.current;
200
+ // Don't need viewBox because the svg is simply a container with overflow:visible in the overlayPane
 
201
 
202
  const defsEl = svg.querySelector('defs');
203
  while (svg.lastChild && svg.lastChild !== defsEl) {
 
209
  const tgt = getHubById(corridor.target);
210
  if (!src || !tgt) return;
211
 
212
+ const p1 = mapInstance.current!.latLngToLayerPoint([src.lat, src.lng]);
213
+ const p2 = mapInstance.current!.latLngToLayerPoint([tgt.lat, tgt.lng]);
214
 
215
  const midX = (p1.x + p2.x) / 2;
216
  const midY = (p1.y + p2.y) / 2;
 
274
  rafId.current = requestAnimationFrame(drawArcs);
275
  };
276
 
277
+ // Since the SVG is in the overlay pane, it will naturally pan and zoom!
278
+ // We only need to redraw when the scale changes significantly, or layout changes
 
279
  map.on('zoomend', scheduleRedraw);
280
+ map.on('moveend', scheduleRedraw);
281
  setTimeout(drawArcs, 200);
282
  setMapReady(true);
283
  };
frontend/components/ui/ReplayControls.tsx CHANGED
@@ -29,38 +29,43 @@ export default function ReplayControls({
29
 
30
  return (
31
  <div className={styles.replayBar}>
32
- <button className={styles.replayBtn} onClick={onStepBack} title="Step Back">β—„</button>
33
- <button
34
- className={`${styles.replayBtn} ${isPlaying ? styles.replayBtnActive : ''}`}
35
- onClick={isPlaying ? onPause : onPlay}
36
- title={isPlaying ? 'Pause' : 'Play'}
37
- >
38
- {isPlaying ? '⏸' : 'β–Ά'}
39
- </button>
40
- <button className={styles.replayBtn} onClick={onStepForward} title="Step Forward">β–Ί</button>
 
 
41
 
42
- <input
43
- type="range"
44
- className={styles.replayScrubber}
45
- min={0}
46
- max={Math.max(totalSteps - 1, 0)}
47
- value={currentStep}
48
- onChange={e => onSeek(parseInt(e.target.value))}
49
- />
50
-
51
- <span className={styles.replayStep}>
52
- {String(currentStep + 1).padStart(2, '0')}/{String(totalSteps).padStart(2, '0')}
53
- </span>
54
-
55
- {speeds.map(s => (
56
  <button
57
- key={s}
58
- className={`${styles.replayBtn} ${speed === s ? styles.replayBtnActive : ''}`}
59
- onClick={() => onSpeedChange(s)}
60
  >
61
- {s}x
62
  </button>
63
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  </div>
65
  );
66
  }
 
29
 
30
  return (
31
  <div className={styles.replayBar}>
32
+ <div className={styles.replayGroup}>
33
+ {speeds.map(s => (
34
+ <button
35
+ key={s}
36
+ className={`${styles.replayBtn} ${speed === s ? styles.replayBtnActive : ''}`}
37
+ onClick={() => onSpeedChange(s)}
38
+ >
39
+ {s}x
40
+ </button>
41
+ ))}
42
+ </div>
43
 
44
+ <div className={styles.replayGroupCenter}>
45
+ <button className={styles.replayBtn} onClick={onStepBack} title="Step Back">β—„</button>
 
 
 
 
 
 
 
 
 
 
 
 
46
  <button
47
+ className={`${styles.replayBtn} ${isPlaying ? styles.replayBtnActive : ''} ${styles.replayPlayBtn}`}
48
+ onClick={isPlaying ? onPause : onPlay}
49
+ title={isPlaying ? 'Pause' : 'Play'}
50
  >
51
+ {isPlaying ? '⏸' : 'β–Ά'}
52
  </button>
53
+ <button className={styles.replayBtn} onClick={onStepForward} title="Step Forward">β–Ί</button>
54
+ </div>
55
+
56
+ <div className={styles.replayGroupRight}>
57
+ <span className={styles.replayStep}>
58
+ {String(currentStep + 1).padStart(2, '0')}/{String(totalSteps).padStart(2, '0')}
59
+ </span>
60
+ <input
61
+ type="range"
62
+ className={styles.replayScrubber}
63
+ min={0}
64
+ max={Math.max(totalSteps - 1, 0)}
65
+ value={currentStep}
66
+ onChange={e => onSeek(parseInt(e.target.value))}
67
+ />
68
+ </div>
69
  </div>
70
  );
71
  }
frontend/components/ui/ui.module.css CHANGED
@@ -12,17 +12,34 @@
12
  flex-shrink: 0;
13
  }
14
 
15
- .statusGreen { background: var(--nx-green); }
16
- .statusAmber { background: var(--nx-yellow); }
17
- .statusRed { background: var(--nx-red); }
 
 
 
 
 
 
 
 
18
 
19
  .statusPulse {
20
  animation: statusPulseAnim 2s ease-in-out infinite;
21
  }
22
 
23
  @keyframes statusPulseAnim {
24
- 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 currentColor; }
25
- 50% { opacity: 0.6; box-shadow: 0 0 6px 2px currentColor; }
 
 
 
 
 
 
 
 
 
26
  }
27
 
28
  .statusLabel {
@@ -36,11 +53,37 @@
36
  .replayBar {
37
  display: flex;
38
  align-items: center;
 
39
  gap: 8px;
40
  padding: 6px 12px;
41
  background: var(--nx-surface);
42
  border: 1px solid var(--nx-border);
43
  border-radius: 2px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
  .replayBtn {
@@ -68,8 +111,14 @@
68
  background: var(--nx-orange-glow2);
69
  }
70
 
 
 
 
 
 
71
  .replayScrubber {
72
- flex: 1;
 
73
  height: 3px;
74
  appearance: none;
75
  background: var(--nx-border);
@@ -92,5 +141,5 @@
92
  color: var(--nx-text-dim);
93
  font-variant-numeric: tabular-nums;
94
  min-width: 48px;
95
- text-align: center;
96
- }
 
12
  flex-shrink: 0;
13
  }
14
 
15
+ .statusGreen {
16
+ background: var(--nx-green);
17
+ }
18
+
19
+ .statusAmber {
20
+ background: var(--nx-yellow);
21
+ }
22
+
23
+ .statusRed {
24
+ background: var(--nx-red);
25
+ }
26
 
27
  .statusPulse {
28
  animation: statusPulseAnim 2s ease-in-out infinite;
29
  }
30
 
31
  @keyframes statusPulseAnim {
32
+
33
+ 0%,
34
+ 100% {
35
+ opacity: 1;
36
+ box-shadow: 0 0 0 0 currentColor;
37
+ }
38
+
39
+ 50% {
40
+ opacity: 0.6;
41
+ box-shadow: 0 0 6px 2px currentColor;
42
+ }
43
  }
44
 
45
  .statusLabel {
 
53
  .replayBar {
54
  display: flex;
55
  align-items: center;
56
+ justify-content: space-between;
57
  gap: 8px;
58
  padding: 6px 12px;
59
  background: var(--nx-surface);
60
  border: 1px solid var(--nx-border);
61
  border-radius: 2px;
62
+ width: 100%;
63
+ max-width: 600px;
64
+ }
65
+
66
+ .replayGroup,
67
+ .replayGroupCenter,
68
+ .replayGroupRight {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 8px;
72
+ }
73
+
74
+ .replayGroup {
75
+ flex: 1;
76
+ /* Match the space taken by scrubber so center is geometrically true */
77
+ }
78
+
79
+ .replayGroupCenter {
80
+ gap: 12px;
81
+ justify-content: center;
82
+ }
83
+
84
+ .replayGroupRight {
85
+ flex: 1;
86
+ justify-content: flex-end;
87
  }
88
 
89
  .replayBtn {
 
111
  background: var(--nx-orange-glow2);
112
  }
113
 
114
+ .replayPlayBtn {
115
+ font-size: 14px;
116
+ padding: 4px 12px;
117
+ }
118
+
119
  .replayScrubber {
120
+ width: 120px;
121
+ /* Don't flex 1 here, let it be fixed size so it looks neat */
122
  height: 3px;
123
  appearance: none;
124
  background: var(--nx-border);
 
141
  color: var(--nx-text-dim);
142
  font-variant-numeric: tabular-nums;
143
  min-width: 48px;
144
+ text-align: right;
145
+ }
frontend/lib/geoData.ts CHANGED
@@ -6,92 +6,92 @@ import { MLHub, MLCorridor } from './types';
6
 
7
  // ── Major Money Laundering Hub Locations ──────────────────────
8
  export const ML_HUBS: MLHub[] = [
9
- { id: 'panama', name: 'Panama City', lat: 8.9824, lng: -79.5199, riskLevel: 'critical', entityCount: 340, region: 'Americas' },
10
- { id: 'cayman', name: 'Cayman Islands', lat: 19.3133, lng: -81.2546, riskLevel: 'critical', entityCount: 520, region: 'Americas' },
11
- { id: 'bvi', name: 'British Virgin Islands', lat: 18.4207, lng: -64.6400, riskLevel: 'critical', entityCount: 680, region: 'Americas' },
12
- { id: 'london', name: 'London', lat: 51.5074, lng: -0.1278, riskLevel: 'high', entityCount: 890, region: 'Europe' },
13
- { id: 'zurich', name: 'ZΓΌrich', lat: 47.3769, lng: 8.5417, riskLevel: 'high', entityCount: 420, region: 'Europe' },
14
- { id: 'cyprus', name: 'Nicosia', lat: 35.1856, lng: 33.3823, riskLevel: 'high', entityCount: 310, region: 'Europe' },
15
- { id: 'dubai', name: 'Dubai', lat: 25.2048, lng: 55.2708, riskLevel: 'critical', entityCount: 760, region: 'MENA' },
16
- { id: 'singapore', name: 'Singapore', lat: 1.3521, lng: 103.8198, riskLevel: 'high', entityCount: 540, region: 'Asia' },
17
- { id: 'hongkong', name: 'Hong Kong', lat: 22.3193, lng: 114.1694, riskLevel: 'high', entityCount: 620, region: 'Asia' },
18
- { id: 'moscow', name: 'Moscow', lat: 55.7558, lng: 37.6173, riskLevel: 'critical', entityCount: 480, region: 'Europe' },
19
- { id: 'lagos', name: 'Lagos', lat: 6.5244, lng: 3.3792, riskLevel: 'high', entityCount: 290, region: 'Africa' },
20
- { id: 'mumbai', name: 'Mumbai', lat: 19.0760, lng: 72.8777, riskLevel: 'medium', entityCount: 350, region: 'Asia' },
21
- { id: 'seychelles', name: 'Seychelles', lat: -4.6796, lng: 55.4920, riskLevel: 'critical', entityCount: 190, region: 'Africa' },
22
- { id: 'malta', name: 'Malta', lat: 35.8989, lng: 14.5146, riskLevel: 'high', entityCount: 210, region: 'Europe' },
23
- { id: 'kualalumpur',name: 'Kuala Lumpur', lat: 3.1390, lng: 101.6869, riskLevel: 'medium', entityCount: 280, region: 'Asia' },
24
- { id: 'newyork', name: 'New York', lat: 40.7128, lng: -74.0060, riskLevel: 'medium', entityCount: 450, region: 'Americas' },
25
- { id: 'liechtenstein', name: 'Vaduz', lat: 47.1410, lng: 9.5215, riskLevel: 'high', entityCount: 130, region: 'Europe' },
26
- { id: 'bahamas', name: 'Nassau', lat: 25.0343, lng: -77.3963, riskLevel: 'high', entityCount: 240, region: 'Americas' },
27
- { id: 'macau', name: 'Macau', lat: 22.1987, lng: 113.5439, riskLevel: 'high', entityCount: 180, region: 'Asia' },
28
- { id: 'myanmar', name: 'Yangon', lat: 16.8661, lng: 96.1951, riskLevel: 'critical', entityCount: 110, region: 'Asia' },
29
  ];
30
 
31
  // ── Pre-built ML Corridors (static threat intel) ─────────────
32
  export const ML_CORRIDORS: MLCorridor[] = [
33
- { id: 'c-01', source: 'panama', target: 'cayman', volume: 2400000000, riskScore: 92, typology: 'layering', label: 'Shell Company Pipeline' },
34
- { id: 'c-02', source: 'bvi', target: 'london', volume: 1800000000, riskScore: 88, typology: 'layering', label: 'Offshore β†’ City Corridor' },
35
- { id: 'c-03', source: 'dubai', target: 'mumbai', volume: 3200000000, riskScore: 85, typology: 'trade_based_ml', label: 'Trade Invoice Manipulation' },
36
- { id: 'c-04', source: 'hongkong', target: 'singapore', volume: 2100000000, riskScore: 78, typology: 'layering', label: 'Asia-Pacific Layer' },
37
- { id: 'c-05', source: 'moscow', target: 'cyprus', volume: 4100000000, riskScore: 95, typology: 'layering', label: 'Russian Capital Flight' },
38
- { id: 'c-06', source: 'lagos', target: 'london', volume: 1200000000, riskScore: 82, typology: 'structuring', label: 'West Africa β†’ UK Pipeline' },
39
- { id: 'c-07', source: 'zurich', target: 'seychelles', volume: 890000000, riskScore: 90, typology: 'layering', label: 'Alpine β†’ Island Route' },
40
- { id: 'c-08', source: 'kualalumpur', target: 'singapore', volume: 681000000, riskScore: 94, typology: 'layering', label: '1MDB Corridor', caseId: 'case-1mdb' },
41
- { id: 'c-09', source: 'cayman', target: 'newyork', volume: 1500000000, riskScore: 75, typology: 'layering', label: 'Caribbean Integration' },
42
- { id: 'c-10', source: 'dubai', target: 'london', volume: 2800000000, riskScore: 87, typology: 'trade_based_ml', label: 'Gold & Commodities' },
43
- { id: 'c-11', source: 'malta', target: 'liechtenstein', volume: 450000000, riskScore: 80, typology: 'layering', label: 'EU Regulatory Arbitrage' },
44
- { id: 'c-12', source: 'seychelles', target: 'bvi', volume: 720000000, riskScore: 91, typology: 'layering', label: 'Island Hop Chain' },
45
- { id: 'c-13', source: 'macau', target: 'hongkong', volume: 1100000000, riskScore: 83, typology: 'structuring', label: 'Casino β†’ Banking' },
46
- { id: 'c-14', source: 'myanmar', target: 'singapore', volume: 340000000, riskScore: 96, typology: 'trade_based_ml', label: 'Narco-Trade Flow' },
47
- { id: 'c-15', source: 'bahamas', target: 'panama', volume: 560000000, riskScore: 79, typology: 'layering', label: 'Caribbean Circuit' },
48
  ];
49
 
50
  // ── Jurisdiction β†’ Coordinates Lookup ────────────────────────
51
  export const JURISDICTION_COORDS: Record<string, [number, number]> = {
52
- 'malaysia': [3.1390, 101.6869],
53
- 'kuala lumpur': [3.1390, 101.6869],
54
- 'seychelles': [-4.6796, 55.4920],
55
- 'british virgin islands': [18.4207, -64.6400],
56
- 'bvi': [18.4207, -64.6400],
57
- 'singapore': [1.3521, 103.8198],
58
- 'hong kong': [22.3193, 114.1694],
59
- 'london': [51.5074, -0.1278],
60
- 'united kingdom': [51.5074, -0.1278],
61
- 'switzerland': [47.3769, 8.5417],
62
- 'zurich': [47.3769, 8.5417],
63
- 'panama': [8.9824, -79.5199],
64
- 'cayman islands': [19.3133, -81.2546],
65
- 'cyprus': [35.1856, 33.3823],
66
- 'dubai': [25.2048, 55.2708],
67
- 'uae': [25.2048, 55.2708],
68
- 'united arab emirates': [25.2048, 55.2708],
69
- 'united states': [40.7128, -74.0060],
70
- 'usa': [40.7128, -74.0060],
71
- 'new york': [40.7128, -74.0060],
72
- 'moscow': [55.7558, 37.6173],
73
- 'russia': [55.7558, 37.6173],
74
- 'lagos': [6.5244, 3.3792],
75
- 'nigeria': [6.5244, 3.3792],
76
- 'mumbai': [19.0760, 72.8777],
77
- 'india': [19.0760, 72.8777],
78
- 'malta': [35.8989, 14.5146],
79
- 'liechtenstein': [47.1410, 9.5215],
80
- 'bahamas': [25.0343, -77.3963],
81
- 'macau': [22.1987, 113.5439],
82
- 'myanmar': [16.8661, 96.1951],
83
- 'isle of man': [54.2361, -4.5481],
84
- 'jersey': [49.2144, -2.1312],
85
- 'belize': [17.1899, -88.4976],
86
- 'mauritius': [-20.3484, 57.5522],
87
- 'geneva': [46.2044, 6.1432],
88
- 'frankfurt': [50.1109, 8.6821],
89
- 'germany': [50.1109, 8.6821],
90
- 'amsterdam': [52.3676, 4.9041],
91
- 'tokyo': [35.6762, 139.6503],
92
- 'japan': [35.6762, 139.6503],
93
- 'toronto': [43.6532, -79.3832],
94
- 'canada': [43.6532, -79.3832],
95
  };
96
 
97
  // ── Helpers ──────────────────────────────────────────────────
@@ -109,7 +109,8 @@ export function getCorridorCoords(corridor: MLCorridor): { from: [number, number
109
  };
110
  }
111
 
112
- export function geocodeJurisdiction(jurisdiction: string): [number, number] | null {
 
113
  const key = jurisdiction.toLowerCase().trim();
114
  return JURISDICTION_COORDS[key] || null;
115
  }
@@ -117,10 +118,10 @@ export function geocodeJurisdiction(jurisdiction: string): [number, number] | nu
117
  export function riskColor(level: string): string {
118
  switch (level) {
119
  case 'critical': return '#E11D48';
120
- case 'high': return '#EA580C';
121
- case 'medium': return '#EAB308';
122
- case 'low': return '#22C55E';
123
- default: return '#404040';
124
  }
125
  }
126
 
 
6
 
7
  // ── Major Money Laundering Hub Locations ──────────────────────
8
  export const ML_HUBS: MLHub[] = [
9
+ { id: 'panama', name: 'Panama City', lat: 8.9824, lng: -79.5199, riskLevel: 'critical', entityCount: 340, region: 'Americas' },
10
+ { id: 'cayman', name: 'Cayman Islands', lat: 19.3133, lng: -81.2546, riskLevel: 'critical', entityCount: 520, region: 'Americas' },
11
+ { id: 'bvi', name: 'British Virgin Islands', lat: 18.4207, lng: -64.6400, riskLevel: 'critical', entityCount: 680, region: 'Americas' },
12
+ { id: 'london', name: 'London', lat: 51.5074, lng: -0.1278, riskLevel: 'high', entityCount: 890, region: 'Europe' },
13
+ { id: 'zurich', name: 'ZΓΌrich', lat: 47.3769, lng: 8.5417, riskLevel: 'high', entityCount: 420, region: 'Europe' },
14
+ { id: 'cyprus', name: 'Nicosia', lat: 35.1856, lng: 33.3823, riskLevel: 'high', entityCount: 310, region: 'Europe' },
15
+ { id: 'dubai', name: 'Dubai', lat: 25.2048, lng: 55.2708, riskLevel: 'critical', entityCount: 760, region: 'MENA' },
16
+ { id: 'singapore', name: 'Singapore', lat: 1.3521, lng: 103.8198, riskLevel: 'high', entityCount: 540, region: 'Asia' },
17
+ { id: 'hongkong', name: 'Hong Kong', lat: 22.3193, lng: 114.1694, riskLevel: 'high', entityCount: 620, region: 'Asia' },
18
+ { id: 'moscow', name: 'Moscow', lat: 55.7558, lng: 37.6173, riskLevel: 'critical', entityCount: 480, region: 'Europe' },
19
+ { id: 'lagos', name: 'Lagos', lat: 6.5244, lng: 3.3792, riskLevel: 'high', entityCount: 290, region: 'Africa' },
20
+ { id: 'mumbai', name: 'Mumbai', lat: 19.0760, lng: 72.8777, riskLevel: 'medium', entityCount: 350, region: 'Asia' },
21
+ { id: 'seychelles', name: 'Seychelles', lat: -4.6796, lng: 55.4920, riskLevel: 'critical', entityCount: 190, region: 'Africa' },
22
+ { id: 'malta', name: 'Malta', lat: 35.8989, lng: 14.5146, riskLevel: 'high', entityCount: 210, region: 'Europe' },
23
+ { id: 'kualalumpur', name: 'Kuala Lumpur', lat: 3.1390, lng: 101.6869, riskLevel: 'medium', entityCount: 280, region: 'Asia' },
24
+ { id: 'newyork', name: 'New York', lat: 40.7128, lng: -74.0060, riskLevel: 'medium', entityCount: 450, region: 'Americas' },
25
+ { id: 'liechtenstein', name: 'Vaduz', lat: 47.1410, lng: 9.5215, riskLevel: 'high', entityCount: 130, region: 'Europe' },
26
+ { id: 'bahamas', name: 'Nassau', lat: 25.0343, lng: -77.3963, riskLevel: 'high', entityCount: 240, region: 'Americas' },
27
+ { id: 'macau', name: 'Macau', lat: 22.1987, lng: 113.5439, riskLevel: 'high', entityCount: 180, region: 'Asia' },
28
+ { id: 'myanmar', name: 'Yangon', lat: 16.8661, lng: 96.1951, riskLevel: 'critical', entityCount: 110, region: 'Asia' },
29
  ];
30
 
31
  // ── Pre-built ML Corridors (static threat intel) ─────────────
32
  export const ML_CORRIDORS: MLCorridor[] = [
33
+ { id: 'c-01', source: 'panama', target: 'cayman', volume: 2400000000, riskScore: 92, typology: 'layering', label: 'Shell Company Pipeline' },
34
+ { id: 'c-02', source: 'bvi', target: 'london', volume: 1800000000, riskScore: 88, typology: 'layering', label: 'Offshore β†’ City Corridor' },
35
+ { id: 'c-03', source: 'dubai', target: 'mumbai', volume: 3200000000, riskScore: 85, typology: 'trade_based_ml', label: 'Trade Invoice Manipulation' },
36
+ { id: 'c-04', source: 'hongkong', target: 'singapore', volume: 2100000000, riskScore: 78, typology: 'layering', label: 'Asia-Pacific Layer' },
37
+ { id: 'c-05', source: 'moscow', target: 'cyprus', volume: 4100000000, riskScore: 95, typology: 'layering', label: 'Russian Capital Flight' },
38
+ { id: 'c-06', source: 'lagos', target: 'london', volume: 1200000000, riskScore: 82, typology: 'structuring', label: 'West Africa β†’ UK Pipeline' },
39
+ { id: 'c-07', source: 'zurich', target: 'seychelles', volume: 890000000, riskScore: 90, typology: 'layering', label: 'Alpine β†’ Island Route' },
40
+ { id: 'c-08', source: 'kualalumpur', target: 'singapore', volume: 681000000, riskScore: 94, typology: 'layering', label: '1MDB Corridor', caseId: 'case-1mdb' },
41
+ { id: 'c-09', source: 'cayman', target: 'newyork', volume: 1500000000, riskScore: 75, typology: 'layering', label: 'Caribbean Integration' },
42
+ { id: 'c-10', source: 'dubai', target: 'london', volume: 2800000000, riskScore: 87, typology: 'trade_based_ml', label: 'Gold & Commodities' },
43
+ { id: 'c-11', source: 'malta', target: 'liechtenstein', volume: 450000000, riskScore: 80, typology: 'layering', label: 'EU Regulatory Arbitrage' },
44
+ { id: 'c-12', source: 'seychelles', target: 'bvi', volume: 720000000, riskScore: 91, typology: 'layering', label: 'Island Hop Chain' },
45
+ { id: 'c-13', source: 'macau', target: 'hongkong', volume: 1100000000, riskScore: 83, typology: 'structuring', label: 'Casino β†’ Banking' },
46
+ { id: 'c-14', source: 'myanmar', target: 'singapore', volume: 340000000, riskScore: 96, typology: 'trade_based_ml', label: 'Narco-Trade Flow' },
47
+ { id: 'c-15', source: 'bahamas', target: 'panama', volume: 560000000, riskScore: 79, typology: 'layering', label: 'Caribbean Circuit' },
48
  ];
49
 
50
  // ── Jurisdiction β†’ Coordinates Lookup ────────────────────────
51
  export const JURISDICTION_COORDS: Record<string, [number, number]> = {
52
+ 'malaysia': [3.1390, 101.6869],
53
+ 'kuala lumpur': [3.1390, 101.6869],
54
+ 'seychelles': [-4.6796, 55.4920],
55
+ 'british virgin islands': [18.4207, -64.6400],
56
+ 'bvi': [18.4207, -64.6400],
57
+ 'singapore': [1.3521, 103.8198],
58
+ 'hong kong': [22.3193, 114.1694],
59
+ 'london': [51.5074, -0.1278],
60
+ 'united kingdom': [51.5074, -0.1278],
61
+ 'switzerland': [47.3769, 8.5417],
62
+ 'zurich': [47.3769, 8.5417],
63
+ 'panama': [8.9824, -79.5199],
64
+ 'cayman islands': [19.3133, -81.2546],
65
+ 'cyprus': [35.1856, 33.3823],
66
+ 'dubai': [25.2048, 55.2708],
67
+ 'uae': [25.2048, 55.2708],
68
+ 'united arab emirates': [25.2048, 55.2708],
69
+ 'united states': [40.7128, -74.0060],
70
+ 'usa': [40.7128, -74.0060],
71
+ 'new york': [40.7128, -74.0060],
72
+ 'moscow': [55.7558, 37.6173],
73
+ 'russia': [55.7558, 37.6173],
74
+ 'lagos': [6.5244, 3.3792],
75
+ 'nigeria': [6.5244, 3.3792],
76
+ 'mumbai': [19.0760, 72.8777],
77
+ 'india': [19.0760, 72.8777],
78
+ 'malta': [35.8989, 14.5146],
79
+ 'liechtenstein': [47.1410, 9.5215],
80
+ 'bahamas': [25.0343, -77.3963],
81
+ 'macau': [22.1987, 113.5439],
82
+ 'myanmar': [16.8661, 96.1951],
83
+ 'isle of man': [54.2361, -4.5481],
84
+ 'jersey': [49.2144, -2.1312],
85
+ 'belize': [17.1899, -88.4976],
86
+ 'mauritius': [-20.3484, 57.5522],
87
+ 'geneva': [46.2044, 6.1432],
88
+ 'frankfurt': [50.1109, 8.6821],
89
+ 'germany': [50.1109, 8.6821],
90
+ 'amsterdam': [52.3676, 4.9041],
91
+ 'tokyo': [35.6762, 139.6503],
92
+ 'japan': [35.6762, 139.6503],
93
+ 'toronto': [43.6532, -79.3832],
94
+ 'canada': [43.6532, -79.3832],
95
  };
96
 
97
  // ── Helpers ──────────────────────────────────────────────────
 
109
  };
110
  }
111
 
112
+ export function geocodeJurisdiction(jurisdiction?: string | null): [number, number] | null {
113
+ if (!jurisdiction) return null;
114
  const key = jurisdiction.toLowerCase().trim();
115
  return JURISDICTION_COORDS[key] || null;
116
  }
 
118
  export function riskColor(level: string): string {
119
  switch (level) {
120
  case 'critical': return '#E11D48';
121
+ case 'high': return '#EA580C';
122
+ case 'medium': return '#EAB308';
123
+ case 'low': return '#22C55E';
124
+ default: return '#404040';
125
  }
126
  }
127