@adiholick/nvstudio-mcp 2.2.3 → 2.4.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.
Files changed (42) hide show
  1. package/README.md +57 -47
  2. package/agent-plugin/mcp_config.json +8 -0
  3. package/agent-plugin/plugin.json +6 -0
  4. package/agent-plugin/rules/AGENTS.md +118 -0
  5. package/agent-plugin/skills/nvstudio-animation-vfx/SKILL.md +120 -0
  6. package/agent-plugin/skills/nvstudio-assets-terrain-guide/SKILL.md +27 -0
  7. package/agent-plugin/skills/nvstudio-gui-standards/SKILL.md +419 -0
  8. package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +83 -0
  9. package/agent-plugin/skills/nvstudio-rig-engine/SKILL.md +170 -0
  10. package/agent-plugin/skills/nvstudio-roblox-sync-guide/SKILL.md +16 -0
  11. package/dist/build-plugin.js +55 -0
  12. package/dist/index.js +841 -3
  13. package/dist/luau-validator.js +99 -34
  14. package/dist/roblox-bridge.js +152 -4
  15. package/dist/roblox-docs.js +264 -0
  16. package/dist/vfx-database.js +507 -0
  17. package/package.json +6 -4
  18. package/public/index.html +1590 -180
  19. package/studio-plugin/Main.server.lua +1 -1
  20. package/studio-plugin/animate_character.lua +36 -0
  21. package/studio-plugin/apply_vfx.lua +59 -0
  22. package/studio-plugin/batch_create_instances.lua +128 -0
  23. package/studio-plugin/batch_set_properties.lua +69 -0
  24. package/studio-plugin/edit_rig.lua +35 -0
  25. package/studio-plugin/insert_asset.lua +85 -12
  26. package/studio-plugin/nv_anim_dispatcher.lua +239 -0
  27. package/studio-plugin/nv_auto_rigger.lua +392 -0
  28. package/studio-plugin/nv_curves.lua +260 -0
  29. package/studio-plugin/nv_joint_math.lua +138 -0
  30. package/studio-plugin/nv_kinematics.lua +248 -0
  31. package/studio-plugin/nv_motion_generator.lua +279 -0
  32. package/studio-plugin/nv_rig_dispatcher.lua +319 -0
  33. package/studio-plugin/nv_vfx_serializer.lua +363 -0
  34. package/studio-plugin/nvstudio_mcp.rbxmx +3824 -60
  35. package/studio-plugin/preview_asset.lua +99 -0
  36. package/studio-plugin/scan_security.lua +81 -0
  37. package/studio-plugin/scan_vfx.lua +85 -0
  38. package/studio-plugin/scrub_timeline.lua +122 -0
  39. package/studio-plugin/security_scanner.lua +346 -0
  40. package/studio-plugin/set_properties.lua +83 -0
  41. package/studio-plugin/solve_limb_ik.lua +63 -0
  42. package/studio-plugin/ui_manager.lua +1 -1
package/public/index.html CHANGED
@@ -3,33 +3,41 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>nvstudio-mcp | Bridge Dashboard</title>
6
+ <title>nvstudio-mcp v2.4 | Bridge Dashboard</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
10
  <style>
11
11
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
12
 
13
13
  :root {
14
- --bg: #000000;
15
- --surface: #0a0a0a;
16
- --surface-glass: rgba(10, 10, 10, 0.7);
17
- --surface-card: #121212;
18
- --surface-hover: #1c1c1c;
14
+ --bg: #0c0c0e;
15
+ --surface: #121216;
16
+ --surface-glass: rgba(18, 18, 22, 0.75);
17
+ --surface-card: #18181d;
18
+ --surface-hover: #222228;
19
19
  --border: rgba(255, 255, 255, 0.08);
20
- --border-highlight: rgba(255, 255, 255, 0.15);
20
+ --border-highlight: rgba(255, 255, 255, 0.16);
21
21
 
22
- --text: #f4f4f5;
22
+ --text: #f4f4f6;
23
23
  --text-muted: #a1a1aa;
24
24
  --text-dim: #71717a;
25
25
 
26
- --accent: #3b82f6;
27
- --accent-glow: rgba(59, 130, 246, 0.2);
26
+ /* NV-Series Accent Colors */
27
+ --accent: #00aaff;
28
+ --accent-glow: rgba(0, 170, 255, 0.25);
29
+ --cyan-glow: rgba(0, 255, 255, 0.2);
28
30
  --success: #10b981;
29
31
  --success-glow: rgba(16, 185, 129, 0.25);
30
32
  --error: #ef4444;
31
33
  --error-glow: rgba(239, 68, 68, 0.2);
32
- --task: #8b5cf6;
34
+ --warning: #f59e0b;
35
+ --warning-glow: rgba(245, 158, 11, 0.25);
36
+
37
+ --task: #a855f7;
38
+ --batch: #00aaff;
39
+ --security: #f59e0b;
40
+ --docs: #10b981;
33
41
  --response: #14b8a6;
34
42
  --system: #a1a1aa;
35
43
 
@@ -50,7 +58,7 @@
50
58
  .layout {
51
59
  display: flex;
52
60
  height: 100vh;
53
- background: radial-gradient(circle at 50% 0%, rgba(20, 20, 25, 1) 0%, rgba(0, 0, 0, 1) 100%);
61
+ background: radial-gradient(circle at 50% 0%, rgba(24, 24, 32, 1) 0%, rgba(12, 12, 14, 1) 100%);
54
62
  }
55
63
 
56
64
  /* ── SIDEBAR ── */
@@ -58,7 +66,7 @@
58
66
  width: var(--sidebar-w);
59
67
  min-width: var(--sidebar-w);
60
68
  background: var(--surface-glass);
61
- backdrop-filter: blur(12px);
69
+ backdrop-filter: blur(16px);
62
70
  border-right: 1px solid var(--border);
63
71
  display: flex;
64
72
  flex-direction: column;
@@ -77,11 +85,11 @@
77
85
  }
78
86
  .sidebar-logo .logo-icon {
79
87
  width: 38px; height: 38px;
80
- background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
88
+ background: linear-gradient(135deg, #0088cc 0%, #00aaff 100%);
81
89
  border-radius: 10px;
82
90
  display: flex; align-items: center; justify-content: center;
83
91
  flex-shrink: 0;
84
- box-shadow: 0 4px 12px var(--accent-glow);
92
+ box-shadow: 0 4px 14px var(--accent-glow);
85
93
  }
86
94
  .sidebar-logo .logo-icon svg { color: #fff; width: 22px; height: 22px; }
87
95
 
@@ -108,7 +116,7 @@
108
116
  display: flex;
109
117
  align-items: center;
110
118
  gap: 12px;
111
- background: rgba(0, 0, 0, 0.2);
119
+ background: rgba(0, 0, 0, 0.25);
112
120
  }
113
121
  .status-indicator-ring {
114
122
  position: relative;
@@ -176,7 +184,7 @@
176
184
  color: var(--text);
177
185
  }
178
186
  .nav-item.active {
179
- background: rgba(59, 130, 246, 0.1);
187
+ background: rgba(0, 170, 255, 0.12);
180
188
  color: var(--accent);
181
189
  border-left: 2px solid var(--accent);
182
190
  }
@@ -192,9 +200,9 @@
192
200
  color: var(--text-dim);
193
201
  }
194
202
  .badge-v2 {
195
- background: rgba(59, 130, 246, 0.15);
203
+ background: rgba(0, 170, 255, 0.15);
196
204
  color: var(--accent);
197
- border: 1px solid rgba(59, 130, 246, 0.3);
205
+ border: 1px solid rgba(0, 170, 255, 0.3);
198
206
  padding: 3px 8px;
199
207
  border-radius: 4px;
200
208
  font-weight: 600;
@@ -218,8 +226,8 @@
218
226
  display: flex;
219
227
  align-items: center;
220
228
  justify-content: space-between;
221
- background: rgba(0, 0, 0, 0.4);
222
- backdrop-filter: blur(10px);
229
+ background: rgba(12, 12, 14, 0.6);
230
+ backdrop-filter: blur(12px);
223
231
  z-index: 5;
224
232
  }
225
233
  .page-title {
@@ -286,8 +294,10 @@
286
294
  padding: 16px 32px;
287
295
  background: var(--surface-glass);
288
296
  border-bottom: 1px solid var(--border);
297
+ flex-wrap: wrap;
298
+ gap: 12px;
289
299
  }
290
- .filter-btns { display: flex; gap: 8px; }
300
+ .filter-btns { display: flex; gap: 8px; flex-wrap: wrap; }
291
301
  .filter-btn {
292
302
  background: var(--surface-card);
293
303
  border: 1px solid var(--border);
@@ -324,7 +334,7 @@
324
334
  display: flex;
325
335
  flex-direction: column;
326
336
  gap: 8px;
327
- height: calc(100vh - 140px); /* 70px topbar + ~70px toolbar */
337
+ height: calc(100vh - 142px);
328
338
  }
329
339
 
330
340
  .log-empty {
@@ -350,6 +360,7 @@
350
360
  font-size: 13px;
351
361
  line-height: 1.5;
352
362
  transition: transform 0.2s, background 0.2s;
363
+ align-items: flex-start;
353
364
  }
354
365
  .log-entry:hover {
355
366
  background: rgba(255, 255, 255, 0.04);
@@ -360,26 +371,33 @@
360
371
  font-size: 12px;
361
372
  flex-shrink: 0;
362
373
  white-space: nowrap;
374
+ margin-top: 1px;
363
375
  }
364
376
  .log-entry-badge {
365
377
  font-size: 10px; font-weight: 700;
366
378
  padding: 2px 8px; border-radius: 4px;
367
379
  height: fit-content; flex-shrink: 0;
380
+ letter-spacing: 0.03em;
368
381
  }
369
382
 
370
- .badge-task { color: var(--task); background: rgba(139, 92, 246, 0.15); border: 1px solid rgba(139, 92, 246, 0.3); }
383
+ .badge-task { color: var(--task); background: rgba(168, 85, 247, 0.15); border: 1px solid rgba(168, 85, 247, 0.3); }
384
+ .badge-batch { color: var(--batch); background: rgba(0, 170, 255, 0.15); border: 1px solid rgba(0, 170, 255, 0.3); }
385
+ .badge-security { color: var(--security); background: rgba(245, 158, 11, 0.15); border: 1px solid rgba(245, 158, 11, 0.3); }
386
+ .badge-docs { color: var(--docs); background: rgba(16, 185, 129, 0.15); border: 1px solid rgba(16, 185, 129, 0.3); }
387
+ .badge-patch { color: #38bdf8; background: rgba(56, 189, 248, 0.15); border: 1px solid rgba(56, 189, 248, 0.3); }
388
+ .badge-luau { color: #c084fc; background: rgba(192, 132, 252, 0.15); border: 1px solid rgba(192, 132, 252, 0.3); }
371
389
  .badge-response { color: var(--response); background: rgba(20, 184, 166, 0.15); border: 1px solid rgba(20, 184, 166, 0.3); }
372
390
  .badge-error { color: var(--error); background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.3); }
373
391
  .badge-system { color: var(--system); background: rgba(161, 161, 170, 0.15); border: 1px solid rgba(161, 161, 170, 0.3); }
374
392
 
375
- .log-entry-msg { color: var(--text-primary); word-break: break-word; }
393
+ .log-entry-msg { color: var(--text); word-break: break-word; flex: 1; }
376
394
 
377
- /* ── STATS PAGE ── */
395
+ /* ── STATS & SESSIONS PAGE ── */
378
396
  .stats-grid {
379
397
  display: grid;
380
398
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
381
399
  gap: 20px;
382
- max-width: 1000px;
400
+ max-width: 1040px;
383
401
  }
384
402
  .stat-card {
385
403
  background: var(--surface-card);
@@ -410,11 +428,212 @@
410
428
  .info-row:last-child { border-bottom: none; padding-bottom: 0; }
411
429
  .info-row-label { font-size: 13px; color: var(--text-muted); }
412
430
  .info-row-value { font-size: 14px; font-weight: 600; color: var(--text); font-family: 'JetBrains Mono', monospace; }
413
-
431
+
432
+ /* Studio Sessions Table */
433
+ .sessions-table {
434
+ width: 100%;
435
+ border-collapse: collapse;
436
+ margin-top: 16px;
437
+ font-size: 13px;
438
+ }
439
+ .sessions-table th {
440
+ text-align: left;
441
+ padding: 10px 14px;
442
+ color: var(--text-dim);
443
+ font-weight: 600;
444
+ border-bottom: 1px solid var(--border);
445
+ text-transform: uppercase;
446
+ font-size: 11px;
447
+ letter-spacing: 0.05em;
448
+ }
449
+ .sessions-table td {
450
+ padding: 12px 14px;
451
+ border-bottom: 1px solid rgba(255,255,255,0.04);
452
+ color: var(--text);
453
+ font-family: 'JetBrains Mono', monospace;
454
+ }
455
+ .sessions-table tr:hover td {
456
+ background: rgba(255,255,255,0.02);
457
+ }
458
+ .badge-pill {
459
+ display: inline-flex; align-items: center; gap: 6px;
460
+ font-size: 11px; padding: 2px 8px; border-radius: 12px; font-weight: 600;
461
+ }
462
+ .badge-ws { background: rgba(0, 170, 255, 0.15); color: var(--accent); border: 1px solid rgba(0, 170, 255, 0.3); }
463
+ .badge-poll { background: rgba(245, 158, 11, 0.15); color: var(--warning); border: 1px solid rgba(245, 158, 11, 0.3); }
464
+
465
+ /* ── DOCS EXPLORER PAGE ── */
466
+ .docs-container {
467
+ max-width: 900px;
468
+ display: flex;
469
+ flex-direction: column;
470
+ gap: 20px;
471
+ }
472
+ .docs-search-card {
473
+ background: var(--surface-card);
474
+ border: 1px solid var(--border);
475
+ border-radius: 12px;
476
+ padding: 24px;
477
+ display: flex;
478
+ flex-direction: column;
479
+ gap: 16px;
480
+ }
481
+ .docs-form {
482
+ display: flex;
483
+ gap: 12px;
484
+ flex-wrap: wrap;
485
+ }
486
+ .docs-input {
487
+ flex: 2;
488
+ min-width: 200px;
489
+ background: #0a0a0c;
490
+ border: 1px solid var(--border);
491
+ color: var(--text);
492
+ padding: 10px 14px;
493
+ border-radius: 8px;
494
+ font-family: inherit;
495
+ font-size: 14px;
496
+ outline: none;
497
+ transition: border-color 0.2s;
498
+ }
499
+ .docs-input:focus {
500
+ border-color: var(--accent);
501
+ box-shadow: 0 0 10px var(--accent-glow);
502
+ }
503
+ .docs-member-input {
504
+ flex: 1;
505
+ min-width: 140px;
506
+ background: #0a0a0c;
507
+ border: 1px solid var(--border);
508
+ color: var(--text);
509
+ padding: 10px 14px;
510
+ border-radius: 8px;
511
+ font-family: inherit;
512
+ font-size: 14px;
513
+ outline: none;
514
+ }
515
+ .docs-member-input:focus { border-color: var(--accent); }
516
+ .docs-btn {
517
+ background: linear-gradient(135deg, #0088cc 0%, #00aaff 100%);
518
+ color: #fff;
519
+ border: none;
520
+ padding: 10px 22px;
521
+ border-radius: 8px;
522
+ font-family: inherit;
523
+ font-size: 14px;
524
+ font-weight: 600;
525
+ cursor: pointer;
526
+ transition: transform 0.15s, box-shadow 0.2s;
527
+ display: flex; align-items: center; gap: 8px;
528
+ }
529
+ .docs-btn:hover {
530
+ transform: translateY(-1px);
531
+ box-shadow: 0 4px 14px var(--accent-glow);
532
+ }
533
+ .docs-chips {
534
+ display: flex;
535
+ gap: 8px;
536
+ flex-wrap: wrap;
537
+ align-items: center;
538
+ }
539
+ .chip-label { font-size: 12px; color: var(--text-dim); }
540
+ .doc-chip {
541
+ font-size: 12px;
542
+ background: rgba(255,255,255,0.04);
543
+ border: 1px solid var(--border);
544
+ color: var(--text-muted);
545
+ padding: 4px 10px;
546
+ border-radius: 6px;
547
+ cursor: pointer;
548
+ transition: all 0.2s;
549
+ font-family: 'JetBrains Mono', monospace;
550
+ }
551
+ .doc-chip:hover {
552
+ background: rgba(0, 170, 255, 0.15);
553
+ color: var(--accent);
554
+ border-color: var(--accent);
555
+ }
556
+
557
+ .docs-result-card {
558
+ background: var(--surface-card);
559
+ border: 1px solid var(--border);
560
+ border-radius: 12px;
561
+ padding: 28px;
562
+ min-height: 200px;
563
+ display: flex;
564
+ flex-direction: column;
565
+ position: relative;
566
+ }
567
+ .docs-result-header {
568
+ display: flex;
569
+ justify-content: space-between;
570
+ align-items: center;
571
+ margin-bottom: 16px;
572
+ padding-bottom: 12px;
573
+ border-bottom: 1px solid var(--border);
574
+ }
575
+ .docs-title {
576
+ font-size: 16px;
577
+ font-weight: 600;
578
+ color: var(--text);
579
+ display: flex; align-items: center; gap: 8px;
580
+ }
581
+ .docs-copy-btn {
582
+ background: transparent;
583
+ border: 1px solid var(--border);
584
+ color: var(--text-muted);
585
+ font-size: 12px;
586
+ padding: 4px 10px;
587
+ border-radius: 6px;
588
+ cursor: pointer;
589
+ transition: all 0.2s;
590
+ }
591
+ .docs-copy-btn:hover {
592
+ background: var(--surface-hover);
593
+ color: var(--text);
594
+ }
595
+ .docs-content {
596
+ font-family: 'Inter', sans-serif;
597
+ font-size: 14px;
598
+ line-height: 1.7;
599
+ color: var(--text);
600
+ overflow-x: auto;
601
+ }
602
+ .docs-content pre {
603
+ background: #09090b;
604
+ padding: 14px 18px;
605
+ border-radius: 8px;
606
+ border: 1px solid var(--border);
607
+ margin: 14px 0;
608
+ overflow-x: auto;
609
+ font-family: 'JetBrains Mono', monospace;
610
+ font-size: 13px;
611
+ color: #38bdf8;
612
+ }
613
+ .docs-content code {
614
+ font-family: 'JetBrains Mono', monospace;
615
+ background: rgba(255,255,255,0.06);
616
+ color: var(--accent);
617
+ padding: 2px 6px;
618
+ border-radius: 4px;
619
+ font-size: 12px;
620
+ }
621
+ .docs-content h1, .docs-content h2, .docs-content h3 {
622
+ margin: 18px 0 8px;
623
+ color: #fff;
624
+ }
625
+ .docs-content ul {
626
+ padding-left: 20px;
627
+ margin: 8px 0;
628
+ }
629
+ .docs-content li {
630
+ margin-bottom: 4px;
631
+ }
632
+
414
633
  /* ── ABOUT PAGE ── */
415
- .about-section { max-width: 680px; }
634
+ .about-section { max-width: 720px; }
416
635
  .about-hero {
417
- background: linear-gradient(145deg, var(--surface-card) 0%, rgba(10,10,10,1) 100%);
636
+ background: linear-gradient(145deg, var(--surface-card) 0%, rgba(14,14,18,1) 100%);
418
637
  border: 1px solid var(--border);
419
638
  border-radius: 16px;
420
639
  padding: 40px;
@@ -431,7 +650,7 @@
431
650
  pointer-events: none;
432
651
  }
433
652
  .about-hero h2 { font-size: 28px; font-weight: 700; color: var(--text); letter-spacing: -0.02em; display: flex; align-items: center; gap: 12px;}
434
- .v2-chip { font-size: 12px; background: var(--accent); color: white; padding: 2px 8px; border-radius: 12px; vertical-align: middle;}
653
+ .v2-chip { font-size: 12px; background: var(--accent); color: white; padding: 2px 8px; border-radius: 12px; vertical-align: middle; font-weight: 700;}
435
654
  .about-hero p { font-size: 15px; color: var(--text-muted); line-height: 1.6; }
436
655
 
437
656
  .about-links { display: flex; gap: 12px; margin-top: 8px; }
@@ -465,106 +684,523 @@
465
684
  font-size: 12px;
466
685
  }
467
686
 
468
- /* Scrollbar */
469
- ::-webkit-scrollbar { width: 6px; height: 6px; }
470
- ::-webkit-scrollbar-track { background: transparent; }
471
- ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
472
- ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
473
-
474
- /* Transitions */
475
- .log-entry { animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
476
- @keyframes slideIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
477
- </style>
478
- </head>
479
- <body>
480
- <div class="layout">
481
-
482
- <!-- ── SIDEBAR ── -->
483
- <aside class="sidebar">
484
- <div class="sidebar-logo">
485
- <div class="logo-mark">
486
- <div class="logo-icon">
487
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
488
- <path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
489
- </svg>
490
- </div>
491
- <div class="logo-text-wrapper">
492
- <div class="logo-name">nvstudio-mcp</div>
493
- <div class="logo-sub">Zero-Delay Engine</div>
494
- </div>
495
- </div>
496
- </div>
687
+ /* ── VFX Database & Library Styles ── */
688
+ .vfx-sidebar-badge {
689
+ font-size: 10px;
690
+ font-weight: 700;
691
+ background: var(--accent);
692
+ color: #fff;
693
+ padding: 1px 7px;
694
+ border-radius: 10px;
695
+ margin-left: auto;
696
+ letter-spacing: 0.02em;
697
+ }
497
698
 
498
- <div class="sidebar-status">
499
- <div class="status-indicator-ring">
500
- <div class="status-dot waiting" id="status-dot"></div>
501
- <div class="status-ping"></div>
502
- </div>
503
- <span class="status-label" id="status-label">Menunggu Studio...</span>
504
- </div>
699
+ .vfx-container {
700
+ display: flex;
701
+ flex-direction: column;
702
+ gap: 20px;
703
+ padding: 24px 32px 60px 32px;
704
+ overflow-y: auto;
705
+ height: 100%;
706
+ }
505
707
 
506
- <nav class="sidebar-nav">
507
- <div class="nav-group-label">Monitor</div>
508
- <div class="nav-item active" data-page="activity">
509
- <svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
510
- Activity Log
511
- </div>
512
- <div class="nav-item" data-page="stats">
513
- <svg viewBox="0 0 24 24"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
514
- Statistics
515
- </div>
708
+ .vfx-header {
709
+ display: flex;
710
+ justify-content: space-between;
711
+ align-items: flex-start;
712
+ gap: 16px;
713
+ flex-wrap: wrap;
714
+ }
715
+ .vfx-header-info h2 {
716
+ font-size: 22px;
717
+ font-weight: 700;
718
+ color: var(--text);
719
+ display: flex;
720
+ align-items: center;
721
+ gap: 10px;
722
+ }
723
+ .vfx-header-info p {
724
+ font-size: 13px;
725
+ color: var(--text-muted);
726
+ margin-top: 4px;
727
+ }
516
728
 
517
- <div class="nav-group-label" style="margin-top:20px;">Info</div>
518
- <div class="nav-item" data-page="about">
519
- <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
520
- About v2.0
521
- </div>
522
- </nav>
729
+ .vfx-actions-bar {
730
+ display: flex;
731
+ gap: 10px;
732
+ align-items: center;
733
+ }
523
734
 
524
- <div class="sidebar-footer">
525
- <span class="badge-v2" id="pkg-version">v2.0.1</span>
526
- <span style="font-size:10px; opacity:0.5;">Model Context Protocol</span>
527
- </div>
528
- </aside>
735
+ .vfx-btn-primary {
736
+ background: linear-gradient(135deg, #0088cc 0%, #00aaff 100%);
737
+ color: #fff;
738
+ font-weight: 600;
739
+ font-size: 13px;
740
+ padding: 8px 16px;
741
+ border-radius: 8px;
742
+ border: none;
743
+ cursor: pointer;
744
+ display: inline-flex;
745
+ align-items: center;
746
+ gap: 8px;
747
+ box-shadow: 0 4px 12px var(--accent-glow);
748
+ transition: all 0.2s;
749
+ }
750
+ .vfx-btn-primary:hover {
751
+ transform: translateY(-1px);
752
+ box-shadow: 0 6px 16px var(--accent-glow);
753
+ filter: brightness(1.1);
754
+ }
529
755
 
530
- <!-- ── MAIN ── -->
531
- <main class="main">
532
- <div class="topbar">
533
- <span class="page-title" id="topbar-title">Activity Log</span>
534
- <div class="topbar-right">
535
- <span class="topbar-badge" id="log-count">0 events</span>
536
- <button class="clear-btn" id="clear-btn">Clear</button>
537
- </div>
538
- </div>
756
+ .vfx-btn-secondary {
757
+ background: rgba(255, 255, 255, 0.05);
758
+ color: var(--text);
759
+ border: 1px solid var(--border);
760
+ font-weight: 600;
761
+ font-size: 13px;
762
+ padding: 8px 14px;
763
+ border-radius: 8px;
764
+ cursor: pointer;
765
+ display: inline-flex;
766
+ align-items: center;
767
+ gap: 6px;
768
+ transition: all 0.2s;
769
+ }
770
+ .vfx-btn-secondary:hover {
771
+ background: rgba(255, 255, 255, 0.1);
772
+ border-color: var(--border-highlight);
773
+ }
539
774
 
540
- <div class="content">
775
+ .vfx-search-box {
776
+ position: relative;
777
+ flex: 1;
778
+ min-width: 260px;
779
+ max-width: 480px;
780
+ }
781
+ .vfx-search-box input {
782
+ width: 100%;
783
+ background: #0d0d0d;
784
+ border: 1px solid var(--border);
785
+ border-radius: 8px;
786
+ padding: 9px 14px 9px 36px;
787
+ color: var(--text);
788
+ font-size: 13px;
789
+ outline: none;
790
+ transition: all 0.2s;
791
+ }
792
+ .vfx-search-box input:focus {
793
+ border-color: var(--accent);
794
+ box-shadow: 0 0 10px var(--accent-glow);
795
+ }
796
+ .vfx-search-box svg {
797
+ position: absolute;
798
+ left: 12px;
799
+ top: 50%;
800
+ transform: translateY(-50%);
801
+ width: 15px;
802
+ height: 15px;
803
+ color: var(--text-dim);
804
+ }
541
805
 
542
- <!-- Activity Log Page -->
543
- <div class="page active" id="page-activity" style="padding:0; gap:0;">
544
- <div class="log-toolbar">
545
- <div class="filter-btns">
546
- <button class="filter-btn active" data-filter="all">All Logs</button>
547
- <button class="filter-btn" data-filter="task">Task / Patch</button>
548
- <button class="filter-btn" data-filter="response">Response</button>
549
- <button class="filter-btn" data-filter="error">Error / Validate</button>
550
- <button class="filter-btn" data-filter="system">System</button>
551
- </div>
552
- <button class="scroll-lock-btn locked" id="scroll-lock-btn">
553
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
554
- <line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/>
555
- </svg>
806
+ /* Category Filter Pills */
807
+ .vfx-category-tabs {
808
+ display: flex;
809
+ align-items: center;
810
+ gap: 8px;
811
+ overflow-x: auto;
812
+ padding-bottom: 6px;
813
+ }
814
+ .vfx-cat-pill {
815
+ background: #18181d;
816
+ border: 1px solid var(--border);
817
+ color: var(--text-muted);
818
+ font-size: 12px;
819
+ font-weight: 600;
820
+ padding: 6px 14px;
821
+ border-radius: 20px;
822
+ cursor: pointer;
823
+ white-space: nowrap;
824
+ display: inline-flex;
825
+ align-items: center;
826
+ gap: 6px;
827
+ transition: all 0.2s;
828
+ }
829
+ .vfx-cat-pill:hover {
830
+ color: var(--text);
831
+ border-color: var(--border-highlight);
832
+ background: #222228;
833
+ }
834
+ .vfx-cat-pill.active {
835
+ background: var(--accent);
836
+ color: #fff;
837
+ border-color: var(--accent);
838
+ box-shadow: 0 2px 10px var(--accent-glow);
839
+ }
840
+ .vfx-cat-count {
841
+ font-size: 10px;
842
+ background: rgba(0, 0, 0, 0.35);
843
+ padding: 1px 6px;
844
+ border-radius: 10px;
845
+ opacity: 0.9;
846
+ }
847
+
848
+ /* Summary Stats Cards */
849
+ .vfx-stats-row {
850
+ display: grid;
851
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
852
+ gap: 14px;
853
+ }
854
+ .vfx-stat-box {
855
+ background: var(--surface-card);
856
+ border: 1px solid var(--border);
857
+ border-radius: 10px;
858
+ padding: 16px 20px;
859
+ display: flex;
860
+ flex-direction: column;
861
+ gap: 4px;
862
+ }
863
+ .vfx-stat-label { font-size: 11px; text-transform: uppercase; color: var(--text-dim); font-weight: 600; letter-spacing: 0.05em; }
864
+ .vfx-stat-val { font-size: 24px; font-weight: 700; color: var(--text); }
865
+ .vfx-stat-sub { font-size: 12px; color: var(--text-muted); }
866
+
867
+ /* VFX Grid & Cards */
868
+ .vfx-grid {
869
+ display: grid;
870
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
871
+ gap: 16px;
872
+ }
873
+ .vfx-card {
874
+ background: #18181d;
875
+ border: 1px solid var(--border);
876
+ border-radius: 10px;
877
+ padding: 20px;
878
+ display: flex;
879
+ flex-direction: column;
880
+ gap: 14px;
881
+ transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
882
+ position: relative;
883
+ }
884
+ .vfx-card:hover {
885
+ border-color: rgba(0, 170, 255, 0.4);
886
+ transform: translateY(-2px);
887
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
888
+ }
889
+
890
+ .vfx-card-top {
891
+ display: flex;
892
+ justify-content: space-between;
893
+ align-items: flex-start;
894
+ gap: 10px;
895
+ }
896
+ .vfx-card-title-group h4 {
897
+ font-size: 15px;
898
+ font-weight: 600;
899
+ color: var(--text);
900
+ letter-spacing: -0.01em;
901
+ }
902
+ .vfx-card-date {
903
+ font-size: 11px;
904
+ color: var(--text-dim);
905
+ margin-top: 2px;
906
+ }
907
+
908
+ .vfx-badge-cat {
909
+ font-size: 10px;
910
+ font-weight: 700;
911
+ text-transform: uppercase;
912
+ padding: 3px 8px;
913
+ border-radius: 6px;
914
+ letter-spacing: 0.04em;
915
+ }
916
+ .vfx-badge-Slash { background: rgba(0, 170, 255, 0.15); color: #00aaff; border: 1px solid rgba(0, 170, 255, 0.3); }
917
+ .vfx-badge-Explosion { background: rgba(245, 158, 11, 0.15); color: #f59e0b; border: 1px solid rgba(245, 158, 11, 0.3); }
918
+ .vfx-badge-Aura { background: rgba(16, 185, 129, 0.15); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.3); }
919
+ .vfx-badge-Magic { background: rgba(168, 85, 247, 0.15); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); }
920
+ .vfx-badge-Impact { background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); }
921
+ .vfx-badge-Beam { background: rgba(6, 182, 212, 0.15); color: #22d3ee; border: 1px solid rgba(6, 182, 212, 0.3); }
922
+ .vfx-badge-Projectile { background: rgba(236, 72, 153, 0.15); color: #f472b6; border: 1px solid rgba(236, 72, 153, 0.3); }
923
+ .vfx-badge-Ambient { background: rgba(161, 161, 170, 0.15); color: #a1a1aa; border: 1px solid rgba(161, 161, 170, 0.3); }
924
+
925
+ .vfx-card-desc {
926
+ font-size: 13px;
927
+ color: var(--text-muted);
928
+ line-height: 1.5;
929
+ min-height: 38px;
930
+ }
931
+
932
+ .vfx-composition-badges {
933
+ display: flex;
934
+ flex-wrap: wrap;
935
+ gap: 6px;
936
+ }
937
+ .vfx-comp-pill {
938
+ font-size: 11px;
939
+ background: rgba(255, 255, 255, 0.05);
940
+ border: 1px solid rgba(255, 255, 255, 0.08);
941
+ color: var(--text-muted);
942
+ padding: 2px 7px;
943
+ border-radius: 4px;
944
+ font-family: 'JetBrains Mono', monospace;
945
+ }
946
+
947
+ .vfx-card-tags {
948
+ display: flex;
949
+ flex-wrap: wrap;
950
+ gap: 4px;
951
+ }
952
+ .vfx-tag-chip {
953
+ font-size: 11px;
954
+ color: var(--text-dim);
955
+ background: #0d0d0d;
956
+ padding: 2px 6px;
957
+ border-radius: 4px;
958
+ }
959
+
960
+ .vfx-card-footer {
961
+ margin-top: auto;
962
+ display: flex;
963
+ gap: 8px;
964
+ padding-top: 12px;
965
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
966
+ }
967
+ .vfx-btn-apply {
968
+ flex: 1;
969
+ background: rgba(0, 170, 255, 0.12);
970
+ color: var(--accent);
971
+ border: 1px solid rgba(0, 170, 255, 0.3);
972
+ font-size: 12px;
973
+ font-weight: 600;
974
+ padding: 7px 12px;
975
+ border-radius: 6px;
976
+ cursor: pointer;
977
+ display: inline-flex;
978
+ align-items: center;
979
+ justify-content: center;
980
+ gap: 6px;
981
+ transition: all 0.2s;
982
+ }
983
+ .vfx-btn-apply:hover {
984
+ background: var(--accent);
985
+ color: #fff;
986
+ box-shadow: 0 0 12px var(--accent-glow);
987
+ }
988
+
989
+ .vfx-btn-json {
990
+ background: rgba(255, 255, 255, 0.04);
991
+ color: var(--text-muted);
992
+ border: 1px solid var(--border);
993
+ font-size: 12px;
994
+ padding: 7px 10px;
995
+ border-radius: 6px;
996
+ cursor: pointer;
997
+ transition: all 0.2s;
998
+ }
999
+ .vfx-btn-json:hover {
1000
+ background: rgba(255, 255, 255, 0.09);
1001
+ color: var(--text);
1002
+ }
1003
+
1004
+ .vfx-btn-del {
1005
+ background: rgba(239, 68, 68, 0.08);
1006
+ color: #f87171;
1007
+ border: 1px solid rgba(239, 68, 68, 0.2);
1008
+ font-size: 12px;
1009
+ padding: 7px 10px;
1010
+ border-radius: 6px;
1011
+ cursor: pointer;
1012
+ transition: all 0.2s;
1013
+ }
1014
+ .vfx-btn-del:hover {
1015
+ background: #ef4444;
1016
+ color: #fff;
1017
+ }
1018
+
1019
+ /* Modals */
1020
+ .vfx-modal-backdrop {
1021
+ display: none;
1022
+ position: fixed;
1023
+ top: 0; left: 0; right: 0; bottom: 0;
1024
+ background: rgba(0, 0, 0, 0.7);
1025
+ backdrop-filter: blur(8px);
1026
+ z-index: 1000;
1027
+ align-items: center;
1028
+ justify-content: center;
1029
+ padding: 20px;
1030
+ }
1031
+ .vfx-modal-backdrop.open { display: flex; }
1032
+ .vfx-modal-card {
1033
+ background: #18181d;
1034
+ border: 1px solid var(--border-highlight);
1035
+ border-radius: 12px;
1036
+ max-width: 600px;
1037
+ width: 100%;
1038
+ max-height: 85vh;
1039
+ display: flex;
1040
+ flex-direction: column;
1041
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
1042
+ overflow: hidden;
1043
+ }
1044
+ .vfx-modal-header {
1045
+ padding: 18px 24px;
1046
+ border-bottom: 1px solid var(--border);
1047
+ display: flex;
1048
+ align-items: center;
1049
+ justify-content: space-between;
1050
+ }
1051
+ .vfx-modal-header h3 { font-size: 16px; font-weight: 600; color: #fff; }
1052
+ .vfx-modal-close {
1053
+ background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 20px; line-height: 1;
1054
+ }
1055
+ .vfx-modal-close:hover { color: #fff; }
1056
+ .vfx-modal-body {
1057
+ padding: 20px 24px;
1058
+ overflow-y: auto;
1059
+ display: flex;
1060
+ flex-direction: column;
1061
+ gap: 16px;
1062
+ }
1063
+ .vfx-modal-footer {
1064
+ padding: 16px 24px;
1065
+ border-top: 1px solid var(--border);
1066
+ display: flex;
1067
+ justify-content: flex-end;
1068
+ gap: 10px;
1069
+ background: rgba(0, 0, 0, 0.2);
1070
+ }
1071
+ .vfx-form-group {
1072
+ display: flex;
1073
+ flex-direction: column;
1074
+ gap: 6px;
1075
+ }
1076
+ .vfx-form-label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
1077
+ .vfx-form-input, .vfx-form-select {
1078
+ background: #0d0d0d;
1079
+ border: 1px solid var(--border);
1080
+ border-radius: 6px;
1081
+ padding: 9px 12px;
1082
+ color: var(--text);
1083
+ font-size: 13px;
1084
+ outline: none;
1085
+ }
1086
+ .vfx-form-input:focus, .vfx-form-select:focus {
1087
+ border-color: var(--accent);
1088
+ box-shadow: 0 0 8px var(--accent-glow);
1089
+ }
1090
+
1091
+ /* Scrollbar */
1092
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
1093
+ ::-webkit-scrollbar-track { background: transparent; }
1094
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 3px; }
1095
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
1096
+
1097
+ /* Transitions */
1098
+ .log-entry { animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
1099
+ @keyframes slideIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
1100
+ </style>
1101
+ </head>
1102
+ <body>
1103
+ <div class="layout">
1104
+
1105
+ <!-- ── SIDEBAR ── -->
1106
+ <aside class="sidebar">
1107
+ <div class="sidebar-logo">
1108
+ <div class="logo-mark">
1109
+ <div class="logo-icon">
1110
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
1111
+ <path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
1112
+ </svg>
1113
+ </div>
1114
+ <div class="logo-text-wrapper">
1115
+ <div class="logo-name">nvstudio-mcp</div>
1116
+ <div class="logo-sub">Zero-Delay Bridge</div>
1117
+ </div>
1118
+ </div>
1119
+ </div>
1120
+
1121
+ <div class="sidebar-status">
1122
+ <div class="status-indicator-ring">
1123
+ <div class="status-dot waiting" id="status-dot"></div>
1124
+ <div class="status-ping"></div>
1125
+ </div>
1126
+ <span class="status-label" id="status-label">Menunggu Studio...</span>
1127
+ </div>
1128
+
1129
+ <nav class="sidebar-nav">
1130
+ <div class="nav-group-label">Monitor</div>
1131
+ <div class="nav-item active" data-page="activity">
1132
+ <svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
1133
+ Activity Log
1134
+ </div>
1135
+ <div class="nav-item" data-page="stats">
1136
+ <svg viewBox="0 0 24 24"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
1137
+ Diagnostics & Sessions
1138
+ </div>
1139
+
1140
+ <div class="nav-group-label" style="margin-top:20px;">Engine Tools</div>
1141
+ <div class="nav-item" data-page="vfx-database">
1142
+ <svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
1143
+ VFX Database
1144
+ <span class="vfx-sidebar-badge" id="vfx-sidebar-count">0</span>
1145
+ </div>
1146
+ <div class="nav-item" data-page="docs">
1147
+ <svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
1148
+ Docs Explorer
1149
+ </div>
1150
+
1151
+ <div class="nav-group-label" style="margin-top:20px;">Info</div>
1152
+ <div class="nav-item" data-page="about">
1153
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
1154
+ About v2.4
1155
+ </div>
1156
+ </nav>
1157
+
1158
+ <div class="sidebar-footer">
1159
+ <span class="badge-v2" id="pkg-version">v2.4.0</span>
1160
+ <span style="font-size:10px; opacity:0.6;">Model Context Protocol</span>
1161
+ </div>
1162
+ </aside>
1163
+
1164
+ <!-- ── MAIN ── -->
1165
+ <main class="main">
1166
+ <div class="topbar">
1167
+ <span class="page-title" id="topbar-title">Activity Log</span>
1168
+ <div class="topbar-right">
1169
+ <span class="topbar-badge" id="log-count">0 events</span>
1170
+ <button class="clear-btn" id="clear-btn">Clear</button>
1171
+ </div>
1172
+ </div>
1173
+
1174
+ <div class="content">
1175
+
1176
+ <!-- Activity Log Page -->
1177
+ <div class="page active" id="page-activity" style="padding:0; gap:0;">
1178
+ <div class="log-toolbar">
1179
+ <div class="filter-btns">
1180
+ <button class="filter-btn active" data-filter="all">All Logs</button>
1181
+ <button class="filter-btn" data-filter="task">Task / Command</button>
1182
+ <button class="filter-btn" data-filter="batch">Batch Builder</button>
1183
+ <button class="filter-btn" data-filter="security">Security / Scan</button>
1184
+ <button class="filter-btn" data-filter="response">Response</button>
1185
+ <button class="filter-btn" data-filter="error">Errors</button>
1186
+ <button class="filter-btn" data-filter="system">System</button>
1187
+ </div>
1188
+ <button class="scroll-lock-btn locked" id="scroll-lock-btn">
1189
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1190
+ <line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/>
1191
+ </svg>
556
1192
  Auto Scroll
557
1193
  </button>
558
1194
  </div>
559
1195
  <div class="log-list" id="log-list">
560
1196
  <div class="log-empty" id="log-empty">
561
1197
  <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
562
- <p>Menunggu aktivitas dari Roblox Studio...</p>
1198
+ <p>Menunggu aktivitas perintah dari AI / Roblox Studio...</p>
563
1199
  </div>
564
1200
  </div>
565
1201
  </div>
566
1202
 
567
- <!-- Statistics Page -->
1203
+ <!-- Diagnostics & Sessions Page -->
568
1204
  <div class="page" id="page-stats">
569
1205
  <div class="stats-grid">
570
1206
  <div class="stat-card">
@@ -592,6 +1228,33 @@
592
1228
  <div class="progress-bar error" id="bar-error" style="width:0%"></div>
593
1229
  </div>
594
1230
  </div>
1231
+
1232
+ <!-- Active Connected Studios -->
1233
+ <div class="stat-card full">
1234
+ <div class="stat-card-label" style="display:flex; justify-content:space-between; align-items:center;">
1235
+ <span>🎮 Active Studio Sessions</span>
1236
+ <span id="studios-count-badge" class="badge-v2">0 Terhubung</span>
1237
+ </div>
1238
+ <div id="studios-container" style="margin-top:12px;">
1239
+ <table class="sessions-table" id="sessions-table" style="display:none;">
1240
+ <thead>
1241
+ <tr>
1242
+ <th>Studio ID</th>
1243
+ <th>Place ID / Name</th>
1244
+ <th>Transport</th>
1245
+ <th>Status</th>
1246
+ <th>Last Ping</th>
1247
+ </tr>
1248
+ </thead>
1249
+ <tbody id="sessions-tbody"></tbody>
1250
+ </table>
1251
+ <div id="sessions-empty" style="padding:24px; text-align:center; color:var(--text-dim);">
1252
+ Tidak ada sesi Studio aktif saat ini. Buka game di Roblox Studio dan pastikan plugin <code>nvstudio_mcp</code> aktif.
1253
+ </div>
1254
+ </div>
1255
+ </div>
1256
+
1257
+ <!-- Diagnostics Summary -->
595
1258
  <div class="stat-card full">
596
1259
  <div class="stat-card-label">Session Diagnostics</div>
597
1260
  <div class="info-list">
@@ -601,14 +1264,14 @@
601
1264
  </div>
602
1265
  <div class="info-row">
603
1266
  <span class="info-row-label">Luau Pre-Validation</span>
604
- <span class="info-row-value" style="color:var(--success);">Active ✓</span>
1267
+ <span class="info-row-value" style="color:var(--success);">Active (AST-Protected) ✓</span>
605
1268
  </div>
606
1269
  <div class="info-row">
607
1270
  <span class="info-row-label">Port Server</span>
608
1271
  <span class="info-row-value">3055</span>
609
1272
  </div>
610
1273
  <div class="info-row">
611
- <span class="info-row-label">Studio Connection</span>
1274
+ <span class="info-row-label">Overall Studio Status</span>
612
1275
  <span class="info-row-value" id="info-studio">Disconnected</span>
613
1276
  </div>
614
1277
  <div class="info-row">
@@ -620,16 +1283,213 @@
620
1283
  </div>
621
1284
  </div>
622
1285
 
1286
+ <!-- Docs Explorer Page -->
1287
+ <div class="page" id="page-docs">
1288
+ <div class="docs-container">
1289
+ <div class="docs-search-card">
1290
+ <div style="font-size:16px; font-weight:600; color:#fff; display:flex; align-items:center; gap:8px;">
1291
+ <span>📚 Roblox Creator Docs Lookup</span>
1292
+ <span class="badge-v2">Official Engine API</span>
1293
+ </div>
1294
+ <p style="font-size:13px; color:var(--text-muted);">
1295
+ Cari dokumentasi resmi Class, Service, Datatype, Enum, atau Library Roblox langsung dari jembatan lokal (dilengkapi cache disk ~2 ms).
1296
+ </p>
1297
+ <form class="docs-form" id="docs-form">
1298
+ <input type="text" class="docs-input" id="docs-query" placeholder="Nama Class / Service (contoh: TweenService, CanvasGroup, UDim2, task)" required />
1299
+ <input type="text" class="docs-member-input" id="docs-member" placeholder="Member opsional (misal: Create)" />
1300
+ <button type="submit" class="docs-btn" id="docs-btn">
1301
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
1302
+ Cari Docs
1303
+ </button>
1304
+ </form>
1305
+ <div class="docs-chips">
1306
+ <span class="chip-label">Pencarian Cepat:</span>
1307
+ <span class="doc-chip" data-q="TweenService">TweenService</span>
1308
+ <span class="doc-chip" data-q="CanvasGroup">CanvasGroup</span>
1309
+ <span class="doc-chip" data-q="Frame">Frame</span>
1310
+ <span class="doc-chip" data-q="UDim2">UDim2</span>
1311
+ <span class="doc-chip" data-q="task">task</span>
1312
+ <span class="doc-chip" data-q="UserInputService">UserInputService</span>
1313
+ <span class="doc-chip" data-q="PathfindingService">PathfindingService</span>
1314
+ </div>
1315
+ </div>
1316
+
1317
+ <div class="docs-result-card" id="docs-result-card">
1318
+ <div class="docs-result-header">
1319
+ <span class="docs-title" id="docs-result-title">Hasil Dokumentasi</span>
1320
+ <button class="docs-copy-btn" id="docs-copy-btn" style="display:none;">Salin Markdown</button>
1321
+ </div>
1322
+ <div class="docs-content" id="docs-content">
1323
+ <span style="color:var(--text-dim); font-size:13px;">Ketik kata kunci class di atas atau klik salah satu chip pencarian cepat untuk menampilkan dokumentasi resmi Roblox Engine.</span>
1324
+ </div>
1325
+ </div>
1326
+ </div>
1327
+ </div>
1328
+
1329
+ <!-- VFX Database & Library Page -->
1330
+ <div class="page" id="page-vfx-database">
1331
+ <div class="vfx-container">
1332
+ <div class="vfx-header">
1333
+ <div class="vfx-header-info">
1334
+ <h2>✨ NV-VFX Library & Database <span class="v2-chip" id="vfx-hero-badge">Portable</span></h2>
1335
+ <p>Katalog efek visual portabel lintas-proyek Roblox (ParticleEmitter, Beam, Trail, Light). Pindai dari satu place dan pasang ke place lain secara instan.</p>
1336
+ </div>
1337
+ <div class="vfx-actions-bar">
1338
+ <div class="vfx-search-box">
1339
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
1340
+ <input type="text" id="vfx-search-input" placeholder="Cari VFX berdasarkan nama, tag, atau kategori...">
1341
+ </div>
1342
+ <button class="vfx-btn-primary" id="btn-open-scan-modal">
1343
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
1344
+ Scan dari Studio
1345
+ </button>
1346
+ <button class="vfx-btn-secondary" id="btn-refresh-vfx" title="Muat ulang data">
1347
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
1348
+ </button>
1349
+ </div>
1350
+ </div>
1351
+
1352
+ <!-- Category Pills Segment Bar -->
1353
+ <div class="vfx-category-tabs" id="vfx-category-tabs">
1354
+ <button class="vfx-cat-pill active" data-cat="All">Semua <span class="vfx-cat-count" id="vfx-count-all">0</span></button>
1355
+ </div>
1356
+
1357
+ <!-- Stats Overview Row -->
1358
+ <div class="vfx-stats-row">
1359
+ <div class="vfx-stat-box">
1360
+ <div class="vfx-stat-label">Total Efek Visual</div>
1361
+ <div class="vfx-stat-val" id="vfx-stat-total">0</div>
1362
+ <div class="vfx-stat-sub">Tersimpan di database MCP</div>
1363
+ </div>
1364
+ <div class="vfx-stat-box">
1365
+ <div class="vfx-stat-label">Kategori Aktif</div>
1366
+ <div class="vfx-stat-val" id="vfx-stat-categories" style="color:#00aaff;">0</div>
1367
+ <div class="vfx-stat-sub">Slash, Aura, Magic, Explosion dll</div>
1368
+ </div>
1369
+ <div class="vfx-stat-box">
1370
+ <div class="vfx-stat-label">Total Emitters</div>
1371
+ <div class="vfx-stat-val" id="vfx-stat-emitters" style="color:#f59e0b;">0</div>
1372
+ <div class="vfx-stat-sub">ParticleEmitter aktif</div>
1373
+ </div>
1374
+ <div class="vfx-stat-box">
1375
+ <div class="vfx-stat-label">Beams & Trails</div>
1376
+ <div class="vfx-stat-val" id="vfx-stat-beams" style="color:#10b981;">0</div>
1377
+ <div class="vfx-stat-sub">Elemen trajektori & sinar</div>
1378
+ </div>
1379
+ </div>
1380
+
1381
+ <!-- Cards Grid -->
1382
+ <div class="vfx-grid" id="vfx-cards-grid">
1383
+ <!-- Kartu VFX di-render secara dinamis oleh JavaScript -->
1384
+ </div>
1385
+ </div>
1386
+ </div>
1387
+
1388
+ <!-- Modal: Scan VFX dari Studio -->
1389
+ <div class="vfx-modal-backdrop" id="modal-scan-vfx">
1390
+ <div class="vfx-modal-card">
1391
+ <div class="vfx-modal-header">
1392
+ <h3>📥 Pindai VFX dari Studio Aktif</h3>
1393
+ <button class="vfx-modal-close" onclick="closeVfxModals()">&times;</button>
1394
+ </div>
1395
+ <div class="vfx-modal-body">
1396
+ <div class="vfx-form-group">
1397
+ <label class="vfx-form-label">Path Objek di Studio (Target)</label>
1398
+ <input type="text" class="vfx-form-input" id="scan-input-target" placeholder="contoh: Workspace.FlameBurst atau Selection" value="Workspace.">
1399
+ </div>
1400
+ <div class="vfx-form-group">
1401
+ <label class="vfx-form-label">Nama VFX</label>
1402
+ <input type="text" class="vfx-form-input" id="scan-input-name" placeholder="contoh: Crimson_Flame_Slash">
1403
+ </div>
1404
+ <div class="vfx-form-group">
1405
+ <label class="vfx-form-label">Kategori</label>
1406
+ <select class="vfx-form-select" id="scan-select-category">
1407
+ <option value="Slash">Slash (Tebasan & Bilah)</option>
1408
+ <option value="Explosion">Explosion (Ledakan & Api)</option>
1409
+ <option value="Aura">Aura (Glow & Perisai)</option>
1410
+ <option value="Magic">Magic (Sihir & Lingkaran Rune)</option>
1411
+ <option value="Beam">Beam (Laser & Sinar Energi)</option>
1412
+ <option value="Impact">Impact (Benturan & Hantaman)</option>
1413
+ <option value="Projectile">Projectile (Proyektil & Peluru)</option>
1414
+ <option value="Ambient">Ambient (Lingkungan & Partikel Halus)</option>
1415
+ </select>
1416
+ </div>
1417
+ <div class="vfx-form-group">
1418
+ <label class="vfx-form-label">Tags (Pisahkan dengan koma)</label>
1419
+ <input type="text" class="vfx-form-input" id="scan-input-tags" placeholder="contoh: anime, sword, fire, fast">
1420
+ </div>
1421
+ </div>
1422
+ <div class="vfx-modal-footer">
1423
+ <button class="vfx-btn-secondary" onclick="closeVfxModals()">Batal</button>
1424
+ <button class="vfx-btn-primary" id="btn-submit-scan">Mulai Pindai</button>
1425
+ </div>
1426
+ </div>
1427
+ </div>
1428
+
1429
+ <!-- Modal: Apply VFX ke Studio -->
1430
+ <div class="vfx-modal-backdrop" id="modal-apply-vfx">
1431
+ <div class="vfx-modal-card">
1432
+ <div class="vfx-modal-header">
1433
+ <h3>⚡ Terapkan VFX ke Studio</h3>
1434
+ <button class="vfx-modal-close" onclick="closeVfxModals()">&times;</button>
1435
+ </div>
1436
+ <div class="vfx-modal-body">
1437
+ <input type="hidden" id="apply-vfx-id">
1438
+ <div class="vfx-form-group">
1439
+ <label class="vfx-form-label">VFX Terpilih</label>
1440
+ <div id="apply-vfx-title" style="font-weight:600; color:var(--accent);"></div>
1441
+ </div>
1442
+ <div class="vfx-form-group">
1443
+ <label class="vfx-form-label">Target Instance di Studio</label>
1444
+ <input type="text" class="vfx-form-input" id="apply-input-target" placeholder="contoh: Workspace.Dummy.RightHand atau Workspace" value="Workspace">
1445
+ </div>
1446
+ <div class="vfx-form-group">
1447
+ <label class="vfx-form-label">Skala Ukuran (Scale Factor)</label>
1448
+ <input type="number" class="vfx-form-input" id="apply-input-scale" value="1.0" step="0.1" min="0.1" max="10">
1449
+ </div>
1450
+ <div class="vfx-form-group">
1451
+ <label class="vfx-form-label">Status Partikel (Enabled)</label>
1452
+ <select class="vfx-form-select" id="apply-select-enabled">
1453
+ <option value="default">Sesuai Preset / Default</option>
1454
+ <option value="true">Paksa Aktif (Enabled = true)</option>
1455
+ <option value="false">Paksa Nonaktif (Enabled = false)</option>
1456
+ </select>
1457
+ </div>
1458
+ </div>
1459
+ <div class="vfx-modal-footer">
1460
+ <button class="vfx-btn-secondary" onclick="closeVfxModals()">Batal</button>
1461
+ <button class="vfx-btn-primary" id="btn-submit-apply">Terapkan Sekarang</button>
1462
+ </div>
1463
+ </div>
1464
+ </div>
1465
+
1466
+ <!-- Modal: View JSON / Inspect Data -->
1467
+ <div class="vfx-modal-backdrop" id="modal-json-vfx">
1468
+ <div class="vfx-modal-card" style="max-width:700px;">
1469
+ <div class="vfx-modal-header">
1470
+ <h3 id="json-modal-title">Inspeksi Data Murni VFX</h3>
1471
+ <button class="vfx-modal-close" onclick="closeVfxModals()">&times;</button>
1472
+ </div>
1473
+ <div class="vfx-modal-body" style="padding:0;">
1474
+ <pre id="json-modal-content" style="background:#0d0d0d; padding:16px; margin:0; font-family:'JetBrains Mono', monospace; font-size:12px; color:#e0e0e0; overflow:auto; max-height:55vh;"></pre>
1475
+ </div>
1476
+ <div class="vfx-modal-footer">
1477
+ <button class="vfx-btn-secondary" id="btn-copy-vfx-json">Salin JSON</button>
1478
+ <button class="vfx-btn-primary" onclick="closeVfxModals()">Tutup</button>
1479
+ </div>
1480
+ </div>
1481
+ </div>
1482
+
623
1483
  <!-- About Page -->
624
1484
  <div class="page" id="page-about">
625
1485
  <div class="about-section">
626
1486
  <div class="about-hero">
627
- <h2>nvstudio-mcp <span class="v2-chip">v2.0.0</span></h2>
628
- <p>Jembatan pintar berbasis <strong>Model Context Protocol (MCP)</strong> yang menghubungkan AI Agent (Antigravity IDE, Cursor, Claude) secara langsung ke dalam <strong>Roblox Studio</strong>.</p>
1487
+ <h2>nvstudio-mcp <span class="v2-chip" id="about-version-chip">v2.4.0</span></h2>
1488
+ <p>Jembatan pintar berbasis <strong>Model Context Protocol (MCP)</strong> yang menghubungkan AI Agent (Antigravity IDE, Cursor, Claude Desktop, Windsurf) secara langsung ke dalam <strong>Roblox Studio</strong>.</p>
629
1489
  <div class="about-links">
630
1490
  <a class="about-link" href="https://github.com/Adiholick/nvstudio-mcp" target="_blank">
631
1491
  <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
632
- GitHub
1492
+ GitHub Repository
633
1493
  </a>
634
1494
  <a class="about-link" href="https://www.npmjs.com/package/@adiholick/nvstudio-mcp" target="_blank">
635
1495
  <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h24v24H0V0zm7.315 5.876H2.17v12.248H12v-9.6h-4.685V5.876zm9.515 0h-5.145v12.248h5.145V5.876zm-7.315 4.8H9.87v4.8H9.515v-4.8z"/></svg>
@@ -639,22 +1499,28 @@
639
1499
  </div>
640
1500
 
641
1501
  <div class="about-card">
642
- <h3>⚡ Arsitektur v2.0 Baru</h3>
1502
+ <h3>🚀 Fitur Baru v2.4.0</h3>
643
1503
  <ul>
644
- <li><strong>Zero-Delay Long-Polling:</strong> Plugin Studio kini menunggu task secara realtime hingga 20 detik tanpa putus. Memangkas latensi drastis menjadi &lt; 50ms (dibandingkan polling 1-detik di v1).</li>
645
- <li><strong>Luau Pre-Commit Validator:</strong> Mencegah engine Roblox crash dengan memvalidasi sintaks Luau <i>sebelum</i> kode dikirim ke Studio.</li>
646
- <li><strong>Partial Script Patching:</strong> AI kini menggunakan metode <code>patch_script_source</code> (pengeditan parsial per baris) yang jauh lebih cepat dan menghemat ribuan token.</li>
1504
+ <li><strong>🦴 NV-Rig Engine:</strong> Rigging headless presisi $O(1)$ untuk R6, R15, dan Custom Rig. Menyediakan inspeksi sendi (<code>inspect_rig</code>), pemindahan poros C0/C1 simultan anti-dislokasi (<code>edit_rig_joint</code>), auto-rigging instan (<code>auto_rig</code>), dan assembly welding senjata/prop (<code>weld_assembly</code>).</li>
1505
+ <li><strong>🎬 NV-Animation Engine:</strong> Pembuatan animasi karakter halus alami (Walk, Slash, Punch, Idle, IK) langsung menjadi <code>KeyframeSequence</code> resmi dengan interpolasi Bezier (Cubic/Quad) dan penegakan batas anatomis aman tanpa glitch.</li>
1506
+ <li><strong>✨ NV-VFX Subsystem & Database:</strong> Serialisasi efek visual (ParticleEmitter, Beam, Trail, PointLight) lintas-proyek dengan tool <code>scan_vfx</code> dan rekonstruksi instan via <code>apply_vfx</code>, dilengkapi web category browser interaktif.</li>
1507
+ <li><strong>⚡ Batch Instance Builder & Security:</strong> Membangun hierarki massal, sanitasi keamanan backdoor aset, dan live Creator Docs lookup.</li>
647
1508
  </ul>
648
1509
  </div>
649
1510
 
650
1511
  <div class="about-card">
651
- <h3>🛠️ Commands MCP Unggulan</h3>
1512
+ <h3>🛠️ Daftar Semantic MCP Tools Lengkap</h3>
652
1513
  <ul>
653
- <li><code>patch_script_source</code> — Mengedit bagian spesifik dari script (super cepat).</li>
654
- <li><code>execute_luau</code> — Menjalankan script Luau secara realtime di Studio.</li>
655
- <li><code>insert_asset</code> — Memasukkan model 3D/asset dari library Roblox.</li>
656
- <li><code>generate_terrain</code> — Men-generate atau memanipulasi terrain voxel.</li>
657
- <li><code>get_children</code> / <code>get_script_source</code> — Inspeksi hirarki dan source code.</li>
1514
+ <li><code>inspect_rig</code> / <code>edit_rig_joint</code> / <code>auto_rig</code> / <code>weld_assembly</code> — NV-Rig Engine headless.</li>
1515
+ <li><code>animate_character</code> / <code>solve_limb_ik</code> / <code>scrub_timeline</code> — NV-Animation Engine.</li>
1516
+ <li><code>scan_vfx</code> / <code>apply_vfx</code> / <code>list_vfx_database</code> / <code>delete_vfx</code> — NV-VFX Subsystem & Database.</li>
1517
+ <li><code>batch_create_instances</code> / <code>batch_set_properties</code> — Pembuatan objek/hierarki massal pohon bersarang.</li>
1518
+ <li><code>get_roblox_docs</code> — Lookup dokumentasi resmi engine Roblox (Class, Datatype, Service, Enum).</li>
1519
+ <li><code>preview_asset</code> / <code>scan_security</code> — Audit keamanan backdoor & sanitasi script model.</li>
1520
+ <li><code>patch_script_source</code> / <code>update_script_source</code> — Mengedit script Roblox secara terisolasi.</li>
1521
+ <li><code>execute_luau</code> — Eksekusi sembarang kode Luau dinamis di Studio.</li>
1522
+ <li><code>get_full_tree</code> / <code>get_children</code> — Eksplorasi hierarki DataModel.</li>
1523
+ <li><code>insert_asset</code> / <code>generate_terrain</code> — Memasukkan model atau membuat terrain voxel.</li>
658
1524
  </ul>
659
1525
  </div>
660
1526
  </div>
@@ -671,19 +1537,28 @@
671
1537
  let activeFilter = 'all';
672
1538
  let autoScroll = true;
673
1539
  let sessionStartTime = null;
1540
+ let latestDocsMarkdown = '';
674
1541
 
675
1542
  const logList = document.getElementById('log-list');
676
1543
  const logEmpty = document.getElementById('log-empty');
677
1544
  const logCount = document.getElementById('log-count');
678
1545
  const statusDot = document.getElementById('status-dot');
679
1546
  const statusLabel = document.getElementById('status-label');
1547
+ const pkgVersion = document.getElementById('pkg-version');
1548
+ const aboutVersionChip = document.getElementById('about-version-chip');
680
1549
 
681
1550
  /* ── Navigation ── */
682
1551
  const navItems = document.querySelectorAll('.nav-item');
683
1552
  const pages = document.querySelectorAll('.page');
684
1553
  const topbarTitle = document.getElementById('topbar-title');
685
1554
  const clearBtn = document.getElementById('clear-btn');
686
- const pageTitles = { activity: 'Activity Log', stats: 'Session Diagnostics', about: 'About v2.0' };
1555
+ const pageTitles = {
1556
+ activity: 'Activity Log',
1557
+ stats: 'Session Diagnostics & Studios',
1558
+ 'vfx-database': 'NV-VFX Database & Library',
1559
+ docs: 'Roblox Docs Explorer',
1560
+ about: 'About v2.4'
1561
+ };
687
1562
 
688
1563
  navItems.forEach(item => {
689
1564
  item.addEventListener('click', () => {
@@ -691,7 +1566,8 @@
691
1566
  navItems.forEach(n => n.classList.remove('active'));
692
1567
  pages.forEach(p => p.classList.remove('active'));
693
1568
  item.classList.add('active');
694
- document.getElementById('page-' + page).classList.add('active');
1569
+ const targetPage = document.getElementById('page-' + page);
1570
+ if (targetPage) targetPage.classList.add('active');
695
1571
  topbarTitle.textContent = pageTitles[page] || page;
696
1572
 
697
1573
  const isActivity = page === 'activity';
@@ -700,7 +1576,7 @@
700
1576
  });
701
1577
  });
702
1578
 
703
- /* ── Clear ── */
1579
+ /* ── Clear Logs ── */
704
1580
  clearBtn.addEventListener('click', () => {
705
1581
  allLogs = [];
706
1582
  renderLogs();
@@ -723,9 +1599,80 @@
723
1599
  scrollLockBtn.classList.toggle('locked', autoScroll);
724
1600
  });
725
1601
 
726
- /* ── Render ── */
1602
+ /* ── Categorize Log ── */
1603
+ function getLogCategory(log) {
1604
+ const msg = log.message || '';
1605
+ if (msg.includes('batch_create') || msg.includes('batch_set')) return 'batch';
1606
+ if (msg.includes('preview_asset') || msg.includes('scan_security') || msg.includes('security') || msg.includes('strip_scripts')) return 'security';
1607
+ if (msg.includes('get_roblox_docs')) return 'docs';
1608
+ if (msg.includes('patch_script_source')) return 'patch';
1609
+ if (msg.includes('execute_luau')) return 'luau';
1610
+ return log.type;
1611
+ }
1612
+
1613
+ /* ── Create Log Element ── */
1614
+ function createLogEl(log) {
1615
+ const el = document.createElement('div');
1616
+ el.className = 'log-entry';
1617
+ const cat = getLogCategory(log);
1618
+ el.dataset.category = cat;
1619
+
1620
+ let displayType = log.type.toUpperCase();
1621
+ let badgeClass = 'badge-' + log.type;
1622
+
1623
+ if (cat === 'batch') {
1624
+ displayType = 'BATCH';
1625
+ badgeClass = 'badge-batch';
1626
+ } else if (cat === 'security') {
1627
+ displayType = 'SECURITY';
1628
+ badgeClass = 'badge-security';
1629
+ } else if (cat === 'docs') {
1630
+ displayType = 'DOCS';
1631
+ badgeClass = 'badge-docs';
1632
+ } else if (cat === 'patch') {
1633
+ displayType = 'PATCH';
1634
+ badgeClass = 'badge-patch';
1635
+ } else if (cat === 'luau') {
1636
+ displayType = 'LUAU';
1637
+ badgeClass = 'badge-luau';
1638
+ } else if (log.type === 'error' && log.message.includes('Syntax Error')) {
1639
+ displayType = 'SYNTAX';
1640
+ badgeClass = 'badge-error';
1641
+ }
1642
+
1643
+ el.innerHTML = `
1644
+ <span class="log-entry-time">${log.time}</span>
1645
+ <span class="log-entry-badge ${badgeClass}">${displayType}</span>
1646
+ <span class="log-entry-msg">${escapeHtml(log.message)}</span>
1647
+ `;
1648
+ return el;
1649
+ }
1650
+
1651
+ function escapeHtml(str) {
1652
+ if (str === null || str === undefined) return '';
1653
+ return String(str)
1654
+ .replace(/&/g, '&amp;')
1655
+ .replace(/</g, '&lt;')
1656
+ .replace(/>/g, '&gt;')
1657
+ .replace(/"/g, '&quot;')
1658
+ .replace(/'/g, '&#39;');
1659
+ }
1660
+
1661
+ function shouldShowLog(log) {
1662
+ if (activeFilter === 'all') return true;
1663
+ const cat = getLogCategory(log);
1664
+ if (activeFilter === 'batch') return cat === 'batch';
1665
+ if (activeFilter === 'security') return cat === 'security';
1666
+ if (activeFilter === 'task') return log.type === 'task';
1667
+ if (activeFilter === 'response') return log.type === 'response';
1668
+ if (activeFilter === 'error') return log.type === 'error';
1669
+ if (activeFilter === 'system') return log.type === 'system';
1670
+ return false;
1671
+ }
1672
+
1673
+ /* ── Render Logs ── */
727
1674
  function renderLogs() {
728
- const filtered = activeFilter === 'all' ? allLogs : allLogs.filter(l => l.type === activeFilter);
1675
+ const filtered = allLogs.filter(shouldShowLog);
729
1676
  logCount.textContent = `${allLogs.length} event${allLogs.length !== 1 ? 's' : ''}`;
730
1677
 
731
1678
  if (filtered.length === 0) {
@@ -748,37 +1695,12 @@
748
1695
  if (autoScroll) logList.scrollTop = 0;
749
1696
  }
750
1697
 
751
- function createLogEl(log) {
752
- const el = document.createElement('div');
753
- el.className = 'log-entry';
754
- el.dataset.type = log.type;
755
-
756
- let displayType = log.type.toUpperCase();
757
- // Custom badges for v2 features based on message content
758
- if (log.type === 'task' && log.message.includes('patch_script_source')) {
759
- displayType = 'PATCH';
760
- } else if (log.type === 'error' && log.message.includes('Syntax Error')) {
761
- displayType = 'SYNTAX';
762
- }
763
-
764
- el.innerHTML = `
765
- <span class="log-entry-time">${log.time}</span>
766
- <span class="log-entry-badge badge-${log.type}">${displayType}</span>
767
- <span class="log-entry-msg">${escapeHtml(log.message)}</span>
768
- `;
769
- return el;
770
- }
771
-
772
- function escapeHtml(str) {
773
- return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
774
- }
775
-
776
1698
  function prependLog(log) {
777
1699
  allLogs.unshift(log);
778
- if (allLogs.length > 1000) allLogs.pop(); // Keep max 1000 logs in memory
1700
+ if (allLogs.length > 1000) allLogs.pop();
779
1701
  logCount.textContent = `${allLogs.length} event${allLogs.length !== 1 ? 's' : ''}`;
780
1702
 
781
- if (activeFilter !== 'all' && log.type !== activeFilter) return;
1703
+ if (!shouldShowLog(log)) return;
782
1704
 
783
1705
  logEmpty.style.display = 'none';
784
1706
  const el = createLogEl(log);
@@ -789,17 +1711,69 @@
789
1711
  if (autoScroll) logList.scrollTop = 0;
790
1712
  }
791
1713
 
792
- /* ── Stats ── */
1714
+ /* ── Update Connected Studios Table ── */
1715
+ function updateStudiosTable(studios) {
1716
+ const tbody = document.getElementById('sessions-tbody');
1717
+ const table = document.getElementById('sessions-table');
1718
+ const empty = document.getElementById('sessions-empty');
1719
+ const badge = document.getElementById('studios-count-badge');
1720
+
1721
+ const list = Array.isArray(studios) ? studios : [];
1722
+ badge.textContent = `${list.length} Terhubung`;
1723
+
1724
+ if (list.length === 0) {
1725
+ table.style.display = 'none';
1726
+ empty.style.display = 'block';
1727
+ return;
1728
+ }
1729
+
1730
+ empty.style.display = 'none';
1731
+ table.style.display = 'table';
1732
+ tbody.innerHTML = '';
1733
+
1734
+ const now = Date.now();
1735
+ list.forEach(s => {
1736
+ const tr = document.createElement('tr');
1737
+ const sidShort = s.studioId ? (s.studioId.length > 12 ? s.studioId.substring(0, 10) + '…' : s.studioId) : 'anon';
1738
+ const placeText = (s.placeId && s.placeId > 0) ? `Place #${s.placeId}` : 'Local Place (ID: 0)';
1739
+ const isWs = s.transport === 'websocket';
1740
+ const transportBadge = isWs
1741
+ ? '<span class="badge-pill badge-ws">WebSocket ⚡</span>'
1742
+ : '<span class="badge-pill badge-poll">Long-Poll 🔄</span>';
1743
+ const statusBadge = s.isBusy
1744
+ ? '<span style="color:var(--warning);">● Sibuk</span>'
1745
+ : '<span style="color:var(--success);">● Siap</span>';
1746
+ const elapsed = s.lastSeen ? Math.max(0, Math.round((now - s.lastSeen) / 1000)) + 'd lalu' : 'Baru saja';
1747
+
1748
+ tr.innerHTML = `
1749
+ <td title="${s.studioId || ''}"><code>${sidShort}</code></td>
1750
+ <td><strong>${placeText}</strong></td>
1751
+ <td>${transportBadge}</td>
1752
+ <td>${statusBadge}</td>
1753
+ <td style="color:var(--text-dim); font-size:12px;">${elapsed}</td>
1754
+ `;
1755
+ tbody.appendChild(tr);
1756
+ });
1757
+ }
1758
+
1759
+ /* ── Update Statistics ── */
793
1760
  function updateStats(s) {
794
- document.getElementById('stat-total').textContent = s.totalTasks;
795
- document.getElementById('stat-success').textContent = s.successCount;
796
- document.getElementById('stat-error').textContent = s.errorCount;
1761
+ if (!s) return;
1762
+ if (s.totalTasks !== undefined) document.getElementById('stat-total').textContent = s.totalTasks;
1763
+ if (s.successCount !== undefined) document.getElementById('stat-success').textContent = s.successCount;
1764
+ if (s.errorCount !== undefined) document.getElementById('stat-error').textContent = s.errorCount;
1765
+
1766
+ if (s.version) {
1767
+ if (pkgVersion) pkgVersion.textContent = 'v' + s.version;
1768
+ if (aboutVersionChip) aboutVersionChip.textContent = 'v' + s.version;
1769
+ }
797
1770
 
798
- const rate = s.totalTasks > 0 ? Math.round((s.successCount / s.totalTasks) * 100) : null;
1771
+ const total = s.totalTasks || 0;
1772
+ const rate = total > 0 ? Math.round(((s.successCount || 0) / total) * 100) : null;
799
1773
  document.getElementById('stat-rate').textContent = rate !== null ? rate + '%' : '—';
800
1774
  document.getElementById('bar-success').style.width = (rate || 0) + '%';
801
1775
 
802
- const errRate = s.totalTasks > 0 ? Math.round((s.errorCount / s.totalTasks) * 100) : 0;
1776
+ const errRate = total > 0 ? Math.round(((s.errorCount || 0) / total) * 100) : 0;
803
1777
  document.getElementById('bar-error').style.width = errRate + '%';
804
1778
 
805
1779
  if (s.sessionStart) {
@@ -809,7 +1783,7 @@
809
1783
  const statusEl = document.getElementById('info-studio');
810
1784
  const transportEl = document.getElementById('info-transport');
811
1785
  if (s.studioConnected) {
812
- statusEl.textContent = 'Connected ✓';
1786
+ statusEl.textContent = `Connected ✓ (${s.studioCount || 1} Studio)`;
813
1787
  statusEl.style.color = 'var(--success)';
814
1788
  if (s.wsStudios && s.wsStudios > 0) {
815
1789
  if (transportEl) {
@@ -817,7 +1791,7 @@
817
1791
  transportEl.style.color = 'var(--success)';
818
1792
  }
819
1793
  } else if (transportEl) {
820
- transportEl.textContent = 'Long-Polling (Fallback)';
1794
+ transportEl.textContent = 'Long-Polling (Active Fallback)';
821
1795
  transportEl.style.color = 'var(--accent)';
822
1796
  }
823
1797
  } else {
@@ -828,9 +1802,13 @@
828
1802
  transportEl.style.color = 'var(--accent)';
829
1803
  }
830
1804
  }
1805
+
1806
+ if (s.connectedStudios) {
1807
+ updateStudiosTable(s.connectedStudios);
1808
+ }
831
1809
  }
832
1810
 
833
- /* ── Uptime ── */
1811
+ /* ── Uptime Timer ── */
834
1812
  setInterval(() => {
835
1813
  if (!sessionStartTime) return;
836
1814
  const diff = Math.floor((Date.now() - sessionStartTime) / 1000);
@@ -840,14 +1818,15 @@
840
1818
  document.getElementById('info-uptime').textContent = `${h}:${m}:${s}`;
841
1819
  }, 1000);
842
1820
 
843
- /* ── Studio Status ── */
844
- function setStudioStatus(connected) {
1821
+ /* ── Studio Status Indicator ── */
1822
+ function setStudioStatus(connected, studioCount) {
845
1823
  statusDot.className = 'status-dot ' + (connected ? 'connected' : 'waiting');
846
- statusLabel.textContent = connected ? 'Studio Connected' : 'Menunggu Studio...';
1824
+ const countText = studioCount && studioCount > 1 ? ` (${studioCount} Studios)` : '';
1825
+ statusLabel.textContent = connected ? `Studio Connected${countText}` : 'Menunggu Studio...';
847
1826
 
848
1827
  const statusEl = document.getElementById('info-studio');
849
1828
  if (connected) {
850
- statusEl.textContent = 'Connected ✓';
1829
+ statusEl.textContent = `Connected ✓${countText}`;
851
1830
  statusEl.style.color = 'var(--success)';
852
1831
  } else {
853
1832
  statusEl.textContent = 'Disconnected';
@@ -855,7 +1834,432 @@
855
1834
  }
856
1835
  }
857
1836
 
858
- /* ── Socket Events ── */
1837
+ /* ── Simple Markdown Formatter for Docs ── */
1838
+ function formatMarkdown(md) {
1839
+ if (!md) return '';
1840
+ let html = escapeHtml(md);
1841
+
1842
+ // Fenced code blocks ```
1843
+ html = html.replace(/```([a-zA-Z]*)\n([\s\S]*?)```/g, function(match, lang, code) {
1844
+ return '<pre><code>' + code + '</code></pre>';
1845
+ });
1846
+
1847
+ // Inline code `...`
1848
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
1849
+
1850
+ // Headers
1851
+ html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
1852
+ html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
1853
+ html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
1854
+
1855
+ // Bold & Italic
1856
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1857
+ html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
1858
+
1859
+ // Unordered Lists
1860
+ html = html.replace(/^\- (.*$)/gim, '<li>$1</li>');
1861
+ html = html.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>');
1862
+
1863
+ // Clean double <ul> tags
1864
+ html = html.replace(/<\/ul>\s*<ul>/g, '');
1865
+
1866
+ // Paragraph breaks
1867
+ html = html.replace(/\n\n/g, '<br><br>');
1868
+
1869
+ return html;
1870
+ }
1871
+
1872
+ /* ── Docs Explorer Handler ── */
1873
+ const docsForm = document.getElementById('docs-form');
1874
+ const docsQuery = document.getElementById('docs-query');
1875
+ const docsMember = document.getElementById('docs-member');
1876
+ const docsBtn = document.getElementById('docs-btn');
1877
+ const docsContent = document.getElementById('docs-content');
1878
+ const docsResultTitle = document.getElementById('docs-result-title');
1879
+ const docsCopyBtn = document.getElementById('docs-copy-btn');
1880
+
1881
+ async function searchDocs(query, member) {
1882
+ if (!query) return;
1883
+ docsQuery.value = query;
1884
+ if (member) docsMember.value = member;
1885
+
1886
+ docsBtn.disabled = true;
1887
+ docsBtn.innerHTML = 'Mencari...';
1888
+ docsContent.innerHTML = '<span style="color:var(--accent);">Mengambil dokumentasi resmi Creator Docs...</span>';
1889
+
1890
+ try {
1891
+ const url = new URL('/api/docs', window.location.origin);
1892
+ url.searchParams.set('query', query);
1893
+ if (member) url.searchParams.set('member', member);
1894
+
1895
+ const res = await fetch(url.toString());
1896
+ const data = await res.json();
1897
+
1898
+ if (!res.ok || data.error) {
1899
+ docsContent.innerHTML = `<span style="color:var(--error);">Error: ${escapeHtml(data.error || 'Gagal mengambil dokumentasi')}</span>`;
1900
+ docsCopyBtn.style.display = 'none';
1901
+ } else {
1902
+ latestDocsMarkdown = data.result;
1903
+ docsResultTitle.innerHTML = `Dokumentasi: <code>${escapeHtml(query)}${member ? '.' + escapeHtml(member) : ''}</code>`;
1904
+ docsContent.innerHTML = formatMarkdown(data.result);
1905
+ docsCopyBtn.style.display = 'inline-block';
1906
+ }
1907
+ } catch (err) {
1908
+ docsContent.innerHTML = `<span style="color:var(--error);">Koneksi gagal: ${escapeHtml(err.message)}</span>`;
1909
+ docsCopyBtn.style.display = 'none';
1910
+ } finally {
1911
+ docsBtn.disabled = false;
1912
+ docsBtn.innerHTML = `
1913
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
1914
+ Cari Docs
1915
+ `;
1916
+ }
1917
+ }
1918
+
1919
+ docsForm.addEventListener('submit', (e) => {
1920
+ e.preventDefault();
1921
+ searchDocs(docsQuery.value.trim(), docsMember.value.trim());
1922
+ });
1923
+
1924
+ document.querySelectorAll('.doc-chip').forEach(chip => {
1925
+ chip.addEventListener('click', () => {
1926
+ const q = chip.dataset.q;
1927
+ searchDocs(q, '');
1928
+ });
1929
+ });
1930
+
1931
+ docsCopyBtn.addEventListener('click', () => {
1932
+ if (latestDocsMarkdown) {
1933
+ navigator.clipboard.writeText(latestDocsMarkdown);
1934
+ docsCopyBtn.textContent = 'Tersalin ✓';
1935
+ setTimeout(() => { docsCopyBtn.textContent = 'Salin Markdown'; }, 2000);
1936
+ }
1937
+ });
1938
+
1939
+ /* ── VFX Database & Library Controller ── */
1940
+ let currentVfxItems = [];
1941
+ let currentVfxCategories = [];
1942
+ let activeVfxCategory = 'All';
1943
+ let vfxSearchTimeout = null;
1944
+
1945
+ const vfxSidebarCount = document.getElementById('vfx-sidebar-count');
1946
+ const vfxCategoryTabs = document.getElementById('vfx-category-tabs');
1947
+ const vfxCardsGrid = document.getElementById('vfx-cards-grid');
1948
+ const vfxSearchInput = document.getElementById('vfx-search-input');
1949
+ const vfxStatTotal = document.getElementById('vfx-stat-total');
1950
+ const vfxStatCategories = document.getElementById('vfx-stat-categories');
1951
+ const vfxStatEmitters = document.getElementById('vfx-stat-emitters');
1952
+ const vfxStatBeams = document.getElementById('vfx-stat-beams');
1953
+
1954
+ const modalScanVfx = document.getElementById('modal-scan-vfx');
1955
+ const modalApplyVfx = document.getElementById('modal-apply-vfx');
1956
+ const modalJsonVfx = document.getElementById('modal-json-vfx');
1957
+
1958
+ window.closeVfxModals = function() {
1959
+ modalScanVfx.classList.remove('open');
1960
+ modalApplyVfx.classList.remove('open');
1961
+ modalJsonVfx.classList.remove('open');
1962
+ };
1963
+
1964
+ window.openApplyModal = function(id) {
1965
+ const decodedId = decodeURIComponent(id);
1966
+ const item = currentVfxItems.find(i => i.id === decodedId);
1967
+ if (!item) return;
1968
+ document.getElementById('apply-vfx-id').value = item.id;
1969
+ document.getElementById('apply-vfx-title').textContent = item.name;
1970
+ modalApplyVfx.classList.add('open');
1971
+ };
1972
+
1973
+ window.openJsonModal = function(id) {
1974
+ const decodedId = decodeURIComponent(id);
1975
+ const item = currentVfxItems.find(i => i.id === decodedId);
1976
+ if (!item) return;
1977
+ document.getElementById('json-modal-title').textContent = 'Pure Data: ' + item.name;
1978
+ document.getElementById('json-modal-content').textContent = JSON.stringify(item.data, null, 2);
1979
+ modalJsonVfx.classList.add('open');
1980
+ };
1981
+
1982
+ window.deleteVfxItem = async function(id) {
1983
+ const decodedId = decodeURIComponent(id);
1984
+ const item = currentVfxItems.find(i => i.id === decodedId);
1985
+ const name = item ? item.name : decodedId;
1986
+ if (!confirm(`Apakah Anda yakin ingin menghapus VFX "${name}" dari database?`)) return;
1987
+ try {
1988
+ const res = await fetch('/api/vfx/' + encodeURIComponent(decodedId), { method: 'DELETE' });
1989
+ if (res.ok) {
1990
+ loadVfxDatabase();
1991
+ } else {
1992
+ alert('Gagal menghapus VFX');
1993
+ }
1994
+ } catch (err) {
1995
+ alert('Error: ' + err.message);
1996
+ }
1997
+ };
1998
+
1999
+ async function loadVfxDatabase() {
2000
+ try {
2001
+ const [vfxRes, catRes] = await Promise.all([
2002
+ fetch('/api/vfx' + (activeVfxCategory !== 'All' ? `?category=${encodeURIComponent(activeVfxCategory)}` : '')),
2003
+ fetch('/api/vfx/categories')
2004
+ ]);
2005
+ if (!vfxRes.ok || !catRes.ok) return;
2006
+
2007
+ const vfxData = await vfxRes.json();
2008
+ const catData = await catRes.json();
2009
+
2010
+ currentVfxItems = vfxData.items || [];
2011
+ currentVfxCategories = catData.categories || [];
2012
+
2013
+ renderVfxCategories();
2014
+ renderVfxStats();
2015
+ renderVfxCards();
2016
+
2017
+ // Hitung total di sidebar
2018
+ let totalCount = 0;
2019
+ currentVfxCategories.forEach(c => totalCount += c.count);
2020
+ if (vfxSidebarCount) vfxSidebarCount.textContent = totalCount;
2021
+ } catch (err) {
2022
+ console.error('Gagal memuat VFX Database:', err);
2023
+ }
2024
+ }
2025
+
2026
+ function renderVfxCategories() {
2027
+ if (!vfxCategoryTabs) return;
2028
+ let totalCount = 0;
2029
+ currentVfxCategories.forEach(c => totalCount += c.count);
2030
+
2031
+ let html = `
2032
+ <button class="vfx-cat-pill ${activeVfxCategory === 'All' ? 'active' : ''}" data-cat="All">
2033
+ Semua <span class="vfx-cat-count">${totalCount}</span>
2034
+ </button>
2035
+ `;
2036
+
2037
+ currentVfxCategories.forEach(cat => {
2038
+ html += `
2039
+ <button class="vfx-cat-pill ${activeVfxCategory === cat.name ? 'active' : ''}" data-cat="${escapeHtml(cat.name)}">
2040
+ ${escapeHtml(cat.name)} <span class="vfx-cat-count">${cat.count}</span>
2041
+ </button>
2042
+ `;
2043
+ });
2044
+
2045
+ vfxCategoryTabs.innerHTML = html;
2046
+
2047
+ vfxCategoryTabs.querySelectorAll('.vfx-cat-pill').forEach(btn => {
2048
+ btn.addEventListener('click', () => {
2049
+ activeVfxCategory = btn.dataset.cat;
2050
+ vfxCategoryTabs.querySelectorAll('.vfx-cat-pill').forEach(b => b.classList.remove('active'));
2051
+ btn.classList.add('active');
2052
+ loadVfxDatabase();
2053
+ });
2054
+ });
2055
+ }
2056
+
2057
+ function renderVfxStats() {
2058
+ let totalCount = 0;
2059
+ let totalEmitters = 0;
2060
+ let totalBeams = 0;
2061
+
2062
+ currentVfxItems.forEach(item => {
2063
+ totalCount++;
2064
+ if (item.stats) {
2065
+ totalEmitters += (item.stats.emitters || 0);
2066
+ totalBeams += ((item.stats.beams || 0) + (item.stats.trails || 0));
2067
+ }
2068
+ });
2069
+
2070
+ if (vfxStatTotal) vfxStatTotal.textContent = totalCount;
2071
+ if (vfxStatCategories) vfxStatCategories.textContent = currentVfxCategories.length;
2072
+ if (vfxStatEmitters) vfxStatEmitters.textContent = totalEmitters;
2073
+ if (vfxStatBeams) vfxStatBeams.textContent = totalBeams;
2074
+ }
2075
+
2076
+ function renderVfxCards() {
2077
+ if (!vfxCardsGrid) return;
2078
+ if (currentVfxItems.length === 0) {
2079
+ vfxCardsGrid.innerHTML = `
2080
+ <div style="grid-column: 1 / -1; padding: 48px; text-align: center; color: var(--text-dim); background: var(--surface-card); border: 1px dashed var(--border); border-radius: 12px;">
2081
+ <div style="font-size: 32px; margin-bottom: 8px;">✨</div>
2082
+ <div style="font-size: 15px; font-weight: 600; color: var(--text);">Belum ada efek visual di kategori ini</div>
2083
+ <div style="font-size: 13px; margin-top: 4px;">Klik tombol "Scan dari Studio" di atas untuk memindai partikel dari proyek aktif Anda.</div>
2084
+ </div>
2085
+ `;
2086
+ return;
2087
+ }
2088
+
2089
+ const filterQuery = (vfxSearchInput ? vfxSearchInput.value.toLowerCase().trim() : '');
2090
+ const items = filterQuery
2091
+ ? currentVfxItems.filter(i =>
2092
+ i.name.toLowerCase().includes(filterQuery) ||
2093
+ i.category.toLowerCase().includes(filterQuery) ||
2094
+ (i.description && i.description.toLowerCase().includes(filterQuery)) ||
2095
+ (i.tags && i.tags.some(t => t.toLowerCase().includes(filterQuery))))
2096
+ : currentVfxItems;
2097
+
2098
+ vfxCardsGrid.innerHTML = items.map(item => {
2099
+ const dateStr = item.createdAt ? new Date(item.createdAt).toLocaleDateString('id-ID', { day:'numeric', month:'short', year:'numeric' }) : 'NVStudio';
2100
+ const stats = item.stats || { emitters: 0, beams: 0, trails: 0, lights: 0 };
2101
+ const compPills = [];
2102
+ if (stats.emitters > 0) compPills.push(`${stats.emitters} Emitters`);
2103
+ if (stats.beams > 0) compPills.push(`${stats.beams} Beams`);
2104
+ if (stats.trails > 0) compPills.push(`${stats.trails} Trails`);
2105
+ if (stats.lights > 0) compPills.push(`${stats.lights} Lights`);
2106
+ if (compPills.length === 0) compPills.push(`${item.nodeCount || 1} Nodes`);
2107
+
2108
+ const tagChips = (item.tags || []).slice(0, 4).map(t => `<span class="vfx-tag-chip">#${escapeHtml(t)}</span>`).join('');
2109
+
2110
+ return `
2111
+ <div class="vfx-card">
2112
+ <div class="vfx-card-top">
2113
+ <div class="vfx-card-title-group">
2114
+ <h4>${escapeHtml(item.name)}</h4>
2115
+ <div class="vfx-card-date">${escapeHtml(dateStr)} • ${escapeHtml(item.author || 'NVStudio')}</div>
2116
+ </div>
2117
+ <span class="vfx-badge-cat vfx-badge-${escapeHtml(item.category || 'Ambient')}">${escapeHtml(item.category || 'Ambient')}</span>
2118
+ </div>
2119
+
2120
+ <div class="vfx-card-desc">${escapeHtml(item.description || 'Efek visual siap pakai untuk proyek Roblox.')}</div>
2121
+
2122
+ <div class="vfx-composition-badges">
2123
+ ${compPills.map(p => `<span class="vfx-comp-pill">${escapeHtml(p)}</span>`).join('')}
2124
+ </div>
2125
+
2126
+ ${tagChips ? `<div class="vfx-card-tags">${tagChips}</div>` : ''}
2127
+
2128
+ <div class="vfx-card-footer">
2129
+ <button class="vfx-btn-apply" onclick="openApplyModal('${encodeURIComponent(item.id)}')">
2130
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
2131
+ Apply ke Studio
2132
+ </button>
2133
+ <button class="vfx-btn-json" title="Inspeksi Data JSON" onclick="openJsonModal('${encodeURIComponent(item.id)}')">
2134
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
2135
+ </button>
2136
+ <button class="vfx-btn-del" title="Hapus dari Database" onclick="deleteVfxItem('${encodeURIComponent(item.id)}')">
2137
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
2138
+ </button>
2139
+ </div>
2140
+ </div>
2141
+ `;
2142
+ }).join('');
2143
+ }
2144
+
2145
+ // Search debounce
2146
+ if (vfxSearchInput) {
2147
+ vfxSearchInput.addEventListener('input', () => {
2148
+ clearTimeout(vfxSearchTimeout);
2149
+ vfxSearchTimeout = setTimeout(() => {
2150
+ renderVfxCards();
2151
+ }, 180);
2152
+ });
2153
+ }
2154
+
2155
+ // Modal triggers & handlers
2156
+ const btnOpenScanModal = document.getElementById('btn-open-scan-modal');
2157
+ if (btnOpenScanModal) {
2158
+ btnOpenScanModal.addEventListener('click', () => {
2159
+ modalScanVfx.classList.add('open');
2160
+ });
2161
+ }
2162
+
2163
+ const btnRefreshVfx = document.getElementById('btn-refresh-vfx');
2164
+ if (btnRefreshVfx) {
2165
+ btnRefreshVfx.addEventListener('click', () => {
2166
+ loadVfxDatabase();
2167
+ });
2168
+ }
2169
+
2170
+ // Submit Scan form
2171
+ const btnSubmitScan = document.getElementById('btn-submit-scan');
2172
+ if (btnSubmitScan) {
2173
+ btnSubmitScan.addEventListener('click', async () => {
2174
+ const target = document.getElementById('scan-input-target').value.trim();
2175
+ const name = document.getElementById('scan-input-name').value.trim();
2176
+ const category = document.getElementById('scan-select-category').value;
2177
+ const tags = document.getElementById('scan-input-tags').value.split(',').map(t => t.trim()).filter(Boolean);
2178
+
2179
+ if (!target) {
2180
+ alert('Harap isi path target di Studio');
2181
+ return;
2182
+ }
2183
+
2184
+ btnSubmitScan.disabled = true;
2185
+ btnSubmitScan.textContent = 'Memindai...';
2186
+ try {
2187
+ const res = await fetch('/api/vfx/scan', {
2188
+ method: 'POST',
2189
+ headers: { 'Content-Type': 'application/json' },
2190
+ body: JSON.stringify({ target, name, category, tags })
2191
+ });
2192
+ const data = await res.json();
2193
+ if (res.ok && data.success) {
2194
+ closeVfxModals();
2195
+ loadVfxDatabase();
2196
+ alert(`✅ Sukses memindai VFX "${data.item.name}" (${data.item.nodeCount} node)!`);
2197
+ } else {
2198
+ alert('Gagal scan: ' + (data.error || 'Unknown error'));
2199
+ }
2200
+ } catch (err) {
2201
+ alert('Error: ' + err.message);
2202
+ } finally {
2203
+ btnSubmitScan.disabled = false;
2204
+ btnSubmitScan.textContent = 'Mulai Pindai';
2205
+ }
2206
+ });
2207
+ }
2208
+
2209
+ // Submit Apply form
2210
+ const btnSubmitApply = document.getElementById('btn-submit-apply');
2211
+ if (btnSubmitApply) {
2212
+ btnSubmitApply.addEventListener('click', async () => {
2213
+ const id = document.getElementById('apply-vfx-id').value;
2214
+ const target = document.getElementById('apply-input-target').value.trim();
2215
+ const scale = parseFloat(document.getElementById('apply-input-scale').value) || 1.0;
2216
+ const enabledVal = document.getElementById('apply-select-enabled').value;
2217
+ const enabled = enabledVal === 'default' ? undefined : (enabledVal === 'true');
2218
+
2219
+ if (!target) {
2220
+ alert('Harap isi target di Studio');
2221
+ return;
2222
+ }
2223
+
2224
+ btnSubmitApply.disabled = true;
2225
+ btnSubmitApply.textContent = 'Menerapkan...';
2226
+ try {
2227
+ const res = await fetch('/api/vfx/apply', {
2228
+ method: 'POST',
2229
+ headers: { 'Content-Type': 'application/json' },
2230
+ body: JSON.stringify({ id, target, scale, enabled })
2231
+ });
2232
+ const data = await res.json();
2233
+ if (res.ok && data.success) {
2234
+ closeVfxModals();
2235
+ alert(`✅ VFX berhasil diterapkan ke "${target}" di Studio!`);
2236
+ } else {
2237
+ alert('Gagal apply: ' + (data.error || 'Unknown error'));
2238
+ }
2239
+ } catch (err) {
2240
+ alert('Error: ' + err.message);
2241
+ } finally {
2242
+ btnSubmitApply.disabled = false;
2243
+ btnSubmitApply.textContent = 'Terapkan Sekarang';
2244
+ }
2245
+ });
2246
+ }
2247
+
2248
+ // Copy JSON button
2249
+ const btnCopyVfxJson = document.getElementById('btn-copy-vfx-json');
2250
+ if (btnCopyVfxJson) {
2251
+ btnCopyVfxJson.addEventListener('click', () => {
2252
+ const content = document.getElementById('json-modal-content').textContent;
2253
+ navigator.clipboard.writeText(content);
2254
+ btnCopyVfxJson.textContent = 'Tersalin ✓';
2255
+ setTimeout(() => { btnCopyVfxJson.textContent = 'Salin JSON'; }, 1500);
2256
+ });
2257
+ }
2258
+
2259
+ // Muat VFX Database saat pertama kali dibuka
2260
+ loadVfxDatabase();
2261
+
2262
+ /* ── Socket.IO Event Listeners ── */
859
2263
  socket.on('connect', () => {});
860
2264
  socket.on('disconnect', () => setStudioStatus(false));
861
2265
 
@@ -864,7 +2268,7 @@
864
2268
  renderLogs();
865
2269
  if (stats) {
866
2270
  updateStats(stats);
867
- if (stats.studioConnected) setStudioStatus(true);
2271
+ if (stats.studioConnected) setStudioStatus(true, stats.studioCount);
868
2272
  }
869
2273
  });
870
2274
 
@@ -876,9 +2280,15 @@
876
2280
  updateStats(s);
877
2281
  });
878
2282
 
879
- socket.on('studio-status', ({ connected }) => {
880
- setStudioStatus(connected);
2283
+ socket.on('vfx_updated', () => {
2284
+ loadVfxDatabase();
2285
+ });
2286
+
2287
+ socket.on('studio-status', ({ connected, studioCount, connectedStudios, version }) => {
2288
+ setStudioStatus(connected, studioCount);
2289
+ if (connectedStudios) updateStudiosTable(connectedStudios);
2290
+ if (version && pkgVersion) pkgVersion.textContent = 'v' + version;
881
2291
  });
882
2292
  </script>
883
2293
  </body>
884
- </html>
2294
+ </html>