pixelcrew 0.1.0
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.
- package/LICENSE +190 -0
- package/README.md +341 -0
- package/bin/pixel-agents.js +372 -0
- package/package.json +47 -0
- package/src/dashboard/app.js +1047 -0
- package/src/dashboard/index.html +232 -0
- package/src/dashboard/styles.css +892 -0
- package/src/index.js +3 -0
- package/src/orchestrator/engine.js +652 -0
- package/src/scaffold/analyzer.js +398 -0
- package/src/scaffold/init.js +155 -0
- package/src/scaffold/templates.js +384 -0
- package/src/server/server.js +232 -0
|
@@ -0,0 +1,1047 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PIXEL CORPS HQ โ Interactive Startup Office Engine & Pixel Art Canvas Renderer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Global State
|
|
6
|
+
const appState = {
|
|
7
|
+
config: null,
|
|
8
|
+
state: null,
|
|
9
|
+
events: [],
|
|
10
|
+
activeFilter: 'all',
|
|
11
|
+
audioEnabled: true,
|
|
12
|
+
crtEnabled: true,
|
|
13
|
+
nightMode: false,
|
|
14
|
+
missionStartTime: null,
|
|
15
|
+
missionTimerInterval: null,
|
|
16
|
+
hoveredDesk: null
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Workstations Layout on the Office Floor Canvas (960 x 420)
|
|
20
|
+
const WORKSTATIONS = {
|
|
21
|
+
orchestrator: {
|
|
22
|
+
id: 'orchestrator',
|
|
23
|
+
name: 'Tech Lead / Master CPU',
|
|
24
|
+
role: 'Staff Swarm Architect',
|
|
25
|
+
x: 60,
|
|
26
|
+
y: 60,
|
|
27
|
+
w: 180,
|
|
28
|
+
h: 130,
|
|
29
|
+
color: '#ffd700',
|
|
30
|
+
icon: '๐',
|
|
31
|
+
deskType: 'lead'
|
|
32
|
+
},
|
|
33
|
+
frontend: {
|
|
34
|
+
id: 'frontend',
|
|
35
|
+
name: 'Frontend Engineer',
|
|
36
|
+
role: 'Senior UI/UX & React Lead',
|
|
37
|
+
x: 280,
|
|
38
|
+
y: 60,
|
|
39
|
+
w: 170,
|
|
40
|
+
h: 130,
|
|
41
|
+
color: '#00f0ff',
|
|
42
|
+
icon: '๐จ',
|
|
43
|
+
deskType: 'frontend'
|
|
44
|
+
},
|
|
45
|
+
backend: {
|
|
46
|
+
id: 'backend',
|
|
47
|
+
name: 'Backend Engineer',
|
|
48
|
+
role: 'Principal API & Distributed Systems',
|
|
49
|
+
x: 480,
|
|
50
|
+
y: 60,
|
|
51
|
+
w: 170,
|
|
52
|
+
h: 130,
|
|
53
|
+
color: '#ff007f',
|
|
54
|
+
icon: 'โก',
|
|
55
|
+
deskType: 'backend'
|
|
56
|
+
},
|
|
57
|
+
database: {
|
|
58
|
+
id: 'database',
|
|
59
|
+
name: 'Database Architect',
|
|
60
|
+
role: 'Principal DBA & Query Tuning',
|
|
61
|
+
x: 680,
|
|
62
|
+
y: 60,
|
|
63
|
+
w: 220,
|
|
64
|
+
h: 130,
|
|
65
|
+
color: '#ffd700',
|
|
66
|
+
icon: '๐๏ธ',
|
|
67
|
+
deskType: 'database'
|
|
68
|
+
},
|
|
69
|
+
security: {
|
|
70
|
+
id: 'security',
|
|
71
|
+
name: 'Security Sentinel',
|
|
72
|
+
role: 'InfoSec Lead & OWASP Auditor',
|
|
73
|
+
x: 60,
|
|
74
|
+
y: 230,
|
|
75
|
+
w: 180,
|
|
76
|
+
h: 130,
|
|
77
|
+
color: '#ff3344',
|
|
78
|
+
icon: '๐ก๏ธ',
|
|
79
|
+
deskType: 'security'
|
|
80
|
+
},
|
|
81
|
+
performance: {
|
|
82
|
+
id: 'performance',
|
|
83
|
+
name: 'Performance SRE',
|
|
84
|
+
role: 'DevOps & CWV Optimization',
|
|
85
|
+
x: 280,
|
|
86
|
+
y: 230,
|
|
87
|
+
w: 170,
|
|
88
|
+
h: 130,
|
|
89
|
+
color: '#39ff14',
|
|
90
|
+
icon: '๐',
|
|
91
|
+
deskType: 'performance'
|
|
92
|
+
},
|
|
93
|
+
qa: {
|
|
94
|
+
id: 'qa',
|
|
95
|
+
name: 'QA Automation Lead',
|
|
96
|
+
role: 'End-to-End & Playwright Master',
|
|
97
|
+
x: 480,
|
|
98
|
+
y: 230,
|
|
99
|
+
w: 170,
|
|
100
|
+
h: 130,
|
|
101
|
+
color: '#b026ff',
|
|
102
|
+
icon: '๐งช',
|
|
103
|
+
deskType: 'qa'
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Retro Web Audio Synthesizer
|
|
108
|
+
class RetroAudioSynth {
|
|
109
|
+
constructor() {
|
|
110
|
+
this.ctx = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
initContext() {
|
|
114
|
+
if (!this.ctx) {
|
|
115
|
+
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
116
|
+
if (AudioCtx) {
|
|
117
|
+
this.ctx = new AudioCtx();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (this.ctx && this.ctx.state === 'suspended') {
|
|
121
|
+
this.ctx.resume();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
playTone(freq, type = 'square', duration = 0.08, gainVal = 0.05) {
|
|
126
|
+
if (!appState.audioEnabled) return;
|
|
127
|
+
try {
|
|
128
|
+
this.initContext();
|
|
129
|
+
if (!this.ctx) return;
|
|
130
|
+
const osc = this.ctx.createOscillator();
|
|
131
|
+
const gain = this.ctx.createGain();
|
|
132
|
+
|
|
133
|
+
osc.type = type;
|
|
134
|
+
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
|
|
135
|
+
gain.gain.setValueAtTime(gainVal, this.ctx.currentTime);
|
|
136
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, this.ctx.currentTime + duration);
|
|
137
|
+
|
|
138
|
+
osc.connect(gain);
|
|
139
|
+
gain.connect(this.ctx.destination);
|
|
140
|
+
|
|
141
|
+
osc.start();
|
|
142
|
+
osc.stop(this.ctx.currentTime + duration);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
// Audio autoplay policy
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
playSpawn() {
|
|
149
|
+
if (!appState.audioEnabled) return;
|
|
150
|
+
this.playTone(330, 'triangle', 0.06);
|
|
151
|
+
setTimeout(() => this.playTone(440, 'triangle', 0.06), 60);
|
|
152
|
+
setTimeout(() => this.playTone(660, 'square', 0.1), 120);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
playSkill() {
|
|
156
|
+
if (!appState.audioEnabled) return;
|
|
157
|
+
this.playTone(659.25, 'square', 0.05, 0.04);
|
|
158
|
+
setTimeout(() => this.playTone(987.77, 'square', 0.1, 0.04), 50);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
playComplete() {
|
|
162
|
+
if (!appState.audioEnabled) return;
|
|
163
|
+
const notes = [523.25, 659.25, 783.99, 1046.50];
|
|
164
|
+
notes.forEach((note, i) => {
|
|
165
|
+
setTimeout(() => this.playTone(note, 'square', 0.12, 0.05), i * 80);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
playError() {
|
|
170
|
+
if (!appState.audioEnabled) return;
|
|
171
|
+
this.playTone(180, 'sawtooth', 0.15, 0.08);
|
|
172
|
+
setTimeout(() => this.playTone(140, 'sawtooth', 0.25, 0.08), 100);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
playClick() {
|
|
176
|
+
this.playTone(800, 'triangle', 0.02, 0.02);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const synth = new RetroAudioSynth();
|
|
181
|
+
|
|
182
|
+
// Pixel Office Canvas Renderer
|
|
183
|
+
let officeCanvas, officeCtx;
|
|
184
|
+
let animFrame = 0;
|
|
185
|
+
|
|
186
|
+
function initOfficeCanvas() {
|
|
187
|
+
officeCanvas = document.getElementById('officeCanvas');
|
|
188
|
+
if (!officeCanvas) return;
|
|
189
|
+
officeCtx = officeCanvas.getContext('2d');
|
|
190
|
+
|
|
191
|
+
// Mouse interaction for tooltips and clicking workstations
|
|
192
|
+
officeCanvas.addEventListener('mousemove', (e) => {
|
|
193
|
+
const rect = officeCanvas.getBoundingClientRect();
|
|
194
|
+
const scaleX = officeCanvas.width / rect.width;
|
|
195
|
+
const scaleY = officeCanvas.height / rect.height;
|
|
196
|
+
const mouseX = (e.clientX - rect.left) * scaleX;
|
|
197
|
+
const mouseY = (e.clientY - rect.top) * scaleY;
|
|
198
|
+
|
|
199
|
+
let hit = null;
|
|
200
|
+
for (const [key, ws] of Object.entries(WORKSTATIONS)) {
|
|
201
|
+
if (mouseX >= ws.x && mouseX <= ws.x + ws.w && mouseY >= ws.y && mouseY <= ws.y + ws.h) {
|
|
202
|
+
hit = ws;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const tooltip = document.getElementById('deskTooltip');
|
|
208
|
+
if (hit) {
|
|
209
|
+
appState.hoveredDesk = hit.id;
|
|
210
|
+
tooltip.style.display = 'flex';
|
|
211
|
+
tooltip.style.left = `${e.clientX}px`;
|
|
212
|
+
tooltip.style.top = `${e.clientY}px`;
|
|
213
|
+
document.getElementById('tooltipName').textContent = `${hit.name}`;
|
|
214
|
+
document.getElementById('tooltipName').style.color = hit.color;
|
|
215
|
+
|
|
216
|
+
const aState = (appState.state && appState.state.agents) ? appState.state.agents[hit.id] : {};
|
|
217
|
+
const stateStr = hit.id === 'orchestrator' ? (appState.state?.orchestrator?.state || 'IDLE') : (aState.state || 'IDLE');
|
|
218
|
+
const taskStr = hit.id === 'orchestrator' ? (appState.state?.activeTask || 'Standing by') : (aState.currentTask || 'Idle');
|
|
219
|
+
|
|
220
|
+
document.getElementById('tooltipStatus').textContent = `STATUS: ${stateStr}`;
|
|
221
|
+
document.getElementById('tooltipTask').textContent = `${taskStr}`;
|
|
222
|
+
} else {
|
|
223
|
+
appState.hoveredDesk = null;
|
|
224
|
+
tooltip.style.display = 'none';
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
officeCanvas.addEventListener('mouseleave', () => {
|
|
229
|
+
appState.hoveredDesk = null;
|
|
230
|
+
document.getElementById('deskTooltip').style.display = 'none';
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
officeCanvas.addEventListener('click', (e) => {
|
|
234
|
+
if (appState.hoveredDesk) {
|
|
235
|
+
synth.playClick();
|
|
236
|
+
openAgentModal(appState.hoveredDesk);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Draw Pixel Startup Office Floor
|
|
242
|
+
function renderOfficeFloor() {
|
|
243
|
+
if (!officeCtx) return;
|
|
244
|
+
const ctx = officeCtx;
|
|
245
|
+
const w = officeCanvas.width;
|
|
246
|
+
const h = officeCanvas.height;
|
|
247
|
+
|
|
248
|
+
// 1. Office Floor Tiles (Checkered / Modern Startup Floor)
|
|
249
|
+
const isNight = appState.nightMode;
|
|
250
|
+
const tileA = isNight ? '#0b0d18' : '#141829';
|
|
251
|
+
const tileB = isNight ? '#080a13' : '#101424';
|
|
252
|
+
const tileSize = 24;
|
|
253
|
+
|
|
254
|
+
for (let y = 0; y < h; y += tileSize) {
|
|
255
|
+
for (let x = 0; x < w; x += tileSize) {
|
|
256
|
+
ctx.fillStyle = ((x / tileSize + y / tileSize) % 2 === 0) ? tileA : tileB;
|
|
257
|
+
ctx.fillRect(x, y, tileSize, tileSize);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 2. Wall Partitions & Glass Meeting Rooms
|
|
262
|
+
ctx.strokeStyle = isNight ? '#1f274a' : '#2b3558';
|
|
263
|
+
ctx.lineWidth = 4;
|
|
264
|
+
ctx.strokeRect(4, 4, w - 8, h - 8);
|
|
265
|
+
|
|
266
|
+
// Executive Glass Partition
|
|
267
|
+
ctx.fillStyle = 'rgba(0, 240, 255, 0.04)';
|
|
268
|
+
ctx.fillRect(50, 40, 200, 160);
|
|
269
|
+
ctx.strokeStyle = 'rgba(0, 240, 255, 0.25)';
|
|
270
|
+
ctx.strokeRect(50, 40, 200, 160);
|
|
271
|
+
|
|
272
|
+
// Server Room Glass Partition
|
|
273
|
+
ctx.fillStyle = 'rgba(255, 215, 0, 0.04)';
|
|
274
|
+
ctx.fillRect(670, 40, 240, 160);
|
|
275
|
+
ctx.strokeStyle = 'rgba(255, 215, 0, 0.25)';
|
|
276
|
+
ctx.strokeRect(670, 40, 240, 160);
|
|
277
|
+
|
|
278
|
+
// Break Lounge Area (Bottom Right)
|
|
279
|
+
renderBreakLounge(ctx, 680, 230, 230, 150);
|
|
280
|
+
|
|
281
|
+
// 3. Render Workstations
|
|
282
|
+
for (const [key, ws] of Object.entries(WORKSTATIONS)) {
|
|
283
|
+
renderWorkstationDesk(ctx, ws);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 4. Whiteboard in hallway
|
|
287
|
+
renderWhiteboard(ctx, 420, 15, 120, 28);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Draw Workstation Desk & Animated Agent
|
|
291
|
+
function renderWorkstationDesk(ctx, ws) {
|
|
292
|
+
const isHovered = appState.hoveredDesk === ws.id;
|
|
293
|
+
const isNight = appState.nightMode;
|
|
294
|
+
|
|
295
|
+
const aState = ws.id === 'orchestrator'
|
|
296
|
+
? (appState.state?.orchestrator || { state: 'IDLE', expression: 'โ_โ' })
|
|
297
|
+
: (appState.state?.agents?.[ws.id] || { state: 'IDLE', expression: 'โ_โ' });
|
|
298
|
+
|
|
299
|
+
const state = aState.state || 'IDLE';
|
|
300
|
+
|
|
301
|
+
// Desk boundary glow if hovered or working
|
|
302
|
+
if (isHovered || state === 'WORKING') {
|
|
303
|
+
ctx.strokeStyle = ws.color;
|
|
304
|
+
ctx.lineWidth = 2;
|
|
305
|
+
ctx.strokeRect(ws.x - 4, ws.y - 4, ws.w + 8, ws.h + 8);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Desk Mat & Wooden Table
|
|
309
|
+
ctx.fillStyle = isNight ? '#161a2e' : '#222842';
|
|
310
|
+
ctx.fillRect(ws.x + 10, ws.y + 30, ws.w - 20, 50);
|
|
311
|
+
ctx.fillStyle = '#0a0d18';
|
|
312
|
+
ctx.fillRect(ws.x + 14, ws.y + 34, ws.w - 28, 42); // desk mat
|
|
313
|
+
|
|
314
|
+
// Workstation Label
|
|
315
|
+
ctx.font = '8px "Press Start 2P", monospace';
|
|
316
|
+
ctx.fillStyle = ws.color;
|
|
317
|
+
ctx.fillText(ws.id.toUpperCase(), ws.x + 14, ws.y + 20);
|
|
318
|
+
|
|
319
|
+
// State Badge
|
|
320
|
+
ctx.font = '7px "Silkscreen", monospace';
|
|
321
|
+
ctx.fillStyle = state === 'WORKING' ? '#ff007f' : (state === 'COMPLETED' ? '#39ff14' : '#8a96b0');
|
|
322
|
+
ctx.fillText(`[${state}]`, ws.x + ws.w - 65, ws.y + 20);
|
|
323
|
+
|
|
324
|
+
// Multi-Monitor Setup with Glowing Code / UI
|
|
325
|
+
const monitorGlow = state === 'WORKING' || state === 'ANALYZING';
|
|
326
|
+
|
|
327
|
+
// Left/Main Monitor
|
|
328
|
+
ctx.fillStyle = '#1c223a';
|
|
329
|
+
ctx.fillRect(ws.x + 20, ws.y + 38, 36, 24);
|
|
330
|
+
ctx.fillStyle = monitorGlow ? ws.color : '#0f1424';
|
|
331
|
+
ctx.fillRect(ws.x + 22, ws.y + 40, 32, 20);
|
|
332
|
+
|
|
333
|
+
// Animated Code Lines on Screen
|
|
334
|
+
if (monitorGlow) {
|
|
335
|
+
ctx.fillStyle = '#ffffff';
|
|
336
|
+
const lineOffset = (animFrame % 6);
|
|
337
|
+
ctx.fillRect(ws.x + 24, ws.y + 43 + (lineOffset % 3) * 4, 18, 2);
|
|
338
|
+
ctx.fillRect(ws.x + 24, ws.y + 49, 24, 2);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Right Secondary Monitor
|
|
342
|
+
ctx.fillStyle = '#1c223a';
|
|
343
|
+
ctx.fillRect(ws.x + 60, ws.y + 40, 28, 20);
|
|
344
|
+
ctx.fillStyle = monitorGlow ? '#39ff14' : '#0f1424';
|
|
345
|
+
ctx.fillRect(ws.x + 62, ws.y + 42, 24, 16);
|
|
346
|
+
|
|
347
|
+
// Mechanical Keyboard
|
|
348
|
+
ctx.fillStyle = '#2c3558';
|
|
349
|
+
ctx.fillRect(ws.x + 30, ws.y + 66, 32, 10);
|
|
350
|
+
ctx.fillStyle = '#445182';
|
|
351
|
+
for (let k = 0; k < 4; k++) {
|
|
352
|
+
ctx.fillRect(ws.x + 33 + k * 7, ws.y + 68, 5, 3);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Coffee Mug (with rising steam when active)
|
|
356
|
+
ctx.fillStyle = '#ffffff';
|
|
357
|
+
ctx.fillRect(ws.x + ws.w - 36, ws.y + 46, 8, 10);
|
|
358
|
+
ctx.fillStyle = '#b08968';
|
|
359
|
+
ctx.fillRect(ws.x + ws.w - 34, ws.y + 48, 4, 4); // coffee
|
|
360
|
+
if (state === 'WORKING' || state === 'ANALYZING') {
|
|
361
|
+
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
|
362
|
+
const steamY = (animFrame % 10) * 1.2;
|
|
363
|
+
ctx.fillRect(ws.x + ws.w - 33, ws.y + 42 - steamY, 2, 3);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Server Racks in Database Room
|
|
367
|
+
if (ws.id === 'database') {
|
|
368
|
+
ctx.fillStyle = '#060810';
|
|
369
|
+
ctx.fillRect(ws.x + ws.w - 75, ws.y + 35, 45, 80);
|
|
370
|
+
ctx.fillStyle = '#1a2238';
|
|
371
|
+
for (let r = 0; r < 5; r++) {
|
|
372
|
+
ctx.fillRect(ws.x + ws.w - 71, ws.y + 40 + r * 14, 37, 10);
|
|
373
|
+
// Blinking LEDs
|
|
374
|
+
ctx.fillStyle = ((animFrame + r) % 4 === 0) ? '#39ff14' : '#00f0ff';
|
|
375
|
+
ctx.fillRect(ws.x + ws.w - 68, ws.y + 43 + r * 14, 3, 3);
|
|
376
|
+
ctx.fillStyle = ((animFrame + r) % 3 === 0) ? '#ffd700' : '#ff007f';
|
|
377
|
+
ctx.fillRect(ws.x + ws.w - 62, ws.y + 43 + r * 14, 3, 3);
|
|
378
|
+
ctx.fillStyle = '#1a2238';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Security Radar / CCTV in Security Room
|
|
383
|
+
if (ws.id === 'security') {
|
|
384
|
+
ctx.fillStyle = '#081a10';
|
|
385
|
+
ctx.fillRect(ws.x + ws.w - 50, ws.y + 38, 28, 22);
|
|
386
|
+
ctx.strokeStyle = '#39ff14';
|
|
387
|
+
ctx.beginPath();
|
|
388
|
+
ctx.arc(ws.x + ws.w - 36, ws.y + 49, 8, 0, Math.PI * 2);
|
|
389
|
+
ctx.stroke();
|
|
390
|
+
// Radar sweep
|
|
391
|
+
const sweepAngle = (animFrame * 0.1) % (Math.PI * 2);
|
|
392
|
+
ctx.beginPath();
|
|
393
|
+
ctx.moveTo(ws.x + ws.w - 36, ws.y + 49);
|
|
394
|
+
ctx.lineTo(
|
|
395
|
+
ws.x + ws.w - 36 + Math.cos(sweepAngle) * 8,
|
|
396
|
+
ws.y + 49 + Math.sin(sweepAngle) * 8
|
|
397
|
+
);
|
|
398
|
+
ctx.stroke();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Animated Pixel Character sitting in Ergonomic Chair
|
|
402
|
+
const charX = ws.x + 36;
|
|
403
|
+
const charY = ws.y + 82;
|
|
404
|
+
renderOfficeWorker(ctx, ws.id, state, charX, charY, animFrame);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Draw Pixel Office Worker in Chair
|
|
408
|
+
function renderOfficeWorker(ctx, agentKey, state, x, y, frame) {
|
|
409
|
+
// Ergonomic Chair Back
|
|
410
|
+
ctx.fillStyle = '#111422';
|
|
411
|
+
ctx.fillRect(x - 8, y - 2, 24, 26);
|
|
412
|
+
ctx.fillStyle = '#1d233a';
|
|
413
|
+
ctx.fillRect(x - 6, y, 20, 22);
|
|
414
|
+
|
|
415
|
+
// Bobbing animation offset
|
|
416
|
+
let offsetY = 0;
|
|
417
|
+
if (state === 'WORKING') {
|
|
418
|
+
offsetY = (frame % 2 === 0) ? -1 : 1;
|
|
419
|
+
} else if (state === 'IDLE' || state === 'COMPLETED') {
|
|
420
|
+
offsetY = Math.sin(frame * 0.15) * 1.5;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Worker Head & Torso
|
|
424
|
+
const colors = {
|
|
425
|
+
orchestrator: '#ffd700',
|
|
426
|
+
frontend: '#00f0ff',
|
|
427
|
+
backend: '#ff007f',
|
|
428
|
+
database: '#ffd700',
|
|
429
|
+
security: '#ff3344',
|
|
430
|
+
performance: '#39ff14',
|
|
431
|
+
qa: '#b026ff'
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const suitColor = colors[agentKey] || '#00f0ff';
|
|
435
|
+
|
|
436
|
+
// Hair / Head
|
|
437
|
+
ctx.fillStyle = '#ffd1a4'; // skin
|
|
438
|
+
ctx.fillRect(x, y + offsetY, 10, 10);
|
|
439
|
+
ctx.fillStyle = suitColor;
|
|
440
|
+
ctx.fillRect(x - 1, y - 2 + offsetY, 12, 4); // hair/cap
|
|
441
|
+
|
|
442
|
+
// Eyes & Expression
|
|
443
|
+
let eyeColor = '#000000';
|
|
444
|
+
if (state === 'ERROR') eyeColor = '#ff0000';
|
|
445
|
+
else if (state === 'COMPLETED') eyeColor = '#39ff14';
|
|
446
|
+
else if (state === 'ANALYZING') eyeColor = (frame % 4 < 2) ? '#ffd700' : '#00f0ff';
|
|
447
|
+
|
|
448
|
+
ctx.fillStyle = eyeColor;
|
|
449
|
+
ctx.fillRect(x + 2, y + 3 + offsetY, 2, 2);
|
|
450
|
+
ctx.fillRect(x + 6, y + 3 + offsetY, 2, 2);
|
|
451
|
+
|
|
452
|
+
// Torso / Startup Hoodie
|
|
453
|
+
ctx.fillStyle = suitColor;
|
|
454
|
+
ctx.fillRect(x - 2, y + 10 + offsetY, 14, 12);
|
|
455
|
+
|
|
456
|
+
// Typing Arms / Actions
|
|
457
|
+
if (state === 'WORKING') {
|
|
458
|
+
ctx.fillStyle = '#ffd1a4';
|
|
459
|
+
const armW = (frame % 2 === 0) ? 6 : 4;
|
|
460
|
+
ctx.fillRect(x - 5, y + 12 + offsetY, armW, 3);
|
|
461
|
+
ctx.fillRect(x + 9, y + 12 + offsetY, armW, 3);
|
|
462
|
+
|
|
463
|
+
// Typing Sparkles
|
|
464
|
+
ctx.fillStyle = '#ffffff';
|
|
465
|
+
ctx.fillRect(x + ((frame * 3) % 12) - 2, y + 8, 2, 2);
|
|
466
|
+
} else if (state === 'ANALYZING') {
|
|
467
|
+
// Thinking bubble
|
|
468
|
+
ctx.fillStyle = '#ffffff';
|
|
469
|
+
ctx.fillRect(x + 12, y - 6, 8, 6);
|
|
470
|
+
ctx.fillStyle = '#000000';
|
|
471
|
+
ctx.fillRect(x + 14, y - 4, 4, 2);
|
|
472
|
+
} else if (state === 'COMPLETED') {
|
|
473
|
+
// Victory hands up
|
|
474
|
+
ctx.fillStyle = '#ffd1a4';
|
|
475
|
+
ctx.fillRect(x - 5, y + 6 + offsetY, 3, 5);
|
|
476
|
+
ctx.fillRect(x + 12, y + 6 + offsetY, 3, 5);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Break Lounge Area (Espresso machine, water cooler, beanbag)
|
|
481
|
+
function renderBreakLounge(ctx, x, y, w, h) {
|
|
482
|
+
// Carpet
|
|
483
|
+
ctx.fillStyle = '#1c162b';
|
|
484
|
+
ctx.fillRect(x, y, w, h);
|
|
485
|
+
ctx.strokeStyle = '#3d285c';
|
|
486
|
+
ctx.strokeRect(x, y, w, h);
|
|
487
|
+
|
|
488
|
+
// Label
|
|
489
|
+
ctx.font = '8px "Press Start 2P", monospace';
|
|
490
|
+
ctx.fillStyle = '#b08968';
|
|
491
|
+
ctx.fillText('โ BREAK LOUNGE', x + 10, y + 16);
|
|
492
|
+
|
|
493
|
+
// Espresso Bar
|
|
494
|
+
ctx.fillStyle = '#2c223b';
|
|
495
|
+
ctx.fillRect(x + 14, y + 30, 65, 45);
|
|
496
|
+
ctx.fillStyle = '#silver';
|
|
497
|
+
ctx.fillStyle = '#a0aab8';
|
|
498
|
+
ctx.fillRect(x + 20, y + 34, 24, 20); // coffee maker
|
|
499
|
+
ctx.fillStyle = '#00f0ff';
|
|
500
|
+
ctx.fillRect(x + 24, y + 38, 8, 4); // screen
|
|
501
|
+
|
|
502
|
+
// Water Cooler
|
|
503
|
+
ctx.fillStyle = '#00f0ff';
|
|
504
|
+
ctx.fillRect(x + 95, y + 30, 18, 20); // water bottle
|
|
505
|
+
ctx.fillStyle = '#ffffff';
|
|
506
|
+
ctx.fillRect(x + 93, y + 50, 22, 25); // cooler base
|
|
507
|
+
|
|
508
|
+
// Potted Startup Plant / Ficus
|
|
509
|
+
ctx.fillStyle = '#8b5a2b';
|
|
510
|
+
ctx.fillRect(x + 150, y + 55, 18, 18); // pot
|
|
511
|
+
ctx.fillStyle = '#39ff14';
|
|
512
|
+
ctx.fillRect(x + 146, y + 35, 26, 20); // leaves
|
|
513
|
+
ctx.fillRect(x + 152, y + 25, 14, 12);
|
|
514
|
+
|
|
515
|
+
// Comfy Beanbag
|
|
516
|
+
ctx.fillStyle = '#ff007f';
|
|
517
|
+
ctx.fillRect(x + 20, y + 90, 40, 25);
|
|
518
|
+
ctx.fillStyle = '#d6006b';
|
|
519
|
+
ctx.fillRect(x + 25, y + 95, 30, 16);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Whiteboard
|
|
523
|
+
function renderWhiteboard(ctx, x, y, w, h) {
|
|
524
|
+
ctx.fillStyle = '#ffffff';
|
|
525
|
+
ctx.fillRect(x, y, w, h);
|
|
526
|
+
ctx.strokeStyle = '#a0aab8';
|
|
527
|
+
ctx.strokeRect(x, y, w, h);
|
|
528
|
+
|
|
529
|
+
// Flowchart boxes on whiteboard
|
|
530
|
+
ctx.fillStyle = '#ff007f';
|
|
531
|
+
ctx.fillRect(x + 10, y + 6, 20, 8);
|
|
532
|
+
ctx.fillStyle = '#00f0ff';
|
|
533
|
+
ctx.fillRect(x + 45, y + 6, 20, 8);
|
|
534
|
+
ctx.fillStyle = '#39ff14';
|
|
535
|
+
ctx.fillRect(x + 80, y + 6, 20, 8);
|
|
536
|
+
|
|
537
|
+
ctx.strokeStyle = '#000000';
|
|
538
|
+
ctx.lineWidth = 1;
|
|
539
|
+
ctx.beginPath();
|
|
540
|
+
ctx.moveTo(x + 30, y + 10); ctx.lineTo(x + 45, y + 10);
|
|
541
|
+
ctx.moveTo(x + 65, y + 10); ctx.lineTo(x + 80, y + 10);
|
|
542
|
+
ctx.stroke();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Animation Loop for Office Floor
|
|
546
|
+
function startOfficeAnimationLoop() {
|
|
547
|
+
function loop() {
|
|
548
|
+
animFrame++;
|
|
549
|
+
renderOfficeFloor();
|
|
550
|
+
requestAnimationFrame(loop);
|
|
551
|
+
}
|
|
552
|
+
requestAnimationFrame(loop);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Initialize Application
|
|
556
|
+
async function initApp() {
|
|
557
|
+
setupEventListeners();
|
|
558
|
+
initOfficeCanvas();
|
|
559
|
+
startOfficeAnimationLoop();
|
|
560
|
+
|
|
561
|
+
// Instant initial fetch before SSE stream connects
|
|
562
|
+
try {
|
|
563
|
+
const [cfgRes, stRes] = await Promise.all([
|
|
564
|
+
fetch('/api/config').then(r => r.ok ? r.json() : null),
|
|
565
|
+
fetch('/api/state').then(r => r.ok ? r.json() : null)
|
|
566
|
+
]);
|
|
567
|
+
if (cfgRes) {
|
|
568
|
+
appState.config = cfgRes;
|
|
569
|
+
renderProjectInfo();
|
|
570
|
+
renderAgentGrid();
|
|
571
|
+
renderSkillsList();
|
|
572
|
+
}
|
|
573
|
+
if (stRes) {
|
|
574
|
+
appState.state = stRes;
|
|
575
|
+
renderState();
|
|
576
|
+
}
|
|
577
|
+
} catch (e) {
|
|
578
|
+
// Handled by SSE
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
connectSSE();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Setup DOM Event Listeners
|
|
585
|
+
function setupEventListeners() {
|
|
586
|
+
// Task form submission
|
|
587
|
+
const taskForm = document.getElementById('taskForm');
|
|
588
|
+
const taskInput = document.getElementById('taskInput');
|
|
589
|
+
taskForm.addEventListener('submit', async (e) => {
|
|
590
|
+
e.preventDefault();
|
|
591
|
+
const prompt = taskInput.value.trim();
|
|
592
|
+
if (!prompt) return;
|
|
593
|
+
synth.playClick();
|
|
594
|
+
await submitTask(prompt);
|
|
595
|
+
taskInput.value = '';
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Quick mission tags
|
|
599
|
+
document.querySelectorAll('.quick-tag').forEach((btn) => {
|
|
600
|
+
btn.addEventListener('click', async () => {
|
|
601
|
+
synth.playClick();
|
|
602
|
+
const prompt = btn.getAttribute('data-prompt');
|
|
603
|
+
await submitTask(prompt);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Header buttons
|
|
608
|
+
document.getElementById('btnDemo').addEventListener('click', async () => {
|
|
609
|
+
synth.playClick();
|
|
610
|
+
await fetch('/api/demo', { method: 'POST' });
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
document.getElementById('btnReset').addEventListener('click', async () => {
|
|
614
|
+
synth.playClick();
|
|
615
|
+
await fetch('/api/reset', { method: 'POST' });
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
document.getElementById('btnCrtToggle').addEventListener('click', () => {
|
|
619
|
+
synth.playClick();
|
|
620
|
+
appState.crtEnabled = !appState.crtEnabled;
|
|
621
|
+
document.body.classList.toggle('crt-active', appState.crtEnabled);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
document.getElementById('btnNightToggle').addEventListener('click', () => {
|
|
625
|
+
synth.playClick();
|
|
626
|
+
appState.nightMode = !appState.nightMode;
|
|
627
|
+
document.body.classList.toggle('night-mode', appState.nightMode);
|
|
628
|
+
document.getElementById('nightIcon').textContent = appState.nightMode ? 'โ๏ธ' : '๐';
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
document.getElementById('btnAudioToggle').addEventListener('click', () => {
|
|
632
|
+
appState.audioEnabled = !appState.audioEnabled;
|
|
633
|
+
document.getElementById('audioIcon').textContent = appState.audioEnabled ? '๐' : '๐';
|
|
634
|
+
synth.playClick();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
document.getElementById('btnClearLogs').addEventListener('click', () => {
|
|
638
|
+
synth.playClick();
|
|
639
|
+
document.getElementById('terminalLogs').innerHTML = '';
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Log filter chips
|
|
643
|
+
document.querySelectorAll('.filter-chip').forEach((chip) => {
|
|
644
|
+
chip.addEventListener('click', () => {
|
|
645
|
+
synth.playClick();
|
|
646
|
+
document.querySelectorAll('.filter-chip').forEach((c) => c.classList.remove('active'));
|
|
647
|
+
chip.classList.add('active');
|
|
648
|
+
appState.activeFilter = chip.getAttribute('data-filter');
|
|
649
|
+
applyLogFilters();
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// Modal close
|
|
654
|
+
document.getElementById('btnCloseModal').addEventListener('click', () => {
|
|
655
|
+
document.getElementById('agentModal').classList.remove('open');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
document.getElementById('agentModal').addEventListener('click', (e) => {
|
|
659
|
+
if (e.target === document.getElementById('agentModal')) {
|
|
660
|
+
document.getElementById('agentModal').classList.remove('open');
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Global Keyboard Shortcuts
|
|
665
|
+
window.addEventListener('keydown', (e) => {
|
|
666
|
+
if (e.target.tagName === 'INPUT') return;
|
|
667
|
+
if (e.key === ' ') {
|
|
668
|
+
e.preventDefault();
|
|
669
|
+
document.getElementById('btnDemo').click();
|
|
670
|
+
} else if (e.key === 'Escape') {
|
|
671
|
+
document.getElementById('agentModal').classList.remove('open');
|
|
672
|
+
} else if (['1', '2', '3', '4', '5', '6'].includes(e.key)) {
|
|
673
|
+
const keys = ['frontend', 'backend', 'database', 'security', 'performance', 'qa'];
|
|
674
|
+
const agentKey = keys[parseInt(e.key) - 1];
|
|
675
|
+
if (agentKey) openAgentModal(agentKey);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Submit Task to Server
|
|
681
|
+
async function submitTask(prompt) {
|
|
682
|
+
try {
|
|
683
|
+
await fetch('/api/task', {
|
|
684
|
+
method: 'POST',
|
|
685
|
+
headers: { 'Content-Type': 'application/json' },
|
|
686
|
+
body: JSON.stringify({ prompt })
|
|
687
|
+
});
|
|
688
|
+
} catch (err) {
|
|
689
|
+
console.error('Failed to submit task:', err);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Server-Sent Events Connection
|
|
694
|
+
function connectSSE() {
|
|
695
|
+
const eventSource = new EventSource('/api/events');
|
|
696
|
+
|
|
697
|
+
eventSource.addEventListener('init', (e) => {
|
|
698
|
+
const data = JSON.parse(e.data);
|
|
699
|
+
appState.config = data.config;
|
|
700
|
+
appState.state = data.state;
|
|
701
|
+
appState.events = data.history || [];
|
|
702
|
+
|
|
703
|
+
renderProjectInfo();
|
|
704
|
+
renderAgentGrid();
|
|
705
|
+
renderSkillsList();
|
|
706
|
+
renderState();
|
|
707
|
+
renderLogs(appState.events);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
eventSource.addEventListener('agent_event', (e) => {
|
|
711
|
+
const event = JSON.parse(e.data);
|
|
712
|
+
appState.events.push(event);
|
|
713
|
+
appendLog(event);
|
|
714
|
+
|
|
715
|
+
// Real-time visual state & sprite animation update
|
|
716
|
+
if (appState.state && event.agent) {
|
|
717
|
+
if (event.agent !== 'orchestrator') {
|
|
718
|
+
if (!appState.state.agents[event.agent]) {
|
|
719
|
+
appState.state.agents[event.agent] = {
|
|
720
|
+
state: 'IDLE',
|
|
721
|
+
expression: 'โ_โ',
|
|
722
|
+
currentTask: 'Idle',
|
|
723
|
+
skillsStatus: {}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const aState = appState.state.agents[event.agent];
|
|
727
|
+
if (event.type === 'spawn' || event.type === 'thinking') {
|
|
728
|
+
aState.state = 'ANALYZING';
|
|
729
|
+
aState.expression = 'โ_โ';
|
|
730
|
+
} else if (event.type === 'complete') {
|
|
731
|
+
aState.state = 'COMPLETED';
|
|
732
|
+
aState.expression = '^โฟ^';
|
|
733
|
+
} else if (event.type === 'error') {
|
|
734
|
+
aState.state = 'ERROR';
|
|
735
|
+
aState.expression = 'x_x';
|
|
736
|
+
} else if (event.type === 'idle') {
|
|
737
|
+
aState.state = 'IDLE';
|
|
738
|
+
aState.expression = 'โ_โ';
|
|
739
|
+
} else {
|
|
740
|
+
aState.state = 'WORKING';
|
|
741
|
+
aState.expression = 'โโโ';
|
|
742
|
+
}
|
|
743
|
+
aState.currentTask = event.message || aState.currentTask;
|
|
744
|
+
|
|
745
|
+
if (event.skill) {
|
|
746
|
+
aState.skillsStatus = aState.skillsStatus || {};
|
|
747
|
+
aState.skillsStatus[event.skill] = event.type === 'complete' ? 'completed' : 'active';
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
if (event.type === 'complete') {
|
|
751
|
+
appState.state.status = 'COMPLETED';
|
|
752
|
+
} else {
|
|
753
|
+
appState.state.status = 'RUNNING';
|
|
754
|
+
}
|
|
755
|
+
if (appState.state.orchestrator) {
|
|
756
|
+
appState.state.orchestrator.currentTask = event.message || appState.state.orchestrator.currentTask;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
renderState();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Audio cues
|
|
763
|
+
if (event.type === 'spawn') synth.playSpawn();
|
|
764
|
+
else if (event.type === 'skill') synth.playSkill();
|
|
765
|
+
else if (event.type === 'complete') synth.playComplete();
|
|
766
|
+
else if (event.type === 'error') synth.playError();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
eventSource.addEventListener('state_change', (e) => {
|
|
770
|
+
const data = JSON.parse(e.data);
|
|
771
|
+
appState.state = data.state;
|
|
772
|
+
renderState();
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
eventSource.onerror = () => {
|
|
776
|
+
console.warn('SSE disconnected. Reconnecting in 3s...');
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Render Project Information across the Dashboard dynamically
|
|
781
|
+
function renderProjectInfo() {
|
|
782
|
+
if (!appState.config) return;
|
|
783
|
+
const project = appState.config.project || 'my-app';
|
|
784
|
+
const projectUpper = project.toUpperCase();
|
|
785
|
+
|
|
786
|
+
// 1. Project badge in top header
|
|
787
|
+
const projectBadgeEl = document.getElementById('projectName');
|
|
788
|
+
if (projectBadgeEl) {
|
|
789
|
+
projectBadgeEl.textContent = project;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// 2. Browser Tab Title
|
|
793
|
+
document.title = `${projectUpper} // PixelCrew HQ โ Autonomous Swarm Workspace`;
|
|
794
|
+
|
|
795
|
+
// 3. Header Logo Subtitle
|
|
796
|
+
const subTitleEl = document.getElementById('logoSubtitle') || document.querySelector('.logo-subtitle');
|
|
797
|
+
if (subTitleEl) {
|
|
798
|
+
subTitleEl.textContent = `FLOOR 42 // ${projectUpper} SPRINT HQ`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// 4. Floor Plan Panel Title
|
|
802
|
+
const floorTitleEl = document.getElementById('officeFloorTitle');
|
|
803
|
+
if (floorTitleEl) {
|
|
804
|
+
floorTitleEl.textContent = `${projectUpper} OPEN-PLAN ENGINEERING FLOOR`;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// 5. Dynamic Task Input Placeholder
|
|
808
|
+
const taskInput = document.getElementById('taskInput');
|
|
809
|
+
if (taskInput && (!taskInput.value || taskInput.value.length === 0)) {
|
|
810
|
+
taskInput.placeholder = `Assign sprint objective (e.g. 'Optimize ${project} queries & build responsive UI')...`;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Render Agent Cards Grid
|
|
815
|
+
function renderAgentGrid() {
|
|
816
|
+
const grid = document.getElementById('agentsGrid');
|
|
817
|
+
if (!grid || !appState.config || !appState.config.agents) return;
|
|
818
|
+
|
|
819
|
+
grid.innerHTML = '';
|
|
820
|
+
const agents = appState.config.agents;
|
|
821
|
+
|
|
822
|
+
for (const [key, agent] of Object.entries(agents)) {
|
|
823
|
+
const card = document.createElement('div');
|
|
824
|
+
card.className = 'agent-card';
|
|
825
|
+
card.id = `agent-card-${key}`;
|
|
826
|
+
card.style.setProperty('--agent-color', agent.color || '#00f0ff');
|
|
827
|
+
|
|
828
|
+
const ws = WORKSTATIONS[key] || { icon: '๐ผ' };
|
|
829
|
+
|
|
830
|
+
card.innerHTML = `
|
|
831
|
+
<div class="agent-card-header">
|
|
832
|
+
<span class="agent-role-title">${ws.icon} ${agent.name.toUpperCase()}</span>
|
|
833
|
+
<span class="agent-state-pill" id="pill-${key}">IDLE</span>
|
|
834
|
+
</div>
|
|
835
|
+
<div class="agent-card-body">
|
|
836
|
+
<div class="agent-current-task" id="task-${key}">Standing by at workstation</div>
|
|
837
|
+
<div class="agent-skills-mini" id="skills-mini-${key}">
|
|
838
|
+
${(agent.skills || []).slice(0, 3).map(skill => `
|
|
839
|
+
<div class="mini-skill-row">
|
|
840
|
+
<span>${skill}</span>
|
|
841
|
+
<span class="mini-skill-icon" id="mini-skill-${key}-${skill}">โ</span>
|
|
842
|
+
</div>
|
|
843
|
+
`).join('')}
|
|
844
|
+
</div>
|
|
845
|
+
</div>
|
|
846
|
+
`;
|
|
847
|
+
|
|
848
|
+
card.addEventListener('click', () => openAgentModal(key));
|
|
849
|
+
grid.appendChild(card);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Render Overall Skills Matrix Panel
|
|
854
|
+
function renderSkillsList() {
|
|
855
|
+
const container = document.getElementById('skillsList');
|
|
856
|
+
if (!container || !appState.config) return;
|
|
857
|
+
|
|
858
|
+
container.innerHTML = '';
|
|
859
|
+
const agents = appState.config.agents || {};
|
|
860
|
+
|
|
861
|
+
for (const [agentKey, agent] of Object.entries(agents)) {
|
|
862
|
+
for (const skill of (agent.skills || [])) {
|
|
863
|
+
const item = document.createElement('div');
|
|
864
|
+
item.className = 'skill-item';
|
|
865
|
+
item.id = `skill-row-${agentKey}-${skill}`;
|
|
866
|
+
item.innerHTML = `
|
|
867
|
+
<div class="skill-left">
|
|
868
|
+
<span class="skill-icon status-idle" id="skill-status-${agentKey}-${skill}">โ</span>
|
|
869
|
+
<span class="skill-name">${skill}</span>
|
|
870
|
+
</div>
|
|
871
|
+
<span class="skill-agent-tag" style="color: ${agent.color}">${agentKey.toUpperCase()}</span>
|
|
872
|
+
`;
|
|
873
|
+
container.appendChild(item);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Render State Updates
|
|
879
|
+
function renderState() {
|
|
880
|
+
if (!appState.state) return;
|
|
881
|
+
const state = appState.state;
|
|
882
|
+
|
|
883
|
+
// Swarm Status Pill
|
|
884
|
+
const statusBadge = document.getElementById('swarmStatusBadge');
|
|
885
|
+
const statusText = document.getElementById('swarmStatusText');
|
|
886
|
+
if (state.status === 'RUNNING') {
|
|
887
|
+
statusBadge.classList.add('running');
|
|
888
|
+
statusText.textContent = 'โ SPRINT IN PROGRESS';
|
|
889
|
+
document.getElementById('coffeeFill').style.width = '45%';
|
|
890
|
+
document.getElementById('velocityFill').style.width = '100%';
|
|
891
|
+
} else if (state.status === 'COMPLETED') {
|
|
892
|
+
statusBadge.classList.remove('running');
|
|
893
|
+
statusText.textContent = 'โ
SPRINT COMPLETED';
|
|
894
|
+
document.getElementById('coffeeFill').style.width = '90%';
|
|
895
|
+
} else {
|
|
896
|
+
statusBadge.classList.remove('running');
|
|
897
|
+
statusText.textContent = 'STANDBY';
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Orchestrator Progress Text
|
|
901
|
+
const progress = state.orchestrator?.progress || 0;
|
|
902
|
+
document.getElementById('orchProgressText').textContent = `${progress}%`;
|
|
903
|
+
|
|
904
|
+
// Agent counts & UI cards
|
|
905
|
+
let activeCount = 0;
|
|
906
|
+
const agentStates = state.agents || {};
|
|
907
|
+
for (const [key, aState] of Object.entries(agentStates)) {
|
|
908
|
+
if (aState.state === 'WORKING' || aState.state === 'ANALYZING' || aState.state === 'SPAWNING') {
|
|
909
|
+
activeCount++;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const pill = document.getElementById(`pill-${key}`);
|
|
913
|
+
if (pill) {
|
|
914
|
+
pill.textContent = aState.state || 'IDLE';
|
|
915
|
+
pill.className = `agent-state-pill state-${(aState.state || 'idle').toLowerCase()}`;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const taskEl = document.getElementById(`task-${key}`);
|
|
919
|
+
if (taskEl) taskEl.textContent = aState.currentTask || 'Idle';
|
|
920
|
+
|
|
921
|
+
const skillsStatus = aState.skillsStatus || {};
|
|
922
|
+
for (const [skillName, sStatus] of Object.entries(skillsStatus)) {
|
|
923
|
+
const miniIcon = document.getElementById(`mini-skill-${key}-${skillName}`);
|
|
924
|
+
if (miniIcon) {
|
|
925
|
+
miniIcon.className = `mini-skill-icon ${sStatus}`;
|
|
926
|
+
miniIcon.textContent = sStatus === 'completed' ? 'โ' : (sStatus === 'active' ? 'โ' : 'โ');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const mainIcon = document.getElementById(`skill-status-${key}-${skillName}`);
|
|
930
|
+
if (mainIcon) {
|
|
931
|
+
mainIcon.className = `skill-icon status-${sStatus}`;
|
|
932
|
+
mainIcon.textContent = sStatus === 'completed' ? 'โ' : (sStatus === 'active' ? 'โ' : 'โ');
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
document.getElementById('activeAgentCount').textContent = `${activeCount}/6 WORKING`;
|
|
938
|
+
|
|
939
|
+
// Mission Timer
|
|
940
|
+
if (state.status === 'RUNNING' && !appState.missionTimerInterval) {
|
|
941
|
+
appState.missionStartTime = Date.now();
|
|
942
|
+
appState.missionTimerInterval = setInterval(() => {
|
|
943
|
+
const elapsedSec = Math.floor((Date.now() - appState.missionStartTime) / 1000);
|
|
944
|
+
const mins = String(Math.floor(elapsedSec / 60)).padStart(2, '0');
|
|
945
|
+
const secs = String(elapsedSec % 60).padStart(2, '0');
|
|
946
|
+
document.getElementById('missionElapsed').textContent = `${mins}:${secs}`;
|
|
947
|
+
}, 1000);
|
|
948
|
+
} else if (state.status !== 'RUNNING' && appState.missionTimerInterval) {
|
|
949
|
+
clearInterval(appState.missionTimerInterval);
|
|
950
|
+
appState.missionTimerInterval = null;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Terminal / Slack Log System
|
|
955
|
+
function appendLog(event) {
|
|
956
|
+
const terminal = document.getElementById('terminalLogs');
|
|
957
|
+
if (!terminal) return;
|
|
958
|
+
|
|
959
|
+
const timeStr = new Date(event.timestamp).toTimeString().split(' ')[0];
|
|
960
|
+
const entry = document.createElement('div');
|
|
961
|
+
entry.className = `log-entry log-${event.type || 'progress'}`;
|
|
962
|
+
entry.setAttribute('data-agent', event.agent || 'orchestrator');
|
|
963
|
+
|
|
964
|
+
if (appState.activeFilter !== 'all' && appState.activeFilter !== event.agent) {
|
|
965
|
+
entry.style.display = 'none';
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const agentUpper = (event.agent || 'LEAD').toUpperCase();
|
|
969
|
+
entry.innerHTML = `
|
|
970
|
+
<span class="log-time">[${timeStr}]</span>
|
|
971
|
+
<span class="log-agent log-agent-${event.agent}">#${agentUpper}</span>
|
|
972
|
+
<span class="log-msg">${event.message}</span>
|
|
973
|
+
`;
|
|
974
|
+
|
|
975
|
+
terminal.appendChild(entry);
|
|
976
|
+
terminal.scrollTop = terminal.scrollHeight;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function renderLogs(events) {
|
|
980
|
+
const terminal = document.getElementById('terminalLogs');
|
|
981
|
+
if (!terminal) return;
|
|
982
|
+
terminal.innerHTML = '';
|
|
983
|
+
for (const evt of events) {
|
|
984
|
+
appendLog(evt);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function applyLogFilters() {
|
|
989
|
+
const entries = document.querySelectorAll('.log-entry');
|
|
990
|
+
entries.forEach((entry) => {
|
|
991
|
+
const agent = entry.getAttribute('data-agent');
|
|
992
|
+
if (appState.activeFilter === 'all' || appState.activeFilter === agent) {
|
|
993
|
+
entry.style.display = 'flex';
|
|
994
|
+
} else {
|
|
995
|
+
entry.style.display = 'none';
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Modal Inspector
|
|
1001
|
+
function openAgentModal(agentKey) {
|
|
1002
|
+
const modal = document.getElementById('agentModal');
|
|
1003
|
+
const agent = (appState.config && appState.config.agents) ? appState.config.agents[agentKey] : null;
|
|
1004
|
+
const ws = WORKSTATIONS[agentKey] || {};
|
|
1005
|
+
|
|
1006
|
+
const modalTitle = agent ? `${agent.name}` : (ws.name || 'Workstation Profile');
|
|
1007
|
+
const modalColor = agent ? agent.color : (ws.color || '#00f0ff');
|
|
1008
|
+
|
|
1009
|
+
document.getElementById('modalAgentName').textContent = `${modalTitle} [${agentKey.toUpperCase()}]`;
|
|
1010
|
+
document.getElementById('modalAgentName').style.color = modalColor;
|
|
1011
|
+
|
|
1012
|
+
const aState = agentKey === 'orchestrator'
|
|
1013
|
+
? (appState.state?.orchestrator || {})
|
|
1014
|
+
: (appState.state?.agents?.[agentKey] || {});
|
|
1015
|
+
|
|
1016
|
+
const body = document.getElementById('modalAgentBody');
|
|
1017
|
+
body.innerHTML = `
|
|
1018
|
+
<div>
|
|
1019
|
+
<strong>OFFICE TITLE & ROLE:</strong> ${ws.role || agent?.role || 'Autonomous Engineer'}
|
|
1020
|
+
</div>
|
|
1021
|
+
<div>
|
|
1022
|
+
<strong>DESK STATUS:</strong> <span style="color: ${modalColor}">${aState.state || 'IDLE'}</span> (${aState.expression || 'โ_โ'})
|
|
1023
|
+
</div>
|
|
1024
|
+
<div>
|
|
1025
|
+
<strong>CURRENT SPRINT TICKET:</strong> ${aState.currentTask || 'None'}
|
|
1026
|
+
</div>
|
|
1027
|
+
<div>
|
|
1028
|
+
<strong>ASSIGNED TECH STACK & SKILLS:</strong>
|
|
1029
|
+
<ul style="margin: 6px 0 0 20px;">
|
|
1030
|
+
${(agent?.skills || ['task-decomposition', 'architecture', 'coordination']).map(s => `<li>${s}</li>`).join('')}
|
|
1031
|
+
</ul>
|
|
1032
|
+
</div>
|
|
1033
|
+
<div>
|
|
1034
|
+
<strong>SECURITY & FILESYSTEM PERMISSIONS:</strong>
|
|
1035
|
+
<div style="background: #000; padding: 8px; margin-top: 4px; border: 1px solid #333; font-size: 13px;">
|
|
1036
|
+
<div>Read Access: <code>${JSON.stringify(agent?.permissions?.read || ['**/*'])}</code></div>
|
|
1037
|
+
<div>Write Access: <code>${JSON.stringify(agent?.permissions?.write || ['src/**'])}</code></div>
|
|
1038
|
+
</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
`;
|
|
1041
|
+
|
|
1042
|
+
modal.classList.add('open');
|
|
1043
|
+
synth.playClick();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Bootstrap
|
|
1047
|
+
window.addEventListener('DOMContentLoaded', initApp);
|