opencroc 1.8.2 → 1.8.4

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.
Files changed (45) hide show
  1. package/README.md +383 -417
  2. package/package.json +1 -1
  3. package/dist/web/index.html +0 -12
  4. package/dist/web/public/botreview/char_0.png +0 -0
  5. package/dist/web/public/botreview/char_1.png +0 -0
  6. package/dist/web/public/botreview/char_2.png +0 -0
  7. package/dist/web/public/botreview/coffee-machine.gif +0 -0
  8. package/dist/web/public/botreview/server.gif +0 -0
  9. package/dist/web/public/botreview/walls.png +0 -0
  10. package/dist/web/public/star/desk-v3.webp +0 -0
  11. package/dist/web/public/star/office_bg_small.webp +0 -0
  12. package/dist/web/public/star/star-idle-v5.png +0 -0
  13. package/dist/web/public/star/star-working-spritesheet-grid.webp +0 -0
  14. package/dist/web/src/app/AppLayout.tsx +0 -34
  15. package/dist/web/src/app/AppRouter.tsx +0 -46
  16. package/dist/web/src/app/bootstrap.tsx +0 -22
  17. package/dist/web/src/app/routes.tsx +0 -52
  18. package/dist/web/src/features/office/runtime/index.ts +0 -1
  19. package/dist/web/src/features/office/runtime/mount.ts +0 -809
  20. package/dist/web/src/features/pixel/runtime/index.ts +0 -1
  21. package/dist/web/src/features/pixel/runtime/mount.ts +0 -728
  22. package/dist/web/src/features/studio/runtime/index.ts +0 -1
  23. package/dist/web/src/features/studio/runtime/mount.ts +0 -664
  24. package/dist/web/src/features/three/engine/index.ts +0 -1
  25. package/dist/web/src/main.tsx +0 -7
  26. package/dist/web/src/pages/office/index.ts +0 -1
  27. package/dist/web/src/pages/office/page.tsx +0 -283
  28. package/dist/web/src/pages/pixel/index.ts +0 -1
  29. package/dist/web/src/pages/pixel/page.tsx +0 -564
  30. package/dist/web/src/pages/studio/index.ts +0 -1
  31. package/dist/web/src/pages/studio/page.tsx +0 -446
  32. package/dist/web/src/runtime/agents.ts +0 -738
  33. package/dist/web/src/runtime/camera.ts +0 -132
  34. package/dist/web/src/runtime/dataviz.ts +0 -312
  35. package/dist/web/src/runtime/effects.ts +0 -482
  36. package/dist/web/src/runtime/engine.ts +0 -528
  37. package/dist/web/src/runtime/office.ts +0 -932
  38. package/dist/web/src/runtime/state.ts +0 -37
  39. package/dist/web/src/runtime/ui.ts +0 -388
  40. package/dist/web/src/shared/assets.ts +0 -4
  41. package/dist/web/src/shared/navigation.ts +0 -47
  42. package/dist/web/src/styles/app-layout.css +0 -19
  43. package/dist/web/src/styles/office.css +0 -268
  44. package/dist/web/tsconfig.json +0 -28
  45. package/dist/web/vite.config.ts +0 -93
@@ -1,728 +0,0 @@
1
- type ProjectPayload = {
2
- graph?: { nodes?: Array<Record<string, any>>; edges?: Array<Record<string, any>> };
3
- agents?: Array<Record<string, any>>;
4
- stats?: Record<string, number>;
5
- backendRoot?: string;
6
- };
7
-
8
- function must<T extends HTMLElement>(id: string): T {
9
- const element = document.getElementById(id);
10
- if (!element) {
11
- throw new Error(`Missing element #${id}`);
12
- }
13
- return element as T;
14
- }
15
-
16
- function esc(value: unknown): string {
17
- return String(value ?? '')
18
- .replace(/&/g, '&amp;')
19
- .replace(/</g, '&lt;')
20
- .replace(/>/g, '&gt;')
21
- .replace(/"/g, '&quot;');
22
- }
23
-
24
- async function fetchJson(url: string, init?: RequestInit) {
25
- const response = await fetch(url, init);
26
- if (!response.ok) {
27
- throw new Error(`${response.status} ${response.statusText}`);
28
- }
29
- return response.json();
30
- }
31
-
32
- export async function mountPixelRuntime(): Promise<() => void> {
33
- const listeners: Array<() => void> = [];
34
- const nodePos = new Map<string, { x: number; y: number; node: Record<string, any> }>();
35
- const bubbleTimers = new Map<string, number>();
36
- const state = {
37
- project: null as ProjectPayload | null,
38
- graph: { nodes: [], edges: [] } as { nodes: Array<Record<string, any>>; edges: Array<Record<string, any>> },
39
- agents: [] as Array<Record<string, any>>,
40
- generatedFiles: [] as Array<Record<string, any>>,
41
- testMetrics: null as Record<string, any> | null,
42
- testQuality: null as Record<string, any> | null,
43
- reports: [] as Array<Record<string, any>>,
44
- runMode: 'auto',
45
- currentView: 'dashboard',
46
- theme: localStorage.getItem('opencroc-pixel-theme') || 'dark',
47
- ws: null as WebSocket | null,
48
- reconnectTimer: 0,
49
- shortcutTimer: 0,
50
- logs: [] as Array<{ ts: string; level: string; message: string }>,
51
- running: false,
52
- };
53
-
54
- const tooltip = must('tooltip');
55
- const canvas = must<HTMLCanvasElement>('graph-canvas');
56
- const ctx = canvas.getContext('2d');
57
- if (!ctx) {
58
- throw new Error('Graph canvas is not available.');
59
- }
60
-
61
- function listen(
62
- target: Pick<EventTarget, 'addEventListener' | 'removeEventListener'>,
63
- eventName: string,
64
- handler: EventListenerOrEventListenerObject,
65
- ) {
66
- target.addEventListener(eventName, handler);
67
- listeners.push(() => target.removeEventListener(eventName, handler));
68
- }
69
-
70
- function setTheme(theme: string) {
71
- state.theme = theme;
72
- document.documentElement.setAttribute('data-theme', theme);
73
- localStorage.setItem('opencroc-pixel-theme', theme);
74
- }
75
-
76
- function addLog(message: string, level = 'info') {
77
- state.logs.push({
78
- ts: new Date().toLocaleTimeString('zh-CN', { hour12: false }),
79
- level,
80
- message,
81
- });
82
- if (state.logs.length > 80) {
83
- state.logs = state.logs.slice(-80);
84
- }
85
- renderLogs();
86
- }
87
-
88
- function updateButtons() {
89
- const disabled = state.running;
90
- ['btn-scan', 'btn-pipeline', 'btn-reset', 'btn-run-tests', 'btn-reports'].forEach((id) => {
91
- must<HTMLButtonElement>(id).disabled = disabled;
92
- });
93
- }
94
-
95
- function updateStats() {
96
- const graph = state.graph;
97
- const stats = state.project?.stats || {};
98
- must('s-mod').textContent = String(stats.modules ?? graph.nodes.filter((node) => node.type === 'module').length);
99
- must('s-mdl').textContent = String(stats.models ?? graph.nodes.filter((node) => node.type === 'model').length);
100
- must('s-api').textContent = String(stats.endpoints ?? graph.nodes.filter((node) => node.type === 'api').length);
101
- must('s-files').textContent = String(state.generatedFiles.length || 0);
102
- if (state.testMetrics) {
103
- const wrap = must('s-results-wrap');
104
- wrap.style.display = '';
105
- must('s-results').textContent = `${state.testMetrics.passed || 0}/${state.testMetrics.failed || 0}`;
106
- }
107
- }
108
-
109
- function renderModules() {
110
- const container = must('mod-list');
111
- const modules = state.graph.nodes.filter((node) => node.type === 'module');
112
- if (!modules.length) {
113
- container.innerHTML = '<div class="pixel-list-item">No modules yet. Run Scan first.</div>';
114
- return;
115
- }
116
-
117
- container.innerHTML = modules
118
- .map((moduleNode) => `<button class="pixel-list-item" type="button" data-module-id="${esc(moduleNode.id)}"><strong>${esc(moduleNode.label || moduleNode.id)}</strong><div style="margin-top:6px;color:var(--pixel-dim);font-size:12px">${esc(moduleNode.path || '')}</div></button>`)
119
- .join('');
120
- }
121
-
122
- function renderAgentSidebar() {
123
- const container = must('agent-sidebar');
124
- if (!state.agents.length) {
125
- container.innerHTML = '<div class="pixel-list-item">No agents connected.</div>';
126
- return;
127
- }
128
- container.innerHTML = state.agents
129
- .map((agent) => `
130
- <div class="pixel-list-item">
131
- <strong>${esc(agent.name || agent.id)}</strong>
132
- <div style="margin-top:6px;color:var(--pixel-dim);font-size:12px">${esc(agent.role || 'agent')}</div>
133
- <div style="margin-top:4px;color:var(--pixel-dim);font-size:12px">${esc(agent.status || 'idle')}</div>
134
- </div>
135
- `)
136
- .join('');
137
- }
138
-
139
- function layoutGraph() {
140
- nodePos.clear();
141
- const width = canvas.clientWidth || canvas.parentElement?.clientWidth || 960;
142
- const height = canvas.clientHeight || canvas.parentElement?.clientHeight || 640;
143
- const modules = state.graph.nodes.filter((node) => node.type === 'module');
144
- const others = state.graph.nodes.filter((node) => node.type !== 'module');
145
-
146
- modules.forEach((node, index) => {
147
- const angle = (index / Math.max(modules.length, 1)) * Math.PI * 2;
148
- const x = width / 2 + Math.cos(angle) * Math.min(width, height) * 0.28;
149
- const y = height / 2 + Math.sin(angle) * Math.min(width, height) * 0.22;
150
- nodePos.set(String(node.id), { x, y, node });
151
- });
152
-
153
- others.forEach((node, index) => {
154
- const moduleNode = modules.find((entry) => entry.label === node.module || entry.id === node.module);
155
- const anchor = moduleNode ? nodePos.get(String(moduleNode.id)) : undefined;
156
- const angle = (index / Math.max(others.length, 1)) * Math.PI * 2;
157
- const radius = anchor ? 90 + (index % 6) * 18 : Math.min(width, height) * 0.18;
158
- nodePos.set(String(node.id), {
159
- x: (anchor?.x || width / 2) + Math.cos(angle) * radius,
160
- y: (anchor?.y || height / 2) + Math.sin(angle) * radius,
161
- node,
162
- });
163
- });
164
- }
165
-
166
- function renderCanvas() {
167
- const width = canvas.clientWidth || canvas.parentElement?.clientWidth || 960;
168
- const height = canvas.clientHeight || canvas.parentElement?.clientHeight || 640;
169
- canvas.width = width * Math.max(window.devicePixelRatio, 1);
170
- canvas.height = height * Math.max(window.devicePixelRatio, 1);
171
- canvas.style.width = `${width}px`;
172
- canvas.style.height = `${height}px`;
173
- ctx.setTransform(Math.max(window.devicePixelRatio, 1), 0, 0, Math.max(window.devicePixelRatio, 1), 0, 0);
174
- ctx.clearRect(0, 0, width, height);
175
- layoutGraph();
176
-
177
- ctx.strokeStyle = state.theme === 'dark' ? 'rgba(148,163,184,0.28)' : 'rgba(100,116,139,0.26)';
178
- ctx.lineWidth = 1.4;
179
- for (const edge of state.graph.edges) {
180
- const source = nodePos.get(String(edge.source));
181
- const target = nodePos.get(String(edge.target));
182
- if (!source || !target) {
183
- continue;
184
- }
185
- ctx.beginPath();
186
- ctx.moveTo(source.x, source.y);
187
- ctx.lineTo(target.x, target.y);
188
- ctx.stroke();
189
- }
190
-
191
- for (const { x, y, node } of nodePos.values()) {
192
- const fill = node.type === 'module'
193
- ? '#a78bfa'
194
- : node.type === 'api'
195
- ? '#fbbf24'
196
- : node.type === 'model'
197
- ? '#34d399'
198
- : '#60a5fa';
199
- ctx.fillStyle = fill;
200
- ctx.beginPath();
201
- ctx.arc(x, y, node.type === 'module' ? 16 : 10, 0, Math.PI * 2);
202
- ctx.fill();
203
-
204
- ctx.fillStyle = state.theme === 'dark' ? '#e2e8f0' : '#0f172a';
205
- ctx.font = node.type === 'module' ? '12px sans-serif' : '10px sans-serif';
206
- ctx.textAlign = 'center';
207
- ctx.fillText(String(node.label || node.id), x, y + (node.type === 'module' ? 32 : 24));
208
- }
209
- }
210
-
211
- function renderLogs() {
212
- const container = must('log-list');
213
- container.innerHTML = state.logs
214
- .slice()
215
- .reverse()
216
- .map((entry) => `<div class="pixel-log-entry"><span class="ts">${esc(entry.ts)}</span><strong>${esc(entry.level)}</strong><div style="margin-top:4px">${esc(entry.message)}</div></div>`)
217
- .join('');
218
- }
219
-
220
- function renderOfficeCards() {
221
- const container = must('croc-office');
222
- if (!state.agents.length) {
223
- container.innerHTML = '<div class="pixel-agent-card">No active agents yet.</div>';
224
- return;
225
- }
226
-
227
- container.innerHTML = state.agents
228
- .map((agent) => {
229
- const progress = typeof agent.progress === 'number' ? Math.max(0, Math.min(100, agent.progress)) : 0;
230
- return `
231
- <div class="pixel-agent-card">
232
- <strong>${esc(agent.name || agent.id)}</strong>
233
- <span class="role">${esc(agent.role || 'agent')}</span>
234
- <span class="task">${esc(agent.currentTask || 'Awaiting task')}</span>
235
- <div class="pixel-progress"><span style="width:${progress}%"></span></div>
236
- </div>
237
- `;
238
- })
239
- .join('');
240
- }
241
-
242
- function clearBubbles() {
243
- for (const timer of bubbleTimers.values()) {
244
- window.clearInterval(timer);
245
- }
246
- bubbleTimers.clear();
247
- }
248
-
249
- function scheduleBubbles() {
250
- clearBubbles();
251
- const layer = must('pixel-agent-layer');
252
- for (const agent of state.agents) {
253
- const timer = window.setInterval(() => {
254
- const agentElement = layer.querySelector<HTMLElement>(`[data-agent-name="${CSS.escape(String(agent.name || agent.id))}"]`);
255
- if (!agentElement || state.currentView !== 'office') {
256
- return;
257
- }
258
- const bubble = document.createElement('div');
259
- bubble.className = 'pixel-bubble';
260
- bubble.textContent = String(agent.status || 'idle');
261
- bubble.style.left = `${agentElement.offsetLeft + 50}px`;
262
- bubble.style.top = `${agentElement.offsetTop - 10}px`;
263
- layer.appendChild(bubble);
264
- window.setTimeout(() => bubble.remove(), 3000);
265
- }, 7000 + Math.round(Math.random() * 4000));
266
- bubbleTimers.set(String(agent.name || agent.id), timer);
267
- }
268
- }
269
-
270
- function renderPixelOffice() {
271
- const layer = must('pixel-agent-layer');
272
- const view = must('pixel-view');
273
- const width = view.clientWidth || 900;
274
- const height = view.clientHeight || 540;
275
- const presets = [
276
- { x: 0.18, y: 0.64 }, { x: 0.3, y: 0.66 }, { x: 0.42, y: 0.62 }, { x: 0.56, y: 0.6 }, { x: 0.72, y: 0.63 },
277
- { x: 0.82, y: 0.52 }, { x: 0.22, y: 0.45 }, { x: 0.48, y: 0.42 }, { x: 0.66, y: 0.4 }, { x: 0.78, y: 0.72 },
278
- ];
279
- const working = state.agents.filter((agent) => agent.status === 'working' || agent.status === 'testing').length;
280
- const errors = state.agents.filter((agent) => agent.status === 'error' || agent.status === 'failed').length;
281
- const done = state.agents.filter((agent) => agent.status === 'done' || agent.status === 'passed').length;
282
- must('kpi-working').textContent = String(working);
283
- must('kpi-errors').textContent = String(errors);
284
- must('kpi-done').textContent = String(done);
285
-
286
- layer.innerHTML = state.agents
287
- .map((agent, index) => {
288
- const preset = presets[index % presets.length];
289
- const left = Math.round(width * preset.x);
290
- const top = Math.round(height * preset.y);
291
- return `
292
- <div class="pixel-avatar ${esc(agent.status || 'idle')}" data-agent-name="${esc(agent.name || agent.id)}" style="left:${left}px;top:${top}px">🐊</div>
293
- <div class="pixel-label" style="left:${left + 24}px;top:${top - 18}px">
294
- <strong>${esc(agent.name || agent.id)}</strong>
295
- <span class="role">${esc(agent.role || 'agent')}</span>
296
- <span class="task">${esc(agent.currentTask || agent.status || 'idle')}</span>
297
- </div>
298
- `;
299
- })
300
- .join('');
301
-
302
- scheduleBubbles();
303
- }
304
-
305
- function renderFiles() {
306
- const container = must('file-list');
307
- if (!state.generatedFiles.length) {
308
- container.innerHTML = '<div class="pixel-file-item">No generated tests yet.</div>';
309
- return;
310
- }
311
- container.innerHTML = state.generatedFiles
312
- .map((file, index) => `
313
- <button class="pixel-file-item" data-file-index="${index}" type="button">
314
- <strong>${esc(file.filePath || `Generated file ${index + 1}`)}</strong>
315
- <span class="pixel-file-meta">${esc(file.module || '')} ${file.lines ? `| ${file.lines} lines` : ''}</span>
316
- </button>
317
- `)
318
- .join('');
319
- }
320
-
321
- function renderResults() {
322
- const container = must('results-panel');
323
- if (!state.testMetrics && !state.testQuality) {
324
- container.innerHTML = '<div class="pixel-file-item">No test results yet.</div>';
325
- return;
326
- }
327
- const metrics = state.testMetrics;
328
- const quality = state.testQuality;
329
- const total = metrics ? (metrics.passed || 0) + (metrics.failed || 0) + (metrics.skipped || 0) + (metrics.timedOut || 0) : 0;
330
- const passRate = total ? Math.round(((metrics?.passed || 0) / total) * 100) : 0;
331
- container.innerHTML = `
332
- <div class="pixel-file-item">
333
- <strong>Test Results</strong>
334
- <div style="margin-top:10px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px">
335
- <div class="pixel-agent-card"><strong>${metrics?.passed || 0}</strong><span class="role">Passed</span></div>
336
- <div class="pixel-agent-card"><strong>${metrics?.failed || 0}</strong><span class="role">Failed</span></div>
337
- <div class="pixel-agent-card"><strong>${metrics?.skipped || 0}</strong><span class="role">Skipped</span></div>
338
- <div class="pixel-agent-card"><strong>${metrics?.timedOut || 0}</strong><span class="role">Timed Out</span></div>
339
- </div>
340
- <div class="pixel-progress" style="margin-top:14px"><span style="width:${passRate}%"></span></div>
341
- <span class="pixel-file-meta">Pass rate ${passRate}%</span>
342
- </div>
343
- ${quality ? `<div class="pixel-file-item" style="margin-top:10px"><strong>Execution Quality</strong><pre style="white-space:pre-wrap;color:var(--pixel-dim);font-size:12px">${esc(JSON.stringify(quality, null, 2))}</pre></div>` : ''}
344
- `;
345
- }
346
-
347
- function renderReports() {
348
- const container = must('reports-panel');
349
- if (!state.reports.length) {
350
- container.innerHTML = '<div class="pixel-file-item">No reports available.</div>';
351
- return;
352
- }
353
- container.innerHTML = state.reports
354
- .map((report) => `
355
- <button class="pixel-file-item" data-report-format="${esc(report.format || '')}" type="button">
356
- <strong>${esc(report.filename || `report.${report.format || 'txt'}`)}</strong>
357
- <span class="pixel-file-meta">${esc(report.format || 'report')}</span>
358
- </button>
359
- `)
360
- .join('');
361
- }
362
-
363
- function updateAll() {
364
- updateStats();
365
- renderModules();
366
- renderAgentSidebar();
367
- renderCanvas();
368
- renderPixelOffice();
369
- renderOfficeCards();
370
- renderFiles();
371
- renderResults();
372
- renderReports();
373
- }
374
-
375
- async function fetchProject() {
376
- const project = (await fetchJson('/api/project')) as ProjectPayload;
377
- state.project = project;
378
- state.graph = {
379
- nodes: project.graph?.nodes || [],
380
- edges: project.graph?.edges || [],
381
- };
382
- state.agents = project.agents || [];
383
- updateAll();
384
- }
385
-
386
- async function doScan() {
387
- state.running = true;
388
- updateButtons();
389
- addLog('Starting project scan...');
390
- try {
391
- await fetchJson('/api/scan', { method: 'POST' });
392
- } catch (error) {
393
- addLog(`Scan failed: ${error instanceof Error ? error.message : String(error)}`, 'error');
394
- state.running = false;
395
- updateButtons();
396
- }
397
- }
398
-
399
- async function doPipeline() {
400
- state.running = true;
401
- updateButtons();
402
- addLog('Pipeline started...');
403
- try {
404
- await fetchJson('/api/pipeline', { method: 'POST' });
405
- } catch (error) {
406
- addLog(`Pipeline failed: ${error instanceof Error ? error.message : String(error)}`, 'error');
407
- state.running = false;
408
- updateButtons();
409
- }
410
- }
411
-
412
- async function doReset() {
413
- try {
414
- await fetchJson('/api/reset', { method: 'POST' });
415
- addLog('Agents reset.');
416
- } catch (error) {
417
- addLog(`Reset failed: ${error instanceof Error ? error.message : String(error)}`, 'error');
418
- }
419
- state.running = false;
420
- updateButtons();
421
- }
422
-
423
- async function doRunTests() {
424
- state.running = true;
425
- updateButtons();
426
- addLog(`Running tests (${state.runMode})...`);
427
- try {
428
- const payload = await fetchJson('/api/run-tests', {
429
- method: 'POST',
430
- headers: { 'Content-Type': 'application/json' },
431
- body: JSON.stringify({ mode: state.runMode }),
432
- });
433
- if (payload.error) {
434
- addLog(`Test run failed: ${payload.error}`, 'error');
435
- }
436
- } catch (error) {
437
- addLog(`Tests failed: ${error instanceof Error ? error.message : String(error)}`, 'error');
438
- state.running = false;
439
- updateButtons();
440
- }
441
- }
442
-
443
- async function doReports() {
444
- state.running = true;
445
- updateButtons();
446
- addLog('Generating reports...');
447
- try {
448
- await fetchJson('/api/reports/generate', { method: 'POST' });
449
- } catch (error) {
450
- addLog(`Report generation failed: ${error instanceof Error ? error.message : String(error)}`, 'error');
451
- state.running = false;
452
- updateButtons();
453
- }
454
- }
455
-
456
- function connectWs() {
457
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
458
- const socket = new WebSocket(`${protocol}//${location.host}/ws`);
459
- state.ws = socket;
460
-
461
- socket.onopen = () => {
462
- must('conn-dot').classList.add('connected');
463
- addLog('WebSocket connected.');
464
- };
465
-
466
- socket.onclose = () => {
467
- must('conn-dot').classList.remove('connected');
468
- addLog('WebSocket disconnected. Retrying...', 'warn');
469
- state.reconnectTimer = window.setTimeout(connectWs, 3000);
470
- };
471
-
472
- socket.onmessage = (event) => {
473
- try {
474
- const message = JSON.parse(event.data);
475
- if (message.type === 'agent:update') {
476
- state.agents = message.payload || [];
477
- updateAll();
478
- return;
479
- }
480
- if (message.type === 'graph:update') {
481
- state.graph = {
482
- nodes: message.payload?.nodes || [],
483
- edges: message.payload?.edges || [],
484
- };
485
- renderCanvas();
486
- renderModules();
487
- return;
488
- }
489
- if (message.type === 'log') {
490
- addLog(String(message.payload?.message || ''), String(message.payload?.level || 'info'));
491
- return;
492
- }
493
- if (message.type === 'files:generated') {
494
- state.generatedFiles = message.payload || [];
495
- renderFiles();
496
- updateStats();
497
- return;
498
- }
499
- if (message.type === 'pipeline:complete') {
500
- state.running = false;
501
- updateButtons();
502
- addLog(message.payload?.error ? `Pipeline failed: ${message.payload.error}` : 'Pipeline completed.', message.payload?.error ? 'error' : 'info');
503
- void fetchProject();
504
- return;
505
- }
506
- if (message.type === 'test:complete') {
507
- state.running = false;
508
- state.testMetrics = message.payload?.metrics || null;
509
- state.testQuality = message.payload?.quality || null;
510
- updateButtons();
511
- renderResults();
512
- updateStats();
513
- return;
514
- }
515
- if (message.type === 'reports:generated') {
516
- state.running = false;
517
- state.reports = message.payload || [];
518
- updateButtons();
519
- renderReports();
520
- return;
521
- }
522
- if (message.type === 'scan:complete') {
523
- state.running = false;
524
- updateButtons();
525
- addLog('Scan completed.');
526
- void fetchProject();
527
- }
528
- } catch {
529
- // Ignore malformed message.
530
- }
531
- };
532
- }
533
-
534
- function setView(view: 'dashboard' | 'office') {
535
- state.currentView = view;
536
- must('graph-view').classList.toggle('hidden', view !== 'dashboard');
537
- must('pixel-view').classList.toggle('hidden', view !== 'office');
538
- must('view-dashboard').classList.toggle('active', view === 'dashboard');
539
- must('view-office').classList.toggle('active', view === 'office');
540
- tooltip.classList.remove('visible');
541
- if (view === 'dashboard') {
542
- renderCanvas();
543
- return;
544
- }
545
- renderPixelOffice();
546
- }
547
-
548
- async function openFilePreview(index: number) {
549
- const payload = await fetchJson(`/api/files/${index}`);
550
- must('fp-title').textContent = payload.filePath || 'file.ts';
551
- must('fp-code').textContent = payload.content || '';
552
- must('file-preview').classList.add('visible');
553
- }
554
-
555
- async function openReportPreview(format: string) {
556
- const response = await fetch(`/api/reports/${format}`);
557
- const content = await response.text();
558
- if (format === 'html') {
559
- const nextWindow = window.open('', '_blank');
560
- if (nextWindow) {
561
- nextWindow.document.write(content);
562
- nextWindow.document.close();
563
- }
564
- return;
565
- }
566
- must('fp-title').textContent = `report.${format}`;
567
- must('fp-code').textContent = content;
568
- must('file-preview').classList.add('visible');
569
- }
570
-
571
- function showShortcutLegend() {
572
- const legend = must('shortcut-legend');
573
- legend.classList.add('visible');
574
- window.clearTimeout(state.shortcutTimer);
575
- state.shortcutTimer = window.setTimeout(() => legend.classList.remove('visible'), 4000);
576
- }
577
-
578
- listen(must('btn-scan'), 'click', () => void doScan());
579
- listen(must('btn-pipeline'), 'click', () => void doPipeline());
580
- listen(must('btn-reset'), 'click', () => void doReset());
581
- listen(must('btn-run-tests'), 'click', () => void doRunTests());
582
- listen(must('btn-reports'), 'click', () => void doReports());
583
- listen(must('run-mode'), 'change', (event) => {
584
- state.runMode = (event.target as HTMLSelectElement).value;
585
- });
586
- listen(must('view-dashboard'), 'click', () => setView('dashboard'));
587
- listen(must('view-office'), 'click', () => setView('office'));
588
- listen(must('theme-toggle'), 'click', () => {
589
- setTheme(state.theme === 'dark' ? 'light' : 'dark');
590
- renderCanvas();
591
- });
592
- listen(must('fp-close'), 'click', () => must('file-preview').classList.remove('visible'));
593
- listen(must('file-preview'), 'click', (event) => {
594
- if (event.target === must('file-preview')) {
595
- must('file-preview').classList.remove('visible');
596
- }
597
- });
598
- listen(document.querySelectorAll('.pixel-tab').item(0).parentElement as HTMLElement, 'click', (event) => {
599
- const target = (event.target as HTMLElement).closest<HTMLElement>('.pixel-tab');
600
- if (!target) {
601
- return;
602
- }
603
- const tab = target.dataset.tab;
604
- document.querySelectorAll<HTMLElement>('.pixel-tab').forEach((button) => {
605
- button.classList.toggle('active', button === target);
606
- });
607
- must('log-list').style.display = tab === 'log' ? '' : 'none';
608
- must('file-list').style.display = tab === 'files' ? '' : 'none';
609
- must('results-panel').style.display = tab === 'results' ? '' : 'none';
610
- must('reports-panel').style.display = tab === 'reports' ? '' : 'none';
611
- });
612
- listen(must('file-list'), 'click', (event) => {
613
- const target = (event.target as HTMLElement).closest<HTMLElement>('[data-file-index]');
614
- if (!target) {
615
- return;
616
- }
617
- void openFilePreview(Number(target.dataset.fileIndex));
618
- });
619
- listen(must('reports-panel'), 'click', (event) => {
620
- const target = (event.target as HTMLElement).closest<HTMLElement>('[data-report-format]');
621
- if (!target) {
622
- return;
623
- }
624
- void openReportPreview(target.dataset.reportFormat || 'txt');
625
- });
626
- listen(canvas, 'mousemove', (event) => {
627
- const rect = canvas.getBoundingClientRect();
628
- const x = event.clientX - rect.left;
629
- const y = event.clientY - rect.top;
630
- let hit: { node: Record<string, any> } | undefined;
631
- for (const item of nodePos.values()) {
632
- const radius = item.node.type === 'module' ? 18 : 12;
633
- if (Math.abs(item.x - x) <= radius && Math.abs(item.y - y) <= radius) {
634
- hit = item;
635
- }
636
- }
637
- if (!hit) {
638
- tooltip.classList.remove('visible');
639
- return;
640
- }
641
- tooltip.classList.add('visible');
642
- tooltip.innerHTML = `<strong>${esc(hit.node.label || hit.node.id)}</strong><div>${esc(hit.node.type || 'unknown')}</div>`;
643
- tooltip.style.left = `${event.clientX + 12}px`;
644
- tooltip.style.top = `${event.clientY + 12}px`;
645
- });
646
- listen(canvas, 'mouseleave', () => tooltip.classList.remove('visible'));
647
- listen(window, 'resize', () => {
648
- renderCanvas();
649
- renderPixelOffice();
650
- });
651
- listen(document, 'keydown', (event) => {
652
- const keyboardEvent = event as KeyboardEvent;
653
- const target = keyboardEvent.target as HTMLElement | null;
654
- const tag = target?.tagName?.toLowerCase();
655
- if (tag === 'input' || tag === 'textarea' || tag === 'select') {
656
- return;
657
- }
658
- if (keyboardEvent.key === 'Escape') {
659
- must('file-preview').classList.remove('visible');
660
- must('shortcut-legend').classList.remove('visible');
661
- return;
662
- }
663
- const key = keyboardEvent.key.toLowerCase();
664
- if (key === '?' || (keyboardEvent.key === '/' && keyboardEvent.shiftKey)) {
665
- keyboardEvent.preventDefault();
666
- showShortcutLegend();
667
- return;
668
- }
669
- if (key === '1') {
670
- keyboardEvent.preventDefault();
671
- setView('dashboard');
672
- return;
673
- }
674
- if (key === '2') {
675
- keyboardEvent.preventDefault();
676
- setView('office');
677
- return;
678
- }
679
- if (key === 's') {
680
- keyboardEvent.preventDefault();
681
- void doScan();
682
- return;
683
- }
684
- if (key === 'p') {
685
- keyboardEvent.preventDefault();
686
- void doPipeline();
687
- return;
688
- }
689
- if (key === 't') {
690
- keyboardEvent.preventDefault();
691
- void doRunTests();
692
- return;
693
- }
694
- if (key === 'r') {
695
- keyboardEvent.preventDefault();
696
- void doReports();
697
- return;
698
- }
699
- if (key === 'x') {
700
- keyboardEvent.preventDefault();
701
- void doReset();
702
- return;
703
- }
704
- if (key === 'd') {
705
- keyboardEvent.preventDefault();
706
- setTheme(state.theme === 'dark' ? 'light' : 'dark');
707
- renderCanvas();
708
- }
709
- });
710
-
711
- setTheme(state.theme);
712
- updateButtons();
713
- addLog('OpenCroc Studio pixel dashboard ready. Press ? for shortcuts.');
714
- await fetchProject();
715
- connectWs();
716
- updateAll();
717
-
718
- return () => {
719
- listeners.forEach((cleanup) => cleanup());
720
- listeners.length = 0;
721
- if (state.ws) {
722
- state.ws.close();
723
- }
724
- clearBubbles();
725
- window.clearTimeout(state.reconnectTimer);
726
- window.clearTimeout(state.shortcutTimer);
727
- };
728
- }