opencroc 1.8.0 → 1.8.2

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 (71) hide show
  1. package/dist/cli/index.js +1107 -49
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/index.d.ts +128 -1
  4. package/dist/index.js +548 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/web/dist/assets/main-Ccg3eDNK.js +1 -0
  7. package/dist/web/dist/assets/office-runtime-B3iNctxE.css +1 -0
  8. package/dist/web/dist/assets/office-runtime-BsCh82Pj.js +183 -0
  9. package/dist/web/dist/assets/pixel-page-3BYGm7dH.js +470 -0
  10. package/dist/web/dist/assets/react-vendor-C8RhVn0h.js +49 -0
  11. package/dist/web/dist/assets/studio-page-BInoyoV2.css +1 -0
  12. package/dist/web/dist/assets/studio-page-o3SCvE_v.js +351 -0
  13. package/dist/web/dist/assets/three-addons-BdrPp04O.js +470 -0
  14. package/dist/web/dist/assets/three-core-CsxM1PCY.js +4057 -0
  15. package/dist/web/dist/index.html +15 -0
  16. package/dist/web/index.html +11 -572
  17. package/dist/web/public/botreview/char_0.png +0 -0
  18. package/dist/web/public/botreview/char_1.png +0 -0
  19. package/dist/web/public/botreview/char_2.png +0 -0
  20. package/dist/web/public/botreview/coffee-machine.gif +0 -0
  21. package/dist/web/public/botreview/server.gif +0 -0
  22. package/dist/web/public/botreview/walls.png +0 -0
  23. package/dist/web/public/star/desk-v3.webp +0 -0
  24. package/dist/web/public/star/office_bg_small.webp +0 -0
  25. package/dist/web/public/star/star-idle-v5.png +0 -0
  26. package/dist/web/public/star/star-working-spritesheet-grid.webp +0 -0
  27. package/dist/web/src/app/AppLayout.tsx +34 -0
  28. package/dist/web/src/app/AppRouter.tsx +46 -0
  29. package/dist/web/src/app/bootstrap.tsx +22 -0
  30. package/dist/web/src/app/routes.tsx +52 -0
  31. package/dist/web/src/features/office/runtime/index.ts +1 -0
  32. package/dist/web/src/features/office/runtime/mount.ts +809 -0
  33. package/dist/web/src/features/pixel/runtime/index.ts +1 -0
  34. package/dist/web/src/features/pixel/runtime/mount.ts +728 -0
  35. package/dist/web/src/features/studio/runtime/index.ts +1 -0
  36. package/dist/web/src/features/studio/runtime/mount.ts +664 -0
  37. package/dist/web/src/features/three/engine/index.ts +1 -0
  38. package/dist/web/src/main.tsx +7 -0
  39. package/dist/web/src/pages/office/index.ts +1 -0
  40. package/dist/web/src/pages/office/page.tsx +283 -0
  41. package/dist/web/src/pages/pixel/index.ts +1 -0
  42. package/dist/web/src/pages/pixel/page.tsx +564 -0
  43. package/dist/web/src/pages/studio/index.ts +1 -0
  44. package/dist/web/src/pages/studio/page.tsx +446 -0
  45. package/dist/web/{js/agents.js → src/runtime/agents.ts} +304 -31
  46. package/dist/web/{js/camera.js → src/runtime/camera.ts} +12 -5
  47. package/dist/web/{js/dataviz.js → src/runtime/dataviz.ts} +38 -14
  48. package/dist/web/{js/effects.js → src/runtime/effects.ts} +139 -2
  49. package/dist/web/{js/engine.js → src/runtime/engine.ts} +45 -6
  50. package/dist/web/{js/office.js → src/runtime/office.ts} +136 -20
  51. package/dist/web/{js/ui.js → src/runtime/ui.ts} +11 -7
  52. package/dist/web/src/shared/assets.ts +4 -0
  53. package/dist/web/src/shared/navigation.ts +47 -0
  54. package/dist/web/src/styles/app-layout.css +19 -0
  55. package/dist/web/src/styles/office.css +268 -0
  56. package/dist/web/tsconfig.json +28 -0
  57. package/dist/web/vite.config.ts +93 -0
  58. package/package.json +11 -2
  59. package/dist/web/index-studio.html +0 -804
  60. package/dist/web/index-v2-pixel.html +0 -1571
  61. /package/dist/web/{assets → dist}/botreview/char_0.png +0 -0
  62. /package/dist/web/{assets → dist}/botreview/char_1.png +0 -0
  63. /package/dist/web/{assets → dist}/botreview/char_2.png +0 -0
  64. /package/dist/web/{assets → dist}/botreview/coffee-machine.gif +0 -0
  65. /package/dist/web/{assets → dist}/botreview/server.gif +0 -0
  66. /package/dist/web/{assets → dist}/botreview/walls.png +0 -0
  67. /package/dist/web/{assets → dist}/star/desk-v3.webp +0 -0
  68. /package/dist/web/{assets → dist}/star/office_bg_small.webp +0 -0
  69. /package/dist/web/{assets → dist}/star/star-idle-v5.png +0 -0
  70. /package/dist/web/{assets → dist}/star/star-working-spritesheet-grid.webp +0 -0
  71. /package/dist/web/{js/state.js → src/runtime/state.ts} +0 -0
@@ -0,0 +1 @@
1
+ export { mountStudioRuntime } from './mount';
@@ -0,0 +1,664 @@
1
+ type StudioGraph = {
2
+ nodes?: Array<Record<string, any>>;
3
+ edges?: Array<Record<string, any>>;
4
+ };
5
+
6
+ import { navigate } from '@shared/navigation';
7
+
8
+ type StudioSummary = {
9
+ stats?: Record<string, number>;
10
+ risks?: number;
11
+ };
12
+
13
+ type StudioSnapshot = {
14
+ id: string;
15
+ name?: string;
16
+ createdAt?: string;
17
+ pinned?: boolean;
18
+ tags?: string[];
19
+ source?: string;
20
+ };
21
+
22
+ function must<T extends HTMLElement>(id: string): T {
23
+ const element = document.getElementById(id);
24
+ if (!element) {
25
+ throw new Error(`Missing element #${id}`);
26
+ }
27
+ return element as T;
28
+ }
29
+
30
+ function esc(value: unknown): string {
31
+ return String(value ?? '')
32
+ .replace(/&/g, '&amp;')
33
+ .replace(/</g, '&lt;')
34
+ .replace(/>/g, '&gt;')
35
+ .replace(/"/g, '&quot;');
36
+ }
37
+
38
+ async function fetchJson(url: string, init?: RequestInit) {
39
+ const response = await fetch(url, init);
40
+ if (!response.ok) {
41
+ throw new Error(`${response.status} ${response.statusText}`);
42
+ }
43
+ return response.json();
44
+ }
45
+
46
+ export async function mountStudioRuntime(): Promise<() => void> {
47
+ const listeners: Array<() => void> = [];
48
+ const graphPositions = new Map<string, { x: number; y: number; node: Record<string, any> }>();
49
+ let graphData: StudioGraph = { nodes: [], edges: [] };
50
+ let summaryData: StudioSummary | null = null;
51
+ let riskData: Array<Record<string, any>> = [];
52
+ let snapshotData: StudioSnapshot[] = [];
53
+ let snapshotFilter = '';
54
+ let snapshotQuery = '';
55
+ let selectedNode: Record<string, any> | null = null;
56
+ let activeTypeFilter = '';
57
+ let ws: WebSocket | null = null;
58
+ let reconnectTimer = 0;
59
+ let currentReport: any = null;
60
+ let currentReportMode = 'markdown';
61
+ let currentPerspective = '';
62
+ const reportCache = new Map<string, any>();
63
+
64
+ const svg = must<SVGSVGElement>('graph-canvas');
65
+ const panel = must('panel');
66
+ const tooltip = must('tooltip');
67
+
68
+ function listen(
69
+ target: Pick<EventTarget, 'addEventListener' | 'removeEventListener'>,
70
+ eventName: string,
71
+ handler: EventListenerOrEventListenerObject,
72
+ ) {
73
+ target.addEventListener(eventName, handler);
74
+ listeners.push(() => target.removeEventListener(eventName, handler));
75
+ }
76
+
77
+ function setTheme(theme: string) {
78
+ document.documentElement.setAttribute('data-theme', theme);
79
+ localStorage.setItem('opencroc-studio-theme', theme);
80
+ }
81
+
82
+ function showLoading(message: string, detail = '') {
83
+ must('loading').classList.remove('hidden');
84
+ must('loading-text').textContent = message;
85
+ must('loading-detail').textContent = detail;
86
+ }
87
+
88
+ function hideLoading() {
89
+ must('loading').classList.add('hidden');
90
+ }
91
+
92
+ function showGraphView() {
93
+ must('report-view').classList.add('hidden');
94
+ svg.style.display = '';
95
+ must('graph-empty').classList.toggle('hidden', Boolean(graphData.nodes?.length));
96
+ document.querySelectorAll<HTMLElement>('.studio-tab').forEach((tab) => {
97
+ tab.classList.toggle('active', tab.dataset.view === 'graph');
98
+ });
99
+ }
100
+
101
+ function showReportView() {
102
+ must('report-view').classList.remove('hidden');
103
+ svg.style.display = 'none';
104
+ must('graph-empty').classList.add('hidden');
105
+ }
106
+
107
+ function updateSummary() {
108
+ const stats = summaryData?.stats || {};
109
+ must('stat-modules').textContent = String(stats.moduleCount || 0);
110
+ must('stat-apis').textContent = String(stats.functionCount || 0);
111
+ must('stat-models').textContent = String(stats.classCount || 0);
112
+ must('stat-risks').textContent = String(summaryData?.risks || 0);
113
+ }
114
+
115
+ function renderNodeTypes() {
116
+ const container = must('node-type-list');
117
+ const counts = new Map<string, number>();
118
+ for (const node of graphData.nodes || []) {
119
+ const type = String(node.type || 'unknown');
120
+ counts.set(type, (counts.get(type) || 0) + 1);
121
+ }
122
+
123
+ container.innerHTML = Array.from(counts.entries())
124
+ .sort((a, b) => b[1] - a[1])
125
+ .map(([type, count]) => {
126
+ const active = activeTypeFilter === type ? ' active' : '';
127
+ return `<button class="studio-list-item${active}" data-type="${esc(type)}" type="button"><strong>${esc(type)}</strong><div style="margin-top:6px;color:var(--studio-dim);font-size:12px">${count} nodes</div></button>`;
128
+ })
129
+ .join('');
130
+ }
131
+
132
+ function renderRisks() {
133
+ const container = must('risk-list');
134
+ if (!riskData.length) {
135
+ container.innerHTML = '<div class="studio-list-item">No risks detected yet.</div>';
136
+ return;
137
+ }
138
+
139
+ container.innerHTML = riskData
140
+ .map((risk, index) => {
141
+ const severity = String(risk.severity || 'info');
142
+ const color = severity === 'critical' || severity === 'high'
143
+ ? 'var(--studio-red)'
144
+ : severity === 'medium'
145
+ ? 'var(--studio-orange)'
146
+ : 'var(--studio-blue)';
147
+ return `<button class="studio-list-item" data-risk-index="${index}" type="button"><strong style="color:${color}">${esc(risk.title || risk.message || `Risk ${index + 1}`)}</strong><div style="margin-top:6px;color:var(--studio-dim);font-size:12px">${esc(risk.filePath || risk.module || severity)}</div></button>`;
148
+ })
149
+ .join('');
150
+ }
151
+
152
+ function filteredSnapshots() {
153
+ return snapshotData.filter((snapshot) => {
154
+ const matchesTag = !snapshotFilter || snapshot.tags?.includes(snapshotFilter);
155
+ const haystack = `${snapshot.name || ''} ${snapshot.source || ''}`.toLowerCase();
156
+ const matchesText = !snapshotQuery || haystack.includes(snapshotQuery.toLowerCase());
157
+ return matchesTag && matchesText;
158
+ });
159
+ }
160
+
161
+ function renderSnapshotTags() {
162
+ const container = must('snapshot-tag-filters');
163
+ const tags = Array.from(new Set(snapshotData.flatMap((snapshot) => snapshot.tags || []))).sort();
164
+ const chips = ['all', ...tags];
165
+ container.innerHTML = chips
166
+ .map((tag) => {
167
+ const active = (!snapshotFilter && tag === 'all') || snapshotFilter === tag;
168
+ return `<button class="studio-chip${active ? ' active' : ''}" data-snapshot-filter="${esc(tag)}" type="button">${tag === 'all' ? 'All' : esc(tag)}</button>`;
169
+ })
170
+ .join('');
171
+ }
172
+
173
+ function renderSnapshots() {
174
+ const container = must('snapshot-list');
175
+ const snapshots = filteredSnapshots();
176
+ if (!snapshots.length) {
177
+ container.innerHTML = '<div class="snapshot-item">No snapshots found.</div>';
178
+ return;
179
+ }
180
+
181
+ container.innerHTML = snapshots
182
+ .map((snapshot) => {
183
+ const tags = snapshot.tags || [];
184
+ return `
185
+ <div class="snapshot-item">
186
+ <strong>${esc(snapshot.name || snapshot.id)}</strong>
187
+ <div style="margin-top:6px;color:var(--studio-dim);font-size:12px">${esc(snapshot.createdAt || '')}</div>
188
+ <div style="margin-top:4px;color:var(--studio-dim);font-size:12px">${esc(snapshot.source || '')}</div>
189
+ <div class="snapshot-tags">${tags.map((tag) => `<span class="snapshot-tag">${esc(tag)}</span>`).join('')}</div>
190
+ <div class="snapshot-actions">
191
+ <button class="snapshot-action" data-snapshot-action="restore" data-snapshot-id="${esc(snapshot.id)}" type="button">Restore</button>
192
+ <button class="snapshot-action" data-snapshot-action="pin" data-snapshot-id="${esc(snapshot.id)}" data-pinned="${snapshot.pinned ? '1' : '0'}" type="button">${snapshot.pinned ? 'Unpin' : 'Pin'}</button>
193
+ <button class="snapshot-action" data-snapshot-action="rename" data-snapshot-id="${esc(snapshot.id)}" type="button">Rename</button>
194
+ <button class="snapshot-action" data-snapshot-action="delete" data-snapshot-id="${esc(snapshot.id)}" type="button">Delete</button>
195
+ </div>
196
+ </div>
197
+ `;
198
+ })
199
+ .join('');
200
+ }
201
+
202
+ function renderPanel(title: string, html: string) {
203
+ must('panel-title').textContent = title;
204
+ must('panel-body').innerHTML = html;
205
+ panel.style.display = '';
206
+ }
207
+
208
+ async function showNodeDetail(node: Record<string, any>) {
209
+ selectedNode = node;
210
+ let payload = node;
211
+ try {
212
+ payload = await fetchJson(`/api/studio/node/${encodeURIComponent(String(node.id))}`);
213
+ } catch {
214
+ // Use fallback payload.
215
+ }
216
+
217
+ renderPanel(
218
+ String(payload.label || payload.id || 'Node Detail'),
219
+ `
220
+ <h3>${esc(payload.label || payload.id || 'Node')}</h3>
221
+ <p><strong>Type:</strong> ${esc(payload.type || 'unknown')}</p>
222
+ <p><strong>Module:</strong> ${esc(payload.module || '-')}</p>
223
+ <p><strong>Status:</strong> ${esc(payload.status || '-')}</p>
224
+ <div class="studio-report-block"><pre>${esc(JSON.stringify(payload, null, 2))}</pre></div>
225
+ `,
226
+ );
227
+ }
228
+
229
+ function showRiskDetail(index: number) {
230
+ const risk = riskData[index];
231
+ if (!risk) {
232
+ return;
233
+ }
234
+
235
+ renderPanel(
236
+ String(risk.title || `Risk ${index + 1}`),
237
+ `
238
+ <p><strong>Severity:</strong> ${esc(risk.severity || 'unknown')}</p>
239
+ <p><strong>Location:</strong> ${esc(risk.filePath || risk.module || '-')}</p>
240
+ <div class="studio-report-block"><pre>${esc(JSON.stringify(risk, null, 2))}</pre></div>
241
+ `,
242
+ );
243
+ }
244
+
245
+ function showTooltip(event: MouseEvent, node: Record<string, any>) {
246
+ tooltip.innerHTML = `<strong>${esc(node.label || node.id)}</strong><div style="margin-top:4px">${esc(node.type || 'unknown')}</div>`;
247
+ tooltip.classList.add('visible');
248
+ tooltip.style.left = `${event.clientX + 12}px`;
249
+ tooltip.style.top = `${event.clientY + 12}px`;
250
+ }
251
+
252
+ function hideTooltip() {
253
+ tooltip.classList.remove('visible');
254
+ }
255
+
256
+ function layoutGraph() {
257
+ graphPositions.clear();
258
+ const bounds = svg.getBoundingClientRect();
259
+ const width = bounds.width || 960;
260
+ const height = bounds.height || 720;
261
+ const nodes = (graphData.nodes || []).filter((node) => !activeTypeFilter || node.type === activeTypeFilter);
262
+ const modules = nodes.filter((node) => node.type === 'module');
263
+ const others = nodes.filter((node) => node.type !== 'module');
264
+
265
+ modules.forEach((node, index) => {
266
+ const angle = (index / Math.max(modules.length, 1)) * Math.PI * 2;
267
+ const x = width / 2 + Math.cos(angle) * Math.min(width, height) * 0.28;
268
+ const y = height / 2 + Math.sin(angle) * Math.min(width, height) * 0.28;
269
+ graphPositions.set(String(node.id), { x, y, node });
270
+ });
271
+
272
+ others.forEach((node, index) => {
273
+ const parent = modules.find((moduleNode) => moduleNode.label === node.module || moduleNode.id === node.module);
274
+ const parentPos = parent ? graphPositions.get(String(parent.id)) : undefined;
275
+ const baseAngle = (index / Math.max(others.length, 1)) * Math.PI * 2;
276
+ const radius = parentPos ? 84 + (index % 5) * 16 : Math.min(width, height) * 0.16;
277
+ const x = (parentPos?.x || width / 2) + Math.cos(baseAngle) * radius;
278
+ const y = (parentPos?.y || height / 2) + Math.sin(baseAngle) * radius;
279
+ graphPositions.set(String(node.id), { x, y, node });
280
+ });
281
+ }
282
+
283
+ function renderGraph() {
284
+ layoutGraph();
285
+ const edges = (graphData.edges || []).filter((edge) => graphPositions.has(String(edge.source)) && graphPositions.has(String(edge.target)));
286
+
287
+ svg.setAttribute('viewBox', `0 0 ${svg.clientWidth || 960} ${svg.clientHeight || 720}`);
288
+ svg.innerHTML = `
289
+ <defs>
290
+ <linearGradient id="studio-edge" x1="0" y1="0" x2="1" y2="1">
291
+ <stop offset="0%" stop-color="rgba(96,165,250,0.45)" />
292
+ <stop offset="100%" stop-color="rgba(52,211,153,0.22)" />
293
+ </linearGradient>
294
+ </defs>
295
+ ${edges
296
+ .map((edge) => {
297
+ const source = graphPositions.get(String(edge.source));
298
+ const target = graphPositions.get(String(edge.target));
299
+ return `<line x1="${source?.x}" y1="${source?.y}" x2="${target?.x}" y2="${target?.y}" stroke="url(#studio-edge)" stroke-width="1.4" opacity="0.8" />`;
300
+ })
301
+ .join('')}
302
+ ${Array.from(graphPositions.entries())
303
+ .map(([id, position]) => {
304
+ const node = position.node;
305
+ const color = node.type === 'module'
306
+ ? 'var(--studio-purple)'
307
+ : node.type === 'api'
308
+ ? 'var(--studio-orange)'
309
+ : node.type === 'model'
310
+ ? 'var(--studio-accent)'
311
+ : 'var(--studio-blue)';
312
+ const radius = node.type === 'module' ? 16 : 10;
313
+ const stroke = selectedNode?.id === node.id ? 'var(--studio-accent)' : 'rgba(255,255,255,0.16)';
314
+ return `
315
+ <g class="studio-node" data-node-id="${esc(id)}" style="cursor:pointer">
316
+ <circle cx="${position.x}" cy="${position.y}" r="${radius}" fill="${color}" stroke="${stroke}" stroke-width="2" opacity="0.92" />
317
+ <text x="${position.x}" y="${position.y + radius + 16}" text-anchor="middle" font-size="${node.type === 'module' ? 12 : 10}" fill="var(--studio-text)">${esc(node.label || node.id)}</text>
318
+ </g>
319
+ `;
320
+ })
321
+ .join('')}
322
+ `;
323
+
324
+ must('graph-empty').classList.toggle('hidden', graphPositions.size > 0);
325
+ must('welcome').classList.toggle('hidden', graphPositions.size > 0);
326
+
327
+ svg.querySelectorAll<SVGGElement>('.studio-node').forEach((group) => {
328
+ const id = group.dataset.nodeId || '';
329
+ const position = graphPositions.get(id);
330
+ if (!position) {
331
+ return;
332
+ }
333
+ listen(group, 'click', () => void showNodeDetail(position.node));
334
+ listen(group, 'mouseenter', (event) => showTooltip(event as MouseEvent, position.node));
335
+ listen(group, 'mouseleave', hideTooltip);
336
+ });
337
+ }
338
+
339
+ async function loadGraph() {
340
+ graphData = await fetchJson('/api/studio/graph');
341
+ renderNodeTypes();
342
+ renderGraph();
343
+ }
344
+
345
+ async function loadRisks() {
346
+ riskData = await fetchJson('/api/studio/risks');
347
+ renderRisks();
348
+ }
349
+
350
+ async function loadSummary() {
351
+ summaryData = await fetchJson('/api/studio/summary');
352
+ updateSummary();
353
+ }
354
+
355
+ async function loadSnapshots() {
356
+ snapshotData = await fetchJson('/api/studio/snapshots');
357
+ renderSnapshotTags();
358
+ renderSnapshots();
359
+ }
360
+
361
+ async function refreshAll() {
362
+ await Promise.all([loadGraph(), loadRisks(), loadSummary(), loadSnapshots()]);
363
+ }
364
+
365
+ async function doScan(target: string) {
366
+ showLoading('Scanning project...', target);
367
+ try {
368
+ await fetchJson('/api/studio/scan', {
369
+ method: 'POST',
370
+ headers: { 'Content-Type': 'application/json' },
371
+ body: JSON.stringify({ target }),
372
+ });
373
+ await refreshAll();
374
+ } finally {
375
+ hideLoading();
376
+ }
377
+ }
378
+
379
+ async function fetchPerspectiveReport(perspective: string) {
380
+ const cached = reportCache.get(perspective);
381
+ if (cached) {
382
+ return cached;
383
+ }
384
+ const report = await fetchJson(`/api/studio/report/${perspective}`);
385
+ reportCache.set(perspective, report);
386
+ return report;
387
+ }
388
+
389
+ async function ensureMermaidReady() {
390
+ const w = window as any;
391
+ if (w.mermaid) {
392
+ return w.mermaid;
393
+ }
394
+ if (w.__mermaidPromise) {
395
+ return w.__mermaidPromise;
396
+ }
397
+ w.__mermaidPromise = new Promise((resolve, reject) => {
398
+ const script = document.createElement('script');
399
+ script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
400
+ script.onload = () => resolve((window as any).mermaid);
401
+ script.onerror = () => reject(new Error('Failed to load Mermaid'));
402
+ document.head.appendChild(script);
403
+ });
404
+ return w.__mermaidPromise;
405
+ }
406
+
407
+ async function hydrateMermaid() {
408
+ if (!must('report-content').querySelector('.mermaid')) {
409
+ return;
410
+ }
411
+ const mermaid = await ensureMermaidReady();
412
+ mermaid.initialize({ startOnLoad: false, securityLevel: 'loose', theme: 'default' });
413
+ await mermaid.run({ querySelector: '.mermaid' });
414
+ }
415
+
416
+ function markdownToHtml(markdown: string) {
417
+ return esc(markdown || '')
418
+ .replace(/^### (.+)$/gm, '<h3>$1</h3>')
419
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
420
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
421
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
422
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
423
+ .replace(/^- (.+)$/gm, '<li>$1</li>')
424
+ .replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>')
425
+ .replace(/\n/g, '<br />');
426
+ }
427
+
428
+ async function renderCurrentReport() {
429
+ if (!currentReport) {
430
+ return;
431
+ }
432
+
433
+ const content = must('report-content');
434
+ if (currentReportMode === 'raw') {
435
+ content.innerHTML = `<div class="studio-report-block"><pre>${esc(JSON.stringify(currentReport, null, 2))}</pre></div>`;
436
+ return;
437
+ }
438
+
439
+ if (currentReportMode === 'mermaid') {
440
+ const blocks = (currentReport.sections || [])
441
+ .filter((section: any) => section.visualization?.type === 'mermaid' && section.visualization?.data)
442
+ .map((section: any) => `<h3>${esc(section.heading)}</h3><div class="studio-report-block"><pre class="mermaid">${esc(section.visualization.data)}</pre></div>`)
443
+ .join('');
444
+ content.innerHTML = blocks || '<p>No Mermaid sections available for this perspective.</p>';
445
+ await hydrateMermaid();
446
+ return;
447
+ }
448
+
449
+ content.innerHTML = `
450
+ <h1>${esc(currentReport.title || currentPerspective)}</h1>
451
+ <p>${esc(currentReport.summary || '')}</p>
452
+ ${(currentReport.sections || [])
453
+ .map((section: any) => {
454
+ const visualization = section.visualization?.data
455
+ ? `<div class="studio-report-block"><pre>${esc(section.visualization.data)}</pre></div>`
456
+ : '';
457
+ return `<section style="margin-top:24px"><h2>${esc(section.heading)}</h2><div>${markdownToHtml(section.content || '')}</div>${visualization}</section>`;
458
+ })
459
+ .join('')}
460
+ `;
461
+ }
462
+
463
+ function connectWs() {
464
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
465
+ ws = new WebSocket(`${protocol}//${location.host}/ws`);
466
+
467
+ ws.onmessage = (event) => {
468
+ try {
469
+ const message = JSON.parse(event.data);
470
+ if (message.type === 'graph:update') {
471
+ graphData = message.payload;
472
+ renderNodeTypes();
473
+ renderGraph();
474
+ }
475
+ if (message.type === 'agent:update') {
476
+ const ids: Record<string, string> = {
477
+ 'parser-croc': 'agent-parser',
478
+ 'analyzer-croc': 'agent-analyzer',
479
+ 'planner-croc': 'agent-planner',
480
+ 'tester-croc': 'agent-tester',
481
+ 'healer-croc': 'agent-healer',
482
+ 'reporter-croc': 'agent-reporter',
483
+ };
484
+ for (const agent of message.payload || []) {
485
+ const dot = ids[agent.id] ? document.getElementById(ids[agent.id]) : null;
486
+ if (dot) {
487
+ dot.className = `agent-status-dot ${agent.status || 'idle'}`;
488
+ }
489
+ }
490
+ }
491
+ if (message.type === 'scan:progress') {
492
+ showLoading(message.payload?.phase || 'Scanning...', message.payload?.detail || '');
493
+ }
494
+ } catch {
495
+ // Ignore malformed message.
496
+ }
497
+ };
498
+
499
+ ws.onclose = () => {
500
+ reconnectTimer = window.setTimeout(connectWs, 3000);
501
+ };
502
+ }
503
+
504
+ async function handleSnapshotAction(action: string, snapshotId: string, pinned: boolean) {
505
+ if (action === 'restore') {
506
+ await fetchJson(`/api/studio/snapshots/${encodeURIComponent(snapshotId)}/load`, { method: 'POST' });
507
+ }
508
+ if (action === 'pin') {
509
+ await fetchJson(`/api/studio/snapshots/${encodeURIComponent(snapshotId)}/pin`, {
510
+ method: 'POST',
511
+ headers: { 'Content-Type': 'application/json' },
512
+ body: JSON.stringify({ pinned: !pinned }),
513
+ });
514
+ }
515
+ if (action === 'rename') {
516
+ const nextName = window.prompt('Snapshot name');
517
+ if (!nextName) {
518
+ return;
519
+ }
520
+ await fetchJson(`/api/studio/snapshots/${encodeURIComponent(snapshotId)}/rename`, {
521
+ method: 'POST',
522
+ headers: { 'Content-Type': 'application/json' },
523
+ body: JSON.stringify({ name: nextName }),
524
+ });
525
+ }
526
+ if (action === 'delete') {
527
+ await fetchJson(`/api/studio/snapshots/${encodeURIComponent(snapshotId)}/delete`, { method: 'POST' });
528
+ }
529
+ await loadSnapshots();
530
+ }
531
+
532
+ listen(must('scan-btn'), 'click', () => {
533
+ const value = must<HTMLInputElement>('scan-input').value.trim();
534
+ if (value) {
535
+ void doScan(value);
536
+ }
537
+ });
538
+ listen(must('welcome-scan-btn'), 'click', () => {
539
+ const value = must<HTMLInputElement>('welcome-input').value.trim();
540
+ if (value) {
541
+ void doScan(value);
542
+ }
543
+ });
544
+ listen(must('theme-btn'), 'click', () => {
545
+ const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
546
+ setTheme(next);
547
+ });
548
+ listen(must('panel-btn'), 'click', () => {
549
+ panel.style.display = panel.style.display === 'none' ? '' : 'none';
550
+ });
551
+ listen(must('panel-close-btn'), 'click', () => {
552
+ panel.style.display = 'none';
553
+ });
554
+ listen(must('focus-btn'), 'click', () => {
555
+ if (selectedNode) {
556
+ void showNodeDetail(selectedNode);
557
+ }
558
+ });
559
+ listen(must('snapshot-search'), 'input', (event) => {
560
+ snapshotQuery = (event.target as HTMLInputElement).value;
561
+ renderSnapshots();
562
+ });
563
+ listen(must('snapshot-tag-filters'), 'click', (event) => {
564
+ const target = (event.target as HTMLElement).closest<HTMLElement>('[data-snapshot-filter]');
565
+ if (!target) {
566
+ return;
567
+ }
568
+ const tag = target.dataset.snapshotFilter || '';
569
+ snapshotFilter = tag === 'all' ? '' : tag;
570
+ renderSnapshotTags();
571
+ renderSnapshots();
572
+ });
573
+ listen(must('snapshot-list'), 'click', (event) => {
574
+ const target = (event.target as HTMLElement).closest<HTMLElement>('[data-snapshot-action]');
575
+ if (!target) {
576
+ return;
577
+ }
578
+ void handleSnapshotAction(
579
+ target.dataset.snapshotAction || '',
580
+ target.dataset.snapshotId || '',
581
+ target.dataset.pinned === '1',
582
+ );
583
+ });
584
+ listen(must('node-type-list'), 'click', (event) => {
585
+ const target = (event.target as HTMLElement).closest<HTMLElement>('[data-type]');
586
+ if (!target) {
587
+ return;
588
+ }
589
+ const nextType = target.dataset.type || '';
590
+ activeTypeFilter = activeTypeFilter === nextType ? '' : nextType;
591
+ renderNodeTypes();
592
+ renderGraph();
593
+ });
594
+ listen(must('risk-list'), 'click', (event) => {
595
+ const target = (event.target as HTMLElement).closest<HTMLElement>('[data-risk-index]');
596
+ if (!target) {
597
+ return;
598
+ }
599
+ showRiskDetail(Number(target.dataset.riskIndex));
600
+ });
601
+ document.querySelectorAll<HTMLElement>('.studio-tab').forEach((tab) => {
602
+ listen(tab, 'click', async () => {
603
+ if (tab.dataset.view === 'office') {
604
+ navigate('/');
605
+ return;
606
+ }
607
+ if (tab.dataset.view === 'graph') {
608
+ showGraphView();
609
+ return;
610
+ }
611
+ const perspective = tab.dataset.perspective;
612
+ if (!perspective) {
613
+ return;
614
+ }
615
+ currentPerspective = perspective;
616
+ currentReport = await fetchPerspectiveReport(perspective);
617
+ document.querySelectorAll<HTMLElement>('.studio-tab').forEach((item) => {
618
+ item.classList.toggle('active', item === tab);
619
+ });
620
+ showReportView();
621
+ await renderCurrentReport();
622
+ });
623
+ });
624
+ document.querySelectorAll<HTMLElement>('[data-mode]').forEach((button) => {
625
+ listen(button, 'click', async () => {
626
+ currentReportMode = button.dataset.mode || 'markdown';
627
+ document.querySelectorAll<HTMLElement>('[data-mode]').forEach((item) => {
628
+ item.classList.toggle('active', item === button);
629
+ });
630
+ await renderCurrentReport();
631
+ });
632
+ });
633
+ listen(must('copy-report-btn'), 'click', async () => {
634
+ const content = must('report-content').innerText;
635
+ await navigator.clipboard.writeText(content);
636
+ });
637
+ listen(window, 'resize', renderGraph);
638
+ listen(document, 'keydown', (event) => {
639
+ const keyboardEvent = event as KeyboardEvent;
640
+ if (keyboardEvent.key === 'Escape') {
641
+ hideTooltip();
642
+ panel.style.display = 'none';
643
+ }
644
+ });
645
+
646
+ setTheme(localStorage.getItem('opencroc-studio-theme') || 'dark');
647
+ panel.style.display = '';
648
+ try {
649
+ await refreshAll();
650
+ } catch {
651
+ must('welcome').classList.remove('hidden');
652
+ must('graph-empty').classList.remove('hidden');
653
+ }
654
+ connectWs();
655
+
656
+ return () => {
657
+ listeners.forEach((cleanup) => cleanup());
658
+ listeners.length = 0;
659
+ if (ws) {
660
+ ws.close();
661
+ }
662
+ window.clearTimeout(reconnectTimer);
663
+ };
664
+ }
@@ -0,0 +1 @@
1
+ export * from '../../../runtime/engine';
@@ -0,0 +1,7 @@
1
+ import { bootstrapApp } from './app/bootstrap';
2
+ import AppRouter from './app/AppRouter';
3
+
4
+ bootstrapApp({
5
+ app: <AppRouter />,
6
+ missingMessage: 'Missing #root container for OpenCroc Studio.',
7
+ });
@@ -0,0 +1 @@
1
+ export { default } from './page';