File size: 7,952 Bytes
1d68c54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
"use client";

import { useRef, useEffect } from "react";

type AgentData = { id: string; trust: number; type: string };

type Props = {
  trustSnapshot: Record<string, number>;
  adversarialAgents: Set<string>;
  activeSpec: string | null;
};

const AGENT_ANGLES = [0, 72, 144, 216, 288];
const COLORS: Record<string, string> = {
  orch: "#00F5FF", normal: "#00FF88", degraded: "#FFB800", adversarial: "#FF2D55",
};

function hexToRgb(hex: string) {
  if (hex === "#00F5FF") return "0,245,255";
  if (hex === "#00FF88") return "0,255,136";
  if (hex === "#FFB800") return "255,184,0";
  if (hex === "#FF2D55") return "255,45,85";
  return "0,200,255";
}

function agentType(id: string, trust: number, isAdv: boolean): string {
  if (isAdv) return "adversarial";
  if (trust < 0.4) return "degraded";
  return "normal";
}

export default function SimCanvas({ trustSnapshot, adversarialAgents, activeSpec }: Props) {
  const ref = useRef<HTMLCanvasElement>(null);
  const dataRef = useRef({ trustSnapshot, adversarialAgents, activeSpec });

  useEffect(() => {
    dataRef.current = { trustSnapshot, adversarialAgents, activeSpec };
  }, [trustSnapshot, adversarialAgents, activeSpec]);

  useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    let W = 0, H = 0, tick = 0, animId = 0;
    type Packet = { agentIdx: number; progress: number; dir: number };
    let dataPackets: Packet[] = [];

    function resize() {
      W = canvas!.width = canvas!.offsetWidth;
      H = canvas!.height = canvas!.offsetHeight;
    }

    function getPos(angle: number, cx: number, cy: number, r: number) {
      const rad = ((angle - 90) * Math.PI) / 180;
      return { x: cx + Math.cos(rad) * r, y: cy + Math.sin(rad) * r };
    }

    function draw() {
      const { trustSnapshot: ts, adversarialAgents: advSet, activeSpec: active } = dataRef.current;
      ctx!.clearRect(0, 0, W, H);
      tick++;

      const cx = W / 2, cy = H / 2;
      const outerR = Math.min(W, H) * 0.35;
      const orchR = 30, agentR = 18;

      const agents: AgentData[] = ["S0", "S1", "S2", "S3", "S4"].map((id, i) => ({
        id, trust: ts[id] ?? 0.5,
        type: i === 0 ? "orch" : agentType(id, ts[id] ?? 0.5, advSet.has(id)),
      }));

      // Grid
      ctx!.strokeStyle = "rgba(0,100,200,0.04)";
      ctx!.lineWidth = 0.5;
      for (let x = 0; x < W; x += 40) { ctx!.beginPath(); ctx!.moveTo(x, 0); ctx!.lineTo(x, H); ctx!.stroke(); }
      for (let y = 0; y < H; y += 40) { ctx!.beginPath(); ctx!.moveTo(0, y); ctx!.lineTo(W, y); ctx!.stroke(); }

      // Orbit ring
      ctx!.beginPath(); ctx!.arc(cx, cy, outerR, 0, Math.PI * 2);
      ctx!.strokeStyle = "rgba(0,200,255,0.06)"; ctx!.lineWidth = 1;
      ctx!.setLineDash([4, 8]); ctx!.stroke(); ctx!.setLineDash([]);

      // Scanning ring
      const scanAngle = (tick * 0.02) % (Math.PI * 2);
      ctx!.save(); ctx!.translate(cx, cy); ctx!.rotate(scanAngle);
      const scanArc = ctx!.createLinearGradient(-outerR, 0, outerR, 0);
      scanArc.addColorStop(0, "rgba(0,200,255,0)");
      scanArc.addColorStop(1, "rgba(0,200,255,0.08)");
      ctx!.beginPath(); ctx!.moveTo(0, 0); ctx!.arc(0, 0, outerR, -0.4, 0); ctx!.closePath();
      ctx!.fillStyle = scanArc; ctx!.fill(); ctx!.restore();

      // Connections from orch to agents
      const orchPos = { x: cx, y: cy };
      agents.slice(1).forEach((a, i) => {
        const pos = getPos(AGENT_ANGLES[i + 1], cx, cy, outerR);
        const col = COLORS[a.type] || COLORS.normal;
        const alpha = a.type === "adversarial" ? 0.12 : a.trust * 0.35;
        const isDash = a.type === "adversarial";
        if (isDash) ctx!.setLineDash([4, 6]); else ctx!.setLineDash([]);
        ctx!.beginPath(); ctx!.moveTo(orchPos.x, orchPos.y); ctx!.lineTo(pos.x, pos.y);
        ctx!.strokeStyle = `rgba(${hexToRgb(col)},${alpha})`;
        ctx!.lineWidth = a.trust * 2; ctx!.stroke(); ctx!.setLineDash([]);
      });

      // Data packets
      if (tick % 20 === 0) {
        const ai = Math.floor(Math.random() * 4) + 1;
        dataPackets.push({ agentIdx: ai, progress: 0, dir: Math.random() > 0.5 ? 1 : -1 });
      }
      dataPackets = dataPackets.filter(p => p.progress <= 1);
      dataPackets.forEach(p => {
        p.progress += 0.025;
        const a = agents[p.agentIdx];
        const pos = getPos(AGENT_ANGLES[p.agentIdx], cx, cy, outerR);
        const t = p.progress;
        const px = orchPos.x + (pos.x - orchPos.x) * (p.dir > 0 ? t : 1 - t);
        const py = orchPos.y + (pos.y - orchPos.y) * (p.dir > 0 ? t : 1 - t);
        const col = COLORS[a.type] || COLORS.normal;
        ctx!.beginPath(); ctx!.arc(px, py, 3, 0, Math.PI * 2); ctx!.fillStyle = col; ctx!.fill();
        const g = ctx!.createRadialGradient(px, py, 0, px, py, 8);
        g.addColorStop(0, col); g.addColorStop(1, "transparent");
        ctx!.beginPath(); ctx!.arc(px, py, 8, 0, Math.PI * 2); ctx!.fillStyle = g; ctx!.fill();
      });

      // Outer agents
      agents.slice(1).forEach((a, i) => {
        const pos = getPos(AGENT_ANGLES[i + 1], cx, cy, outerR);
        const col = COLORS[a.type] || COLORS.normal;
        const pulse = 0.7 + 0.3 * Math.sin(tick * 0.05 + AGENT_ANGLES[i + 1]);
        const isActive = active === a.id;

        // Glow
        const g = ctx!.createRadialGradient(pos.x, pos.y, 0, pos.x, pos.y, agentR * 2.5);
        g.addColorStop(0, `rgba(${hexToRgb(col)},${(isActive ? 0.35 : 0.2) * pulse})`);
        g.addColorStop(1, "transparent");
        ctx!.beginPath(); ctx!.arc(pos.x, pos.y, agentR * 2.5, 0, Math.PI * 2);
        ctx!.fillStyle = g; ctx!.fill();

        // Node circle
        ctx!.beginPath(); ctx!.arc(pos.x, pos.y, agentR, 0, Math.PI * 2);
        ctx!.fillStyle = `rgba(${hexToRgb(col)},0.1)`; ctx!.fill();
        ctx!.strokeStyle = `rgba(${hexToRgb(col)},${0.6 * pulse})`;
        ctx!.lineWidth = isActive ? 2.5 : 1.5; ctx!.stroke();

        // Adversarial warning ring
        if (a.type === "adversarial") {
          ctx!.beginPath();
          ctx!.arc(pos.x, pos.y, agentR + 6 + Math.sin(tick * 0.1) * 3, 0, Math.PI * 2);
          ctx!.strokeStyle = `rgba(255,45,85,${0.3 * pulse})`;
          ctx!.lineWidth = 1; ctx!.setLineDash([3, 4]); ctx!.stroke(); ctx!.setLineDash([]);
        }

        // Labels
        ctx!.font = '9px "Share Tech Mono"'; ctx!.fillStyle = col; ctx!.textAlign = "center";
        ctx!.fillText(a.id, pos.x, pos.y - agentR - 8);
        ctx!.fillStyle = "rgba(232,244,255,0.3)";
        ctx!.fillText(a.trust.toFixed(2), pos.x, pos.y + 4);
      });

      // Orchestrator
      const orchPulse = 0.7 + 0.3 * Math.sin(tick * 0.04);
      const orchG = ctx!.createRadialGradient(cx, cy, 0, cx, cy, orchR * 3);
      orchG.addColorStop(0, `rgba(0,245,255,${0.15 * orchPulse})`);
      orchG.addColorStop(1, "transparent");
      ctx!.beginPath(); ctx!.arc(cx, cy, orchR * 3, 0, Math.PI * 2);
      ctx!.fillStyle = orchG; ctx!.fill();

      ctx!.beginPath(); ctx!.arc(cx, cy, orchR, 0, Math.PI * 2);
      ctx!.fillStyle = "rgba(0,245,255,0.1)"; ctx!.fill();
      ctx!.strokeStyle = `rgba(0,245,255,${0.8 * orchPulse})`;
      ctx!.lineWidth = 2; ctx!.stroke();

      ctx!.beginPath(); ctx!.arc(cx, cy, orchR * 0.6, 0, Math.PI * 2);
      ctx!.strokeStyle = `rgba(0,245,255,${0.4 * orchPulse})`;
      ctx!.lineWidth = 1; ctx!.stroke();

      ctx!.font = 'bold 9px "Share Tech Mono"'; ctx!.fillStyle = "#00F5FF";
      ctx!.textAlign = "center"; ctx!.fillText("S0", cx, cy - 4);
      ctx!.font = '8px "Share Tech Mono"'; ctx!.fillStyle = "rgba(0,245,255,0.5)";
      ctx!.fillText("ORCH", cx, cy + 8);

      animId = requestAnimationFrame(draw);
    }

    const ro = new ResizeObserver(() => resize());
    ro.observe(canvas.parentElement!);
    resize(); draw();

    return () => { cancelAnimationFrame(animId); ro.disconnect(); };
  }, []);

  return <canvas ref={ref} />;
}