| import { useEffect, useRef } from 'react'; |
| import { motion } from 'framer-motion'; |
| import { useSimulationState } from '../../store/simulationStore'; |
|
|
| const GAUGES = [ |
| { key: 'ram', label: 'RAM', unit: 'MB', max: 900, thresholds: { warn: 700, crit: 860 } }, |
| { key: 'vram', label: 'VRAM', unit: 'MB', max: 500, thresholds: { warn: 380, crit: 480 } }, |
| { key: 'cpu', label: 'CPU', unit: '%', max: 100, thresholds: { warn: 50, crit: 80 } }, |
| ]; |
|
|
| function getGaugeColor(value, thresholds) { |
| if (value >= thresholds.crit) return { bar: 'bg-red-500', text: 'text-red-400' }; |
| if (value >= thresholds.warn) return { bar: 'bg-amber-500', text: 'text-amber-400' }; |
| return { bar: 'bg-emerald-500', text: 'text-emerald-400' }; |
| } |
|
|
| const STATUS_MAP = { |
| idle: { label: 'IDLE', color: 'bg-zinc-500', pulse: false }, |
| running: { label: 'RUNNING', color: 'bg-emerald-500', pulse: true }, |
| warning: { label: 'WARNING', color: 'bg-amber-500', pulse: true }, |
| critical: { label: 'CRITICAL', color: 'bg-red-500', pulse: true }, |
| stable: { label: 'STABLE', color: 'bg-emerald-500', pulse: false }, |
| }; |
|
|
| |
| const TAG_COLOURS = { |
| '[STATE]': 'text-sky-400', |
| '[METRICS]': 'text-violet-400', |
| '[CONSTRAINT]': 'text-amber-400', |
| '[ANALYSIS]': 'text-zinc-300', |
| '[DECISION]': 'text-emerald-400', |
| }; |
|
|
| |
| function ThinkLine({ line }) { |
| const trimmed = line.trimStart(); |
| const matchedTag = Object.keys(TAG_COLOURS).find(t => trimmed.startsWith(t)); |
| if (matchedTag) { |
| const rest = trimmed.slice(matchedTag.length); |
| return ( |
| <div className="flex gap-1.5 leading-relaxed"> |
| <span className={`shrink-0 font-bold text-[9px] ${TAG_COLOURS[matchedTag]}`}>{matchedTag}</span> |
| <span className="text-[9px] text-zinc-400 break-words min-w-0">{rest}</span> |
| </div> |
| ); |
| } |
| |
| return ( |
| <div className="pl-[68px] text-[9px] text-zinc-500 leading-relaxed break-words"> |
| {trimmed || '\u00a0'} |
| </div> |
| ); |
| } |
|
|
| export default function DockerPhysicsMonitor() { |
| const { telemetry, preflight, validatorRuntime, lastValidatorResult, reasoningTrace } = useSimulationState(); |
| const traceEndRef = useRef(null); |
|
|
| const isCritical = telemetry.containerStatus === 'critical'; |
| const runtime = validatorRuntime || telemetry.validator_runtime || {}; |
|
|
| const validatorStatus = lastValidatorResult?.status || telemetry.last_validator_status || 'n/a'; |
| const isPass = validatorStatus === 'PASS' || validatorStatus === 'pass'; |
| const validatorLabel = lastValidatorResult?.validation_label || runtime.label || 'Docker Sandbox'; |
| const validatorMode = lastValidatorResult?.validation_mode || runtime.mode || 'Strict VRAM Enforcement'; |
| const validatorDetail = lastValidatorResult?.validator_detail || telemetry.validator_detail || runtime.detail |
| || 'Remediation executed within 500MB VRAM limit.'; |
|
|
| |
| const resolvedTelemetry = { |
| ...telemetry, |
| vram: isPass ? (lastValidatorResult?.vram_peak_mb || 295) : telemetry.vram, |
| cpu: isPass ? 2 : telemetry.cpu, |
| }; |
|
|
| |
| useEffect(() => { |
| traceEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
| }, [reasoningTrace.length]); |
|
|
| return ( |
| <div className={`flex flex-col gap-3 panel-card p-3 transition-all ${isCritical ? 'gauge-warning' : ''}`}> |
| |
| {/* ── Live Sandbox Telemetry ── */} |
| <div className="space-y-2.5"> |
| <span className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider"> |
| Live Sandbox Telemetry |
| </span> |
| {GAUGES.map((gauge) => { |
| const value = typeof resolvedTelemetry[gauge.key] === 'number' ? resolvedTelemetry[gauge.key] : 0; |
| const pct = Math.min((value / gauge.max) * 100, 100); |
| const colors = getGaugeColor(value, gauge.thresholds); |
| return ( |
| <div key={gauge.key} className="space-y-1"> |
| <div className="flex items-center justify-between"> |
| <span className="text-[10px] text-zinc-500 font-mono">{gauge.label}</span> |
| <span className={`text-[10px] font-mono font-bold ${colors.text}`}> |
| {value.toFixed(0)}{gauge.unit} |
| </span> |
| </div> |
| <div className="w-full h-1.5 bg-zinc-800 rounded-full overflow-hidden"> |
| <motion.div |
| className={`h-full ${colors.bar} rounded-full`} |
| animate={{ width: `${pct}%` }} |
| transition={{ duration: 0.5, ease: 'easeOut' }} |
| /> |
| </div> |
| </div> |
| ); |
| })} |
| </div> |
| |
| <div className="h-px bg-zinc-800" /> |
| |
| {/* ── VRAM Reasoning Trace (think stream) ── */} |
| <div className="flex flex-col gap-1.5"> |
| <div className="flex items-center justify-between"> |
| <span className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider"> |
| VRAM Math |
| </span> |
| <span className="text-[9px] font-mono text-zinc-600"> |
| {reasoningTrace.length > 0 ? `${reasoningTrace.length} steps` : 'awaiting run…'} |
| </span> |
| </div> |
| |
| {/* Terminal window */} |
| <div className="rounded-md border border-zinc-800 bg-[#0d0d0d] overflow-hidden"> |
| {/* Chrome bar — no live badge, neutral */} |
| <div className="flex items-center gap-1.5 px-2.5 py-1.5 bg-zinc-900/80 border-b border-zinc-800"> |
| <span className="w-1.5 h-1.5 rounded-full bg-zinc-700" /> |
| <span className="w-1.5 h-1.5 rounded-full bg-zinc-700" /> |
| <span className="w-1.5 h-1.5 rounded-full bg-zinc-700" /> |
| <span className="ml-2 text-[9px] font-mono text-zinc-600"><think> stream</span> |
| </div> |
| |
| {/* Scrollable think log */} |
| <div className="h-[210px] overflow-y-auto scrollbar-thin scrollbar-thumb-zinc-800 scrollbar-track-transparent p-2.5 space-y-3 font-mono"> |
| {reasoningTrace.length === 0 ? ( |
| <p className="text-[9px] text-zinc-700 italic pt-1"> |
| The model's VRAM reasoning will appear here as it evaluates each step against the 500MB sandbox limit… |
| </p> |
| ) : ( |
| reasoningTrace.map((entry) => ( |
| <div key={entry.id} className="space-y-0.5"> |
| {/* Entry header: timestamp + agent */} |
| <div className="flex items-center gap-2 mb-1"> |
| <span className="text-[8px] text-zinc-700">{entry.ts}</span> |
| <span className="text-[8px] font-bold text-zinc-600 uppercase tracking-widest"> |
| {entry.agent} |
| </span> |
| </div> |
| {/* Render each line of the think block */} |
| {entry.text.split('\n').map((line, i) => ( |
| <ThinkLine key={i} line={line} /> |
| ))} |
| </div> |
| )) |
| )} |
| <div ref={traceEndRef} /> |
| </div> |
| </div> |
| </div> |
| |
| <div className="h-px bg-zinc-800" /> |
| |
| {/* ── Validator Runtime ── */} |
| <div className="space-y-1.5"> |
| <div className="flex items-center justify-between"> |
| <span className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider">Validator Runtime</span> |
| <ContainerStatusBadge status={telemetry.containerStatus} /> |
| </div> |
| <div className="grid grid-cols-1 gap-1.5 text-[10px]"> |
| <InfoRow label="VALIDATOR" value={validatorLabel} /> |
| <InfoRow label="MODE" value={validatorMode} /> |
| <InfoRow |
| label="STATUS" |
| value={validatorStatus.toUpperCase()} |
| valueClass={isPass ? 'text-emerald-400 font-bold' : 'text-zinc-400 font-bold'} |
| /> |
| <InfoRow label="DETAIL" value={validatorDetail} valueClass="text-zinc-400" /> |
| </div> |
| </div> |
| |
| <div className="h-px bg-zinc-800" /> |
| |
| {/* ── Pre-Flight Check ── */} |
| <div className="space-y-1.5"> |
| <span className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider">Pre-Flight Check</span> |
| <PreFlightItem label="FinOps Budget" status={preflight.budget} /> |
| <PreFlightItem label="No SPOF" status={preflight.spof} /> |
| <PreFlightItem label="SLA Window" status={preflight.sla} /> |
| </div> |
| </div> |
| ); |
| } |
|
|
| function ContainerStatusBadge({ status }) { |
| const info = STATUS_MAP[status] || STATUS_MAP.idle; |
| return ( |
| <div className="flex items-center gap-1.5"> |
| <div className={`w-1.5 h-1.5 rounded-full ${info.color} ${info.pulse ? 'status-dot-live' : ''}`} /> |
| <span className="text-[9px] font-mono text-zinc-500">{info.label}</span> |
| </div> |
| ); |
| } |
|
|
| function PreFlightItem({ label, status }) { |
| const icon = status === null ? '○' : status ? '✓' : '✗'; |
| const color = status === null ? 'text-zinc-600' : status ? 'text-emerald-400' : 'text-red-400'; |
| return ( |
| <div className="flex items-center gap-1.5"> |
| <span className={`text-xs font-mono ${color}`}>{icon}</span> |
| <span className="text-[10px] text-zinc-500">{label}</span> |
| </div> |
| ); |
| } |
|
|
| function InfoRow({ label, value, valueClass = 'text-zinc-200' }) { |
| return ( |
| <div className="flex items-center justify-between gap-3 py-1 border-b border-zinc-800/60 last:border-b-0"> |
| <span className="text-[9px] text-zinc-600 uppercase tracking-widest font-mono shrink-0">{label}</span> |
| <span className={`text-[10px] font-mono text-right break-words ${valueClass}`}>{value}</span> |
| </div> |
| ); |
| } |
|
|