alignscope 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alignscope/__init__.py +150 -0
- alignscope/_frontend/css/style.css +663 -0
- alignscope/_frontend/index.html +169 -0
- alignscope/_frontend/js/app.js +360 -0
- alignscope/_frontend/js/metrics.js +220 -0
- alignscope/_frontend/js/timeline.js +494 -0
- alignscope/_frontend/js/topology.js +368 -0
- alignscope/adapters.py +169 -0
- alignscope/cli.py +99 -0
- alignscope/detector.py +242 -0
- alignscope/integrations/__init__.py +28 -0
- alignscope/integrations/mlflow_bridge.py +70 -0
- alignscope/integrations/wandb_bridge.py +81 -0
- alignscope/metrics.py +383 -0
- alignscope/patches/__init__.py +50 -0
- alignscope/patches/pettingzoo.py +332 -0
- alignscope/patches/pymarl.py +277 -0
- alignscope/patches/rllib.py +170 -0
- alignscope/sdk.py +606 -0
- alignscope/server.py +298 -0
- alignscope/simulator.py +493 -0
- alignscope-0.1.0.dist-info/METADATA +183 -0
- alignscope-0.1.0.dist-info/RECORD +26 -0
- alignscope-0.1.0.dist-info/WHEEL +4 -0
- alignscope-0.1.0.dist-info/entry_points.txt +2 -0
- alignscope-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AlignScope — MARL Alignment Observability Dashboard</title>
|
|
7
|
+
<meta name="description" content="Real-time alignment observability for any multi-agent reinforcement learning system. Track coalition formation, role specialization, and defection events across episodes.">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
11
|
+
<link rel="stylesheet" href="/css/style.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<!-- Header -->
|
|
15
|
+
<header id="header">
|
|
16
|
+
<div class="header-left">
|
|
17
|
+
<h1 class="logo">Align<span class="logo-accent">Scope</span></h1>
|
|
18
|
+
<span class="header-divider"></span>
|
|
19
|
+
<span class="header-subtitle">MARL alignment observability</span>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="header-center">
|
|
22
|
+
<div class="status-group">
|
|
23
|
+
<div class="status-item" id="tick-display">
|
|
24
|
+
<span class="status-label">Tick</span>
|
|
25
|
+
<span class="status-value mono" id="tick-value">0</span>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="status-item" id="alignment-display">
|
|
28
|
+
<span class="status-label">Alignment</span>
|
|
29
|
+
<span class="status-value mono" id="alignment-value">—</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="status-item" id="events-display">
|
|
32
|
+
<span class="status-label">Events</span>
|
|
33
|
+
<span class="status-value mono" id="events-count">0</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="header-right">
|
|
38
|
+
<button class="btn btn-secondary" id="btn-restart" title="Restart with new seed">
|
|
39
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
|
|
40
|
+
Restart
|
|
41
|
+
</button>
|
|
42
|
+
<div class="connection-status" id="connection-status">
|
|
43
|
+
<span class="connection-dot"></span>
|
|
44
|
+
<span class="connection-text">Connecting</span>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</header>
|
|
48
|
+
|
|
49
|
+
<!-- Main Layout: 3 panels -->
|
|
50
|
+
<main id="main-layout">
|
|
51
|
+
<!-- Left: Topology Graph -->
|
|
52
|
+
<section class="panel panel-topology" id="panel-topology">
|
|
53
|
+
<div class="panel-header">
|
|
54
|
+
<h2 class="panel-title">Agent Topology</h2>
|
|
55
|
+
<div class="panel-controls">
|
|
56
|
+
<span class="panel-badge" id="topology-badge">0 nodes</span>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="panel-body" id="topology-container">
|
|
60
|
+
<svg id="topology-svg"></svg>
|
|
61
|
+
<!-- Legend — dynamically populated from config -->
|
|
62
|
+
<div class="topology-legend" id="topology-legend">
|
|
63
|
+
<div class="legend-section" id="legend-teams">
|
|
64
|
+
<span class="legend-title">Teams</span>
|
|
65
|
+
<!-- Populated dynamically by app.js -->
|
|
66
|
+
</div>
|
|
67
|
+
<div class="legend-section" id="legend-roles">
|
|
68
|
+
<span class="legend-title">Roles</span>
|
|
69
|
+
<!-- Populated dynamically by app.js -->
|
|
70
|
+
</div>
|
|
71
|
+
<div class="legend-section">
|
|
72
|
+
<span class="legend-title">Edges</span>
|
|
73
|
+
<div class="legend-item">
|
|
74
|
+
<span class="legend-line" style="opacity: 0.3"></span>
|
|
75
|
+
<span>Weak bond</span>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="legend-item">
|
|
78
|
+
<span class="legend-line" style="opacity: 1"></span>
|
|
79
|
+
<span>Strong bond</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<!-- Right: Metrics Sidebar -->
|
|
87
|
+
<aside class="panel panel-metrics" id="panel-metrics">
|
|
88
|
+
<div class="panel-header">
|
|
89
|
+
<h2 class="panel-title" id="metrics-panel-title">Alignment Metrics</h2>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="panel-body" id="metrics-container">
|
|
92
|
+
<!-- Overall Alignment Score -->
|
|
93
|
+
<div class="metric-card" id="metric-overall">
|
|
94
|
+
<div class="metric-header">
|
|
95
|
+
<span class="metric-name">Overall Alignment</span>
|
|
96
|
+
<span class="metric-value mono" id="metric-overall-value">—</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="metric-bar-container">
|
|
99
|
+
<div class="metric-bar" id="metric-overall-bar"></div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Team metric sections — dynamically generated by app.js -->
|
|
104
|
+
<div id="team-metrics-container"></div>
|
|
105
|
+
|
|
106
|
+
<!-- Sparkline area for alignment history -->
|
|
107
|
+
<div class="metric-card" id="metric-history">
|
|
108
|
+
<div class="metric-header">
|
|
109
|
+
<span class="metric-name">Alignment History</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="sparkline-container">
|
|
112
|
+
<canvas id="alignment-sparkline" width="280" height="80"></canvas>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- Agent detail panel (shown on node click) -->
|
|
117
|
+
<div class="metric-card agent-detail hidden" id="agent-detail">
|
|
118
|
+
<div class="metric-header">
|
|
119
|
+
<span class="metric-name">Agent <span id="detail-agent-id">—</span></span>
|
|
120
|
+
<button class="btn-close" id="btn-close-detail">×</button>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="metric-row">
|
|
123
|
+
<span class="metric-label">Team</span>
|
|
124
|
+
<span class="metric-val mono" id="detail-team">—</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="metric-row">
|
|
127
|
+
<span class="metric-label">Role</span>
|
|
128
|
+
<span class="metric-val mono" id="detail-role">—</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="metric-row">
|
|
131
|
+
<span class="metric-label">Role Stability</span>
|
|
132
|
+
<span class="metric-val mono" id="detail-stability">—</span>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="metric-row">
|
|
135
|
+
<span class="metric-label">Coalition</span>
|
|
136
|
+
<span class="metric-val mono" id="detail-coalition">—</span>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="metric-row">
|
|
139
|
+
<span class="metric-label">Status</span>
|
|
140
|
+
<span class="metric-val" id="detail-status">Active</span>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</aside>
|
|
145
|
+
</main>
|
|
146
|
+
|
|
147
|
+
<!-- Bottom: Defection Timeline -->
|
|
148
|
+
<section class="panel panel-timeline" id="panel-timeline">
|
|
149
|
+
<div class="panel-header">
|
|
150
|
+
<h2 class="panel-title" id="timeline-panel-title">Event Timeline</h2>
|
|
151
|
+
<span class="panel-badge" id="timeline-badge">0 events</span>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="panel-body timeline-body" id="timeline-container">
|
|
154
|
+
<canvas id="timeline-canvas"></canvas>
|
|
155
|
+
<!-- Event detail tooltip -->
|
|
156
|
+
<div class="timeline-tooltip hidden" id="timeline-tooltip">
|
|
157
|
+
<div class="tooltip-header" id="tooltip-header">—</div>
|
|
158
|
+
<div class="tooltip-body" id="tooltip-body">—</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</section>
|
|
162
|
+
|
|
163
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
164
|
+
<script src="/js/app.js?v=2"></script>
|
|
165
|
+
<script src="/js/topology.js?v=2"></script>
|
|
166
|
+
<script src="/js/timeline.js?v=2"></script>
|
|
167
|
+
<script src="/js/metrics.js?v=2"></script>
|
|
168
|
+
</body>
|
|
169
|
+
</html>
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AlignScope — Main Application Controller
|
|
3
|
+
*
|
|
4
|
+
* Manages WebSocket connection, state, and coordinates
|
|
5
|
+
* the three dashboard panels (topology, timeline, metrics).
|
|
6
|
+
*
|
|
7
|
+
* Dynamically builds UI elements based on the config message
|
|
8
|
+
* received from the backend — supports any number of teams and roles.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const AlignScope = {
|
|
12
|
+
ws: null,
|
|
13
|
+
config: null,
|
|
14
|
+
state: {
|
|
15
|
+
tick: 0,
|
|
16
|
+
agents: [],
|
|
17
|
+
relationships: [],
|
|
18
|
+
events: [],
|
|
19
|
+
metrics: null,
|
|
20
|
+
teamScores: {},
|
|
21
|
+
alignmentHistory: [],
|
|
22
|
+
isConnected: false,
|
|
23
|
+
selectedAgent: null,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Default color palette for teams (expandable)
|
|
27
|
+
teamColors: [
|
|
28
|
+
'#6d9eeb', '#e8925a', '#4abe7d', '#b06ec7',
|
|
29
|
+
'#d4a843', '#dc4a4a', '#5bc0de', '#8cc152',
|
|
30
|
+
],
|
|
31
|
+
|
|
32
|
+
// Role shape symbols (unicode) for the legend
|
|
33
|
+
roleShapeSymbols: ['●', '◆', '■', '▲', '★', '⬟', '⬠', '◈'],
|
|
34
|
+
|
|
35
|
+
init() {
|
|
36
|
+
this.connectWebSocket();
|
|
37
|
+
this.bindUI();
|
|
38
|
+
window.addEventListener('resize', () => {
|
|
39
|
+
TopologyGraph.resize();
|
|
40
|
+
TimelineView.resize();
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
connectWebSocket() {
|
|
45
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
46
|
+
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
|
47
|
+
|
|
48
|
+
this.ws = new WebSocket(wsUrl);
|
|
49
|
+
this.updateConnectionStatus('connecting');
|
|
50
|
+
|
|
51
|
+
this.ws.onopen = () => {
|
|
52
|
+
this.updateConnectionStatus('connected');
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this.ws.onmessage = (event) => {
|
|
56
|
+
const msg = JSON.parse(event.data);
|
|
57
|
+
this.handleMessage(msg);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
this.ws.onclose = () => {
|
|
61
|
+
this.updateConnectionStatus('disconnected');
|
|
62
|
+
setTimeout(() => this.connectWebSocket(), 2000);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.ws.onerror = () => {
|
|
66
|
+
this.updateConnectionStatus('disconnected');
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
handleMessage(msg) {
|
|
71
|
+
switch (msg.type) {
|
|
72
|
+
case 'config':
|
|
73
|
+
this.config = msg.data;
|
|
74
|
+
this.buildDynamicUI(msg.data);
|
|
75
|
+
try { TopologyGraph.init(this.config); } catch(e) { console.error('TopologyGraph.init error:', e); }
|
|
76
|
+
try { TimelineView.init(this.config); } catch(e) { console.error('TimelineView.init error:', e); }
|
|
77
|
+
try { MetricsPanel.init(this.config); } catch(e) { console.error('MetricsPanel.init error:', e); }
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case 'tick':
|
|
81
|
+
this.processTick(msg.data);
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'restart':
|
|
85
|
+
this.resetState();
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case 'episode_complete':
|
|
89
|
+
this.handleEpisodeComplete(msg.data);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Dynamically build UI elements from the config payload.
|
|
96
|
+
* This makes the dashboard work with any team/role configuration.
|
|
97
|
+
*/
|
|
98
|
+
buildDynamicUI(config) {
|
|
99
|
+
// Update panel titles based on paradigm if available
|
|
100
|
+
if (config.paradigm && config.paradigm.environment) {
|
|
101
|
+
const envName = config.paradigm.environment.charAt(0).toUpperCase() + config.paradigm.environment.slice(1);
|
|
102
|
+
const metricsTitle = document.getElementById('metrics-panel-title');
|
|
103
|
+
if (metricsTitle) metricsTitle.textContent = `${envName} Metrics`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Build team legend
|
|
107
|
+
const teamLegend = document.getElementById('legend-teams');
|
|
108
|
+
// Keep the title, remove old items
|
|
109
|
+
const teamTitle = teamLegend.querySelector('.legend-title');
|
|
110
|
+
teamLegend.innerHTML = '';
|
|
111
|
+
teamLegend.appendChild(teamTitle);
|
|
112
|
+
|
|
113
|
+
(config.teams || []).forEach((team, idx) => {
|
|
114
|
+
const color = team.color || this.teamColors[idx % this.teamColors.length];
|
|
115
|
+
const item = document.createElement('div');
|
|
116
|
+
item.className = 'legend-item';
|
|
117
|
+
item.innerHTML = `
|
|
118
|
+
<span class="legend-dot" style="background: ${color}"></span>
|
|
119
|
+
<span>${team.name}</span>
|
|
120
|
+
`;
|
|
121
|
+
teamLegend.appendChild(item);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Build role legend
|
|
125
|
+
const roleLegend = document.getElementById('legend-roles');
|
|
126
|
+
const roleTitle = roleLegend.querySelector('.legend-title');
|
|
127
|
+
roleLegend.innerHTML = '';
|
|
128
|
+
roleLegend.appendChild(roleTitle);
|
|
129
|
+
|
|
130
|
+
(config.roles || []).forEach((role, idx) => {
|
|
131
|
+
const symbol = this.roleShapeSymbols[idx % this.roleShapeSymbols.length];
|
|
132
|
+
const item = document.createElement('div');
|
|
133
|
+
item.className = 'legend-item';
|
|
134
|
+
item.innerHTML = `
|
|
135
|
+
<span class="legend-shape">${symbol}</span>
|
|
136
|
+
<span>${role.charAt(0).toUpperCase() + role.slice(1)}</span>
|
|
137
|
+
`;
|
|
138
|
+
roleLegend.appendChild(item);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Build team metric sections in the sidebar
|
|
142
|
+
const container = document.getElementById('team-metrics-container');
|
|
143
|
+
container.innerHTML = '';
|
|
144
|
+
|
|
145
|
+
(config.teams || []).forEach((team, idx) => {
|
|
146
|
+
const color = team.color || this.teamColors[idx % this.teamColors.length];
|
|
147
|
+
const teamId = `team-${idx}`;
|
|
148
|
+
|
|
149
|
+
const section = document.createElement('div');
|
|
150
|
+
section.className = 'metric-group';
|
|
151
|
+
section.id = `${teamId}-metrics`;
|
|
152
|
+
|
|
153
|
+
let html = `
|
|
154
|
+
<div class="metric-group-header">
|
|
155
|
+
<span class="team-dot" style="background: ${color}"></span>
|
|
156
|
+
<span class="metric-group-title">${team.name} (Team ${idx})</span>
|
|
157
|
+
</div>
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
// Generate standard metrics from config
|
|
161
|
+
(config.metrics || []).forEach(m => {
|
|
162
|
+
html += `
|
|
163
|
+
<div class="metric-row">
|
|
164
|
+
<span class="metric-label">${m.label}</span>
|
|
165
|
+
<span class="metric-val mono" id="${teamId}-metric-${m.id}">—</span>
|
|
166
|
+
</div>`;
|
|
167
|
+
|
|
168
|
+
// Preserve mini-bar explicitly for role_stability if present
|
|
169
|
+
if (m.id === 'role_stability') {
|
|
170
|
+
html += `
|
|
171
|
+
<div class="metric-mini-bar-container">
|
|
172
|
+
<div class="metric-mini-bar" id="${teamId}-stability-bar" style="background: ${color}"></div>
|
|
173
|
+
</div>`;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
section.innerHTML = html;
|
|
178
|
+
container.appendChild(section);
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
processTick(data) {
|
|
183
|
+
this.state.tick = data.tick || this.state.tick;
|
|
184
|
+
|
|
185
|
+
if (data.agents && data.agents.length > 0) {
|
|
186
|
+
this.state.agents = data.agents;
|
|
187
|
+
}
|
|
188
|
+
if (data.relationships && data.relationships.length > 0) {
|
|
189
|
+
this.state.relationships = data.relationships;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (data.team_scores && Object.keys(data.team_scores).length > 0) {
|
|
193
|
+
this.state.teamScores = { ...this.state.teamScores, ...data.team_scores };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Merge metrics seamlessly to support partial update logging
|
|
197
|
+
if (data.metrics) {
|
|
198
|
+
if (!this.state.metrics) this.state.metrics = { agent_metrics: {}, team_metrics: {}, pair_metrics: [] };
|
|
199
|
+
|
|
200
|
+
if (data.metrics.agent_metrics) {
|
|
201
|
+
Object.keys(data.metrics.agent_metrics).forEach(aId => {
|
|
202
|
+
this.state.metrics.agent_metrics[aId] = {
|
|
203
|
+
...(this.state.metrics.agent_metrics[aId] || {}),
|
|
204
|
+
...data.metrics.agent_metrics[aId]
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (data.metrics.team_metrics) {
|
|
209
|
+
Object.keys(data.metrics.team_metrics).forEach(tId => {
|
|
210
|
+
this.state.metrics.team_metrics[tId] = {
|
|
211
|
+
...(this.state.metrics.team_metrics[tId] || {}),
|
|
212
|
+
...data.metrics.team_metrics[tId]
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (data.metrics.overall_alignment_score !== undefined) {
|
|
217
|
+
this.state.metrics.overall_alignment_score = data.metrics.overall_alignment_score;
|
|
218
|
+
this.state.alignmentHistory.push({
|
|
219
|
+
tick: data.tick,
|
|
220
|
+
score: data.metrics.overall_alignment_score,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (data.events && data.events.length > 0) {
|
|
226
|
+
this.state.events.push(...data.events);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Alignment history logic was moved to merge section above
|
|
230
|
+
|
|
231
|
+
this.updateHeader(data);
|
|
232
|
+
|
|
233
|
+
try { TopologyGraph.update(data); } catch(e) { console.error('TopologyGraph error:', e); }
|
|
234
|
+
try { TimelineView.update(data); } catch(e) { console.error('TimelineView error:', e); }
|
|
235
|
+
try { MetricsPanel.update(data); } catch(e) { console.error('MetricsPanel error:', e); }
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
updateHeader(data) {
|
|
239
|
+
document.getElementById('tick-value').textContent = data.tick;
|
|
240
|
+
|
|
241
|
+
if (data.metrics) {
|
|
242
|
+
const score = data.metrics.overall_alignment_score;
|
|
243
|
+
const el = document.getElementById('alignment-value');
|
|
244
|
+
el.textContent = score.toFixed(3);
|
|
245
|
+
if (score > 0.8) el.style.color = 'var(--color-success)';
|
|
246
|
+
else if (score > 0.5) el.style.color = 'var(--text-primary)';
|
|
247
|
+
else el.style.color = 'var(--color-defection)';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
document.getElementById('events-count').textContent = this.state.events.length;
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
updateConnectionStatus(status) {
|
|
254
|
+
const el = document.getElementById('connection-status');
|
|
255
|
+
const textEl = el.querySelector('.connection-text');
|
|
256
|
+
|
|
257
|
+
el.className = 'connection-status ' + status;
|
|
258
|
+
|
|
259
|
+
switch (status) {
|
|
260
|
+
case 'connected':
|
|
261
|
+
textEl.textContent = 'Live';
|
|
262
|
+
this.state.isConnected = true;
|
|
263
|
+
break;
|
|
264
|
+
case 'disconnected':
|
|
265
|
+
textEl.textContent = 'Disconnected';
|
|
266
|
+
this.state.isConnected = false;
|
|
267
|
+
break;
|
|
268
|
+
default:
|
|
269
|
+
textEl.textContent = 'Connecting…';
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
bindUI() {
|
|
274
|
+
document.getElementById('btn-restart').addEventListener('click', () => {
|
|
275
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
276
|
+
this.ws.send(JSON.stringify({ action: 'restart' }));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
document.getElementById('btn-close-detail').addEventListener('click', () => {
|
|
281
|
+
document.getElementById('agent-detail').classList.add('hidden');
|
|
282
|
+
this.state.selectedAgent = null;
|
|
283
|
+
TopologyGraph.clearSelection();
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
resetState() {
|
|
288
|
+
this.state.tick = 0;
|
|
289
|
+
this.state.agents = [];
|
|
290
|
+
this.state.relationships = [];
|
|
291
|
+
this.state.events = [];
|
|
292
|
+
this.state.metrics = null;
|
|
293
|
+
this.state.teamScores = {};
|
|
294
|
+
this.state.alignmentHistory = [];
|
|
295
|
+
this.state.selectedAgent = null;
|
|
296
|
+
|
|
297
|
+
document.getElementById('tick-value').textContent = '0';
|
|
298
|
+
document.getElementById('alignment-value').textContent = '—';
|
|
299
|
+
document.getElementById('events-count').textContent = '0';
|
|
300
|
+
document.getElementById('agent-detail').classList.add('hidden');
|
|
301
|
+
|
|
302
|
+
TopologyGraph.reset();
|
|
303
|
+
TimelineView.reset();
|
|
304
|
+
MetricsPanel.reset();
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
handleEpisodeComplete(data) {
|
|
308
|
+
console.log('Episode complete:', data);
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get team name from config by team index.
|
|
313
|
+
*/
|
|
314
|
+
getTeamName(teamIdx) {
|
|
315
|
+
if (this.config && this.config.teams && this.config.teams[teamIdx]) {
|
|
316
|
+
return this.config.teams[teamIdx].name;
|
|
317
|
+
}
|
|
318
|
+
return `Team ${teamIdx}`;
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get team color from config by team index.
|
|
323
|
+
*/
|
|
324
|
+
getTeamColor(teamIdx) {
|
|
325
|
+
if (this.config && this.config.teams && this.config.teams[teamIdx]) {
|
|
326
|
+
return this.config.teams[teamIdx].color || this.teamColors[teamIdx % this.teamColors.length];
|
|
327
|
+
}
|
|
328
|
+
return this.teamColors[teamIdx % this.teamColors.length];
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
selectAgent(agentId) {
|
|
332
|
+
this.state.selectedAgent = agentId;
|
|
333
|
+
const agent = this.state.agents.find(a => a.agent_id === agentId);
|
|
334
|
+
if (!agent) return;
|
|
335
|
+
|
|
336
|
+
const metrics = this.state.metrics?.agent_metrics?.[agentId];
|
|
337
|
+
|
|
338
|
+
document.getElementById('detail-agent-id').textContent = agentId;
|
|
339
|
+
document.getElementById('detail-team').textContent = this.getTeamName(agent.team);
|
|
340
|
+
document.getElementById('detail-role').textContent = agent.role;
|
|
341
|
+
document.getElementById('detail-stability').textContent =
|
|
342
|
+
metrics ? metrics.role_stability.toFixed(4) : '—';
|
|
343
|
+
document.getElementById('detail-coalition').textContent =
|
|
344
|
+
agent.coalition_id >= 0 ? `Coalition ${agent.coalition_id}` : 'None (loner)';
|
|
345
|
+
|
|
346
|
+
const statusEl = document.getElementById('detail-status');
|
|
347
|
+
if (agent.is_defector) {
|
|
348
|
+
statusEl.textContent = 'Defected';
|
|
349
|
+
statusEl.style.color = 'var(--color-defection)';
|
|
350
|
+
} else {
|
|
351
|
+
statusEl.textContent = 'Active';
|
|
352
|
+
statusEl.style.color = 'var(--color-success)';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
document.getElementById('agent-detail').classList.remove('hidden');
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Boot
|
|
360
|
+
document.addEventListener('DOMContentLoaded', () => AlignScope.init());
|