| <!DOCTYPE html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>Robust World Model Planner</title>
|
| <script src="https://cdn.tailwindcss.com"></script>
|
| <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| <style>
|
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
| .animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
| @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
| .animate-fade-in { animation: fadeIn 0.5s ease-out; }
|
| </style>
|
| </head>
|
| <body class="bg-slate-950">
|
| <div id="root"></div>
|
|
|
| <script type="text/babel">
|
| const { useState, useEffect, useRef, useCallback, useMemo } = React;
|
|
|
|
|
| const Play = () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>;
|
| const Pause = () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>;
|
| const RefreshCw = () => <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>;
|
| const BarChart2 = () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>;
|
| const MapIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></svg>;
|
| const Zap = () => <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>;
|
| const Layers = () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>;
|
| const AlertTriangle = () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>;
|
| const Sliders = () => <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line></svg>;
|
| const Database = () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>;
|
| const Terminal = () => <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>;
|
| const Grid = () => <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>;
|
| const ZapSmall = () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>;
|
|
|
|
|
| const SimpleLineChart = ({ data, width = 500, height = 250 }) => {
|
| const padding = { top: 30, right: 20, bottom: 30, left: 50 };
|
| const chartWidth = width - padding.left - padding.right;
|
| const chartHeight = height - padding.top - padding.bottom;
|
|
|
| const maxY = Math.max(...data.flatMap(d => [d.DINO, d.Online, d.Adversarial]));
|
| const minY = Math.min(...data.flatMap(d => [d.DINO, d.Online, d.Adversarial]));
|
|
|
| const getX = (i) => padding.left + (i / (data.length - 1)) * chartWidth;
|
| const getY = (val) => padding.top + chartHeight - ((val - minY) / (maxY - minY)) * chartHeight;
|
|
|
| const makePath = (key) => {
|
| return data.map((d, i) => `${i === 0 ? 'M' : 'L'} ${getX(i)} ${getY(d[key])}`).join(' ');
|
| };
|
|
|
| return (
|
| <svg width={width} height={height} className="overflow-visible">
|
| {/* Grid lines */}
|
| {[0, 0.25, 0.5, 0.75, 1].map((t, i) => (
|
| <line key={i} x1={padding.left} x2={width - padding.right}
|
| y1={padding.top + t * chartHeight} y2={padding.top + t * chartHeight}
|
| stroke="#1e293b" strokeDasharray="3,3" />
|
| ))}
|
|
|
| {/* Lines */}
|
| <path d={makePath('DINO')} fill="none" stroke="#d946ef" strokeWidth="2" style={{filter: 'drop-shadow(0 0 4px #d946ef)'}} />
|
| <path d={makePath('Online')} fill="none" stroke="#3b82f6" strokeWidth="2" style={{filter: 'drop-shadow(0 0 4px #3b82f6)'}} />
|
| <path d={makePath('Adversarial')} fill="none" stroke="#22d3ee" strokeWidth="3" style={{filter: 'drop-shadow(0 0 6px #22d3ee)'}} />
|
|
|
| {/* Legend */}
|
| <g transform={`translate(${padding.left}, 10)`}>
|
| <circle cx="0" cy="0" r="4" fill="#d946ef" />
|
| <text x="10" y="4" fill="#94a3b8" fontSize="10" fontFamily="monospace">DINO</text>
|
| <circle cx="70" cy="0" r="4" fill="#3b82f6" />
|
| <text x="80" y="4" fill="#94a3b8" fontSize="10" fontFamily="monospace">Online</text>
|
| <circle cx="150" cy="0" r="4" fill="#22d3ee" />
|
| <text x="160" y="4" fill="#94a3b8" fontSize="10" fontFamily="monospace">Adversarial</text>
|
| </g>
|
|
|
| {/* Y axis label */}
|
| <text x="15" y={padding.top + chartHeight/2} fill="#64748b" fontSize="10" fontFamily="monospace"
|
| transform={`rotate(-90, 15, ${padding.top + chartHeight/2})`} textAnchor="middle">Loss</text>
|
| </svg>
|
| );
|
| };
|
|
|
|
|
| const SimpleBarChart = ({ data, width = 500, height = 250 }) => {
|
| const padding = { top: 40, right: 20, bottom: 40, left: 50 };
|
| const chartWidth = width - padding.left - padding.right;
|
| const chartHeight = height - padding.top - padding.bottom;
|
|
|
| const maxY = Math.max(...data.flatMap(d => [d.GD, d.Online_GD, d.Adversarial_GD]));
|
| const barGroupWidth = chartWidth / data.length;
|
| const barWidth = barGroupWidth / 4;
|
| const gap = barWidth * 0.3;
|
|
|
| const getY = (val) => chartHeight - (val / maxY) * chartHeight;
|
| const getHeight = (val) => (val / maxY) * chartHeight;
|
|
|
| return (
|
| <svg width={width} height={height} className="overflow-visible">
|
| {/* Grid lines */}
|
| {[0, 0.25, 0.5, 0.75, 1].map((t, i) => (
|
| <g key={i}>
|
| <line x1={padding.left} x2={width - padding.right}
|
| y1={padding.top + t * chartHeight} y2={padding.top + t * chartHeight}
|
| stroke="#1e293b" strokeDasharray="3,3" />
|
| <text x={padding.left - 10} y={padding.top + t * chartHeight + 4}
|
| fill="#64748b" fontSize="10" fontFamily="monospace" textAnchor="end">
|
| {Math.round(maxY * (1 - t))}%
|
| </text>
|
| </g>
|
| ))}
|
|
|
| {/* Bars */}
|
| {data.map((d, i) => {
|
| const groupX = padding.left + i * barGroupWidth + barGroupWidth / 2;
|
| return (
|
| <g key={i}>
|
| <rect x={groupX - barWidth * 1.5 - gap} y={padding.top + getY(d.GD)}
|
| width={barWidth} height={getHeight(d.GD)} fill="#d946ef" rx="2"
|
| style={{filter: 'drop-shadow(0 0 4px #d946ef)'}} />
|
| <rect x={groupX - barWidth * 0.5} y={padding.top + getY(d.Online_GD)}
|
| width={barWidth} height={getHeight(d.Online_GD)} fill="#3b82f6" rx="2"
|
| style={{filter: 'drop-shadow(0 0 4px #3b82f6)'}} />
|
| <rect x={groupX + barWidth * 0.5 + gap} y={padding.top + getY(d.Adversarial_GD)}
|
| width={barWidth} height={getHeight(d.Adversarial_GD)} fill="#22d3ee" rx="2"
|
| style={{filter: 'drop-shadow(0 0 6px #22d3ee)'}} />
|
| <text x={groupX} y={height - 15} fill="#94a3b8" fontSize="10"
|
| fontFamily="monospace" textAnchor="middle">{d.name}</text>
|
| </g>
|
| );
|
| })}
|
|
|
| {/* Legend */}
|
| <g transform={`translate(${padding.left}, 15)`}>
|
| <rect x="0" y="-6" width="12" height="12" fill="#d946ef" rx="2" />
|
| <text x="18" y="4" fill="#94a3b8" fontSize="10" fontFamily="monospace">DINO</text>
|
| <rect x="70" y="-6" width="12" height="12" fill="#3b82f6" rx="2" />
|
| <text x="88" y="4" fill="#94a3b8" fontSize="10" fontFamily="monospace">Online</text>
|
| <rect x="150" y="-6" width="12" height="12" fill="#22d3ee" rx="2" />
|
| <text x="168" y="4" fill="#94a3b8" fontSize="10" fontFamily="monospace">Adversarial</text>
|
| </g>
|
| </svg>
|
| );
|
| };
|
|
|
| const WorldModelDashboard = () => {
|
| const [activeTab, setActiveTab] = useState('simulation');
|
| const [isRunning, setIsRunning] = useState(false);
|
| const [modelType, setModelType] = useState('adversarial');
|
| const [plannerType, setPlannerType] = useState('gd');
|
| const [mapType, setMapType] = useState('maze');
|
| const [status, setStatus] = useState('Ready');
|
| const [showVectors, setShowVectors] = useState(false);
|
|
|
| const [params, setParams] = useState({
|
| steerStrength: 3.0,
|
| lookahead: 70,
|
| noise: 0
|
| });
|
|
|
| const canvasRef = useRef(null);
|
| const [agentPos, setAgentPos] = useState({ x: 50, y: 50 });
|
| const [goalPos, setGoalPos] = useState({ x: 350, y: 250 });
|
| const [trajectory, setTrajectory] = useState([]);
|
| const [obstacles, setObstacles] = useState([]);
|
|
|
| useEffect(() => {
|
| if (plannerType === 'cem') {
|
| setParams(p => ({ ...p, steerStrength: 2.0, noise: 2, lookahead: 60 }));
|
| } else {
|
| if (modelType === 'adversarial') {
|
| setParams(p => ({ ...p, steerStrength: 3.5, noise: 0, lookahead: 80 }));
|
| } else if (modelType === 'online') {
|
| setParams(p => ({ ...p, steerStrength: 2.5, noise: 1.0, lookahead: 70 }));
|
| } else {
|
| setParams(p => ({ ...p, steerStrength: 0.5, noise: 5, lookahead: 50 }));
|
| }
|
| }
|
| }, [modelType, plannerType]);
|
|
|
| const performanceData = [
|
| { name: 'PushT', GD: 56, CEM: 54, Online_GD: 34, Adversarial_GD: 56 },
|
| { name: 'PointMaze', GD: 12, CEM: 24, Online_GD: 20, Adversarial_GD: 32 },
|
| { name: 'Wall', GD: 2, CEM: 10, Online_GD: 16, Adversarial_GD: 32 },
|
| ];
|
|
|
| const lossLandscapeData = useMemo(() => Array.from({ length: 50 }, (_, i) => {
|
| const x = i / 5;
|
| const dinoLoss = Math.sin(x * 3) * 0.5 + 1.5 + (x - 5) ** 2 * 0.1;
|
| const onlineLoss = (x - 5) ** 2 * 0.1 + 0.5 + 1;
|
| const advLoss = (x - 5) ** 2 * 0.08 + 0.5;
|
| return { x, DINO: dinoLoss, Online: onlineLoss, Adversarial: advLoss };
|
| }), []);
|
|
|
| const resetSim = useCallback(() => {
|
| setAgentPos({ x: 50, y: 50 });
|
| setTrajectory([]);
|
| setIsRunning(false);
|
| setStatus('Ready');
|
| }, []);
|
|
|
| useEffect(() => {
|
| let newObstacles = [];
|
| if (mapType === 'simple') {
|
| newObstacles = [{ x: 200, y: 100, w: 20, h: 100 }];
|
| } else if (mapType === 'wall') {
|
| newObstacles = [{ x: 180, y: 50, w: 40, h: 200 }];
|
| } else if (mapType === 'maze') {
|
| newObstacles = [
|
| { x: 120, y: 0, w: 20, h: 220 },
|
| { x: 240, y: 80, w: 20, h: 220 },
|
| { x: 320, y: 0, w: 20, h: 100 }
|
| ];
|
| }
|
| setObstacles(newObstacles);
|
| resetSim();
|
| }, [mapType, resetSim]);
|
|
|
| useEffect(() => {
|
| if (!isRunning) return;
|
|
|
| const interval = setInterval(() => {
|
| setAgentPos((prev) => {
|
| const dx = goalPos.x - prev.x;
|
| const dy = goalPos.y - prev.y;
|
| const distToGoal = Math.sqrt(dx * dx + dy * dy);
|
|
|
| if (distToGoal < 10) {
|
| setIsRunning(false);
|
| setStatus('Success');
|
| return prev;
|
| }
|
|
|
| let vx = dx / (distToGoal || 1);
|
| let vy = dy / (distToGoal || 1);
|
|
|
| const avoidanceRadius = params.lookahead;
|
| obstacles.forEach(obs => {
|
| const closestX = Math.max(obs.x, Math.min(prev.x, obs.x + obs.w));
|
| const closestY = Math.max(obs.y, Math.min(prev.y, obs.y + obs.h));
|
| const distX = prev.x - closestX;
|
| const distY = prev.y - closestY;
|
| const dist = Math.sqrt(distX * distX + distY * distY);
|
|
|
| if (dist < avoidanceRadius) {
|
| let repX = distX / (dist || 1);
|
| let repY = distY / (dist || 1);
|
| const repulsionStrength = (avoidanceRadius - dist) / avoidanceRadius;
|
| const steerFactor = params.steerStrength;
|
| vx += repX * repulsionStrength * steerFactor;
|
| vy += repY * repulsionStrength * steerFactor;
|
| }
|
| });
|
|
|
| const margin = 20;
|
| if (prev.x < margin) vx += (margin - prev.x) * 0.5;
|
| if (prev.x > 400 - margin) vx -= (prev.x - (400 - margin)) * 0.5;
|
| if (prev.y < margin) vy += (margin - prev.y) * 0.5;
|
| if (prev.y > 300 - margin) vy -= (prev.y - (300 - margin)) * 0.5;
|
|
|
| const speed = 5;
|
| const finalMag = Math.sqrt(vx * vx + vy * vy);
|
| vx = (vx / finalMag) * speed;
|
| vy = (vy / finalMag) * speed;
|
|
|
| if (params.noise > 0) {
|
| vx += (Math.random() - 0.5) * params.noise;
|
| vy += (Math.random() - 0.5) * params.noise;
|
| }
|
|
|
| const nextX = Math.max(5, Math.min(395, prev.x + vx));
|
| const nextY = Math.max(5, Math.min(295, prev.y + vy));
|
|
|
| const hitObstacle = obstacles.some(obs =>
|
| nextX > obs.x - 5 && nextX < obs.x + obs.w + 5 &&
|
| nextY > obs.y - 5 && nextY < obs.y + obs.h + 5
|
| );
|
|
|
| if (hitObstacle) {
|
| setIsRunning(false);
|
| setStatus('Crashed');
|
| return prev;
|
| }
|
|
|
| setTrajectory(t => [...t, { x: nextX, y: nextY }]);
|
| return { x: nextX, y: nextY };
|
| });
|
|
|
| }, 40);
|
|
|
| return () => clearInterval(interval);
|
| }, [isRunning, goalPos, obstacles, params]);
|
|
|
| useEffect(() => {
|
| const canvas = canvasRef.current;
|
| if (!canvas) return;
|
| const ctx = canvas.getContext('2d');
|
|
|
| ctx.clearRect(0, 0, 400, 300);
|
| ctx.fillStyle = '#0f172a';
|
| ctx.fillRect(0, 0, 400, 300);
|
|
|
| ctx.strokeStyle = '#1e293b';
|
| ctx.lineWidth = 1;
|
| for(let i=0; i<400; i+=40) { ctx.beginPath(); ctx.moveTo(i,0); ctx.lineTo(i,300); ctx.stroke(); }
|
| for(let i=0; i<300; i+=40) { ctx.beginPath(); ctx.moveTo(0,i); ctx.lineTo(400,i); ctx.stroke(); }
|
|
|
| if (showVectors) {
|
| const gridSize = 25;
|
| const arrowLen = 10;
|
|
|
| for(let x = 10; x < 400; x += gridSize) {
|
| for(let y = 10; y < 300; y += gridSize) {
|
| const dx = goalPos.x - x;
|
| const dy = goalPos.y - y;
|
| const distToGoal = Math.sqrt(dx * dx + dy * dy);
|
| let vx = dx / (distToGoal || 1);
|
| let vy = dy / (distToGoal || 1);
|
|
|
| let inObstacle = false;
|
| obstacles.forEach(obs => {
|
| if (x > obs.x && x < obs.x+obs.w && y > obs.y && y < obs.y+obs.h) inObstacle = true;
|
|
|
| const closestX = Math.max(obs.x, Math.min(x, obs.x + obs.w));
|
| const closestY = Math.max(obs.y, Math.min(y, obs.y + obs.h));
|
| const distX = x - closestX;
|
| const distY = y - closestY;
|
| const dist = Math.sqrt(distX * distX + distY * distY);
|
|
|
| if (dist < params.lookahead) {
|
| let repX = distX / (dist || 1);
|
| let repY = distY / (dist || 1);
|
| const repulsionStrength = (params.lookahead - dist) / params.lookahead;
|
| vx += repX * repulsionStrength * params.steerStrength;
|
| vy += repY * repulsionStrength * params.steerStrength;
|
| }
|
| });
|
|
|
| if (!inObstacle) {
|
| if (params.noise > 0) {
|
| vx += (Math.random() - 0.5) * params.noise * 0.8;
|
| vy += (Math.random() - 0.5) * params.noise * 0.8;
|
| }
|
|
|
| const mag = Math.sqrt(vx*vx + vy*vy);
|
| vx = (vx/mag) * arrowLen;
|
| vy = (vy/mag) * arrowLen;
|
|
|
| ctx.beginPath();
|
| ctx.moveTo(x, y);
|
| ctx.lineTo(x + vx, y + vy);
|
|
|
| let color = 'rgba(217, 70, 239, 0.3)';
|
| if (modelType === 'adversarial') color = 'rgba(34, 211, 238, 0.4)';
|
| if (modelType === 'online') color = 'rgba(59, 130, 246, 0.4)';
|
|
|
| ctx.strokeStyle = color;
|
| ctx.lineWidth = 1;
|
| ctx.stroke();
|
|
|
| ctx.beginPath();
|
| const headLen = 3;
|
| const angle = Math.atan2(vy, vx);
|
| ctx.moveTo(x + vx, y + vy);
|
| ctx.lineTo(x + vx - headLen * Math.cos(angle - Math.PI / 6), y + vy - headLen * Math.sin(angle - Math.PI / 6));
|
| ctx.lineTo(x + vx - headLen * Math.cos(angle + Math.PI / 6), y + vy - headLen * Math.sin(angle + Math.PI / 6));
|
| ctx.fillStyle = color;
|
| ctx.fill();
|
| }
|
| }
|
| }
|
| }
|
|
|
| if (isRunning || status === 'Ready') {
|
| ctx.beginPath();
|
| ctx.arc(agentPos.x, agentPos.y, params.lookahead, 0, Math.PI * 2);
|
| let color = 'rgba(217, 70, 239, 0.1)';
|
| if (modelType === 'adversarial') color = 'rgba(34, 211, 238, 0.1)';
|
| if (modelType === 'online') color = 'rgba(59, 130, 246, 0.1)';
|
| ctx.fillStyle = color;
|
| ctx.fill();
|
| ctx.strokeStyle = color.replace('0.1', '0.3');
|
| ctx.lineWidth = 1;
|
| ctx.setLineDash([5, 5]);
|
| ctx.stroke();
|
| ctx.setLineDash([]);
|
| }
|
|
|
| obstacles.forEach(obs => {
|
| ctx.fillStyle = '#1e293b';
|
| ctx.fillRect(obs.x, obs.y, obs.w, obs.h);
|
| ctx.shadowBlur = 10;
|
| ctx.shadowColor = '#94a3b8';
|
| ctx.strokeStyle = '#64748b';
|
| ctx.lineWidth = 2;
|
| ctx.strokeRect(obs.x, obs.y, obs.w, obs.h);
|
| ctx.shadowBlur = 0;
|
| });
|
|
|
| ctx.beginPath();
|
| let trajColor = '#d946ef';
|
| if (modelType === 'adversarial') trajColor = '#22d3ee';
|
| if (modelType === 'online') trajColor = '#3b82f6';
|
|
|
| ctx.strokeStyle = trajColor;
|
| ctx.lineWidth = 3;
|
| ctx.lineCap = 'round';
|
| ctx.lineJoin = 'round';
|
| ctx.shadowBlur = 10;
|
| ctx.shadowColor = trajColor;
|
| trajectory.forEach((p, i) => {
|
| if (i === 0) ctx.moveTo(p.x, p.y);
|
| else ctx.lineTo(p.x, p.y);
|
| });
|
| ctx.stroke();
|
| ctx.shadowBlur = 0;
|
|
|
| ctx.beginPath();
|
| ctx.arc(goalPos.x, goalPos.y, 8, 0, Math.PI * 2);
|
| ctx.fillStyle = '#f472b6';
|
| ctx.shadowBlur = 15;
|
| ctx.shadowColor = '#f472b6';
|
| ctx.fill();
|
| ctx.shadowBlur = 0;
|
| ctx.fillStyle = '#000';
|
| ctx.font = 'bold 10px monospace';
|
| ctx.fillText("G", goalPos.x - 4, goalPos.y + 3);
|
|
|
| ctx.beginPath();
|
| ctx.arc(agentPos.x, agentPos.y, 6, 0, Math.PI * 2);
|
| ctx.fillStyle = '#fff';
|
| ctx.shadowBlur = 10;
|
| ctx.shadowColor = '#fff';
|
| ctx.fill();
|
| ctx.shadowBlur = 0;
|
|
|
| }, [agentPos, goalPos, obstacles, trajectory, modelType, params.lookahead, isRunning, status, showVectors, params.steerStrength, params.noise]);
|
|
|
| return (
|
| <div className="flex flex-col h-screen bg-slate-950 text-cyan-50 font-mono">
|
| {/* Header */}
|
| <header className="bg-slate-900/80 backdrop-blur-md border-b border-slate-800 p-4 flex items-center justify-between z-10">
|
| <div className="flex items-center space-x-3">
|
| <div className="bg-cyan-500/10 border border-cyan-500/50 text-cyan-400 p-2 rounded shadow-[0_0_15px_rgba(6,182,212,0.3)]">
|
| <Zap />
|
| </div>
|
| <div>
|
| <h1 className="text-xl font-bold tracking-tight text-cyan-100 uppercase">Robust_World_Planner_v2.0</h1>
|
| <p className="text-xs text-slate-400 font-medium">System: Online // Paper: "Closing the Train-Test Gap"</p>
|
| </div>
|
| </div>
|
| <div className="flex items-center space-x-4">
|
| {status === 'Crashed' && <span className="text-red-400 font-bold flex items-center bg-red-900/20 px-3 py-1 rounded border border-red-500/30"><AlertTriangle /><span className="ml-2">SYSTEM_FAILURE</span></span>}
|
| {status === 'Success' && <span className="text-emerald-400 font-bold flex items-center bg-emerald-900/20 px-3 py-1 rounded border border-emerald-500/30"><ZapSmall /><span className="ml-2">TARGET_ACQUIRED</span></span>}
|
| <a href="https://github.com/qw3rtman/robust-world-model-planning" target="_blank" rel="noreferrer" className="text-xs text-slate-500 hover:text-cyan-400 transition-colors uppercase tracking-widest">
|
| Source_Code
|
| </a>
|
| </div>
|
| </header>
|
|
|
| {/* Main Content */}
|
| <div className="flex flex-1 overflow-hidden">
|
|
|
| {/* Sidebar Controls */}
|
| <aside className="w-80 bg-slate-900 border-r border-slate-800 p-5 flex flex-col overflow-y-auto">
|
| <div className="mb-6">
|
| <h3 className="text-xs font-bold text-cyan-700 mb-3 uppercase tracking-widest flex items-center">
|
| <Terminal /> <span className="ml-2">System_Config</span>
|
| </h3>
|
|
|
| <div className="space-y-4">
|
| <div>
|
| <label className="text-xs font-semibold text-slate-400 mb-2 block uppercase">Core Model</label>
|
| <div className="grid grid-cols-1 gap-2">
|
| <button
|
| onClick={() => { setModelType('dino'); resetSim(); }}
|
| className={`p-3 text-sm font-medium rounded border flex items-center transition-all ${modelType === 'dino' ? 'bg-fuchsia-900/20 border-fuchsia-500 text-fuchsia-400 shadow-[0_0_10px_rgba(217,70,239,0.2)]' : 'bg-slate-800 border-slate-700 text-slate-500 hover:bg-slate-800/80'}`}
|
| >
|
| <AlertTriangle /> <span className="ml-3">STANDARD_DINO</span>
|
| </button>
|
| <button
|
| onClick={() => { setModelType('online'); resetSim(); }}
|
| className={`p-3 text-sm font-medium rounded border flex items-center transition-all ${modelType === 'online' ? 'bg-blue-900/20 border-blue-500 text-blue-400 shadow-[0_0_10px_rgba(59,130,246,0.2)]' : 'bg-slate-800 border-slate-700 text-slate-500 hover:bg-slate-800/80'}`}
|
| >
|
| <Database /> <span className="ml-3">ONLINE_WM</span>
|
| </button>
|
| <button
|
| onClick={() => { setModelType('adversarial'); resetSim(); }}
|
| className={`p-3 text-sm font-medium rounded border flex items-center transition-all ${modelType === 'adversarial' ? 'bg-cyan-900/20 border-cyan-500 text-cyan-400 shadow-[0_0_10px_rgba(34,211,238,0.2)]' : 'bg-slate-800 border-slate-700 text-slate-500 hover:bg-slate-800/80'}`}
|
| >
|
| <ZapSmall /> <span className="ml-3">ADVERSARIAL_WM</span>
|
| </button>
|
| </div>
|
| </div>
|
|
|
| <div>
|
| <label className="text-xs font-semibold text-slate-400 mb-2 block uppercase">Environment</label>
|
| <select
|
| className="w-full p-2.5 bg-slate-800 border border-slate-700 rounded text-sm text-cyan-50 focus:border-cyan-500 outline-none transition-all uppercase"
|
| value={mapType}
|
| onChange={(e) => setMapType(e.target.value)}
|
| >
|
| <option value="simple">Sector_A (Simple)</option>
|
| <option value="wall">Sector_B (Wall)</option>
|
| <option value="maze">Sector_C (Complex)</option>
|
| </select>
|
| </div>
|
|
|
| <div>
|
| <label className="text-xs font-semibold text-slate-400 mb-2 block uppercase">Planner Logic</label>
|
| <select
|
| className="w-full p-2.5 bg-slate-800 border border-slate-700 rounded text-sm text-cyan-50 focus:border-cyan-500 outline-none transition-all uppercase"
|
| value={plannerType}
|
| onChange={(e) => { setPlannerType(e.target.value); resetSim(); }}
|
| >
|
| <option value="gd">Gradient_Descent (Fast)</option>
|
| <option value="cem">Cross_Entropy (Slow)</option>
|
| </select>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div className="mb-6 bg-slate-900/50 p-4 rounded border border-slate-800 relative overflow-hidden">
|
| <div className="absolute top-0 left-0 w-1 h-full bg-cyan-500/50"></div>
|
| <h3 className="text-xs font-bold text-slate-500 mb-3 uppercase tracking-widest flex items-center">
|
| <Sliders /> <span className="ml-2">Telemetry</span>
|
| </h3>
|
| <div className="space-y-4">
|
| <div>
|
| <div className="flex justify-between mb-1">
|
| <label className="text-xs font-semibold text-slate-400">STEERING_GAIN</label>
|
| <span className="text-xs font-mono text-cyan-400">{params.steerStrength.toFixed(1)}</span>
|
| </div>
|
| <input
|
| type="range" min="0" max="6" step="0.1"
|
| value={params.steerStrength}
|
| onChange={(e) => setParams({...params, steerStrength: parseFloat(e.target.value)})}
|
| className="w-full h-1 bg-slate-700 rounded appearance-none cursor-pointer accent-cyan-500"
|
| />
|
| </div>
|
| <div>
|
| <div className="flex justify-between mb-1">
|
| <label className="text-xs font-semibold text-slate-400">SCAN_HORIZON</label>
|
| <span className="text-xs font-mono text-cyan-400">{params.lookahead}PX</span>
|
| </div>
|
| <input
|
| type="range" min="10" max="150" step="5"
|
| value={params.lookahead}
|
| onChange={(e) => setParams({...params, lookahead: parseInt(e.target.value)})}
|
| className="w-full h-1 bg-slate-700 rounded appearance-none cursor-pointer accent-cyan-500"
|
| />
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div className="mb-6">
|
| <div className="flex space-x-2">
|
| <button
|
| onClick={() => { setIsRunning(!isRunning); if(status !== 'Planning' && !isRunning) setStatus('Planning'); }}
|
| disabled={status === 'Crashed' || status === 'Success'}
|
| className={`flex-1 flex items-center justify-center p-3 rounded text-slate-900 font-bold uppercase tracking-wide transition-all ${
|
| status === 'Crashed' || status === 'Success' ? 'bg-slate-700 cursor-not-allowed opacity-50' :
|
| isRunning ? 'bg-fuchsia-500 hover:bg-fuchsia-400 shadow-[0_0_15px_rgba(217,70,239,0.5)]' : 'bg-cyan-500 hover:bg-cyan-400 shadow-[0_0_15px_rgba(6,182,212,0.5)]'}`}
|
| >
|
| {isRunning ? <><Pause /> <span className="ml-2">Halt</span></> : <><Play /> <span className="ml-2">Execute</span></>}
|
| </button>
|
| <button
|
| onClick={resetSim}
|
| className="p-3 border border-slate-700 rounded text-slate-400 hover:bg-slate-800 hover:text-cyan-400 transition-colors"
|
| >
|
| <RefreshCw />
|
| </button>
|
| </div>
|
| </div>
|
| </aside>
|
|
|
| {}
|
| <main className="flex-1 flex flex-col bg-slate-950 overflow-y-auto">
|
| {}
|
| <div className="bg-slate-900 border-b border-slate-800 px-8 pt-4 flex space-x-8 sticky top-0 z-10">
|
| <button
|
| onClick={() => setActiveTab('simulation')}
|
| className={`pb-4 text-xs font-bold border-b-2 transition-colors uppercase tracking-widest ${activeTab === 'simulation' ? 'border-cyan-500 text-cyan-400' : 'border-transparent text-slate-500 hover:text-slate-300'}`}
|
| >
|
| <span className="flex items-center"><MapIcon /> <span className="ml-2">Live_Feed</span></span>
|
| </button>
|
| <button
|
| onClick={() => setActiveTab('landscape')}
|
| className={`pb-4 text-xs font-bold border-b-2 transition-colors uppercase tracking-widest ${activeTab === 'landscape' ? 'border-cyan-500 text-cyan-400' : 'border-transparent text-slate-500 hover:text-slate-300'}`}
|
| >
|
| <span className="flex items-center"><Layers /> <span className="ml-2">Data_Surface</span></span>
|
| </button>
|
| <button
|
| onClick={() => setActiveTab('benchmarks')}
|
| className={`pb-4 text-xs font-bold border-b-2 transition-colors uppercase tracking-widest ${activeTab === 'benchmarks' ? 'border-cyan-500 text-cyan-400' : 'border-transparent text-slate-500 hover:text-slate-300'}`}
|
| >
|
| <span className="flex items-center"><BarChart2 /> <span className="ml-2">Analytics</span></span>
|
| </button>
|
| </div>
|
|
|
| <div className="p-8 max-w-5xl mx-auto w-full">
|
| {activeTab === 'simulation' && (
|
| <div className="flex flex-col items-center animate-fade-in">
|
| <div className="bg-slate-900 p-1 rounded-lg border border-slate-800 shadow-[0_0_30px_rgba(0,0,0,0.5)] mb-6 relative group">
|
| <div className="absolute inset-0 bg-cyan-500/5 rounded-lg pointer-events-none"></div>
|
| <canvas
|
| ref={canvasRef}
|
| width={400}
|
| height={300}
|
| className="rounded cursor-crosshair block"
|
| onClick={(e) => {
|
| const rect = canvasRef.current.getBoundingClientRect();
|
| setGoalPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
| resetSim();
|
| }}
|
| />
|
|
|
| <div className="absolute top-4 left-4 flex flex-col space-y-2">
|
| <button
|
| onClick={() => setShowVectors(!showVectors)}
|
| className={`px-3 py-1.5 rounded border text-[10px] font-bold uppercase tracking-wider backdrop-blur-md transition-all ${showVectors ? 'bg-cyan-500/20 border-cyan-500 text-cyan-400' : 'bg-slate-900/80 border-slate-700 text-slate-400 hover:bg-slate-800'}`}
|
| >
|
| <span className="flex items-center"><Grid /> <span className="ml-2">Matrix_View {showVectors ? '[ON]' : '[OFF]'}</span></span>
|
| </button>
|
| </div>
|
|
|
| <div className="absolute top-4 right-4 bg-slate-950/80 backdrop-blur px-3 py-1.5 rounded border border-slate-700 text-[10px] font-medium text-cyan-400 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none uppercase">
|
| Target_Designation_Mode
|
| </div>
|
| </div>
|
|
|
| <div className="flex space-x-6 text-xs font-medium text-slate-400 bg-slate-900 px-6 py-3 rounded border border-slate-800 uppercase tracking-wide">
|
| <div className="flex items-center"><span className="w-2 h-2 bg-white rounded-full mr-2 shadow-[0_0_8px_white]"></span> Agent</div>
|
| <div className="flex items-center"><span className="w-2 h-2 bg-pink-500 rounded-full mr-2 shadow-[0_0_8px_magenta]"></span> Target</div>
|
| <div className="flex items-center">
|
| <span className={`w-2 h-2 rounded-full mr-2 shadow-[0_0_8px_currentColor] ${modelType === 'adversarial' ? 'text-cyan-400 bg-cyan-400' : modelType === 'online' ? 'text-blue-400 bg-blue-400' : 'text-fuchsia-400 bg-fuchsia-400'}`}></span> Path
|
| </div>
|
| <div className="flex items-center"><span className="w-2 h-2 border border-slate-500 border-dashed rounded-full mr-2"></span> Scanner</div>
|
| </div>
|
| </div>
|
| )}
|
|
|
| {activeTab === 'landscape' && (
|
| <div className="bg-slate-900 p-6 rounded border border-slate-800 animate-fade-in">
|
| <div className="mb-6">
|
| <h3 className="text-lg font-bold text-cyan-100 uppercase tracking-wide">Loss Surface Topology</h3>
|
| <p className="text-xs text-slate-500 mt-1">
|
| Adversarial training (Cyan) smooths the optimization landscape. Standard DINO (Fuchsia) is volatile and non-convex.
|
| </p>
|
| </div>
|
| <div className="flex justify-center">
|
| <SimpleLineChart data={lossLandscapeData} width={550} height={280} />
|
| </div>
|
| </div>
|
| )}
|
|
|
| {activeTab === 'benchmarks' && (
|
| <div className="bg-slate-900 p-6 rounded border border-slate-800 animate-fade-in">
|
| <div className="mb-6">
|
| <h3 className="text-lg font-bold text-cyan-100 uppercase tracking-wide">Performance Metrics</h3>
|
| <p className="text-xs text-slate-500 mt-1">
|
| Success rates (%) across control environments. Adversarial optimization provides superior stability.
|
| </p>
|
| </div>
|
| <div className="flex justify-center">
|
| <SimpleBarChart data={performanceData} width={550} height={280} />
|
| </div>
|
| </div>
|
| )}
|
| </div>
|
| </main>
|
| </div>
|
| </div>
|
| );
|
| };
|
|
|
| ReactDOM.createRoot(document.getElementById('root')).render(<WorldModelDashboard />);
|
| </script>
|
| </body>
|
| </html>
|
|
|