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.
@@ -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);