osiris-ai-manu / src /components /LayerPanel.tsx
Claude
Add resource intelligence layer: mining/RAM datasets, macro & utilities APIs, map layers
5aeda2b unverified
Raw
History Blame Contribute Delete
13.4 kB
'use client';
import { memo, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Plane, Satellite, Sun, AlertTriangle, Camera,
CloudLightning, Ship, Network, Database, Ghost,
Flame, Tv, Radio, Mountain, Anchor, Radar
} from 'lucide-react';
interface LayerPanelProps {
data: any;
activeLayers: any;
setActiveLayers: React.Dispatch<React.SetStateAction<any>>;
isMobile?: boolean;
theme?: 'core' | 'ghost';
setTheme?: (theme: 'core' | 'ghost') => void;
}
const LAYER_GROUPS = [
{
label: 'SDK',
fullLabel: 'OSIRIS SDK',
icon: Database,
layers: [
{ key: 'sdk_sea', label: 'Maritime Lines', dataKey: 'sdk_entities' },
],
},
{
label: 'AVIATION',
fullLabel: 'AVIATION',
icon: Plane,
layers: [
{ key: 'flights', label: 'Commercial', dataKey: 'commercial_flights' },
{ key: 'private', label: 'Private', dataKey: 'private_flights' },
{ key: 'jets', label: 'Private Jets', dataKey: 'private_jets' },
{ key: 'military', label: 'Military', dataKey: 'military_flights' },
],
},
{
label: 'MARITIME',
fullLabel: 'MARITIME',
icon: Ship,
layers: [
{ key: 'maritime', label: 'Maritime / Naval', dataKey: 'maritime_ships,maritime_ports,maritime_chokepoints' },
],
},
{
label: 'SPACE',
fullLabel: 'SPACE TRACKING',
icon: Satellite,
layers: [
{ key: 'satellites', label: 'All Satellites', dataKey: 'satellites' },
{ key: 'sat_comms', label: 'Starlink / Comms', dataKey: 'satellites', catKey: 'comms' },
{ key: 'sat_military', label: 'Military / Intel', dataKey: 'satellites', catKey: 'military' },
{ key: 'sat_navigation', label: 'GPS / Navigation', dataKey: 'satellites', catKey: 'navigation' },
{ key: 'sat_earth', label: 'Earth Observation', dataKey: 'satellites', catKey: 'earth_obs' },
{ key: 'sat_science', label: 'Stations / Telescopes', dataKey: 'satellites', catKey: 'science' },
],
},
{
label: 'SURVEIL',
fullLabel: 'SURVEILLANCE',
icon: Camera,
layers: [
{ key: 'cctv', label: 'CCTV Cameras', dataKey: 'cameras' },
{ key: 'live_news', label: 'Live News Feeds', dataKey: 'live_feeds' },
{ key: 'news_intel', label: 'SIGINT News', dataKey: 'sigint_news' },
],
},
{
label: 'HAZARD',
fullLabel: 'NATURAL HAZARDS',
icon: CloudLightning,
layers: [
{ key: 'earthquakes', label: 'Earthquakes', dataKey: 'earthquakes' },
{ key: 'fires', label: 'Active Fires', dataKey: 'fires' },
{ key: 'weather', label: 'Severe Weather', dataKey: 'weather_events' },
],
},
{
label: 'THREAT',
fullLabel: 'THREATS & INTEL',
icon: AlertTriangle,
layers: [
{ key: 'infrastructure', label: 'Nuclear Facilities', dataKey: 'infrastructure' },
{ key: 'global_incidents', label: 'Global Incidents', dataKey: 'gdelt' },
{ key: 'gps_jamming', label: 'GPS Jamming', dataKey: 'gps_jamming' },
],
},
{
label: 'NETWORK',
fullLabel: 'NETWORK INTEL',
icon: Network,
layers: [
{ key: 'malware', label: 'Live Malware', dataKey: 'malware_threats' },
],
},
{
label: 'RESOURCE',
fullLabel: 'RESOURCE INTEL',
icon: Mountain,
layers: [
{ key: 'mining', label: 'Mining Companies', dataKey: 'mining_companies' },
{ key: 'ram_fabs', label: 'Memory / RAM Fabs', dataKey: 'ram_companies' },
],
},
{
label: 'DISPLAY',
fullLabel: 'DISPLAY',
icon: Sun,
layers: [
{ key: 'day_night', label: 'Day / Night Cycle', dataKey: '' },
{ key: 'terrain_3d', label: '3D Terrain & Buildings', dataKey: '' },
],
},
];
/* ── Minimal Toggle Switch ── */
function ToggleSwitch({ active, onClick }: { active: boolean; onClick: () => void }) {
return (
<button
onClick={onClick}
className="relative flex-shrink-0 cursor-pointer"
style={{ width: 28, height: 14 }}
>
<div
className="absolute inset-0 rounded-full transition-all duration-300"
style={{
background: active ? 'rgba(255,255,255,0.2)' : 'transparent',
border: active ? '1px solid rgba(255,255,255,0.35)' : '1px solid rgba(255,255,255,0.12)',
boxShadow: active ? '0 0 8px rgba(255,255,255,0.1)' : 'none',
}}
/>
<motion.div
className="absolute top-[2px] rounded-full"
style={{
width: 10,
height: 10,
background: active ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.2)',
boxShadow: active ? '0 0 6px rgba(255,255,255,0.4)' : 'none',
}}
animate={{ left: active ? 16 : 2 }}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
/>
</button>
);
}
function LayerPanel({ data, activeLayers, setActiveLayers, isMobile, theme = 'core', setTheme }: LayerPanelProps) {
const [hoveredGroup, setHoveredGroup] = useState<string | null>(null);
const toggle = (key: string) => setActiveLayers((prev: any) => ({ ...prev, [key]: !prev[key] }));
const getCount = (dk: string, catKey?: string): number | null => {
if (!dk) return null;
if (catKey && data.category_counts) {
return data.category_counts[catKey] || 0;
}
let total = 0;
let found = false;
for (const k of dk.split(',')) {
if (data[k] && Array.isArray(data[k])) {
total += data[k].length;
found = true;
}
}
return found ? total : null;
};
/* ── MOBILE ── */
if (isMobile) {
return (
<div className="flex flex-col gap-5 py-2">
{LAYER_GROUPS.map((group) => (
<div key={group.label} className="flex flex-col gap-2">
<div className="text-[9px] font-mono tracking-[0.2em] uppercase text-white/30 border-b border-white/[0.06] pb-1.5">
{group.fullLabel}
</div>
<div className="flex flex-col gap-1">
{group.layers.map((layer) => {
const isLayerActive = activeLayers[layer.key];
const count = getCount(layer.dataKey, layer.catKey);
return (
<div key={layer.key} className="flex items-center gap-3 px-1 py-1.5">
<ToggleSwitch
active={!!isLayerActive}
onClick={() => toggle(layer.key)}
/>
<span className={`text-[10px] font-mono uppercase tracking-wider flex-1 transition-colors ${isLayerActive ? 'text-white/80' : 'text-white/40'}`}>
{layer.label}
</span>
{count !== null && (
<span className="text-[8px] font-mono tabular-nums text-white/20">
{count.toLocaleString()}
</span>
)}
</div>
);
})}
</div>
</div>
))}
{/* MOBILE GHOST TOGGLE */}
{setTheme && (
<div className="flex items-center justify-between mt-2 pt-3 border-t border-white/[0.06] px-1">
<span className="text-[9px] font-mono tracking-[0.2em] text-white/25 uppercase">Ghost Protocol</span>
<button
onClick={() => setTheme(theme === 'core' ? 'ghost' : 'core')}
className="w-8 h-8 rounded-full flex items-center justify-center transition-all"
style={{
background: theme === 'ghost' ? 'rgba(179, 136, 255, 0.15)' : 'transparent',
boxShadow: theme === 'ghost' ? '0 0 12px rgba(179, 136, 255, 0.3)' : 'none',
}}
>
<Ghost className="w-4 h-4" style={{ color: theme === 'ghost' ? '#B388FF' : 'rgba(255,255,255,0.25)' }} />
</button>
</div>
)}
</div>
);
}
/* ── DESKTOP ── */
return (
<motion.div
initial={{ x: -60, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ type: 'spring', damping: 30, stiffness: 200, delay: 2.8 }}
className="absolute top-0 left-0 h-full w-[48px] flex flex-col items-center pt-24 pb-6 z-50 pointer-events-auto"
style={{
background: 'rgba(0,0,0,0.15)',
backdropFilter: 'blur(24px) saturate(1.2)',
WebkitBackdropFilter: 'blur(24px) saturate(1.2)',
}}
>
<div className="flex-1 flex flex-col items-center gap-1">
{LAYER_GROUPS.map((group) => {
const groupActive = group.layers.some(l => activeLayers[l.key]);
const isHovered = hoveredGroup === group.label;
const Icon = group.icon;
return (
<div
key={group.label}
className="relative flex items-center justify-center"
onMouseEnter={() => setHoveredGroup(group.label)}
onMouseLeave={() => setHoveredGroup(null)}
>
{/* Icon Button */}
<div
className="w-10 h-10 flex items-center justify-center cursor-pointer rounded-lg transition-all duration-300"
style={{
background: isHovered ? 'rgba(255,255,255,0.05)' : 'transparent',
}}
>
<Icon
className="transition-all duration-300"
style={{
width: 16,
height: 16,
color: groupActive
? 'rgba(255,255,255,0.7)'
: isHovered
? 'rgba(255,255,255,0.4)'
: 'rgba(255,255,255,0.2)',
filter: groupActive ? 'drop-shadow(0 0 4px rgba(255,255,255,0.3))' : 'none',
}}
/>
</div>
{/* Flyout (LEFT side) */}
<AnimatePresence>
{isHovered && (
<motion.div
initial={{ opacity: 0, x: -8, filter: 'blur(4px)' }}
animate={{ opacity: 1, x: 0, filter: 'blur(0px)' }}
exit={{ opacity: 0, x: -4, filter: 'blur(2px)' }}
transition={{ duration: 0.18, ease: 'easeOut' }}
className="absolute left-[52px] top-1/2 -translate-y-1/2 min-w-[220px] rounded-xl p-3 z-[100] pointer-events-auto"
style={{
background: 'rgba(0,0,0,0.6)',
backdropFilter: 'blur(40px) saturate(1.5)',
WebkitBackdropFilter: 'blur(40px) saturate(1.5)',
border: '1px solid rgba(255,255,255,0.06)',
boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
}}
>
<div className="text-[9px] font-mono tracking-[0.2em] uppercase text-white/30 mb-2.5 pb-1.5 border-b border-white/[0.04]">
{group.fullLabel}
</div>
<div className="flex flex-col gap-0.5">
{group.layers.map((layer) => {
const isLayerActive = activeLayers[layer.key];
const count = getCount(layer.dataKey, layer.catKey);
return (
<div
key={layer.key}
className="flex items-center gap-3 px-1 py-[5px] rounded-md hover:bg-white/[0.03] transition-colors cursor-pointer"
onClick={() => toggle(layer.key)}
>
<ToggleSwitch active={!!isLayerActive} onClick={() => {}} />
<span className={`text-[10px] font-mono uppercase tracking-wider flex-1 transition-colors duration-200 ${isLayerActive ? 'text-white/70' : 'text-white/35'}`}>
{layer.label}
</span>
{count !== null && (
<span className="text-[9px] font-mono tabular-nums text-white/20">
{count.toLocaleString()}
</span>
)}
</div>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
{/* Subtle separator */}
<div className="w-5 h-px bg-white/[0.06] my-2" />
{/* Ghost Protocol Toggle */}
{setTheme && (
<button
onClick={() => setTheme(theme === 'core' ? 'ghost' : 'core')}
className="w-10 h-10 flex items-center justify-center rounded-lg transition-all duration-500 cursor-pointer"
style={{
background: theme === 'ghost' ? 'rgba(179, 136, 255, 0.1)' : 'transparent',
}}
title="Ghost Protocol"
>
<Ghost
className="transition-all duration-500"
style={{
width: 15,
height: 15,
color: theme === 'ghost' ? '#B388FF' : 'rgba(255,255,255,0.15)',
filter: theme === 'ghost' ? 'drop-shadow(0 0 6px rgba(179, 136, 255, 0.5))' : 'none',
}}
/>
</button>
)}
</motion.div>
);
}
export default memo(LayerPanel);