opencroc 1.6.8 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/index-v2-pixel.html +1571 -0
- package/dist/web/index.html +531 -912
- package/dist/web/js/agents.js +465 -0
- package/dist/web/js/camera.js +125 -0
- package/dist/web/js/dataviz.js +288 -0
- package/dist/web/js/effects.js +345 -0
- package/dist/web/js/engine.js +489 -0
- package/dist/web/js/office.js +816 -0
- package/dist/web/js/state.js +37 -0
- package/dist/web/js/ui.js +384 -0
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
OpenCroc Studio 3D — State Manager
|
|
3
|
+
Simple reactive state management
|
|
4
|
+
═══════════════════════════════════════════════════════════════════════════════ */
|
|
5
|
+
|
|
6
|
+
export class StateManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
this._state = {};
|
|
9
|
+
this._listeners = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Set one or more state properties */
|
|
13
|
+
set(partial) {
|
|
14
|
+
Object.assign(this._state, partial);
|
|
15
|
+
for (const key of Object.keys(partial)) {
|
|
16
|
+
const cbs = this._listeners.get(key);
|
|
17
|
+
if (cbs) cbs.forEach(cb => cb(partial[key], this._state));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Get a single state property */
|
|
22
|
+
get(key) {
|
|
23
|
+
return this._state[key];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Get full state snapshot */
|
|
27
|
+
getAll() {
|
|
28
|
+
return this._state;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Subscribe to changes on a specific key */
|
|
32
|
+
on(key, cb) {
|
|
33
|
+
if (!this._listeners.has(key)) this._listeners.set(key, new Set());
|
|
34
|
+
this._listeners.get(key).add(cb);
|
|
35
|
+
return () => this._listeners.get(key)?.delete(cb);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
OpenCroc Studio 3D — UI Manager
|
|
3
|
+
Glass-morphism overlay panels, logs, toasts, sidebars, desk cards
|
|
4
|
+
═══════════════════════════════════════════════════════════════════════════════ */
|
|
5
|
+
|
|
6
|
+
const STATUS_LABEL = {
|
|
7
|
+
idle: '空闲', scanning: '扫描中', navigating: '导航中',
|
|
8
|
+
interacting: '交互中', asserting: '断言中', reporting: '报告中',
|
|
9
|
+
thinking: '思考中', complete: '完成', error: '错误',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const STATUS_DOT_CLASS = {
|
|
13
|
+
idle: 'dot-idle', scanning: 'dot-active', navigating: 'dot-active',
|
|
14
|
+
interacting: 'dot-active', asserting: 'dot-active', reporting: 'dot-active',
|
|
15
|
+
thinking: 'dot-active', complete: 'dot-ok', error: 'dot-err',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/* ═══════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
UIManager — all DOM operations go through here
|
|
20
|
+
═══════════════════════════════════════════════════════════════════════════════ */
|
|
21
|
+
export class UIManager {
|
|
22
|
+
|
|
23
|
+
constructor(state, options) {
|
|
24
|
+
this._state = state;
|
|
25
|
+
this._options = options || {};
|
|
26
|
+
this._callbacks = {};
|
|
27
|
+
this._logCount = 0;
|
|
28
|
+
this._esc = options?.esc || (s => String(s));
|
|
29
|
+
this._ROLE_ICONS = options?.ROLE_ICONS || {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ── init: store callbacks (event binding done in index.html) ────────── */
|
|
33
|
+
init(callbacks) {
|
|
34
|
+
this._callbacks = callbacks || {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ── setLoading ──────────────────────────────────────────────────────── */
|
|
38
|
+
setLoading(pct, text) {
|
|
39
|
+
const fill = document.getElementById('loading-fill');
|
|
40
|
+
const txt = document.getElementById('loading-text');
|
|
41
|
+
if (fill) fill.style.width = pct + '%';
|
|
42
|
+
if (txt) txt.textContent = text || 'Loading…';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ── addLog ──────────────────────────────────────────────────────────── */
|
|
46
|
+
addLog(msg, level = 'info', typewriter = false) {
|
|
47
|
+
const logBody = document.getElementById('log-list');
|
|
48
|
+
if (!logBody) return;
|
|
49
|
+
|
|
50
|
+
this._logCount++;
|
|
51
|
+
const ts = new Date();
|
|
52
|
+
const timeStr = ts.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
53
|
+
|
|
54
|
+
const row = document.createElement('div');
|
|
55
|
+
row.className = `log-row log-${level}`;
|
|
56
|
+
|
|
57
|
+
const timePart = `<span class="log-time">${timeStr}</span>`;
|
|
58
|
+
const levelPart = `<span class="log-level ${level}">[${level.toUpperCase()}]</span>`;
|
|
59
|
+
const msgSpan = document.createElement('span');
|
|
60
|
+
msgSpan.className = 'log-msg';
|
|
61
|
+
|
|
62
|
+
row.innerHTML = timePart + levelPart;
|
|
63
|
+
row.appendChild(msgSpan);
|
|
64
|
+
logBody.appendChild(row);
|
|
65
|
+
|
|
66
|
+
// Auto-scroll
|
|
67
|
+
logBody.scrollTop = logBody.scrollHeight;
|
|
68
|
+
|
|
69
|
+
if (typewriter && msg.length > 0) {
|
|
70
|
+
this._typewrite(msgSpan, msg, 0);
|
|
71
|
+
} else {
|
|
72
|
+
msgSpan.textContent = msg;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Trim old logs
|
|
76
|
+
while (logBody.children.length > 300) {
|
|
77
|
+
logBody.removeChild(logBody.firstChild);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_typewrite(el, text, i) {
|
|
82
|
+
if (i < text.length) {
|
|
83
|
+
el.textContent += text[i];
|
|
84
|
+
setTimeout(() => this._typewrite(el, text, i + 1), 12);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ── showToast ───────────────────────────────────────────────────────── */
|
|
89
|
+
showToast(msg, type = 'info') {
|
|
90
|
+
const container = document.getElementById('toast-container');
|
|
91
|
+
if (!container) return;
|
|
92
|
+
|
|
93
|
+
const toast = document.createElement('div');
|
|
94
|
+
toast.className = `toast toast-${type}`;
|
|
95
|
+
toast.textContent = msg;
|
|
96
|
+
container.appendChild(toast);
|
|
97
|
+
|
|
98
|
+
setTimeout(() => toast.classList.add('show'), 10);
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
toast.classList.remove('show');
|
|
101
|
+
setTimeout(() => toast.remove(), 400);
|
|
102
|
+
}, 3500);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── updateSidebar: modules + agent list ─────────────────────────────── */
|
|
106
|
+
updateSidebar(graph, agents) {
|
|
107
|
+
// Module list
|
|
108
|
+
const modList = document.getElementById('mod-list');
|
|
109
|
+
if (modList && graph && graph.nodes) {
|
|
110
|
+
const modules = {};
|
|
111
|
+
graph.nodes.forEach(n => {
|
|
112
|
+
const mod = n.module || 'default';
|
|
113
|
+
if (!modules[mod]) modules[mod] = 0;
|
|
114
|
+
modules[mod]++;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
modList.innerHTML = '';
|
|
118
|
+
Object.entries(modules).forEach(([mod, count]) => {
|
|
119
|
+
const item = document.createElement('div');
|
|
120
|
+
item.className = 'mod-item';
|
|
121
|
+
item.innerHTML = `
|
|
122
|
+
<span class="mod-dot"></span>
|
|
123
|
+
<span class="mod-name">${this._esc(mod)}</span>
|
|
124
|
+
<span class="mod-count">${count}</span>
|
|
125
|
+
`;
|
|
126
|
+
modList.appendChild(item);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Agent sidebar
|
|
131
|
+
const agentSidebar = document.getElementById('agent-sidebar');
|
|
132
|
+
if (agentSidebar && agents) {
|
|
133
|
+
agentSidebar.innerHTML = '';
|
|
134
|
+
const entries = Array.isArray(agents) ? agents : Object.entries(agents).map(([name, info]) => ({name, ...info}));
|
|
135
|
+
entries.forEach(agent => {
|
|
136
|
+
const name = agent.name || agent.role || '';
|
|
137
|
+
const status = agent.status || 'idle';
|
|
138
|
+
const item = document.createElement('div');
|
|
139
|
+
item.className = 'agent-sidebar-item';
|
|
140
|
+
item.innerHTML = `
|
|
141
|
+
<span class="agent-icon">${this._ROLE_ICONS[name] || '🤖'}</span>
|
|
142
|
+
<span class="agent-name">${this._esc(name)}</span>
|
|
143
|
+
<span class="agent-status-dot ${STATUS_DOT_CLASS[status] || 'dot-idle'}"></span>
|
|
144
|
+
<span class="agent-status-text">${STATUS_LABEL[status] || status}</span>
|
|
145
|
+
`;
|
|
146
|
+
agentSidebar.appendChild(item);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ── updateDeskCards: bottom bar ──────────────────────────────────────── */
|
|
152
|
+
updateDeskCards(agents) {
|
|
153
|
+
const row = document.getElementById('desk-row');
|
|
154
|
+
if (!row || !agents) return;
|
|
155
|
+
|
|
156
|
+
const entries = Array.isArray(agents) ? agents : Object.entries(agents).map(([name, info]) => ({name, ...info}));
|
|
157
|
+
|
|
158
|
+
// Only rebuild if count changes
|
|
159
|
+
const existing = row.querySelectorAll('.desk-card');
|
|
160
|
+
if (existing.length !== entries.length) {
|
|
161
|
+
row.innerHTML = '';
|
|
162
|
+
entries.forEach(agent => {
|
|
163
|
+
const name = agent.name || agent.role || '';
|
|
164
|
+
const card = document.createElement('div');
|
|
165
|
+
card.className = 'desk-card';
|
|
166
|
+
card.id = `desk-${name}`;
|
|
167
|
+
card.innerHTML = this._renderDeskCard(name, agent);
|
|
168
|
+
row.appendChild(card);
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
entries.forEach(agent => {
|
|
172
|
+
const name = agent.name || agent.role || '';
|
|
173
|
+
const card = document.getElementById(`desk-${name}`);
|
|
174
|
+
if (card) card.innerHTML = this._renderDeskCard(name, agent);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_renderDeskCard(name, info) {
|
|
180
|
+
const status = info.status || 'idle';
|
|
181
|
+
const progress = info.progress || 0;
|
|
182
|
+
return `
|
|
183
|
+
<div class="dc-icon">${this._ROLE_ICONS[name] || '🤖'}</div>
|
|
184
|
+
<div class="dc-name">${this._esc(name)}</div>
|
|
185
|
+
<div class="dc-status ${STATUS_DOT_CLASS[status] || ''}">${STATUS_LABEL[status] || status}</div>
|
|
186
|
+
<div class="dc-bar"><div class="dc-fill" style="width:${progress}%"></div></div>
|
|
187
|
+
${info.currentTask ? `<div class="dc-task">${this._esc(info.currentTask)}</div>` : ''}
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ── updateStats: header stat counters ───────────────────────────────── */
|
|
192
|
+
updateStats(graph) {
|
|
193
|
+
if (!graph) return;
|
|
194
|
+
const set = (id, val) => {
|
|
195
|
+
const el = document.getElementById(id);
|
|
196
|
+
if (el) el.textContent = val;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const nodes = graph.nodes || [];
|
|
200
|
+
const modules = new Set(nodes.map(n => n.module || 'default'));
|
|
201
|
+
const models = nodes.filter(n => n.type === 'model');
|
|
202
|
+
const apis = nodes.filter(n => n.type === 'api' || n.type === 'endpoint');
|
|
203
|
+
|
|
204
|
+
set('s-mod', modules.size);
|
|
205
|
+
set('s-mdl', models.length);
|
|
206
|
+
set('s-api', apis.length);
|
|
207
|
+
set('s-files', nodes.length);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* ── updateResults: test results panel ───────────────────────────────── */
|
|
211
|
+
updateResults(data) {
|
|
212
|
+
const panel = document.getElementById('results-panel');
|
|
213
|
+
if (!panel) return;
|
|
214
|
+
|
|
215
|
+
if (!data) {
|
|
216
|
+
panel.innerHTML = '<div class="empty-hint">暂无测试结果</div>';
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const total = data.total || 0;
|
|
221
|
+
const passed = data.passed || 0;
|
|
222
|
+
const failed = data.failed || 0;
|
|
223
|
+
const skipped = data.skipped || 0;
|
|
224
|
+
const rate = total ? Math.round((passed / total) * 100) : 0;
|
|
225
|
+
|
|
226
|
+
panel.innerHTML = `
|
|
227
|
+
<div class="result-summary">
|
|
228
|
+
<div class="result-metric">
|
|
229
|
+
<span class="metric-val">${total}</span>
|
|
230
|
+
<span class="metric-label">总计</span>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="result-metric ok">
|
|
233
|
+
<span class="metric-val">${passed}</span>
|
|
234
|
+
<span class="metric-label">通过</span>
|
|
235
|
+
</div>
|
|
236
|
+
<div class="result-metric err">
|
|
237
|
+
<span class="metric-val">${failed}</span>
|
|
238
|
+
<span class="metric-label">失败</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="result-metric warn">
|
|
241
|
+
<span class="metric-val">${skipped}</span>
|
|
242
|
+
<span class="metric-label">跳过</span>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="result-metric accent">
|
|
245
|
+
<span class="metric-val">${rate}%</span>
|
|
246
|
+
<span class="metric-label">通过率</span>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
${data.suites ? this._renderSuites(data.suites) : ''}
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_renderSuites(suites) {
|
|
254
|
+
if (!suites || !suites.length) return '';
|
|
255
|
+
return '<div class="result-suites">' + suites.map(s => `
|
|
256
|
+
<div class="suite-row">
|
|
257
|
+
<span class="suite-icon">${s.passed ? '✅' : '❌'}</span>
|
|
258
|
+
<span class="suite-name">${this._esc(s.name)}</span>
|
|
259
|
+
<span class="suite-dur">${s.duration || '-'}ms</span>
|
|
260
|
+
</div>
|
|
261
|
+
`).join('') + '</div>';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* ── updateFileList: scanned files ───────────────────────────────────── */
|
|
265
|
+
updateFileList(files) {
|
|
266
|
+
const panel = document.getElementById('file-list');
|
|
267
|
+
if (!panel) return;
|
|
268
|
+
|
|
269
|
+
if (!files || !files.length) {
|
|
270
|
+
panel.innerHTML = '<div class="empty-hint">暂无扫描文件</div>';
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
panel.innerHTML = '<div class="file-list">' + files.map(f => {
|
|
275
|
+
const name = typeof f === 'string' ? f : (f.path || f.name || '');
|
|
276
|
+
const ext = name.split('.').pop().toLowerCase();
|
|
277
|
+
const icon = this._fileIcon(ext);
|
|
278
|
+
return `
|
|
279
|
+
<div class="file-item" data-path="${this._escAttr(name)}">
|
|
280
|
+
<span class="file-icon">${icon}</span>
|
|
281
|
+
<span class="file-name">${this._esc(name)}</span>
|
|
282
|
+
</div>
|
|
283
|
+
`;
|
|
284
|
+
}).join('') + '</div>';
|
|
285
|
+
|
|
286
|
+
// Click handlers
|
|
287
|
+
panel.querySelectorAll('.file-item').forEach(el => {
|
|
288
|
+
el.addEventListener('click', () => {
|
|
289
|
+
const path = el.dataset.path;
|
|
290
|
+
if (path) this._fire('openFile', path);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
_fileIcon(ext) {
|
|
296
|
+
const map = {
|
|
297
|
+
ts: '📘', tsx: '📘', js: '📒', jsx: '📒',
|
|
298
|
+
vue: '💚', css: '🎨', html: '🌐', json: '📋',
|
|
299
|
+
md: '📝', py: '🐍', go: '🐹', rs: '🦀',
|
|
300
|
+
java: '☕', sql: '🗃️', yaml: '⚙️', yml: '⚙️',
|
|
301
|
+
};
|
|
302
|
+
return map[ext] || '📄';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* ── updateReports ───────────────────────────────────────────────────── */
|
|
306
|
+
updateReports(reports) {
|
|
307
|
+
const panel = document.getElementById('reports-panel');
|
|
308
|
+
if (!panel) return;
|
|
309
|
+
|
|
310
|
+
if (!reports || !reports.length) {
|
|
311
|
+
panel.innerHTML = '<div class="empty-hint">暂无报告</div>';
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
panel.innerHTML = '<div class="report-list">' + reports.map(r => {
|
|
316
|
+
const name = r.name || r.title || 'Report';
|
|
317
|
+
return `
|
|
318
|
+
<div class="report-item" data-id="${this._escAttr(r.id || '')}">
|
|
319
|
+
<span class="report-icon">📊</span>
|
|
320
|
+
<span class="report-name">${this._esc(name)}</span>
|
|
321
|
+
<span class="report-date">${r.date || ''}</span>
|
|
322
|
+
</div>
|
|
323
|
+
`;
|
|
324
|
+
}).join('') + '</div>';
|
|
325
|
+
|
|
326
|
+
panel.querySelectorAll('.report-item').forEach(el => {
|
|
327
|
+
el.addEventListener('click', () => {
|
|
328
|
+
const id = el.dataset.id;
|
|
329
|
+
if (id) this._fire('openReport', id);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* ── openFilePreview ─────────────────────────────────────────────────── */
|
|
335
|
+
openFilePreview(path, content) {
|
|
336
|
+
const title = document.getElementById('fp-title');
|
|
337
|
+
const body = document.getElementById('fp-code');
|
|
338
|
+
const modal = document.getElementById('file-preview');
|
|
339
|
+
if (title) title.textContent = path;
|
|
340
|
+
if (body) body.textContent = content || '(empty)';
|
|
341
|
+
if (modal) modal.classList.add('visible');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ── openReportPreview ───────────────────────────────────────────────── */
|
|
345
|
+
openReportPreview(reportData) {
|
|
346
|
+
const title = document.getElementById('fp-title');
|
|
347
|
+
const body = document.getElementById('fp-code');
|
|
348
|
+
const modal = document.getElementById('file-preview');
|
|
349
|
+
if (title) title.textContent = reportData.name || 'Report';
|
|
350
|
+
if (body) {
|
|
351
|
+
body.textContent = typeof reportData.content === 'string'
|
|
352
|
+
? reportData.content : JSON.stringify(reportData, null, 2);
|
|
353
|
+
}
|
|
354
|
+
if (modal) modal.classList.add('visible');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ── Connection status ───────────────────────────────────────────────── */
|
|
358
|
+
setConnected(connected) {
|
|
359
|
+
const dot = document.getElementById('conn-dot');
|
|
360
|
+
if (dot) dot.classList.toggle('connected', connected);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* ── Utility ─────────────────────────────────────────────────────────── */
|
|
364
|
+
_bind(id, event, fn) {
|
|
365
|
+
const el = document.getElementById(id);
|
|
366
|
+
if (el) el.addEventListener(event, fn);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_fire(name, ...args) {
|
|
370
|
+
if (this._callbacks[name]) this._callbacks[name](...args);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_esc(s) {
|
|
374
|
+
if (!s) return '';
|
|
375
|
+
const d = document.createElement('div');
|
|
376
|
+
d.textContent = s;
|
|
377
|
+
return d.innerHTML;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_escAttr(s) {
|
|
381
|
+
if (!s) return '';
|
|
382
|
+
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
383
|
+
}
|
|
384
|
+
}
|