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.
- package/README.md +383 -417
- package/package.json +1 -1
- package/dist/web/index.html +0 -12
- package/dist/web/public/botreview/char_0.png +0 -0
- package/dist/web/public/botreview/char_1.png +0 -0
- package/dist/web/public/botreview/char_2.png +0 -0
- package/dist/web/public/botreview/coffee-machine.gif +0 -0
- package/dist/web/public/botreview/server.gif +0 -0
- package/dist/web/public/botreview/walls.png +0 -0
- package/dist/web/public/star/desk-v3.webp +0 -0
- package/dist/web/public/star/office_bg_small.webp +0 -0
- package/dist/web/public/star/star-idle-v5.png +0 -0
- package/dist/web/public/star/star-working-spritesheet-grid.webp +0 -0
- package/dist/web/src/app/AppLayout.tsx +0 -34
- package/dist/web/src/app/AppRouter.tsx +0 -46
- package/dist/web/src/app/bootstrap.tsx +0 -22
- package/dist/web/src/app/routes.tsx +0 -52
- package/dist/web/src/features/office/runtime/index.ts +0 -1
- package/dist/web/src/features/office/runtime/mount.ts +0 -809
- package/dist/web/src/features/pixel/runtime/index.ts +0 -1
- package/dist/web/src/features/pixel/runtime/mount.ts +0 -728
- package/dist/web/src/features/studio/runtime/index.ts +0 -1
- package/dist/web/src/features/studio/runtime/mount.ts +0 -664
- package/dist/web/src/features/three/engine/index.ts +0 -1
- package/dist/web/src/main.tsx +0 -7
- package/dist/web/src/pages/office/index.ts +0 -1
- package/dist/web/src/pages/office/page.tsx +0 -283
- package/dist/web/src/pages/pixel/index.ts +0 -1
- package/dist/web/src/pages/pixel/page.tsx +0 -564
- package/dist/web/src/pages/studio/index.ts +0 -1
- package/dist/web/src/pages/studio/page.tsx +0 -446
- package/dist/web/src/runtime/agents.ts +0 -738
- package/dist/web/src/runtime/camera.ts +0 -132
- package/dist/web/src/runtime/dataviz.ts +0 -312
- package/dist/web/src/runtime/effects.ts +0 -482
- package/dist/web/src/runtime/engine.ts +0 -528
- package/dist/web/src/runtime/office.ts +0 -932
- package/dist/web/src/runtime/state.ts +0 -37
- package/dist/web/src/runtime/ui.ts +0 -388
- package/dist/web/src/shared/assets.ts +0 -4
- package/dist/web/src/shared/navigation.ts +0 -47
- package/dist/web/src/styles/app-layout.css +0 -19
- package/dist/web/src/styles/office.css +0 -268
- package/dist/web/tsconfig.json +0 -28
- package/dist/web/vite.config.ts +0 -93
|
@@ -1,809 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createEngine,
|
|
3
|
-
disposeEngine,
|
|
4
|
-
getCamera,
|
|
5
|
-
getClock,
|
|
6
|
-
getComposer,
|
|
7
|
-
getRenderer,
|
|
8
|
-
getScene,
|
|
9
|
-
resizeEngine,
|
|
10
|
-
updateEngine,
|
|
11
|
-
updateEngineTheme,
|
|
12
|
-
} from '@runtime/engine';
|
|
13
|
-
import {
|
|
14
|
-
createOffice,
|
|
15
|
-
disposeOffice,
|
|
16
|
-
updateOfficeLighting,
|
|
17
|
-
} from '@runtime/office';
|
|
18
|
-
import { AgentManager } from '@runtime/agents';
|
|
19
|
-
import { ParticleManager } from '@runtime/effects';
|
|
20
|
-
import { CameraController } from '@runtime/camera';
|
|
21
|
-
import { GraphViz, HologramDisplay } from '@runtime/dataviz';
|
|
22
|
-
import { StateManager } from '@runtime/state';
|
|
23
|
-
import { UIManager } from '@runtime/ui';
|
|
24
|
-
import { navigate } from '@shared/navigation';
|
|
25
|
-
|
|
26
|
-
type GraphPayload = {
|
|
27
|
-
nodes?: Array<Record<string, unknown>>;
|
|
28
|
-
edges?: Array<Record<string, unknown>>;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
type AgentPayload = Array<Record<string, any>>;
|
|
32
|
-
|
|
33
|
-
type StudioSummary = {
|
|
34
|
-
stats?: Record<string, number>;
|
|
35
|
-
risks?: number;
|
|
36
|
-
[key: string]: unknown;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const ICONS = {
|
|
40
|
-
croc: '<svg viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="10" rx="1" stroke="currentColor" stroke-width="1.5"/><circle cx="8" cy="9" r="2.5" fill="currentColor"/><rect x="5" y="2" width="6" height="2" rx="0.5" fill="currentColor" opacity="0.6"/></svg>',
|
|
41
|
-
parser: '<svg viewBox="0 0 16 16" fill="none"><rect x="2" y="9" width="3" height="5" rx="0.5" fill="currentColor" opacity="0.7"/><rect x="6.5" y="5" width="3" height="9" rx="0.5" fill="currentColor" opacity="0.8"/><rect x="11" y="2" width="3" height="12" rx="0.5" fill="currentColor"/></svg>',
|
|
42
|
-
analyzer: '<svg viewBox="0 0 16 16" fill="none"><rect x="2" y="8" width="2.5" height="6" rx="0.5" fill="currentColor" opacity="0.5"/><rect x="5.5" y="5" width="2.5" height="9" rx="0.5" fill="currentColor" opacity="0.7"/><rect x="9" y="3" width="2.5" height="11" rx="0.5" fill="currentColor" opacity="0.85"/><rect x="12.5" y="6" width="2.5" height="8" rx="0.5" fill="currentColor"/></svg>',
|
|
43
|
-
tester: '<svg viewBox="0 0 16 16" fill="none"><path d="M6 2h4v4l3 7a1 1 0 01-1 1H4a1 1 0 01-1-1l3-7V2z" stroke="currentColor" stroke-width="1.2"/><line x1="5" y1="2" x2="11" y2="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
|
|
44
|
-
healer: '<svg viewBox="0 0 16 16" fill="none"><path d="M8 2L3 9h4l-1 5 6-7H8l1-5z" fill="currentColor" opacity="0.8"/></svg>',
|
|
45
|
-
planner: '<svg viewBox="0 0 16 16" fill="none"><rect x="3" y="1" width="10" height="14" rx="1" stroke="currentColor" stroke-width="1.2"/><path d="M6 1V3M10 1V3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><line x1="5" y1="6" x2="11" y2="6" stroke="currentColor" stroke-width="1" opacity="0.4"/><line x1="5" y1="9" x2="11" y2="9" stroke="currentColor" stroke-width="1" opacity="0.4"/><line x1="5" y1="12" x2="9" y2="12" stroke="currentColor" stroke-width="1" opacity="0.4"/></svg>',
|
|
46
|
-
reporter: '<svg viewBox="0 0 16 16" fill="none"><polyline points="2,12 5,6 8,9 11,4 14,7" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const ROLE_ICONS: Record<string, string> = {
|
|
50
|
-
parser: ICONS.parser,
|
|
51
|
-
analyzer: ICONS.analyzer,
|
|
52
|
-
tester: ICONS.tester,
|
|
53
|
-
healer: ICONS.healer,
|
|
54
|
-
planner: ICONS.planner,
|
|
55
|
-
reporter: ICONS.reporter,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const DYNAMIC_ROLE_ICONS: Record<string, string> = {
|
|
59
|
-
security: 'shield',
|
|
60
|
-
performance: 'perf',
|
|
61
|
-
architecture: 'arch',
|
|
62
|
-
'data-modeling': 'data',
|
|
63
|
-
devops: 'ops',
|
|
64
|
-
'api-design': 'api',
|
|
65
|
-
refactor: 'ref',
|
|
66
|
-
microservice: 'svc',
|
|
67
|
-
python: 'py',
|
|
68
|
-
go: 'go',
|
|
69
|
-
java: 'java',
|
|
70
|
-
rust: 'rs',
|
|
71
|
-
react: 'react',
|
|
72
|
-
vue: 'vue',
|
|
73
|
-
express: 'express',
|
|
74
|
-
django: 'django',
|
|
75
|
-
springboot: 'spring',
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const BUBBLE_TEXTS = {
|
|
79
|
-
working: ['Working...', 'Almost there...', 'Processing...', 'On it.'],
|
|
80
|
-
testing: ['Running tests...', 'Checking API...', 'Verifying...'],
|
|
81
|
-
thinking: ['Thinking...', 'Analyzing...', 'Reasoning...'],
|
|
82
|
-
error: ['Something broke.', 'Fixing it...', 'Investigating...'],
|
|
83
|
-
idle: ['Standing by.', 'Waiting for work.', 'Coffee break.'],
|
|
84
|
-
done: ['Done.', 'Completed.', 'Ready for the next task.'],
|
|
85
|
-
passed: ['All green.', 'Checks passed.'],
|
|
86
|
-
failed: ['Needs attention.', 'Requires a fix.'],
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
function esc(value: unknown): string {
|
|
90
|
-
return String(value ?? '')
|
|
91
|
-
.replace(/&/g, '&')
|
|
92
|
-
.replace(/</g, '<')
|
|
93
|
-
.replace(/>/g, '>')
|
|
94
|
-
.replace(/"/g, '"');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function mustElement<T extends HTMLElement>(id: string): T {
|
|
98
|
-
const element = document.getElementById(id);
|
|
99
|
-
if (!element) {
|
|
100
|
-
throw new Error(`Missing required element: #${id}`);
|
|
101
|
-
}
|
|
102
|
-
return element as T;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
class OfficeRuntime {
|
|
106
|
-
private readonly state = new StateManager();
|
|
107
|
-
private readonly ui = new UIManager(this.state, {
|
|
108
|
-
ICONS,
|
|
109
|
-
ROLE_ICONS,
|
|
110
|
-
DYNAMIC_ROLE_ICONS,
|
|
111
|
-
resolveRoleIcon: (name: string) => ROLE_ICONS[name] || DYNAMIC_ROLE_ICONS[name] || 'bot',
|
|
112
|
-
BUBBLE_TEXTS,
|
|
113
|
-
esc,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
private agentMgr: any;
|
|
117
|
-
private particleMgr: any;
|
|
118
|
-
private camCtrl: any;
|
|
119
|
-
private graphViz: any;
|
|
120
|
-
private hologram: any;
|
|
121
|
-
private ws: WebSocket | null = null;
|
|
122
|
-
private rafId = 0;
|
|
123
|
-
private reconnectTimer = 0;
|
|
124
|
-
private shortcutTimer = 0;
|
|
125
|
-
private disposed = false;
|
|
126
|
-
private listeners: Array<() => void> = [];
|
|
127
|
-
|
|
128
|
-
async mount(): Promise<void> {
|
|
129
|
-
this.ui.setLoading(5, 'Creating runtime state...');
|
|
130
|
-
this.state.set({
|
|
131
|
-
project: null,
|
|
132
|
-
graph: { nodes: [], edges: [] },
|
|
133
|
-
agents: [],
|
|
134
|
-
ws: null,
|
|
135
|
-
running: false,
|
|
136
|
-
generatedFiles: [],
|
|
137
|
-
testMetrics: null,
|
|
138
|
-
testQuality: null,
|
|
139
|
-
reports: [],
|
|
140
|
-
runMode: 'auto',
|
|
141
|
-
currentView: '3d',
|
|
142
|
-
theme: localStorage.getItem('opencroc-theme') || 'light',
|
|
143
|
-
modMeta: new Map(),
|
|
144
|
-
nodePos: new Map(),
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const theme = this.state.get('theme') as string;
|
|
148
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
149
|
-
mustElement<HTMLElement>('theme-icon-dark').style.display = theme === 'dark' ? '' : 'none';
|
|
150
|
-
mustElement<HTMLElement>('theme-icon-light').style.display = theme === 'light' ? '' : 'none';
|
|
151
|
-
|
|
152
|
-
this.ui.setLoading(10, 'Initializing 3D engine...');
|
|
153
|
-
const canvas = mustElement<HTMLCanvasElement>('three-canvas');
|
|
154
|
-
await createEngine(canvas, theme);
|
|
155
|
-
|
|
156
|
-
this.ui.setLoading(25, 'Building office...');
|
|
157
|
-
await createOffice(theme);
|
|
158
|
-
|
|
159
|
-
this.ui.setLoading(40, 'Preparing camera...');
|
|
160
|
-
this.camCtrl = new CameraController(canvas, getCamera(), getScene());
|
|
161
|
-
|
|
162
|
-
this.ui.setLoading(50, 'Creating agents...');
|
|
163
|
-
this.agentMgr = new AgentManager(getScene());
|
|
164
|
-
|
|
165
|
-
this.ui.setLoading(60, 'Creating effects...');
|
|
166
|
-
this.particleMgr = new ParticleManager(getScene());
|
|
167
|
-
|
|
168
|
-
this.ui.setLoading(70, 'Preparing data visualization...');
|
|
169
|
-
this.graphViz = new GraphViz(getScene());
|
|
170
|
-
this.hologram = new HologramDisplay(getScene());
|
|
171
|
-
|
|
172
|
-
this.ui.setLoading(80, 'Binding controls...');
|
|
173
|
-
this.ui.init({
|
|
174
|
-
doScan: () => void this.doScan(),
|
|
175
|
-
doPipeline: () => void this.doPipeline(),
|
|
176
|
-
doReset: () => void this.doReset(),
|
|
177
|
-
doRunTests: () => void this.doRunTests(),
|
|
178
|
-
doReports: () => void this.doReports(),
|
|
179
|
-
toggleTheme: () => this.toggleTheme(),
|
|
180
|
-
setView: (view: string) => this.setView(view),
|
|
181
|
-
openFile: (value: string | number) => void this.openFilePreview(value),
|
|
182
|
-
openReport: (value: string) => void this.openReportPreview(value),
|
|
183
|
-
openFilePreview: (value: string | number) => void this.openFilePreview(value),
|
|
184
|
-
openReportPreview: (value: string) => void this.openReportPreview(value),
|
|
185
|
-
});
|
|
186
|
-
this.bindEvents();
|
|
187
|
-
|
|
188
|
-
await this.fetchProject();
|
|
189
|
-
try {
|
|
190
|
-
const summaryResponse = await fetch('/api/studio/summary');
|
|
191
|
-
if (summaryResponse.ok) {
|
|
192
|
-
const summary = (await summaryResponse.json()) as StudioSummary;
|
|
193
|
-
this.state.set({ studioScan: summary });
|
|
194
|
-
this.updateStudioStats(summary);
|
|
195
|
-
}
|
|
196
|
-
} catch {
|
|
197
|
-
// Best effort only.
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
this.ui.setLoading(90, 'Connecting...');
|
|
201
|
-
this.connectWS();
|
|
202
|
-
|
|
203
|
-
this.ui.setLoading(100, 'Ready.');
|
|
204
|
-
window.setTimeout(() => {
|
|
205
|
-
mustElement<HTMLElement>('loading-overlay').classList.add('hidden');
|
|
206
|
-
}, 400);
|
|
207
|
-
|
|
208
|
-
this.rafId = window.requestAnimationFrame(this.renderLoop);
|
|
209
|
-
this.ui.addLog('OpenCroc Studio 3D is ready. Press ? for shortcuts.', 'info', true);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
dispose(): void {
|
|
213
|
-
this.disposed = true;
|
|
214
|
-
window.cancelAnimationFrame(this.rafId);
|
|
215
|
-
window.clearTimeout(this.reconnectTimer);
|
|
216
|
-
window.clearTimeout(this.shortcutTimer);
|
|
217
|
-
|
|
218
|
-
this.listeners.forEach((cleanup) => cleanup());
|
|
219
|
-
this.listeners = [];
|
|
220
|
-
|
|
221
|
-
if (this.ws) {
|
|
222
|
-
this.ws.onclose = null;
|
|
223
|
-
this.ws.onopen = null;
|
|
224
|
-
this.ws.onmessage = null;
|
|
225
|
-
this.ws.close();
|
|
226
|
-
this.ws = null;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
this.camCtrl?.dispose?.();
|
|
230
|
-
this.agentMgr?.dispose?.();
|
|
231
|
-
this.particleMgr?.dispose?.();
|
|
232
|
-
this.graphViz?.dispose?.();
|
|
233
|
-
this.hologram?.dispose?.();
|
|
234
|
-
disposeOffice();
|
|
235
|
-
disposeEngine();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private readonly renderLoop = () => {
|
|
239
|
-
if (this.disposed) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
this.rafId = window.requestAnimationFrame(this.renderLoop);
|
|
244
|
-
const clock = getClock();
|
|
245
|
-
if (!clock) {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const dt = clock.getDelta();
|
|
250
|
-
if (dt > 0.1) {
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
updateEngine(dt);
|
|
255
|
-
this.camCtrl?.update?.(dt);
|
|
256
|
-
this.particleMgr?.update?.(dt);
|
|
257
|
-
this.agentMgr?.update?.(dt);
|
|
258
|
-
this.hologram?.update?.(dt, this.state.get('graph'));
|
|
259
|
-
|
|
260
|
-
const composer = getComposer();
|
|
261
|
-
if (composer) {
|
|
262
|
-
composer.render(dt);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const renderer = getRenderer();
|
|
267
|
-
const scene = getScene();
|
|
268
|
-
const camera = getCamera();
|
|
269
|
-
if (renderer && scene && camera) {
|
|
270
|
-
renderer.render(scene, camera);
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
private bindEvents(): void {
|
|
275
|
-
this.listen(mustElement('btn-scan'), 'click', () => void this.doScan());
|
|
276
|
-
this.listen(mustElement('btn-pipeline'), 'click', () => void this.doPipeline());
|
|
277
|
-
this.listen(mustElement('btn-reset'), 'click', () => void this.doReset());
|
|
278
|
-
this.listen(mustElement('btn-run-tests'), 'click', () => void this.doRunTests());
|
|
279
|
-
this.listen(mustElement('btn-reports'), 'click', () => void this.doReports());
|
|
280
|
-
this.listen(mustElement('view-3d'), 'click', () => this.setView('3d'));
|
|
281
|
-
this.listen(mustElement('view-graph'), 'click', () => this.setView('graph'));
|
|
282
|
-
this.listen(mustElement('theme-toggle'), 'click', () => this.toggleTheme());
|
|
283
|
-
this.listen(mustElement('sidebar-toggle'), 'click', () => {
|
|
284
|
-
mustElement('sidebar').classList.toggle('collapsed');
|
|
285
|
-
});
|
|
286
|
-
this.listen(mustElement('fp-close'), 'click', () => {
|
|
287
|
-
mustElement('file-preview').classList.remove('visible');
|
|
288
|
-
});
|
|
289
|
-
this.listen(mustElement('fp-backdrop'), 'click', () => {
|
|
290
|
-
mustElement('file-preview').classList.remove('visible');
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
const runMode = mustElement<HTMLSelectElement>('run-mode');
|
|
294
|
-
this.listen(runMode, 'change', (event) => {
|
|
295
|
-
const target = event.target as HTMLSelectElement;
|
|
296
|
-
this.state.set({ runMode: target.value });
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
document.querySelectorAll<HTMLElement>('.panel-tabs .tab').forEach((tab) => {
|
|
300
|
-
this.listen(tab, 'click', () => {
|
|
301
|
-
document.querySelectorAll('.panel-tabs .tab').forEach((node) => node.classList.remove('active'));
|
|
302
|
-
tab.classList.add('active');
|
|
303
|
-
const target = tab.dataset.tab;
|
|
304
|
-
mustElement('log-list').classList.toggle('hidden', target !== 'log');
|
|
305
|
-
mustElement('file-list').classList.toggle('hidden', target !== 'files');
|
|
306
|
-
mustElement('results-panel').classList.toggle('hidden', target !== 'results');
|
|
307
|
-
mustElement('reports-panel').classList.toggle('hidden', target !== 'reports');
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
this.listen(window, 'resize', () => resizeEngine());
|
|
312
|
-
this.listen(document, 'keydown', (event) => this.handleShortcuts(event as KeyboardEvent));
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private handleShortcuts(event: KeyboardEvent): void {
|
|
316
|
-
const target = event.target as HTMLElement | null;
|
|
317
|
-
const tag = target?.tagName?.toLowerCase();
|
|
318
|
-
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const key = event.key.toLowerCase();
|
|
323
|
-
if (event.key === 'Escape') {
|
|
324
|
-
mustElement('file-preview').classList.remove('visible');
|
|
325
|
-
mustElement('shortcut-legend').classList.remove('visible');
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (key === '?' || (event.key === '/' && event.shiftKey)) {
|
|
330
|
-
event.preventDefault();
|
|
331
|
-
const legend = mustElement('shortcut-legend');
|
|
332
|
-
legend.classList.add('visible');
|
|
333
|
-
window.clearTimeout(this.shortcutTimer);
|
|
334
|
-
this.shortcutTimer = window.setTimeout(() => legend.classList.remove('visible'), 4000);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (key === '1') {
|
|
339
|
-
event.preventDefault();
|
|
340
|
-
this.setView('3d');
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (key === '2') {
|
|
344
|
-
event.preventDefault();
|
|
345
|
-
this.setView('graph');
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (key === 's' && !event.ctrlKey && !event.metaKey) {
|
|
349
|
-
event.preventDefault();
|
|
350
|
-
void this.doScan();
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (key === 'p' && !event.ctrlKey && !event.metaKey) {
|
|
354
|
-
event.preventDefault();
|
|
355
|
-
void this.doPipeline();
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
if (key === 't' && !event.ctrlKey && !event.metaKey) {
|
|
359
|
-
event.preventDefault();
|
|
360
|
-
void this.doRunTests();
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
if (key === 'r' && !event.ctrlKey && !event.metaKey) {
|
|
364
|
-
event.preventDefault();
|
|
365
|
-
void this.doReports();
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
if (key === 'x' && !event.ctrlKey && !event.metaKey) {
|
|
369
|
-
event.preventDefault();
|
|
370
|
-
void this.doReset();
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
if (key === 'd' && !event.ctrlKey && !event.metaKey) {
|
|
374
|
-
event.preventDefault();
|
|
375
|
-
this.toggleTheme();
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
private listen(
|
|
380
|
-
target: Pick<EventTarget, 'addEventListener' | 'removeEventListener'>,
|
|
381
|
-
eventName: string,
|
|
382
|
-
handler: EventListenerOrEventListenerObject,
|
|
383
|
-
): void {
|
|
384
|
-
target.addEventListener(eventName, handler);
|
|
385
|
-
this.listeners.push(() => target.removeEventListener(eventName, handler));
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
private async fetchProject(): Promise<void> {
|
|
389
|
-
try {
|
|
390
|
-
const response = await fetch('/api/project');
|
|
391
|
-
const project = await response.json();
|
|
392
|
-
this.state.set({
|
|
393
|
-
project,
|
|
394
|
-
graph: project.graph || this.state.get('graph'),
|
|
395
|
-
agents: project.agents || this.state.get('agents'),
|
|
396
|
-
});
|
|
397
|
-
this.updateAll();
|
|
398
|
-
} catch (error) {
|
|
399
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
400
|
-
this.ui.addLog(`Failed to fetch project: ${message}`, 'error');
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
private async doScan(): Promise<void> {
|
|
405
|
-
if (this.state.get('running')) {
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
this.state.set({ running: true });
|
|
410
|
-
this.updateButtons();
|
|
411
|
-
this.ui.addLog('Starting codebase scan...', 'info', true);
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
await fetch('/api/scan', { method: 'POST' });
|
|
415
|
-
|
|
416
|
-
try {
|
|
417
|
-
const cwd = this.state.get('project')?.backendRoot || '.';
|
|
418
|
-
const summaryResponse = await fetch('/api/studio/scan', {
|
|
419
|
-
method: 'POST',
|
|
420
|
-
headers: { 'Content-Type': 'application/json' },
|
|
421
|
-
body: JSON.stringify({ target: cwd }),
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
if (summaryResponse.ok) {
|
|
425
|
-
const summary = (await summaryResponse.json()) as StudioSummary;
|
|
426
|
-
this.state.set({ studioScan: summary });
|
|
427
|
-
this.updateStudioStats(summary);
|
|
428
|
-
this.ui.addLog(
|
|
429
|
-
`Knowledge graph ready: ${summary.stats?.totalNodes || 0} nodes, ${summary.stats?.totalEdges || 0} edges, ${summary.risks || 0} risks.`,
|
|
430
|
-
'success',
|
|
431
|
-
true,
|
|
432
|
-
);
|
|
433
|
-
this.ui.showToast('Knowledge graph updated.', 'success');
|
|
434
|
-
}
|
|
435
|
-
} catch (error) {
|
|
436
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
437
|
-
this.ui.addLog(`Studio scan skipped: ${message}`, 'warning');
|
|
438
|
-
}
|
|
439
|
-
} catch (error) {
|
|
440
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
441
|
-
this.ui.addLog(`Scan error: ${message}`, 'error');
|
|
442
|
-
this.state.set({ running: false });
|
|
443
|
-
this.updateButtons();
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
private async doPipeline(): Promise<void> {
|
|
448
|
-
if (this.state.get('running')) {
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
this.state.set({ running: true });
|
|
452
|
-
this.updateButtons();
|
|
453
|
-
this.ui.addLog('Pipeline started...', 'info', true);
|
|
454
|
-
try {
|
|
455
|
-
await fetch('/api/pipeline', { method: 'POST' });
|
|
456
|
-
} catch (error) {
|
|
457
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
458
|
-
this.ui.addLog(`Pipeline error: ${message}`, 'error');
|
|
459
|
-
this.state.set({ running: false });
|
|
460
|
-
this.updateButtons();
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
private async doReset(): Promise<void> {
|
|
465
|
-
try {
|
|
466
|
-
await fetch('/api/reset', { method: 'POST' });
|
|
467
|
-
} catch (error) {
|
|
468
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
469
|
-
this.ui.addLog(`Reset error: ${message}`, 'error');
|
|
470
|
-
}
|
|
471
|
-
this.state.set({ running: false });
|
|
472
|
-
this.updateButtons();
|
|
473
|
-
this.ui.addLog('Agents reset.', 'info', true);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
private async doRunTests(): Promise<void> {
|
|
477
|
-
if (this.state.get('running')) {
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
this.state.set({ running: true });
|
|
481
|
-
this.updateButtons();
|
|
482
|
-
this.ui.addLog(`Running tests (mode: ${this.state.get('runMode')})...`, 'info', true);
|
|
483
|
-
try {
|
|
484
|
-
const response = await fetch('/api/run-tests', {
|
|
485
|
-
method: 'POST',
|
|
486
|
-
headers: { 'Content-Type': 'application/json' },
|
|
487
|
-
body: JSON.stringify({ mode: this.state.get('runMode') }),
|
|
488
|
-
});
|
|
489
|
-
const payload = await response.json();
|
|
490
|
-
if (payload.error) {
|
|
491
|
-
this.ui.addLog(`Test error: ${payload.error}`, 'error');
|
|
492
|
-
this.state.set({ running: false });
|
|
493
|
-
this.updateButtons();
|
|
494
|
-
}
|
|
495
|
-
} catch (error) {
|
|
496
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
497
|
-
this.ui.addLog(`Test error: ${message}`, 'error');
|
|
498
|
-
this.state.set({ running: false });
|
|
499
|
-
this.updateButtons();
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
private async doReports(): Promise<void> {
|
|
504
|
-
if (this.state.get('running')) {
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
this.state.set({ running: true });
|
|
508
|
-
this.updateButtons();
|
|
509
|
-
this.ui.addLog('Generating reports...', 'info', true);
|
|
510
|
-
|
|
511
|
-
try {
|
|
512
|
-
const perspectives = ['developer', 'architect', 'tester', 'product', 'student', 'executive'];
|
|
513
|
-
const studioReports: Array<{ perspective: string; title: string; content: string }> = [];
|
|
514
|
-
|
|
515
|
-
for (const perspective of perspectives) {
|
|
516
|
-
try {
|
|
517
|
-
const response = await fetch(`/api/studio/report/${perspective}`);
|
|
518
|
-
if (!response.ok) {
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
const report = await response.json();
|
|
522
|
-
if (report.content) {
|
|
523
|
-
studioReports.push({
|
|
524
|
-
perspective,
|
|
525
|
-
title: report.title || perspective,
|
|
526
|
-
content: report.content,
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
} catch {
|
|
530
|
-
// Skip single perspective failures.
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (studioReports.length > 0) {
|
|
535
|
-
this.state.set({ running: false, studioReports });
|
|
536
|
-
this.updateButtons();
|
|
537
|
-
this.renderStudioReports(studioReports);
|
|
538
|
-
this.ui.addLog(`${studioReports.length} perspective reports generated.`, 'success', true);
|
|
539
|
-
this.ui.showToast(`${studioReports.length} reports ready.`, 'success');
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
await fetch('/api/reports/generate', { method: 'POST' });
|
|
544
|
-
} catch (error) {
|
|
545
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
546
|
-
this.ui.addLog(`Report error: ${message}`, 'error');
|
|
547
|
-
this.state.set({ running: false });
|
|
548
|
-
this.updateButtons();
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
private connectWS(): void {
|
|
553
|
-
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
554
|
-
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
|
|
555
|
-
this.ws = ws;
|
|
556
|
-
|
|
557
|
-
ws.onopen = () => {
|
|
558
|
-
if (this.disposed) {
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
this.ui.setConnected(true);
|
|
562
|
-
this.state.set({ ws });
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
ws.onclose = () => {
|
|
566
|
-
if (this.disposed) {
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
this.ui.setConnected(false);
|
|
570
|
-
this.reconnectTimer = window.setTimeout(() => this.connectWS(), 3000);
|
|
571
|
-
};
|
|
572
|
-
|
|
573
|
-
ws.onmessage = (event) => {
|
|
574
|
-
try {
|
|
575
|
-
this.handleWS(JSON.parse(event.data) as Record<string, any>);
|
|
576
|
-
} catch {
|
|
577
|
-
// Ignore malformed messages.
|
|
578
|
-
}
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
private handleWS(message: Record<string, any>): void {
|
|
583
|
-
switch (message.type) {
|
|
584
|
-
case 'agent:update':
|
|
585
|
-
this.state.set({ agents: message.payload });
|
|
586
|
-
this.agentMgr?.sync?.(message.payload);
|
|
587
|
-
this.ui.updateSidebar(null, message.payload);
|
|
588
|
-
break;
|
|
589
|
-
case 'agent:assigned': {
|
|
590
|
-
const transfer = this.agentMgr?.applyAssignmentEvent?.(message.payload);
|
|
591
|
-
if (transfer && this.particleMgr) {
|
|
592
|
-
this.particleMgr.triggerAgentTransfer(transfer.from, transfer.to, transfer.kind);
|
|
593
|
-
}
|
|
594
|
-
if (message.payload?.name) {
|
|
595
|
-
this.ui.addLog(
|
|
596
|
-
`${esc(message.payload.name)} was assigned${message.payload.currentTask ? `: ${esc(message.payload.currentTask)}` : '.'}`,
|
|
597
|
-
'info',
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
break;
|
|
601
|
-
}
|
|
602
|
-
case 'agent:released': {
|
|
603
|
-
const transfer = this.agentMgr?.applyReleaseEvent?.(message.payload);
|
|
604
|
-
if (transfer && this.particleMgr) {
|
|
605
|
-
this.particleMgr.triggerAgentTransfer(transfer.from, transfer.to, transfer.kind);
|
|
606
|
-
}
|
|
607
|
-
if (message.payload?.name) {
|
|
608
|
-
this.ui.addLog(`${esc(message.payload.name)} returned to the pond.`, 'info');
|
|
609
|
-
}
|
|
610
|
-
break;
|
|
611
|
-
}
|
|
612
|
-
case 'graph:update':
|
|
613
|
-
this.state.set({ graph: message.payload });
|
|
614
|
-
this.graphViz?.update?.(message.payload);
|
|
615
|
-
this.ui.updateSidebar(message.payload, null);
|
|
616
|
-
this.ui.updateStats(message.payload);
|
|
617
|
-
break;
|
|
618
|
-
case 'log':
|
|
619
|
-
this.ui.addLog(message.payload.message, message.payload.level);
|
|
620
|
-
break;
|
|
621
|
-
case 'files:generated':
|
|
622
|
-
this.state.set({ generatedFiles: message.payload });
|
|
623
|
-
this.ui.updateFileList(message.payload);
|
|
624
|
-
break;
|
|
625
|
-
case 'pipeline:complete':
|
|
626
|
-
this.state.set({ running: false });
|
|
627
|
-
this.updateButtons();
|
|
628
|
-
if (message.payload.error) {
|
|
629
|
-
this.ui.addLog(`Pipeline failed: ${message.payload.error}`, 'error');
|
|
630
|
-
} else {
|
|
631
|
-
this.ui.addLog('Pipeline complete.', 'success', true);
|
|
632
|
-
this.ui.showToast('Pipeline completed.', 'success');
|
|
633
|
-
this.particleMgr?.triggerCelebration?.();
|
|
634
|
-
}
|
|
635
|
-
void this.fetchProject();
|
|
636
|
-
break;
|
|
637
|
-
case 'test:complete':
|
|
638
|
-
this.state.set({
|
|
639
|
-
running: false,
|
|
640
|
-
testMetrics: message.payload.metrics,
|
|
641
|
-
testQuality: message.payload.quality,
|
|
642
|
-
});
|
|
643
|
-
this.updateButtons();
|
|
644
|
-
this.ui.updateResults(message.payload);
|
|
645
|
-
this.ui.addLog(
|
|
646
|
-
`Tests: ${message.payload.metrics?.passed || 0} passed, ${message.payload.metrics?.failed || 0} failed.`,
|
|
647
|
-
'info',
|
|
648
|
-
true,
|
|
649
|
-
);
|
|
650
|
-
this.ui.showToast(
|
|
651
|
-
`Tests: ${message.payload.metrics?.passed || 0} passed, ${message.payload.metrics?.failed || 0} failed.`,
|
|
652
|
-
message.payload.metrics?.failed ? 'warning' : 'success',
|
|
653
|
-
);
|
|
654
|
-
break;
|
|
655
|
-
case 'reports:generated':
|
|
656
|
-
this.state.set({ running: false, reports: message.payload });
|
|
657
|
-
this.updateButtons();
|
|
658
|
-
this.ui.updateReports(message.payload);
|
|
659
|
-
this.ui.addLog(`${message.payload.length} reports generated.`, 'success', true);
|
|
660
|
-
break;
|
|
661
|
-
case 'scan:complete':
|
|
662
|
-
this.state.set({ running: false });
|
|
663
|
-
this.updateButtons();
|
|
664
|
-
this.ui.addLog('Scan complete.', 'success', true);
|
|
665
|
-
this.ui.showToast('Scan completed.', 'success');
|
|
666
|
-
void this.fetchProject();
|
|
667
|
-
break;
|
|
668
|
-
default:
|
|
669
|
-
break;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
private updateAll(): void {
|
|
674
|
-
const graph = this.state.get('graph') as GraphPayload;
|
|
675
|
-
const agents = this.state.get('agents') as AgentPayload;
|
|
676
|
-
this.ui.updateSidebar(graph, agents);
|
|
677
|
-
this.ui.updateStats(graph);
|
|
678
|
-
if (agents) {
|
|
679
|
-
this.agentMgr?.sync?.(agents);
|
|
680
|
-
}
|
|
681
|
-
if (graph) {
|
|
682
|
-
this.graphViz?.update?.(graph);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
private updateButtons(): void {
|
|
687
|
-
const running = Boolean(this.state.get('running'));
|
|
688
|
-
['btn-scan', 'btn-pipeline', 'btn-reset', 'btn-run-tests', 'btn-reports'].forEach((id) => {
|
|
689
|
-
mustElement<HTMLButtonElement>(id).disabled = running;
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
private async openFilePreview(indexOrPath: string | number): Promise<void> {
|
|
694
|
-
try {
|
|
695
|
-
const response = await fetch(`/api/files/${indexOrPath}`);
|
|
696
|
-
const payload = await response.json();
|
|
697
|
-
mustElement('fp-title').textContent = payload.filePath || 'File';
|
|
698
|
-
mustElement('fp-code').textContent = payload.content || '';
|
|
699
|
-
mustElement('file-preview').classList.add('visible');
|
|
700
|
-
} catch {
|
|
701
|
-
this.ui.showToast('Failed to load file.', 'error');
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
private async openReportPreview(format: string): Promise<void> {
|
|
706
|
-
try {
|
|
707
|
-
const response = await fetch(`/api/reports/${format}`);
|
|
708
|
-
const content = await response.text();
|
|
709
|
-
if (format === 'html') {
|
|
710
|
-
const nextWindow = window.open('', '_blank');
|
|
711
|
-
if (nextWindow) {
|
|
712
|
-
nextWindow.document.write(content);
|
|
713
|
-
nextWindow.document.close();
|
|
714
|
-
}
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
mustElement('fp-title').textContent = `Report: ${format}`;
|
|
719
|
-
mustElement('fp-code').textContent = content;
|
|
720
|
-
mustElement('file-preview').classList.add('visible');
|
|
721
|
-
} catch {
|
|
722
|
-
this.ui.showToast('Failed to load report.', 'error');
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
private toggleTheme(): void {
|
|
727
|
-
const current = this.state.get('theme') as string;
|
|
728
|
-
const next = current === 'dark' ? 'light' : 'dark';
|
|
729
|
-
this.state.set({ theme: next });
|
|
730
|
-
document.documentElement.setAttribute('data-theme', next);
|
|
731
|
-
localStorage.setItem('opencroc-theme', next);
|
|
732
|
-
mustElement<HTMLElement>('theme-icon-dark').style.display = next === 'dark' ? '' : 'none';
|
|
733
|
-
mustElement<HTMLElement>('theme-icon-light').style.display = next === 'light' ? '' : 'none';
|
|
734
|
-
updateEngineTheme(next);
|
|
735
|
-
updateOfficeLighting(next);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
private setView(view: string): void {
|
|
739
|
-
this.state.set({ currentView: view });
|
|
740
|
-
mustElement('view-3d').classList.toggle('active', view === '3d');
|
|
741
|
-
mustElement('view-graph').classList.toggle('active', view === 'graph');
|
|
742
|
-
if (view === '3d') {
|
|
743
|
-
this.camCtrl?.flyTo?.('office');
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
navigate('/studio');
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
private updateStudioStats(summary: StudioSummary): void {
|
|
750
|
-
if (!summary?.stats) {
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
const stats = summary.stats;
|
|
755
|
-
mustElement('s-mod').textContent = String(stats.moduleCount || 0);
|
|
756
|
-
mustElement('s-mdl').textContent = String(stats.classCount || 0);
|
|
757
|
-
mustElement('s-api').textContent = String(stats.functionCount || 0);
|
|
758
|
-
mustElement('s-files').textContent = String(stats.fileCount || 0);
|
|
759
|
-
mustElement('s-nodes').textContent = String(stats.totalNodes || 0);
|
|
760
|
-
const risks = mustElement('s-risks');
|
|
761
|
-
risks.textContent = String(summary.risks || 0);
|
|
762
|
-
risks.style.color = summary.risks ? 'var(--orange)' : '';
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
private renderStudioReports(reports: Array<{ perspective: string; title: string; content: string }>): void {
|
|
766
|
-
const panel = mustElement('reports-panel');
|
|
767
|
-
const icons: Record<string, string> = {
|
|
768
|
-
developer: 'DEV',
|
|
769
|
-
architect: 'ARC',
|
|
770
|
-
tester: 'TST',
|
|
771
|
-
product: 'PM',
|
|
772
|
-
student: 'EDU',
|
|
773
|
-
executive: 'BIZ',
|
|
774
|
-
};
|
|
775
|
-
|
|
776
|
-
panel.innerHTML = reports
|
|
777
|
-
.map(
|
|
778
|
-
(report) => `
|
|
779
|
-
<div class="report-card" style="padding:12px;margin:8px 0;background:var(--bg-card);border-radius:var(--radius-md);border:1px solid var(--border);cursor:pointer">
|
|
780
|
-
<div style="display:flex;align-items:center;gap:8px;font-weight:600;color:var(--text);font-size:13px">
|
|
781
|
-
<span>${icons[report.perspective] || 'RPT'}</span>
|
|
782
|
-
<span>${esc(report.title || report.perspective)}</span>
|
|
783
|
-
<span style="margin-left:auto;font-size:10px;color:var(--text-subtle)">click to expand</span>
|
|
784
|
-
</div>
|
|
785
|
-
<div class="report-body hidden" style="margin-top:10px;font-size:12px;color:var(--text-dim);white-space:pre-wrap;max-height:400px;overflow:auto;line-height:1.6">${esc(report.content)}</div>
|
|
786
|
-
</div>`,
|
|
787
|
-
)
|
|
788
|
-
.join('');
|
|
789
|
-
|
|
790
|
-
panel.querySelectorAll<HTMLElement>('.report-card').forEach((card) => {
|
|
791
|
-
this.listen(card, 'click', () => {
|
|
792
|
-
card.querySelector('.report-body')?.classList.toggle('hidden');
|
|
793
|
-
});
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
document.querySelectorAll('.panel-tabs .tab').forEach((node) => node.classList.remove('active'));
|
|
797
|
-
document.querySelector<HTMLElement>('.tab[data-tab="reports"]')?.classList.add('active');
|
|
798
|
-
mustElement('log-list').classList.add('hidden');
|
|
799
|
-
mustElement('file-list').classList.add('hidden');
|
|
800
|
-
mustElement('results-panel').classList.add('hidden');
|
|
801
|
-
panel.classList.remove('hidden');
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
export async function mountOfficeRuntime(): Promise<() => void> {
|
|
806
|
-
const runtime = new OfficeRuntime();
|
|
807
|
-
await runtime.mount();
|
|
808
|
-
return () => runtime.dispose();
|
|
809
|
-
}
|