disaster-relief-env / frontend /src /components /CommandCenterTab.tsx
rajx77's picture
Hackathon Finale: Command Center UI, Groq Print Export, and Security Pass
af37c1f
Raw
History Blame Contribute Delete
25.7 kB
import React, { useState, useMemo } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
import { simulate } from '../api/client';
import type { SimResult, TaskInfo } from '../types';
import { Zap } from 'lucide-react';
import { CommsInterceptTerminal } from './CommsInterceptTerminal';
interface Props {
tasks: TaskInfo[];
}
const ACTION_LABELS: Record<string, string> = {
'deploy_team': 'Dispatch Rescue (Ground)',
'send_supplies': 'Route Supply Convoy',
'airlift': 'Call Emergency Airlift',
'wait': 'Wait Penalty Applied'
};
const ACTION_COLORS: Record<string, string> = {
'deploy_team': 'bg-blue-600/90 text-blue-100',
'send_supplies': 'bg-orange-500/90 text-orange-100',
'airlift': 'bg-cyan-600/90 text-cyan-100',
'wait': 'bg-red-800/90 text-red-100'
};
const SectionHeader = ({ title, subtitle }: { title: string, subtitle?: string }) => (
<div className="mb-4 flex flex-col">
<h3 className="text-lg font-bold text-cyan-50 tracking-wider uppercase text-shadow-sm shadow-blue-500/20">{title}</h3>
{subtitle && <p className="text-[10px] sm:text-xs text-zinc-400 font-mono tracking-tight">{subtitle}</p>}
</div>
);
function ZoneContentionMatrix({ data }: { data: { zones: string[], matrix: (number | null)[][] } }) {
const { zones, matrix } = data;
if (!zones.length) return <div className="text-zinc-600 text-sm italic flex h-full items-center justify-center">Run simulation to process matrix.</div>;
return (
<div className="bg-[#0b0c10] border border-zinc-800/80 rounded-xl p-4 md:p-6 w-full h-full flex flex-col relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-10 blur-xl pointer-events-none">
<div className="w-32 h-32 bg-red-500 rounded-full"></div>
</div>
<SectionHeader title="Inter-Zone Escalation Likelihood" subtitle="Predictive conflict over shared strategic resources computed from live step history" />
<div className="flex-1 flex flex-col overflow-auto min-w-max">
{/* Header Row */}
<div className="grid mb-2" style={{ gridTemplateColumns: `40px repeat(${zones.length}, minmax(30px, 1fr))` }}>
<div className="text-center"></div>
{zones.map(z => <div key={z} className="text-center text-[10px] font-bold text-zinc-500">{z}</div>)}
</div>
{/* Body Rows */}
{zones.map((z1, rIdx) => (
<div key={z1} className="grid relative group" style={{ gridTemplateColumns: `40px repeat(${zones.length}, minmax(30px, 1fr))` }}>
<div className="flex items-center justify-end pr-3 text-[10px] font-bold text-zinc-500">{z1}</div>
{zones.map((z2, cIdx) => {
const val = matrix[rIdx][cIdx];
let bg = "bg-[#111318]";
let text = "";
let shadow = "";
if (val === null) text = "•";
else if (val >= 50) { bg = "bg-red-600"; text = `${val}%`; shadow = "shadow-[0_0_15px_rgba(220,38,38,0.7)] z-10 scale-105"; }
else if (val >= 25) { bg = "bg-orange-600"; text = `${val}%`; shadow = "shadow-[0_0_10px_rgba(234,88,12,0.4)] z-10 scale-105"; }
else if (val >= 10) { bg = "bg-yellow-600/80"; text = `${val}%`; }
else if (val > 0) { text = `${val}%`; }
else { text = "0%"; }
return (
<div key={z2} className={`relative flex items-center justify-center p-1.5 sm:p-2 text-[10px] font-bold border border-[#0b0c10] transition-colors duration-300 ${bg} ${shadow}`}>
<span className={val === null || val === 0 ? "text-zinc-600" : "text-white"}>{text}</span>
</div>
);
})}
</div>
))}
</div>
</div>
);
}
function SmallChart({ data, dataKey, color, title, criticalAt }: any) {
if (!data.length) return <div className="h-[200px] bg-[#12141a] rounded flex items-center justify-center text-zinc-600 text-xs italic border border-zinc-800">Awaiting data</div>;
return (
<div className="bg-[#12141a] rounded-lg p-3 border border-zinc-800/50 flex flex-col h-[200px]">
<div className="text-sm font-semibold text-zinc-300 mb-1">{title}</div>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#27272a" vertical={false} />
<XAxis dataKey="step" tick={{ fill: '#71717a', fontSize: 10 }} tickLine={false} axisLine={false} tickMargin={5} />
<YAxis tick={{ fill: '#71717a', fontSize: 10 }} tickLine={false} axisLine={false} />
<Tooltip contentStyle={{ backgroundColor: '#18181b', border: '1px solid #3f3f46', borderRadius: '8px' }} itemStyle={{ color: '#06b6d4', fontWeight: 'bold' }} />
<ReferenceLine y={criticalAt} stroke="#ef4444" strokeDasharray="3 3" label={{ position: 'insideBottomRight', value: 'Critical', fill: '#ef4444', fontSize: 10 }} />
<Line type="monotone" dataKey={dataKey} stroke={color} strokeWidth={2} dot={false} activeDot={{ r: 4, fill: color }} />
</LineChart>
</ResponsiveContainer>
</div>
);
}
function ResourceHistory({ data }: { data: any[] }) {
return (
<div className="bg-[#0b0c10] border border-zinc-800/80 rounded-xl p-4 md:p-6 w-full h-full flex flex-col">
<div className="flex justify-between items-start mb-4">
<SectionHeader title="Resource History" subtitle="Global Live Trend Analytics" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 flex-1">
<SmallChart data={data} dataKey="unattended" color="#0ea5e9" title="Total Unattended Casualties" criticalAt={30} />
<SmallChart data={data} dataKey="supply_gap" color="#10b981" title="Aggregate Supply Deficit" criticalAt={60} />
<SmallChart data={data} dataKey="severity" color="#f59e0b" title="Max Severity Spike %" criticalAt={80} />
<SmallChart data={data} dataKey="logistics" color="#f97316" title="Logistics Criticality %" criticalAt={40} />
</div>
</div>
);
}
function AgentNetworkRadar({ result }: { result: SimResult | null }) {
// Try to find PyTorch scores from the most critical step
const lastStep = result?.steps[result.steps.length - 1];
const scores = lastStep?.reasoning?.pytorch_scores || [];
const topScores = [...scores].sort((a, b) => b.score - a.score).slice(0, 4);
if (!result || !topScores.length) {
return <div className="bg-[#0b0c10] border border-zinc-800/80 rounded-xl p-6 w-full h-full flex items-center justify-center text-zinc-600 text-sm italic">Run simulation with PyTorch AI agent for radar telemetry.</div>
}
const sortedZones = topScores.map(t => ({ zone: t.zone_id, score: Math.round(t.score * 100) }));
const positions = [
{ top: '12%', left: '50%', transform: '-translate-x-1/2', bg: 'bg-red-600', shadow: 'shadow-[0_0_20px_rgba(220,38,38,0.6)]' },
{ top: '50%', right: '12%', transform: '-translate-y-1/2', bg: 'bg-orange-500', shadow: 'shadow-[0_0_15px_rgba(249,115,22,0.5)]' },
{ bottom: '12%', left: '50%', transform: '-translate-x-1/2', bg: 'bg-indigo-600', shadow: 'shadow-[0_0_15px_rgba(79,70,229,0.5)]' },
{ top: '50%', left: '16%', transform: '-translate-y-1/2', bg: 'bg-emerald-600', shadow: '' },
];
return (
<div className="bg-[#0b0c10] border border-zinc-800/80 rounded-xl p-4 md:p-6 w-full h-full flex overflow-hidden relative">
{/* Background circles */}
<div className="absolute inset-0 flex items-center justify-center opacity-10 pointer-events-none">
<div className="w-32 h-32 md:w-48 md:h-48 rounded-full border border-zinc-400 absolute"></div>
<div className="w-64 h-64 md:w-80 md:h-80 rounded-full border border-dashed border-zinc-400 absolute"></div>
</div>
<div className="flex-1 relative flex items-center justify-center">
<svg className="w-full h-full absolute inset-0 text-zinc-600 pointer-events-none">
{sortedZones[0] && <line x1="50%" y1="50%" x2="50%" y2="20%" stroke="url(#redGlow)" strokeWidth="3" />}
{sortedZones[1] && <line x1="50%" y1="50%" x2="80%" y2="50%" stroke="url(#redGlow)" strokeWidth="2" />}
{sortedZones[2] && <line x1="50%" y1="50%" x2="50%" y2="80%" stroke="#1e3a8a" strokeWidth="2" />}
{sortedZones[3] && <line x1="50%" y1="50%" x2="25%" y2="50%" stroke="#1e3a8a" strokeWidth="1" />}
<defs>
<linearGradient id="redGlow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#ef4444" />
<stop offset="100%" stopColor="#f97316" />
</linearGradient>
</defs>
</svg>
{/* Nodes */}
{sortedZones.map((z, i) => (
<div key={z.zone} className="absolute flex flex-col items-center z-10" style={{ top: positions[i].top, left: positions[i].left, right: positions[i].right, bottom: positions[i].bottom, transform: positions[i].transform }}>
<div className={`w-8 h-8 md:w-10 md:h-10 rounded-full flex items-center justify-center font-bold text-white ${positions[i].bg} ${positions[i].shadow}`}>{z.zone}</div>
<div className="text-[10px] text-zinc-400 mt-1 font-bold">Node {z.zone}</div>
<div className="text-white bg-black/50 px-1 rounded text-[10px] mt-0.5 font-bold border border-zinc-700">{z.score}%</div>
</div>
))}
<div className="absolute w-12 h-12 md:w-16 md:h-16 rounded-full bg-cyan-500 flex items-center justify-center font-bold text-white shadow-[0_0_30px_rgba(6,182,212,0.8)] z-10 text-lg border-2 border-white">
HQ
</div>
</div>
<div className="w-1/3 md:w-64 border-l border-zinc-800/50 pl-4 md:pl-6 py-2 flex flex-col h-full bg-gradient-to-r from-transparent to-[#050608]">
<div className="mb-6">
<h4 className="text-[10px] md:text-xs font-bold text-cyan-600 uppercase tracking-widest mb-3">Attention Rank</h4>
<div className="space-y-3">
{sortedZones.map((z, i) => (
<React.Fragment key={z.zone}>
<div className={`flex justify-between text-xs md:text-sm font-black ${i===0?'text-red-500':i===1?'text-orange-500':i===2?'text-indigo-400':'text-zinc-500'}`}>
<span>{i+1}. Zone {z.zone}</span><span>{z.score}%</span>
</div>
<div className={`h-px ${i===0?'bg-red-900/30':i===1?'bg-orange-900/30':i===2?'bg-indigo-900/30':'bg-zinc-800'}`}></div>
</React.Fragment>
))}
</div>
</div>
<div className="flex-1 overflow-auto">
<h4 className="text-[10px] md:text-xs font-bold text-cyan-500 uppercase tracking-widest mb-2">Network Rationale</h4>
<p className="text-[10px] text-zinc-400 leading-relaxed font-mono">
Live API data extraction successful. Neural ranking algorithm evaluates {sortedZones[0]?.zone} as critical failure vector. Deep routing protocols initiated.
</p>
<div className="mt-3 text-[9px] text-zinc-600 flex items-center gap-1">
Engine: <span className="text-cyan-600 border-b border-cyan-900 border-dashed">{result.agent} pipeline</span>
</div>
</div>
</div>
</div>
);
}
function StrategyHeatmap({ displayData, mode, setMode }: any) {
const { actions, zones, matrix } = displayData;
if (!zones?.length) return <div className="bg-[#0b0c10] border border-zinc-800/80 rounded-xl p-6 w-full h-full flex items-center justify-center text-zinc-600 text-sm italic">Run simulation to populate heatmap.</div>;
return (
<div className="bg-[#0b0c10] border border-zinc-800/80 rounded-xl p-4 md:p-6 w-full h-full flex flex-col">
<div className="flex justify-between items-center mb-4">
<SectionHeader title="Strategy Heatmap" subtitle="Actual API action frequencies by zone" />
<div className="flex gap-2 text-[10px] font-bold uppercase tracking-wider">
<button onClick={() => setMode('type')} className={`px-3 py-1 rounded transition-colors ${mode === 'type' ? 'bg-cyan-900/40 text-cyan-400 border border-cyan-800/50' : 'text-zinc-500 hover:text-zinc-300'}`}>By type</button>
<button onClick={() => setMode('spread')} className={`px-3 py-1 rounded transition-colors ${mode === 'spread' ? 'bg-cyan-900/40 text-cyan-400 border border-cyan-800/50' : 'text-zinc-500 hover:text-zinc-300'}`}>By spread</button>
</div>
</div>
<div className="flex-1 w-full overflow-x-auto">
{mode === 'type' ? (
<table className="w-full text-left border-collapse min-w-max">
<thead>
<tr>
<th className="p-2 text-xs text-zinc-600 font-semibold uppercase tracking-widest min-w-[200px]">Enacted Strategy</th>
{zones.map((z: string) => <th key={z} className="p-2 text-center text-xs text-cyan-100 font-semibold"><div className="flex flex-col items-center"><div className="w-1.5 h-1.5 rounded-full bg-cyan-700 mb-1"></div>{z}</div></th>)}
</tr>
</thead>
<tbody>
{actions.map((actionKey: string, rIdx: number) => {
const rowTotal = matrix[rIdx].reduce((a:number,b:number)=>a+b, 0);
if (rowTotal === 0) return null; // hide unused actions
return (
<tr key={actionKey} className="border-b border-[#18181b] hover:bg-zinc-900/20">
<td className="p-2 text-[10px] text-zinc-300 font-bold tracking-wide uppercase border-l-2 border-zinc-700 pl-3">
{ACTION_LABELS[actionKey] || actionKey}
</td>
{zones.map((z: string, cIdx: number) => {
const val = matrix[rIdx][cIdx];
const colorClass = val > 0 ? (ACTION_COLORS[actionKey] || 'bg-zinc-700 text-white') : 'bg-[#18181b] text-transparent';
return (
<td key={z} className="p-1">
<div className={`w-8 h-6 flex items-center justify-center text-[11px] font-black rounded mx-auto transition-all ${colorClass} ${val > 0 ? 'shadow-sm' : ''}`}>
{val > 0 ? val : ''}
</div>
</td>
)
})}
</tr>
)})}
</tbody>
</table>
) : (
<table className="w-full text-left border-collapse min-w-max">
<thead>
<tr>
<th className="p-2 text-xs text-zinc-600 font-semibold uppercase tracking-widest min-w-[100px]">Node Focus</th>
{actions.map((a: string) => {
const colTotal = matrix[actions.indexOf(a)].reduce((acc:number,v:number)=>acc+v,0);
if (colTotal === 0) return null;
return <th key={a} className="p-2 text-center text-[9px] text-zinc-400 font-semibold uppercase">{ACTION_LABELS[a]?.split(' ')[0] || a}</th>
})}
</tr>
</thead>
<tbody>
{zones.map((z: string, cIdx: number) => (
<tr key={z} className="border-b border-[#18181b] hover:bg-zinc-900/20">
<td className="p-2 text-[11px] text-cyan-100 font-bold border-l-2 border-cyan-800 pl-3">Zone {z}</td>
{actions.map((actionKey: string, rIdx: number) => {
const colTotal = matrix[rIdx].reduce((acc:number,v:number)=>acc+v,0);
if (colTotal === 0) return null;
const val = matrix[rIdx][cIdx];
const colorClass = val > 0 ? (ACTION_COLORS[actionKey] || 'bg-zinc-700 text-white') : 'bg-[#18181b] text-transparent';
return (
<td key={actionKey} className="p-1">
<div className={`w-12 h-6 flex items-center justify-center text-[10px] font-black rounded mx-auto ${colorClass}`}>
{val > 0 ? val : ''}
</div>
</td>
)
})}
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}
export function CommandCenterTab({ tasks }: Props) {
const [selectedTask, setSelectedTask] = useState(() => tasks[1]?.task_id ?? tasks[0]?.task_id ?? 'task_2');
const [selectedAgent, setSelectedAgent] = useState<'ai_4stage' | 'greedy' | 'random'>('greedy'); // default greedy so it works instantly without key
const [result, setResult] = useState<SimResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [heatmapMode, setHeatmapMode] = useState<'type' | 'spread'>('type');
const runSimulation = async () => {
setLoading(true);
setError(null);
try {
const r = await simulate(selectedTask, selectedAgent);
setResult(r);
} catch(e: any) {
setError(e.message || String(e));
} finally {
setLoading(false);
}
};
// Math Pipelines
const tsData = useMemo(() => {
if (!result) return [];
return result.steps.map((step, i) => {
const obs = step.observation;
if (!obs) return { step: i, unattended: 0, supply_gap: 0, severity: 0, logistics: 0 };
const unattended = obs.zones.reduce((sum, z) => sum + z.casualties_remaining, 0);
const supplyGap = obs.zones.reduce((sum, z) => sum + z.supply_gap, 0);
const maxSeverity = obs.zones.reduce((max, z) => Math.max(max, z.severity), 0) * 100;
const logistics = (obs.zones.filter(z => z.sos_active || z.road_blocked).length / Math.max(1, obs.zones.length)) * 100;
return { step: i, unattended, supply_gap: supplyGap, severity: maxSeverity, logistics };
});
}, [result]);
const contentionData = useMemo(() => {
if (!result || result.steps.length < 2) return { zones: [], matrix: [] };
const zonesSet = new Set<string>();
result.steps.forEach(s => s.observation?.zones.forEach(z => zonesSet.add(z.zone_id)));
const zones = Array.from(zonesSet).sort();
const matrix = zones.map(z1 => zones.map(z2 => z1 === z2 ? null : 0 as number | null));
for (let i = 1; i < result.steps.length; i++) {
const prevObs = result.steps[i-1].observation;
const currObs = result.steps[i].observation;
if (!prevObs || !currObs) continue;
const severityDiff = zones.map(zId => {
const pv = prevObs.zones.find(z => z.zone_id === zId)?.severity || 0;
const cv = currObs.zones.find(z => z.zone_id === zId)?.severity || 0;
return cv - pv;
});
zones.forEach((z1, rIdx) => {
zones.forEach((z2, cIdx) => {
if (rIdx === cIdx) return;
if (severityDiff[rIdx] > 0.05 && severityDiff[cIdx] > 0.05) {
matrix[rIdx][cIdx]! += 25;
} else if (severityDiff[rIdx] > 0 && severityDiff[cIdx] > 0) {
matrix[rIdx][cIdx]! += 10;
}
});
});
}
zones.forEach((z1, rIdx) => {
zones.forEach((z2, cIdx) => {
if (matrix[rIdx][cIdx] !== null) {
matrix[rIdx][cIdx] = Math.min(100, matrix[rIdx][cIdx]!);
}
});
});
return { zones, matrix };
}, [result]);
const heatmapData = useMemo(() => {
if (!result) return { actions: [], zones: [], matrix: [] };
const zonesSet = new Set<string>();
result.steps.forEach(s => s.observation?.zones.forEach(z => zonesSet.add(z.zone_id)));
const zones = Array.from(zonesSet).sort();
const actionTypes = ['deploy_team', 'send_supplies', 'airlift', 'wait'];
const matrix = actionTypes.map(() => zones.map(() => 0));
result.steps.forEach(s => {
const a = s.action;
if (!a) return;
const targetZone = a.to_zone || a.from_zone; // some actions only have from_zone or to_zone.
const target = targetZone || (a as any).zone // fallback
if (!target) return;
const actName = actionTypes.find(type => typeof a.action === 'string' && a.action.includes(type)) || a.action;
let rIdx = actionTypes.indexOf(actName);
if (rIdx === -1) {
actionTypes.push(actName);
matrix.push(zones.map(() => 0));
rIdx = actionTypes.length - 1;
}
const cIdx = zones.indexOf(target);
if (rIdx >= 0 && cIdx >= 0) {
matrix[rIdx][cIdx] += 1;
}
});
return { actions: actionTypes, zones, matrix };
}, [result]);
return (
<div className="w-full bg-[#050505] min-h-[85vh] font-sans selection:bg-cyan-900 selection:text-cyan-100 flex flex-col space-y-4 pb-12">
{/* API Controls */}
<div className="bg-[#0b0c10] border border-zinc-800 rounded-2xl p-5 flex flex-wrap gap-4 items-end shadow-xl">
<div className="flex-1 min-w-[200px]">
<label className="flex items-center gap-2 text-[11px] text-cyan-500 font-bold mb-2 uppercase tracking-widest"><Zap className="w-3 h-3"/> Target Operation</label>
<select
value={selectedTask}
onChange={e => setSelectedTask(e.target.value)}
className="w-full bg-[#181a20] border border-zinc-700/50 rounded-lg px-3 py-2.5 text-sm text-white font-medium focus:outline-none focus:ring-1 focus:ring-cyan-500/50"
>
{tasks.map(t => (
<option key={t.task_id} value={t.task_id}>{t.name} ({t.difficulty})</option>
))}
</select>
</div>
<div className="flex-1 min-w-[200px]">
<label className="block text-[11px] text-zinc-500 font-bold mb-2 uppercase tracking-widest">Routing Engine</label>
<select
value={selectedAgent}
onChange={e => setSelectedAgent(e.target.value as any)}
className="w-full bg-[#181a20] border border-zinc-700/50 rounded-lg px-3 py-2.5 text-sm text-white font-medium focus:outline-none focus:ring-1 focus:ring-zinc-500/50"
>
<option value="greedy">Greedy Heuristic (Fast)</option>
<option value="ai_4stage">4-Stage AI (Requires Token)</option>
<option value="random">Random Baseline</option>
</select>
</div>
<button
onClick={runSimulation}
disabled={loading}
className="px-6 py-2.5 bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 text-white font-bold tracking-wider uppercase text-sm rounded-lg transition-all shadow-[0_0_15px_rgba(8,145,178,0.3)] hover:shadow-[0_0_20px_rgba(8,145,178,0.5)]"
>
{loading ? 'Fetching API Data...' : '▶ Initialize Link'}
</button>
</div>
{error && (
<div className="bg-red-950/50 border border-red-800 rounded-xl p-4 text-red-300 text-sm font-mono flex items-center justify-center">
ERROR STREAM: {error}
</div>
)}
{/* Dashboard Grids */}
<div className={`grid grid-cols-1 xl:grid-cols-2 gap-4 h-full transition-opacity duration-500 ${loading ? 'opacity-30 pointer-events-none' : 'opacity-100'}`}>
<div className="h-[450px]">
<ZoneContentionMatrix data={contentionData} />
</div>
<div className="h-[450px]">
<AgentNetworkRadar result={result} />
</div>
<div className="h-[550px]">
<ResourceHistory data={tsData} />
</div>
<div className="h-[550px]">
<StrategyHeatmap displayData={heatmapData} mode={heatmapMode} setMode={setHeatmapMode} />
</div>
{result && (
<div className="xl:col-span-2 h-[280px]">
<CommsInterceptTerminal steps={result.steps} autoPlay={true} />
</div>
)}
</div>
</div>
);
}