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.
@@ -1,954 +1,573 @@
1
1
  <!DOCTYPE html>
2
- <html lang="zh-CN">
2
+ <html lang="zh-CN" data-theme="light">
3
3
  <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>OpenCroc Studio ๐ŸŠ</title>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
6
+ <title>OpenCroc Studio ยท 3D</title>
7
7
  <style>
8
- :root {
9
- --bg-dark: #070b14;
10
- --bg-panel: #101829;
11
- --bg-panel-2: #0f1524;
12
- --bg-card: #19263f;
13
- --bg-soft: #233354;
14
- --accent: #53d2a6;
15
- --accent-dim: #2b8f73;
16
- --red: #f05f78;
17
- --orange: #f6ad55;
18
- --blue: #59a5ff;
19
- --purple: #b58cff;
20
- --text: #e7edf8;
21
- --text-dim: #8b9cc0;
22
- --text-subtle: #6a7da5;
23
- --pixel-border: 1px solid rgba(131, 156, 211, 0.22);
24
- --shadow-lg: 0 14px 30px rgba(0, 0, 0, 0.38);
25
- --shadow-md: 0 8px 20px rgba(0, 0, 0, 0.28);
26
- }
27
- * { margin:0; padding:0; box-sizing:border-box; }
28
- body {
29
- background:
30
- radial-gradient(circle at 20% 10%, rgba(83, 210, 166, 0.12), transparent 35%),
31
- radial-gradient(circle at 80% 20%, rgba(89, 165, 255, 0.12), transparent 35%),
32
- linear-gradient(180deg, #05070d 0%, var(--bg-dark) 55%, #060810 100%);
33
- color:var(--text);
34
- font-family:'Courier New','Consolas',monospace;
35
- overflow:hidden;
36
- height:100vh;
37
- }
8
+ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
9
+ OpenCroc Studio 3D โ€” Glass-morphism Design System
10
+ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
11
+
12
+ /* โ”€โ”€โ”€ CSS Reset โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
13
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
14
+ html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans SC","Helvetica Neue",Arial,sans-serif}
15
+ body{background:#000;color:var(--text)}
16
+ ::-webkit-scrollbar{width:6px;height:6px}
17
+ ::-webkit-scrollbar-track{background:transparent}
18
+ ::-webkit-scrollbar-thumb{background:var(--text-subtle);border-radius:3px}
19
+ ::-webkit-scrollbar-thumb:hover{background:var(--text-dim)}
20
+ ::selection{background:var(--accent);color:#000}
21
+
22
+ /* โ”€โ”€โ”€ Design Tokens: Dark (Default) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
23
+ :root{
24
+ --bg-void:#050510;--bg-deep:#0a0f1e;
25
+ --bg-panel:rgba(12,18,36,0.75);--bg-panel-solid:#0c1224;
26
+ --bg-card:rgba(18,26,50,0.6);--bg-hover:rgba(30,42,72,0.5);
27
+ --bg-glass:rgba(15,22,42,0.55);--bg-glass-strong:rgba(15,22,42,0.8);
28
+ --bg-frosted:rgba(20,28,52,0.45);--bg-input:rgba(15,22,44,0.6);
29
+ --accent:#34d399;--accent-rgb:52,211,153;--accent-dim:#059669;
30
+ --accent-glow:rgba(52,211,153,0.4);--accent-bg:rgba(52,211,153,0.1);
31
+ --red:#f87171;--red-rgb:248,113,113;--red-glow:rgba(248,113,113,0.35);--red-bg:rgba(248,113,113,0.1);
32
+ --orange:#fbbf24;--orange-rgb:251,191,36;--orange-glow:rgba(251,191,36,0.35);--orange-bg:rgba(251,191,36,0.1);
33
+ --blue:#60a5fa;--blue-rgb:96,165,250;--blue-glow:rgba(96,165,250,0.35);--blue-bg:rgba(96,165,250,0.1);
34
+ --purple:#a78bfa;--purple-rgb:167,139,250;--purple-glow:rgba(167,139,250,0.35);--purple-bg:rgba(167,139,250,0.1);
35
+ --cyan:#22d3ee;--cyan-rgb:34,211,238;--cyan-glow:rgba(34,211,238,0.35);--cyan-bg:rgba(34,211,238,0.1);
36
+ --pink:#f472b6;--pink-rgb:244,114,182;
37
+ --text:#f1f5f9;--text-dim:#94a3b8;--text-subtle:#4a5568;--text-muted:#2d3748;
38
+ --border:rgba(148,163,184,0.12);--border-accent:rgba(52,211,153,0.25);--border-glass:rgba(255,255,255,0.06);
39
+ --shadow-xl:0 25px 50px -12px rgba(0,0,0,0.6);--shadow-lg:0 20px 40px rgba(0,0,0,0.4);
40
+ --shadow-md:0 8px 24px rgba(0,0,0,0.3);--shadow-sm:0 2px 8px rgba(0,0,0,0.2);
41
+ --shadow-glow:0 0 30px rgba(52,211,153,0.15);--shadow-inset:inset 0 1px 0 rgba(255,255,255,0.05);
42
+ --blur-lg:blur(24px);--blur-md:blur(16px);--blur-sm:blur(8px);--blur-xs:blur(4px);
43
+ --radius-xl:20px;--radius-lg:14px;--radius-md:10px;--radius-sm:6px;--radius-xs:4px;--radius-full:9999px;
44
+ --ease-out-expo:cubic-bezier(0.16,1,0.3,1);--ease-spring:cubic-bezier(0.175,0.885,0.32,1.275);
45
+ --transition-fast:0.15s var(--ease-out-expo);--transition:0.25s var(--ease-out-expo);
46
+ --transition-slow:0.4s var(--ease-out-expo);--transition-spring:0.5s var(--ease-spring);
47
+ --z-canvas:0;--z-overlay:10;--z-panel:20;--z-header:30;--z-modal:40;--z-tooltip:50;--z-notify:60;
48
+ }
38
49
 
39
- .app { display:grid; grid-template-rows:68px 1fr 238px; grid-template-columns:260px 1fr 340px; height:100vh; gap:8px; padding:8px; }
40
-
41
- .header {
42
- grid-column:1/-1;
43
- background:linear-gradient(180deg, rgba(16, 24, 41, 0.95), rgba(15, 21, 36, 0.9));
44
- border:var(--pixel-border);
45
- border-radius:12px;
46
- display:flex;
47
- align-items:center;
48
- padding:0 16px;
49
- gap:12px;
50
- box-shadow:var(--shadow-lg);
51
- backdrop-filter: blur(8px);
52
- }
53
- .header .logo {
54
- font-size:26px;
55
- width:40px;
56
- height:40px;
57
- border-radius:10px;
58
- display:grid;
59
- place-items:center;
60
- background:linear-gradient(135deg, rgba(83,210,166,.2), rgba(89,165,255,.2));
61
- border:1px solid rgba(131,156,211,.26);
62
- }
63
- .header .title-wrap { display:flex; flex-direction:column; gap:2px; }
64
- .header h1 { font-size:15px; color:var(--accent); letter-spacing:.4px; }
65
- .header .subtitle { font-size:10px; color:var(--text-subtle); }
66
- .header .actions { display:flex; gap:6px; }
67
- .view-switch {
68
- display:flex;
69
- background:rgba(13,20,34,.78);
70
- border:1px solid rgba(131,156,211,.26);
71
- border-radius:10px;
72
- padding:2px;
73
- margin-right:4px;
74
- }
75
- .view-switch button {
76
- background:transparent;
77
- color:var(--text-dim);
78
- border:none;
79
- font-family:inherit;
80
- font-size:10px;
81
- padding:5px 10px;
82
- border-radius:8px;
83
- cursor:pointer;
84
- }
85
- .view-switch button.active {
86
- background:rgba(83,210,166,.18);
87
- color:var(--accent);
88
- border:1px solid rgba(83,210,166,.35);
89
- }
90
- .btn {
91
- background:linear-gradient(180deg, rgba(43,143,115,.9), rgba(31,109,88,.9));
92
- color:#fff;
93
- border:1px solid rgba(83,210,166,.35);
94
- padding:6px 12px;
95
- font-family:inherit;
96
- font-size:11px;
97
- border-radius:8px;
98
- cursor:pointer;
99
- transition:all .2s;
100
- box-shadow:var(--shadow-md);
101
- }
102
- .btn:hover { background:linear-gradient(180deg, rgba(83,210,166,.95), rgba(43,143,115,.95)); transform:translateY(-1px); }
103
- .btn:active { transform:translateY(0); }
104
- .btn:disabled { opacity:.4; cursor:not-allowed; transform:none; }
105
- .btn.danger { background:linear-gradient(180deg, rgba(155,49,72,.9), rgba(127,34,55,.9)); border-color:rgba(240,95,120,.38); }
106
- .btn.danger:hover { background:linear-gradient(180deg, rgba(240,95,120,.95), rgba(163,57,80,.95)); }
107
- .mode-select {
108
- background:var(--bg-card);
109
- color:var(--text);
110
- border:1px solid rgba(131,156,211,.26);
111
- border-radius:8px;
112
- font-family:inherit;
113
- font-size:10px;
114
- padding:6px 8px;
115
- min-width:88px;
116
- }
117
- .header .stats { margin-left:auto; display:flex; gap:8px; font-size:10px; color:var(--text-dim); }
118
- .header .stats > div {
119
- background:rgba(25, 38, 63, 0.62);
120
- border:1px solid rgba(131,156,211,.2);
121
- border-radius:10px;
122
- padding:6px 8px;
123
- min-width:72px;
124
- text-align:center;
125
- }
126
- .header .stats span { display:block; color:var(--accent); font-weight:bold; font-size:12px; margin-top:2px; }
127
- .conn-dot {
128
- width:10px;
129
- height:10px;
130
- border-radius:50%;
131
- background:var(--red);
132
- box-shadow:0 0 0 3px rgba(240,95,120,.2);
133
- transition:background .3s, box-shadow .3s;
134
- }
135
- .conn-dot.on { background:var(--accent); box-shadow:0 0 0 3px rgba(83,210,166,.2); }
136
-
137
- .sidebar {
138
- background:linear-gradient(180deg, rgba(16,24,41,.96), rgba(15,21,36,.92));
139
- border:var(--pixel-border);
140
- border-radius:12px;
141
- overflow-y:auto;
142
- padding:10px;
143
- box-shadow:var(--shadow-md);
144
- }
145
- .sidebar h3 { font-size:10px; text-transform:uppercase; color:var(--text-subtle); padding:10px 6px 6px; letter-spacing:1.1px; }
146
- .mod-item {
147
- padding:7px 10px;
148
- border-radius:8px;
149
- font-size:11px;
150
- cursor:pointer;
151
- display:flex;
152
- align-items:center;
153
- gap:6px;
154
- transition:background .15s, transform .15s;
155
- margin-bottom:3px;
156
- border:1px solid transparent;
157
- }
158
- .mod-item:hover {
159
- background:rgba(35,51,84,.58);
160
- border-color:rgba(83,210,166,.2);
161
- transform:translateX(2px);
162
- }
163
- .dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }
164
- .dot.idle { background:var(--text-dim); }
165
- .dot.testing,.dot.working { background:var(--orange); animation:blink .7s infinite; }
166
- .dot.thinking { background:var(--blue); animation:blink 1s infinite; }
167
- .dot.passed,.dot.done { background:var(--accent); }
168
- .dot.failed,.dot.error { background:var(--red); }
169
- @keyframes blink { 50%{opacity:.3} }
170
-
171
- .main {
172
- position:relative;
173
- overflow:hidden;
174
- background:linear-gradient(180deg, rgba(16,24,41,.95), rgba(15,21,36,.92));
175
- border:var(--pixel-border);
176
- border-radius:12px;
177
- box-shadow:var(--shadow-lg);
178
- }
179
- #graph-canvas { width:100%; height:100%; display:block; cursor:grab; }
180
- .view {
181
- position:absolute;
182
- inset:0;
183
- }
184
- .view.hidden { display:none; }
185
- .pixel-stage {
186
- position:relative;
187
- width:100%;
188
- height:100%;
189
- overflow:hidden;
190
- background:
191
- linear-gradient(rgba(8,12,20,.2), rgba(8,12,20,.5)),
192
- url('./assets/star/office_bg_small.webp') center/cover no-repeat;
193
- }
194
- .pixel-stage::before {
195
- content:'';
196
- position:absolute;
197
- inset:0;
198
- background-image:
199
- linear-gradient(rgba(111,141,194,.13) 1px, transparent 1px),
200
- linear-gradient(90deg, rgba(111,141,194,.13) 1px, transparent 1px);
201
- background-size: 28px 28px;
202
- pointer-events:none;
203
- }
204
- .pixel-stage .asset {
205
- position:absolute;
206
- image-rendering:pixelated;
207
- z-index:2;
208
- filter: drop-shadow(0 6px 10px rgba(0,0,0,.35));
209
- opacity:.96;
210
- }
211
- .pixel-stage .desk-asset { width:180px; bottom:90px; left:40px; }
212
- .pixel-stage .server-asset { width:110px; top:24px; right:34px; }
213
- .pixel-stage .coffee-asset { width:72px; top:100px; right:180px; }
214
- .pixel-stage .walls-asset { width:220px; top:10px; left:46%; transform:translateX(-50%); opacity:.45; }
215
- .pixel-agent-layer {
216
- position:absolute;
217
- inset:0;
218
- z-index:3;
219
- }
220
- .pixel-agent {
221
- position:absolute;
222
- width:44px;
223
- transition: left .55s ease, top .55s ease;
224
- image-rendering:pixelated;
225
- filter: drop-shadow(0 5px 7px rgba(0,0,0,.36));
226
- }
227
- .pixel-agent.working,.pixel-agent.testing { animation: bob .45s infinite alternate; }
228
- .pixel-agent.thinking { animation: thinking 1s infinite; }
229
- .pixel-agent.error,.pixel-agent.failed { animation: shake .28s infinite; }
230
- .pixel-agent.done,.pixel-agent.passed { animation: pulse .85s infinite; }
231
- @keyframes bob { from { transform: translateY(0); } to { transform: translateY(-3px); } }
232
- @keyframes thinking { 0%,100%{transform:rotate(0)} 50%{transform:rotate(-3deg)} }
233
- @keyframes shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-2px)} 75%{transform:translateX(2px)} }
234
- @keyframes pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.06)} }
235
- .pixel-label {
236
- position:absolute;
237
- transform:translate(-50%, -50%);
238
- background:rgba(6,10,18,.82);
239
- border:1px solid rgba(83,210,166,.35);
240
- border-radius:6px;
241
- padding:2px 6px;
242
- font-size:9px;
243
- color:#e9f3ff;
244
- white-space:nowrap;
245
- z-index:4;
246
- }
247
- .pixel-kpis {
248
- position:absolute;
249
- left:14px;
250
- top:14px;
251
- display:flex;
252
- gap:8px;
253
- z-index:4;
254
- }
255
- .pixel-kpi {
256
- min-width:90px;
257
- background:rgba(10,16,28,.72);
258
- border:1px solid rgba(131,156,211,.24);
259
- border-radius:8px;
260
- padding:6px 8px;
261
- box-shadow:var(--shadow-md);
262
- }
263
- .pixel-kpi .t { font-size:9px; color:var(--text-subtle); }
264
- .pixel-kpi .v { font-size:12px; color:var(--accent); font-weight:bold; margin-top:2px; }
265
- .tooltip { position:absolute; background:rgba(25,38,63,.94); border:1px solid rgba(83,210,166,.35); border-radius:8px; padding:8px 12px; font-size:11px; pointer-events:none; z-index:100; display:none; max-width:280px; box-shadow:var(--shadow-md); }
266
- .tooltip.visible { display:block; }
267
-
268
- .log-panel { background:linear-gradient(180deg, rgba(16,24,41,.96), rgba(15,21,36,.92)); border:var(--pixel-border); border-radius:12px; display:flex; flex-direction:column; overflow:hidden; box-shadow:var(--shadow-md); }
269
- .panel-tabs { display:flex; border-bottom:1px solid rgba(131,156,211,.18); background:rgba(25,38,63,.3); }
270
- .panel-tabs .tab { background:none; border:none; color:var(--text-dim); font-family:inherit; font-size:10px; padding:9px 12px; cursor:pointer; text-transform:uppercase; letter-spacing:1px; border-bottom:2px solid transparent; }
271
- .panel-tabs .tab.active { color:var(--accent); border-bottom-color:var(--accent); background:rgba(35,51,84,.45); }
272
- .panel-tabs .tab:hover { color:var(--text); }
273
- .file-list { flex:1; overflow-y:auto; padding:4px 8px; font-size:10px; }
274
- .file-item { padding:7px 8px; border-bottom:1px solid rgba(131,156,211,.12); cursor:pointer; border-radius:8px; transition:background .15s; }
275
- .file-item:hover { background:rgba(35,51,84,.55); }
276
- .file-item .fname { color:var(--accent); font-weight:bold; }
277
- .file-item .fmeta { color:var(--text-dim); font-size:9px; margin-top:2px; }
278
- .file-preview { position:fixed; top:60px; left:50%; transform:translateX(-50%); width:760px; max-height:82vh; background:var(--bg-panel); border:1px solid rgba(83,210,166,.35); border-radius:12px; z-index:200; display:none; flex-direction:column; box-shadow:var(--shadow-lg); }
279
- .file-preview.visible { display:flex; }
280
- .file-preview .fp-header { display:flex; justify-content:space-between; align-items:center; padding:10px 14px; border-bottom:1px solid rgba(131,156,211,.18); }
281
- .file-preview .fp-header h4 { font-size:12px; color:var(--accent); margin:0; }
282
- .file-preview .fp-close { background:none; border:none; color:var(--text-dim); font-size:18px; cursor:pointer; }
283
- .file-preview pre { flex:1; overflow:auto; padding:14px; margin:0; font-size:11px; line-height:1.5; color:var(--text); background:#0c1220; }
284
- .log-list { flex:1; overflow-y:auto; padding:4px 8px; font-size:10px; line-height:1.6; }
285
- .log-list .log-entry { padding:4px 2px; border-bottom:1px solid rgba(131,156,211,.08); word-break:break-all; }
286
- .log-list .log-entry.warn { color:var(--orange); }
287
- .log-list .log-entry.error { color:var(--red); }
288
-
289
- .office {
290
- grid-column:1/-1;
291
- background:
292
- linear-gradient(180deg, rgba(16,24,41,.96), rgba(15,21,36,.92));
293
- border:var(--pixel-border);
294
- border-radius:12px;
295
- display:flex;
296
- overflow-x:auto;
297
- padding:10px;
298
- gap:10px;
299
- box-shadow:var(--shadow-md);
300
- }
301
- .desk {
302
- flex:0 0 190px;
303
- background:linear-gradient(180deg, rgba(25,38,63,.95), rgba(22,33,54,.9));
304
- border:1px solid rgba(131,156,211,.22);
305
- border-radius:10px;
306
- padding:10px;
307
- display:flex;
308
- flex-direction:column;
309
- align-items:center;
310
- gap:3px;
311
- position:relative;
312
- overflow:hidden;
313
- box-shadow:0 8px 18px rgba(0,0,0,.25);
314
- }
315
- .desk .badge { position:absolute; top:4px; right:4px; width:8px; height:8px; border-radius:50%; }
316
- .desk .croc-sprite { font-size:40px; position:relative; z-index:1; }
317
- .desk.idle .croc-sprite { animation:croc-idle 3s infinite; }
318
- .desk.working .croc-sprite { animation:croc-work .5s infinite alternate; }
319
- .desk.thinking .croc-sprite { animation:croc-think 1.2s infinite; }
320
- .desk.done .croc-sprite { animation:croc-done .8s 1; }
321
- .desk.error .croc-sprite { animation:croc-error .3s 3; }
322
- @keyframes croc-idle { 0%,90%,100%{transform:translateY(0)} 95%{transform:translateY(-3px)} }
323
- @keyframes croc-work { from{transform:translateY(0) rotate(-3deg)} to{transform:translateY(-5px) rotate(3deg)} }
324
- @keyframes croc-think { 0%,100%{transform:scale(1) rotate(0)} 25%{transform:scale(1.05) rotate(-2deg)} 75%{transform:scale(1.05) rotate(2deg)} }
325
- @keyframes croc-done { 0%{transform:scale(1)} 50%{transform:scale(1.2) translateY(-8px)} 100%{transform:scale(1)} }
326
- @keyframes croc-error { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-4px)} 75%{transform:translateX(4px)} }
327
- .desk .croc-name { font-size:11px; font-weight:bold; color:var(--accent); }
328
- .desk .croc-role { font-size:9px; color:var(--text-dim); text-transform:uppercase; }
329
- .desk .croc-task { font-size:9px; color:var(--orange); text-align:center; max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-height:13px; }
330
- .desk .progress-bar { width:90%; height:3px; background:#222; border-radius:2px; margin-top:2px; overflow:hidden; }
331
- .desk .progress-bar .fill { height:100%; background:var(--accent); transition:width .3s; border-radius:2px; }
332
- .desk .desk-items { position:absolute; bottom:4px; right:8px; font-size:14px; opacity:.4; }
50
+ /* โ”€โ”€โ”€ Design Tokens: Light โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
51
+ [data-theme="light"]{
52
+ --bg-void:#e8ecf4;--bg-deep:#f0f4fa;
53
+ --bg-panel:rgba(255,255,255,0.78);--bg-panel-solid:#ffffff;
54
+ --bg-card:rgba(248,250,255,0.7);--bg-hover:rgba(226,232,240,0.6);
55
+ --bg-glass:rgba(255,255,255,0.6);--bg-glass-strong:rgba(255,255,255,0.85);
56
+ --bg-frosted:rgba(255,255,255,0.5);--bg-input:rgba(241,245,249,0.8);
57
+ --accent:#059669;--accent-rgb:5,150,105;--accent-dim:#047857;
58
+ --accent-glow:rgba(5,150,105,0.25);--accent-bg:rgba(5,150,105,0.08);
59
+ --red:#dc2626;--red-rgb:220,38,38;--red-glow:rgba(220,38,38,0.2);--red-bg:rgba(220,38,38,0.06);
60
+ --orange:#d97706;--orange-rgb:217,119,6;--orange-glow:rgba(217,119,6,0.2);--orange-bg:rgba(217,119,6,0.06);
61
+ --blue:#2563eb;--blue-rgb:37,99,235;--blue-glow:rgba(37,99,235,0.2);--blue-bg:rgba(37,99,235,0.06);
62
+ --purple:#7c3aed;--purple-rgb:124,58,237;--purple-glow:rgba(124,58,237,0.2);--purple-bg:rgba(124,58,237,0.06);
63
+ --cyan:#0891b2;--cyan-rgb:8,145,178;--cyan-glow:rgba(8,145,178,0.2);--cyan-bg:rgba(8,145,178,0.06);
64
+ --pink:#db2777;--pink-rgb:219,39,119;
65
+ --text:#0f172a;--text-dim:#475569;--text-subtle:#94a3b8;--text-muted:#cbd5e1;
66
+ --border:rgba(100,116,139,0.15);--border-accent:rgba(5,150,105,0.3);--border-glass:rgba(0,0,0,0.04);
67
+ --shadow-xl:0 25px 50px -12px rgba(0,0,0,0.12);--shadow-lg:0 20px 40px rgba(0,0,0,0.06);
68
+ --shadow-md:0 8px 24px rgba(0,0,0,0.05);--shadow-sm:0 2px 8px rgba(0,0,0,0.03);
69
+ --shadow-glow:0 0 30px rgba(5,150,105,0.08);--shadow-inset:inset 0 1px 0 rgba(255,255,255,0.8);
70
+ }
71
+
72
+ /* โ”€โ”€โ”€ 3D Canvas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
73
+ #three-canvas{position:fixed;inset:0;width:100%;height:100%;z-index:var(--z-canvas);display:block}
74
+
75
+ /* โ”€โ”€โ”€ Glass Panel Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
76
+ .glass{background:var(--bg-glass);backdrop-filter:var(--blur-md);-webkit-backdrop-filter:var(--blur-md);border:1px solid var(--border-glass);box-shadow:var(--shadow-md),var(--shadow-inset);border-radius:var(--radius-lg);transition:background var(--transition),border-color var(--transition),box-shadow var(--transition)}
77
+ .glass:hover{background:var(--bg-glass-strong);border-color:var(--border);box-shadow:var(--shadow-lg),var(--shadow-inset)}
78
+ .glass-strong{background:var(--bg-glass-strong);backdrop-filter:var(--blur-lg);-webkit-backdrop-filter:var(--blur-lg);border:1px solid var(--border);box-shadow:var(--shadow-lg),var(--shadow-inset);border-radius:var(--radius-lg)}
79
+
80
+ /* โ”€โ”€โ”€ Header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
81
+ .header{position:fixed;top:12px;left:12px;right:12px;height:52px;z-index:var(--z-header);display:flex;align-items:center;gap:8px;padding:0 16px;background:var(--bg-glass-strong);backdrop-filter:var(--blur-lg);-webkit-backdrop-filter:var(--blur-lg);border:1px solid var(--border-glass);border-radius:var(--radius-xl);box-shadow:var(--shadow-lg),var(--shadow-inset);transition:all var(--transition)}
82
+ .header::before{content:'';position:absolute;inset:0;border-radius:var(--radius-xl);background:linear-gradient(135deg,rgba(var(--accent-rgb),0.05),transparent 60%);pointer-events:none}
83
+ .logo{width:32px;height:32px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--accent-bg);border:1px solid var(--border-accent);border-radius:var(--radius-md);transition:all var(--transition)}
84
+ .logo:hover{transform:scale(1.08) rotate(-3deg);box-shadow:var(--shadow-glow)}
85
+ .logo svg{width:20px;height:20px}
86
+ .title-wrap{display:flex;flex-direction:column;gap:0;margin-right:16px;min-width:0}
87
+ .title-wrap h1{font-size:14px;font-weight:700;letter-spacing:-0.02em;color:var(--text);line-height:1.2;white-space:nowrap}
88
+ .title-wrap .subtitle{font-size:10px;color:var(--text-subtle);letter-spacing:0.05em;text-transform:uppercase;line-height:1}
89
+ .h-divider{width:1px;height:28px;flex-shrink:0;background:var(--border);margin:0 4px}
90
+
91
+ /* View Switch */
92
+ .view-switch{display:flex;gap:2px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-md);padding:2px}
93
+ .view-switch button{display:flex;align-items:center;gap:5px;padding:5px 12px;border:none;border-radius:var(--radius-sm);font-size:12px;font-weight:500;color:var(--text-dim);cursor:pointer;background:transparent;transition:all var(--transition);white-space:nowrap}
94
+ .view-switch button:hover{color:var(--text);background:var(--bg-hover)}
95
+ .view-switch button.active{color:var(--text);background:var(--bg-panel);box-shadow:var(--shadow-sm)}
96
+ .view-switch button svg{width:14px;height:14px;flex-shrink:0}
97
+
98
+ /* Actions */
99
+ .actions{display:flex;gap:4px;align-items:center;flex-shrink:0}
100
+ .btn{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;font-size:12px;font-weight:600;transition:all var(--transition);white-space:nowrap;position:relative;overflow:hidden}
101
+ .btn::before{content:'';position:absolute;inset:0;background:linear-gradient(180deg,rgba(255,255,255,0.06),transparent);pointer-events:none}
102
+ .btn-primary{background:var(--accent);color:#000;border-color:var(--accent-dim)}
103
+ .btn-primary:hover{background:var(--accent-dim);transform:translateY(-1px);box-shadow:0 4px 16px var(--accent-glow)}
104
+ .btn-danger{background:var(--red-bg);color:var(--red);border-color:rgba(var(--red-rgb),0.25)}
105
+ .btn-danger:hover{background:rgba(var(--red-rgb),0.18);transform:translateY(-1px);box-shadow:0 4px 16px var(--red-glow)}
106
+ .btn-secondary{background:var(--bg-card);color:var(--text-dim)}
107
+ .btn-secondary:hover{background:var(--bg-hover);color:var(--text);transform:translateY(-1px)}
108
+ .btn:disabled{opacity:0.4;cursor:not-allowed;transform:none!important;box-shadow:none!important}
109
+ .btn svg{width:14px;height:14px;flex-shrink:0}
110
+ .btn:active{transform:translateY(0)}
111
+
112
+ /* Run Mode */
113
+ .run-mode-wrap{position:relative}
114
+ .run-mode-wrap select{appearance:none;background:var(--bg-card);color:var(--text-dim);border:1px solid var(--border);border-radius:var(--radius-sm);padding:5px 28px 5px 10px;font-size:12px;font-weight:500;cursor:pointer;transition:all var(--transition);outline:none}
115
+ .run-mode-wrap select:hover{border-color:var(--border-accent);color:var(--text)}
116
+ .run-mode-wrap::after{content:'โ–พ';position:absolute;right:8px;top:50%;transform:translateY(-50%);color:var(--text-subtle);font-size:10px;pointer-events:none}
117
+
118
+ /* Stats */
119
+ .stats{display:flex;gap:2px;margin-left:auto;flex-shrink:0}
120
+ .stat-box{display:flex;flex-direction:column;align-items:center;padding:4px 12px;min-width:56px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);transition:all var(--transition)}
121
+ .stat-box:first-child{border-radius:var(--radius-sm) var(--radius-xs) var(--radius-xs) var(--radius-sm)}
122
+ .stat-box:last-child{border-radius:var(--radius-xs) var(--radius-sm) var(--radius-sm) var(--radius-xs)}
123
+ .stat-box:hover{background:var(--bg-hover);border-color:var(--border-accent)}
124
+ .stat-label{font-size:9px;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-subtle);line-height:1}
125
+ .stat-value{font-size:16px;font-weight:700;color:var(--accent);line-height:1.3;font-variant-numeric:tabular-nums}
126
+
127
+ /* Header end */
128
+ .header-end{display:flex;align-items:center;gap:6px;margin-left:8px}
129
+ #theme-toggle{width:32px;height:32px;border:none;background:var(--bg-card);border-radius:var(--radius-sm);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text-dim);transition:all var(--transition);border:1px solid var(--border)}
130
+ #theme-toggle:hover{background:var(--bg-hover);color:var(--text);transform:scale(1.05)}
131
+ #theme-toggle svg{width:16px;height:16px}
132
+ #conn-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--text-subtle);transition:background 0.3s}
133
+ #conn-dot.connected{background:var(--accent);box-shadow:0 0 8px var(--accent-glow);animation:conn-pulse 2s infinite}
134
+
135
+ /* โ”€โ”€โ”€ Sidebar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
136
+ .sidebar{position:fixed;top:76px;left:12px;bottom:240px;width:220px;z-index:var(--z-panel);display:flex;flex-direction:column;overflow:hidden;background:var(--bg-glass);backdrop-filter:var(--blur-md);-webkit-backdrop-filter:var(--blur-md);border:1px solid var(--border-glass);border-radius:var(--radius-lg);box-shadow:var(--shadow-md),var(--shadow-inset);transition:all var(--transition-slow)}
137
+ .sidebar.collapsed{width:48px}
138
+ .sidebar.collapsed .sidebar-content{opacity:0;pointer-events:none}
139
+ .sidebar.collapsed .sidebar-toggle svg{transform:rotate(180deg)}
140
+ .sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 8px;flex-shrink:0}
141
+ .sidebar-header h3{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:var(--text-subtle);font-weight:600}
142
+ .sidebar-toggle{width:24px;height:24px;border:none;background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text-subtle);border-radius:var(--radius-xs);transition:all var(--transition)}
143
+ .sidebar-toggle:hover{background:var(--bg-hover);color:var(--text)}
144
+ .sidebar-toggle svg{width:14px;height:14px;transition:transform var(--transition)}
145
+ .sidebar-content{flex:1;overflow-y:auto;padding:0 8px 8px;transition:opacity var(--transition)}
146
+ .mod-item{display:flex;align-items:center;gap:8px;padding:7px 10px;margin-bottom:2px;border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition)}
147
+ .mod-item:hover{background:var(--bg-hover)}
148
+ .mod-item.active{background:var(--accent-bg);color:var(--accent)}
149
+ .mod-item .dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;background:var(--accent);opacity:0.6;transition:all var(--transition)}
150
+ .mod-item:hover .dot{opacity:1;transform:scale(1.3)}
151
+ .mod-item .name{flex:1;font-size:12px;font-weight:500;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:color var(--transition)}
152
+ .mod-item:hover .name{color:var(--text)}
153
+ .mod-item .count{font-size:11px;color:var(--text-subtle);font-variant-numeric:tabular-nums;padding:1px 6px;background:var(--bg-card);border-radius:var(--radius-xs);font-weight:500;transition:all var(--transition)}
154
+ .mod-item:hover .count{background:var(--bg-hover);color:var(--text-dim)}
155
+ .agent-item{display:flex;align-items:center;gap:8px;padding:8px 10px;margin-bottom:2px;border-radius:var(--radius-sm);transition:all var(--transition)}
156
+ .agent-item .status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;transition:all 0.3s}
157
+ .agent-item .status-dot.working{background:var(--accent);box-shadow:0 0 6px var(--accent-glow);animation:dot-pulse 1s infinite}
158
+ .agent-item .status-dot.testing{background:var(--blue);box-shadow:0 0 6px var(--blue-glow);animation:dot-pulse 1s infinite}
159
+ .agent-item .status-dot.thinking{background:var(--purple);box-shadow:0 0 6px var(--purple-glow);animation:dot-pulse 1.2s infinite}
160
+ .agent-item .status-dot.error,.agent-item .status-dot.failed{background:var(--red);box-shadow:0 0 6px var(--red-glow);animation:dot-shake 0.3s infinite}
161
+ .agent-item .status-dot.done,.agent-item .status-dot.passed{background:var(--accent);box-shadow:0 0 6px var(--accent-glow)}
162
+ .agent-item .status-dot.idle{background:var(--text-subtle)}
163
+ .agent-item .agent-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
164
+ .agent-item .agent-name{font-size:12px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
165
+ .agent-item .agent-role{font-size:10px;color:var(--text-subtle);text-transform:uppercase;letter-spacing:0.04em}
166
+
167
+ /* โ”€โ”€โ”€ Right Panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
168
+ .right-panel{position:fixed;top:76px;right:12px;bottom:240px;width:320px;z-index:var(--z-panel);display:flex;flex-direction:column;background:var(--bg-glass);backdrop-filter:var(--blur-md);-webkit-backdrop-filter:var(--blur-md);border:1px solid var(--border-glass);border-radius:var(--radius-lg);box-shadow:var(--shadow-md),var(--shadow-inset);overflow:hidden;transition:all var(--transition-slow)}
169
+ .panel-tabs{display:flex;gap:0;padding:6px 6px 0;flex-shrink:0;border-bottom:1px solid var(--border)}
170
+ .tab{flex:1;display:flex;align-items:center;justify-content:center;gap:5px;padding:8px 4px;border:none;background:transparent;font-size:12px;font-weight:500;color:var(--text-subtle);cursor:pointer;position:relative;border-radius:var(--radius-sm) var(--radius-sm) 0 0;transition:all var(--transition);white-space:nowrap}
171
+ .tab:hover{color:var(--text-dim);background:var(--bg-hover)}
172
+ .tab.active{color:var(--accent);background:var(--bg-card)}
173
+ .tab.active::after{content:'';position:absolute;bottom:0;left:20%;right:20%;height:2px;background:var(--accent);border-radius:1px}
174
+ .tab svg{width:13px;height:13px;flex-shrink:0}
175
+ .tab-badge{font-size:9px;font-weight:700;min-width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;background:var(--accent-bg);color:var(--accent);border-radius:var(--radius-full);padding:0 4px;font-variant-numeric:tabular-nums}
176
+ .tab-badge.alert{background:var(--red-bg);color:var(--red)}
177
+ .panel-content{flex:1;overflow-y:auto;padding:8px}
178
+ .panel-content.hidden{display:none}
179
+
180
+ /* Log */
181
+ .log-entry{padding:5px 10px;margin-bottom:2px;border-radius:var(--radius-xs);font-size:12px;line-height:1.5;font-family:"SF Mono","Fira Code","Cascadia Code",Consolas,monospace;transition:background var(--transition);display:flex;gap:8px;align-items:flex-start}
182
+ .log-entry:hover{background:var(--bg-hover)}
183
+ .log-entry .timestamp{color:var(--text-subtle);font-size:10px;flex-shrink:0;font-variant-numeric:tabular-nums;padding-top:1px}
184
+ .log-entry .message{color:var(--text-dim);word-break:break-word;flex:1}
185
+ .log-entry.warn .message{color:var(--orange)}
186
+ .log-entry.error .message{color:var(--red)}
187
+ .log-entry.success .message{color:var(--accent)}
188
+ .tw-cursor{display:inline-block;width:2px;height:1em;background:var(--accent);margin-left:2px;vertical-align:text-bottom;animation:tw-blink 0.6s step-end infinite}
189
+ @keyframes tw-blink{0%,100%{opacity:1}50%{opacity:0}}
190
+
191
+ /* Files */
192
+ .file-item{display:flex;align-items:center;gap:8px;padding:8px 10px;margin-bottom:2px;border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition)}
193
+ .file-item:hover{background:var(--bg-hover)}
194
+ .file-item svg{width:14px;height:14px;flex-shrink:0;color:var(--text-subtle)}
195
+ .file-item .file-name{flex:1;font-size:12px;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
196
+ .file-item:hover .file-name{color:var(--text)}
197
+
198
+ /* Results */
199
+ .results-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:4px}
200
+ .result-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-md);padding:12px;display:flex;flex-direction:column;gap:4px;transition:all var(--transition)}
201
+ .result-card:hover{border-color:var(--border-accent);background:var(--bg-hover)}
202
+ .result-card .label{font-size:10px;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-subtle)}
203
+ .result-card .value{font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}
204
+ .result-card.passed .value{color:var(--accent)}
205
+ .result-card.failed .value{color:var(--red)}
206
+ .result-card.skipped .value{color:var(--orange)}
207
+ .result-card.timeout .value{color:var(--purple)}
208
+ .pass-rate-bar{height:6px;background:var(--bg-card);border-radius:3px;overflow:hidden;margin:8px 4px;border:1px solid var(--border)}
209
+ .pass-rate-bar .fill{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--accent),var(--cyan));transition:width 0.8s var(--ease-out-expo)}
210
+ .quality-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;padding:4px}
211
+ .quality-item{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px;text-align:center;transition:all var(--transition)}
212
+ .quality-item:hover{border-color:var(--border-accent)}
213
+ .quality-item .q-label{font-size:9px;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-subtle)}
214
+ .quality-item .q-value{font-size:14px;font-weight:700;color:var(--blue);margin-top:2px}
215
+
216
+ /* โ”€โ”€โ”€ Bottom Bar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
217
+ .bottom-bar{position:fixed;bottom:12px;left:12px;right:12px;height:216px;z-index:var(--z-panel);display:flex;flex-direction:column;background:var(--bg-glass);backdrop-filter:var(--blur-md);-webkit-backdrop-filter:var(--blur-md);border:1px solid var(--border-glass);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg),var(--shadow-inset);overflow:hidden;transition:all var(--transition-slow)}
218
+ .bottom-bar.collapsed{height:40px}
219
+ .bottom-bar.collapsed .desk-row{opacity:0;pointer-events:none}
220
+ .bottom-bar-header{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;flex-shrink:0;border-bottom:1px solid var(--border);cursor:pointer}
221
+ .bottom-bar-header h3{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:var(--text-subtle);font-weight:600}
222
+ .bottom-bar-header .toggle-icon{color:var(--text-subtle);transition:transform var(--transition)}
223
+ .bottom-bar.collapsed .toggle-icon{transform:rotate(180deg)}
224
+ .desk-row{flex:1;display:flex;gap:8px;padding:8px 12px;overflow-x:auto;overflow-y:hidden;transition:opacity var(--transition)}
225
+
226
+ /* Desk Cards */
227
+ .desk-card{flex:0 0 200px;display:flex;flex-direction:column;gap:6px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-md);padding:12px;position:relative;overflow:hidden;transition:all var(--transition)}
228
+ .desk-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--accent);opacity:0;transition:opacity var(--transition)}
229
+ .desk-card:hover{border-color:var(--border-accent);transform:translateY(-2px);box-shadow:var(--shadow-md)}
230
+ .desk-card:hover::before{opacity:1}
231
+ .desk-card.working::before{background:var(--accent);opacity:1}
232
+ .desk-card.error::before,.desk-card.failed::before{background:var(--red);opacity:1}
233
+ .desk-card.thinking::before{background:var(--purple);opacity:1}
234
+ .desk-card.testing::before{background:var(--blue);opacity:1}
235
+ .desk-card .card-top{display:flex;align-items:center;gap:8px}
236
+ .desk-card .card-avatar{width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--bg-hover);border:1px solid var(--border);border-radius:var(--radius-sm)}
237
+ .desk-card .card-avatar svg{width:20px;height:20px}
238
+ .desk-card .card-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
239
+ .desk-card .card-name{font-size:13px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
240
+ .desk-card .card-role{font-size:10px;color:var(--text-subtle);text-transform:uppercase;letter-spacing:0.04em}
241
+ .desk-card .status-badge{position:absolute;top:8px;right:8px;font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;padding:2px 6px;border-radius:var(--radius-xs)}
242
+ .desk-card .status-badge.working{background:var(--accent-bg);color:var(--accent)}
243
+ .desk-card .status-badge.testing{background:var(--blue-bg);color:var(--blue)}
244
+ .desk-card .status-badge.thinking{background:var(--purple-bg);color:var(--purple)}
245
+ .desk-card .status-badge.error,.desk-card .status-badge.failed{background:var(--red-bg);color:var(--red)}
246
+ .desk-card .status-badge.done,.desk-card .status-badge.passed{background:var(--accent-bg);color:var(--accent)}
247
+ .desk-card .status-badge.idle{background:var(--bg-hover);color:var(--text-subtle)}
248
+ .desk-card .card-task{font-size:11px;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:16px}
249
+ .desk-card .progress-bar{height:3px;background:var(--bg-hover);border-radius:2px;overflow:hidden}
250
+ .desk-card .progress-bar .fill{height:100%;border-radius:2px;background:linear-gradient(90deg,var(--accent),var(--cyan));transition:width 0.4s var(--ease-out-expo)}
251
+
252
+ /* โ”€โ”€โ”€ File Preview Modal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
253
+ .file-preview{position:fixed;inset:0;z-index:var(--z-modal);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity var(--transition)}
254
+ .file-preview.visible{opacity:1;pointer-events:auto}
255
+ .file-preview .backdrop{position:absolute;inset:0;background:rgba(0,0,0,0.5);backdrop-filter:var(--blur-sm)}
256
+ .file-preview .fp-dialog{position:relative;width:min(800px,90vw);height:min(600px,80vh);background:var(--bg-glass-strong);backdrop-filter:var(--blur-lg);border:1px solid var(--border);border-radius:var(--radius-xl);box-shadow:var(--shadow-xl);display:flex;flex-direction:column;overflow:hidden;transform:scale(0.95) translateY(10px);transition:transform var(--transition-spring)}
257
+ .file-preview.visible .fp-dialog{transform:scale(1) translateY(0)}
258
+ .fp-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border);flex-shrink:0}
259
+ .fp-header .fp-title{font-size:13px;font-weight:600;color:var(--text)}
260
+ .fp-header .fp-close{width:28px;height:28px;border:none;background:var(--bg-card);border-radius:var(--radius-sm);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text-dim);transition:all var(--transition);border:1px solid var(--border)}
261
+ .fp-header .fp-close:hover{background:var(--red-bg);color:var(--red);border-color:rgba(var(--red-rgb),0.25)}
262
+ .fp-code{flex:1;overflow:auto;padding:16px;font-family:"SF Mono","Fira Code","Cascadia Code",Consolas,monospace;font-size:12px;line-height:1.6;color:var(--text-dim);white-space:pre-wrap;word-break:break-all;tab-size:2}
263
+
264
+ /* โ”€โ”€โ”€ Tooltip โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
265
+ .tooltip{position:fixed;z-index:var(--z-tooltip);padding:6px 10px;background:var(--bg-glass-strong);backdrop-filter:var(--blur-md);border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-md);font-size:11px;color:var(--text);pointer-events:none;opacity:0;transition:opacity 0.15s;max-width:280px}
266
+ .tooltip.visible{opacity:1}
267
+
268
+ /* โ”€โ”€โ”€ Shortcut Legend โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
269
+ .shortcut-legend{position:fixed;bottom:240px;right:12px;z-index:var(--z-notify);display:grid;grid-template-columns:auto 1fr;gap:4px 10px;padding:12px 16px;background:var(--bg-glass-strong);backdrop-filter:var(--blur-lg);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-lg);opacity:0;transform:translateY(8px) scale(0.96);pointer-events:none;transition:all var(--transition-spring)}
270
+ .shortcut-legend.visible{opacity:1;transform:translateY(0) scale(1);pointer-events:auto}
271
+ .shortcut-legend kbd{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;padding:0 6px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-xs);font-size:11px;font-weight:600;color:var(--text);font-family:inherit;box-shadow:0 1px 2px rgba(0,0,0,0.15)}
272
+ .shortcut-legend span{font-size:12px;color:var(--text-dim);display:flex;align-items:center}
273
+
274
+ /* โ”€โ”€โ”€ Loading โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
275
+ .loading-overlay{position:fixed;inset:0;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;background:var(--bg-void);transition:opacity 0.8s var(--ease-out-expo)}
276
+ .loading-overlay.hidden{opacity:0;pointer-events:none}
277
+ .loading-bar{width:200px;height:3px;background:var(--bg-card);border-radius:2px;overflow:hidden}
278
+ .loading-bar .fill{width:0%;height:100%;background:linear-gradient(90deg,var(--accent),var(--cyan));border-radius:2px;transition:width 0.3s var(--ease-out-expo)}
279
+ .loading-text{font-size:12px;color:var(--text-subtle);letter-spacing:0.04em}
280
+
281
+ /* โ”€โ”€โ”€ Toast โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
282
+ .toast-container{position:fixed;top:76px;right:12px;z-index:var(--z-notify);display:flex;flex-direction:column;gap:8px;pointer-events:none}
283
+ .toast{display:flex;align-items:center;gap:8px;padding:10px 16px;background:var(--bg-glass-strong);backdrop-filter:var(--blur-md);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-lg);font-size:12px;color:var(--text);pointer-events:auto;animation:toast-in 0.4s var(--ease-spring),toast-out 0.3s ease-in 3.7s forwards;max-width:320px}
284
+ .toast.success{border-left:3px solid var(--accent)}
285
+ .toast.error{border-left:3px solid var(--red)}
286
+ .toast.warning{border-left:3px solid var(--orange)}
287
+ .toast.info{border-left:3px solid var(--blue)}
288
+
289
+ /* โ”€โ”€โ”€ 3D HUD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
290
+ .bubble-3d{padding:6px 12px;background:var(--bg-glass-strong);backdrop-filter:var(--blur-sm);border:1px solid var(--border-accent);border-radius:12px 12px 12px 2px;font-size:12px;color:var(--text);box-shadow:var(--shadow-sm);animation:bubble-3d-in 0.3s var(--ease-spring);pointer-events:none;white-space:nowrap}
291
+ .agent-label-3d{padding:3px 8px;background:var(--bg-glass);backdrop-filter:var(--blur-xs);border:1px solid var(--border-glass);border-radius:var(--radius-sm);font-size:10px;font-weight:600;color:var(--text);text-align:center;pointer-events:none;box-shadow:var(--shadow-sm);white-space:nowrap}
292
+ .agent-label-3d .role{display:block;font-size:8px;font-weight:400;color:var(--text-subtle);text-transform:uppercase;letter-spacing:0.04em}
293
+
294
+ /* โ”€โ”€โ”€ Animations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
295
+ @keyframes conn-pulse{0%{box-shadow:0 0 0 0 var(--accent-glow)}70%{box-shadow:0 0 0 6px transparent}100%{box-shadow:0 0 0 0 transparent}}
296
+ @keyframes dot-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:0.5;transform:scale(0.85)}}
297
+ @keyframes dot-shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-1px)}75%{transform:translateX(1px)}}
298
+ @keyframes toast-in{from{opacity:0;transform:translateX(40px) scale(0.9)}to{opacity:1;transform:translateX(0) scale(1)}}
299
+ @keyframes toast-out{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(40px)}}
300
+ @keyframes bubble-3d-in{from{opacity:0;transform:translateY(6px) scale(0.9)}to{opacity:1;transform:translateY(0) scale(1)}}
301
+ @keyframes skeleton-pulse{0%{background-position:200% 0}100%{background-position:-200% 0}}
302
+ @keyframes float-gentle{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
303
+ @keyframes rotate-slow{from{transform:rotate(0)}to{transform:rotate(360deg)}}
304
+ @keyframes glow-pulse{0%,100%{opacity:0.6}50%{opacity:1}}
305
+ .skeleton{background:linear-gradient(90deg,var(--bg-card) 25%,var(--bg-hover) 50%,var(--bg-card) 75%);background-size:200% 100%;animation:skeleton-pulse 1.5s ease infinite;border-radius:var(--radius-sm)}
306
+
307
+ /* โ”€โ”€โ”€ Responsive โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
308
+ @media(max-width:1200px){.sidebar{width:180px}.right-panel{width:280px}.stats{display:none}}
309
+ @media(max-width:960px){.sidebar{display:none}.right-panel{width:260px}}
310
+ @media(max-width:768px){.header{padding:0 10px;gap:4px}.right-panel{display:none}.bottom-bar{height:180px}}
311
+ @media print{.header,.sidebar,.bottom-bar,.right-panel,.shortcut-legend,.tooltip,.file-preview,.loading-overlay,.toast-container{display:none!important}#three-canvas{position:static;width:100%;height:auto}}
333
312
  </style>
334
313
  </head>
335
314
  <body>
336
- <div class="app">
337
- <header class="header">
338
- <div class="logo">๐ŸŠ</div>
339
- <div class="title-wrap">
340
- <h1>OpenCroc Studio</h1>
341
- <div class="subtitle">Pixel Ops Dashboard ยท Real-time Multi-Agent Runtime</div>
342
- </div>
343
- <div class="actions">
344
- <div class="view-switch">
345
- <button id="view-dashboard" class="active">Dashboard</button>
346
- <button id="view-office">Pixel Office</button>
347
- </div>
348
- <button class="btn" id="btn-scan" title="Scan project">๐Ÿ” Scan</button>
349
- <button class="btn" id="btn-pipeline" title="Run full pipeline">โ–ถ Pipeline</button>
350
- <select id="run-mode" class="mode-select" title="Test run mode">
351
- <option value="auto">Auto</option>
352
- <option value="reuse">Reuse</option>
353
- <option value="managed">Managed</option>
354
- </select>
355
- <button class="btn" id="btn-run-tests" title="Run generated tests" disabled>๐Ÿงช Run Tests</button>
356
- <button class="btn" id="btn-reports" title="Generate reports" disabled>๐Ÿ“Š Reports</button>
357
- <button class="btn danger" id="btn-reset" title="Reset agents">โน Reset</button>
358
- </div>
359
- <div class="stats">
360
- <div>Modules <span id="s-mod">-</span></div>
361
- <div>Models <span id="s-mdl">-</span></div>
362
- <div>APIs <span id="s-api">-</span></div>
363
- <div>Tests <span id="s-files">-</span></div>
364
- <div id="s-results-wrap" style="display:none">Results <span id="s-results" style="color:var(--accent)">-</span></div>
365
- </div>
366
- <div class="conn-dot" id="conn-dot" title="WebSocket"></div>
367
- </header>
368
-
369
- <aside class="sidebar">
370
- <h3>๐Ÿ“‚ Modules</h3>
371
- <div id="mod-list"></div>
372
- <h3 style="margin-top:12px">๐ŸŠ Agents</h3>
373
- <div id="agent-sidebar"></div>
374
- </aside>
375
-
376
- <main class="main">
377
- <div class="view" id="graph-view">
378
- <canvas id="graph-canvas"></canvas>
379
- </div>
380
- <div class="view hidden" id="pixel-view">
381
- <div class="pixel-stage">
382
- <img class="asset desk-asset" src="./assets/star/desk-v3.webp" alt="desk">
383
- <img class="asset server-asset" src="./assets/botreview/server.gif" alt="server">
384
- <img class="asset coffee-asset" src="./assets/botreview/coffee-machine.gif" alt="coffee">
385
- <img class="asset walls-asset" src="./assets/botreview/walls.png" alt="walls">
386
- <div class="pixel-kpis">
387
- <div class="pixel-kpi"><div class="t">Working</div><div class="v" id="kpi-working">0</div></div>
388
- <div class="pixel-kpi"><div class="t">Errors</div><div class="v" id="kpi-errors">0</div></div>
389
- <div class="pixel-kpi"><div class="t">Done</div><div class="v" id="kpi-done">0</div></div>
390
- </div>
391
- <div class="pixel-agent-layer" id="pixel-agent-layer"></div>
392
- </div>
393
- </div>
394
- <div class="tooltip" id="tooltip"></div>
395
- </main>
396
-
397
- <div class="log-panel">
398
- <div class="panel-tabs">
399
- <button class="tab active" data-tab="log">๐Ÿ“‹ Log</button>
400
- <button class="tab" data-tab="files">๐Ÿ“„ Tests <span id="file-badge" style="display:none;background:var(--accent);color:#000;border-radius:8px;padding:0 5px;font-size:9px;margin-left:3px">0</span></button>
401
- <button class="tab" data-tab="results">๐Ÿงช Results <span id="result-badge" style="display:none;background:var(--accent);color:#000;border-radius:8px;padding:0 5px;font-size:9px;margin-left:3px">0</span></button>
402
- <button class="tab" data-tab="reports">๐Ÿ“Š Reports</button>
403
- </div>
404
- <div class="log-list" id="log-list"></div>
405
- <div class="file-list" id="file-list" style="display:none"></div>
406
- <div class="file-list" id="results-panel" style="display:none"></div>
407
- <div class="file-list" id="reports-panel" style="display:none"></div>
315
+
316
+ <!-- Loading Overlay -->
317
+ <div class="loading-overlay" id="loading-overlay">
318
+ <svg width="48" height="48" viewBox="0 0 16 16" fill="none" style="opacity:0.8"><rect x="2" y="4" width="12" height="10" rx="1" fill="none" stroke="var(--accent)" stroke-width="1.5"/><circle cx="8" cy="9" r="2.5" fill="var(--accent)"/><rect x="5" y="2" width="6" height="2" rx="0.5" fill="var(--accent)" opacity="0.6"/></svg>
319
+ <div class="loading-bar"><div class="fill" id="loading-fill"></div></div>
320
+ <div class="loading-text" id="loading-text">Initializing 3D Engineโ€ฆ</div>
321
+ </div>
322
+
323
+ <!-- 3D Canvas -->
324
+ <canvas id="three-canvas"></canvas>
325
+
326
+ <!-- Header -->
327
+ <div class="header" id="header">
328
+ <div class="logo"><svg viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="10" rx="1" fill="none" stroke="var(--accent)" stroke-width="1.5"/><circle cx="8" cy="9" r="2.5" fill="var(--accent)"/><rect x="5" y="2" width="6" height="2" rx="0.5" fill="var(--accent)" opacity="0.6"/></svg></div>
329
+ <div class="title-wrap"><h1>OpenCroc Studio</h1><span class="subtitle">3D Ops Dashboard ยท Real-time Multi-Agent Runtime</span></div>
330
+ <div class="h-divider"></div>
331
+ <div class="view-switch">
332
+ <button id="view-3d" class="active"><svg viewBox="0 0 16 16" fill="none"><path d="M8 1L14.5 5v6L8 15 1.5 11V5z" stroke="currentColor" stroke-width="1.2"/><path d="M8 1v14M1.5 5L8 9l6.5-4" stroke="currentColor" stroke-width="1" opacity="0.5"/></svg>3D Office</button>
333
+ <button id="view-graph"><svg viewBox="0 0 16 16" fill="none"><circle cx="4" cy="4" r="2" fill="currentColor" opacity="0.6"/><circle cx="12" cy="4" r="2" fill="currentColor" opacity="0.6"/><circle cx="8" cy="12" r="2" fill="currentColor" opacity="0.6"/><line x1="4" y1="6" x2="8" y2="10" stroke="currentColor" stroke-width="1"/><line x1="12" y1="6" x2="8" y2="10" stroke="currentColor" stroke-width="1"/></svg>Graph</button>
334
+ </div>
335
+ <div class="h-divider"></div>
336
+ <div class="actions">
337
+ <button class="btn btn-secondary" id="btn-scan"><svg viewBox="0 0 16 16" fill="none"><path d="M2 8h12M4 4h8M6 12h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>Scan</button>
338
+ <button class="btn btn-primary" id="btn-pipeline"><svg viewBox="0 0 16 16" fill="none"><path d="M2 8h3l2-4 2 8 2-4h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>Pipeline</button>
339
+ <div class="run-mode-wrap"><select id="run-mode"><option value="auto">Auto</option><option value="reuse">Reuse</option><option value="managed">Managed</option></select></div>
340
+ <button class="btn btn-secondary" id="btn-run-tests"><svg viewBox="0 0 16 16" fill="none"><path d="M4 2v12l8-6z" fill="currentColor" opacity="0.7"/></svg>Tests</button>
341
+ <button class="btn btn-secondary" id="btn-reports"><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"/><line x1="5" y1="5" x2="11" y2="5" stroke="currentColor" stroke-width="1" opacity="0.5"/><line x1="5" y1="8" x2="11" y2="8" stroke="currentColor" stroke-width="1" opacity="0.5"/><line x1="5" y1="11" x2="9" y2="11" stroke="currentColor" stroke-width="1" opacity="0.5"/></svg>Reports</button>
342
+ <button class="btn btn-danger" id="btn-reset"><svg viewBox="0 0 16 16" fill="none"><path d="M2 8a6 6 0 0111.3-2.8M14 8a6 6 0 01-11.3 2.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M14 2v4h-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>Reset</button>
408
343
  </div>
344
+ <div class="stats">
345
+ <div class="stat-box"><span class="stat-label">MODULES</span><span class="stat-value" id="s-mod">0</span></div>
346
+ <div class="stat-box"><span class="stat-label">MODELS</span><span class="stat-value" id="s-mdl">0</span></div>
347
+ <div class="stat-box"><span class="stat-label">APIs</span><span class="stat-value" id="s-api">0</span></div>
348
+ <div class="stat-box"><span class="stat-label">TESTS</span><span class="stat-value" id="s-files">-</span></div>
349
+ </div>
350
+ <div class="header-end">
351
+ <button id="theme-toggle" title="Toggle theme"><svg id="theme-icon-dark" viewBox="0 0 16 16" fill="none"><path d="M8 1a7 7 0 100 14 5 5 0 010-14z" fill="currentColor"/></svg><svg id="theme-icon-light" viewBox="0 0 16 16" fill="none" style="display:none"><circle cx="8" cy="8" r="3" fill="currentColor"/><g stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="8" y1="1" x2="8" y2="3"/><line x1="8" y1="13" x2="8" y2="15"/><line x1="1" y1="8" x2="3" y2="8"/><line x1="13" y1="8" x2="15" y2="8"/><line x1="3.05" y1="3.05" x2="4.46" y2="4.46"/><line x1="11.54" y1="11.54" x2="12.95" y2="12.95"/><line x1="3.05" y1="12.95" x2="4.46" y2="11.54"/><line x1="11.54" y1="4.46" x2="12.95" y2="3.05"/></g></svg></button>
352
+ <div id="conn-dot" title="WebSocket"></div>
353
+ </div>
354
+ </div>
409
355
 
410
- <section class="office" id="croc-office"></section>
356
+ <!-- Sidebar -->
357
+ <div class="sidebar" id="sidebar">
358
+ <div class="sidebar-header"><h3>Modules</h3><button class="sidebar-toggle" id="sidebar-toggle"><svg viewBox="0 0 16 16" fill="none"><path d="M10 3L5 8l5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></button></div>
359
+ <div class="sidebar-content" id="sidebar-content"><div id="mod-list"></div><div id="agent-sidebar" style="margin-top:12px"></div></div>
360
+ </div>
361
+
362
+ <!-- Right Panel -->
363
+ <div class="right-panel" id="right-panel">
364
+ <div class="panel-tabs">
365
+ <button class="tab active" data-tab="log"><svg viewBox="0 0 16 16" fill="none"><path d="M2 3h12M2 7h8M2 11h10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>Log</button>
366
+ <button class="tab" data-tab="files"><svg viewBox="0 0 16 16" fill="none"><path d="M3 1h6l4 4v10H3z" stroke="currentColor" stroke-width="1.2"/><path d="M9 1v4h4" stroke="currentColor" stroke-width="1" opacity="0.5"/></svg>Tests</button>
367
+ <button class="tab" data-tab="results"><svg viewBox="0 0 16 16" fill="none"><polyline points="2,12 5,6 9,10 14,3" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>Results</button>
368
+ <button class="tab" data-tab="reports"><svg viewBox="0 0 16 16" fill="none"><rect x="2" y="2" width="12" height="12" rx="1" stroke="currentColor" stroke-width="1.2"/><path d="M5 10V7M8 10V5M11 10V8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>Reports</button>
369
+ </div>
370
+ <div class="panel-content" id="log-list"></div>
371
+ <div class="panel-content hidden" id="file-list"></div>
372
+ <div class="panel-content hidden" id="results-panel"></div>
373
+ <div class="panel-content hidden" id="reports-panel"></div>
411
374
  </div>
412
375
 
376
+ <!-- Bottom Bar -->
377
+ <div class="bottom-bar" id="bottom-bar">
378
+ <div class="bottom-bar-header" id="bottom-bar-toggle"><h3>Agent Desks</h3><svg class="toggle-icon" width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 10l4-4 4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
379
+ <div class="desk-row" id="desk-row"></div>
380
+ </div>
381
+
382
+ <!-- File Preview -->
413
383
  <div class="file-preview" id="file-preview">
414
- <div class="fp-header"><h4 id="fp-title">file.spec.ts</h4><button class="fp-close" id="fp-close">&times;</button></div>
415
- <pre id="fp-code"></pre>
384
+ <div class="backdrop" id="fp-backdrop"></div>
385
+ <div class="fp-dialog"><div class="fp-header"><span class="fp-title" id="fp-title">File Preview</span><button class="fp-close" id="fp-close"><svg viewBox="0 0 16 16" width="14" height="14" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button></div><pre class="fp-code" id="fp-code"></pre></div>
386
+ </div>
387
+
388
+ <!-- Tooltip -->
389
+ <div class="tooltip" id="tooltip"><div class="tt-name"></div><div class="tt-module"></div><div class="tt-type"></div></div>
390
+
391
+ <!-- Shortcut Legend -->
392
+ <div class="shortcut-legend" id="shortcut-legend">
393
+ <kbd>1</kbd><span>3D Office</span><kbd>2</kbd><span>Graph View</span>
394
+ <kbd>S</kbd><span>Scan</span><kbd>P</kbd><span>Pipeline</span>
395
+ <kbd>T</kbd><span>Run Tests</span><kbd>R</kbd><span>Reports</span>
396
+ <kbd>X</kbd><span>Reset</span><kbd>D</kbd><span>Dark/Light</span>
397
+ <kbd>?</kbd><span>Shortcuts</span><kbd>Esc</kbd><span>Close</span>
416
398
  </div>
417
399
 
418
- <script>
419
- const S = {
420
- project:null, graph:{nodes:[],edges:[]}, agents:[], ws:null,
421
- pan:{x:0,y:0}, zoom:1, dragging:false, dragStart:{x:0,y:0},
422
- nodePos:new Map(), hoveredNode:null, running:false, _userPanned:false,
423
- generatedFiles:[], testMetrics:null, testQuality:null, reports:[], runMode:'auto',
424
- currentView:'dashboard'
400
+ <!-- Toast Container -->
401
+ <div class="toast-container" id="toast-container"></div>
402
+
403
+ <!-- Import Map -->
404
+ <script type="importmap">{"imports":{"three":"https://unpkg.com/three@0.170.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.170.0/examples/jsm/"}}</script>
405
+
406
+ <!-- Main App -->
407
+ <script type="module">
408
+ import { createEngine, resizeEngine, getRenderer, getScene, getCamera, getComposer, getClock } from './js/engine.js';
409
+ import { createOffice, updateOfficeLighting, getFloorY } from './js/office.js';
410
+ import { AgentManager } from './js/agents.js';
411
+ import { ParticleManager } from './js/effects.js';
412
+ import { CameraController } from './js/camera.js';
413
+ import { GraphViz, HologramDisplay } from './js/dataviz.js';
414
+ import { StateManager } from './js/state.js';
415
+ import { UIManager } from './js/ui.js';
416
+
417
+ /* โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
418
+ const ICONS={
419
+ 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>',
420
+ 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>',
421
+ 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>',
422
+ 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>',
423
+ 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>',
424
+ 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>',
425
+ 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>',
425
426
  };
427
+ const ROLE_ICONS={parser:ICONS.parser,analyzer:ICONS.analyzer,tester:ICONS.tester,healer:ICONS.healer,planner:ICONS.planner,reporter:ICONS.reporter};
428
+ const BUBBLE_TEXTS={
429
+ working:['ๆญฃๅœจๆ‰ง่กŒ...','ๅฟซไบ†ๅฟซไบ†','ๅค„็†ไธญ...','ๅŠ ๆฒน ๐Ÿ’ช'],testing:['่ท‘ๆต‹่ฏ•ไธญ...','้ชŒ่ฏ API...','็ญ‰็ป“ๆžœ...'],
430
+ thinking:['่ฎฉๆˆ‘ๆƒณๆƒณ...','ๅˆ†ๆžไธญ...','ๆŽจ็†...','๐Ÿค”'],error:['ๅ‡บ้”™ไบ†!','ไฟฎๅคไธญ...','็ณŸ็ณ•...','ๆŽ’ๆŸฅ้—ฎ้ข˜...'],
431
+ idle:['ๆ‘ธ้ฑผไธญ~','็ญ‰ไปปๅŠก...','โ˜• ๅ–ๅ’–ๅ•ก','zzZ'],done:['ๆžๅฎš!','ๅฎŒๆˆ โœ“','ไธ‹ไธ€ไธช!'],
432
+ passed:['ๅ…จ็ปฟ โœ“','ๆต‹่ฏ•้€š่ฟ‡!'],failed:['ๆœ‰ๅคฑ่ดฅ...','้œ€่ฆไฟฎๅค'],
433
+ };
434
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
426
435
 
427
- async function fetchProject(){
428
- try{
429
- const r=await fetch('/api/project'); S.project=await r.json();
430
- S.graph=S.project.graph||{nodes:[],edges:[]}; S.agents=S.project.agents||[];
431
- layoutGraph(); updateAll();
432
- }catch(e){addLog('Failed to load: '+e.message,'error');}
433
- }
434
- async function doScan(){
435
- if(S.running)return; S.running=true; updateBtns();
436
- addLog('๐Ÿ” Starting scan...');
437
- try{await fetch('/api/scan',{method:'POST'});}
438
- catch(e){addLog('Scan failed: '+e.message,'error');S.running=false;updateBtns();}
439
- }
440
- async function doPipeline(){
441
- if(S.running)return; S.running=true; updateBtns();
442
- addLog('โ–ถ Starting pipeline...');
443
- try{await fetch('/api/pipeline',{method:'POST'});}
444
- catch(e){addLog('Pipeline failed: '+e.message,'error');S.running=false;updateBtns();}
445
- }
446
- async function doReset(){
447
- try{await fetch('/api/reset',{method:'POST'});}catch(e){addLog('Reset failed','error');}
448
- S.running=false; updateBtns(); addLog('โน Agents reset');
449
- }
450
- async function doRunTests(){
451
- if(S.running)return; S.running=true; updateBtns();
452
- addLog('๐Ÿงช Starting test execution ('+S.runMode+')...');
453
- try{
454
- const r=await fetch('/api/run-tests',{
455
- method:'POST',
456
- headers:{'content-type':'application/json'},
457
- body:JSON.stringify({mode:S.runMode}),
458
- });
459
- if(!r.ok){
460
- const body=await r.json().catch(()=>({error:'unknown error'}));
461
- throw new Error(body.error||('HTTP '+r.status));
462
- }
463
- }
464
- catch(e){addLog('Run tests failed: '+e.message,'error');S.running=false;updateBtns();}
465
- }
466
- async function doReports(){
467
- if(S.running)return; S.running=true; updateBtns();
468
- addLog('๐Ÿ“Š Generating reports...');
469
- try{await fetch('/api/reports/generate',{method:'POST'});}
470
- catch(e){addLog('Report gen failed: '+e.message,'error');S.running=false;updateBtns();}
471
- }
472
- function updateBtns(){
473
- document.getElementById('btn-scan').disabled=S.running;
474
- document.getElementById('btn-pipeline').disabled=S.running;
475
- document.getElementById('run-mode').disabled=S.running||S.generatedFiles.length===0;
476
- document.getElementById('btn-run-tests').disabled=S.running||S.generatedFiles.length===0;
477
- document.getElementById('btn-reports').disabled=S.running||S.generatedFiles.length===0;
478
- }
436
+ /* โ”€โ”€โ”€ Managers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
437
+ const state=new StateManager();
438
+ const ui=new UIManager(state,{ICONS,ROLE_ICONS,BUBBLE_TEXTS,esc});
439
+ let agentMgr, particleMgr, camCtrl, graphViz, hologram;
479
440
 
480
- function connectWS(){
481
- const p=location.protocol==='https:'?'wss:':'ws:';
482
- S.ws=new WebSocket(p+'//'+location.host+'/ws');
483
- S.ws.onopen=()=>{document.getElementById('conn-dot').classList.add('on');};
484
- S.ws.onmessage=(e)=>{
485
- try{
486
- const m=JSON.parse(e.data);
487
- if(m.type==='agent:update'&&Array.isArray(m.payload)){
488
- S.agents=m.payload; renderOffice(); renderAgentSB(); renderPixelOffice();
489
- }else if(m.type==='graph:update'){
490
- S.graph=m.payload; layoutGraph(); renderCanvas(); renderModList(); updateStats();
491
- }else if(m.type==='log'){
492
- addLog(m.payload.message, m.payload.level);
493
- }else if(m.type==='files:generated'){
494
- S.generatedFiles=m.payload||[];
495
- renderFileList();
496
- document.getElementById('s-files').textContent=S.generatedFiles.length;
497
- const badge=document.getElementById('file-badge');
498
- badge.textContent=S.generatedFiles.length;badge.style.display='inline';
499
- }else if(m.type==='pipeline:complete'){
500
- S.running=false; updateBtns();
501
- if(m.payload.status==='success') addLog('โœ… Pipeline complete!');
502
- else addLog('โŒ Pipeline failed: '+(m.payload.error||''),'error');
503
- setTimeout(fetchProject,500);
504
- }else if(m.type==='test:complete'){
505
- S.running=false; S.testMetrics=m.payload.metrics||null; S.testQuality=m.payload.quality||null; updateBtns(); renderResults();
506
- const met=m.payload.metrics;
507
- document.getElementById('s-results-wrap').style.display='';
508
- if(met){
509
- document.getElementById('s-results').textContent=met.passed+'โœ“ '+met.failed+'โœ—';
510
- document.getElementById('s-results').style.color=met.failed>0?'var(--red)':'var(--accent)';
511
- }else{
512
- document.getElementById('s-results').textContent='setup fail';
513
- document.getElementById('s-results').style.color='var(--red)';
514
- }
515
- const rb=document.getElementById('result-badge');
516
- rb.textContent=m.payload.total||0; rb.style.display='inline';
517
- rb.style.background=(met&&met.failed>0)?'var(--red)':'var(--accent)';
518
- }else if(m.type==='reports:generated'){
519
- S.running=false; S.reports=m.payload||[]; updateBtns(); renderReports();
520
- addLog('๐Ÿ“Š '+S.reports.length+' reports generated');
521
- }
522
- }catch{}
523
- };
524
- S.ws.onclose=()=>{document.getElementById('conn-dot').classList.remove('on');setTimeout(connectWS,3000);};
525
- }
441
+ /* โ”€โ”€โ”€ Boot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
442
+ async function boot(){
443
+ ui.setLoading(5,'Creating stateโ€ฆ');
444
+ state.set({project:null,graph:{nodes:[],edges:[]},agents:[],ws:null,running:false,
445
+ generatedFiles:[],testMetrics:null,testQuality:null,reports:[],runMode:'auto',
446
+ currentView:'3d',theme:localStorage.getItem('opencroc-theme')||'light',modMeta:new Map(),nodePos:new Map()});
526
447
 
527
- function addLog(msg,level){
528
- level=level||'info';
529
- const el=document.getElementById('log-list');
530
- const d=document.createElement('div'); d.className='log-entry '+level;
531
- d.textContent='['+new Date().toLocaleTimeString()+'] '+msg;
532
- el.appendChild(d); el.scrollTop=el.scrollHeight;
533
- }
534
- function updateStats(){
535
- document.getElementById('s-mod').textContent=S.graph.nodes.filter(n=>n.type==='module').length;
536
- document.getElementById('s-mdl').textContent=S.graph.nodes.filter(n=>n.type==='model').length;
537
- document.getElementById('s-api').textContent=S.graph.nodes.filter(n=>n.type==='controller'||n.type==='api').length;
538
- }
448
+ const theme=state.get('theme');
449
+ document.documentElement.setAttribute('data-theme',theme);
450
+ if(theme==='light'){document.getElementById('theme-icon-dark').style.display='none';document.getElementById('theme-icon-light').style.display=''}
539
451
 
540
- function layoutGraph(){
541
- const nodes=S.graph.nodes; if(!nodes.length)return;
542
- const c=document.getElementById('graph-canvas');
543
- const w=c.clientWidth||800,h=c.clientHeight||500;
544
- // Group non-module nodes by module
545
- const mods=new Map();
546
- S.modMeta=new Map();
547
- for(const n of nodes){
548
- if(n.type==='module') continue;
549
- const m=n.module||'other';
550
- if(!mods.has(m)) mods.set(m,[]);
551
- mods.get(m).push(n);
552
- }
553
- // Sort by size descending
554
- const keys=[...mods.keys()].sort((a,b)=>(mods.get(b).length-mods.get(a).length));
555
- const nMods=keys.length||1;
556
- const palette=['#4ecca3','#e94560','#f39c12','#3498db','#9b59b6','#1abc9c','#e67e22','#2ecc71','#e84393','#00cec9'];
557
- const RING_CAP=24; // max nodes per ring layer
558
-
559
- // Calculate each module's outer radius (for spacing)
560
- const modRadii=[];
561
- for(let i=0;i<keys.length;i++){
562
- const cnt=mods.get(keys[i]).length;
563
- const rings=Math.ceil(cnt/RING_CAP);
564
- const outerR=rings===1?Math.max(40,cnt*6):rings*32+20;
565
- modRadii.push(outerR);
566
- }
452
+ ui.setLoading(10,'Initializing 3D engineโ€ฆ');
453
+ const canvas=document.getElementById('three-canvas');
454
+ await createEngine(canvas,theme);
567
455
 
568
- // Adaptive base ring: sum of all module diameters / (2*PI) gives minimum perimeter
569
- const totalDiam=modRadii.reduce((s,r)=>s+r*2+60,0); // 60px gap between modules
570
- const baseRadius=Math.max(400, totalDiam/(Math.PI*2));
571
-
572
- // Place modules with angular spacing proportional to their size
573
- const totalWeight=modRadii.reduce((s,r)=>s+r+30,0);
574
- let angle=-Math.PI/2;
575
- for(let i=0;i<keys.length;i++){
576
- const mn=mods.get(keys[i]); if(!mn)continue;
577
- const cnt=mn.length;
578
- const col=palette[i%palette.length];
579
- const rings=Math.ceil(cnt/RING_CAP);
580
- const outerR=modRadii[i];
581
-
582
- // Angular span for this module proportional to its size
583
- const span=((outerR+30)/totalWeight)*Math.PI*2;
584
- const modAngle=angle+span/2;
585
- angle+=span;
586
-
587
- const mcx=w/2+Math.cos(modAngle)*baseRadius;
588
- const mcy=h/2+Math.sin(modAngle)*baseRadius;
589
-
590
- const modNodeId='module:'+keys[i];
591
- S.nodePos.set(modNodeId,{x:mcx,y:mcy});
592
- S.modMeta.set(keys[i],{cx:mcx,cy:mcy,radius:outerR+20,color:col,count:cnt});
593
-
594
- // Spread nodes across concentric rings
595
- let placed=0;
596
- for(let ring=0;ring<rings;ring++){
597
- const nodesInRing=Math.min(RING_CAP,cnt-placed);
598
- const r=rings===1?Math.max(40,nodesInRing*6):(ring+1)*32;
599
- for(let j=0;j<nodesInRing;j++){
600
- const na=(j/nodesInRing)*Math.PI*2;
601
- S.nodePos.set(mn[placed].id,{x:mcx+Math.cos(na)*r,y:mcy+Math.sin(na)*r});
602
- placed++;
603
- }
604
- }
605
- }
456
+ ui.setLoading(25,'Building officeโ€ฆ');
457
+ await createOffice(theme);
606
458
 
607
- // Auto-center
608
- if(nodes.length>0&&!S._userPanned){
609
- let minX=Infinity,maxX=-Infinity,minY=Infinity,maxY=-Infinity;
610
- for(const[,p] of S.nodePos){minX=Math.min(minX,p.x);maxX=Math.max(maxX,p.x);minY=Math.min(minY,p.y);maxY=Math.max(maxY,p.y);}
611
- const gw=maxX-minX+300,gh=maxY-minY+300,gcx=minX+(maxX-minX)/2,gcy=minY+(maxY-minY)/2;
612
- S.zoom=Math.min(1, Math.min(w/gw, h/gh));
613
- S.pan.x=w/2-gcx*S.zoom;S.pan.y=h/2-gcy*S.zoom;
614
- }
615
- }
459
+ ui.setLoading(40,'Setting up cameraโ€ฆ');
460
+ camCtrl=new CameraController(canvas,getCamera(),getScene());
616
461
 
617
- function renderCanvas(){
618
- const canvas=document.getElementById('graph-canvas'),ctx=canvas.getContext('2d');
619
- const dpr=window.devicePixelRatio||1;
620
- canvas.width=canvas.clientWidth*dpr; canvas.height=canvas.clientHeight*dpr;
621
- ctx.scale(dpr,dpr);
622
- const w=canvas.clientWidth,h=canvas.clientHeight;
623
- ctx.clearRect(0,0,w,h); ctx.save(); ctx.translate(S.pan.x,S.pan.y); ctx.scale(S.zoom,S.zoom);
624
- // Subtle grid
625
- ctx.strokeStyle='#151530';ctx.lineWidth=.5;
626
- const gridStep=40;
627
- for(let x=-2000;x<4000;x+=gridStep){ctx.beginPath();ctx.moveTo(x,-2000);ctx.lineTo(x,4000);ctx.stroke();}
628
- for(let y=-2000;y<4000;y+=gridStep){ctx.beginPath();ctx.moveTo(-2000,y);ctx.lineTo(4000,y);ctx.stroke();}
629
-
630
- const edges=S.graph.edges||[],nodes=S.graph.nodes||[];
631
- const largeGraph=nodes.length>80;
632
-
633
- // Draw module cluster backgrounds
634
- if(S.modMeta){
635
- for(const[name,m] of S.modMeta){
636
- ctx.beginPath(); ctx.arc(m.cx,m.cy,m.radius,0,Math.PI*2);
637
- ctx.fillStyle=m.color+'10'; ctx.fill();
638
- ctx.strokeStyle=m.color+'30'; ctx.lineWidth=2; ctx.setLineDash([6,4]); ctx.stroke(); ctx.setLineDash([]);
639
- // Module label at top of cluster
640
- ctx.font='bold 13px "Courier New"'; ctx.fillStyle=m.color+'cc'; ctx.textAlign='center'; ctx.textBaseline='bottom';
641
- ctx.fillText(name+' ('+m.count+')',m.cx,m.cy-m.radius-6);
642
- }
643
- }
462
+ ui.setLoading(50,'Creating agentsโ€ฆ');
463
+ agentMgr=new AgentManager(getScene());
644
464
 
645
- // Draw "uses" edges with curved lines + gradient
646
- for(const e of edges){
647
- if(e.relation==='contains') continue; // never draw contains
648
- const s=S.nodePos.get(e.source),t=S.nodePos.get(e.target);if(!s||!t)continue;
649
- const dx=t.x-s.x,dy=t.y-s.y,dist=Math.sqrt(dx*dx+dy*dy);
650
- // Curved edge via quadratic bezier, perpendicular offset
651
- const mx=(s.x+t.x)/2+dy*0.15, my=(s.y+t.y)/2-dx*0.15;
652
- const grad=ctx.createLinearGradient(s.x,s.y,t.x,t.y);
653
- grad.addColorStop(0,'rgba(233,69,96,.5)'); // controller (red)
654
- grad.addColorStop(1,'rgba(78,204,163,.5)'); // model (green)
655
- ctx.strokeStyle=grad;ctx.lineWidth=2;
656
- ctx.beginPath();ctx.moveTo(s.x,s.y);ctx.quadraticCurveTo(mx,my,t.x,t.y);ctx.stroke();
657
- // Arrowhead
658
- const t2=0.95,at2x=(1-t2)*(1-t2)*s.x+2*(1-t2)*t2*mx+t2*t2*t.x,at2y=(1-t2)*(1-t2)*s.y+2*(1-t2)*t2*my+t2*t2*t.y;
659
- const a=Math.atan2(t.y-at2y,t.x-at2x),al=8;
660
- ctx.fillStyle='rgba(78,204,163,.6)';ctx.beginPath();ctx.moveTo(t.x,t.y);
661
- ctx.lineTo(t.x-al*Math.cos(a-.4),t.y-al*Math.sin(a-.4));
662
- ctx.lineTo(t.x-al*Math.cos(a+.4),t.y-al*Math.sin(a+.4));ctx.closePath();ctx.fill();
663
- }
465
+ ui.setLoading(60,'Creating effectsโ€ฆ');
466
+ particleMgr=new ParticleManager(getScene());
664
467
 
665
- // Draw nodes
666
- const tc={model:'#4ecca3',controller:'#e94560',api:'#f39c12',dto:'#3498db',module:'#9b59b6'};
667
- const sc={idle:'#444',testing:'#f39c12',passed:'#4ecca3',failed:'#e94560'};
668
- for(const n of nodes){
669
- if(n.type==='module') continue; // drawn as cluster bg instead
670
- const p=S.nodePos.get(n.id);if(!p)continue;
671
- const sz=10,c=tc[n.type]||'#888',ol=sc[n.status]||'#444',hov=S.hoveredNode===n.id;
672
- // Glow for active status
673
- if(n.status==='testing'){ctx.shadowColor=sc.testing;ctx.shadowBlur=14;}
674
- else if(n.status==='passed'){ctx.shadowColor=sc.passed;ctx.shadowBlur=10;}
675
- else if(n.status==='failed'){ctx.shadowColor=sc.failed;ctx.shadowBlur=12;}
676
- // Node circle instead of rect for cleaner look
677
- ctx.beginPath();ctx.arc(p.x,p.y,sz,0,Math.PI*2);
678
- ctx.fillStyle=c;ctx.fill();
679
- ctx.shadowBlur=0;
680
- ctx.strokeStyle=hov?'#fff':ol;ctx.lineWidth=hov?3:1.5;ctx.stroke();
681
- // Icon
682
- ctx.font=(sz)+'px serif';ctx.textAlign='center';ctx.textBaseline='middle';
683
- ctx.fillText(n.type==='model'?'๐Ÿ“ฆ':'๐ŸŽฎ',p.x,p.y);
684
- // Label โ€” show at higher zoom or on hover
685
- if(S.zoom>0.4||hov){
686
- ctx.font='9px "Courier New"';ctx.fillStyle=hov?'#fff':'#aaa';ctx.textAlign='center';ctx.textBaseline='top';
687
- ctx.fillText((n.label||n.id.split(':').pop()).substring(0,18),p.x,p.y+sz+3);
688
- }
689
- }
690
- ctx.restore();
468
+ ui.setLoading(70,'Creating data visualizationโ€ฆ');
469
+ graphViz=new GraphViz(getScene());
470
+ hologram=new HologramDisplay(getScene());
691
471
 
692
- // HUD legend
693
- ctx.font='10px "Courier New"';ctx.textAlign='left';
694
- const leg=[['๐Ÿ“ฆ Model','#4ecca3'],['๐ŸŽฎ Controller','#e94560'],['โ”€โ”€โ”€ uses','rgba(200,100,130,.8)']];
695
- for(let i=0;i<leg.length;i++){ctx.fillStyle=leg[i][1];ctx.fillText(leg[i][0],10,h-40+i*14);}
696
- }
472
+ ui.setLoading(80,'Connectingโ€ฆ');
473
+ ui.init({doScan,doPipeline,doReset,doRunTests,doReports,toggleTheme,setView,openFilePreview,openReportPreview});
474
+ await fetchProject();
697
475
 
698
- function setupCanvas(){
699
- const c=document.getElementById('graph-canvas');
700
- c.addEventListener('mousedown',e=>{S.dragging=true;S._userPanned=true;S.dragStart={x:e.clientX-S.pan.x,y:e.clientY-S.pan.y};c.style.cursor='grabbing';});
701
- c.addEventListener('mousemove',e=>{
702
- if(S.dragging){S.pan.x=e.clientX-S.dragStart.x;S.pan.y=e.clientY-S.dragStart.y;renderCanvas();}
703
- const rect=c.getBoundingClientRect(),mx=(e.clientX-rect.left-S.pan.x)/S.zoom,my=(e.clientY-rect.top-S.pan.y)/S.zoom;
704
- let hit=null;
705
- for(const n of S.graph.nodes){const p=S.nodePos.get(n.id);if(!p)continue;const sz=n.type==='module'?22:14;if(mx>=p.x-sz&&mx<=p.x+sz&&my>=p.y-sz&&my<=p.y+sz){hit=n;break;}}
706
- const tt=document.getElementById('tooltip');
707
- if(hit){
708
- S.hoveredNode=hit.id;
709
- const sc={idle:'#888',testing:'#f39c12',passed:'#4ecca3',failed:'#e94560'};
710
- tt.innerHTML='<b>'+esc(hit.label||hit.id)+'</b><br>Type: '+hit.type+'<br>Status: <span style="color:'+(sc[hit.status]||'#888')+'">'+hit.status+'</span>'+(hit.module?'<br>Module: '+esc(hit.module):'');
711
- tt.style.left=(e.clientX+12)+'px';tt.style.top=(e.clientY+12)+'px';tt.classList.add('visible');c.style.cursor='pointer';
712
- }else{if(S.hoveredNode)S.hoveredNode=null;tt.classList.remove('visible');if(!S.dragging)c.style.cursor='grab';}
713
- renderCanvas();
714
- });
715
- c.addEventListener('mouseup',()=>{S.dragging=false;c.style.cursor='grab';});
716
- c.addEventListener('mouseleave',()=>{S.dragging=false;document.getElementById('tooltip').classList.remove('visible');});
717
- c.addEventListener('wheel',e=>{e.preventDefault();S.zoom=Math.max(.2,Math.min(3,S.zoom*(e.deltaY>0?.92:1.08)));renderCanvas();},{passive:false});
718
- }
476
+ ui.setLoading(90,'Starting WebSocketโ€ฆ');
477
+ connectWS();
719
478
 
720
- function updateAll(){
721
- if(S.project){
722
- document.getElementById('s-mod').textContent=S.project.stats?.modules||0;
723
- document.getElementById('s-mdl').textContent=S.project.stats?.models||0;
724
- document.getElementById('s-api').textContent=S.project.stats?.endpoints||0;
725
- }
726
- renderModList();renderOffice();renderAgentSB();renderPixelOffice();renderCanvas();
727
- }
728
- function renderModList(){
729
- const el=document.getElementById('mod-list'),mods=S.graph.nodes.filter(n=>n.type==='module');
730
- if(!mods.length){el.innerHTML='<div style="padding:8px;color:#555;font-size:10px">No modules found</div>';return;}
731
- // Sort by member count descending
732
- const sorted=mods.sort((a,b)=>{
733
- const ca=S.modMeta&&S.modMeta.get(a.label)?S.modMeta.get(a.label).count:0;
734
- const cb=S.modMeta&&S.modMeta.get(b.label)?S.modMeta.get(b.label).count:0;
735
- return cb-ca;
736
- });
737
- el.innerHTML=sorted.map(m=>{
738
- const meta=S.modMeta&&S.modMeta.get(m.label);
739
- const cnt=meta?meta.count:'';
740
- const col=meta?meta.color:'#888';
741
- return '<div class="mod-item" data-mod="'+esc(m.label)+'" style="border-left:3px solid '+col+';padding-left:6px">'
742
- +'<div class="dot '+m.status+'"></div>'
743
- +esc(m.label)+' <span style="color:#555;font-size:9px">('+cnt+')</span></div>';
744
- }).join('');
745
- // Click to navigate
746
- el.querySelectorAll('.mod-item').forEach(item=>{
747
- item.addEventListener('click',()=>{
748
- const name=item.getAttribute('data-mod');
749
- const meta=S.modMeta&&S.modMeta.get(name);
750
- if(meta){
751
- const c=document.getElementById('graph-canvas');
752
- S.zoom=0.8;S._userPanned=true;
753
- S.pan.x=c.clientWidth/2-meta.cx*S.zoom;
754
- S.pan.y=c.clientHeight/2-meta.cy*S.zoom;
755
- renderCanvas();
756
- }
757
- });
758
- });
479
+ ui.setLoading(100,'Ready!');
480
+ setTimeout(()=>document.getElementById('loading-overlay').classList.add('hidden'),400);
481
+
482
+ requestAnimationFrame(renderLoop);
483
+ ui.addLog('OpenCroc Studio 3D ready โ€” press ? for shortcuts','info',true);
759
484
  }
760
- function renderAgentSB(){
761
- document.getElementById('agent-sidebar').innerHTML=S.agents.map(a=>
762
- '<div class="mod-item"><div class="dot '+a.status+'"></div>'+esc(a.name)+' <span style="color:#555;font-size:9px">'+a.status+'</span></div>'
763
- ).join('');
485
+
486
+ /* โ”€โ”€โ”€ Render Loop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
487
+ function renderLoop(){
488
+ requestAnimationFrame(renderLoop);
489
+ const dt=getClock().getDelta();
490
+ if(dt>0.1) return;
491
+ if(camCtrl) camCtrl.update(dt);
492
+ if(particleMgr) particleMgr.update(dt);
493
+ if(agentMgr) agentMgr.update(dt);
494
+ if(hologram) hologram.update(dt,state.get('graph'));
495
+ const composer=getComposer();
496
+ if(composer) composer.render(dt); else getRenderer().render(getScene(),getCamera());
764
497
  }
765
- function renderOffice(){
766
- const dd={parser:'๐Ÿ’ป',analyzer:'๐Ÿ“Š',tester:'๐Ÿงช',healer:'๐Ÿ”ง',planner:'๐Ÿ“‹',reporter:'๐Ÿ“'};
767
- document.getElementById('croc-office').innerHTML=S.agents.map(a=>{
768
- const prog=typeof a.progress==='number'?a.progress:0;
769
- return '<div class="desk '+a.status+'"><div class="badge dot '+a.status+'"></div>'+
770
- '<div class="croc-sprite">๐ŸŠ</div><div class="croc-name">'+esc(a.name)+'</div>'+
771
- '<div class="croc-role">'+esc(a.role)+'</div>'+
772
- '<div class="croc-task">'+(a.currentTask?esc(a.currentTask):'')+'</div>'+
773
- '<div class="progress-bar"><div class="fill" style="width:'+prog+'%"></div></div>'+
774
- '<div class="desk-items">'+(dd[a.role]||'')+'</div></div>';
775
- }).join('');
498
+
499
+ /* โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
500
+ async function fetchProject(){
501
+ try{const r=await fetch('/api/project');const d=await r.json();
502
+ state.set({project:d,graph:d.graph||state.get('graph'),agents:d.agents||state.get('agents')});
503
+ updateAll();
504
+ }catch(e){ui.addLog('Failed to fetch project: '+e.message,'error')}
776
505
  }
777
- function renderPixelOffice(){
778
- const el=document.getElementById('pixel-agent-layer');
779
- const stageWidth=(document.getElementById('pixel-view').clientWidth||900)-80;
780
- const stageHeight=(document.getElementById('pixel-view').clientHeight||520)-120;
781
- const presets=[
782
- {x:.18,y:.64},{x:.3,y:.66},{x:.42,y:.62},{x:.56,y:.6},{x:.72,y:.63},{x:.82,y:.52},
783
- {x:.22,y:.45},{x:.48,y:.42},{x:.66,y:.4},{x:.78,y:.72}
784
- ];
785
- const roles=['char_0','char_1','char_2'];
786
- const working=S.agents.filter(a=>a.status==='working'||a.status==='testing').length;
787
- const errors=S.agents.filter(a=>a.status==='error'||a.status==='failed').length;
788
- const done=S.agents.filter(a=>a.status==='done'||a.status==='passed').length;
789
- document.getElementById('kpi-working').textContent=String(working);
790
- document.getElementById('kpi-errors').textContent=String(errors);
791
- document.getElementById('kpi-done').textContent=String(done);
792
- if(!S.agents.length){el.innerHTML='';return;}
793
- el.innerHTML=S.agents.map((a,i)=>{
794
- const p=presets[i%presets.length];
795
- const x=Math.max(16,Math.round(stageWidth*p.x));
796
- const y=Math.max(20,Math.round(stageHeight*p.y));
797
- const roleSprite=roles[i%roles.length];
798
- const labelY=Math.max(16,y-12);
799
- return '<img class="pixel-agent '+a.status+'" src="./assets/botreview/'+roleSprite+'.png" style="left:'+x+'px;top:'+y+'px" alt="'+esc(a.name)+'" />'
800
- +'<div class="pixel-label" style="left:'+(x+22)+'px;top:'+labelY+'px">'+esc(a.name)+' ยท '+esc(a.status)+'</div>';
801
- }).join('');
506
+ async function doScan(){if(state.get('running'))return;state.set({running:true});updateBtns();ui.addLog('๐Ÿ” Starting codebase scanโ€ฆ','info',true);try{await fetch('/api/scan',{method:'POST'})}catch(e){ui.addLog('Scan error: '+e.message,'error');state.set({running:false});updateBtns()}}
507
+ async function doPipeline(){if(state.get('running'))return;state.set({running:true});updateBtns();ui.addLog('โšก Pipeline startedโ€ฆ','info',true);try{await fetch('/api/pipeline',{method:'POST'})}catch(e){ui.addLog('Pipeline error: '+e.message,'error');state.set({running:false});updateBtns()}}
508
+ async function doReset(){try{await fetch('/api/reset',{method:'POST'})}catch(e){ui.addLog('Reset error: '+e.message,'error')}state.set({running:false});updateBtns();ui.addLog('โ™ป๏ธ Agents reset','info',true)}
509
+ async function doRunTests(){if(state.get('running'))return;state.set({running:true});updateBtns();ui.addLog('๐Ÿงช Running tests (mode: '+state.get('runMode')+')โ€ฆ','info',true);try{const r=await fetch('/api/run-tests',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode:state.get('runMode')})});const d=await r.json();if(d.error){ui.addLog('Test error: '+d.error,'error');state.set({running:false});updateBtns()}}catch(e){ui.addLog('Test error: '+e.message,'error');state.set({running:false});updateBtns()}}
510
+ async function doReports(){if(state.get('running'))return;state.set({running:true});updateBtns();ui.addLog('๐Ÿ“Š Generating reportsโ€ฆ','info',true);try{await fetch('/api/reports/generate',{method:'POST'})}catch(e){ui.addLog('Report error: '+e.message,'error');state.set({running:false});updateBtns()}}
511
+
512
+ /* โ”€โ”€โ”€ WebSocket โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
513
+ function connectWS(){
514
+ const prot=location.protocol==='https:'?'wss:':'ws:';
515
+ const ws=new WebSocket(prot+'//'+location.host);
516
+ ws.onopen=()=>{document.getElementById('conn-dot').classList.add('connected');state.set({ws})};
517
+ ws.onclose=()=>{document.getElementById('conn-dot').classList.remove('connected');setTimeout(connectWS,3000)};
518
+ ws.onmessage=ev=>{try{handleWS(JSON.parse(ev.data))}catch(e){}};
802
519
  }
803
- function setView(view){
804
- S.currentView=view;
805
- const dashboard=view==='dashboard';
806
- document.getElementById('graph-view').classList.toggle('hidden',!dashboard);
807
- document.getElementById('pixel-view').classList.toggle('hidden',dashboard);
808
- document.getElementById('view-dashboard').classList.toggle('active',dashboard);
809
- document.getElementById('view-office').classList.toggle('active',!dashboard);
810
- document.getElementById('tooltip').classList.remove('visible');
811
- if(dashboard){
812
- setTimeout(()=>{layoutGraph();renderCanvas();},0);
813
- }else{
814
- renderPixelOffice();
520
+ function handleWS(msg){
521
+ switch(msg.type){
522
+ case'agent:update':state.set({agents:msg.data});agentMgr.sync(msg.data);ui.updateDeskCards(msg.data);ui.updateSidebar(null,msg.data);break;
523
+ case'graph:update':state.set({graph:msg.data});graphViz.update(msg.data);ui.updateSidebar(msg.data,null);ui.updateStats(msg.data);break;
524
+ case'log':ui.addLog(msg.data.message,msg.data.level);break;
525
+ case'files:generated':state.set({generatedFiles:msg.data});ui.updateFileList(msg.data);break;
526
+ case'pipeline:complete':state.set({running:false});updateBtns();
527
+ if(msg.data.error)ui.addLog('Pipeline failed: '+msg.data.error,'error');
528
+ else{ui.addLog('โœ… Pipeline complete','success',true);ui.showToast('Pipeline completed','success');particleMgr.triggerCelebration()}
529
+ fetchProject();break;
530
+ case'test:complete':state.set({running:false,testMetrics:msg.data.metrics,testQuality:msg.data.quality});updateBtns();
531
+ ui.updateResults(msg.data);ui.addLog(`Tests: ${msg.data.metrics?.passed||0}โœ“ ${msg.data.metrics?.failed||0}โœ—`,'info',true);
532
+ ui.showToast(`Tests: ${msg.data.metrics?.passed||0}โœ“ ${msg.data.metrics?.failed||0}โœ—`,msg.data.metrics?.failed?'warning':'success');break;
533
+ case'reports:generated':state.set({running:false,reports:msg.data});updateBtns();
534
+ ui.updateReports(msg.data);ui.addLog('๐Ÿ“Š '+msg.data.length+' reports generated','success',true);break;
815
535
  }
816
536
  }
817
- function esc(s){return s?s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'):'';}
818
537
 
538
+ /* โ”€โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
539
+ function updateAll(){
540
+ const g=state.get('graph'),a=state.get('agents');
541
+ ui.updateSidebar(g,a);ui.updateStats(g);ui.updateDeskCards(a);
542
+ if(a) agentMgr.sync(a);
543
+ if(g) graphViz.update(g);
544
+ }
545
+ function updateBtns(){const r=state.get('running');['btn-scan','btn-pipeline','btn-reset','btn-run-tests','btn-reports'].forEach(id=>document.getElementById(id).disabled=r)}
546
+ async function openFilePreview(idx){try{const r=await fetch('/api/files/'+idx);const d=await r.json();document.getElementById('fp-title').textContent=d.filePath||'File';document.getElementById('fp-code').textContent=d.content||'';document.getElementById('file-preview').classList.add('visible')}catch(e){ui.showToast('Failed to load file','error')}}
547
+ async function openReportPreview(fmt){try{const r=await fetch('/api/reports/'+fmt);const t=await r.text();if(fmt==='html'){const w=window.open('','_blank');if(w){w.document.write(t);w.document.close()}return}document.getElementById('fp-title').textContent='Report: '+fmt;document.getElementById('fp-code').textContent=t;document.getElementById('file-preview').classList.add('visible')}catch(e){ui.showToast('Failed to load report','error')}}
548
+ function toggleTheme(){const cur=state.get('theme');const nxt=cur==='dark'?'light':'dark';state.set({theme:nxt});document.documentElement.setAttribute('data-theme',nxt);localStorage.setItem('opencroc-theme',nxt);document.getElementById('theme-icon-dark').style.display=nxt==='dark'?'':'none';document.getElementById('theme-icon-light').style.display=nxt==='light'?'':'none';updateOfficeLighting(nxt)}
549
+ function setView(v){state.set({currentView:v});document.getElementById('view-3d').classList.toggle('active',v==='3d');document.getElementById('view-graph').classList.toggle('active',v==='graph');if(v==='3d')camCtrl.flyTo('office');else camCtrl.flyTo('graph')}
550
+
551
+ /* โ”€โ”€โ”€ Events โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
819
552
  document.getElementById('btn-scan').addEventListener('click',doScan);
820
553
  document.getElementById('btn-pipeline').addEventListener('click',doPipeline);
821
554
  document.getElementById('btn-reset').addEventListener('click',doReset);
822
555
  document.getElementById('btn-run-tests').addEventListener('click',doRunTests);
823
556
  document.getElementById('btn-reports').addEventListener('click',doReports);
824
- document.getElementById('run-mode').addEventListener('change',e=>{S.runMode=e.target.value;});
825
- document.getElementById('view-dashboard').addEventListener('click',()=>setView('dashboard'));
826
- document.getElementById('view-office').addEventListener('click',()=>setView('office'));
827
-
828
- // Tab switching
829
- document.querySelectorAll('.panel-tabs .tab').forEach(tab=>{
830
- tab.addEventListener('click',()=>{
831
- document.querySelectorAll('.panel-tabs .tab').forEach(t=>t.classList.remove('active'));
832
- tab.classList.add('active');
833
- const t=tab.getAttribute('data-tab');
834
- document.getElementById('log-list').style.display=t==='log'?'':'none';
835
- document.getElementById('file-list').style.display=t==='files'?'':'none';
836
- document.getElementById('results-panel').style.display=t==='results'?'':'none';
837
- document.getElementById('reports-panel').style.display=t==='reports'?'':'none';
838
- });
839
- });
840
-
841
- // File list rendering
842
- function renderFileList(){
843
- const el=document.getElementById('file-list');
844
- if(!S.generatedFiles.length){el.innerHTML='<div style="padding:12px;color:#555;font-size:10px">No test files generated yet. Run Pipeline first.</div>';return;}
845
- el.innerHTML=S.generatedFiles.map((f,i)=>
846
- '<div class="file-item" data-idx="'+i+'"><div class="fname">'+esc(f.filePath.split('/').pop()||f.filePath)+'</div>'+
847
- '<div class="fmeta">'+esc(f.module)+' / '+esc(f.chain)+' โ€” '+f.lines+' lines</div></div>'
848
- ).join('');
849
- el.querySelectorAll('.file-item').forEach(item=>{
850
- item.addEventListener('click',async()=>{
851
- const idx=item.getAttribute('data-idx');
852
- try{
853
- const r=await fetch('/api/files/'+idx); const file=await r.json();
854
- document.getElementById('fp-title').textContent=file.filePath;
855
- document.getElementById('fp-code').textContent=file.content;
856
- document.getElementById('file-preview').classList.add('visible');
857
- }catch(e){addLog('Failed to load file: '+e.message,'error');}
858
- });
859
- });
860
- }
861
-
862
- // File preview close
863
- document.getElementById('fp-close').addEventListener('click',()=>{
864
- document.getElementById('file-preview').classList.remove('visible');
865
- });
866
- document.addEventListener('keydown',e=>{
867
- if(e.key==='Escape') document.getElementById('file-preview').classList.remove('visible');
868
- });
869
-
870
- // Test Results rendering
871
- function renderResults(){
872
- const el=document.getElementById('results-panel');
873
- if(!S.testMetrics&&!S.testQuality){el.innerHTML='<div style="padding:12px;color:#555;font-size:10px">No test results yet. Run Tests first.</div>';return;}
874
- const q=S.testQuality;
875
- if(!S.testMetrics&&q){
876
- el.innerHTML='<div style="padding:10px">'
877
- +'<div style="font-size:13px;font-weight:bold;margin-bottom:8px">๐Ÿงช Test Execution Results</div>'
878
- +'<div style="padding:10px;border-radius:4px;background:#2d0a12;color:var(--red);font-size:10px;margin-bottom:10px">Setup failed before test execution.</div>'
879
- +'<div style="font-size:11px;margin-bottom:6px">Gate Level: <span style="color:'+(q.level==='fail'?'var(--red)':q.level==='warn'?'var(--orange)':'var(--accent)')+';font-weight:bold">'+q.level.toUpperCase()+'</span></div>'
880
- +'<div style="font-size:10px;color:var(--text-dim)">Auth: '+q.authStatus+' | Backend: '+q.backendStatus+'</div>'
881
- +'<div style="font-size:10px;color:var(--text-dim);margin-top:4px">Reasons: '+(q.reasons&&q.reasons.length?q.reasons.join(', '):'-')+'</div>'
882
- +'</div>';
883
- return;
884
- }
885
- const m=S.testMetrics, total=m.passed+m.failed+m.skipped+m.timedOut;
886
- const passRate=total>0?Math.round(m.passed/total*100):0;
887
- const barColor=m.failed>0?'var(--red)':'var(--accent)';
888
- let qualityHtml='';
889
- if(q){
890
- const gateColor=q.level==='fail'?'var(--red)':q.level==='warn'?'var(--orange)':'var(--accent)';
891
- qualityHtml='<div style="margin-top:12px;padding-top:10px;border-top:1px solid #2a2a45">'
892
- +'<div style="font-size:12px;font-weight:bold;margin-bottom:8px">๐Ÿงญ Execution Quality</div>'
893
- +'<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:10px">'
894
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Gate: <span style="color:'+gateColor+';font-weight:bold">'+q.level.toUpperCase()+'</span></div>'
895
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Setup Fail: '+q.setupFail+'</div>'
896
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Skip Ratio: '+Math.round((q.skipRatio||0)*100)+'%</div>'
897
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Auth Fail Ratio: '+Math.round((q.authFailRatio||0)*100)+'%</div>'
898
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Effective Rate: '+Math.round((q.effectiveExecutionRate||0)*100)+'%</div>'
899
- +'<div style="background:#1a1a2e;padding:6px;border-radius:4px">Auth/Backend: '+q.authStatus+' / '+q.backendStatus+'</div>'
900
- +'</div>'
901
- +'<div style="font-size:10px;color:var(--text-dim);margin-top:6px">Reasons: '+(q.reasons&&q.reasons.length?q.reasons.join(', '):'-')+'</div>'
902
- +'</div>';
903
- }
904
- el.innerHTML='<div style="padding:10px">'
905
- +'<div style="font-size:13px;font-weight:bold;margin-bottom:8px">๐Ÿงช Test Execution Results</div>'
906
- +'<div style="display:flex;gap:12px;margin-bottom:10px">'
907
- +'<div style="flex:1;text-align:center;background:#0d2818;padding:8px;border-radius:4px"><div style="font-size:20px;color:var(--accent)">'+m.passed+'</div><div style="font-size:9px;color:var(--text-dim)">PASSED</div></div>'
908
- +'<div style="flex:1;text-align:center;background:#2d0a12;padding:8px;border-radius:4px"><div style="font-size:20px;color:var(--red)">'+m.failed+'</div><div style="font-size:9px;color:var(--text-dim)">FAILED</div></div>'
909
- +'<div style="flex:1;text-align:center;background:#1a1a2e;padding:8px;border-radius:4px"><div style="font-size:20px;color:var(--orange)">'+m.skipped+'</div><div style="font-size:9px;color:var(--text-dim)">SKIPPED</div></div>'
910
- +'<div style="flex:1;text-align:center;background:#1a1a2e;padding:8px;border-radius:4px"><div style="font-size:20px;color:var(--blue)">'+m.timedOut+'</div><div style="font-size:9px;color:var(--text-dim)">TIMEOUT</div></div>'
911
- +'</div>'
912
- +'<div style="background:#222;border-radius:3px;height:8px;overflow:hidden">'
913
- +'<div style="height:100%;width:'+passRate+'%;background:'+barColor+';transition:width .5s"></div></div>'
914
- +'<div style="text-align:center;font-size:10px;color:var(--text-dim);margin-top:4px">Pass Rate: '+passRate+'% ('+total+' total)</div>'
915
- +qualityHtml
916
- +'</div>';
917
- }
918
-
919
- // Reports rendering
920
- function renderReports(){
921
- const el=document.getElementById('reports-panel');
922
- if(!S.reports.length){el.innerHTML='<div style="padding:12px;color:#555;font-size:10px">No reports generated yet. Click Reports to generate.</div>';return;}
923
- el.innerHTML='<div style="padding:10px"><div style="font-size:13px;font-weight:bold;margin-bottom:8px">๐Ÿ“Š Generated Reports</div>'
924
- +S.reports.map(r=>{
925
- const icon=r.format==='html'?'๐ŸŒ':r.format==='json'?'๐Ÿ“‹':'๐Ÿ“';
926
- const sizeKB=(r.size/1024).toFixed(1);
927
- return '<div class="file-item" data-format="'+esc(r.format)+'" style="cursor:pointer">'
928
- +'<div class="fname">'+icon+' '+esc(r.filename)+'</div>'
929
- +'<div class="fmeta">'+r.format.toUpperCase()+' โ€” '+sizeKB+' KB</div></div>';
930
- }).join('')+'</div>';
931
- el.querySelectorAll('.file-item').forEach(item=>{
932
- item.addEventListener('click',async()=>{
933
- const fmt=item.getAttribute('data-format');
934
- try{
935
- const r=await fetch('/api/reports/'+fmt);
936
- const content=await r.text();
937
- if(fmt==='html'){
938
- const w=window.open('','_blank','width=900,height=700');
939
- w.document.write(content);w.document.close();
940
- }else{
941
- document.getElementById('fp-title').textContent='report.'+fmt;
942
- document.getElementById('fp-code').textContent=content;
943
- document.getElementById('file-preview').classList.add('visible');
944
- }
945
- }catch(e){addLog('Failed to load report: '+e.message,'error');}
946
- });
947
- });
948
- }
949
-
950
- (async()=>{setupCanvas();await fetchProject();connectWS();addLog('๐ŸŠ OpenCroc Studio ready');
951
- window.addEventListener('resize',()=>{layoutGraph();renderCanvas();renderPixelOffice();});})();
557
+ document.getElementById('run-mode').addEventListener('change',e=>state.set({runMode:e.target.value}));
558
+ document.getElementById('view-3d').addEventListener('click',()=>setView('3d'));
559
+ document.getElementById('view-graph').addEventListener('click',()=>setView('graph'));
560
+ document.getElementById('theme-toggle').addEventListener('click',toggleTheme);
561
+ document.getElementById('sidebar-toggle').addEventListener('click',()=>document.getElementById('sidebar').classList.toggle('collapsed'));
562
+ document.getElementById('bottom-bar-toggle').addEventListener('click',()=>document.getElementById('bottom-bar').classList.toggle('collapsed'));
563
+ document.getElementById('fp-close').addEventListener('click',()=>document.getElementById('file-preview').classList.remove('visible'));
564
+ document.getElementById('fp-backdrop').addEventListener('click',()=>document.getElementById('file-preview').classList.remove('visible'));
565
+ document.querySelectorAll('.panel-tabs .tab').forEach(tab=>{tab.addEventListener('click',()=>{document.querySelectorAll('.panel-tabs .tab').forEach(t=>t.classList.remove('active'));tab.classList.add('active');const tgt=tab.dataset.tab;document.getElementById('log-list').classList.toggle('hidden',tgt!=='log');document.getElementById('file-list').classList.toggle('hidden',tgt!=='files');document.getElementById('results-panel').classList.toggle('hidden',tgt!=='results');document.getElementById('reports-panel').classList.toggle('hidden',tgt!=='reports')})});
566
+ window.addEventListener('resize',()=>resizeEngine());
567
+ let slt=null;
568
+ document.addEventListener('keydown',e=>{const tag=e.target.tagName.toLowerCase();if(tag==='input'||tag==='textarea'||tag==='select')return;const k=e.key.toLowerCase();if(e.key==='Escape'){document.getElementById('file-preview').classList.remove('visible');document.getElementById('shortcut-legend').classList.remove('visible');return}if(k==='?'||(e.key==='/'&&e.shiftKey)){e.preventDefault();const el=document.getElementById('shortcut-legend');el.classList.add('visible');if(slt)clearTimeout(slt);slt=setTimeout(()=>el.classList.remove('visible'),4000);return}if(k==='1'){e.preventDefault();setView('3d');return}if(k==='2'){e.preventDefault();setView('graph');return}if(k==='s'&&!e.ctrlKey&&!e.metaKey){e.preventDefault();doScan();return}if(k==='p'&&!e.ctrlKey&&!e.metaKey){e.preventDefault();doPipeline();return}if(k==='t'&&!e.ctrlKey&&!e.metaKey){e.preventDefault();doRunTests();return}if(k==='r'&&!e.ctrlKey&&!e.metaKey){e.preventDefault();doReports();return}if(k==='x'&&!e.ctrlKey&&!e.metaKey){e.preventDefault();doReset();return}if(k==='d'&&!e.ctrlKey&&!e.metaKey){e.preventDefault();toggleTheme();return}});
569
+
570
+ boot().catch(e=>console.error('Boot failed:',e));
952
571
  </script>
953
572
  </body>
954
573
  </html>