voyageai-cli 1.28.0 → 1.30.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 (58) hide show
  1. package/README.md +82 -8
  2. package/package.json +2 -1
  3. package/src/commands/app.js +15 -0
  4. package/src/commands/benchmark.js +22 -8
  5. package/src/commands/chat.js +18 -0
  6. package/src/commands/chunk.js +10 -0
  7. package/src/commands/demo.js +4 -0
  8. package/src/commands/embed.js +13 -0
  9. package/src/commands/estimate.js +3 -0
  10. package/src/commands/eval.js +6 -0
  11. package/src/commands/explain.js +2 -0
  12. package/src/commands/generate.js +2 -0
  13. package/src/commands/ingest.js +4 -0
  14. package/src/commands/init.js +2 -0
  15. package/src/commands/mcp-server.js +2 -0
  16. package/src/commands/models.js +2 -0
  17. package/src/commands/ping.js +7 -0
  18. package/src/commands/pipeline.js +15 -0
  19. package/src/commands/playground.js +685 -8
  20. package/src/commands/query.js +16 -0
  21. package/src/commands/rerank.js +12 -0
  22. package/src/commands/scaffold.js +2 -0
  23. package/src/commands/search.js +11 -0
  24. package/src/commands/similarity.js +9 -0
  25. package/src/commands/store.js +4 -0
  26. package/src/commands/workflow.js +702 -13
  27. package/src/lib/capability-report.js +134 -0
  28. package/src/lib/chat.js +32 -1
  29. package/src/lib/config.js +2 -0
  30. package/src/lib/cost-display.js +107 -0
  31. package/src/lib/explanations.js +94 -0
  32. package/src/lib/llm.js +125 -18
  33. package/src/lib/npm-utils.js +265 -0
  34. package/src/lib/quality-audit.js +71 -0
  35. package/src/lib/security/blocked-domains.json +17 -0
  36. package/src/lib/security-audit.js +198 -0
  37. package/src/lib/telemetry.js +23 -1
  38. package/src/lib/workflow-registry.js +416 -0
  39. package/src/lib/workflow-scaffold.js +380 -0
  40. package/src/lib/workflow-test-runner.js +208 -0
  41. package/src/lib/workflow.js +559 -7
  42. package/src/playground/announcements.md +80 -0
  43. package/src/playground/assets/announcements/appstore.jpg +0 -0
  44. package/src/playground/assets/announcements/circuits.jpg +0 -0
  45. package/src/playground/assets/announcements/csvingest.jpg +0 -0
  46. package/src/playground/assets/announcements/green-wave.jpg +0 -0
  47. package/src/playground/help/workflow-nodes.js +472 -0
  48. package/src/playground/icons/V.png +0 -0
  49. package/src/playground/index.html +3634 -226
  50. package/src/workflows/consistency-check.json +4 -0
  51. package/src/workflows/cost-analysis.json +4 -0
  52. package/src/workflows/enrich-and-ingest.json +56 -0
  53. package/src/workflows/intelligent-ingest.json +66 -0
  54. package/src/workflows/kb-health-report.json +45 -0
  55. package/src/workflows/multi-collection-search.json +4 -0
  56. package/src/workflows/research-and-summarize.json +4 -0
  57. package/src/workflows/search-with-fallback.json +66 -0
  58. package/src/workflows/smart-ingest.json +4 -0
@@ -350,6 +350,79 @@ body {
350
350
  flex-direction: column;
351
351
  overflow: hidden;
352
352
  z-index: 100;
353
+ transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
354
+ }
355
+ .sidebar.collapsed {
356
+ width: 56px;
357
+ min-width: 56px;
358
+ }
359
+ .sidebar.collapsed .sidebar-title,
360
+ .sidebar.collapsed .sidebar-nav-label,
361
+ .sidebar.collapsed .sidebar-nav-divider,
362
+ .sidebar.collapsed .status-label,
363
+ .sidebar.collapsed .sidebar-bug-label,
364
+ .sidebar.collapsed .sidebar-docs-link span,
365
+ .sidebar.collapsed .sidebar-model-group,
366
+ .sidebar.collapsed .sidebar-footer > div:last-child { display: none; }
367
+ .sidebar.collapsed .sidebar-footer { overflow: hidden; padding: 8px 0; align-items: center; border-top: none; }
368
+ .sidebar.collapsed .sidebar-footer > div:first-child { justify-content: center; }
369
+ .sidebar.collapsed .sidebar-footer .theme-toggle { display: none; }
370
+ .sidebar.collapsed #appVersionLabel { display: none; }
371
+ .sidebar.collapsed .sidebar-docs-link { display: none; }
372
+ .sidebar.collapsed .tab-btn {
373
+ justify-content: center;
374
+ padding: 10px;
375
+ border-left-width: 2px;
376
+ }
377
+ .sidebar.collapsed .tab-btn span:not(.tab-btn-icon) { display: none; }
378
+ .sidebar.collapsed .sidebar-settings-btn { display: none; }
379
+ .sidebar.collapsed .sidebar-drag-region { justify-content: center; padding: 0; padding-top: 52px; }
380
+ body:not(.is-electron) .sidebar.collapsed .sidebar-drag-region { padding-top: 16px; padding-bottom: 12px; }
381
+
382
+ /* Sidebar collapse toggle */
383
+ .sidebar-collapse-btn {
384
+ -webkit-app-region: no-drag;
385
+ background: none;
386
+ border: none;
387
+ cursor: pointer;
388
+ color: var(--text-muted);
389
+ padding: 4px;
390
+ border-radius: 4px;
391
+ display: flex;
392
+ align-items: center;
393
+ justify-content: center;
394
+ transition: all 0.15s;
395
+ flex-shrink: 0;
396
+ font-size: 14px;
397
+ line-height: 1;
398
+ }
399
+ .sidebar-collapse-btn:hover { color: var(--text); background: rgba(255,255,255,0.06); }
400
+ [data-theme="light"] .sidebar-collapse-btn:hover { background: rgba(0,0,0,0.04); }
401
+ .sidebar.collapsed .sidebar-collapse-btn { transform: rotate(180deg); }
402
+
403
+ /* Sidebar tooltip for collapsed mode */
404
+ .sidebar.collapsed .tab-btn {
405
+ position: relative;
406
+ }
407
+ .sidebar.collapsed .tab-btn::after {
408
+ content: attr(data-tooltip);
409
+ position: absolute;
410
+ left: calc(100% + 8px);
411
+ top: 50%;
412
+ transform: translateY(-50%);
413
+ background: #2d333b;
414
+ color: #e6edf3;
415
+ font-size: 12px;
416
+ padding: 4px 8px;
417
+ border-radius: 4px;
418
+ white-space: nowrap;
419
+ pointer-events: none;
420
+ opacity: 0;
421
+ transition: opacity 0.15s 0.15s;
422
+ z-index: 1000;
423
+ }
424
+ .sidebar.collapsed .tab-btn:hover::after {
425
+ opacity: 1;
353
426
  }
354
427
 
355
428
  .sidebar-drag-region {
@@ -484,6 +557,7 @@ body:not(.is-electron) .sidebar-drag-region {
484
557
 
485
558
  .sidebar-footer {
486
559
  padding: 12px 16px;
560
+ padding-bottom: 56px; /* Clear the fixed cost status bar */
487
561
  border-top: 1px solid var(--border);
488
562
  display: flex;
489
563
  flex-direction: column;
@@ -1779,6 +1853,43 @@ select:focus { outline: none; border-color: var(--accent); }
1779
1853
  margin-bottom: 8px;
1780
1854
  }
1781
1855
  .about-text { font-size: 14px; line-height: 1.8; color: var(--text); }
1856
+ .about-changelog { font-size: 13px; line-height: 1.6; }
1857
+ .about-changelog details {
1858
+ border-bottom: 1px solid rgba(255,255,255,0.06);
1859
+ padding: 0;
1860
+ }
1861
+ .about-changelog details:last-child { border-bottom: none; }
1862
+ .about-changelog summary {
1863
+ padding: 10px 0;
1864
+ cursor: pointer;
1865
+ color: var(--text);
1866
+ list-style: none;
1867
+ display: flex;
1868
+ align-items: center;
1869
+ gap: 8px;
1870
+ }
1871
+ .about-changelog summary::before {
1872
+ content: '';
1873
+ display: inline-block;
1874
+ width: 5px; height: 5px;
1875
+ border-right: 1.5px solid var(--accent, #00D4AA);
1876
+ border-bottom: 1.5px solid var(--accent, #00D4AA);
1877
+ transform: rotate(-45deg);
1878
+ transition: transform 0.15s ease;
1879
+ flex-shrink: 0;
1880
+ }
1881
+ .about-changelog details[open] > summary::before {
1882
+ transform: rotate(45deg);
1883
+ }
1884
+ .about-changelog summary::-webkit-details-marker { display: none; }
1885
+ .about-changelog summary:hover { color: var(--accent-text); }
1886
+ .about-changelog p {
1887
+ margin: 0 0 10px 13px;
1888
+ color: var(--text-muted);
1889
+ font-size: 12.5px;
1890
+ line-height: 1.6;
1891
+ }
1892
+ [data-theme="light"] .about-changelog details { border-color: rgba(0,0,0,0.08); }
1782
1893
  .about-text a { color: var(--blue); text-decoration: none; }
1783
1894
  .about-text a:hover { text-decoration: underline; }
1784
1895
  .about-disclaimer {
@@ -2531,7 +2642,10 @@ select:focus { outline: none; border-color: var(--accent); }
2531
2642
  .chat-thinking .thinking-icon {
2532
2643
  font-size: 14px;
2533
2644
  line-height: 1;
2645
+ display: flex;
2646
+ align-items: center;
2534
2647
  }
2648
+ .chat-thinking .thinking-icon svg { color: var(--accent); }
2535
2649
  .chat-thinking .thinking-label {
2536
2650
  font-weight: 500;
2537
2651
  }
@@ -2589,6 +2703,7 @@ select:focus { outline: none; border-color: var(--accent); }
2589
2703
  background: var(--bg-input, #112733);
2590
2704
  border: 1px solid var(--border);
2591
2705
  }
2706
+ .thinking-step-icon svg { color: var(--text-dim); }
2592
2707
  .thinking-step.active .thinking-step-icon {
2593
2708
  border-color: var(--accent);
2594
2709
  animation: thinkingPulse 1.2s ease-in-out infinite;
@@ -2983,10 +3098,15 @@ select:focus { outline: none; border-color: var(--accent); }
2983
3098
  min-height: 0;
2984
3099
  }
2985
3100
  .wf-library {
2986
- width: 220px; min-width: 180px; flex-shrink: 0;
3101
+ width: 220px; min-width: 0; flex-shrink: 0;
2987
3102
  border-right: 1px solid var(--border);
2988
3103
  display: flex; flex-direction: column;
2989
3104
  background: var(--bg);
3105
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3106
+ overflow: hidden;
3107
+ }
3108
+ .wf-library.collapsed {
3109
+ width: 0; border-right: none;
2990
3110
  }
2991
3111
  .wf-library-header {
2992
3112
  padding: 14px 16px 10px; font-weight: 700; font-size: 13px;
@@ -3049,6 +3169,88 @@ select:focus { outline: none; border-color: var(--accent); }
3049
3169
  font-size: 10px; font-weight: 600; color: var(--text-muted);
3050
3170
  padding: 8px 12px 2px; text-transform: uppercase; letter-spacing: 0.5px;
3051
3171
  }
3172
+ /* ── Library Search ── */
3173
+ .wf-library-search-wrap {
3174
+ padding: 8px 12px 4px; position: relative;
3175
+ }
3176
+ .wf-library-search-wrap input {
3177
+ width: 100%; padding: 6px 28px 6px 10px; border-radius: 6px;
3178
+ border: 1px solid var(--border); background: var(--bg-card);
3179
+ color: var(--text); font-size: 12px; outline: none;
3180
+ box-sizing: border-box;
3181
+ transition: border-color 0.15s;
3182
+ }
3183
+ .wf-library-search-wrap input:focus {
3184
+ border-color: var(--accent);
3185
+ }
3186
+ .wf-library-search-wrap input::placeholder {
3187
+ color: var(--text-muted);
3188
+ }
3189
+ .wf-library-search-clear {
3190
+ position: absolute; right: 18px; top: 50%; transform: translateY(-50%);
3191
+ background: none; border: none; color: var(--text-muted); cursor: pointer;
3192
+ font-size: 14px; padding: 2px 4px; line-height: 1; display: none;
3193
+ }
3194
+ .wf-library-search-clear:hover { color: var(--text); }
3195
+ /* ── Collapsible Library Sections ── */
3196
+ .wf-lib-section-header {
3197
+ display: flex; align-items: center; gap: 6px;
3198
+ width: 100%; padding: 8px 12px; border: none; background: none;
3199
+ color: var(--text-muted); font-size: 10px; font-weight: 600;
3200
+ text-transform: uppercase; letter-spacing: 1.2px; cursor: pointer;
3201
+ transition: color 0.15s;
3202
+ }
3203
+ .wf-lib-section-header:hover { color: var(--text); }
3204
+ .wf-lib-section-header .wf-chevron {
3205
+ font-size: 10px; transition: transform 0.2s; display: inline-block;
3206
+ }
3207
+ .wf-lib-section-header.expanded .wf-chevron { transform: rotate(90deg); }
3208
+ .wf-lib-section-header .wf-section-count {
3209
+ color: var(--text-muted); font-size: 10px; font-weight: 400;
3210
+ }
3211
+ .wf-lib-section-body {
3212
+ overflow: hidden; transition: max-height 0.2s ease-out;
3213
+ }
3214
+ /* ── Properties Accordion ── */
3215
+ .wf-accordion-header {
3216
+ display: flex; align-items: center; justify-content: space-between;
3217
+ padding: 8px 12px; cursor: pointer; border-bottom: 1px solid var(--border);
3218
+ transition: background-color 0.15s ease; user-select: none;
3219
+ }
3220
+ .wf-accordion-header.expanded {
3221
+ background: rgba(0,212,170,0.05);
3222
+ }
3223
+ .wf-accordion-header .wf-acc-left {
3224
+ display: flex; align-items: center; gap: 6px;
3225
+ font-size: 12px; font-weight: 600; letter-spacing: 0.5px;
3226
+ color: var(--text);
3227
+ }
3228
+ .wf-accordion-header .wf-acc-left .wf-chevron {
3229
+ font-size: 10px; transition: transform 0.2s; display: inline-block;
3230
+ }
3231
+ .wf-accordion-header.expanded .wf-acc-left .wf-chevron { transform: rotate(90deg); }
3232
+ .wf-accordion-header .wf-acc-summary {
3233
+ font-size: 10px; color: var(--text-muted); font-weight: 400;
3234
+ }
3235
+ .wf-accordion-body {
3236
+ overflow: hidden; transition: max-height 0.2s ease-out;
3237
+ }
3238
+ .wf-accordion-body-inner {
3239
+ padding: 12px;
3240
+ }
3241
+ .wf-step-row {
3242
+ display: flex; align-items: center; justify-content: space-between;
3243
+ padding: 4px 0; font-size: 12px;
3244
+ }
3245
+ .wf-step-dot {
3246
+ width: 8px; height: 8px; border-radius: 50%; display: inline-block;
3247
+ margin-right: 8px; flex-shrink: 0;
3248
+ }
3249
+ .wf-step-name { flex: 1; color: var(--text); }
3250
+ .wf-step-time {
3251
+ font-family: 'SF Mono', 'Fira Code', monospace; font-size: 10px;
3252
+ color: var(--text-muted); margin-left: 8px;
3253
+ }
3052
3254
  .wf-canvas-area {
3053
3255
  flex: 1; position: relative; overflow: hidden;
3054
3256
  background: var(--bg);
@@ -3056,8 +3258,8 @@ select:focus { outline: none; border-color: var(--accent); }
3056
3258
  background-size: 20px 20px;
3057
3259
  }
3058
3260
  .wf-canvas-toolbar {
3059
- position: absolute; top: 12px; right: 12px; z-index: 10;
3060
- display: flex; gap: 4px;
3261
+ position: absolute; top: 12px; left: 12px; right: 12px; z-index: 10;
3262
+ display: flex; gap: 4px; align-items: center;
3061
3263
  }
3062
3264
  .wf-canvas-toolbar button {
3063
3265
  width: 32px; height: 32px; border-radius: 8px;
@@ -3127,6 +3329,339 @@ select:focus { outline: none; border-color: var(--accent); }
3127
3329
  .wf-palette-item:active { cursor: grabbing; }
3128
3330
  .wf-palette-icon { width: 20px; text-align: center; display: flex; align-items: center; justify-content: center; }
3129
3331
  .wf-palette-label { font-weight: 500; }
3332
+
3333
+ /* ── Workflow Node Help ── */
3334
+ .wf-palette-item { position: relative; }
3335
+ .wf-help-trigger {
3336
+ position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
3337
+ width: 20px; height: 20px; border-radius: 50%;
3338
+ border: 1px solid var(--border); background: var(--bg);
3339
+ color: var(--text-muted); cursor: pointer;
3340
+ display: flex; align-items: center; justify-content: center;
3341
+ opacity: 0; transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
3342
+ padding: 0; z-index: 2;
3343
+ }
3344
+ .wf-palette-item:hover .wf-help-trigger { opacity: 1; }
3345
+ .wf-help-trigger:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
3346
+ .wf-help-trigger svg { width: 12px; height: 12px; }
3347
+
3348
+ .wf-inspector-help-btn {
3349
+ display: inline-flex; align-items: center; gap: 4px;
3350
+ padding: 3px 8px; border-radius: 4px;
3351
+ border: 1px solid var(--border); background: var(--bg);
3352
+ color: var(--text-muted); font-size: 11px; cursor: pointer;
3353
+ transition: background 0.15s ease, color 0.15s ease;
3354
+ margin-left: 8px; vertical-align: middle;
3355
+ }
3356
+ .wf-inspector-help-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
3357
+ .wf-inspector-help-btn svg { width: 12px; height: 12px; }
3358
+
3359
+ .wf-help-modal-backdrop {
3360
+ position: fixed; inset: 0; z-index: 10000;
3361
+ background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
3362
+ display: flex; align-items: center; justify-content: center;
3363
+ animation: wfStoreDetailFadeIn 0.12s ease-out;
3364
+ }
3365
+ .wf-help-modal {
3366
+ background: var(--bg); border: 1px solid var(--border);
3367
+ border-radius: 12px; width: 540px; max-width: 92vw;
3368
+ max-height: 82vh; display: flex; flex-direction: column;
3369
+ box-shadow: 0 16px 48px rgba(0,0,0,0.3);
3370
+ animation: wfStoreDetailFadeIn 0.12s ease-out;
3371
+ }
3372
+ .wf-help-modal-header {
3373
+ display: flex; align-items: center; gap: 10px;
3374
+ padding: 16px 20px; border-bottom: 1px solid var(--border);
3375
+ flex-shrink: 0;
3376
+ }
3377
+ .wf-help-modal-header .wf-help-icon {
3378
+ width: 32px; height: 32px; border-radius: 8px;
3379
+ display: flex; align-items: center; justify-content: center;
3380
+ flex-shrink: 0;
3381
+ }
3382
+ .wf-help-modal-header .wf-help-title { font-size: 16px; font-weight: 600; flex: 1; }
3383
+ .wf-help-modal-header .wf-help-category-badge {
3384
+ font-size: 10px; font-weight: 600; text-transform: uppercase;
3385
+ letter-spacing: 0.5px; padding: 2px 8px; border-radius: 4px;
3386
+ background: var(--bg-card); color: var(--text-muted); flex-shrink: 0;
3387
+ }
3388
+ .wf-help-modal-header .wf-help-close-btn {
3389
+ width: 28px; height: 28px; border-radius: 6px;
3390
+ border: 1px solid var(--border); background: var(--bg);
3391
+ color: var(--text-muted); cursor: pointer;
3392
+ display: flex; align-items: center; justify-content: center;
3393
+ flex-shrink: 0; padding: 0; transition: background 0.15s ease, color 0.15s ease;
3394
+ }
3395
+ .wf-help-modal-header .wf-help-close-btn:hover { background: var(--bg-card); color: var(--text); }
3396
+
3397
+ .wf-help-modal-body {
3398
+ overflow-y: auto; padding: 20px; flex: 1;
3399
+ }
3400
+ .wf-help-section { margin-bottom: 20px; }
3401
+ .wf-help-section:last-child { margin-bottom: 0; }
3402
+ .wf-help-section-title {
3403
+ font-size: 11px; font-weight: 600; text-transform: uppercase;
3404
+ letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 8px;
3405
+ }
3406
+ .wf-help-section p {
3407
+ font-size: 13px; line-height: 1.6; color: var(--text); margin: 0;
3408
+ }
3409
+
3410
+ .wf-help-io-table {
3411
+ width: 100%; border-collapse: collapse; font-size: 12px;
3412
+ }
3413
+ .wf-help-io-table th {
3414
+ text-align: left; padding: 6px 8px; font-weight: 600;
3415
+ color: var(--text-muted); border-bottom: 1px solid var(--border);
3416
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.3px;
3417
+ }
3418
+ .wf-help-io-table td {
3419
+ padding: 6px 8px; border-bottom: 1px solid var(--border);
3420
+ color: var(--text); vertical-align: top;
3421
+ }
3422
+ .wf-help-io-table tr:last-child td { border-bottom: none; }
3423
+ .wf-help-io-table .wf-help-key {
3424
+ font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px;
3425
+ color: var(--accent); white-space: nowrap;
3426
+ }
3427
+ .wf-help-io-table .wf-help-type {
3428
+ font-size: 10px; color: var(--text-muted); white-space: nowrap;
3429
+ }
3430
+ .wf-help-io-table .wf-help-required {
3431
+ display: inline-block; font-size: 9px; font-weight: 600;
3432
+ color: #ff6b6b; background: rgba(255,107,107,0.1);
3433
+ padding: 1px 4px; border-radius: 3px; margin-left: 4px;
3434
+ }
3435
+
3436
+ .wf-help-tip {
3437
+ display: flex; gap: 8px; margin-bottom: 8px;
3438
+ font-size: 13px; line-height: 1.5; color: var(--text);
3439
+ }
3440
+ .wf-help-tip:last-child { margin-bottom: 0; }
3441
+ .wf-help-tip-dot {
3442
+ width: 6px; height: 6px; border-radius: 50%;
3443
+ background: var(--accent); flex-shrink: 0; margin-top: 7px;
3444
+ }
3445
+
3446
+ /* ── Workflow Store ── */
3447
+ .wf-store-overlay {
3448
+ position: absolute; inset: 0; background: var(--bg); z-index: 50;
3449
+ display: flex; flex-direction: column;
3450
+ opacity: 0; transform: scale(0.98); pointer-events: none;
3451
+ transition: opacity 0.2s ease, transform 0.2s ease;
3452
+ }
3453
+ .wf-store-overlay.open {
3454
+ opacity: 1; transform: scale(1); pointer-events: all;
3455
+ }
3456
+ .wf-store-overlay::before {
3457
+ content: ''; position: absolute; top: -150px; left: 50%; transform: translateX(-50%);
3458
+ width: 700px; height: 500px;
3459
+ background: radial-gradient(ellipse, rgba(0,212,170,.05) 0%, rgba(64,224,255,.02) 40%, transparent 70%);
3460
+ pointer-events: none; z-index: 0;
3461
+ }
3462
+ .wf-store-header {
3463
+ padding: 14px 20px; border-bottom: 1px solid var(--border);
3464
+ display: flex; align-items: center; gap: 14px; position: relative; z-index: 1; flex-shrink: 0;
3465
+ }
3466
+ .wf-store-back {
3467
+ display: flex; align-items: center; gap: 6px; background: none; border: none;
3468
+ color: var(--text-muted); font-family: var(--font); font-size: 13px; cursor: pointer;
3469
+ padding: 6px 10px; border-radius: 6px; transition: all 0.15s;
3470
+ }
3471
+ .wf-store-back:hover { background: var(--bg-surface); color: var(--accent); }
3472
+ .wf-store-title-area { display: flex; align-items: center; gap: 8px; flex: 1; }
3473
+ .wf-store-title { font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--text); letter-spacing: 0.02em; }
3474
+ .wf-store-badge { font-family: var(--mono); font-size: 9px; padding: 3px 8px; background: rgba(0,212,170,.15); border: 1px solid rgba(0,212,170,.2); border-radius: 100px; color: var(--accent); letter-spacing: 0.04em; text-transform: uppercase; }
3475
+ .wf-store-search-wrap { position: relative; width: 220px; }
3476
+ .wf-store-search {
3477
+ width: 100%; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px;
3478
+ padding: 8px 12px 8px 30px; font-family: var(--font); font-size: 13px; color: var(--text); outline: none;
3479
+ transition: border-color 0.15s;
3480
+ }
3481
+ .wf-store-search:focus { border-color: var(--accent); }
3482
+ .wf-store-search::placeholder { color: var(--text-muted); }
3483
+ .wf-store-search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 13px; pointer-events: none; }
3484
+ .wf-store-sort {
3485
+ background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px;
3486
+ padding: 8px 10px; font-family: var(--font); font-size: 12px; color: var(--text-muted); outline: none; cursor: pointer;
3487
+ }
3488
+ .wf-store-body { flex: 1; overflow-y: auto; position: relative; z-index: 1; }
3489
+ .wf-store-body::-webkit-scrollbar { width: 6px; }
3490
+ .wf-store-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
3491
+ .wf-store-inner { max-width: 1100px; margin: 0 auto; padding: 20px 24px 40px; }
3492
+ .wf-store-chips { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 20px; }
3493
+ .wf-store-chip {
3494
+ padding: 6px 14px; background: none; border: 1px solid var(--border); border-radius: 100px;
3495
+ font-family: var(--font); font-size: 12px; color: var(--text-muted); cursor: pointer;
3496
+ transition: all 0.15s; white-space: nowrap;
3497
+ }
3498
+ .wf-store-chip:hover { border-color: var(--text-dim); color: var(--text-dim); }
3499
+ .wf-store-chip.active { background: rgba(0,212,170,.15); border-color: rgba(0,212,170,.3); color: var(--accent); }
3500
+ .wf-store-section-label {
3501
+ font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
3502
+ text-transform: uppercase; color: var(--text-muted); margin-bottom: 12px;
3503
+ display: flex; align-items: center; gap: 8px;
3504
+ }
3505
+ .wf-store-count { background: var(--bg-surface); padding: 2px 7px; border-radius: 100px; font-size: 10px; }
3506
+ /* Featured cards */
3507
+ .wf-store-featured-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 24px; }
3508
+ .wf-store-featured-card {
3509
+ border-radius: 14px; padding: 22px 20px 18px; position: relative; overflow: hidden;
3510
+ cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; min-height: 150px;
3511
+ display: flex; flex-direction: column; justify-content: flex-end;
3512
+ }
3513
+ .wf-store-featured-card:hover { transform: translateY(-3px); box-shadow: 0 16px 48px -8px rgba(0,0,0,.5); }
3514
+ .wf-store-featured-card::before {
3515
+ content: ''; position: absolute; inset: 0;
3516
+ background: linear-gradient(180deg, transparent 15%, rgba(0,0,0,.65) 100%); z-index: 1;
3517
+ }
3518
+ .wf-store-featured-content { position: relative; z-index: 2; }
3519
+ .wf-store-featured-content h3 { font-family: var(--mono); font-size: 14px; font-weight: 700; margin-bottom: 4px; color: #fff; }
3520
+ .wf-store-featured-content p { font-size: 12px; color: rgba(255,255,255,.65); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
3521
+ .wf-store-featured-icon { position: absolute; top: 14px; left: 16px; z-index: 2; width: 36px; height: 36px; border-radius: 10px; background: rgba(255,255,255,.18); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; }
3522
+ .wf-store-featured-icon svg { width: 20px; height: 20px; color: #fff; }
3523
+ .wf-store-featured-dl { position: absolute; top: 12px; right: 14px; z-index: 2; font-family: var(--mono); font-size: 10px; color: rgba(255,255,255,.5); }
3524
+ .wf-store-featured-author { position: relative; z-index: 2; display: flex; align-items: center; gap: 6px; margin-top: 6px; font-size: 11px; color: rgba(255,255,255,.55); }
3525
+ .wf-store-featured-avatar { width: 18px; height: 18px; border-radius: 50%; background: rgba(255,255,255,.2); display: flex; align-items: center; justify-content: center; font-size: 9px; color: #fff; font-weight: 700; overflow: hidden; }
3526
+ .wf-store-featured-avatar img { width: 100%; height: 100%; object-fit: cover; }
3527
+ /* Workflow cards grid */
3528
+ .wf-store-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; }
3529
+ .wf-store-card {
3530
+ background: var(--bg-surface); border: 1px solid var(--border); border-radius: 14px;
3531
+ padding: 16px; cursor: pointer; transition: all 0.15s; position: relative; overflow: hidden;
3532
+ }
3533
+ .wf-store-card:hover { border-color: var(--text-dim); background: var(--bg-card); transform: translateY(-1px); }
3534
+ .wf-store-card-bar { position: absolute; top: 0; left: 0; right: 0; height: 2px; opacity: 0; transition: opacity 0.15s; }
3535
+ .wf-store-card:hover .wf-store-card-bar { opacity: 1; }
3536
+ .wf-store-card-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 6px; gap: 8px; }
3537
+ .wf-store-card-icon { width: 28px; height: 28px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
3538
+ .wf-store-card-icon svg { width: 16px; height: 16px; }
3539
+ .wf-store-card-name-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
3540
+ .wf-store-card-name { font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--text); }
3541
+ .wf-store-card-badges { display: flex; gap: 4px; }
3542
+ .wf-store-badge-official { font-family: var(--mono); font-size: 9px; padding: 2px 7px; border-radius: 100px; background: rgba(0,212,170,.1); color: var(--accent); border: 1px solid rgba(0,212,170,.15); white-space: nowrap; }
3543
+ .wf-store-badge-installed { font-family: var(--mono); font-size: 9px; padding: 2px 7px; border-radius: 100px; background: rgba(16,185,129,.1); color: #10B981; border: 1px solid rgba(16,185,129,.15); white-space: nowrap; }
3544
+ .wf-store-badge-verified { font-family: var(--mono); font-size: 9px; padding: 2px 7px; border-radius: 100px; background: rgba(16,185,129,.1); color: #10B981; border: 1px solid rgba(16,185,129,.15); white-space: nowrap; }
3545
+ .wf-store-badge-capability { font-family: var(--mono); font-size: 9px; padding: 2px 6px; border-radius: 100px; white-space: nowrap; display: inline-flex; align-items: center; gap: 3px; }
3546
+ .wf-store-badge-cap-network { background: rgba(59,130,246,.1); color: #3B82F6; border: 1px solid rgba(59,130,246,.15); }
3547
+ .wf-store-badge-cap-writedb { background: rgba(245,158,11,.1); color: #F59E0B; border: 1px solid rgba(245,158,11,.15); }
3548
+ .wf-store-badge-cap-llm { background: rgba(139,92,246,.1); color: #8B5CF6; border: 1px solid rgba(139,92,246,.15); }
3549
+ .wf-store-badge-cap-loop { background: rgba(6,182,212,.1); color: #06B6D4; border: 1px solid rgba(6,182,212,.15); }
3550
+ .wf-store-badge-cap-readdb { background: rgba(34,197,94,.1); color: #22C55E; border: 1px solid rgba(34,197,94,.15); }
3551
+ .wf-store-detail-security-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--bg-card); border-radius: 6px; margin-bottom: 4px; font-size: 12px; color: var(--text-dim); }
3552
+ .wf-store-detail-security-sev { font-family: var(--mono); font-size: 9px; padding: 2px 7px; border-radius: 100px; font-weight: 700; text-transform: uppercase; }
3553
+ .wf-store-detail-security-sev.critical { background: rgba(239,68,68,.15); color: #EF4444; }
3554
+ .wf-store-detail-security-sev.high { background: rgba(249,115,22,.15); color: #F97316; }
3555
+ .wf-store-detail-security-sev.medium { background: rgba(234,179,8,.15); color: #EAB308; }
3556
+ .wf-store-detail-security-sev.low { background: rgba(148,163,184,.15); color: #94A3B8; }
3557
+ .wf-store-detail-security-ok { color: #10B981; font-size: 12px; padding: 8px 10px; background: rgba(16,185,129,.06); border-radius: 6px; }
3558
+ .wf-store-detail-stars { display: inline-flex; gap: 2px; cursor: pointer; }
3559
+ .wf-store-detail-stars span { font-size: 18px; color: var(--text-muted); transition: color 0.1s; }
3560
+ .wf-store-detail-stars span.filled { color: #F59E0B; }
3561
+ .wf-store-detail-stars span:hover, .wf-store-detail-stars span:hover ~ span { color: var(--text-muted); }
3562
+ .wf-store-report-form { margin-top: 8px; padding: 12px; background: var(--bg-card); border-radius: 8px; border: 1px solid var(--border); }
3563
+ .wf-store-report-form select, .wf-store-report-form textarea { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px; color: var(--text); font-family: var(--font); font-size: 12px; margin-bottom: 8px; }
3564
+ .wf-store-report-form textarea { resize: vertical; min-height: 60px; }
3565
+ .wf-store-report-submit { padding: 6px 14px; background: var(--error); color: #fff; border: none; border-radius: 6px; font-size: 11px; font-weight: 600; cursor: pointer; }
3566
+ .wf-store-report-submit:hover { filter: brightness(1.1); }
3567
+ .wf-store-cap-badges { display: flex; gap: 3px; flex-wrap: wrap; margin-top: 4px; }
3568
+ .wf-store-card-desc { font-size: 12px; color: var(--text-dim); line-height: 1.5; margin-bottom: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
3569
+ .wf-store-card-author { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; }
3570
+ .wf-store-card-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
3571
+ .wf-store-card-cat { font-family: var(--mono); font-size: 10px; padding: 2px 8px; background: var(--bg-card); border-radius: 100px; color: var(--text-muted); }
3572
+ .wf-store-card-dots { display: flex; gap: 2px; }
3573
+ .wf-store-card-dot { width: 7px; height: 7px; border-radius: 50%; opacity: 0.65; }
3574
+ .wf-store-card-complexity { font-family: var(--mono); font-size: 10px; color: var(--text-muted); }
3575
+ .wf-store-card-downloads { margin-left: auto; font-family: var(--mono); font-size: 10px; color: var(--text-muted); }
3576
+ .wf-store-empty { text-align: center; padding: 40px 0; color: var(--text-muted); }
3577
+ .wf-store-empty p { font-family: var(--mono); font-size: 13px; }
3578
+ /* Detail modal */
3579
+ .wf-store-detail-bg {
3580
+ position: fixed; inset: 0; background: rgba(0,0,0,.55); backdrop-filter: blur(6px);
3581
+ z-index: 200; display: flex; align-items: center; justify-content: center;
3582
+ animation: wfStoreDetailFadeIn 0.12s ease-out; padding: 20px;
3583
+ }
3584
+ @keyframes wfStoreDetailFadeIn { from { opacity: 0; } }
3585
+ @keyframes wfStoreDetailSlideIn { from { transform: translateY(20px); opacity: 0; } }
3586
+ .wf-store-detail-panel {
3587
+ background: var(--bg-surface); border: 1px solid var(--border); border-radius: 18px;
3588
+ width: 100%; max-width: 580px; max-height: 82vh; overflow: hidden;
3589
+ animation: wfStoreDetailSlideIn 0.18s ease-out; display: flex; flex-direction: column;
3590
+ }
3591
+ .wf-store-detail-scroll {
3592
+ overflow-y: auto; flex: 1; min-height: 0;
3593
+ }
3594
+ .wf-store-detail-scroll::-webkit-scrollbar { width: 4px; }
3595
+ .wf-store-detail-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
3596
+ .wf-store-detail-hero { padding: 28px 28px 20px; border-radius: 18px 18px 0 0; position: relative; }
3597
+ .wf-store-detail-hero::before { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 25%, var(--bg-surface) 100%); border-radius: 18px 18px 0 0; }
3598
+ .wf-store-detail-hero-inner { position: relative; z-index: 1; display: flex; align-items: center; gap: 14px; }
3599
+ .wf-store-detail-hero-icon { width: 48px; height: 48px; border-radius: 12px; background: rgba(255,255,255,.18); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
3600
+ .wf-store-detail-hero-icon svg { width: 26px; height: 26px; color: #fff; }
3601
+ .wf-store-detail-close {
3602
+ position: absolute; top: 14px; right: 14px; z-index: 2; width: 28px; height: 28px;
3603
+ border-radius: 50%; background: rgba(0,0,0,.35); border: none; color: #fff; font-size: 16px;
3604
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
3605
+ }
3606
+ .wf-store-detail-close:hover { background: rgba(0,0,0,.55); }
3607
+ .wf-store-detail-name { font-family: var(--mono); font-size: 20px; font-weight: 700; color: #fff; margin-bottom: 4px; }
3608
+ .wf-store-detail-pkg { font-family: var(--mono); font-size: 11px; color: rgba(255,255,255,.45); }
3609
+ .wf-store-detail-body { padding: 0 28px 28px; }
3610
+ .wf-store-detail-desc { font-size: 14px; color: var(--text-dim); line-height: 1.65; margin-bottom: 20px; }
3611
+ .wf-store-detail-section { margin-bottom: 16px; }
3612
+ .wf-store-detail-label { font-family: var(--mono); font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 8px; }
3613
+ .wf-store-detail-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
3614
+ .wf-store-detail-stat { background: var(--bg-card); border-radius: 8px; padding: 12px; text-align: center; }
3615
+ .wf-store-detail-stat-val { font-family: var(--mono); font-size: 20px; font-weight: 700; color: var(--text); }
3616
+ .wf-store-detail-stat-lbl { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
3617
+ .wf-store-detail-tools { display: flex; gap: 5px; flex-wrap: wrap; }
3618
+ .wf-store-detail-tool { padding: 4px 10px; border-radius: 100px; font-size: 11px; font-family: var(--mono); border: 1px solid; }
3619
+ .wf-store-detail-tags { display: flex; gap: 5px; flex-wrap: wrap; }
3620
+ .wf-store-detail-tag { padding: 4px 10px; background: var(--bg-card); border-radius: 100px; font-size: 11px; color: var(--text-dim); font-family: var(--mono); }
3621
+ .wf-store-detail-install { margin-top: 20px; padding: 16px; background: var(--bg-card); border-radius: 8px; border: 1px solid var(--border); }
3622
+ .wf-store-detail-cmd {
3623
+ font-family: var(--mono); font-size: 12px; color: var(--accent); background: var(--bg);
3624
+ padding: 10px 14px; border-radius: 6px; margin-top: 6px; display: flex; align-items: center;
3625
+ justify-content: space-between; cursor: pointer; border: 1px solid var(--border);
3626
+ transition: border-color 0.15s; overflow-x: auto; white-space: nowrap; gap: 10px;
3627
+ }
3628
+ .wf-store-detail-cmd:hover { border-color: var(--accent); }
3629
+ .wf-store-detail-cmd-hint { font-size: 10px; color: var(--text-muted); flex-shrink: 0; }
3630
+ .wf-store-detail-author { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
3631
+ .wf-store-detail-avatar { width: 24px; height: 24px; border-radius: 50%; background: var(--bg-card); display: flex; align-items: center; justify-content: center; font-size: 11px; color: var(--text-muted); font-weight: 700; overflow: hidden; flex-shrink: 0; }
3632
+ .wf-store-detail-avatar img { width: 100%; height: 100%; object-fit: cover; }
3633
+ .wf-store-detail-author-name { font-size: 13px; color: var(--text-dim); }
3634
+ .wf-store-detail-author-name a { color: var(--accent); text-decoration: none; }
3635
+ .wf-store-detail-author-name a:hover { text-decoration: underline; }
3636
+ .wf-store-detail-inputs-table { width: 100%; border-collapse: collapse; font-size: 12px; font-family: var(--mono); }
3637
+ .wf-store-detail-inputs-table th { text-align: left; padding: 6px 10px; color: var(--text-muted); font-weight: 600; font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); }
3638
+ .wf-store-detail-inputs-table td { padding: 6px 10px; color: var(--text-dim); border-bottom: 1px solid var(--border); }
3639
+ .wf-store-detail-screenshots { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 6px; }
3640
+ .wf-store-detail-screenshots img { height: 180px; border-radius: 8px; border: 1px solid var(--border); flex-shrink: 0; }
3641
+ .wf-store-detail-actions { display: flex; gap: 8px; margin-top: 16px; }
3642
+ .wf-store-detail-btn {
3643
+ flex: 1; padding: 10px; border-radius: 8px; font-family: var(--mono); font-size: 12px;
3644
+ font-weight: 700; cursor: pointer; border: none; transition: all 0.15s; text-align: center;
3645
+ }
3646
+ .wf-store-detail-btn-primary { background: var(--accent); color: var(--bg); }
3647
+ .wf-store-detail-btn-primary:hover { filter: brightness(1.1); }
3648
+ .wf-store-detail-btn-secondary { background: var(--bg-card); color: var(--text-dim); border: 1px solid var(--border); }
3649
+ .wf-store-detail-btn-secondary:hover { background: var(--border); color: var(--text); }
3650
+ .wf-store-detail-btn-installed { background: rgba(16,185,129,.1); color: #10B981; border: 1px solid rgba(16,185,129,.2); cursor: default; }
3651
+ /* Store button in library header */
3652
+ .wf-store-btn {
3653
+ width: 28px; height: 28px; border-radius: 6px; border: 1px solid transparent;
3654
+ background: none; color: var(--text-muted); font-size: 14px; cursor: pointer;
3655
+ display: flex; align-items: center; justify-content: center; transition: all 0.15s;
3656
+ flex-shrink: 0;
3657
+ }
3658
+ .wf-store-btn:hover { background: var(--bg-card); color: var(--accent); border-color: rgba(0,212,170,.15); }
3659
+ .wf-store-btn.active { color: var(--accent); }
3660
+ @media (max-width: 700px) {
3661
+ .wf-store-featured-grid { grid-template-columns: 1fr; }
3662
+ .wf-store-grid { grid-template-columns: 1fr; }
3663
+ }
3664
+
3130
3665
  /* Builder port styles */
3131
3666
  .wf-port-builder { cursor: crosshair; transition: r 0.15s; pointer-events: all !important; }
3132
3667
  .wf-port-builder:hover { r: 9; filter: brightness(1.4); }
@@ -3400,39 +3935,79 @@ select:focus { outline: none; border-color: var(--accent); }
3400
3935
  }
3401
3936
  @keyframes wf-flow { to { stroke-dashoffset: -12; } }
3402
3937
  .wf-edge--complete { stroke: var(--accent, #6c63ff); opacity: 0.6; stroke-dasharray: none; }
3938
+ .wf-edge--else { stroke-dasharray: 6 4; opacity: 0.4; }
3939
+ .wf-edge--skipped { opacity: 0.12; stroke-dasharray: 4 4; }
3403
3940
  /* Inspector */
3941
+ /* ── Library collapse chevron ── */
3942
+ .wf-library-collapse-btn {
3943
+ width: 28px; height: 28px; border: none; background: none;
3944
+ color: var(--text-muted); cursor: pointer; font-size: 14px;
3945
+ display: flex; align-items: center; justify-content: center;
3946
+ border-radius: 6px; transition: color 0.15s, background 0.15s;
3947
+ flex-shrink: 0; padding: 0;
3948
+ }
3949
+ .wf-library-collapse-btn:hover { color: var(--text); background: var(--bg-card); }
3950
+
3951
+ /* ── Canvas edge handles ── */
3952
+ .wf-edge-handle {
3953
+ position: absolute; top: 50%; transform: translateY(-50%);
3954
+ width: 28px; height: 56px; z-index: 20;
3955
+ background: var(--bg-card); border: 1px solid var(--border);
3956
+ color: var(--text-muted); cursor: pointer;
3957
+ display: none; align-items: center; justify-content: center;
3958
+ font-size: 14px; transition: background 0.15s, color 0.15s;
3959
+ }
3960
+ .wf-edge-handle:hover { background: var(--bg); color: var(--text); }
3961
+ .wf-edge-handle--left {
3962
+ left: 0; border-radius: 0 8px 8px 0; border-left: none;
3963
+ }
3964
+ .wf-edge-handle--right {
3965
+ right: 0; border-radius: 8px 0 0 8px; border-right: none;
3966
+ }
3967
+ .wf-edge-handle.visible { display: flex; }
3968
+
3969
+ /* ── Toolbar panel toggle group ── */
3970
+ .wf-toolbar-spacer { flex: 1; }
3971
+ .wf-toolbar-toggle-group {
3972
+ display: flex; background: var(--bg); border-radius: 6px; padding: 2px;
3973
+ border: 1px solid var(--border);
3974
+ }
3975
+ .wf-toolbar-toggle-group button {
3976
+ width: auto !important; height: 28px !important; padding: 0 10px !important;
3977
+ border: none !important; border-radius: 4px !important;
3978
+ background: transparent !important; color: var(--text-muted) !important;
3979
+ font-size: 11px !important; font-weight: 500 !important; cursor: pointer;
3980
+ transition: background 0.15s, color 0.15s;
3981
+ }
3982
+ .wf-toolbar-toggle-group button.active {
3983
+ background: rgba(0,212,170,0.15) !important; color: var(--accent) !important;
3984
+ }
3985
+ .wf-toolbar-toggle-group button:hover:not(.active) {
3986
+ color: var(--text) !important;
3987
+ }
3988
+
3989
+ /* ── Inspector (upgraded) ── */
3404
3990
  .wf-inspector {
3405
3991
  flex-shrink: 0; position: relative;
3406
3992
  display: flex; flex-direction: row;
3407
3993
  background: var(--bg);
3408
- transition: width 0.25s ease;
3994
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3409
3995
  width: 300px;
3410
3996
  overflow: hidden;
3411
3997
  align-self: stretch;
3998
+ border-left: 1px solid var(--border);
3412
3999
  }
3413
4000
  .wf-inspector.collapsed {
3414
- width: 28px;
4001
+ width: 0; border-left: none;
3415
4002
  }
3416
4003
  .wf-inspector-toggle {
3417
- width: 28px; flex-shrink: 0; align-self: stretch;
3418
- border: none; border-left: 1px solid var(--border);
3419
- background: var(--bg); color: var(--text-muted);
3420
- cursor: pointer; font-size: 14px; padding: 0;
3421
- display: flex; align-items: center; justify-content: center;
3422
- transition: color 0.15s, background 0.15s;
3423
- }
3424
- .wf-inspector-toggle:hover {
3425
- color: var(--text); background: var(--bg-card);
3426
- }
3427
- .wf-inspector.collapsed .wf-inspector-toggle {
3428
- border-left: 1px solid var(--border);
4004
+ display: none; /* replaced by edge handle */
3429
4005
  }
3430
4006
  .wf-inspector.collapsed .wf-inspector-content {
3431
4007
  display: none;
3432
4008
  }
3433
4009
  .wf-inspector-content {
3434
4010
  flex: 1; display: flex; flex-direction: column;
3435
- border-left: 1px solid var(--border);
3436
4011
  overflow-y: auto; min-width: 0; height: 100%;
3437
4012
  }
3438
4013
  .wf-inspector-header {
@@ -3499,11 +4074,11 @@ select:focus { outline: none; border-color: var(--accent); }
3499
4074
  .wf-inspector-result.error { border-color: #e74c3c; }
3500
4075
  /* Responsive */
3501
4076
  @media (max-width: 900px) {
3502
- .wf-library { display: none; }
4077
+ .wf-library { width: 0; border-right: none; }
3503
4078
  .wf-inspector:not(.collapsed) { width: 240px; }
3504
4079
  }
3505
4080
  @media (max-width: 600px) {
3506
- .wf-inspector { display: none; }
4081
+ .wf-inspector { width: 0; border-left: none; }
3507
4082
  }
3508
4083
  /* Workflow visualizer light mode */
3509
4084
  [data-theme="light"] .wf-node-label { fill: #001E2B; }
@@ -3552,105 +4127,890 @@ select:focus { outline: none; border-color: var(--accent); }
3552
4127
  }
3553
4128
  .wf-canvas-watermark img { width: 300px; height: 300px; filter: invert(1); }
3554
4129
  [data-theme="light"] .wf-canvas-watermark img { filter: none; }
3555
- </style>
3556
- </head>
3557
- <body>
3558
4130
 
3559
- <!-- Lucide Icons (lucide.dev) stroke-based, 16×16 -->
3560
- <svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
3561
- <!-- Zap (Embed) -->
3562
- <symbol id="lg-lightning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3563
- <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>
3564
- </symbol>
3565
- <!-- Arrow Left Right (Compare) -->
3566
- <symbol id="lg-arrows" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3567
- <path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>
3568
- </symbol>
3569
- <!-- Search -->
3570
- <symbol id="lg-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3571
- <circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
3572
- </symbol>
3573
- <!-- Gauge (Benchmark) -->
3574
- <symbol id="lg-gauge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3575
- <path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>
3576
- </symbol>
3577
- <!-- Lightbulb (Explore) -->
3578
- <symbol id="lg-bulb" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3579
- <path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>
3580
- </symbol>
3581
- <!-- Info (About) -->
3582
- <symbol id="lg-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3583
- <circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>
3584
- </symbol>
3585
- <!-- Image (Multimodal) -->
3586
- <symbol id="lg-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3587
- <rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>
3588
- </symbol>
3589
- <!-- Settings (Config) -->
3590
- <symbol id="lg-config" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3591
- <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>
3592
- </symbol>
3593
- <!-- Code (Generate) -->
3594
- <symbol id="lg-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3595
- <polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
3596
- </symbol>
3597
- <!-- Palette (Theme) -->
3598
- <symbol id="lg-palette" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3599
- <circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/>
3600
- </symbol>
3601
- <!-- Box (Cube) -->
3602
- <symbol id="lg-cube" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3603
- <path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>
3604
- </symbol>
3605
- <!-- Message Square (Chat) -->
3606
- <symbol id="lg-chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3607
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
3608
- </symbol>
3609
- <!-- Shield -->
3610
- <symbol id="lg-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3611
- <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 .5-.87l7-4a1 1 0 0 1 1 0l7 4A1 1 0 0 1 20 6z"/>
3612
- </symbol>
3613
- <!-- Activity (Pulse) -->
3614
- <symbol id="lg-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3615
- <path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36-3.18-19.64A2 2 0 0 0 10.12 1h-.24a2 2 0 0 0-1.94 1.55L5.18 12H2"/>
3616
- </symbol>
3617
- </svg>
4131
+ /* ── Home Page Styles ── */
3618
4132
 
3619
- <div class="app-shell">
4133
+ /* Home Header */
4134
+ .home-header {
4135
+ display: flex;
4136
+ align-items: center;
4137
+ justify-content: space-between;
4138
+ padding: 24px 32px 16px;
4139
+ border-bottom: 1px solid var(--border);
4140
+ background: var(--bg);
4141
+ }
3620
4142
 
3621
- <!-- Sidebar -->
3622
- <aside class="sidebar">
3623
- <div class="sidebar-drag-region">
3624
- <img class="sidebar-logo" id="sidebarLogo" src="/icons/dark/64.png" alt="Vai">
3625
- <span class="sidebar-title">Vai</span>
3626
- <button class="sidebar-settings-btn" data-tab="settings" title="Settings"><svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-config"/></svg></button>
3627
- </div>
3628
- <nav class="sidebar-nav">
3629
- <div class="sidebar-nav-group" role="tablist" aria-label="Tools">
3630
- <div class="sidebar-nav-label" id="nav-tools-label">Tools</div>
3631
- <button class="tab-btn active" data-tab="embed" role="tab" aria-selected="true" aria-controls="tab-embed" id="tab-btn-embed"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-lightning"/></svg></span><span>Embed</span></button>
3632
- <button class="tab-btn" data-tab="compare" role="tab" aria-selected="false" aria-controls="tab-compare" id="tab-btn-compare"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-arrows"/></svg></span><span>Compare</span></button>
3633
- <button class="tab-btn" data-tab="search" role="tab" aria-selected="false" aria-controls="tab-search" id="tab-btn-search"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-search"/></svg></span><span>Search</span></button>
3634
- <button class="tab-btn" data-tab="multimodal" role="tab" aria-selected="false" aria-controls="tab-multimodal" id="tab-btn-multimodal"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-image"/></svg></span><span>Multimodal</span></button>
3635
- <button class="tab-btn" data-tab="generate" role="tab" aria-selected="false" aria-controls="tab-generate" id="tab-btn-generate"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-code"/></svg></span><span>Generate</span></button>
3636
- <button class="tab-btn" data-tab="chat" role="tab" aria-selected="false" aria-controls="tab-chat" id="tab-btn-chat"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-chat"/></svg></span><span>Chat</span></button>
3637
- <button class="tab-btn" data-tab="workflows" role="tab" aria-selected="false" aria-controls="tab-workflows" id="tab-btn-workflows"><span class="tab-btn-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="12" r="3"/><circle cx="19" cy="6" r="3"/><circle cx="19" cy="18" r="3"/><line x1="7.7" y1="10.7" x2="16.3" y2="7.3"/><line x1="7.7" y1="13.3" x2="16.3" y2="16.7"/></svg></span><span>Workflows</span></button>
3638
- </div>
3639
- <div class="sidebar-nav-divider"></div>
3640
- <div class="sidebar-nav-group" role="tablist" aria-label="Learn">
3641
- <div class="sidebar-nav-label" id="nav-learn-label">Learn</div>
3642
- <button class="tab-btn" data-tab="benchmark" role="tab" aria-selected="false" aria-controls="tab-benchmark" id="tab-btn-benchmark"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-gauge"/></svg></span><span>Benchmark</span></button>
3643
- <button class="tab-btn" data-tab="explore" role="tab" aria-selected="false" aria-controls="tab-explore" id="tab-btn-explore"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-bulb"/></svg></span><span>Explore</span></button>
3644
- <button class="tab-btn" data-tab="about" role="tab" aria-selected="false" aria-controls="tab-about" id="tab-btn-about"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-info"/></svg></span><span>About</span></button>
3645
- </div>
3646
- </nav>
3647
- <div class="sidebar-footer">
3648
- <div style="display:flex;align-items:center;gap:8px;justify-content:space-between;">
3649
- <div style="display:flex;align-items:center;gap:5px;">
3650
- <div class="status-dot" id="statusDot"></div>
3651
- <span class="status-label" id="statusLabel">Checking...</span>
3652
- </div>
3653
- <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">🌙</button>
4143
+ .home-header-left {
4144
+ display: flex;
4145
+ align-items: center;
4146
+ gap: 12px;
4147
+ }
4148
+
4149
+ .home-logo {
4150
+ width: 32px;
4151
+ height: 32px;
4152
+ }
4153
+
4154
+ .home-header-title {
4155
+ font-size: 16px;
4156
+ font-weight: 600;
4157
+ color: var(--text);
4158
+ letter-spacing: -0.01em;
4159
+ }
4160
+
4161
+ .home-version-badge {
4162
+ background: var(--bg-card);
4163
+ border: 1px solid var(--border);
4164
+ color: var(--text-dim);
4165
+ padding: 4px 8px;
4166
+ border-radius: 12px;
4167
+ font-size: 11px;
4168
+ font-weight: 500;
4169
+ }
4170
+
4171
+ .home-header-right {
4172
+ display: flex;
4173
+ align-items: center;
4174
+ gap: 12px;
4175
+ }
4176
+
4177
+ .home-settings-btn {
4178
+ background: none;
4179
+ border: 1px solid var(--border);
4180
+ color: var(--text-dim);
4181
+ padding: 8px;
4182
+ border-radius: 8px;
4183
+ cursor: pointer;
4184
+ transition: all 0.2s;
4185
+ }
4186
+
4187
+ .home-settings-btn:hover {
4188
+ background: var(--bg-card);
4189
+ color: var(--text);
4190
+ border-color: var(--accent);
4191
+ }
4192
+
4193
+ .home-settings-btn svg {
4194
+ width: 16px;
4195
+ height: 16px;
4196
+ }
4197
+
4198
+ /* API Key Warning Banner */
4199
+ .home-api-warning {
4200
+ margin: 16px 32px;
4201
+ background: linear-gradient(135deg, rgba(255, 201, 16, 0.1), rgba(255, 105, 96, 0.1));
4202
+ border: 1px solid rgba(255, 201, 16, 0.3);
4203
+ border-radius: 12px;
4204
+ padding: 16px;
4205
+ animation: slideDown 0.3s ease-out;
4206
+ }
4207
+
4208
+ .home-api-warning-content {
4209
+ display: flex;
4210
+ align-items: center;
4211
+ gap: 12px;
4212
+ color: var(--warning);
4213
+ }
4214
+
4215
+ .home-api-warning svg {
4216
+ width: 20px;
4217
+ height: 20px;
4218
+ flex-shrink: 0;
4219
+ }
4220
+
4221
+ .home-api-warning button {
4222
+ margin-left: auto;
4223
+ background: none;
4224
+ border: 1px solid var(--warning);
4225
+ color: var(--warning);
4226
+ padding: 6px 12px;
4227
+ border-radius: 6px;
4228
+ cursor: pointer;
4229
+ font-size: 12px;
4230
+ transition: all 0.2s;
4231
+ }
4232
+
4233
+ .home-api-warning button:hover {
4234
+ background: var(--warning);
4235
+ color: var(--bg);
4236
+ }
4237
+
4238
+ /* Home Content */
4239
+ .home-content {
4240
+ padding: 32px;
4241
+ max-width: 1200px;
4242
+ margin: 0 auto;
4243
+ overflow-y: auto;
4244
+ height: calc(100vh - 120px);
4245
+ }
4246
+
4247
+ /* Announcements Banner */
4248
+ .home-announcements {
4249
+ margin-bottom: 48px;
4250
+ animation: fadeInUp 0.6s ease-out;
4251
+ }
4252
+
4253
+ .home-announcements-carousel {
4254
+ position: relative;
4255
+ overflow: hidden;
4256
+ border-radius: 16px;
4257
+ background: linear-gradient(135deg, var(--bg-card), var(--bg-surface));
4258
+ border: 1px solid var(--border);
4259
+ min-height: 180px;
4260
+ }
4261
+
4262
+ .home-announcement-card {
4263
+ position: absolute;
4264
+ top: 0;
4265
+ left: 0;
4266
+ right: 0;
4267
+ padding: 32px;
4268
+ text-align: center;
4269
+ opacity: 0;
4270
+ pointer-events: none;
4271
+ transition: opacity 0.5s ease-in-out;
4272
+ }
4273
+
4274
+ .home-announcement-card.active {
4275
+ position: relative;
4276
+ opacity: 1;
4277
+ pointer-events: auto;
4278
+ }
4279
+
4280
+ .home-announcement-card .badge {
4281
+ display: inline-block;
4282
+ background: linear-gradient(135deg, #00ED64, #00C2FF);
4283
+ color: white;
4284
+ padding: 4px 12px;
4285
+ border-radius: 12px;
4286
+ font-size: 11px;
4287
+ font-weight: 600;
4288
+ margin-bottom: 16px;
4289
+ text-transform: uppercase;
4290
+ letter-spacing: 0.5px;
4291
+ }
4292
+
4293
+ .home-announcement-card h3 {
4294
+ font-size: 24px;
4295
+ font-weight: 700;
4296
+ color: var(--accent-text);
4297
+ margin-bottom: 12px;
4298
+ }
4299
+
4300
+ .home-announcement-card p {
4301
+ color: var(--text-dim);
4302
+ line-height: 1.6;
4303
+ margin-bottom: 24px;
4304
+ max-width: 600px;
4305
+ margin-left: auto;
4306
+ margin-right: auto;
4307
+ }
4308
+
4309
+ .home-announcement-card .cta {
4310
+ background: linear-gradient(135deg, #00ED64, #00C2FF);
4311
+ color: white;
4312
+ border: none;
4313
+ padding: 12px 24px;
4314
+ border-radius: 8px;
4315
+ font-weight: 600;
4316
+ cursor: pointer;
4317
+ transition: all 0.2s;
4318
+ }
4319
+
4320
+ .home-announcement-card .cta:hover {
4321
+ transform: translateY(-2px);
4322
+ box-shadow: 0 8px 32px rgba(0, 237, 100, 0.3);
4323
+ }
4324
+
4325
+ .home-announcement-dismiss {
4326
+ position: absolute;
4327
+ top: 16px;
4328
+ right: 16px;
4329
+ background: none;
4330
+ border: none;
4331
+ color: var(--text-muted);
4332
+ cursor: pointer;
4333
+ padding: 4px;
4334
+ border-radius: 4px;
4335
+ transition: all 0.2s;
4336
+ }
4337
+
4338
+ .home-announcement-dismiss:hover {
4339
+ background: var(--bg-card);
4340
+ color: var(--text);
4341
+ }
4342
+
4343
+ .home-announcements-dots {
4344
+ display: flex;
4345
+ justify-content: center;
4346
+ gap: 8px;
4347
+ margin-top: 16px;
4348
+ }
4349
+
4350
+ .home-announcement-dot {
4351
+ width: 8px;
4352
+ height: 8px;
4353
+ border-radius: 50%;
4354
+ background: var(--border);
4355
+ cursor: pointer;
4356
+ transition: all 0.2s;
4357
+ }
4358
+
4359
+ .home-announcement-dot.active {
4360
+ background: linear-gradient(135deg, #00ED64, #00C2FF);
4361
+ }
4362
+
4363
+ .home-announcements-restore {
4364
+ text-align: center;
4365
+ margin-top: 12px;
4366
+ }
4367
+
4368
+ .home-announcements-restore button {
4369
+ background: var(--bg-card);
4370
+ border: 1px dashed var(--border);
4371
+ color: var(--text-dim);
4372
+ font-size: 13px;
4373
+ cursor: pointer;
4374
+ padding: 8px 16px;
4375
+ border-radius: 8px;
4376
+ transition: all 0.2s;
4377
+ }
4378
+
4379
+ .home-announcements-restore button:hover {
4380
+ color: var(--accent-text);
4381
+ border-color: var(--accent-text);
4382
+ background: var(--bg-surface);
4383
+ }
4384
+
4385
+ .home-announcement-card.has-bg-image {
4386
+ background-size: cover;
4387
+ background-position: center;
4388
+ background-repeat: no-repeat;
4389
+ min-height: 220px;
4390
+ display: flex;
4391
+ flex-direction: column;
4392
+ justify-content: center;
4393
+ align-items: center;
4394
+ }
4395
+
4396
+ .home-announcement-card.has-bg-image::before {
4397
+ content: '';
4398
+ position: absolute;
4399
+ inset: 0;
4400
+ background: rgba(0, 0, 0, 0.5);
4401
+ border-radius: inherit;
4402
+ z-index: 0;
4403
+ }
4404
+
4405
+ .home-announcement-card.has-bg-image > * {
4406
+ position: relative;
4407
+ z-index: 1;
4408
+ }
4409
+
4410
+ .home-announcement-card.has-bg-image h3,
4411
+ .home-announcement-card.has-bg-image p {
4412
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
4413
+ }
4414
+
4415
+ .home-announcement-card.has-bg-image h3 {
4416
+ color: #fff;
4417
+ }
4418
+
4419
+ .home-announcement-card.has-bg-image p {
4420
+ color: rgba(255, 255, 255, 0.9);
4421
+ }
4422
+
4423
+ .home-announcement-icon {
4424
+ font-size: 48px;
4425
+ margin-bottom: 12px;
4426
+ line-height: 1;
4427
+ }
4428
+
4429
+ /* Section Headers */
4430
+ .home-section {
4431
+ margin-bottom: 48px;
4432
+ }
4433
+
4434
+ .home-section-header {
4435
+ display: flex;
4436
+ align-items: center;
4437
+ justify-content: space-between;
4438
+ margin-bottom: 24px;
4439
+ }
4440
+
4441
+ .home-section-header h3 {
4442
+ font-size: 20px;
4443
+ font-weight: 700;
4444
+ color: var(--accent-text);
4445
+ }
4446
+
4447
+ .home-section-header a,
4448
+ .home-marketplace-cta {
4449
+ color: var(--blue);
4450
+ text-decoration: none;
4451
+ font-weight: 500;
4452
+ transition: all 0.2s;
4453
+ }
4454
+
4455
+ .home-marketplace-cta {
4456
+ background: linear-gradient(135deg, #00ED64, #00C2FF);
4457
+ color: white;
4458
+ border: none;
4459
+ padding: 10px 20px;
4460
+ border-radius: 8px;
4461
+ cursor: pointer;
4462
+ font-weight: 600;
4463
+ transition: all 0.2s;
4464
+ }
4465
+
4466
+ .home-marketplace-cta:hover {
4467
+ transform: translateY(-1px);
4468
+ box-shadow: 0 4px 16px rgba(0, 237, 100, 0.3);
4469
+ }
4470
+
4471
+ .home-section-header a:hover {
4472
+ color: var(--accent);
4473
+ }
4474
+
4475
+ /* Release Notes Timeline */
4476
+ .home-releases-timeline {
4477
+ display: flex;
4478
+ flex-direction: column;
4479
+ gap: 24px;
4480
+ }
4481
+
4482
+ .home-release-item {
4483
+ display: flex;
4484
+ gap: 16px;
4485
+ padding: 24px;
4486
+ background: var(--bg-card);
4487
+ border: 1px solid var(--border);
4488
+ border-radius: 12px;
4489
+ border-left: 4px solid transparent;
4490
+ border-left-color: #00ED64;
4491
+ background: linear-gradient(90deg, transparent, var(--bg-card) 4px);
4492
+ transition: all 0.2s;
4493
+ animation: fadeInUp 0.6s ease-out;
4494
+ }
4495
+
4496
+ .home-release-item:hover {
4497
+ background: var(--bg-surface);
4498
+ border-color: var(--accent);
4499
+ }
4500
+
4501
+ .home-release-version {
4502
+ font-size: 16px;
4503
+ font-weight: 700;
4504
+ color: var(--accent-text);
4505
+ margin-bottom: 4px;
4506
+ }
4507
+
4508
+ .home-release-date {
4509
+ font-size: 12px;
4510
+ color: var(--text-muted);
4511
+ margin-bottom: 12px;
4512
+ }
4513
+
4514
+ .home-release-highlights {
4515
+ list-style: none;
4516
+ display: flex;
4517
+ flex-direction: column;
4518
+ gap: 8px;
4519
+ }
4520
+
4521
+ .home-release-highlights li {
4522
+ display: flex;
4523
+ align-items: flex-start;
4524
+ gap: 8px;
4525
+ color: var(--text-dim);
4526
+ line-height: 1.5;
4527
+ }
4528
+
4529
+ .home-release-highlights li:before {
4530
+ content: "•";
4531
+ color: var(--accent);
4532
+ font-weight: bold;
4533
+ flex-shrink: 0;
4534
+ margin-top: 2px;
4535
+ }
4536
+
4537
+ /* Subsections */
4538
+ .home-subsection {
4539
+ margin-bottom: 32px;
4540
+ }
4541
+
4542
+ .home-subsection h4 {
4543
+ font-size: 16px;
4544
+ font-weight: 600;
4545
+ color: var(--text);
4546
+ margin-bottom: 16px;
4547
+ }
4548
+
4549
+ .home-subsection-header {
4550
+ display: flex;
4551
+ align-items: center;
4552
+ justify-content: space-between;
4553
+ margin-bottom: 16px;
4554
+ }
4555
+
4556
+ /* Workflow Carousels */
4557
+ .home-workflows-carousel {
4558
+ display: flex;
4559
+ gap: 16px;
4560
+ overflow-x: auto;
4561
+ padding-bottom: 8px;
4562
+ scroll-snap-type: x mandatory;
4563
+ }
4564
+
4565
+ .home-workflows-carousel::-webkit-scrollbar {
4566
+ height: 6px;
4567
+ }
4568
+
4569
+ .home-workflows-carousel::-webkit-scrollbar-track {
4570
+ background: var(--bg-surface);
4571
+ border-radius: 3px;
4572
+ }
4573
+
4574
+ .home-workflows-carousel::-webkit-scrollbar-thumb {
4575
+ background: var(--border);
4576
+ border-radius: 3px;
4577
+ }
4578
+
4579
+ .home-workflow-card {
4580
+ flex: 0 0 280px;
4581
+ background: var(--bg-card);
4582
+ border: 1px solid var(--border);
4583
+ border-radius: 12px;
4584
+ padding: 20px;
4585
+ scroll-snap-align: start;
4586
+ transition: all 0.2s;
4587
+ animation: fadeInUp 0.6s ease-out;
4588
+ }
4589
+
4590
+ .home-workflow-card:hover {
4591
+ background: var(--bg-surface);
4592
+ border-color: var(--accent);
4593
+ transform: translateY(-2px);
4594
+ box-shadow: 0 8px 32px rgba(0, 212, 170, 0.1);
4595
+ }
4596
+
4597
+ .home-workflow-card h5 {
4598
+ font-size: 14px;
4599
+ font-weight: 600;
4600
+ color: var(--accent-text);
4601
+ margin-bottom: 8px;
4602
+ }
4603
+
4604
+ .home-workflow-card p {
4605
+ font-size: 12px;
4606
+ color: var(--text-dim);
4607
+ line-height: 1.5;
4608
+ margin-bottom: 12px;
4609
+ display: -webkit-box;
4610
+ -webkit-line-clamp: 2;
4611
+ -webkit-box-orient: vertical;
4612
+ overflow: hidden;
4613
+ }
4614
+
4615
+ .home-workflow-meta {
4616
+ display: flex;
4617
+ align-items: center;
4618
+ gap: 8px;
4619
+ margin-bottom: 16px;
4620
+ }
4621
+
4622
+ .home-workflow-domain {
4623
+ background: rgba(0, 237, 100, 0.1);
4624
+ color: var(--accent);
4625
+ padding: 4px 8px;
4626
+ border-radius: 6px;
4627
+ font-size: 10px;
4628
+ font-weight: 500;
4629
+ }
4630
+
4631
+ .home-workflow-author {
4632
+ font-size: 11px;
4633
+ color: var(--text-muted);
4634
+ }
4635
+
4636
+ .home-workflow-actions {
4637
+ display: flex;
4638
+ gap: 8px;
4639
+ }
4640
+
4641
+ .home-workflow-btn {
4642
+ flex: 1;
4643
+ background: none;
4644
+ border: 1px solid var(--border);
4645
+ color: var(--text-dim);
4646
+ padding: 8px 12px;
4647
+ border-radius: 6px;
4648
+ font-size: 11px;
4649
+ cursor: pointer;
4650
+ transition: all 0.2s;
4651
+ }
4652
+
4653
+ .home-workflow-btn:hover {
4654
+ background: var(--accent);
4655
+ color: white;
4656
+ border-color: var(--accent);
4657
+ }
4658
+
4659
+ /* Domain Pills */
4660
+ .home-domain-pills {
4661
+ display: flex;
4662
+ flex-wrap: wrap;
4663
+ gap: 8px;
4664
+ }
4665
+
4666
+ .home-domain-pill {
4667
+ background: var(--bg-card);
4668
+ border: 1px solid var(--border);
4669
+ color: var(--text-dim);
4670
+ padding: 8px 16px;
4671
+ border-radius: 20px;
4672
+ font-size: 12px;
4673
+ font-weight: 500;
4674
+ cursor: pointer;
4675
+ transition: all 0.2s;
4676
+ }
4677
+
4678
+ .home-domain-pill:hover {
4679
+ background: linear-gradient(135deg, #00ED64, #00C2FF);
4680
+ color: white;
4681
+ border-color: transparent;
4682
+ transform: translateY(-1px);
4683
+ }
4684
+
4685
+ /* Sort Tabs */
4686
+ .home-sort-tabs {
4687
+ display: flex;
4688
+ gap: 2px;
4689
+ background: var(--bg-surface);
4690
+ border: 1px solid var(--border);
4691
+ border-radius: 8px;
4692
+ padding: 4px;
4693
+ }
4694
+
4695
+ .home-sort-tab {
4696
+ background: none;
4697
+ border: none;
4698
+ color: var(--text-muted);
4699
+ padding: 6px 12px;
4700
+ border-radius: 6px;
4701
+ font-size: 12px;
4702
+ cursor: pointer;
4703
+ transition: all 0.2s;
4704
+ }
4705
+
4706
+ .home-sort-tab.active {
4707
+ background: var(--accent);
4708
+ color: white;
4709
+ }
4710
+
4711
+ .home-sort-tab:hover:not(.active) {
4712
+ background: var(--bg-card);
4713
+ color: var(--text);
4714
+ }
4715
+
4716
+ /* Community Grid */
4717
+ .home-community-grid {
4718
+ display: grid;
4719
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
4720
+ gap: 16px;
4721
+ }
4722
+
4723
+ .home-community-empty {
4724
+ grid-column: 1 / -1;
4725
+ text-align: center;
4726
+ padding: 48px;
4727
+ color: var(--text-muted);
4728
+ }
4729
+
4730
+ .home-community-empty h4 {
4731
+ font-size: 16px;
4732
+ margin-bottom: 8px;
4733
+ color: var(--text);
4734
+ }
4735
+
4736
+ /* Quick Actions */
4737
+ .home-quick-actions {
4738
+ display: grid;
4739
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
4740
+ gap: 16px;
4741
+ }
4742
+
4743
+ .home-quick-action {
4744
+ display: flex;
4745
+ flex-direction: column;
4746
+ align-items: center;
4747
+ gap: 12px;
4748
+ background: var(--bg-card);
4749
+ border: 1px solid var(--border);
4750
+ border-radius: 12px;
4751
+ padding: 24px;
4752
+ cursor: pointer;
4753
+ transition: all 0.2s;
4754
+ text-decoration: none;
4755
+ color: var(--text);
4756
+ animation: fadeInUp 0.6s ease-out;
4757
+ }
4758
+
4759
+ .home-quick-action:hover {
4760
+ background: var(--bg-surface);
4761
+ border-color: var(--accent);
4762
+ transform: translateY(-4px);
4763
+ box-shadow: 0 12px 40px rgba(0, 212, 170, 0.1);
4764
+ color: var(--text);
4765
+ }
4766
+
4767
+ .home-quick-action svg {
4768
+ width: 24px;
4769
+ height: 24px;
4770
+ color: var(--accent);
4771
+ }
4772
+
4773
+ .home-quick-action span {
4774
+ font-size: 14px;
4775
+ font-weight: 500;
4776
+ text-align: center;
4777
+ }
4778
+
4779
+ /* Footer */
4780
+ .home-footer {
4781
+ margin-top: 64px;
4782
+ padding-top: 32px;
4783
+ border-top: 1px solid var(--border);
4784
+ text-align: center;
4785
+ }
4786
+
4787
+ .home-footer-links {
4788
+ display: flex;
4789
+ justify-content: center;
4790
+ gap: 24px;
4791
+ margin-bottom: 16px;
4792
+ flex-wrap: wrap;
4793
+ }
4794
+
4795
+ .home-footer-links a {
4796
+ color: var(--text-dim);
4797
+ text-decoration: none;
4798
+ font-size: 12px;
4799
+ transition: color 0.2s;
4800
+ }
4801
+
4802
+ .home-footer-links a:hover {
4803
+ color: var(--accent);
4804
+ }
4805
+
4806
+ .home-footer-branding {
4807
+ display: flex;
4808
+ flex-direction: column;
4809
+ gap: 4px;
4810
+ color: var(--text-muted);
4811
+ font-size: 11px;
4812
+ }
4813
+
4814
+ /* Animations */
4815
+ @keyframes fadeIn {
4816
+ from { opacity: 0; }
4817
+ to { opacity: 1; }
4818
+ }
4819
+
4820
+ @keyframes fadeInUp {
4821
+ from {
4822
+ opacity: 0;
4823
+ transform: translateY(20px);
4824
+ }
4825
+ to {
4826
+ opacity: 1;
4827
+ transform: translateY(0);
4828
+ }
4829
+ }
4830
+
4831
+ @keyframes slideDown {
4832
+ from {
4833
+ opacity: 0;
4834
+ transform: translateY(-10px);
4835
+ }
4836
+ to {
4837
+ opacity: 1;
4838
+ transform: translateY(0);
4839
+ }
4840
+ }
4841
+
4842
+ /* Responsive */
4843
+ @media (max-width: 1024px) {
4844
+ .home-content {
4845
+ padding: 24px;
4846
+ }
4847
+
4848
+ .home-quick-actions {
4849
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
4850
+ }
4851
+
4852
+ .home-community-grid {
4853
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
4854
+ }
4855
+ }
4856
+
4857
+ @media (max-width: 768px) {
4858
+ .home-header {
4859
+ padding: 16px 24px 12px;
4860
+ }
4861
+
4862
+ .home-content {
4863
+ padding: 16px;
4864
+ }
4865
+
4866
+ .home-section-header {
4867
+ flex-direction: column;
4868
+ align-items: flex-start;
4869
+ gap: 12px;
4870
+ }
4871
+
4872
+ .home-footer-links {
4873
+ flex-direction: column;
4874
+ gap: 12px;
4875
+ }
4876
+ }
4877
+ </style>
4878
+ </head>
4879
+ <body>
4880
+
4881
+ <!-- Lucide Icons (lucide.dev) — stroke-based, 16×16 -->
4882
+ <svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
4883
+ <!-- Zap (Embed) -->
4884
+ <symbol id="lg-lightning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4885
+ <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>
4886
+ </symbol>
4887
+ <!-- Arrow Left Right (Compare) -->
4888
+ <symbol id="lg-arrows" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4889
+ <path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>
4890
+ </symbol>
4891
+ <!-- Search -->
4892
+ <symbol id="lg-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4893
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
4894
+ </symbol>
4895
+ <!-- Gauge (Benchmark) -->
4896
+ <symbol id="lg-gauge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4897
+ <path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>
4898
+ </symbol>
4899
+ <!-- Lightbulb (Explore) -->
4900
+ <symbol id="lg-bulb" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4901
+ <path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>
4902
+ </symbol>
4903
+ <!-- Info (About) -->
4904
+ <symbol id="lg-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4905
+ <circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>
4906
+ </symbol>
4907
+ <!-- Image (Multimodal) -->
4908
+ <symbol id="lg-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4909
+ <rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>
4910
+ </symbol>
4911
+ <!-- Settings (Config) -->
4912
+ <symbol id="lg-config" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4913
+ <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>
4914
+ </symbol>
4915
+ <!-- Code (Generate) -->
4916
+ <symbol id="lg-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4917
+ <polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
4918
+ </symbol>
4919
+ <!-- Palette (Theme) -->
4920
+ <symbol id="lg-palette" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4921
+ <circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/>
4922
+ </symbol>
4923
+ <!-- Box (Cube) -->
4924
+ <symbol id="lg-cube" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4925
+ <path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>
4926
+ </symbol>
4927
+ <!-- Message Square (Chat) -->
4928
+ <symbol id="lg-chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4929
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
4930
+ </symbol>
4931
+ <!-- Shield -->
4932
+ <symbol id="lg-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4933
+ <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 .5-.87l7-4a1 1 0 0 1 1 0l7 4A1 1 0 0 1 20 6z"/>
4934
+ </symbol>
4935
+ <!-- Activity (Pulse) -->
4936
+ <symbol id="lg-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4937
+ <path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36-3.18-19.64A2 2 0 0 0 10.12 1h-.24a2 2 0 0 0-1.94 1.55L5.18 12H2"/>
4938
+ </symbol>
4939
+ <!-- Brain (Thinking) -->
4940
+ <symbol id="lg-brain" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4941
+ <path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/>
4942
+ </symbol>
4943
+ <!-- Sparkles -->
4944
+ <symbol id="lg-sparkles" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4945
+ <path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>
4946
+ </symbol>
4947
+ <!-- Target (Similarity) -->
4948
+ <symbol id="lg-target" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4949
+ <circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>
4950
+ </symbol>
4951
+ <!-- Database (Collections) -->
4952
+ <symbol id="lg-database" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4953
+ <ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 21 19V5"/><path d="M3 12A9 3 0 0 0 21 12"/>
4954
+ </symbol>
4955
+ <!-- Bot (Models) -->
4956
+ <symbol id="lg-bot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4957
+ <path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>
4958
+ </symbol>
4959
+ <!-- ArrowUpDown (Rerank) -->
4960
+ <symbol id="lg-arrow-up-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4961
+ <path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/>
4962
+ </symbol>
4963
+ <!-- Coins (Cost) -->
4964
+ <symbol id="lg-coins" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4965
+ <circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/>
4966
+ </symbol>
4967
+ <!-- Inbox (Ingest) -->
4968
+ <symbol id="lg-inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4969
+ <polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
4970
+ </symbol>
4971
+ <!-- SearchCode (Vector Search) -->
4972
+ <symbol id="lg-search-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4973
+ <path d="m13 13.5 2-2.5-2-2.5"/><path d="m9 8.5-2 2.5 2 2.5"/><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
4974
+ </symbol>
4975
+ </svg>
4976
+
4977
+ <div class="app-shell">
4978
+
4979
+ <!-- Sidebar -->
4980
+ <aside class="sidebar">
4981
+ <div class="sidebar-drag-region">
4982
+ <img class="sidebar-logo" id="sidebarLogo" src="/icons/dark/64.png" alt="Vai">
4983
+ <span class="sidebar-title">Vai</span>
4984
+ <button class="sidebar-collapse-btn" id="sidebarCollapseBtn" onclick="toggleSidebarCollapse()" title="Toggle sidebar"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>
4985
+ <button class="sidebar-settings-btn" data-tab="settings" title="Settings"><svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-config"/></svg></button>
4986
+ </div>
4987
+ <nav class="sidebar-nav">
4988
+ <div class="sidebar-nav-group" role="tablist" aria-label="Tools">
4989
+ <div class="sidebar-nav-label" id="nav-tools-label">Tools</div>
4990
+ <button class="tab-btn active" data-tab="home" data-tooltip="Home" role="tab" aria-selected="true" aria-controls="tab-home" id="tab-btn-home"><span class="tab-btn-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></span><span>Home</span></button>
4991
+ <button class="tab-btn" data-tab="embed" data-tooltip="Embed" role="tab" aria-selected="false" aria-controls="tab-embed" id="tab-btn-embed"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-lightning"/></svg></span><span>Embed</span></button>
4992
+ <button class="tab-btn" data-tab="compare" data-tooltip="Compare" role="tab" aria-selected="false" aria-controls="tab-compare" id="tab-btn-compare"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-arrows"/></svg></span><span>Compare</span></button>
4993
+ <button class="tab-btn" data-tab="search" data-tooltip="Search" role="tab" aria-selected="false" aria-controls="tab-search" id="tab-btn-search"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-search"/></svg></span><span>Search</span></button>
4994
+ <button class="tab-btn" data-tab="multimodal" data-tooltip="Multimodal" role="tab" aria-selected="false" aria-controls="tab-multimodal" id="tab-btn-multimodal"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-image"/></svg></span><span>Multimodal</span></button>
4995
+ <button class="tab-btn" data-tab="generate" data-tooltip="Generate" role="tab" aria-selected="false" aria-controls="tab-generate" id="tab-btn-generate"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-code"/></svg></span><span>Generate</span></button>
4996
+ <button class="tab-btn" data-tab="chat" data-tooltip="Chat" role="tab" aria-selected="false" aria-controls="tab-chat" id="tab-btn-chat"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-chat"/></svg></span><span>Chat</span></button>
4997
+ <button class="tab-btn" data-tab="workflows" data-tooltip="Workflows" role="tab" aria-selected="false" aria-controls="tab-workflows" id="tab-btn-workflows"><span class="tab-btn-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="12" r="3"/><circle cx="19" cy="6" r="3"/><circle cx="19" cy="18" r="3"/><line x1="7.7" y1="10.7" x2="16.3" y2="7.3"/><line x1="7.7" y1="13.3" x2="16.3" y2="16.7"/></svg></span><span>Workflows</span></button>
4998
+ </div>
4999
+ <div class="sidebar-nav-divider"></div>
5000
+ <div class="sidebar-nav-group" role="tablist" aria-label="Learn">
5001
+ <div class="sidebar-nav-label" id="nav-learn-label">Learn</div>
5002
+ <button class="tab-btn" data-tab="benchmark" data-tooltip="Benchmark" role="tab" aria-selected="false" aria-controls="tab-benchmark" id="tab-btn-benchmark"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-gauge"/></svg></span><span>Benchmark</span></button>
5003
+ <button class="tab-btn" data-tab="explore" data-tooltip="Explore" role="tab" aria-selected="false" aria-controls="tab-explore" id="tab-btn-explore"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-bulb"/></svg></span><span>Explore</span></button>
5004
+ <button class="tab-btn" data-tab="about" data-tooltip="About" role="tab" aria-selected="false" aria-controls="tab-about" id="tab-btn-about"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-info"/></svg></span><span>About</span></button>
5005
+ </div>
5006
+ </nav>
5007
+ <div class="sidebar-footer">
5008
+ <div style="display:flex;align-items:center;gap:8px;justify-content:space-between;">
5009
+ <div style="display:flex;align-items:center;gap:5px;">
5010
+ <div class="status-dot" id="statusDot"></div>
5011
+ <span class="status-label" id="statusLabel">Checking...</span>
5012
+ </div>
5013
+ <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">🌙</button>
3654
5014
  </div>
3655
5015
  <div style="display:flex;align-items:center;justify-content:space-between;">
3656
5016
  <div id="appVersionLabel" style="font-size:10px;color:var(--text-muted);"></div>
@@ -3687,8 +5047,172 @@ select:focus { outline: none; border-color: var(--accent); }
3687
5047
  </div>
3688
5048
  <div class="main">
3689
5049
 
5050
+ <!-- ========== HOME TAB ========== -->
5051
+ <div class="tab-panel active" id="tab-home" role="tabpanel" aria-labelledby="tab-btn-home" tabindex="0">
5052
+ <!-- Header Bar -->
5053
+ <div class="home-header">
5054
+ <div class="home-header-left">
5055
+ <img class="home-logo" id="homeLogo" src="/icons/V.png" alt="Vai" onerror="this.src='/icons/dark/64.png'">
5056
+ <span class="home-header-title">Voyage AI Playground</span>
5057
+ <span class="home-version-badge" id="homeVersionBadge">v1.0.0</span>
5058
+ </div>
5059
+ <div class="home-header-right">
5060
+ <button class="home-settings-btn" onclick="switchTab('settings')" title="Settings">
5061
+ <svg width="20" height="20" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
5062
+ </button>
5063
+ </div>
5064
+ </div>
5065
+
5066
+ <!-- API Key Warning Banner (hidden by default) -->
5067
+ <div class="home-api-warning" id="homeApiWarning" style="display: none;">
5068
+ <div class="home-api-warning-content">
5069
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
5070
+ <triangle cx="12" cy="12" r="10"/>
5071
+ <line x1="12" y1="8" x2="12" y2="12"/>
5072
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
5073
+ </svg>
5074
+ <span>Set up your Voyage AI API key to get started</span>
5075
+ <button onclick="switchTab('settings')">Settings →</button>
5076
+ </div>
5077
+ </div>
5078
+
5079
+ <div class="home-content">
5080
+ <!-- Announcements Banner -->
5081
+ <div class="home-announcements" id="homeAnnouncements" style="display: none;">
5082
+ <div class="home-announcements-carousel" id="announcementsCarousel">
5083
+ <!-- Cards will be inserted here -->
5084
+ </div>
5085
+ <div class="home-announcements-dots" id="announcementsDots">
5086
+ <!-- Dots will be inserted here -->
5087
+ </div>
5088
+ <div class="home-announcements-restore" id="announcementsRestore" style="display: none;">
5089
+ <button onclick="restoreAnnouncements()">Show dismissed announcements</button>
5090
+ </div>
5091
+ </div>
5092
+
5093
+ <!-- What's New (Release Notes) -->
5094
+ <div class="home-section" id="homeReleases">
5095
+ <div class="home-section-header">
5096
+ <h3>What's New</h3>
5097
+ <a href="https://github.com/mrlynn/voyageai-cli/releases" target="_blank" rel="noopener">View All →</a>
5098
+ </div>
5099
+ <div class="home-releases-timeline" id="releasesTimeline">
5100
+ <!-- Release items will be inserted here -->
5101
+ </div>
5102
+ </div>
5103
+
5104
+ <!-- Marketplace Spotlight -->
5105
+ <div class="home-section">
5106
+ <div class="home-section-header">
5107
+ <h3>Marketplace Spotlight</h3>
5108
+ <button class="home-marketplace-cta" onclick="switchTab('workflows')">
5109
+ Explore the Marketplace →
5110
+ </button>
5111
+ </div>
5112
+
5113
+ <!-- Featured Workflows -->
5114
+ <div class="home-subsection">
5115
+ <h4>Featured Workflows</h4>
5116
+ <div class="home-workflows-carousel" id="featuredWorkflows">
5117
+ <!-- Workflow cards will be inserted here -->
5118
+ </div>
5119
+ </div>
5120
+
5121
+ <!-- Browse by Domain -->
5122
+ <div class="home-subsection">
5123
+ <h4>Browse by Domain</h4>
5124
+ <div class="home-domain-pills">
5125
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('Healthcare')">Healthcare</button>
5126
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('Finance')">Finance</button>
5127
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('Legal')">Legal</button>
5128
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('DevOps')">DevOps</button>
5129
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('Education')">Education</button>
5130
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('E-Commerce')">E-Commerce</button>
5131
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('Marketing')">Marketing</button>
5132
+ </div>
5133
+ </div>
5134
+
5135
+ <!-- Community Workflows -->
5136
+ <div class="home-subsection">
5137
+ <div class="home-subsection-header">
5138
+ <h4>Community Workflows</h4>
5139
+ <div class="home-sort-tabs">
5140
+ <button class="home-sort-tab active" data-sort="trending" onclick="sortCommunityWorkflows('trending')">Trending</button>
5141
+ <button class="home-sort-tab" data-sort="newest" onclick="sortCommunityWorkflows('newest')">Newest</button>
5142
+ <button class="home-sort-tab" data-sort="installs" onclick="sortCommunityWorkflows('installs')">Most Installed</button>
5143
+ </div>
5144
+ </div>
5145
+ <div class="home-community-grid" id="communityWorkflows">
5146
+ <!-- Community workflow cards will be inserted here -->
5147
+ </div>
5148
+ </div>
5149
+ </div>
5150
+
5151
+ <!-- Quick Actions -->
5152
+ <div class="home-section">
5153
+ <div class="home-section-header">
5154
+ <h3>Quick Actions</h3>
5155
+ </div>
5156
+ <div class="home-quick-actions">
5157
+ <button class="home-quick-action" onclick="switchTab('embed')" title="Generate embeddings for text">
5158
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
5159
+ <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>
5160
+ </svg>
5161
+ <span>New Embedding</span>
5162
+ </button>
5163
+ <button class="home-quick-action" onclick="switchTab('compare')" title="Compare model similarities">
5164
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
5165
+ <path d="M21 12c0 1.2-4.03 6-9 6s-9-4.8-9-6c0-1.2 4.03-6 9-6s9 4.8 9 6"/>
5166
+ <circle cx="12" cy="12" r="3"/>
5167
+ </svg>
5168
+ <span>Compare Models</span>
5169
+ </button>
5170
+ <button class="home-quick-action" onclick="switchTab('search')" title="Search through vectors">
5171
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
5172
+ <circle cx="11" cy="11" r="8"/>
5173
+ <path d="m21 21-4.35-4.35"/>
5174
+ </svg>
5175
+ <span>Search Vectors</span>
5176
+ </button>
5177
+ <button class="home-quick-action" onclick="switchTab('workflows')" title="View installed workflows">
5178
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
5179
+ <circle cx="5" cy="12" r="3"/>
5180
+ <circle cx="19" cy="6" r="3"/>
5181
+ <circle cx="19" cy="18" r="3"/>
5182
+ <line x1="7.7" y1="10.7" x2="16.3" y2="7.3"/>
5183
+ <line x1="7.7" y1="13.3" x2="16.3" y2="16.7"/>
5184
+ </svg>
5185
+ <span>My Workflows</span>
5186
+ </button>
5187
+ <button class="home-quick-action" onclick="publishWorkflow()" title="Publish a new workflow">
5188
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
5189
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
5190
+ <polyline points="17,8 12,3 7,8"/>
5191
+ <line x1="12" y1="3" x2="12" y2="15"/>
5192
+ </svg>
5193
+ <span>Publish Workflow</span>
5194
+ </button>
5195
+ </div>
5196
+ </div>
5197
+
5198
+ <!-- Footer -->
5199
+ <div class="home-footer">
5200
+ <div class="home-footer-links">
5201
+ <a href="https://docs.vaicli.com" target="_blank" rel="noopener">Documentation</a>
5202
+ <a href="https://github.com/mrlynn/voyageai-cli" target="_blank" rel="noopener">GitHub</a>
5203
+ <a href="#" onclick="reportBug()">Report Bug</a>
5204
+ <!-- <a href="#" target="_blank" rel="noopener">Community / Discord</a> -->
5205
+ </div>
5206
+ <div class="home-footer-branding">
5207
+ <span>Built with Voyage AI + MongoDB Atlas</span>
5208
+ <span id="homeFooterVersion">Version 1.0.0 • © 2026 VAI</span>
5209
+ </div>
5210
+ </div>
5211
+ </div>
5212
+ </div>
5213
+
3690
5214
  <!-- ========== EMBED TAB ========== -->
3691
- <div class="tab-panel active" id="tab-embed" role="tabpanel" aria-labelledby="tab-btn-embed" tabindex="0">
5215
+ <div class="tab-panel" id="tab-embed" role="tabpanel" aria-labelledby="tab-btn-embed" tabindex="0">
3692
5216
  <div class="page-header">
3693
5217
  <h2 class="page-header-title">Embed</h2>
3694
5218
  <p class="page-header-subtitle">Generate vector embeddings for text</p>
@@ -4492,12 +6016,20 @@ Reranking models rescore initial search results to improve relevance ordering.</
4492
6016
  <!-- ========== WORKFLOWS TAB ========== -->
4493
6017
  <div class="tab-panel" id="tab-workflows" role="tabpanel" aria-labelledby="tab-btn-workflows" tabindex="0">
4494
6018
  <div class="wf-container">
4495
- <div class="wf-library">
6019
+ <div class="wf-library" id="wfLibrary">
4496
6020
  <div class="wf-library-header">
4497
- <div class="wf-library-tabs">
6021
+ <div class="wf-library-tabs" style="flex:1;">
4498
6022
  <button class="wf-lib-tab active" data-lib-tab="library" onclick="wfSwitchLibTab('library')">Library</button>
4499
6023
  <button class="wf-lib-tab" data-lib-tab="palette" onclick="wfSwitchLibTab('palette')">Palette</button>
4500
6024
  </div>
6025
+ <button class="wf-store-btn" id="wfStoreBtn" onclick="wfStoreOpen()" title="Browse Workflow Store">
6026
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
6027
+ </button>
6028
+ <button class="wf-library-collapse-btn" onclick="wfToggleLibrary()" title="Collapse library">&#x2039;</button>
6029
+ </div>
6030
+ <div class="wf-library-search-wrap">
6031
+ <input type="text" id="wfLibrarySearch" placeholder="Search workflows..." oninput="wfOnLibrarySearch()">
6032
+ <button class="wf-library-search-clear" id="wfLibrarySearchClear" onclick="wfClearLibrarySearch()">✕</button>
4501
6033
  </div>
4502
6034
  <div class="wf-library-list" id="wfLibraryList">
4503
6035
  <div style="padding: 16px; color: var(--text-muted); font-size: 12px;">Loading...</div>
@@ -4528,7 +6060,15 @@ Reranking models rescore initial search results to improve relevance ordering.</
4528
6060
  <button onclick="wfExportJson()" id="wfExportBtn" disabled title="Export workflow JSON">
4529
6061
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h10M8 14V5M5 8l3-3 3 3"/></svg>
4530
6062
  </button>
6063
+ <span class="wf-toolbar-spacer"></span>
6064
+ <div class="wf-toolbar-toggle-group">
6065
+ <button id="wfToggleLibBtn" class="active" onclick="wfToggleLibrary()">Library</button>
6066
+ <button id="wfTogglePropsBtn" class="active" onclick="wfToggleInspector()">Props</button>
6067
+ </div>
4531
6068
  </div>
6069
+ <!-- Edge handles for collapsed panels -->
6070
+ <div class="wf-edge-handle wf-edge-handle--left" id="wfEdgeHandleLeft" onclick="wfToggleLibrary()" title="Expand library">&#x203A;</div>
6071
+ <div class="wf-edge-handle wf-edge-handle--right" id="wfEdgeHandleRight" onclick="wfToggleInspector()" title="Expand properties">&#x2039;</div>
4532
6072
  <!-- Execution status bar -->
4533
6073
  <div class="wf-exec-status" id="wfExecStatus" style="display:none;">
4534
6074
  <span class="wf-exec-status-dot"></span>
@@ -4541,9 +6081,32 @@ Reranking models rescore initial search results to improve relevance ordering.</
4541
6081
  </div>
4542
6082
  <div class="wf-canvas-watermark" id="wfCanvasWatermark"><img src="/icons/watermark.png" alt=""></div>
4543
6083
  <svg id="wf-canvas" xmlns="http://www.w3.org/2000/svg" ondragover="event.preventDefault()" ondrop="wfCanvasDrop(event)"></svg>
6084
+ <!-- Workflow Store Overlay -->
6085
+ <div class="wf-store-overlay" id="wfStoreOverlay">
6086
+ <div class="wf-store-header">
6087
+ <button class="wf-store-back" onclick="wfStoreClose()">← Library</button>
6088
+ <div class="wf-store-title-area">
6089
+ <span class="wf-store-title">Workflow Store</span>
6090
+ <span class="wf-store-badge">@vaicli</span>
6091
+ </div>
6092
+ <div class="wf-store-search-wrap">
6093
+ <span class="wf-store-search-icon">⌕</span>
6094
+ <input class="wf-store-search" id="wfStoreSearch" placeholder="Search..." oninput="wfStoreRender()">
6095
+ </div>
6096
+ <select class="wf-store-sort" id="wfStoreSort" onchange="wfStoreRender()">
6097
+ <option value="downloads">Popular</option>
6098
+ <option value="name">Name A–Z</option>
6099
+ <option value="complexity">Complex</option>
6100
+ </select>
6101
+ </div>
6102
+ <div class="wf-store-body">
6103
+ <div class="wf-store-inner" id="wfStoreContent">
6104
+ <div style="padding:40px;text-align:center;color:var(--text-muted);">Loading catalog...</div>
6105
+ </div>
6106
+ </div>
6107
+ </div>
4544
6108
  </div>
4545
6109
  <div class="wf-inspector collapsed" id="wfInspector">
4546
- <button class="wf-inspector-toggle" id="wfInspectorToggle" onclick="wfToggleInspector()" title="Toggle inspector">&lsaquo;</button>
4547
6110
  <div class="wf-inspector-content">
4548
6111
  <div class="wf-inspector-header" id="wfInspectorHeader">Inspector</div>
4549
6112
  <div class="wf-inspector-body" id="wfInspectorBody">
@@ -4571,6 +6134,14 @@ Reranking models rescore initial search results to improve relevance ordering.</
4571
6134
  </div>
4572
6135
  </div>
4573
6136
 
6137
+ <!-- ── Workflow Node Help Modal ── -->
6138
+ <div class="wf-help-modal-backdrop" id="wfHelpModalBackdrop" style="display:none;" onclick="wfCloseHelpModal()">
6139
+ <div class="wf-help-modal" onclick="event.stopPropagation()">
6140
+ <div class="wf-help-modal-header" id="wfHelpModalHeader"></div>
6141
+ <div class="wf-help-modal-body" id="wfHelpModalBody"></div>
6142
+ </div>
6143
+ </div>
6144
+
4574
6145
  <!-- ── Workflow Input Modal (pre-execution) ── -->
4575
6146
  <div class="wf-input-modal-backdrop" id="wfInputModalBackdrop" style="display:none;" onclick="wfCloseInputModal()">
4576
6147
  <div class="wf-input-modal" onclick="event.stopPropagation()">
@@ -4665,13 +6236,39 @@ Reranking models rescore initial search results to improve relevance ordering.</
4665
6236
  <div class="card" style="margin-top:16px;">
4666
6237
  <div class="about-section" style="padding-bottom:0;">
4667
6238
  <div class="about-section-title">What's New</div>
4668
- <div class="about-text" style="font-size:13px;">
4669
- <strong>v1.26</strong> — Agent workflows with thinking panel &amp; markdown rendering, multi-step tool orchestration<br>
4670
- <strong>v1.25</strong> — Code generation &amp; project scaffolding tabs<br>
4671
- <strong>v1.24</strong> MCP server install/uninstall/status commands, Electron app v1.5<br>
4672
- <strong>v1.23</strong> — MCP server (expose vai tools to AI agents), HTTP transport, bearer auth, 71+ MCP tests<br>
4673
- <strong>v1.22</strong> — RAG chat with smart source labels, configurable system prompts, streaming responses<br>
4674
- <strong>v1.2</strong> — Multimodal tab, 4 new Explore concepts, auto-update, hidden easter egg 🕹️
6239
+ <div class="about-changelog">
6240
+ <details open>
6241
+ <summary><strong>v1.28</strong> — Lucide icon overhaul</summary>
6242
+ <p>Replaced all emoji and filled LeafyGreen icons with clean, stroke-based Lucide SVGs across the entire app — tabs, workflow nodes, action buttons, benchmark page, explore cards, settings, and more. Added reusable icon helper and path library.</p>
6243
+ </details>
6244
+ <details>
6245
+ <summary><strong>v1.27</strong> — Workflow builder improvements</summary>
6246
+ <p>Workflow palette with drag-and-drop node creation, edge drawing, dry-run preview, and builder mode toggle. New workflow node types: query, rerank, search, ingest.</p>
6247
+ </details>
6248
+ <details>
6249
+ <summary><strong>v1.26</strong> — Agent workflows</summary>
6250
+ <p>Agent workflows with thinking panel &amp; markdown rendering, multi-step tool orchestration, interactive DAG visualization with execution animation.</p>
6251
+ </details>
6252
+ <details>
6253
+ <summary><strong>v1.25</strong> — Code generation &amp; scaffolding</summary>
6254
+ <p>Generate embedding/search code snippets and scaffold full project directories with best-practice structure, ready-to-run RAG pipelines.</p>
6255
+ </details>
6256
+ <details>
6257
+ <summary><strong>v1.24</strong> — MCP server management</summary>
6258
+ <p>MCP server install/uninstall/status commands. Electron app v1.5 with signed macOS builds.</p>
6259
+ </details>
6260
+ <details>
6261
+ <summary><strong>v1.23</strong> — MCP server</summary>
6262
+ <p>Expose vai tools to AI agents via MCP protocol. HTTP transport, bearer auth, 71+ MCP tests.</p>
6263
+ </details>
6264
+ <details>
6265
+ <summary><strong>v1.22</strong> — RAG chat</summary>
6266
+ <p>RAG chat with smart source labels, configurable system prompts, streaming responses.</p>
6267
+ </details>
6268
+ <details>
6269
+ <summary><strong>v1.2</strong> — Multimodal &amp; more</summary>
6270
+ <p>Multimodal tab, 4 new Explore concepts, auto-update, hidden easter egg <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/></svg></p>
6271
+ </details>
4675
6272
  </div>
4676
6273
  </div>
4677
6274
  </div>
@@ -4976,6 +6573,26 @@ Reranking models rescore initial search results to improve relevance ordering.</
4976
6573
  </select>
4977
6574
  </div>
4978
6575
  </div>
6576
+ <div class="settings-row">
6577
+ <div class="settings-label">
6578
+ <span class="settings-label-text">Default Tab</span>
6579
+ <span class="settings-label-hint">Which tab to open when the app starts</span>
6580
+ </div>
6581
+ <div class="settings-control">
6582
+ <select class="settings-select" id="settingsDefaultTab">
6583
+ <option value="home" selected>Home</option>
6584
+ <option value="embed">Embed</option>
6585
+ <option value="compare">Compare</option>
6586
+ <option value="search">Search</option>
6587
+ <option value="multimodal">Multimodal</option>
6588
+ <option value="generate">Generate</option>
6589
+ <option value="chat">Chat</option>
6590
+ <option value="workflows">Workflows</option>
6591
+ <option value="benchmark">Benchmark</option>
6592
+ <option value="explore">Explore</option>
6593
+ </select>
6594
+ </div>
6595
+ </div>
4979
6596
  </div>
4980
6597
  </div>
4981
6598
 
@@ -5440,6 +7057,17 @@ function sendTelemetry(event, extra = {}) {
5440
7057
  } catch { /* telemetry should never break the app */ }
5441
7058
  }
5442
7059
 
7060
+ function telemetryTimer(event, baseFields = {}) {
7061
+ const start = performance.now();
7062
+ return (extra = {}) => {
7063
+ sendTelemetry(event, {
7064
+ ...baseFields,
7065
+ ...extra,
7066
+ durationMs: Math.round(performance.now() - start),
7067
+ });
7068
+ };
7069
+ }
7070
+
5443
7071
  function initTelemetryToggle() {
5444
7072
  const toggle = document.getElementById('settingsTelemetry');
5445
7073
  if (!toggle) return;
@@ -5464,9 +7092,15 @@ async function init() {
5464
7092
  populateModelSelects();
5465
7093
  buildExploreCards();
5466
7094
 
7095
+ // Apply default tab setting
7096
+ const settings = JSON.parse(localStorage.getItem('vai-settings') || '{}');
7097
+ const defaultTab = settings.defaultTab || 'home';
7098
+ switchTab(defaultTab);
7099
+
5467
7100
  // Telemetry
5468
7101
  initTelemetryToggle();
5469
7102
  sendTelemetry('app_launch');
7103
+ sendTelemetry('playground_open');
5470
7104
  }
5471
7105
 
5472
7106
  // ── Tabs ──
@@ -5533,10 +7167,457 @@ function switchTab(tab) {
5533
7167
  if (settingsBtn) settingsBtn.classList.toggle('active', tab === 'settings');
5534
7168
  // Track tab views
5535
7169
  sendTelemetry('tab_view', { tab });
7170
+ sendTelemetry('playground_tab', { tab });
7171
+
7172
+ // Initialize Home page if switching to it
7173
+ if (tab === 'home') {
7174
+ homeInit();
7175
+ }
5536
7176
  }
5537
7177
  // Expose globally so Electron main process can call it
5538
7178
  window.switchTab = switchTab;
5539
7179
 
7180
+ // ── Home Page ──
7181
+
7182
+ let homeData = {
7183
+ announcements: null,
7184
+ releases: null,
7185
+ featuredWorkflows: null,
7186
+ communityWorkflows: null,
7187
+ currentAnnouncementIndex: 0,
7188
+ sortMode: 'trending'
7189
+ };
7190
+
7191
+ async function homeInit() {
7192
+ // Only initialize once per session
7193
+ if (homeData.announcements !== null) return;
7194
+
7195
+ try {
7196
+ // Load version info
7197
+ await updateHomeVersionInfo();
7198
+
7199
+ // Check API key status and show warning if needed
7200
+ checkApiKeyStatus();
7201
+
7202
+ // Load announcements and releases first (fast, local data)
7203
+ await Promise.all([
7204
+ loadAnnouncements(),
7205
+ loadReleases(),
7206
+ ]);
7207
+
7208
+ // Render available sections immediately
7209
+ renderAnnouncements();
7210
+ renderReleases();
7211
+
7212
+ // Load marketplace data in background (slower, network-dependent)
7213
+ loadMarketplaceData().then(() => {
7214
+ renderFeaturedWorkflows();
7215
+ renderCommunityWorkflows();
7216
+ }).catch(() => {});
7217
+
7218
+ } catch (err) {
7219
+ console.error('Failed to initialize Home page:', err);
7220
+ }
7221
+ }
7222
+
7223
+ async function updateHomeVersionInfo() {
7224
+ let version = window.appVersion;
7225
+
7226
+ // If version not set yet, try to fetch from Electron
7227
+ if (!version && window.vai && window.vai.getVersion) {
7228
+ try {
7229
+ const v = await window.vai.getVersion();
7230
+ if (v) {
7231
+ version = typeof v === 'object' ? v.app : v;
7232
+ window.appVersion = version;
7233
+ }
7234
+ } catch (e) {
7235
+ console.warn('Failed to get version:', e);
7236
+ }
7237
+ }
7238
+
7239
+ // Fallback for web mode: try to get from CLI package.json via API
7240
+ if (!version) {
7241
+ try {
7242
+ const res = await fetch('/api/version');
7243
+ if (res.ok) {
7244
+ const data = await res.json();
7245
+ version = data.version || data.app;
7246
+ if (version) window.appVersion = version;
7247
+ }
7248
+ } catch (e) {
7249
+ // Ignore fetch errors in web mode
7250
+ }
7251
+ }
7252
+
7253
+ version = version || 'dev';
7254
+ document.getElementById('homeVersionBadge').textContent = `v${version}`;
7255
+ const footerEl = document.getElementById('homeFooterVersion');
7256
+ if (footerEl) footerEl.textContent = `Version ${version} • © 2026 VAI`;
7257
+ }
7258
+
7259
+ async function checkApiKeyStatus() {
7260
+ try {
7261
+ const res = await fetch('/api/config');
7262
+ const data = await res.json();
7263
+ const warningEl = document.getElementById('homeApiWarning');
7264
+
7265
+ if (!data.hasKey) {
7266
+ warningEl.style.display = 'block';
7267
+ } else {
7268
+ warningEl.style.display = 'none';
7269
+ }
7270
+ } catch {
7271
+ // If config check fails, assume no API key
7272
+ document.getElementById('homeApiWarning').style.display = 'block';
7273
+ }
7274
+ }
7275
+
7276
+ async function loadAnnouncements() {
7277
+ try {
7278
+ const res = await fetch('/api/home/announcements');
7279
+ const data = await res.json();
7280
+
7281
+ // Store total count before filtering
7282
+ homeData.totalAnnouncements = data.announcements.length;
7283
+
7284
+ // Filter out dismissed announcements
7285
+ const dismissed = JSON.parse(localStorage.getItem('vai-dismissed-announcements') || '[]');
7286
+ homeData.announcements = data.announcements.filter(a => !dismissed.includes(a.id));
7287
+
7288
+ } catch (err) {
7289
+ console.error('Failed to load announcements:', err);
7290
+ homeData.announcements = [];
7291
+ homeData.totalAnnouncements = 0;
7292
+ }
7293
+ }
7294
+
7295
+ async function loadReleases() {
7296
+ try {
7297
+ // Try cache first
7298
+ const cached = localStorage.getItem('vai-releases-cache');
7299
+ if (cached) {
7300
+ const { data, timestamp } = JSON.parse(cached);
7301
+ // Use cache if less than 30 minutes old AND it has real data (not fallback)
7302
+ const hasRealData = data && data.length > 0 && !data[0].version?.includes('1.0.0');
7303
+ if (hasRealData && Date.now() - timestamp < 30 * 60 * 1000) {
7304
+ homeData.releases = data;
7305
+ return;
7306
+ }
7307
+ }
7308
+
7309
+ const res = await fetch('/api/home/releases');
7310
+ const data = await res.json();
7311
+ homeData.releases = data.releases;
7312
+
7313
+ // Only cache if we got real data (not fallback)
7314
+ const hasRealData = data.releases && data.releases.length > 0 && !data.releases[0].version?.includes('1.0.0');
7315
+ if (hasRealData) {
7316
+ localStorage.setItem('vai-releases-cache', JSON.stringify({
7317
+ data: data.releases,
7318
+ timestamp: Date.now()
7319
+ }));
7320
+ } else {
7321
+ // Clear stale cache if we got fallback data
7322
+ localStorage.removeItem('vai-releases-cache');
7323
+ }
7324
+
7325
+ } catch (err) {
7326
+ console.error('Failed to load releases:', err);
7327
+ homeData.releases = [];
7328
+ }
7329
+ }
7330
+
7331
+ async function loadMarketplaceData() {
7332
+ try {
7333
+ const res = await fetch('/api/workflows/catalog');
7334
+ const data = await res.json();
7335
+
7336
+ // Extract featured and community workflows
7337
+ homeData.featuredWorkflows = data.workflows.filter(w => w.featured).slice(0, 4);
7338
+ homeData.communityWorkflows = data.workflows.filter(w => !w.featured);
7339
+
7340
+ } catch (err) {
7341
+ console.error('Failed to load marketplace data:', err);
7342
+ homeData.featuredWorkflows = [];
7343
+ homeData.communityWorkflows = [];
7344
+ }
7345
+ }
7346
+
7347
+ function renderAnnouncements() {
7348
+ const container = document.getElementById('homeAnnouncements');
7349
+ const carousel = document.getElementById('announcementsCarousel');
7350
+ const dots = document.getElementById('announcementsDots');
7351
+ const restoreBtn = document.getElementById('announcementsRestore');
7352
+
7353
+ // Show restore button if any announcements have been dismissed
7354
+ const hasDismissed = (homeData.totalAnnouncements || 0) > homeData.announcements.length;
7355
+ if (restoreBtn) restoreBtn.style.display = hasDismissed ? 'block' : 'none';
7356
+
7357
+ if (!homeData.announcements.length) {
7358
+ container.style.display = hasDismissed ? 'block' : 'none';
7359
+ carousel.innerHTML = '';
7360
+ dots.innerHTML = '';
7361
+ return;
7362
+ }
7363
+
7364
+ // Render cards
7365
+ carousel.innerHTML = homeData.announcements.map((ann, i) => {
7366
+ const bgClasses = [
7367
+ 'home-announcement-card',
7368
+ i === 0 ? 'active' : '',
7369
+ ann.bg_image ? 'has-bg-image' : ''
7370
+ ].filter(Boolean).join(' ');
7371
+ const bgStyle = ann.bg_image
7372
+ ? `background-image: url('${ann.bg_image}');`
7373
+ : ann.bg_color
7374
+ ? `background: ${ann.bg_color};`
7375
+ : '';
7376
+ return `
7377
+ <div class="${bgClasses}" data-id="${ann.id}" style="${bgStyle}">
7378
+ <button class="home-announcement-dismiss" onclick="dismissAnnouncement('${ann.id}')">×</button>
7379
+ ${ann.icon ? `<div class="home-announcement-icon">${ann.icon}</div>` : ''}
7380
+ ${ann.badge ? `<div class="badge">${ann.badge}</div>` : ''}
7381
+ <h3>${ann.title}</h3>
7382
+ <p>${ann.description}</p>
7383
+ ${ann.cta ? `<button class="cta" onclick="${ann.cta.action === 'navigate' ? `switchTab('${ann.cta.target.slice(1)}')` : 'void(0)'}">${ann.cta.label}</button>` : ''}
7384
+ </div>`;
7385
+ }).join('');
7386
+
7387
+ // Render dots
7388
+ if (homeData.announcements.length > 1) {
7389
+ dots.innerHTML = homeData.announcements.map((_, i) =>
7390
+ `<div class="home-announcement-dot${i === 0 ? ' active' : ''}" onclick="showAnnouncement(${i})"></div>`
7391
+ ).join('');
7392
+
7393
+ // Start rotation
7394
+ startAnnouncementRotation();
7395
+ }
7396
+
7397
+ container.style.display = 'block';
7398
+ }
7399
+
7400
+ function showAnnouncement(index) {
7401
+ homeData.currentAnnouncementIndex = index;
7402
+
7403
+ // Update cards
7404
+ document.querySelectorAll('.home-announcement-card').forEach((card, i) => {
7405
+ if (i === index) {
7406
+ card.classList.add('active');
7407
+ } else {
7408
+ card.classList.remove('active');
7409
+ }
7410
+ });
7411
+
7412
+ // Update dots
7413
+ document.querySelectorAll('.home-announcement-dot').forEach((dot, i) => {
7414
+ if (i === index) {
7415
+ dot.classList.add('active');
7416
+ } else {
7417
+ dot.classList.remove('active');
7418
+ }
7419
+ });
7420
+
7421
+ // Reset the rotation timer so we get a full interval after manual clicks
7422
+ resetAnnouncementRotation();
7423
+ }
7424
+
7425
+ let announcementRotationTimer = null;
7426
+
7427
+ function startAnnouncementRotation() {
7428
+ stopAnnouncementRotation();
7429
+ if (homeData.announcements.length <= 1) return;
7430
+ announcementRotationTimer = setInterval(() => {
7431
+ const next = (homeData.currentAnnouncementIndex + 1) % homeData.announcements.length;
7432
+ homeData.currentAnnouncementIndex = next;
7433
+
7434
+ // Update cards directly (don't call showAnnouncement to avoid resetting timer)
7435
+ document.querySelectorAll('.home-announcement-card').forEach((card, i) => {
7436
+ if (i === next) {
7437
+ card.classList.add('active');
7438
+ } else {
7439
+ card.classList.remove('active');
7440
+ }
7441
+ });
7442
+ document.querySelectorAll('.home-announcement-dot').forEach((dot, i) => {
7443
+ if (i === next) {
7444
+ dot.classList.add('active');
7445
+ } else {
7446
+ dot.classList.remove('active');
7447
+ }
7448
+ });
7449
+ }, 6000);
7450
+ }
7451
+
7452
+ function stopAnnouncementRotation() {
7453
+ if (announcementRotationTimer) {
7454
+ clearInterval(announcementRotationTimer);
7455
+ announcementRotationTimer = null;
7456
+ }
7457
+ }
7458
+
7459
+ function resetAnnouncementRotation() {
7460
+ stopAnnouncementRotation();
7461
+ startAnnouncementRotation();
7462
+ }
7463
+
7464
+ function dismissAnnouncement(id) {
7465
+ const dismissed = JSON.parse(localStorage.getItem('vai-dismissed-announcements') || '[]');
7466
+ dismissed.push(id);
7467
+ localStorage.setItem('vai-dismissed-announcements', JSON.stringify(dismissed));
7468
+
7469
+ // Remove from current data and re-render
7470
+ homeData.announcements = homeData.announcements.filter(a => a.id !== id);
7471
+ renderAnnouncements();
7472
+ }
7473
+
7474
+ async function restoreAnnouncements() {
7475
+ localStorage.removeItem('vai-dismissed-announcements');
7476
+ // Re-fetch all announcements
7477
+ try {
7478
+ const res = await fetch('/api/home/announcements');
7479
+ const data = await res.json();
7480
+ homeData.announcements = data.announcements;
7481
+ renderAnnouncements();
7482
+ } catch (err) {
7483
+ console.error('Failed to restore announcements:', err);
7484
+ }
7485
+ }
7486
+
7487
+ function renderReleases() {
7488
+ const container = document.getElementById('releasesTimeline');
7489
+
7490
+ const releases = homeData.releases.slice(0, 1); // Show only the latest release
7491
+ container.innerHTML = releases.map(release => `
7492
+ <div class="home-release-item">
7493
+ <div>
7494
+ <div class="home-release-version">${release.version}</div>
7495
+ <div class="home-release-date">${new Date(release.date).toLocaleDateString()}</div>
7496
+ <ul class="home-release-highlights">
7497
+ ${release.highlights.map(highlight => `<li>${highlight}</li>`).join('')}
7498
+ </ul>
7499
+ </div>
7500
+ </div>
7501
+ `).join('');
7502
+ }
7503
+
7504
+ function renderFeaturedWorkflows() {
7505
+ const container = document.getElementById('featuredWorkflows');
7506
+
7507
+ if (!homeData.featuredWorkflows.length) {
7508
+ container.innerHTML = '<div class="home-community-empty"><h4>Coming Soon</h4><p>Featured workflows launching soon</p></div>';
7509
+ return;
7510
+ }
7511
+
7512
+ container.innerHTML = homeData.featuredWorkflows.map(workflow => `
7513
+ <div class="home-workflow-card">
7514
+ <h5>${workflow.name || 'Untitled'}</h5>
7515
+ <p>${workflow.description || 'No description available'}</p>
7516
+ <div class="home-workflow-meta">
7517
+ <div class="home-workflow-domain">${workflow.category || 'utility'}</div>
7518
+ <div class="home-workflow-author">by ${workflow.author?.name || 'unknown'}</div>
7519
+ </div>
7520
+ <div class="home-workflow-actions">
7521
+ <button class="home-workflow-btn" onclick="installWorkflow('${workflow.packageName}')">Install</button>
7522
+ <button class="home-workflow-btn" onclick="viewWorkflowDetails('${workflow.packageName}')">Details</button>
7523
+ </div>
7524
+ </div>
7525
+ `).join('');
7526
+ }
7527
+
7528
+ function renderCommunityWorkflows() {
7529
+ const container = document.getElementById('communityWorkflows');
7530
+
7531
+ if (!homeData.communityWorkflows.length) {
7532
+ container.innerHTML = '<div class="home-community-empty"><h4>Be the First!</h4><p>Publish a workflow to get started</p></div>';
7533
+ return;
7534
+ }
7535
+
7536
+ let workflows = [...homeData.communityWorkflows];
7537
+
7538
+ // Sort based on current mode
7539
+ switch (homeData.sortMode) {
7540
+ case 'trending':
7541
+ // Use downloads as proxy for trending
7542
+ workflows.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
7543
+ break;
7544
+ case 'newest':
7545
+ // Sort by name alphabetically as fallback (no publish date available)
7546
+ workflows.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
7547
+ break;
7548
+ case 'installs':
7549
+ workflows.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
7550
+ break;
7551
+ }
7552
+
7553
+ container.innerHTML = workflows.slice(0, 6).map(workflow => `
7554
+ <div class="home-workflow-card">
7555
+ <h5>${workflow.name || 'Untitled'}</h5>
7556
+ <p>${workflow.description || 'No description available'}</p>
7557
+ <div class="home-workflow-meta">
7558
+ <div class="home-workflow-domain">${workflow.category || 'utility'}</div>
7559
+ <div class="home-workflow-author">by ${workflow.author?.name || 'unknown'}</div>
7560
+ </div>
7561
+ <div class="home-workflow-actions">
7562
+ <button class="home-workflow-btn" onclick="installWorkflow('${workflow.packageName}')">Install</button>
7563
+ <button class="home-workflow-btn" onclick="viewWorkflowDetails('${workflow.packageName}')">Details</button>
7564
+ </div>
7565
+ </div>
7566
+ `).join('');
7567
+ }
7568
+
7569
+ function sortCommunityWorkflows(mode) {
7570
+ homeData.sortMode = mode;
7571
+
7572
+ // Update tab appearance
7573
+ document.querySelectorAll('.home-sort-tab').forEach(tab => {
7574
+ tab.classList.toggle('active', tab.dataset.sort === mode);
7575
+ });
7576
+
7577
+ renderCommunityWorkflows();
7578
+ }
7579
+
7580
+ function filterAndSwitchWorkflows(domain) {
7581
+ // Switch to workflows tab with domain filter
7582
+ switchTab('workflows');
7583
+ // TODO: Apply domain filter when workflows tab supports it
7584
+ console.log('Filtering workflows by domain:', domain);
7585
+ }
7586
+
7587
+ function installWorkflow(packageName) {
7588
+ // Use the workflow store install function
7589
+ wfStoreInstall(packageName, null);
7590
+ }
7591
+
7592
+ function viewWorkflowDetails(packageName) {
7593
+ // Find the workflow by packageName and show its details
7594
+ const allWorkflows = [...(homeData.featuredWorkflows || []), ...(homeData.communityWorkflows || [])];
7595
+ const wf = allWorkflows.find(w => w.packageName === packageName);
7596
+ if (wf) {
7597
+ // Use the workflow store detail modal
7598
+ wfStoreShowDetail(wf.name);
7599
+ } else {
7600
+ // Fallback: switch to workflows tab
7601
+ switchTab('workflows');
7602
+ }
7603
+ }
7604
+
7605
+ function publishWorkflow() {
7606
+ // TODO: Open workflow publishing flow
7607
+ console.log('Opening workflow publisher');
7608
+ switchTab('workflows');
7609
+ }
7610
+
7611
+ function reportBug() {
7612
+ // Open bug report (reuse existing functionality if available)
7613
+ const bugButton = document.getElementById('bugButton');
7614
+ if (bugButton) {
7615
+ bugButton.click();
7616
+ } else {
7617
+ window.open('https://github.com/mrlynn/voyageai-cli/issues/new', '_blank');
7618
+ }
7619
+ }
7620
+
5540
7621
  // ── Config ──
5541
7622
  async function loadConfig() {
5542
7623
  try {
@@ -5659,11 +7740,13 @@ window.doEmbed = async function() {
5659
7740
  hideError('embedError');
5660
7741
  const text = document.getElementById('embedInput').value.trim();
5661
7742
  if (!text) { showError('embedError', 'Enter some text to embed'); return; }
5662
- sendTelemetry('api_call', { endpoint: 'embed', model: document.getElementById('embedModel').value });
7743
+ const _embedModel = document.getElementById('embedModel').value;
7744
+ sendTelemetry('api_call', { endpoint: 'embed', model: _embedModel });
7745
+ const _embedDone = telemetryTimer('playground_embed', { model: _embedModel, inputType: document.getElementById('embedInputType').value || undefined });
5663
7746
 
5664
7747
  setLoading('embedBtn', true);
5665
7748
  try {
5666
- const model = document.getElementById('embedModel').value;
7749
+ const model = _embedModel;
5667
7750
  const inputType = document.getElementById('embedInputType').value || undefined;
5668
7751
  const dims = document.getElementById('embedDimensions').value;
5669
7752
  const dimensions = dims ? parseInt(dims, 10) : undefined;
@@ -5701,6 +7784,8 @@ window.doEmbed = async function() {
5701
7784
  buildHeatmap(emb, document.getElementById('embedHeatmap'));
5702
7785
 
5703
7786
  document.getElementById('embedResult').classList.add('visible');
7787
+ CostTracker.addOperation('embed', model, data.usage?.total_tokens || 0);
7788
+ _embedDone();
5704
7789
  } catch (err) {
5705
7790
  showError('embedError', err.message);
5706
7791
  } finally {
@@ -5743,6 +7828,7 @@ function buildHeatmap(vec, container) {
5743
7828
  window.doCompare = async function() {
5744
7829
  hideError('compareError');
5745
7830
  sendTelemetry('api_call', { endpoint: 'compare', model: document.getElementById('compareModel').value });
7831
+ const _compareDone = telemetryTimer('playground_similarity', { model: document.getElementById('compareModel').value });
5746
7832
  const a = document.getElementById('compareA').value.trim();
5747
7833
  const b = document.getElementById('compareB').value.trim();
5748
7834
  if (!a || !b) { showError('compareError', 'Enter text in both fields'); return; }
@@ -5825,6 +7911,8 @@ window.doCompare = async function() {
5825
7911
  `;
5826
7912
 
5827
7913
  document.getElementById('compareResult').classList.add('visible');
7914
+ CostTracker.addOperation('compare', data.model || model, data.usage?.total_tokens || 0);
7915
+ _compareDone();
5828
7916
  } catch (err) {
5829
7917
  showError('compareError', err.message);
5830
7918
  } finally {
@@ -5836,6 +7924,7 @@ window.doCompare = async function() {
5836
7924
  window.doSearch = async function(withRerank) {
5837
7925
  hideError('searchError');
5838
7926
  sendTelemetry('api_call', { endpoint: withRerank ? 'rerank' : 'search' });
7927
+ const _searchDone = telemetryTimer(withRerank ? 'playground_rerank' : 'playground_search', { model: document.getElementById('searchEmbedModel').value });
5839
7928
  const query = document.getElementById('searchQuery').value.trim();
5840
7929
  const docsText = document.getElementById('searchDocs').value.trim();
5841
7930
  if (!query || !docsText) { showError('searchError', 'Enter a query and documents'); return; }
@@ -5868,9 +7957,10 @@ window.doSearch = async function(withRerank) {
5868
7957
  const embeddingResults = scores.slice(0, topK);
5869
7958
 
5870
7959
  let rerankResults = null;
7960
+ let rerankData = null;
5871
7961
  if (withRerank) {
5872
7962
  const rerankModel = document.getElementById('searchRerankModel').value;
5873
- const rerankData = await apiPost('/api/rerank', { query, documents, model: rerankModel, topK });
7963
+ rerankData = await apiPost('/api/rerank', { query, documents, model: rerankModel, topK });
5874
7964
  rerankResults = rerankData.data.map(r => ({
5875
7965
  index: r.index,
5876
7966
  text: documents[r.index],
@@ -5880,6 +7970,12 @@ window.doSearch = async function(withRerank) {
5880
7970
 
5881
7971
  renderSearchResults(embeddingResults, rerankResults);
5882
7972
  document.getElementById('searchResult').classList.add('visible');
7973
+ CostTracker.addOperation('search', embedModel, embedData.usage?.total_tokens || 0);
7974
+ if (withRerank && rerankResults) {
7975
+ const rerankModel = document.getElementById('searchRerankModel').value;
7976
+ CostTracker.addOperation('rerank', rerankModel, rerankData?.usage?.total_tokens || 0);
7977
+ }
7978
+ _searchDone();
5883
7979
  } catch (err) {
5884
7980
  showError('searchError', err.message);
5885
7981
  } finally {
@@ -6533,6 +8629,7 @@ window.doBenchLatency = async function() {
6533
8629
  latencies.push(data.elapsed);
6534
8630
  tokens = data.tokens;
6535
8631
  dims = data.dimensions;
8632
+ CostTracker.addOperation('bench-latency', model, data.tokens || 0);
6536
8633
  } catch (err) {
6537
8634
  document.getElementById(`bench-stats-${mi}`).textContent = 'Error';
6538
8635
  document.getElementById(`bench-bar-${mi}`).classList.remove('running');
@@ -6626,6 +8723,8 @@ window.doBenchRanking = async function() {
6626
8723
 
6627
8724
  rankedA = rankBySimilarity(dataA.embeddings, documents, topK);
6628
8725
  rankedB = rankBySimilarity(dataB.embeddings, documents, topK);
8726
+ CostTracker.addOperation('bench-ranking', modelA, dataA.tokens || 0);
8727
+ CostTracker.addOperation('bench-ranking', modelB, dataB.tokens || 0);
6629
8728
  } else {
6630
8729
  // Rerank mode
6631
8730
  const [dataA, dataB] = await Promise.all([
@@ -6643,6 +8742,8 @@ window.doBenchRanking = async function() {
6643
8742
  text: documents[r.index],
6644
8743
  score: r.relevance_score,
6645
8744
  }));
8745
+ CostTracker.addOperation('bench-ranking', modelA, dataA.usage?.total_tokens || 0);
8746
+ CostTracker.addOperation('bench-ranking', modelB, dataB.usage?.total_tokens || 0);
6646
8747
  }
6647
8748
 
6648
8749
  // Render comparison
@@ -6783,6 +8884,7 @@ window.doBenchQuantization = async function() {
6783
8884
  const start = performance.now();
6784
8885
  const data = await apiPost('/api/embed', body);
6785
8886
  const elapsed = performance.now() - start;
8887
+ CostTracker.addOperation('bench-quantization', model, data.usage?.total_tokens || 0);
6786
8888
 
6787
8889
  const embeddings = data.data.map(d => d.embedding);
6788
8890
  const queryEmbed = embeddings[0];
@@ -7461,6 +9563,15 @@ function initSettings() {
7461
9563
  timeoutSel.addEventListener('change', () => saveSetting('timeout', timeoutSel.value));
7462
9564
  }
7463
9565
 
9566
+ // Default Tab
9567
+ const defaultTabSel = document.getElementById('settingsDefaultTab');
9568
+ if (defaultTabSel) {
9569
+ defaultTabSel.value = s.defaultTab || 'home';
9570
+ defaultTabSel.addEventListener('change', () => {
9571
+ saveSetting('defaultTab', defaultTabSel.value);
9572
+ });
9573
+ }
9574
+
7464
9575
  // Benchmark iterations
7465
9576
  const benchIterSel = document.getElementById('settingsBenchIter');
7466
9577
  if (benchIterSel) {
@@ -8335,6 +10446,7 @@ window.doMultimodalCompare = async function() {
8335
10446
  }
8336
10447
 
8337
10448
  document.getElementById('mmResult').classList.add('visible');
10449
+ CostTracker.addOperation('multimodal-compare', data.model || model, usage.total_tokens || 0);
8338
10450
  } catch (err) {
8339
10451
  showError('mmError', err.message);
8340
10452
  } finally {
@@ -8538,6 +10650,7 @@ window.doMultimodalSearch = async function() {
8538
10650
  });
8539
10651
 
8540
10652
  document.getElementById('mmSearchResult').classList.add('visible');
10653
+ CostTracker.addOperation('multimodal-search', model, data.usage?.total_tokens || 0);
8541
10654
  } catch (err) {
8542
10655
  showError('mmSearchError', err.message);
8543
10656
  } finally {
@@ -9436,20 +11549,24 @@ function renderMarkdown(md) {
9436
11549
  /**
9437
11550
  * Tool metadata: icon, label, and a function to summarize the call for the thinking panel.
9438
11551
  */
11552
+ function lucideIcon(id, size = 16) {
11553
+ return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" style="color:var(--accent)"><use href="#${id}"/></svg>`;
11554
+ }
11555
+
9439
11556
  const TOOL_META = {
9440
- vai_query: { icon: '\uD83D\uDD0D', label: 'RAG Query', verb: 'Searching', descFn: a => a.query ? `"${a.query}"` : '' },
9441
- vai_search: { icon: '\uD83D\uDD0E', label: 'Vector Search', verb: 'Searching vectors', descFn: a => a.query ? `"${a.query}"` : '' },
9442
- vai_rerank: { icon: '\u2195\uFE0F', label: 'Rerank', verb: 'Reranking', descFn: a => a.query ? `${a.documents?.length || '?'} docs for "${a.query}"` : '' },
9443
- vai_embed: { icon: '\uD83E\uDDE0', label: 'Embed', verb: 'Embedding', descFn: a => a.text ? `"${a.text.slice(0, 60)}${a.text.length > 60 ? '...' : ''}"` : '' },
9444
- vai_similarity: { icon: '\uD83C\uDFAF', label: 'Similarity', verb: 'Comparing', descFn: a => a.text1 ? `two texts` : '' },
9445
- vai_collections: { icon: '\uD83D\uDDC4\uFE0F', label: 'Collections', verb: 'Discovering', descFn: a => a.db ? `in ${a.db}` : 'available databases' },
9446
- vai_models: { icon: '\uD83E\uDD16', label: 'Models', verb: 'Listing', descFn: () => 'available models' },
9447
- vai_topics: { icon: '\uD83D\uDCDA', label: 'Topics', verb: 'Browsing', descFn: () => 'educational topics' },
9448
- vai_explain: { icon: '\uD83D\uDCA1', label: 'Explain', verb: 'Explaining', descFn: a => a.topic || '' },
9449
- vai_estimate: { icon: '\uD83D\uDCB0', label: 'Cost Estimate', verb: 'Estimating', descFn: a => a.docs ? `${a.docs} docs` : '' },
9450
- vai_ingest: { icon: '\uD83D\uDCE5', label: 'Ingest', verb: 'Ingesting', descFn: a => a.source || 'document' },
11557
+ vai_query: { iconId: 'lg-search', label: 'RAG Query', verb: 'Searching', descFn: a => a.query ? `"${a.query}"` : '' },
11558
+ vai_search: { iconId: 'lg-search-code', label: 'Vector Search', verb: 'Searching vectors', descFn: a => a.query ? `"${a.query}"` : '' },
11559
+ vai_rerank: { iconId: 'lg-arrow-up-down', label: 'Rerank', verb: 'Reranking', descFn: a => a.query ? `${a.documents?.length || '?'} docs for "${a.query}"` : '' },
11560
+ vai_embed: { iconId: 'lg-lightning', label: 'Embed', verb: 'Embedding', descFn: a => a.text ? `"${a.text.slice(0, 60)}${a.text.length > 60 ? '...' : ''}"` : '' },
11561
+ vai_similarity: { iconId: 'lg-target', label: 'Similarity', verb: 'Comparing', descFn: a => a.text1 ? `two texts` : '' },
11562
+ vai_collections: { iconId: 'lg-database', label: 'Collections', verb: 'Discovering', descFn: a => a.db ? `in ${a.db}` : 'available databases' },
11563
+ vai_models: { iconId: 'lg-bot', label: 'Models', verb: 'Listing', descFn: () => 'available models' },
11564
+ vai_topics: { iconId: 'lg-cube', label: 'Topics', verb: 'Browsing', descFn: () => 'educational topics' },
11565
+ vai_explain: { iconId: 'lg-bulb', label: 'Explain', verb: 'Explaining', descFn: a => a.topic || '' },
11566
+ vai_estimate: { iconId: 'lg-coins', label: 'Cost Estimate', verb: 'Estimating', descFn: a => a.docs ? `${a.docs} docs` : '' },
11567
+ vai_ingest: { iconId: 'lg-inbox', label: 'Ingest', verb: 'Ingesting', descFn: a => a.source || 'document' },
9451
11568
  };
9452
- const DEFAULT_TOOL_META = { icon: '\u2699\uFE0F', label: '', verb: 'Running', descFn: () => '' };
11569
+ const DEFAULT_TOOL_META = { iconId: 'lg-config', label: '', verb: 'Running', descFn: () => '' };
9453
11570
 
9454
11571
  /**
9455
11572
  * Create the thinking panel <details> element.
@@ -9462,7 +11579,7 @@ function createThinkingPanel() {
9462
11579
 
9463
11580
  const summary = document.createElement('summary');
9464
11581
  summary.innerHTML =
9465
- '<span class="thinking-icon">\uD83E\uDDE0</span>' +
11582
+ '<span class="thinking-icon">' + lucideIcon('lg-brain', 14) + '</span>' +
9466
11583
  '<span class="thinking-label">Thinking</span>' +
9467
11584
  '<span class="thinking-count">0</span>' +
9468
11585
  '<span class="thinking-elapsed"></span>' +
@@ -9505,7 +11622,7 @@ function createThinkingPanel() {
9505
11622
 
9506
11623
  const iconDiv = document.createElement('div');
9507
11624
  iconDiv.className = 'thinking-step-icon';
9508
- iconDiv.textContent = meta.icon;
11625
+ iconDiv.innerHTML = lucideIcon(meta.iconId, 14);
9509
11626
  step.appendChild(iconDiv);
9510
11627
 
9511
11628
  const body = document.createElement('div');
@@ -9560,7 +11677,7 @@ function createThinkingPanel() {
9560
11677
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
9561
11678
  summary.querySelector('.thinking-elapsed').textContent = elapsed + 's';
9562
11679
  summary.querySelector('.thinking-label').textContent = 'Thought for ' + elapsed + 's';
9563
- summary.querySelector('.thinking-icon').textContent = '\u2728';
11680
+ summary.querySelector('.thinking-icon').innerHTML = lucideIcon('lg-sparkles', 14);
9564
11681
  // Mark last step done and collapse
9565
11682
  if (activeStep) {
9566
11683
  activeStep.classList.remove('active');
@@ -9685,6 +11802,7 @@ async function sendChatMessage() {
9685
11802
  let fullText = '';
9686
11803
  let sources = [];
9687
11804
  let thinkingPanel = null;
11805
+ let retrievalCostTracked = false;
9688
11806
 
9689
11807
  while (true) {
9690
11808
  const { done, value } = await reader.read();
@@ -9705,6 +11823,14 @@ async function sendChatMessage() {
9705
11823
 
9706
11824
  if (currentEvent === 'retrieval') {
9707
11825
  typing.textContent = `Retrieved ${data.docs?.length || 0} docs (${data.timeMs}ms)`;
11826
+ // Track embedding cost from retrieval (flag to avoid double-counting with done)
11827
+ if (data.tokens?.embed) {
11828
+ CostTracker.addOperation('chat-embed', 'voyage-4-lite', data.tokens.embed);
11829
+ retrievalCostTracked = true;
11830
+ }
11831
+ if (data.tokens?.rerank) {
11832
+ CostTracker.addOperation('chat-rerank', 'rerank-2.5', data.tokens.rerank);
11833
+ }
9708
11834
  }
9709
11835
 
9710
11836
  if (currentEvent === 'tool_call') {
@@ -9729,6 +11855,24 @@ async function sendChatMessage() {
9729
11855
  }
9730
11856
 
9731
11857
  if (currentEvent === 'done') {
11858
+ // Track costs from done metadata (only if retrieval event didn't already)
11859
+ const meta = data.metadata || {};
11860
+ if (!retrievalCostTracked && meta.tokens?.embed) {
11861
+ CostTracker.addOperation('chat-embed', 'voyage-4-lite', meta.tokens.embed);
11862
+ }
11863
+ if (!retrievalCostTracked && meta.tokens?.rerank) {
11864
+ CostTracker.addOperation('chat-rerank', 'rerank-2.5', meta.tokens.rerank);
11865
+ }
11866
+ // Track LLM generation cost
11867
+ if (meta.tokens?.llmInput || meta.tokens?.llmOutput) {
11868
+ CostTracker.addLLMOperation('chat-llm', meta.llmModel, meta.tokens.llmInput, meta.tokens.llmOutput);
11869
+ }
11870
+ // Agent mode: track tool call tokens from steps/layers
11871
+ if (data.steps) {
11872
+ for (const step of data.steps) {
11873
+ if (step.tokens) CostTracker.addOperation('chat-tool', step.model || 'voyage-4-lite', step.tokens);
11874
+ }
11875
+ }
9732
11876
  // Finalize the thinking panel (collapse, show elapsed)
9733
11877
  if (thinkingPanel) thinkingPanel.finalize();
9734
11878
  // Render accumulated text as markdown for assistant messages
@@ -9935,6 +12079,57 @@ init();
9935
12079
  .bug-success h3{margin:0 0 12px;color:var(--accent-text);font-size:20px}
9936
12080
  .bug-success p{margin:8px 0;color:var(--text-muted)}
9937
12081
  .bug-success code{background:rgba(255,255,255,0.1);padding:4px 8px;border-radius:4px;font-size:12px;color:var(--accent)}
12082
+
12083
+ /* ── Cost Dashboard ── */
12084
+ #costStatusBar {
12085
+ position: fixed; bottom: 0; left: 0; right: 0; z-index: 9999;
12086
+ background: var(--bg-surface); border-top: 1px solid var(--border);
12087
+ padding: 8px 16px; font-size: 13px; color: var(--text-dim);
12088
+ display: flex; align-items: center; gap: 12px; backdrop-filter: blur(8px);
12089
+ opacity: 0.95; font-family: 'SF Mono', 'Fira Code', monospace;
12090
+ }
12091
+ #costStatusBar .cost-sep { color: var(--border); }
12092
+ #costStatusBar .cost-savings { color: var(--green, #00D4AA); }
12093
+ #costStatusBar .cost-more { color: var(--red, #FF6960); }
12094
+ #costStatusBar .cost-val { color: var(--accent); font-weight: 600; }
12095
+ #costDetailsToggle {
12096
+ margin-left: auto; cursor: pointer; color: var(--accent);
12097
+ background: none; border: 1px solid var(--border); border-radius: 6px;
12098
+ padding: 3px 10px; font-size: 12px; font-family: inherit;
12099
+ }
12100
+ #costDetailsToggle:hover { background: var(--bg-card); }
12101
+ #costDetailPanel {
12102
+ position: fixed; bottom: 40px; left: 0; right: 0; z-index: 9998;
12103
+ background: var(--bg-surface); border-top: 1px solid var(--border);
12104
+ max-height: 0; overflow: hidden; transition: max-height 0.3s ease;
12105
+ }
12106
+ #costDetailPanel.open { max-height: 50vh; overflow-y: auto; }
12107
+ #costDetailPanel .cost-panel-inner { padding: 16px 20px; }
12108
+ #costDetailPanel table {
12109
+ width: 100%; border-collapse: collapse; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace;
12110
+ }
12111
+ #costDetailPanel th { text-align: left; color: var(--text-muted); font-weight: 500; padding: 4px 8px; border-bottom: 1px solid var(--border); }
12112
+ #costDetailPanel td { padding: 4px 8px; color: var(--text-dim); border-bottom: 1px solid var(--border); }
12113
+ #costDetailPanel .cost-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
12114
+ #costDetailPanel .cost-panel-header h3 { margin: 0; font-size: 14px; color: var(--text); }
12115
+ #costResetBtn {
12116
+ background: none; border: 1px solid var(--border); border-radius: 6px;
12117
+ padding: 3px 10px; font-size: 12px; color: var(--text-dim); cursor: pointer; font-family: inherit;
12118
+ }
12119
+ #costResetBtn:hover { background: var(--bg-card); color: var(--text); }
12120
+ .cost-projection { margin-top: 12px; font-size: 12px; color: var(--text-muted); }
12121
+ .cost-projection span { color: var(--accent); font-weight: 600; }
12122
+ .cost-toast {
12123
+ position: fixed; bottom: 52px; left: 50%; transform: translateX(-50%);
12124
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
12125
+ padding: 8px 16px; font-size: 12px; color: var(--text-dim); z-index: 10000;
12126
+ font-family: 'SF Mono', 'Fira Code', monospace; white-space: nowrap;
12127
+ animation: costToastIn 0.2s ease, costToastOut 0.3s ease 2.7s forwards;
12128
+ pointer-events: none;
12129
+ }
12130
+ @keyframes costToastIn { from { opacity: 0; transform: translateX(-50%) translateY(10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
12131
+ @keyframes costToastOut { from { opacity: 1; } to { opacity: 0; transform: translateX(-50%) translateY(-10px); } }
12132
+ body { padding-bottom: 44px; }
9938
12133
  </style>
9939
12134
 
9940
12135
  <!-- Bug button moved to sidebar footer -->
@@ -10016,6 +12211,13 @@ const WF_NODE_META = {
10016
12211
  rerank: { icon: 'M3 6h18M7 12h10M10 18h4', label: 'Rerank', color: '#CE93D8', category: 'retrieval' },
10017
12212
  search: { icon: 'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', label: 'Search', color: '#64B5F6', category: 'retrieval' },
10018
12213
  ingest: { icon: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3', label: 'Ingest', color: '#4DB6AC', category: 'management' },
12214
+ // Phase 1-3: New workflow nodes
12215
+ conditional: { icon: 'M12 3l9 9-9 9-9-9z', label: 'Conditional', color: '#90A4AE', category: 'control', shape: 'diamond' },
12216
+ loop: { icon: 'M17 1l4 4-4 4M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4M21 13v2a4 4 0 0 1-4 4H3', label: 'Loop', color: '#90A4AE', category: 'control' },
12217
+ template: { icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M16 13H8M16 17H8M10 9H8', label: 'Template', color: '#90A4AE', category: 'control' },
12218
+ chunk: { icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M9 15h6M9 11h6M9 19h4', label: 'Chunk', color: '#00D4AA', category: 'processing' },
12219
+ aggregate: { icon: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z', label: 'Aggregate', color: '#00D4AA', category: 'processing' },
12220
+ http: { icon: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z', label: 'HTTP Request', color: '#F5A623', category: 'integration' },
10019
12221
  };
10020
12222
 
10021
12223
  // Fallback icon (gear) for unknown workflow node types
@@ -10061,6 +12263,8 @@ async function wfLoadLibrary() {
10061
12263
  const wfData = await wfRes.json();
10062
12264
  const exData = await exRes.json();
10063
12265
  wfState.workflows = wfData.workflows || [];
12266
+ wfState.official = wfData.official || [];
12267
+ wfState.community = wfData.community || [];
10064
12268
  wfState.examples = exData.examples || [];
10065
12269
  wfRenderLibrary();
10066
12270
  } catch (err) {
@@ -10069,51 +12273,238 @@ async function wfLoadLibrary() {
10069
12273
  }
10070
12274
  }
10071
12275
 
12276
+ // ── Library Search ──
12277
+ let _wfLibSearchTimer = null;
12278
+ function wfOnLibrarySearch() {
12279
+ clearTimeout(_wfLibSearchTimer);
12280
+ _wfLibSearchTimer = setTimeout(() => wfRenderLibrary(), 150);
12281
+ const input = document.getElementById('wfLibrarySearch');
12282
+ const clear = document.getElementById('wfLibrarySearchClear');
12283
+ if (input && clear) clear.style.display = input.value ? 'block' : 'none';
12284
+ }
12285
+ function wfClearLibrarySearch() {
12286
+ const input = document.getElementById('wfLibrarySearch');
12287
+ if (input) { input.value = ''; input.focus(); }
12288
+ const clear = document.getElementById('wfLibrarySearchClear');
12289
+ if (clear) clear.style.display = 'none';
12290
+ wfRenderLibrary();
12291
+ }
12292
+
12293
+ function wfGetLibSectionStates() {
12294
+ const state = wfGetLayoutState();
12295
+ return (state.library && state.library.sections) || { builtin: true, examples: true, catalog: false, community: false };
12296
+ }
12297
+ function wfSaveLibSectionState(key, expanded) {
12298
+ const state = wfGetLayoutState();
12299
+ if (!state.library) state.library = { open: true };
12300
+ if (!state.library.sections) state.library.sections = { builtin: true, examples: true, catalog: false, community: false };
12301
+ state.library.sections[key] = expanded;
12302
+ try { localStorage.setItem('vai-workflow-layout', JSON.stringify(state)); } catch(e) {}
12303
+ }
12304
+ function wfToggleLibSection(key) {
12305
+ const states = wfGetLibSectionStates();
12306
+ const newVal = !states[key];
12307
+ wfSaveLibSectionState(key, newVal);
12308
+ // Toggle UI directly
12309
+ const header = document.querySelector(`.wf-lib-section-header[data-section="${key}"]`);
12310
+ const body = document.querySelector(`.wf-lib-section-body[data-section="${key}"]`);
12311
+ if (header && body) {
12312
+ header.classList.toggle('expanded', newVal);
12313
+ if (newVal) {
12314
+ body.style.maxHeight = body.scrollHeight + 'px';
12315
+ setTimeout(() => { body.style.maxHeight = 'none'; }, 200);
12316
+ } else {
12317
+ body.style.maxHeight = body.scrollHeight + 'px';
12318
+ requestAnimationFrame(() => { body.style.maxHeight = '0px'; });
12319
+ }
12320
+ }
12321
+ }
12322
+
10072
12323
  function wfRenderLibrary() {
10073
12324
  const list = document.getElementById('wfLibraryList');
10074
12325
  if (!list) return;
10075
- if (wfState.workflows.length === 0 && (!wfState.examples || wfState.examples.length === 0)) {
10076
- list.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;">No workflows found</div>';
10077
- return;
12326
+
12327
+ const searchInput = document.getElementById('wfLibrarySearch');
12328
+ const query = (searchInput ? searchInput.value : '').toLowerCase().trim();
12329
+ const sectionStates = wfGetLibSectionStates();
12330
+
12331
+ function matchesSearch(w) {
12332
+ if (!query) return true;
12333
+ const name = (w.name || '').toLowerCase();
12334
+ const desc = (w.description || '').toLowerCase();
12335
+ return name.includes(query) || desc.includes(query);
10078
12336
  }
10079
12337
 
10080
- // Built-in templates
10081
- let html = wfState.workflows.map(w => {
10082
- const displayName = w.name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
12338
+ function renderItem(w, prefix) {
12339
+ const displayName = prefix + w.name.replace(/^@vaicli\/vai-workflow-/, '').replace(/^vai-workflow-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
12340
+ const authorLine = w.author ? `<span style="color:var(--text-muted);font-size:10px;">by ${w.author}${w.version ? ' · v' + w.version : ''}</span>` : '';
12341
+ const tagLine = (w.tags || []).length ? `<div style="margin-top:2px;font-size:10px;color:var(--text-muted);">${w.tags.join(' · ')}</div>` : '';
10083
12342
  return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
10084
12343
  <div class="wf-library-item-name">${displayName}</div>
10085
12344
  <div class="wf-library-item-desc">${w.description || ''}</div>
12345
+ ${authorLine}${tagLine}
10086
12346
  </div>`;
10087
- }).join('');
12347
+ }
12348
+
12349
+ function renderSection(key, label, items, prefix) {
12350
+ const filtered = items.filter(matchesSearch);
12351
+ if (filtered.length === 0) return '';
12352
+ const expanded = sectionStates[key] !== false;
12353
+ const chevron = '&#9654;';
12354
+ const itemsHtml = filtered.map(w => renderItem(w, prefix)).join('');
12355
+ return `<div class="wf-library-section" data-section="${key}">
12356
+ <div class="wf-lib-section-header ${expanded ? 'expanded' : ''}" data-section="${key}" onclick="wfToggleLibSection('${key}')">
12357
+ <span class="wf-chevron">${chevron}</span>
12358
+ <span>${label}</span>
12359
+ <span class="wf-section-count">(${filtered.length})</span>
12360
+ </div>
12361
+ <div class="wf-lib-section-body" data-section="${key}" style="max-height:${expanded ? 'none' : '0px'};">
12362
+ ${itemsHtml}
12363
+ </div>
12364
+ </div>`;
12365
+ }
12366
+
12367
+ let html = '';
10088
12368
 
10089
- // Collapsible examples section
12369
+ // Built-in
12370
+ html += renderSection('builtin', 'Built-in', wfState.workflows, '');
12371
+
12372
+ // Examples
10090
12373
  const examples = wfState.examples || [];
10091
12374
  if (examples.length > 0) {
10092
- html += `<div class="wf-library-section">
10093
- <button class="wf-library-section-toggle" onclick="wfToggleExamples(this)">
10094
- <span class="arrow">&#9654;</span> Examples (${examples.length})
10095
- </button>
10096
- <div class="wf-examples-content" style="display:none;">`;
10097
-
10098
- const categories = ['Retrieval', 'RAG', 'Ingestion', 'Analysis', 'Other'];
10099
- for (const cat of categories) {
10100
- const items = examples.filter(e => e.category === cat);
10101
- if (items.length === 0) continue;
10102
- html += `<div class="wf-library-category">${cat}</div>`;
10103
- html += items.map(w => {
10104
- const displayName = w.name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
10105
- return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
10106
- <div class="wf-library-item-name">${displayName}</div>
10107
- <div class="wf-library-item-desc">${w.description || ''}</div>
10108
- </div>`;
10109
- }).join('');
10110
- }
10111
- html += '</div></div>';
12375
+ html += renderSection('examples', 'Examples', examples, '');
12376
+ }
12377
+
12378
+ // Official catalog
12379
+ const official = wfState.official || [];
12380
+ html += renderSection('catalog', 'Official Catalog (@vaicli)', official, '✓ ');
12381
+
12382
+ // Community
12383
+ const community = wfState.community || [];
12384
+ html += renderSection('community', 'Community', community, '🌐 ');
12385
+
12386
+ if (!html) {
12387
+ html = '<div style="padding:16px;color:var(--text-muted);font-size:12px;">No workflows found</div>';
10112
12388
  }
10113
12389
 
12390
+ // Install from npm button
12391
+ html += `<div style="padding:8px 12px;">
12392
+ <button onclick="wfShowInstallDialog()" style="background:none;border:1px dashed var(--border);color:var(--accent);cursor:pointer;padding:6px 12px;border-radius:6px;font-size:11px;width:100%;text-align:center;">+ Install from npm</button>
12393
+ </div>`;
12394
+
10114
12395
  list.innerHTML = html;
10115
12396
  }
10116
12397
 
12398
+ // ── Install Dialog ──
12399
+ function wfShowInstallDialog() {
12400
+ let modal = document.getElementById('wfInstallModal');
12401
+ if (!modal) {
12402
+ modal = document.createElement('div');
12403
+ modal.id = 'wfInstallModal';
12404
+ modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:9999;';
12405
+ modal.innerHTML = `
12406
+ <div style="background:var(--bg-panel);border:1px solid var(--border);border-radius:12px;padding:24px;width:480px;max-height:70vh;display:flex;flex-direction:column;">
12407
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
12408
+ <h3 style="margin:0;font-size:16px;">Install Workflow Package</h3>
12409
+ <button onclick="wfCloseInstallDialog()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:18px;">✕</button>
12410
+ </div>
12411
+ <div style="display:flex;gap:8px;margin-bottom:12px;">
12412
+ <input id="wfInstallSearch" type="text" placeholder="Search vai workflows on npm..." style="flex:1;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;" onkeydown="if(event.key==='Enter')wfSearchNpm()">
12413
+ <button onclick="wfSearchNpm()" style="padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">Search</button>
12414
+ </div>
12415
+ <div id="wfInstallResults" style="overflow-y:auto;flex:1;min-height:100px;"></div>
12416
+ </div>`;
12417
+ document.body.appendChild(modal);
12418
+ modal.addEventListener('click', (e) => { if (e.target === modal) wfCloseInstallDialog(); });
12419
+ }
12420
+ modal.style.display = 'flex';
12421
+ document.getElementById('wfInstallSearch').value = '';
12422
+ document.getElementById('wfInstallResults').innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;text-align:center;">Search for vai-workflow packages on npm</div>';
12423
+ document.getElementById('wfInstallSearch').focus();
12424
+ }
12425
+
12426
+ function wfCloseInstallDialog() {
12427
+ const modal = document.getElementById('wfInstallModal');
12428
+ if (modal) modal.style.display = 'none';
12429
+ }
12430
+
12431
+ async function wfSearchNpm() {
12432
+ const query = document.getElementById('wfInstallSearch').value.trim();
12433
+ const results = document.getElementById('wfInstallResults');
12434
+ if (!query) return;
12435
+ results.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;text-align:center;">Searching...</div>';
12436
+ try {
12437
+ const res = await fetch('/api/workflows/community/search?q=' + encodeURIComponent(query) + '&limit=10');
12438
+ const data = await res.json();
12439
+ if (!data.results || data.results.length === 0) {
12440
+ results.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;text-align:center;">No packages found</div>';
12441
+ return;
12442
+ }
12443
+ results.innerHTML = data.results.map(r => {
12444
+ const isOfficial = r.name.startsWith('@vaicli/');
12445
+ const badge = isOfficial ? '<span style="background:var(--accent);color:#fff;font-size:9px;padding:1px 5px;border-radius:3px;margin-left:6px;">OFFICIAL</span>' : '';
12446
+ const installed = [...(wfState.official||[]), ...(wfState.community||[])].some(w => w.name === r.name);
12447
+ const btn = installed
12448
+ ? '<button disabled style="padding:4px 10px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text-muted);font-size:11px;cursor:default;">Installed</button>'
12449
+ : `<button onclick="wfInstallPkg('${r.name.replace(/'/g,"\\'")}')" style="padding:4px 10px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;">Install</button>`;
12450
+ return `<div style="padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;justify-content:space-between;">
12451
+ <div style="flex:1;min-width:0;">
12452
+ <div style="font-size:13px;font-weight:500;">${r.name}${badge}</div>
12453
+ <div style="font-size:11px;color:var(--text-muted);margin-top:2px;">${r.description || ''}</div>
12454
+ <div style="font-size:10px;color:var(--text-muted);margin-top:2px;">v${r.version || '?'}</div>
12455
+ </div>
12456
+ <div style="margin-left:12px;flex-shrink:0;">${btn}</div>
12457
+ </div>`;
12458
+ }).join('');
12459
+ } catch (err) {
12460
+ results.innerHTML = `<div style="padding:16px;color:#f44;font-size:12px;text-align:center;">Search failed: ${err.message}</div>`;
12461
+ }
12462
+ }
12463
+
12464
+ async function wfInstallPkg(name) {
12465
+ const results = document.getElementById('wfInstallResults');
12466
+ // Find the clicked button and show installing state
12467
+ const btns = results.querySelectorAll('button');
12468
+ let clickedBtn = null;
12469
+ btns.forEach(b => {
12470
+ if (b.textContent === 'Install' && !clickedBtn) {
12471
+ // Find the button in the same row as the package name
12472
+ const row = b.closest('div[style*="border-bottom"]');
12473
+ if (row && row.textContent.includes(name)) {
12474
+ clickedBtn = b;
12475
+ b.textContent = 'Installing...';
12476
+ b.disabled = true;
12477
+ b.style.opacity = '0.7';
12478
+ }
12479
+ }
12480
+ });
12481
+
12482
+ try {
12483
+ const res = await fetch('/api/workflows/community/install', {
12484
+ method: 'POST',
12485
+ headers: { 'Content-Type': 'application/json' },
12486
+ body: JSON.stringify({ name })
12487
+ });
12488
+ const text = await res.text();
12489
+ let data;
12490
+ try { data = JSON.parse(text); } catch { data = { error: text || 'Empty response' }; }
12491
+ if (data.success) {
12492
+ if (clickedBtn) { clickedBtn.textContent = '✓ Installed'; clickedBtn.style.opacity = '1'; }
12493
+ await wfLoadLibrary();
12494
+ await wfSearchNpm();
12495
+ } else {
12496
+ const errMsg = data.error || 'Unknown error';
12497
+ if (clickedBtn) { clickedBtn.textContent = 'Install'; clickedBtn.disabled = false; clickedBtn.style.opacity = '1'; }
12498
+ console.error('Install failed:', errMsg);
12499
+ alert('Install failed: ' + errMsg);
12500
+ }
12501
+ } catch (err) {
12502
+ if (clickedBtn) { clickedBtn.textContent = 'Install'; clickedBtn.disabled = false; clickedBtn.style.opacity = '1'; }
12503
+ console.error('Install error:', err);
12504
+ alert('Install failed: ' + err.message);
12505
+ }
12506
+ }
12507
+
10117
12508
  function wfToggleExamples(btn) {
10118
12509
  const content = btn.nextElementSibling;
10119
12510
  const isOpen = content.style.display !== 'none';
@@ -10254,7 +12645,20 @@ async function wfRenderWorkflow(definition) {
10254
12645
  nodeHasDeps[stepId] = true;
10255
12646
  nodeHasDependents[depId] = true;
10256
12647
  }
10257
- });
12648
+ });
12649
+ }
12650
+
12651
+ // Build conditional branch maps for edge styling
12652
+ const elseBranchEdges = new Set(); // "fromId->toId" for else branches
12653
+ if (definition.steps) {
12654
+ for (const step of definition.steps) {
12655
+ if (step.tool === 'conditional' && step.inputs) {
12656
+ const elseSteps = step.inputs.else || [];
12657
+ for (const eId of elseSteps) {
12658
+ elseBranchEdges.add(`${step.id}->${eId}`);
12659
+ }
12660
+ }
12661
+ }
10258
12662
  }
10259
12663
 
10260
12664
  // Draw edges first (behind nodes)
@@ -10267,6 +12671,15 @@ async function wfRenderWorkflow(definition) {
10267
12671
  const depId = rawDepId.replace(/^!/, '');
10268
12672
  if (positions[depId] && positions[stepId]) {
10269
12673
  const edge = wfDrawEdge(depId, stepId, positions);
12674
+ // Mark else-branch edges with dashed style
12675
+ const edgeKey = `${depId}->${stepId}`;
12676
+ if (elseBranchEdges.has(edgeKey)) {
12677
+ edge.querySelector('.wf-edge')?.classList.add('wf-edge--else');
12678
+ }
12679
+ // Mark edges to skipped nodes
12680
+ if (wfState.executionState[stepId] === 'skipped') {
12681
+ edge.querySelector('.wf-edge')?.classList.add('wf-edge--skipped');
12682
+ }
10270
12683
  edgeGroup.appendChild(edge);
10271
12684
  }
10272
12685
  });
@@ -10334,14 +12747,27 @@ function wfDrawNode(step, x, y, state, hasDeps, hasDependents) {
10334
12747
  });
10335
12748
  }
10336
12749
 
10337
- // Background rect
10338
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
10339
- rect.setAttribute('width', WF_NODE_W);
10340
- rect.setAttribute('height', WF_NODE_H);
10341
- rect.setAttribute('fill', meta.color);
10342
- rect.setAttribute('stroke', meta.color);
10343
- rect.setAttribute('opacity', '0.85');
10344
- g.appendChild(rect);
12750
+ // Background shape: diamond for conditional, rounded rect for others
12751
+ if (meta.shape === 'diamond') {
12752
+ const diamond = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
12753
+ const cx = WF_NODE_W / 2, cy = WF_NODE_H / 2;
12754
+ const rx = WF_NODE_W / 2 + 8, ry = WF_NODE_H / 2 + 4;
12755
+ diamond.setAttribute('points', `${cx},${cy - ry} ${cx + rx},${cy} ${cx},${cy + ry} ${cx - rx},${cy}`);
12756
+ diamond.setAttribute('fill', meta.color);
12757
+ diamond.setAttribute('stroke', meta.color);
12758
+ diamond.setAttribute('opacity', '0.85');
12759
+ g.appendChild(diamond);
12760
+ } else {
12761
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
12762
+ rect.setAttribute('width', WF_NODE_W);
12763
+ rect.setAttribute('height', WF_NODE_H);
12764
+ rect.setAttribute('rx', '6');
12765
+ rect.setAttribute('ry', '6');
12766
+ rect.setAttribute('fill', meta.color);
12767
+ rect.setAttribute('stroke', meta.color);
12768
+ rect.setAttribute('opacity', '0.85');
12769
+ g.appendChild(rect);
12770
+ }
10345
12771
 
10346
12772
  // Input port (left side): show in builder mode always, otherwise only if has deps
10347
12773
  const showInPort = wfState.builderMode || hasDeps;
@@ -10516,22 +12942,200 @@ function wfDrawEdge(fromId, toId, positions) {
10516
12942
  return g;
10517
12943
  }
10518
12944
 
12945
+ // ── Panel Layout State ──
12946
+ let _wfLayoutSaveTimer = null;
12947
+ function wfGetLayoutState() {
12948
+ try {
12949
+ const s = localStorage.getItem('vai-workflow-layout');
12950
+ if (s) return JSON.parse(s);
12951
+ } catch(e) {}
12952
+ return { library: { open: true }, properties: { open: true } };
12953
+ }
12954
+ function wfSaveLayoutState() {
12955
+ clearTimeout(_wfLayoutSaveTimer);
12956
+ _wfLayoutSaveTimer = setTimeout(() => {
12957
+ const lib = document.getElementById('wfLibrary');
12958
+ const insp = document.getElementById('wfInspector');
12959
+ const existing = wfGetLayoutState();
12960
+ const state = {
12961
+ library: { open: lib ? !lib.classList.contains('collapsed') : true, sections: (existing.library && existing.library.sections) || { builtin: true, examples: true, catalog: false, community: false } },
12962
+ properties: { open: insp ? !insp.classList.contains('collapsed') : true, sections: (existing.properties && existing.properties.sections) || { inputs: true, steps: false, output: false } }
12963
+ };
12964
+ try { localStorage.setItem('vai-workflow-layout', JSON.stringify(state)); } catch(e) {}
12965
+ }, 300);
12966
+ }
12967
+ // ── Properties Accordion State ──
12968
+ function wfGetAccordionStates() {
12969
+ const state = wfGetLayoutState();
12970
+ return (state.properties && state.properties.sections) || { inputs: true, steps: false, output: false };
12971
+ }
12972
+ function wfSaveAccordionState(key, expanded) {
12973
+ const state = wfGetLayoutState();
12974
+ if (!state.properties) state.properties = { open: true };
12975
+ if (!state.properties.sections) state.properties.sections = { inputs: true, steps: false, output: false };
12976
+ state.properties.sections[key] = expanded;
12977
+ try { localStorage.setItem('vai-workflow-layout', JSON.stringify(state)); } catch(e) {}
12978
+ }
12979
+ function wfToggleAccordion(key, headerEl) {
12980
+ const body = document.querySelector(`.wf-accordion-body[data-acc="${key}"]`);
12981
+ if (!body) return;
12982
+ const expanded = headerEl.classList.contains('expanded');
12983
+ const newVal = !expanded;
12984
+ headerEl.classList.toggle('expanded', newVal);
12985
+ if (newVal) {
12986
+ body.style.maxHeight = body.scrollHeight + 'px';
12987
+ setTimeout(() => { body.style.maxHeight = 'none'; }, 200);
12988
+ } else {
12989
+ body.style.maxHeight = body.scrollHeight + 'px';
12990
+ requestAnimationFrame(() => { body.style.maxHeight = '0px'; });
12991
+ }
12992
+ wfSaveAccordionState(key, newVal);
12993
+ }
12994
+
12995
+ function wfSyncPanelUI() {
12996
+ const lib = document.getElementById('wfLibrary');
12997
+ const insp = document.getElementById('wfInspector');
12998
+ const libBtn = document.getElementById('wfToggleLibBtn');
12999
+ const propsBtn = document.getElementById('wfTogglePropsBtn');
13000
+ const leftHandle = document.getElementById('wfEdgeHandleLeft');
13001
+ const rightHandle = document.getElementById('wfEdgeHandleRight');
13002
+ const libCollapsed = lib && lib.classList.contains('collapsed');
13003
+ const inspCollapsed = insp && insp.classList.contains('collapsed');
13004
+ if (libBtn) libBtn.classList.toggle('active', !libCollapsed);
13005
+ if (propsBtn) propsBtn.classList.toggle('active', !inspCollapsed);
13006
+ if (leftHandle) leftHandle.classList.toggle('visible', !!libCollapsed);
13007
+ if (rightHandle) rightHandle.classList.toggle('visible', !!inspCollapsed);
13008
+ }
13009
+ function wfRestoreLayoutState() {
13010
+ const state = wfGetLayoutState();
13011
+ const lib = document.getElementById('wfLibrary');
13012
+ const insp = document.getElementById('wfInspector');
13013
+ if (lib) lib.classList.toggle('collapsed', !state.library.open);
13014
+ if (insp) insp.classList.toggle('collapsed', !state.properties.open);
13015
+ wfSyncPanelUI();
13016
+ }
13017
+
13018
+ // ── Library Toggle ──
13019
+ function wfToggleLibrary() {
13020
+ const panel = document.getElementById('wfLibrary');
13021
+ if (!panel) return;
13022
+ panel.classList.toggle('collapsed');
13023
+ wfSyncPanelUI();
13024
+ wfSaveLayoutState();
13025
+ }
13026
+
10519
13027
  // ── Inspector Toggle ──
10520
13028
  function wfToggleInspector() {
10521
13029
  const panel = document.getElementById('wfInspector');
10522
- const btn = document.getElementById('wfInspectorToggle');
10523
13030
  if (!panel) return;
10524
13031
  panel.classList.toggle('collapsed');
10525
- if (btn) btn.innerHTML = panel.classList.contains('collapsed') ? '&lsaquo;' : '&rsaquo;';
13032
+ wfSyncPanelUI();
13033
+ wfSaveLayoutState();
10526
13034
  }
10527
13035
 
10528
13036
  function wfOpenInspector() {
10529
13037
  const panel = document.getElementById('wfInspector');
10530
- const btn = document.getElementById('wfInspectorToggle');
10531
13038
  if (!panel || !panel.classList.contains('collapsed')) return;
10532
13039
  panel.classList.remove('collapsed');
10533
- if (btn) btn.innerHTML = '&rsaquo;';
13040
+ wfSyncPanelUI();
13041
+ wfSaveLayoutState();
13042
+ }
13043
+
13044
+ // ── Sidebar Collapse ──
13045
+ function toggleSidebarCollapse() {
13046
+ const sidebar = document.querySelector('.sidebar');
13047
+ if (!sidebar) return;
13048
+ sidebar.classList.toggle('collapsed');
13049
+ // Persist
13050
+ const state = wfGetLayoutState();
13051
+ if (!state.sidebar) state.sidebar = {};
13052
+ state.sidebar.collapsed = sidebar.classList.contains('collapsed');
13053
+ try { localStorage.setItem('vai-workflow-layout', JSON.stringify(state)); } catch(e) {}
13054
+ }
13055
+ function restoreSidebarState() {
13056
+ const state = wfGetLayoutState();
13057
+ if (state.sidebar && state.sidebar.collapsed) {
13058
+ const sidebar = document.querySelector('.sidebar');
13059
+ if (sidebar) sidebar.classList.add('collapsed');
13060
+ }
10534
13061
  }
13062
+ restoreSidebarState();
13063
+
13064
+ // ── Keyboard Shortcuts (Workflows tab only) ──
13065
+ let _wfLastOpenedPanel = null; // 'library' or 'properties'
13066
+ (function() {
13067
+ const origToggleLib = window.wfToggleLibrary;
13068
+ window.wfToggleLibrary = function() {
13069
+ origToggleLib();
13070
+ const lib = document.getElementById('wfLibrary');
13071
+ if (lib && !lib.classList.contains('collapsed')) _wfLastOpenedPanel = 'library';
13072
+ };
13073
+ const origToggleInsp = window.wfToggleInspector;
13074
+ window.wfToggleInspector = function() {
13075
+ origToggleInsp();
13076
+ const insp = document.getElementById('wfInspector');
13077
+ if (insp && !insp.classList.contains('collapsed')) _wfLastOpenedPanel = 'properties';
13078
+ };
13079
+ })();
13080
+
13081
+ document.addEventListener('keydown', function(e) {
13082
+ // Only active when Workflows tab is visible
13083
+ const wfTab = document.getElementById('tab-workflows');
13084
+ if (!wfTab || wfTab.style.display === 'none' || !wfTab.classList.contains('active')) {
13085
+ // Check if tab-btn-workflows is active instead
13086
+ const wfBtn = document.getElementById('tab-btn-workflows');
13087
+ if (!wfBtn || !wfBtn.classList.contains('active')) return;
13088
+ }
13089
+
13090
+ const mod = e.metaKey || e.ctrlKey;
13091
+ if (!mod && e.key !== 'Escape') return;
13092
+
13093
+ // Cmd/Ctrl + Shift + B → Toggle sidebar
13094
+ if (mod && e.shiftKey && (e.key === 'B' || e.key === 'b')) {
13095
+ e.preventDefault();
13096
+ toggleSidebarCollapse();
13097
+ return;
13098
+ }
13099
+ // Cmd/Ctrl + B → Toggle Library
13100
+ if (mod && !e.shiftKey && (e.key === 'B' || e.key === 'b')) {
13101
+ e.preventDefault();
13102
+ wfToggleLibrary();
13103
+ return;
13104
+ }
13105
+ // Cmd/Ctrl + I → Toggle Properties
13106
+ if (mod && (e.key === 'I' || e.key === 'i')) {
13107
+ e.preventDefault();
13108
+ wfToggleInspector();
13109
+ return;
13110
+ }
13111
+ // Cmd/Ctrl + \ → Collapse both panels
13112
+ if (mod && e.key === '\\') {
13113
+ e.preventDefault();
13114
+ const lib = document.getElementById('wfLibrary');
13115
+ const insp = document.getElementById('wfInspector');
13116
+ if (lib && !lib.classList.contains('collapsed')) { lib.classList.add('collapsed'); }
13117
+ if (insp && !insp.classList.contains('collapsed')) { insp.classList.add('collapsed'); }
13118
+ wfSyncPanelUI();
13119
+ wfSaveLayoutState();
13120
+ return;
13121
+ }
13122
+ // Escape → Close most recently opened panel
13123
+ if (e.key === 'Escape' && !mod) {
13124
+ if (_wfLastOpenedPanel === 'library') {
13125
+ const lib = document.getElementById('wfLibrary');
13126
+ if (lib && !lib.classList.contains('collapsed')) { wfToggleLibrary(); _wfLastOpenedPanel = null; return; }
13127
+ }
13128
+ if (_wfLastOpenedPanel === 'properties') {
13129
+ const insp = document.getElementById('wfInspector');
13130
+ if (insp && !insp.classList.contains('collapsed')) { wfToggleInspector(); _wfLastOpenedPanel = null; return; }
13131
+ }
13132
+ // Fallback: close properties then library
13133
+ const insp = document.getElementById('wfInspector');
13134
+ if (insp && !insp.classList.contains('collapsed')) { wfToggleInspector(); return; }
13135
+ const lib = document.getElementById('wfLibrary');
13136
+ if (lib && !lib.classList.contains('collapsed')) { wfToggleLibrary(); return; }
13137
+ }
13138
+ });
10535
13139
 
10536
13140
  // ── Node Selection ──
10537
13141
  function wfSelectNode(stepId) {
@@ -10593,56 +13197,83 @@ function wfUpdateInspector() {
10593
13197
  <div style="font-size:10px;color:var(--text-muted);">Add steps from the Palette tab, then drag between ports to connect them.</div>
10594
13198
  </div>`;
10595
13199
  } else {
10596
- // Read-only: Description
13200
+ // Read-only: Description (above accordion)
10597
13201
  if (def.description) {
10598
13202
  html += `<div class="wf-inspector-section">
10599
- <div class="wf-inspector-section-title">Description</div>
10600
13203
  <div style="font-size:12px;color:var(--text);line-height:1.4;">${escapeHtml(def.description)}</div>
10601
13204
  </div>`;
10602
13205
  }
10603
13206
 
10604
- // Read-only: Inputs
10605
- if (def.inputs && Object.keys(def.inputs).length > 0) {
10606
- html += '<div class="wf-inspector-section"><div class="wf-inspector-section-title">Inputs</div>';
13207
+ // Get accordion states from localStorage
13208
+ const accStates = wfGetAccordionStates();
13209
+ const hasDone = !!wfState.executionResults._done;
13210
+
13211
+ // ── INPUTS Accordion ──
13212
+ const inputKeys = def.inputs ? Object.keys(def.inputs) : [];
13213
+ const inputsExpanded = accStates.inputs !== false;
13214
+ html += `<div class="wf-accordion-header ${inputsExpanded ? 'expanded' : ''}" onclick="wfToggleAccordion('inputs', this)">
13215
+ <span class="wf-acc-left"><span class="wf-chevron">&#9654;</span> INPUTS</span>
13216
+ <span class="wf-acc-summary">${inputKeys.length} field${inputKeys.length !== 1 ? 's' : ''}</span>
13217
+ </div>
13218
+ <div class="wf-accordion-body" data-acc="inputs" style="max-height:${inputsExpanded ? 'none' : '0px'};">
13219
+ <div class="wf-accordion-body-inner">`;
13220
+ if (inputKeys.length > 0) {
10607
13221
  for (const [key, spec] of Object.entries(def.inputs)) {
10608
13222
  const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
10609
- const defVal = spec.default !== undefined ? ` (default: ${spec.default})` : '';
10610
- html += `<div style="margin-bottom:8px;">
13223
+ html += `<div style="margin-bottom:12px;">
10611
13224
  <div style="font-size:12px;font-weight:600;color:var(--text);">${escapeHtml(key)}${req}</div>
10612
- <div style="font-size:11px;color:var(--text-muted);">${escapeHtml(spec.description || spec.type || '')}${defVal}</div>
10613
- <input class="wf-inspector-input" id="wf-input-${key}" placeholder="${escapeHtml(key)}" value="${spec.default !== undefined ? spec.default : ''}">
13225
+ <div style="font-size:10px;color:var(--text-muted);margin-bottom:4px;">${escapeHtml(spec.description || spec.type || '')}</div>
13226
+ <input class="wf-inspector-input" id="wf-input-${key}" placeholder="${escapeHtml(key)}" value="${spec.default !== undefined ? escapeHtml(String(spec.default)) : ''}">
10614
13227
  </div>`;
10615
13228
  }
10616
- html += '</div>';
13229
+ } else {
13230
+ html += '<div style="font-size:12px;color:var(--text-muted);">No inputs defined</div>';
10617
13231
  }
10618
-
10619
- // Steps summary
10620
- html += `<div class="wf-inspector-section">
10621
- <div class="wf-inspector-section-title">Steps</div>
10622
- <div style="font-size:12px;color:var(--text);">${def.steps.length} step${def.steps.length !== 1 ? 's' : ''}${wfState.layers ? ' in ' + wfState.layers.length + ' layer' + (wfState.layers.length !== 1 ? 's' : '') : ''}</div>
10623
- </div>`;
10624
-
10625
- // Output mapping
10626
- if (def.output) {
10627
- html += `<div class="wf-inspector-section">
10628
- <div class="wf-inspector-section-title">Output</div>
10629
- <div class="wf-inspector-code">${escapeHtml(JSON.stringify(def.output, null, 2))}</div>
13232
+ html += '</div></div>';
13233
+
13234
+ // ── STEPS Accordion ──
13235
+ const stepCount = def.steps.length;
13236
+ const layerCount = wfState.layers ? wfState.layers.length : 0;
13237
+ const stepsSummary = `${stepCount} step${stepCount !== 1 ? 's' : ''}${layerCount ? ' · ' + layerCount + ' layer' + (layerCount !== 1 ? 's' : '') : ''}`;
13238
+ const stepsExpanded = accStates.steps === true;
13239
+ html += `<div class="wf-accordion-header ${stepsExpanded ? 'expanded' : ''}" onclick="wfToggleAccordion('steps', this)">
13240
+ <span class="wf-acc-left"><span class="wf-chevron">&#9654;</span> STEPS</span>
13241
+ <span class="wf-acc-summary">${stepsSummary}</span>
13242
+ </div>
13243
+ <div class="wf-accordion-body" data-acc="steps" style="max-height:${stepsExpanded ? 'none' : '0px'};">
13244
+ <div class="wf-accordion-body-inner">`;
13245
+ for (const step of def.steps) {
13246
+ const meta = WF_NODE_META[step.tool] || { color: '#666' };
13247
+ const result = wfState.executionResults[step.id];
13248
+ const timeStr = result && result.timeMs != null ? result.timeMs + 'ms' : '—';
13249
+ html += `<div class="wf-step-row">
13250
+ <span class="wf-step-dot" style="background:${meta.color}"></span>
13251
+ <span class="wf-step-name">${escapeHtml(step.name || step.id)}</span>
13252
+ <span class="wf-step-time">${timeStr}</span>
10630
13253
  </div>`;
10631
13254
  }
10632
- }
13255
+ html += '</div></div>';
10633
13256
 
10634
- // Execution result (shown in both modes)
10635
- if (wfState.executionResults._done) {
10636
- const r = wfState.executionResults._done;
10637
- const doneJson = JSON.stringify(r.output, null, 2);
10638
- html += `<div class="wf-inspector-section">
10639
- <div class="wf-inspector-section-title">Result</div>
10640
- <div class="wf-inspector-result success">
10641
- <div style="font-weight:600;margin-bottom:4px;">Completed in ${r.totalTimeMs}ms</div>
10642
- <div class="wf-inspector-code" style="max-height:150px;overflow:auto;">${escapeHtml(doneJson)}</div>
10643
- </div>
10644
- <button class="wf-output-expand-btn" data-expand-step="_done">&#x2922; Expand</button>
10645
- </div>`;
13257
+ // ── OUTPUT Accordion ──
13258
+ const outputExpanded = hasDone || accStates.output === true;
13259
+ html += `<div class="wf-accordion-header ${outputExpanded ? 'expanded' : ''}" onclick="wfToggleAccordion('output', this)">
13260
+ <span class="wf-acc-left"><span class="wf-chevron">&#9654;</span> OUTPUT</span>
13261
+ <span class="wf-acc-summary"></span>
13262
+ </div>
13263
+ <div class="wf-accordion-body" data-acc="output" style="max-height:${outputExpanded ? 'none' : '0px'};">
13264
+ <div class="wf-accordion-body-inner">`;
13265
+ if (hasDone) {
13266
+ const r = wfState.executionResults._done;
13267
+ const doneJson = JSON.stringify(r.output, null, 2);
13268
+ html += `<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;">Completed in ${r.totalTimeMs}ms</div>
13269
+ <div class="wf-inspector-code" style="max-height:200px;overflow:auto;color:#40E0FF;">${escapeHtml(doneJson)}</div>
13270
+ <button class="wf-output-expand-btn" data-expand-step="_done">&#x2922; Expand</button>`;
13271
+ } else if (def.output) {
13272
+ html += `<div class="wf-inspector-code" style="color:#40E0FF;">${escapeHtml(JSON.stringify(def.output, null, 2))}</div>`;
13273
+ } else {
13274
+ html += '<div style="font-size:12px;color:var(--text-muted);">Run the workflow to see output</div>';
13275
+ }
13276
+ html += '</div></div>';
10646
13277
  }
10647
13278
 
10648
13279
  body.innerHTML = html;
@@ -10662,10 +13293,13 @@ function wfUpdateInspector() {
10662
13293
 
10663
13294
  let html = '';
10664
13295
 
10665
- // Tool badge (always shown)
13296
+ // Tool badge (always shown) with help button
10666
13297
  html += `<div class="wf-inspector-section">
10667
13298
  <div class="wf-inspector-section-title">Tool</div>
10668
- <span class="wf-tool-badge" style="background:${meta.color}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="${meta.icon || WF_FALLBACK_ICON}"/></svg>${meta.label}</span>
13299
+ <div style="display:flex;align-items:center;">
13300
+ <span class="wf-tool-badge" style="background:${meta.color}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="${meta.icon || WF_FALLBACK_ICON}"/></svg>${meta.label}</span>
13301
+ <button class="wf-inspector-help-btn" onclick="wfOpenHelpModal('${step.tool}')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> Help</button>
13302
+ </div>
10669
13303
  </div>`;
10670
13304
 
10671
13305
  if (wfState.builderMode) {
@@ -11323,9 +13957,105 @@ function wfCopyOutput() {
11323
13957
  });
11324
13958
  }
11325
13959
 
11326
- // Close modal on Escape
13960
+ // ── Node Help Modal ──
13961
+ let _nodeHelpCache = null;
13962
+ async function getNodeHelp() {
13963
+ if (_nodeHelpCache) return _nodeHelpCache;
13964
+ try {
13965
+ const res = await fetch('/api/workflows/node-help');
13966
+ const data = await res.json();
13967
+ _nodeHelpCache = data.nodeHelp || {};
13968
+ } catch { _nodeHelpCache = {}; }
13969
+ return _nodeHelpCache;
13970
+ }
13971
+
13972
+ async function wfOpenHelpModal(tool) {
13973
+ const helpData = await getNodeHelp();
13974
+ const help = helpData[tool];
13975
+ const meta = WF_NODE_META[tool] || { icon: WF_FALLBACK_ICON, label: tool, color: '#666', category: 'unknown' };
13976
+ const categoryLabel = WF_CATEGORY_LABELS[meta.category] || meta.category || 'Unknown';
13977
+
13978
+ const headerEl = document.getElementById('wfHelpModalHeader');
13979
+ const bodyEl = document.getElementById('wfHelpModalBody');
13980
+ const backdrop = document.getElementById('wfHelpModalBackdrop');
13981
+ if (!headerEl || !bodyEl || !backdrop) return;
13982
+
13983
+ // Build header
13984
+ const isEmoji = !meta.icon.includes(' ');
13985
+ const iconHtml = isEmoji
13986
+ ? `<div class="wf-help-icon" style="background:${meta.color}22;font-size:18px;">${meta.icon}</div>`
13987
+ : `<div class="wf-help-icon" style="background:${meta.color}22;color:${meta.color};"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="${meta.icon}"/></svg></div>`;
13988
+
13989
+ headerEl.innerHTML = iconHtml
13990
+ + `<span class="wf-help-title">${escapeHtml(meta.label)}</span>`
13991
+ + `<span class="wf-help-category-badge">${escapeHtml(categoryLabel)}</span>`
13992
+ + `<button class="wf-help-close-btn" onclick="wfCloseHelpModal()" title="Close"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>`;
13993
+
13994
+ // Build body
13995
+ if (!help) {
13996
+ bodyEl.innerHTML = `<div class="wf-help-section"><p>No help content available for this node yet.</p></div>`;
13997
+ backdrop.style.display = 'flex';
13998
+ return;
13999
+ }
14000
+
14001
+ let html = '';
14002
+
14003
+ // Overview
14004
+ if (help.description) {
14005
+ html += `<div class="wf-help-section"><div class="wf-help-section-title">Overview</div><p>${escapeHtml(help.description)}</p></div>`;
14006
+ }
14007
+
14008
+ // How it Works
14009
+ if (help.howItWorks) {
14010
+ html += `<div class="wf-help-section"><div class="wf-help-section-title">How it Works</div><p>${escapeHtml(help.howItWorks)}</p></div>`;
14011
+ }
14012
+
14013
+ // Inputs
14014
+ if (help.inputs && help.inputs.length > 0) {
14015
+ html += `<div class="wf-help-section"><div class="wf-help-section-title">Inputs</div><table class="wf-help-io-table"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody>`;
14016
+ for (const inp of help.inputs) {
14017
+ const reqBadge = inp.required ? '<span class="wf-help-required">required</span>' : '';
14018
+ html += `<tr><td><span class="wf-help-key">${escapeHtml(inp.key)}</span>${reqBadge}</td><td><span class="wf-help-type">${escapeHtml(inp.type)}</span></td><td>${escapeHtml(inp.desc)}</td></tr>`;
14019
+ }
14020
+ html += '</tbody></table></div>';
14021
+ }
14022
+
14023
+ // Outputs
14024
+ if (help.outputs && help.outputs.length > 0) {
14025
+ html += `<div class="wf-help-section"><div class="wf-help-section-title">Output</div><table class="wf-help-io-table"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody>`;
14026
+ for (const out of help.outputs) {
14027
+ html += `<tr><td><span class="wf-help-key">${escapeHtml(out.key)}</span></td><td><span class="wf-help-type">${escapeHtml(out.type)}</span></td><td>${escapeHtml(out.desc)}</td></tr>`;
14028
+ }
14029
+ html += '</tbody></table></div>';
14030
+ }
14031
+
14032
+ // Tips
14033
+ if (help.tips && help.tips.length > 0) {
14034
+ html += `<div class="wf-help-section"><div class="wf-help-section-title">Tips</div>`;
14035
+ for (const tip of help.tips) {
14036
+ html += `<div class="wf-help-tip"><span class="wf-help-tip-dot"></span><span>${escapeHtml(tip)}</span></div>`;
14037
+ }
14038
+ html += '</div>';
14039
+ }
14040
+
14041
+ bodyEl.innerHTML = html;
14042
+ backdrop.style.display = 'flex';
14043
+ }
14044
+
14045
+ function wfCloseHelpModal() {
14046
+ const backdrop = document.getElementById('wfHelpModalBackdrop');
14047
+ if (backdrop) backdrop.style.display = 'none';
14048
+ }
14049
+
14050
+ // Close modals on Escape
11327
14051
  document.addEventListener('keydown', (e) => {
11328
14052
  if (e.key === 'Escape') {
14053
+ const helpBackdrop = document.getElementById('wfHelpModalBackdrop');
14054
+ if (helpBackdrop && helpBackdrop.style.display !== 'none') {
14055
+ wfCloseHelpModal();
14056
+ e.preventDefault();
14057
+ return;
14058
+ }
11329
14059
  const backdrop = document.getElementById('wfOutputModalBackdrop');
11330
14060
  if (backdrop && backdrop.style.display !== 'none') {
11331
14061
  wfCloseOutputModal();
@@ -11454,10 +14184,16 @@ const WF_INPUT_DEFS = {
11454
14184
  filter: [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'condition', type: 'text', required: true, placeholder: 'item.score > 0.5' }],
11455
14185
  transform: [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'expression', type: 'text', required: true, placeholder: 'item.text' }],
11456
14186
  generate: [{ key: 'prompt', type: 'textarea', required: true, placeholder: 'Generate a summary of...' }, { key: 'context', type: 'text', required: false, placeholder: '{{ step.output }}' }],
14187
+ conditional: [{ key: 'condition', type: 'text', required: true, placeholder: '{{ step.output.results.length > 0 }}' }, { key: 'then', type: 'json', required: true, placeholder: '["step_a"]' }, { key: 'else', type: 'json', required: false, placeholder: '["step_b"]' }],
14188
+ loop: [{ key: 'items', type: 'text', required: true, placeholder: '{{ step.output.results }}' }, { key: 'as', type: 'text', required: true, placeholder: 'item' }, { key: 'step', type: 'json', required: true, placeholder: '{"tool":"template","inputs":{"text":"{{ item }}"}}' }, { key: 'maxIterations', type: 'number', required: false, placeholder: '100' }],
14189
+ template: [{ key: 'text', type: 'textarea', required: true, placeholder: 'Compose text with {{ step.output }} references' }],
14190
+ chunk: [{ key: 'text', type: 'textarea', required: true, placeholder: '{{ step.output.text }}' }, { key: 'strategy', type: 'select', required: false, options: ['fixed','sentence','paragraph','recursive','markdown'] }, { key: 'size', type: 'number', required: false, placeholder: '512' }, { key: 'overlap', type: 'number', required: false, placeholder: '50' }, { key: 'source', type: 'text', required: false, placeholder: 'document.md' }],
14191
+ aggregate: [{ key: 'pipeline', type: 'json', required: true, placeholder: '[{"$group":{"_id":"$field","count":{"$sum":1}}}]' }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }],
14192
+ http: [{ key: 'url', type: 'text', required: true, placeholder: 'https://api.example.com/data' }, { key: 'method', type: 'select', required: false, options: ['GET','POST','PUT','PATCH','DELETE'] }, { key: 'headers', type: 'json', required: false, placeholder: '{"Authorization":"Bearer ..."}' }, { key: 'body', type: 'json', required: false, placeholder: '{}' }, { key: 'timeout', type: 'number', required: false, placeholder: '30000' }],
11457
14193
  };
11458
14194
 
11459
- const WF_CATEGORY_ORDER = ['retrieval', 'embedding', 'management', 'utility', 'control', 'generation'];
11460
- const WF_CATEGORY_LABELS = { retrieval: 'Retrieval', embedding: 'Embedding', management: 'Management', utility: 'Utility', control: 'Control Flow', generation: 'Generation' };
14195
+ const WF_CATEGORY_ORDER = ['retrieval', 'embedding', 'processing', 'control', 'generation', 'integration', 'management', 'utility'];
14196
+ const WF_CATEGORY_LABELS = { retrieval: 'Retrieval', embedding: 'Embedding', management: 'Management', utility: 'Utility', control: 'Control Flow', generation: 'Generation', processing: 'Processing', integration: 'Integration' };
11461
14197
 
11462
14198
  // ── Builder: Library/Palette tab toggle ──
11463
14199
  function wfSwitchLibTab(tab) {
@@ -11488,6 +14224,7 @@ function wfRenderPalette() {
11488
14224
  html += `<div class="wf-palette-item" draggable="true" ondragstart="event.dataTransfer.setData('text/plain','${item.tool}')" onclick="wfAddNodeFromPalette('${item.tool}')">
11489
14225
  <span class="wf-palette-icon" style="color:${item.color}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="${item.icon || WF_FALLBACK_ICON}"/></svg></span>
11490
14226
  <span class="wf-palette-label">${item.label}</span>
14227
+ <button class="wf-help-trigger" onclick="event.stopPropagation();wfOpenHelpModal('${item.tool}')" title="Help: ${item.label}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
11491
14228
  </div>`;
11492
14229
  }
11493
14230
  html += '</div>';
@@ -11935,7 +14672,442 @@ document.addEventListener('keydown', (e) => {
11935
14672
  });
11936
14673
 
11937
14674
  // ── Init ──
14675
+ // ── Workflow Store ──
14676
+ const WF_STORE_TOOL_COLORS = {
14677
+ query:'#00D4AA',search:'#40E0FF',rerank:'#0EA5E9',embed:'#8B5CF6',similarity:'#A78BFA',
14678
+ ingest:'#10B981',collections:'#F59E0B',models:'#EF4444',explain:'#EC4899',estimate:'#F97316',
14679
+ merge:'#6366F1',filter:'#14B8A6',transform:'#D946EF',generate:'#F472B6'
14680
+ };
14681
+ const WF_STORE_CATEGORIES = [
14682
+ { id:'all', label:'All', icon:'◈' }, { id:'retrieval', label:'Retrieval', icon:'⊕' },
14683
+ { id:'analysis', label:'Analysis', icon:'◉' }, { id:'domain-specific', label:'Domain', icon:'◆' },
14684
+ { id:'ingestion', label:'Ingestion', icon:'⊞' }, { id:'utility', label:'Utility', icon:'⊡' },
14685
+ { id:'integration', label:'Integration', icon:'⊗' },
14686
+ ];
14687
+ // Hardcoded fallback data matching the spec's 20 workflows
14688
+ const WF_STORE_FALLBACK = [
14689
+ { name:'model-shootout', packageName:'@vaicli/vai-workflow-model-shootout', description:'Compare voyage-4-large, voyage-4, and voyage-4-lite side-by-side on your data.', category:'utility', tags:['benchmarking','model-comparison','cost','shared-space'], tools:['query','estimate','similarity','generate'], steps:7, tier:'official', downloads:3241, featured:true, installed:false, gradient:'linear-gradient(135deg, #0D9488, #06B6D4)' },
14690
+ { name:'asymmetric-search', packageName:'@vaicli/vai-workflow-asymmetric-search', description:'The canonical shared embedding space demo. Embed with voyage-4-large, query with voyage-4-lite. ~83% cost reduction.', category:'retrieval', tags:['asymmetric','shared-space','cost-savings'], tools:['embed','search','rerank'], steps:3, tier:'official', downloads:4812, featured:true, installed:false, gradient:'linear-gradient(135deg, #00D4AA, #40E0FF)' },
14691
+ { name:'cost-optimizer', packageName:'@vaicli/vai-workflow-cost-optimizer', description:'Quantify the exact cost savings of asymmetric retrieval on your collection.', category:'utility', tags:['cost','optimization','asymmetric'], tools:['query','estimate','similarity','generate'], steps:6, tier:'official', downloads:2918, featured:true, installed:false, gradient:'linear-gradient(135deg, #F59E0B, #EF4444)' },
14692
+ { name:'question-decomposition', packageName:'@vaicli/vai-workflow-question-decomposition', description:'Break complex questions into focused sub-queries, search each in parallel, merge and rerank.', category:'retrieval', tags:['decomposition','fan-out','synthesis'], tools:['generate','query','merge','rerank'], steps:6, tier:'official', downloads:1876, installed:false, gradient:'linear-gradient(135deg, #8B5CF6, #EC4899)' },
14693
+ { name:'contract-clause-finder', packageName:'@vaicli/vai-workflow-contract-clause-finder', description:'Search legal documents for specific clause types using voyage-law-2.', category:'domain-specific', tags:['legal','contracts','voyage-law-2'], tools:['query','rerank','generate'], steps:3, tier:'official', downloads:1543, installed:false, gradient:'linear-gradient(135deg, #1E40AF, #7C3AED)' },
14694
+ { name:'knowledge-base-bootstrap', packageName:'@vaicli/vai-workflow-knowledge-base-bootstrap', description:'End-to-end onboarding: ingest documents, verify, test a query, and generate a status report.', category:'integration', tags:['onboarding','bootstrap','end-to-end'], tools:['ingest','collections','query','generate'], steps:4, tier:'official', downloads:3654, installed:false, gradient:'linear-gradient(135deg, #059669, #10B981)' },
14695
+ { name:'embedding-drift-detector', packageName:'@vaicli/vai-workflow-embedding-drift-detector', description:'Monitor embedding quality over time. Re-embed sample documents and compare against stored vectors.', category:'analysis', tags:['monitoring','drift','operations'], tools:['search','embed','similarity','generate'], steps:5, tier:'official', downloads:987, installed:false, gradient:'linear-gradient(135deg, #DC2626, #F97316)' },
14696
+ { name:'multilingual-search', packageName:'@vaicli/vai-workflow-multilingual-search', description:'Translate your query into multiple languages, search each in parallel, merge and rerank.', category:'retrieval', tags:['multilingual','translation','cross-lingual'], tools:['generate','query','merge','rerank'], steps:5, tier:'official', downloads:1234, installed:false, gradient:'linear-gradient(135deg, #0EA5E9, #6366F1)' },
14697
+ { name:'financial-risk-scanner', packageName:'@vaicli/vai-workflow-financial-risk-scanner', description:'Scan financial documents for risk signals using voyage-finance-2.', category:'domain-specific', tags:['finance','risk','voyage-finance-2'], tools:['query','rerank','filter','generate'], steps:4, tier:'official', downloads:1102, installed:false, gradient:'linear-gradient(135deg, #B45309, #D97706)' },
14698
+ { name:'doc-freshness', packageName:'@vaicli/vai-workflow-doc-freshness', description:'Audit your knowledge base for stale content. Gathers metadata from 5 tools in parallel.', category:'utility', tags:['freshness','maintenance','monitoring'], tools:['collections','models','search','explain','estimate','generate'], steps:6, tier:'official', downloads:1456, installed:false, gradient:'linear-gradient(135deg, #4338CA, #7C3AED)' },
14699
+ { name:'incremental-sync', packageName:'@vaicli/vai-workflow-incremental-sync', description:'Smart ingestion with deduplication. Checks similarity before storing.', category:'ingestion', tags:['dedup','sync','conditional'], tools:['search','similarity','ingest','filter','generate'], steps:5, tier:'official', downloads:2103, installed:false, gradient:'linear-gradient(135deg, #15803D, #4ADE80)' },
14700
+ { name:'rag-ab-test', packageName:'@vaicli/vai-workflow-rag-ab-test', description:'Run the same query through two RAG configurations side-by-side.', category:'integration', tags:['ab-test','comparison','evaluation'], tools:['query','generate'], steps:5, tier:'official', downloads:1678, installed:false, gradient:'linear-gradient(135deg, #BE185D, #F472B6)' },
14701
+ { name:'hybrid-precision-search', packageName:'@vaicli/vai-workflow-hybrid-precision-search', description:'Three retrieval strategies in parallel — lite, large, and filtered — merged and reranked.', category:'retrieval', tags:['hybrid','parallel','precision'], tools:['query','search','merge','rerank'], steps:5, tier:'official', downloads:1890, installed:false, gradient:'linear-gradient(135deg, #0891B2, #22D3EE)' },
14702
+ { name:'code-migration-helper', packageName:'@vaicli/vai-workflow-code-migration-helper', description:'Find similar code patterns across your codebase using voyage-code-3.', category:'domain-specific', tags:['code','migration','voyage-code-3'], tools:['query','rerank','generate'], steps:3, tier:'official', downloads:932, installed:false, gradient:'linear-gradient(135deg, #475569, #94A3B8)' },
14703
+ { name:'meeting-action-items', packageName:'@vaicli/vai-workflow-meeting-action-items', description:'Ingest meeting notes and extract structured action items with owners, deadlines, and context.', category:'domain-specific', tags:['meetings','action-items','productivity'], tools:['ingest','query','generate'], steps:3, tier:'official', downloads:2567, installed:false, gradient:'linear-gradient(135deg, #7C2D12, #EA580C)' },
14704
+ { name:'collection-overlap-audit', packageName:'@vaicli/vai-workflow-collection-overlap-audit', description:'Detect duplicate content across two collections.', category:'analysis', tags:['dedup','overlap','audit'], tools:['search','similarity','merge','filter','generate'], steps:5, tier:'official', downloads:678, installed:false, gradient:'linear-gradient(135deg, #6D28D9, #A78BFA)' },
14705
+ { name:'query-quality-scorer', packageName:'@vaicli/vai-workflow-query-quality-scorer', description:'Evaluate retrieval quality without ground truth labels.', category:'analysis', tags:['evaluation','quality','scoring'], tools:['query','similarity','transform','generate'], steps:4, tier:'official', downloads:823, installed:false, gradient:'linear-gradient(135deg, #9333EA, #C084FC)' },
14706
+ { name:'clinical-protocol-match', packageName:'@vaicli/vai-workflow-clinical-protocol-match', description:'Match clinical presentations to treatment protocols.', category:'domain-specific', tags:['clinical','healthcare','protocols'], tools:['query','rerank','generate'], steps:3, tier:'official', downloads:712, installed:false, gradient:'linear-gradient(135deg, #0F766E, #2DD4BF)' },
14707
+ { name:'batch-quality-gate', packageName:'@vaicli/vai-workflow-batch-quality-gate', description:'Quality-filtered ingestion. Only ingest content that meets your similarity threshold.', category:'ingestion', tags:['quality','gate','filtering'], tools:['search','embed','similarity','ingest','filter','generate'], steps:5, tier:'official', downloads:543, installed:false, gradient:'linear-gradient(135deg, #166534, #86EFAC)' },
14708
+ { name:'index-health-check', packageName:'@vaicli/vai-workflow-index-health-check', description:'Diagnostic pipeline for your vector index.', category:'utility', tags:['diagnostics','health','index'], tools:['collections','search','query','estimate','generate'], steps:5, tier:'official', downloads:1345, installed:false, gradient:'linear-gradient(135deg, #1D4ED8, #60A5FA)' },
14709
+ ];
14710
+
14711
+ // Inject branding into fallback data (matches DEFAULT_BRANDING in catalog API)
14712
+ const _WF_FALLBACK_BRANDING = {
14713
+ 'model-shootout': { icon:'trophy', color:'#0D9488', iconPath:'M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0 0 12 0V2z' },
14714
+ 'asymmetric-search': { icon:'split', color:'#00D4AA', iconPath:'M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3M21 3l-7.828 7.828A4 4 0 0 0 12 13.7V22' },
14715
+ 'cost-optimizer': { icon:'dollar-sign', color:'#F59E0B', iconPath:'M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6' },
14716
+ 'question-decomposition': { icon:'sparkle', color:'#8B5CF6', iconPath:'M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z' },
14717
+ 'contract-clause-finder': { icon:'file-search', color:'#1E40AF', iconPath:'M14 2v4a2 2 0 0 0 2 2h4M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7zM9.5 12.5a2.5 2.5 0 1 0 5 0 2.5 2.5 0 1 0-5 0M13.3 14.3 15 16' },
14718
+ 'knowledge-base-bootstrap': { icon:'database', color:'#059669', iconPath:'M21 5c0 1.1-3.134 3-9 3S3 6.1 3 5M21 5c0-1.1-3.134-3-9-3S3 3.9 3 5M21 5v14c0 1.1-3.134 3-9 3s-9-1.9-9-3V5M21 12c0 1.1-3.134 3-9 3s-9-1.9-9-3' },
14719
+ 'embedding-drift-detector': { icon:'activity', color:'#DC2626', iconPath:'M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36-3.18-19.64A2 2 0 0 0 10.12 1h-.24a2 2 0 0 0-1.94 1.55L5.18 12H2' },
14720
+ 'multilingual-search': { icon:'globe', color:'#0EA5E9', iconPath:'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z' },
14721
+ 'financial-risk-scanner': { icon:'shield-alert', color:'#B45309', iconPath:'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 .5-.87l7-4a1 1 0 0 1 1 0l7 4A1 1 0 0 1 20 6zM12 8v4M12 16h.01' },
14722
+ 'doc-freshness': { icon:'timer', color:'#4338CA', iconPath:'M10 2h4M12 14l3-3M12 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16z' },
14723
+ 'incremental-sync': { icon:'refresh-cw', color:'#15803D', iconPath:'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16' },
14724
+ 'rag-ab-test': { icon:'flask-conical', color:'#BE185D', iconPath:'M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2M8.5 2h7M7 16h10' },
14725
+ 'hybrid-precision-search': { icon:'target', color:'#0891B2', iconPath:'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M12 12m-6 0a6 6 0 1 0 12 0 6 6 0 1 0-12 0M12 12m-2 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0' },
14726
+ 'code-migration-helper': { icon:'code', color:'#475569', iconPath:'M16 18l6-6-6-6M8 6l-6 6 6 6' },
14727
+ 'meeting-action-items': { icon:'clipboard-list',color:'#7C2D12', iconPath:'M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2M9 2h6a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1zM12 11h4M12 16h4M8 11h.01M8 16h.01' },
14728
+ 'collection-overlap-audit': { icon:'layers', color:'#6D28D9', iconPath:'M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.84zM2 12l8.58 3.91a2 2 0 0 0 1.66 0L22 12M2 17l8.58 3.91a2 2 0 0 0 1.66 0L22 17' },
14729
+ 'query-quality-scorer': { icon:'microscope', color:'#9333EA', iconPath:'M6 18h8M3 22h18M14 22a7 7 0 1 0 0-14h-1M9 14h2M9 12a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2z' },
14730
+ 'clinical-protocol-match': { icon:'heart-pulse', color:'#0F766E', iconPath:'M19.5 12.572l-7.5 7.428-7.5-7.428A5 5 0 0 1 7.5 5c1.8 0 3.3.9 4.5 2.7C13.2 5.9 14.7 5 16.5 5a5 5 0 0 1 3 9.572zM12 6l-1 4h4l-1 4' },
14731
+ 'batch-quality-gate': { icon:'check-circle', color:'#166534', iconPath:'M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4 12 14.01l-3-3' },
14732
+ 'index-health-check': { icon:'bar-chart-3', color:'#1D4ED8', iconPath:'M12 20V10M18 20V4M6 20v-4' },
14733
+ };
14734
+ WF_STORE_FALLBACK.forEach(wf => {
14735
+ wf.branding = _WF_FALLBACK_BRANDING[wf.name] || { icon:'zap', color:'#64748B', iconPath:'M13 2 3 14h9l-1 8 10-12h-9l1-8z' };
14736
+ wf.verified = true;
14737
+ wf.security = [];
14738
+ wf.rating = null;
14739
+ });
14740
+
14741
+ function getCapabilityBadges(tools) {
14742
+ if (!Array.isArray(tools)) return [];
14743
+ const badges = [];
14744
+ if (tools.includes('http')) badges.push({ emoji: '🌐', label: 'NETWORK', cls: 'wf-store-badge-cap-network' });
14745
+ if (tools.includes('ingest') || tools.includes('aggregate')) badges.push({ emoji: '💾', label: 'WRITE_DB', cls: 'wf-store-badge-cap-writedb' });
14746
+ if (tools.includes('generate')) badges.push({ emoji: '🤖', label: 'LLM', cls: 'wf-store-badge-cap-llm' });
14747
+ if (tools.includes('loop') || tools.includes('forEach')) badges.push({ emoji: '🔄', label: 'LOOP', cls: 'wf-store-badge-cap-loop' });
14748
+ if (tools.some(t => ['query','search','collections','aggregate'].includes(t))) badges.push({ emoji: '📊', label: 'READ_DB', cls: 'wf-store-badge-cap-readdb' });
14749
+ return badges;
14750
+ }
14751
+
14752
+ let _wfStoreUserRatings = {};
14753
+
14754
+ let _wfStoreCatalog = null;
14755
+ let _wfStoreCat = 'all';
14756
+
14757
+ function wfStoreOpen() {
14758
+ const overlay = document.getElementById('wfStoreOverlay');
14759
+ if (!overlay) return;
14760
+ overlay.classList.add('open');
14761
+ document.getElementById('wfStoreBtn').classList.add('active');
14762
+ if (!_wfStoreCatalog) wfStoreFetchCatalog();
14763
+ }
14764
+
14765
+ function wfStoreClose() {
14766
+ const overlay = document.getElementById('wfStoreOverlay');
14767
+ if (overlay) overlay.classList.remove('open');
14768
+ document.getElementById('wfStoreBtn').classList.remove('active');
14769
+ // Close detail modal if open
14770
+ const detail = document.getElementById('wfStoreDetail');
14771
+ if (detail) detail.remove();
14772
+ }
14773
+
14774
+ // Store icons map (populated from API response or used from inline fallback)
14775
+ let _wfStoreIcons = {};
14776
+
14777
+ // Render a branding icon as inline SVG
14778
+ function wfBrandingIcon(wf, size = 16) {
14779
+ const b = wf.branding || {};
14780
+ const iconPath = b.iconPath || _wfStoreIcons[b.icon] || '';
14781
+ if (!iconPath) return '';
14782
+ return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="${iconPath}"/></svg>`;
14783
+ }
14784
+
14785
+ async function wfStoreFetchCatalog() {
14786
+ try {
14787
+ const res = await fetch('/api/workflows/catalog');
14788
+ const data = await res.json();
14789
+ if (data.icons) _wfStoreIcons = data.icons;
14790
+ if (data.workflows && data.workflows.length > 0) {
14791
+ _wfStoreCatalog = data.workflows;
14792
+ } else {
14793
+ _wfStoreCatalog = WF_STORE_FALLBACK;
14794
+ }
14795
+ } catch {
14796
+ _wfStoreCatalog = WF_STORE_FALLBACK;
14797
+ }
14798
+ wfStoreRender();
14799
+ }
14800
+
14801
+ function wfStoreRender() {
14802
+ const container = document.getElementById('wfStoreContent');
14803
+ if (!container || !_wfStoreCatalog) return;
14804
+ const search = (document.getElementById('wfStoreSearch')?.value || '').toLowerCase();
14805
+ const sort = document.getElementById('wfStoreSort')?.value || 'downloads';
14806
+
14807
+ const filtered = _wfStoreCatalog.filter(wf => {
14808
+ const mc = _wfStoreCat === 'all' || wf.category === _wfStoreCat;
14809
+ const ms = !search || wf.name.includes(search) || (wf.description||'').toLowerCase().includes(search)
14810
+ || (wf.tags||[]).some(t => t.includes(search)) || (wf.tools||[]).some(t => t.includes(search));
14811
+ return mc && ms;
14812
+ }).sort((a, b) => sort === 'downloads' ? (b.downloads||0) - (a.downloads||0) : sort === 'name' ? a.name.localeCompare(b.name) : (b.steps||0) - (a.steps||0));
14813
+
14814
+ const featured = _wfStoreCatalog.filter(w => w.featured);
14815
+ const showFeat = _wfStoreCat === 'all' && !search;
14816
+
14817
+ let html = '';
14818
+ // Category chips
14819
+ html += '<div class="wf-store-chips">';
14820
+ WF_STORE_CATEGORIES.forEach(c => {
14821
+ html += `<button class="wf-store-chip ${_wfStoreCat===c.id?'active':''}" onclick="_wfStoreCat='${c.id}';wfStoreRender()">${c.icon} ${c.label}</button>`;
14822
+ });
14823
+ html += '</div>';
14824
+
14825
+ // Featured section
14826
+ if (showFeat && featured.length > 0) {
14827
+ html += '<div class="wf-store-section-label">Featured</div>';
14828
+ html += '<div class="wf-store-featured-grid">';
14829
+ featured.forEach(wf => {
14830
+ const featAuthor = wf.author && wf.author.name && wf.author.name !== 'unknown' ? wf.author : null;
14831
+ const featAvatarHtml = featAuthor ? (featAuthor.avatar
14832
+ ? `<div class="wf-store-featured-avatar"><img src="${featAuthor.avatar}" onerror="this.parentNode.textContent='${featAuthor.name.charAt(0).toUpperCase()}'"></div>`
14833
+ : `<div class="wf-store-featured-avatar">${featAuthor.name.charAt(0).toUpperCase()}</div>`) : '';
14834
+ const featAuthorHtml = featAuthor ? `<div class="wf-store-featured-author">${featAvatarHtml} by ${featAuthor.name}</div>` : '';
14835
+ const featIconHtml = wfBrandingIcon(wf, 20) ? `<div class="wf-store-featured-icon">${wfBrandingIcon(wf, 20)}</div>` : '';
14836
+ html += `<div class="wf-store-featured-card" style="background:${wf.gradient}" onclick="wfStoreShowDetail('${wf.name}')">
14837
+ ${featIconHtml}
14838
+ <div class="wf-store-featured-dl">↓ ${(wf.downloads||0).toLocaleString()}</div>
14839
+ <div class="wf-store-featured-content"><h3>${wf.name}</h3><p>${wf.description||''}</p>${featAuthorHtml}</div>
14840
+ </div>`;
14841
+ });
14842
+ html += '</div>';
14843
+ }
14844
+
14845
+ // Grid label
14846
+ const label = _wfStoreCat === 'all' ? 'All Workflows' : (WF_STORE_CATEGORIES.find(c=>c.id===_wfStoreCat)?.label || _wfStoreCat);
14847
+ html += `<div class="wf-store-section-label">${label} <span class="wf-store-count">${filtered.length}</span></div>`;
14848
+
14849
+ if (filtered.length === 0) {
14850
+ html += '<div class="wf-store-empty"><p>No workflows match your search</p></div>';
14851
+ } else {
14852
+ html += '<div class="wf-store-grid">';
14853
+ filtered.forEach(wf => {
14854
+ const dots = (wf.tools||[]).slice(0,5).map(t =>
14855
+ `<div class="wf-store-card-dot" style="background:${WF_STORE_TOOL_COLORS[t]||'#666'}"></div>`
14856
+ ).join('') + ((wf.tools||[]).length > 5 ? `<span style="font-family:var(--mono);font-size:9px;color:var(--text-muted);margin-left:2px">+${wf.tools.length-5}</span>` : '');
14857
+ const badges = (wf.verified ? '<span class="wf-store-badge-verified">✓ VERIFIED</span>' : '') +
14858
+ (wf.installed ? '<span class="wf-store-badge-installed">installed</span>' : '') +
14859
+ `<span class="wf-store-badge-official">${wf.tier==='official' ? '✓ OFFICIAL' : 'COMMUNITY'}</span>`;
14860
+ const capBadges = getCapabilityBadges(wf.tools);
14861
+ const capBadgesHtml = capBadges.length > 0
14862
+ ? `<div class="wf-store-cap-badges">${capBadges.map(b => `<span class="wf-store-badge-capability ${b.cls}">${b.emoji} ${b.label}</span>`).join('')}</div>`
14863
+ : '';
14864
+ const cardIconColor = (wf.branding && wf.branding.color) || '#64748B';
14865
+ const cardIconSvg = wfBrandingIcon(wf, 16);
14866
+ const cardIconHtml = cardIconSvg
14867
+ ? `<div class="wf-store-card-icon" style="background:${cardIconColor}18;color:${cardIconColor}">${cardIconSvg}</div>`
14868
+ : '';
14869
+ html += `<div class="wf-store-card" onclick="wfStoreShowDetail('${wf.name}')">
14870
+ <div class="wf-store-card-bar" style="background:${wf.gradient}"></div>
14871
+ <div class="wf-store-card-top">
14872
+ <div class="wf-store-card-name-wrap">${cardIconHtml}<span class="wf-store-card-name">${wf.name}</span></div>
14873
+ <div class="wf-store-card-badges">${badges}</div>
14874
+ </div>
14875
+ <div class="wf-store-card-desc">${wf.description||''}</div>
14876
+ ${wf.author && wf.author.name && wf.author.name !== 'unknown' ? `<div class="wf-store-card-author">by ${wf.author.name}</div>` : ''}
14877
+ ${capBadgesHtml}
14878
+ <div class="wf-store-card-meta">
14879
+ <span class="wf-store-card-cat">${wf.category||'utility'}</span>
14880
+ <div class="wf-store-card-dots">${dots}</div>
14881
+ <span class="wf-store-card-complexity">${wf.steps||0}s·${wf.toolCount||(Array.isArray(wf.tools)?wf.tools.length:0)}t</span>
14882
+ <span class="wf-store-card-downloads">↓ ${(wf.downloads||0).toLocaleString()}</span>
14883
+ </div>
14884
+ </div>`;
14885
+ });
14886
+ html += '</div>';
14887
+ }
14888
+
14889
+ container.innerHTML = html;
14890
+ }
14891
+
14892
+ function wfStoreShowDetail(name) {
14893
+ const wf = (_wfStoreCatalog || []).find(w => w.name === name);
14894
+ if (!wf) return;
14895
+ // Remove existing detail
14896
+ const existing = document.getElementById('wfStoreDetail');
14897
+ if (existing) existing.remove();
14898
+
14899
+ const ic = `vai workflow install ${wf.packageName}`;
14900
+ const rc = `vai workflow run ${wf.packageName}`;
14901
+
14902
+ // Escape for use in HTML attributes (single-quoted onclick handlers)
14903
+ const icEsc = ic.replace(/'/g, "\\'");
14904
+ const rcEsc = rc.replace(/'/g, "\\'");
14905
+
14906
+ const toolsHtml = (wf.tools||[]).map(t => {
14907
+ const c = WF_STORE_TOOL_COLORS[t] || '#666';
14908
+ return `<span class="wf-store-detail-tool" style="color:${c};border-color:${c}44;background:${c}11">${t}</span>`;
14909
+ }).join('');
14910
+
14911
+ const tagsHtml = `<span class="wf-store-detail-tag">${wf.category||'utility'}</span>` +
14912
+ (wf.tags||[]).map(t => `<span class="wf-store-detail-tag">${t}</span>`).join('');
14913
+
14914
+ const installBtn = wf.installed
14915
+ ? '<button class="wf-store-detail-btn wf-store-detail-btn-installed">✓ Installed</button>'
14916
+ : `<button class="wf-store-detail-btn wf-store-detail-btn-primary" onclick="wfStoreInstall('${wf.packageName}',this)">Install</button>`;
14917
+
14918
+ const modal = document.createElement('div');
14919
+ modal.id = 'wfStoreDetail';
14920
+ modal.className = 'wf-store-detail-bg';
14921
+ modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
14922
+ modal.innerHTML = `<div class="wf-store-detail-panel" onclick="event.stopPropagation()">
14923
+ <div class="wf-store-detail-scroll">
14924
+ <div class="wf-store-detail-hero" style="background:${wf.gradient}">
14925
+ <button class="wf-store-detail-close" onclick="document.getElementById('wfStoreDetail').remove()">×</button>
14926
+ <div class="wf-store-detail-hero-inner">
14927
+ ${wfBrandingIcon(wf, 26) ? `<div class="wf-store-detail-hero-icon">${wfBrandingIcon(wf, 26)}</div>` : ''}
14928
+ <div>
14929
+ <div class="wf-store-detail-name">${wf.name}</div>
14930
+ <div class="wf-store-detail-pkg">${wf.packageName}</div>
14931
+ </div>
14932
+ </div>
14933
+ </div>
14934
+ <div class="wf-store-detail-body">
14935
+ ${(() => {
14936
+ const a = wf.author && wf.author.name && wf.author.name !== 'unknown' ? wf.author : null;
14937
+ if (!a) return '';
14938
+ const avatarInner = a.avatar
14939
+ ? `<img src="${a.avatar}" onerror="this.parentNode.textContent='${a.name.charAt(0).toUpperCase()}'">`
14940
+ : a.name.charAt(0).toUpperCase();
14941
+ const nameHtml = a.url
14942
+ ? `<a href="${a.url}" target="_blank" rel="noopener">${a.name}</a>`
14943
+ : a.name;
14944
+ return `<div class="wf-store-detail-author"><div class="wf-store-detail-avatar">${avatarInner}</div><div class="wf-store-detail-author-name">${nameHtml}</div></div>`;
14945
+ })()}
14946
+ <p class="wf-store-detail-desc">${wf.description||''}</p>
14947
+ ${wf.assets && wf.assets.screenshots && wf.assets.screenshots.length > 0 ? `
14948
+ <div class="wf-store-detail-section">
14949
+ <div class="wf-store-detail-label">Screenshots</div>
14950
+ <div class="wf-store-detail-screenshots">${wf.assets.screenshots.map(s => `<img src="${s}" alt="Screenshot">`).join('')}</div>
14951
+ </div>` : ''}
14952
+ <div class="wf-store-detail-section">
14953
+ <div class="wf-store-detail-label">Stats</div>
14954
+ <div class="wf-store-detail-stats">
14955
+ <div class="wf-store-detail-stat"><div class="wf-store-detail-stat-val">${wf.steps||0}</div><div class="wf-store-detail-stat-lbl">Steps</div></div>
14956
+ <div class="wf-store-detail-stat"><div class="wf-store-detail-stat-val">${wf.toolCount||(Array.isArray(wf.tools)?wf.tools.length:0)}</div><div class="wf-store-detail-stat-lbl">Tools</div></div>
14957
+ <div class="wf-store-detail-stat"><div class="wf-store-detail-stat-val">${(wf.downloads||0).toLocaleString()}</div><div class="wf-store-detail-stat-lbl">Downloads</div></div>
14958
+ </div>
14959
+ </div>
14960
+ <div class="wf-store-detail-section">
14961
+ <div class="wf-store-detail-label">Tools</div>
14962
+ <div class="wf-store-detail-tools">${toolsHtml}</div>
14963
+ </div>
14964
+ <div class="wf-store-detail-section">
14965
+ <div class="wf-store-detail-label">Tags</div>
14966
+ <div class="wf-store-detail-tags">${tagsHtml}</div>
14967
+ </div>
14968
+ ${wf.inputs && wf.inputs.length > 0 ? `
14969
+ <div class="wf-store-detail-section">
14970
+ <div class="wf-store-detail-label">Input Parameters</div>
14971
+ <table class="wf-store-detail-inputs-table">
14972
+ <thead><tr><th>Name</th><th>Type</th><th>Required / Default</th></tr></thead>
14973
+ <tbody>${wf.inputs.map(inp => `<tr><td>${inp.name}</td><td>${inp.type}</td><td>${inp.required ? 'required' : inp.default !== undefined ? 'default: ' + inp.default : 'optional'}</td></tr>`).join('')}</tbody>
14974
+ </table>
14975
+ </div>` : ''}
14976
+ <div class="wf-store-detail-install">
14977
+ <div class="wf-store-detail-label">Install</div>
14978
+ <div class="wf-store-detail-cmd" onclick="navigator.clipboard.writeText('${icEsc}');this.querySelector('.wf-store-detail-cmd-hint').textContent='✓ copied';setTimeout(()=>this.querySelector('.wf-store-detail-cmd-hint').textContent='copy',2000)">
14979
+ <span>$ ${ic}</span><span class="wf-store-detail-cmd-hint">copy</span>
14980
+ </div>
14981
+ <div class="wf-store-detail-label" style="margin-top:12px">Run</div>
14982
+ <div class="wf-store-detail-cmd" style="color:var(--text-dim)" onclick="navigator.clipboard.writeText('${rcEsc}');this.querySelector('.wf-store-detail-cmd-hint').textContent='✓ copied';setTimeout(()=>this.querySelector('.wf-store-detail-cmd-hint').textContent='copy',2000)">
14983
+ <span>$ ${rc}</span><span class="wf-store-detail-cmd-hint">copy</span>
14984
+ </div>
14985
+ </div>
14986
+ ${(() => {
14987
+ const capBadges = getCapabilityBadges(wf.tools);
14988
+ if (capBadges.length === 0) return '';
14989
+ return `<div class="wf-store-detail-section">
14990
+ <div class="wf-store-detail-label">Capabilities</div>
14991
+ <div style="display:flex;gap:5px;flex-wrap:wrap">${capBadges.map(b => `<span class="wf-store-badge-capability ${b.cls}">${b.emoji} ${b.label}</span>`).join('')}</div>
14992
+ </div>`;
14993
+ })()}
14994
+ ${(() => {
14995
+ const sec = wf.security || [];
14996
+ if (sec.length === 0) return `<div class="wf-store-detail-section"><div class="wf-store-detail-label">Security</div><div class="wf-store-detail-security-ok">✓ No security issues found</div></div>`;
14997
+ return `<div class="wf-store-detail-section"><div class="wf-store-detail-label">Security</div>${sec.map(f => `<div class="wf-store-detail-security-item"><span class="wf-store-detail-security-sev ${f.severity||'low'}">${f.severity||'low'}</span><span>${f.message||f.rule||'Issue found'}</span></div>`).join('')}</div>`;
14998
+ })()}
14999
+ ${wf.verified ? `<div class="wf-store-detail-section"><div class="wf-store-detail-label">Verified</div><div class="wf-store-detail-security-ok">✓ This workflow has passed all validation checks (L1–L4): schema, security audit, quality audit, and capability analysis.</div></div>` : ''}
15000
+ <div class="wf-store-detail-section">
15001
+ <div class="wf-store-detail-label">Rating</div>
15002
+ <div style="display:flex;align-items:center;gap:12px">
15003
+ <span style="font-size:12px;color:var(--text-dim)">${wf.rating != null ? wf.rating.toFixed(1) + ' / 5' : 'Not yet rated'}</span>
15004
+ </div>
15005
+ <div style="margin-top:8px;font-size:11px;color:var(--text-muted)">Rate this workflow:</div>
15006
+ <div class="wf-store-detail-stars" id="wfStoreRateStars">
15007
+ ${[1,2,3,4,5].map(i => `<span onclick="_wfStoreUserRatings['${wf.name}']=${i};document.querySelectorAll('#wfStoreRateStars span').forEach((s,j)=>s.className=j<${i}?'filled':'')" class="${(_wfStoreUserRatings[wf.name]||0)>=i?'filled':''}">★</span>`).join('')}
15008
+ </div>
15009
+ </div>
15010
+ <div class="wf-store-detail-section">
15011
+ <button class="wf-store-detail-btn wf-store-detail-btn-secondary" style="flex:none;width:auto;padding:6px 14px;font-size:11px" onclick="document.getElementById('wfStoreReportForm').style.display=document.getElementById('wfStoreReportForm').style.display==='none'?'block':'none'">⚑ Report</button>
15012
+ <div id="wfStoreReportForm" class="wf-store-report-form" style="display:none">
15013
+ <select id="wfStoreReportReason"><option value="">Select reason...</option><option value="malicious">Malicious</option><option value="broken">Broken</option><option value="low quality">Low quality</option><option value="spam">Spam</option></select>
15014
+ <textarea id="wfStoreReportComment" placeholder="Optional comment..."></textarea>
15015
+ <button class="wf-store-report-submit" onclick="document.getElementById('wfStoreReportForm').style.display='none';const t=document.createElement('div');t.style.cssText='position:fixed;bottom:20px;right:20px;background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:12px 20px;color:var(--text);font-size:13px;z-index:9999;animation:wfStoreDetailFadeIn .2s';t.textContent='✓ Report submitted';document.body.appendChild(t);setTimeout(()=>t.remove(),3000)">Submit Report</button>
15016
+ </div>
15017
+ </div>
15018
+ <div class="wf-store-detail-actions">
15019
+ ${installBtn}
15020
+ <button class="wf-store-detail-btn wf-store-detail-btn-secondary" onclick="wfStoreRun('${wf.name}')">Run</button>
15021
+ <button class="wf-store-detail-btn wf-store-detail-btn-secondary" onclick="wfStoreCanvas('${wf.name}')">Canvas</button>
15022
+ </div>
15023
+ </div>
15024
+ </div>
15025
+ </div>`;
15026
+ document.body.appendChild(modal);
15027
+ }
15028
+
15029
+ async function wfStoreInstall(packageName, btn) {
15030
+ if (btn) { btn.textContent = 'Installing...'; btn.disabled = true; }
15031
+ try {
15032
+ const res = await fetch('/api/workflows/community/install', {
15033
+ method: 'POST',
15034
+ headers: { 'Content-Type': 'application/json' },
15035
+ body: JSON.stringify({ name: packageName })
15036
+ });
15037
+ const text = await res.text();
15038
+ let data;
15039
+ try { data = JSON.parse(text); } catch { data = { error: text || 'Empty response (status ' + res.status + ')' }; }
15040
+ console.log('[Store Install]', packageName, data);
15041
+ if (data.success) {
15042
+ if (btn) { btn.textContent = '✓ Installed'; btn.className = 'wf-store-detail-btn wf-store-detail-btn-installed'; btn.disabled = true; }
15043
+ // Update catalog state
15044
+ const wf = (_wfStoreCatalog||[]).find(w => w.packageName === packageName);
15045
+ if (wf) wf.installed = true;
15046
+ wfStoreRender();
15047
+ await wfLoadLibrary(); // Refresh library panel
15048
+ } else {
15049
+ const errMsg = data.error || 'Unknown error';
15050
+ console.error('[Store Install Error]', errMsg);
15051
+ if (btn) { btn.textContent = 'Failed'; btn.title = errMsg; setTimeout(() => { btn.textContent = 'Install'; btn.disabled = false; btn.title = ''; }, 4000); }
15052
+ }
15053
+ } catch (err) {
15054
+ console.error('[Store Install Exception]', err);
15055
+ if (btn) { btn.textContent = 'Failed'; btn.title = err.message; setTimeout(() => { btn.textContent = 'Install'; btn.disabled = false; btn.title = ''; }, 4000); }
15056
+ }
15057
+ }
15058
+
15059
+ async function wfStoreRun(name) {
15060
+ const detail = document.getElementById('wfStoreDetail');
15061
+ if (detail) detail.remove();
15062
+ wfStoreClose();
15063
+
15064
+ // Find the workflow in catalog
15065
+ const wf = (_wfStoreCatalog||[]).find(w => w.name === name);
15066
+ if (!wf) {
15067
+ alert('Workflow not found: ' + name);
15068
+ return;
15069
+ }
15070
+
15071
+ // Check if installed
15072
+ if (!wf.installed) {
15073
+ const doInstall = confirm(`"${wf.name}" is not installed. Install it now?`);
15074
+ if (doInstall) {
15075
+ await wfStoreInstall(wf.packageName, null);
15076
+ // Refresh catalog to update installed status
15077
+ await wfStoreFetchCatalog();
15078
+ // Check again
15079
+ const updated = (_wfStoreCatalog||[]).find(w => w.name === name);
15080
+ if (!updated || !updated.installed) {
15081
+ alert('Installation may have failed. Please try again.');
15082
+ return;
15083
+ }
15084
+ } else {
15085
+ return;
15086
+ }
15087
+ }
15088
+
15089
+ // Switch to workflows tab and load the workflow
15090
+ switchTab('workflows');
15091
+ // Use the full package name for resolution
15092
+ wfSelectWorkflow(wf.packageName);
15093
+ }
15094
+
15095
+ function wfStoreCanvas(name) {
15096
+ wfStoreRun(name); // Same behavior — load the workflow into canvas
15097
+ }
15098
+
15099
+ // Escape key handler for store
15100
+ document.addEventListener('keydown', (e) => {
15101
+ if (e.key === 'Escape') {
15102
+ const detail = document.getElementById('wfStoreDetail');
15103
+ if (detail) { detail.remove(); return; }
15104
+ const overlay = document.getElementById('wfStoreOverlay');
15105
+ if (overlay && overlay.classList.contains('open')) { wfStoreClose(); }
15106
+ }
15107
+ });
15108
+
11938
15109
  function wfInit() {
15110
+ wfRestoreLayoutState();
11939
15111
  wfLoadLibrary();
11940
15112
  wfInitPan();
11941
15113
  }
@@ -12067,5 +15239,241 @@ let wfInitialized = false;
12067
15239
  })();
12068
15240
  </script>
12069
15241
 
15242
+ <!-- Cost Dashboard -->
15243
+ <div id="costDetailPanel">
15244
+ <div class="cost-panel-inner">
15245
+ <div class="cost-panel-header">
15246
+ <h3>💰 Cost Dashboard</h3>
15247
+ <button id="costResetBtn" onclick="CostTracker.reset()">Reset Session</button>
15248
+ </div>
15249
+ <table id="costOpsTable">
15250
+ <thead><tr><th>Time</th><th>Operation</th><th>Model</th><th>Tokens</th><th>Cost</th><th>If v4-large</th><th>If OpenAI</th></tr></thead>
15251
+ <tbody></tbody>
15252
+ </table>
15253
+ <div class="cost-projection" id="costProjection"></div>
15254
+ </div>
15255
+ </div>
15256
+ <div id="costStatusBar">
15257
+ <span>💰 Session: <span class="cost-val" id="costTotal">$0.000000</span></span>
15258
+ <span class="cost-sep">│</span>
15259
+ <span><span id="costTokens">0</span> tokens</span>
15260
+ <span class="cost-sep">│</span>
15261
+ <span><span id="costOps">0</span> ops</span>
15262
+ <span class="cost-sep">│</span>
15263
+ <span>LLM: <span class="cost-val" id="costLLM">$0.000000</span> (<span id="costLLMIn">0</span>in/<span id="costLLMOut">0</span>out)</span>
15264
+ <span class="cost-sep">│</span>
15265
+ <span>If symmetric: <span id="costSymmetric">$0.000000</span> <span id="costSymDelta"></span></span>
15266
+ <span class="cost-sep">│</span>
15267
+ <span>If OpenAI: <span id="costCompetitor">$0.000000</span> <span id="costCompDelta"></span></span>
15268
+ <button id="costDetailsToggle" onclick="CostTracker.togglePanel()">Details ▾</button>
15269
+ </div>
15270
+
15271
+ <script>
15272
+ (function() {
15273
+ const PRICING = {
15274
+ 'voyage-4-large': 0.12, 'voyage-4': 0.06, 'voyage-4-lite': 0.02,
15275
+ 'voyage-code-3': 0.18, 'voyage-finance-2': 0.12, 'voyage-law-2': 0.12,
15276
+ 'rerank-2.5': 0.05, 'rerank-2.5-lite': 0.02,
15277
+ 'voyage-multimodal-3': 0.12,
15278
+ };
15279
+ const LARGE_PRICE = 0.12;
15280
+ const OPENAI_PRICE = 0.13;
15281
+
15282
+ // LLM pricing: per-million tokens { input, output }
15283
+ const LLM_PRICING = {
15284
+ 'claude-sonnet-4-5-20250929': { input: 3.00, output: 15.00 },
15285
+ 'claude-opus-4-20250514': { input: 15.00, output: 75.00 },
15286
+ 'claude-3-5-haiku-20241022': { input: 0.80, output: 4.00 },
15287
+ 'claude-sonnet-4': { input: 3.00, output: 15.00 },
15288
+ 'claude-opus-4': { input: 15.00, output: 75.00 },
15289
+ 'gpt-4o': { input: 2.50, output: 10.00 },
15290
+ 'gpt-4o-mini': { input: 0.15, output: 0.60 },
15291
+ 'gpt-4-turbo': { input: 10.00, output: 30.00 },
15292
+ 'o1': { input: 15.00, output: 60.00 },
15293
+ 'o1-mini': { input: 3.00, output: 12.00 },
15294
+ 'o3-mini': { input: 1.10, output: 4.40 },
15295
+ };
15296
+
15297
+ function getLLMPrice(model) {
15298
+ if (!model) return { input: 0, output: 0 };
15299
+ if (LLM_PRICING[model]) return LLM_PRICING[model];
15300
+ // Fuzzy match
15301
+ for (const k of Object.keys(LLM_PRICING)) {
15302
+ if (model.includes(k) || k.includes(model)) return LLM_PRICING[k];
15303
+ }
15304
+ // Match by prefix (e.g. "claude-sonnet-4-5" matches the full versioned key)
15305
+ for (const k of Object.keys(LLM_PRICING)) {
15306
+ const base = k.replace(/-\d{8}$/, '');
15307
+ if (model.includes(base) || base.includes(model)) return LLM_PRICING[k];
15308
+ }
15309
+ return { input: 0, output: 0 }; // Unknown model: free (don't inflate costs)
15310
+ }
15311
+
15312
+ function llmCostForTokens(model, inputTokens, outputTokens) {
15313
+ const prices = getLLMPrice(model);
15314
+ return (prices.input / 1_000_000) * inputTokens + (prices.output / 1_000_000) * outputTokens;
15315
+ }
15316
+
15317
+ function getPrice(model) {
15318
+ if (PRICING[model] != null) return PRICING[model];
15319
+ // fuzzy match
15320
+ for (const k of Object.keys(PRICING)) {
15321
+ if (model.includes(k) || k.includes(model)) return PRICING[k];
15322
+ }
15323
+ return 0.06; // default fallback
15324
+ }
15325
+
15326
+ function costForTokens(pricePerMillion, tokens) {
15327
+ return (pricePerMillion / 1_000_000) * tokens;
15328
+ }
15329
+
15330
+ const session = {
15331
+ operations: [], totalCost: 0, totalTokens: 0, totalOperations: 0,
15332
+ symmetricCost: 0, competitorCost: 0,
15333
+ llmCost: 0, llmInputTokens: 0, llmOutputTokens: 0,
15334
+ };
15335
+
15336
+ window.CostTracker = {
15337
+ addOperation(op, model, tokens) {
15338
+ if (!tokens || tokens <= 0) return;
15339
+ const price = getPrice(model);
15340
+ const cost = costForTokens(price, tokens);
15341
+ const largeCost = costForTokens(LARGE_PRICE, tokens);
15342
+ const competitorCost = costForTokens(OPENAI_PRICE, tokens);
15343
+
15344
+ session.operations.push({
15345
+ timestamp: new Date(), operation: op, model, tokens, cost,
15346
+ hypotheticalLargeCost: largeCost, hypotheticalCompetitorCost: competitorCost,
15347
+ });
15348
+ session.totalCost += cost;
15349
+ session.totalTokens += tokens;
15350
+ session.totalOperations++;
15351
+ session.symmetricCost += largeCost;
15352
+ session.competitorCost += competitorCost;
15353
+
15354
+ this._updateUI();
15355
+ this._showToast(op, model, tokens, cost, largeCost);
15356
+ },
15357
+
15358
+ addLLMOperation(op, model, inputTokens, outputTokens) {
15359
+ if ((!inputTokens || inputTokens <= 0) && (!outputTokens || outputTokens <= 0)) return;
15360
+ const cost = llmCostForTokens(model, inputTokens || 0, outputTokens || 0);
15361
+ const totalTokens = (inputTokens || 0) + (outputTokens || 0);
15362
+
15363
+ session.operations.push({
15364
+ timestamp: new Date(), operation: op, model: model || 'unknown',
15365
+ tokens: totalTokens, cost, isLLM: true,
15366
+ inputTokens: inputTokens || 0, outputTokens: outputTokens || 0,
15367
+ hypotheticalLargeCost: 0, hypotheticalCompetitorCost: 0,
15368
+ });
15369
+ session.totalCost += cost;
15370
+ session.totalTokens += totalTokens;
15371
+ session.totalOperations++;
15372
+ session.llmCost += cost;
15373
+ session.llmInputTokens += (inputTokens || 0);
15374
+ session.llmOutputTokens += (outputTokens || 0);
15375
+
15376
+ this._updateUI();
15377
+ this._showLLMToast(op, model, inputTokens || 0, outputTokens || 0, cost);
15378
+ },
15379
+
15380
+ reset() {
15381
+ session.operations.length = 0;
15382
+ session.totalCost = 0; session.totalTokens = 0; session.totalOperations = 0;
15383
+ session.symmetricCost = 0; session.competitorCost = 0;
15384
+ session.llmCost = 0; session.llmInputTokens = 0; session.llmOutputTokens = 0;
15385
+ this._updateUI();
15386
+ },
15387
+
15388
+ getProjectedMonthlyCost(queriesPerMonth) {
15389
+ if (session.totalOperations === 0) return { actual: 0, symmetric: 0, competitor: 0 };
15390
+ const avgCost = session.totalCost / session.totalOperations;
15391
+ const avgSym = session.symmetricCost / session.totalOperations;
15392
+ const avgComp = session.competitorCost / session.totalOperations;
15393
+ return { actual: avgCost * queriesPerMonth, symmetric: avgSym * queriesPerMonth, competitor: avgComp * queriesPerMonth };
15394
+ },
15395
+
15396
+ togglePanel() {
15397
+ const panel = document.getElementById('costDetailPanel');
15398
+ const btn = document.getElementById('costDetailsToggle');
15399
+ const isOpen = panel.classList.toggle('open');
15400
+ btn.textContent = isOpen ? 'Details ▴' : 'Details ▾';
15401
+ if (isOpen && typeof sendTelemetry === 'function') sendTelemetry('cost_panel_viewed');
15402
+ },
15403
+
15404
+ _updateUI() {
15405
+ document.getElementById('costTotal').textContent = '$' + session.totalCost.toFixed(6);
15406
+ document.getElementById('costTokens').textContent = session.totalTokens.toLocaleString();
15407
+ document.getElementById('costOps').textContent = session.totalOperations;
15408
+ document.getElementById('costLLM').textContent = '$' + session.llmCost.toFixed(6);
15409
+ document.getElementById('costLLMIn').textContent = session.llmInputTokens.toLocaleString();
15410
+ document.getElementById('costLLMOut').textContent = session.llmOutputTokens.toLocaleString();
15411
+ document.getElementById('costSymmetric').textContent = '$' + session.symmetricCost.toFixed(6);
15412
+ document.getElementById('costCompetitor').textContent = '$' + session.competitorCost.toFixed(6);
15413
+
15414
+ // Deltas
15415
+ const symDelta = document.getElementById('costSymDelta');
15416
+ const compDelta = document.getElementById('costCompDelta');
15417
+ if (session.totalCost > 0) {
15418
+ const symPct = ((session.symmetricCost - session.totalCost) / session.totalCost * 100).toFixed(0);
15419
+ const compPct = ((session.competitorCost - session.totalCost) / session.totalCost * 100).toFixed(0);
15420
+ symDelta.textContent = symPct > 0 ? `(+${symPct}%)` : `(${symPct}%)`;
15421
+ symDelta.className = symPct > 0 ? 'cost-more' : 'cost-savings';
15422
+ compDelta.textContent = compPct > 0 ? `(+${compPct}%)` : `(${compPct}%)`;
15423
+ compDelta.className = compPct > 0 ? 'cost-more' : 'cost-savings';
15424
+ } else {
15425
+ symDelta.textContent = ''; compDelta.textContent = '';
15426
+ }
15427
+
15428
+ // Table
15429
+ const tbody = document.querySelector('#costOpsTable tbody');
15430
+ tbody.innerHTML = session.operations.map(o => {
15431
+ const tokensDisplay = o.isLLM
15432
+ ? `${o.inputTokens.toLocaleString()}in / ${o.outputTokens.toLocaleString()}out`
15433
+ : o.tokens.toLocaleString();
15434
+ const largeCostDisplay = o.isLLM ? 'n/a' : `$${o.hypotheticalLargeCost.toFixed(6)}`;
15435
+ const compCostDisplay = o.isLLM ? 'n/a' : `$${o.hypotheticalCompetitorCost.toFixed(6)}`;
15436
+ return `<tr${o.isLLM ? ' style="background:rgba(180,90,242,0.08)"' : ''}>
15437
+ <td>${o.timestamp.toLocaleTimeString()}</td>
15438
+ <td>${o.operation}</td>
15439
+ <td>${o.model}</td>
15440
+ <td>${tokensDisplay}</td>
15441
+ <td>$${o.cost.toFixed(6)}</td>
15442
+ <td>${largeCostDisplay}</td>
15443
+ <td>${compCostDisplay}</td>
15444
+ </tr>`;
15445
+ }).join('');
15446
+
15447
+ // Projection
15448
+ const proj = this.getProjectedMonthlyCost(1_000_000);
15449
+ const projEl = document.getElementById('costProjection');
15450
+ if (session.totalOperations > 0) {
15451
+ projEl.innerHTML = `📊 Projected at 1M queries/month: <span>$${proj.actual.toFixed(2)}</span> actual │ $${proj.symmetric.toFixed(2)} symmetric │ $${proj.competitor.toFixed(2)} OpenAI`;
15452
+ } else {
15453
+ projEl.innerHTML = '';
15454
+ }
15455
+ },
15456
+
15457
+ _showToast(op, model, tokens, cost, largeCost) {
15458
+ const saved = largeCost > 0 ? Math.round((1 - cost / largeCost) * 100) : 0;
15459
+ const savingsText = saved > 0 ? ` — saved ${saved}% vs v4-large` : '';
15460
+ const toast = document.createElement('div');
15461
+ toast.className = 'cost-toast';
15462
+ toast.textContent = `✅ ${op} — $${cost.toFixed(6)} (${model}, ${tokens.toLocaleString()} tokens)${savingsText}`;
15463
+ document.body.appendChild(toast);
15464
+ setTimeout(() => toast.remove(), 3100);
15465
+ },
15466
+
15467
+ _showLLMToast(op, model, inputTokens, outputTokens, cost) {
15468
+ const toast = document.createElement('div');
15469
+ toast.className = 'cost-toast';
15470
+ toast.textContent = `🤖 ${op} — $${cost.toFixed(6)} (${model || 'LLM'}, ${inputTokens.toLocaleString()}in/${outputTokens.toLocaleString()}out)`;
15471
+ document.body.appendChild(toast);
15472
+ setTimeout(() => toast.remove(), 3100);
15473
+ }
15474
+ };
15475
+ })();
15476
+ </script>
15477
+
12070
15478
  </body>
12071
15479
  </html>