voyageai-cli 1.27.0 → 1.28.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.
@@ -992,8 +992,9 @@ select:focus { outline: none; border-color: var(--accent); }
992
992
  }
993
993
 
994
994
  .explore-card-icon {
995
- font-size: 28px;
996
995
  margin-bottom: 8px;
996
+ color: var(--accent, #00D4AA);
997
+ opacity: 0.85;
997
998
  }
998
999
  .explore-card-title {
999
1000
  font-size: 16px;
@@ -1056,7 +1057,7 @@ select:focus { outline: none; border-color: var(--accent); }
1056
1057
  padding: 24px 28px 16px;
1057
1058
  border-bottom: 1px solid var(--border);
1058
1059
  }
1059
- .explore-modal-icon { font-size: 32px; }
1060
+ .explore-modal-icon { color: var(--accent, #00D4AA); }
1060
1061
  .explore-modal-title {
1061
1062
  font-size: 18px;
1062
1063
  font-weight: 600;
@@ -3030,6 +3031,24 @@ select:focus { outline: none; border-color: var(--accent); }
3030
3031
  font-size: 10px; color: var(--text-muted); margin-top: 4px;
3031
3032
  display: flex; gap: 8px;
3032
3033
  }
3034
+ /* Collapsible examples section */
3035
+ .wf-library-section { padding: 0; margin-top: 4px; }
3036
+ .wf-library-section-toggle {
3037
+ display: flex; align-items: center; gap: 6px;
3038
+ width: 100%; padding: 8px 12px; border: none; background: none;
3039
+ color: var(--text-muted); font-size: 11px; font-weight: 600;
3040
+ text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer;
3041
+ transition: color 0.15s;
3042
+ }
3043
+ .wf-library-section-toggle:hover { color: var(--text); }
3044
+ .wf-library-section-toggle .arrow {
3045
+ font-size: 8px; transition: transform 0.2s; display: inline-block;
3046
+ }
3047
+ .wf-library-section-toggle.open .arrow { transform: rotate(90deg); }
3048
+ .wf-library-category {
3049
+ font-size: 10px; font-weight: 600; color: var(--text-muted);
3050
+ padding: 8px 12px 2px; text-transform: uppercase; letter-spacing: 0.5px;
3051
+ }
3033
3052
  .wf-canvas-area {
3034
3053
  flex: 1; position: relative; overflow: hidden;
3035
3054
  background: var(--bg);
@@ -3060,9 +3079,70 @@ select:focus { outline: none; border-color: var(--accent); }
3060
3079
  }
3061
3080
  .wf-canvas-toolbar .wf-run-btn:hover { opacity: 0.9; }
3062
3081
  .wf-canvas-toolbar .wf-run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
3082
+ .wf-canvas-toolbar .wf-new-btn {
3083
+ width: auto; padding: 0 12px; gap: 4px;
3084
+ font-weight: 600; font-size: 12px;
3085
+ background: var(--bg-card); border: 1px solid var(--accent);
3086
+ color: var(--accent);
3087
+ }
3088
+ .wf-canvas-toolbar .wf-new-btn:hover { background: var(--accent); color: #fff; }
3089
+ .wf-canvas-toolbar .wf-edit-btn {
3090
+ width: auto; padding: 0 12px; gap: 4px;
3091
+ font-weight: 600; font-size: 12px;
3092
+ background: var(--bg-card); border: 1px solid var(--border);
3093
+ color: var(--text-muted);
3094
+ }
3095
+ .wf-canvas-toolbar .wf-edit-btn:hover { border-color: var(--accent); color: var(--accent); }
3096
+ .wf-canvas-toolbar .wf-edit-btn:disabled { opacity: 0.4; cursor: not-allowed; }
3097
+ .wf-canvas-toolbar .wf-edit-btn:disabled:hover { border-color: var(--border); color: var(--text-muted); }
3063
3098
  .wf-toolbar-sep {
3064
3099
  width: 1px; height: 20px; background: var(--border); margin: 0 2px;
3065
3100
  }
3101
+ /* Library / Palette tabs */
3102
+ .wf-library-tabs {
3103
+ display: flex; gap: 2px; width: 100%;
3104
+ background: var(--bg-input); border-radius: 6px; padding: 2px;
3105
+ }
3106
+ .wf-lib-tab {
3107
+ flex: 1; padding: 5px 0; border: none; border-radius: 4px;
3108
+ background: transparent; color: var(--text-muted); cursor: pointer;
3109
+ font-size: 11px; font-weight: 600; font-family: var(--font);
3110
+ transition: background 0.15s, color 0.15s; text-align: center;
3111
+ }
3112
+ .wf-lib-tab.active { background: var(--accent); color: #fff; }
3113
+ .wf-lib-tab:hover:not(.active) { color: var(--text); }
3114
+ /* Palette items */
3115
+ .wf-palette-category { margin-bottom: 8px; }
3116
+ .wf-palette-category-title {
3117
+ font-size: 10px; font-weight: 700; text-transform: uppercase;
3118
+ letter-spacing: 0.5px; color: var(--text-muted);
3119
+ padding: 4px 12px; margin-bottom: 2px;
3120
+ }
3121
+ .wf-palette-item {
3122
+ display: flex; align-items: center; gap: 8px;
3123
+ padding: 7px 12px; border-radius: 6px; cursor: grab;
3124
+ font-size: 12px; color: var(--text); transition: background 0.15s;
3125
+ }
3126
+ .wf-palette-item:hover { background: var(--bg-card); }
3127
+ .wf-palette-item:active { cursor: grabbing; }
3128
+ .wf-palette-icon { width: 20px; text-align: center; display: flex; align-items: center; justify-content: center; }
3129
+ .wf-palette-label { font-weight: 500; }
3130
+ /* Builder port styles */
3131
+ .wf-port-builder { cursor: crosshair; transition: r 0.15s; pointer-events: all !important; }
3132
+ .wf-port-builder:hover { r: 9; filter: brightness(1.4); }
3133
+ .wf-inspector-delete-btn {
3134
+ width: 100%; padding: 8px 0; border: none; border-radius: 6px;
3135
+ background: var(--error); color: #fff; cursor: pointer;
3136
+ font-size: 12px; font-weight: 600; margin-top: 12px;
3137
+ transition: opacity 0.15s;
3138
+ }
3139
+ .wf-inspector-delete-btn:hover { opacity: 0.85; }
3140
+ .wf-validation-errors {
3141
+ background: rgba(255,105,96,0.08); border: 1px solid var(--error);
3142
+ border-radius: 6px; padding: 10px 12px; margin-bottom: 12px;
3143
+ font-size: 11px; color: var(--error);
3144
+ }
3145
+ .wf-validation-errors ul { margin: 4px 0 0 16px; }
3066
3146
  /* Dry-run plan overlay */
3067
3147
  .wf-dryrun-overlay {
3068
3148
  position: absolute; top: 0; left: 0; right: 0; bottom: 0;
@@ -3107,7 +3187,7 @@ select:focus { outline: none; border-color: var(--accent); }
3107
3187
  background: var(--bg-card); border: 1px solid var(--border);
3108
3188
  display: flex; align-items: center; gap: 10px; font-size: 12px;
3109
3189
  }
3110
- .wf-dryrun-step-icon { font-size: 16px; flex-shrink: 0; }
3190
+ .wf-dryrun-step-icon { flex-shrink: 0; display: flex; align-items: center; }
3111
3191
  .wf-dryrun-step-info { flex: 1; min-width: 0; }
3112
3192
  .wf-dryrun-step-name { font-weight: 600; color: var(--text); }
3113
3193
  .wf-dryrun-step-tool { color: var(--text-muted); font-size: 11px; }
@@ -3195,8 +3275,61 @@ select:focus { outline: none; border-color: var(--accent); }
3195
3275
  transition: color 0.15s, border-color 0.15s;
3196
3276
  }
3197
3277
  .wf-output-expand-btn:hover { color: var(--text); border-color: var(--text-muted); }
3278
+ /* Input modal (pre-execution) */
3279
+ .wf-input-modal-backdrop {
3280
+ position: fixed; inset: 0; z-index: 9999;
3281
+ background: rgba(0,0,0,0.55); display: flex;
3282
+ align-items: center; justify-content: center;
3283
+ }
3284
+ .wf-input-modal {
3285
+ background: var(--bg-surface, var(--bg)); border: 1px solid var(--border);
3286
+ border-radius: 10px; width: 440px; max-width: 90vw;
3287
+ max-height: 80vh; display: flex; flex-direction: column;
3288
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
3289
+ }
3290
+ .wf-input-modal-header {
3291
+ display: flex; align-items: center; justify-content: space-between;
3292
+ padding: 14px 20px; border-bottom: 1px solid var(--border);
3293
+ }
3294
+ .wf-input-modal-title { font-weight: 700; font-size: 14px; color: var(--text); }
3295
+ .wf-input-modal-body {
3296
+ flex: 1; overflow-y: auto; padding: 16px 20px;
3297
+ }
3298
+ .wf-input-modal-field { margin-bottom: 14px; }
3299
+ .wf-input-modal-label {
3300
+ font-size: 12px; font-weight: 600; color: var(--text); margin-bottom: 4px;
3301
+ }
3302
+ .wf-input-modal-desc {
3303
+ font-size: 11px; color: var(--text-muted); margin-bottom: 4px; line-height: 1.3;
3304
+ }
3305
+ .wf-input-modal-input {
3306
+ width: 100%; box-sizing: border-box; padding: 6px 10px;
3307
+ border: 1px solid var(--border); border-radius: 6px;
3308
+ background: var(--bg); color: var(--text); font-size: 13px;
3309
+ font-family: inherit; outline: none;
3310
+ transition: border-color 0.15s;
3311
+ }
3312
+ .wf-input-modal-input:focus { border-color: var(--accent, #6c63ff); }
3313
+ .wf-input-modal-input.error { border-color: #e74c3c; }
3314
+ .wf-input-modal-error { font-size: 11px; color: #e74c3c; margin-top: 3px; display: none; }
3315
+ .wf-input-modal-footer {
3316
+ display: flex; align-items: center; justify-content: flex-end; gap: 8px;
3317
+ padding: 12px 20px; border-top: 1px solid var(--border);
3318
+ }
3319
+ .wf-input-modal-cancel {
3320
+ padding: 6px 16px; border: 1px solid var(--border); border-radius: 6px;
3321
+ background: transparent; color: var(--text); font-size: 12px; cursor: pointer;
3322
+ transition: background 0.15s, border-color 0.15s;
3323
+ }
3324
+ .wf-input-modal-cancel:hover { background: var(--bg); border-color: var(--text-muted); }
3325
+ .wf-input-modal-run {
3326
+ padding: 6px 16px; border: none; border-radius: 6px;
3327
+ background: var(--accent, #6c63ff); color: #fff; font-size: 12px;
3328
+ font-weight: 600; cursor: pointer; transition: filter 0.15s;
3329
+ }
3330
+ .wf-input-modal-run:hover { filter: brightness(1.15); }
3198
3331
  #wf-canvas {
3199
- width: 100%; height: 100%; display: block;
3332
+ width: 100%; height: 100%; display: block; position: relative; z-index: 1;
3200
3333
  }
3201
3334
  /* SVG node styles */
3202
3335
  .wf-node { cursor: pointer; }
@@ -3211,8 +3344,8 @@ select:focus { outline: none; border-color: var(--accent); }
3211
3344
  pointer-events: none; text-anchor: middle; dominant-baseline: central;
3212
3345
  }
3213
3346
  .wf-node-icon {
3214
- font-size: 16px; pointer-events: none;
3215
- text-anchor: middle; dominant-baseline: central;
3347
+ pointer-events: none;
3348
+ color: #fff;
3216
3349
  }
3217
3350
  .wf-node-badge {
3218
3351
  font-size: 10px; fill: rgba(255,255,255,0.7);
@@ -3374,7 +3507,7 @@ select:focus { outline: none; border-color: var(--accent); }
3374
3507
  }
3375
3508
  /* Workflow visualizer light mode */
3376
3509
  [data-theme="light"] .wf-node-label { fill: #001E2B; }
3377
- [data-theme="light"] .wf-node-icon { fill: #001E2B; }
3510
+ [data-theme="light"] .wf-node-icon { color: #001E2B; }
3378
3511
  [data-theme="light"] .wf-node-badge { fill: rgba(0,30,43,0.55); }
3379
3512
  [data-theme="light"] .wf-node-time { fill: rgba(0,30,43,0.5); }
3380
3513
  [data-theme="light"] .wf-node-condition { fill: #944F01; }
@@ -3396,6 +3529,8 @@ select:focus { outline: none; border-color: var(--accent); }
3396
3529
  [data-theme="light"] .wf-exec-status { box-shadow: 0 2px 8px rgba(0,30,43,0.08); }
3397
3530
  [data-theme="light"] .wf-run-btn { background: var(--accent); }
3398
3531
  [data-theme="light"] .wf-output-modal { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
3532
+ [data-theme="light"] .wf-input-modal { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
3533
+ [data-theme="light"] .wf-input-modal-backdrop { background: rgba(0,0,0,0.3); }
3399
3534
  @keyframes wf-pulse-light {
3400
3535
  0%, 100% { filter: drop-shadow(0 0 4px rgba(0,158,128,0.3)); }
3401
3536
  50% { filter: drop-shadow(0 0 12px rgba(0,158,128,0.6)); }
@@ -3406,61 +3541,78 @@ select:focus { outline: none; border-color: var(--accent); }
3406
3541
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
3407
3542
  text-align: center; color: var(--text-muted); pointer-events: none;
3408
3543
  }
3409
- .wf-canvas-empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.5; }
3544
+ .wf-canvas-empty-icon { margin-bottom: 16px; opacity: 0.15; }
3545
+ .wf-canvas-empty-icon img { width: 200px; height: 200px; filter: invert(1); }
3546
+ [data-theme="light"] .wf-canvas-empty-icon img { filter: none; opacity: 0.1; }
3410
3547
  .wf-canvas-empty-text { font-size: 14px; }
3548
+ /* Persistent watermark behind workflows */
3549
+ .wf-canvas-watermark {
3550
+ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
3551
+ pointer-events: none; z-index: 0; opacity: 0.04;
3552
+ }
3553
+ .wf-canvas-watermark img { width: 300px; height: 300px; filter: invert(1); }
3554
+ [data-theme="light"] .wf-canvas-watermark img { filter: none; }
3411
3555
  </style>
3412
3556
  </head>
3413
3557
  <body>
3414
3558
 
3415
- <!-- LeafyGreen Icon Sprites (mongodb.design) -->
3559
+ <!-- Lucide Icons (lucide.dev) — stroke-based, 16×16 -->
3416
3560
  <svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
3417
- <symbol id="lg-lightning" viewBox="0 0 16 16">
3418
- <path d="M9.22274 1.99296C9.22274 1.49561 8.56293 1.31233 8.30107 1.73667L4.07384 8.58696C3.87133 8.91513 4.10921 9.33717 4.49748 9.33717H6.77682L6.77682 14.0066C6.77682 14.504 7.43627 14.6879 7.69813 14.2635L11.9262 7.4118C12.1288 7.08363 11.8903 6.66244 11.5021 6.66244H9.22274V1.99296Z" fill="currentColor"/>
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"/>
3419
3564
  </symbol>
3420
- <symbol id="lg-arrows" viewBox="0 0 16 16">
3421
- <path d="M5 8.57279V13.4272C5 13.9565 4.39241 14.2015 4.0721 13.8014L2.12898 11.3742C1.95701 11.1594 1.95701 10.8406 2.12898 10.6258L4.0721 8.19856C4.39241 7.79846 5 8.04351 5 8.57279Z" fill="currentColor"/>
3422
- <path d="M5 10H12.5C12.7761 10 13 10.2239 13 10.5V11.5C13 11.7761 12.7761 12 12.5 12H5V10Z" fill="currentColor"/>
3423
- <path d="M11 7.42721V2.57279C11 2.04351 11.6076 1.79846 11.9279 2.19856L13.871 4.62577C14.043 4.84058 14.043 5.15942 13.871 5.37423L11.9279 7.80144C11.6076 8.20154 11 7.95649 11 7.42721Z" fill="currentColor"/>
3424
- <path d="M3 4.5C3 4.22386 3.22386 4 3.5 4H11V6H3.5C3.22386 6 3 5.77614 3 5.5V4.5Z" fill="currentColor"/>
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"/>
3425
3568
  </symbol>
3426
- <symbol id="lg-search" viewBox="0 0 16 16">
3427
- <path fill-rule="evenodd" clip-rule="evenodd" d="M2.3234 9.81874C4.07618 11.5715 6.75062 11.8398 8.78588 10.6244L12.93 14.7685C13.4377 15.2762 14.2608 15.2762 14.7685 14.7685C15.2762 14.2608 15.2762 13.4377 14.7685 12.93L10.6244 8.78588C11.8398 6.75062 11.5715 4.07619 9.81873 2.32341C7.74896 0.253628 4.39318 0.253628 2.3234 2.32341C0.253624 4.39319 0.253624 7.74896 2.3234 9.81874ZM7.98026 4.16188C9.03467 5.2163 9.03467 6.92585 7.98026 7.98026C6.92584 9.03468 5.2163 9.03468 4.16188 7.98026C3.10746 6.92585 3.10746 5.2163 4.16188 4.16188C5.2163 3.10747 6.92584 3.10747 7.98026 4.16188Z" fill="currentColor"/>
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"/>
3428
3572
  </symbol>
3429
- <symbol id="lg-gauge" viewBox="0 0 16 16">
3430
- <path d="M1.041 10.2514C0.996713 10.6632 1.33666 11 1.75088 11H4.2449C4.65912 11 4.98569 10.6591 5.08798 10.2577C5.22027 9.73864 5.49013 9.25966 5.87533 8.87446C6.43906 8.31073 7.20364 7.99403 8.00088 7.99403C8.27011 7.99403 8.53562 8.03015 8.79093 8.0997L11.7818 5.10887C10.6623 4.39046 9.35172 4 8.00088 4C6.14436 4 4.36388 4.7375 3.05113 6.05025C1.91604 7.18534 1.21104 8.67012 1.041 10.2514Z" fill="currentColor"/>
3431
- <path d="M13.2967 6.42237L10.455 9.26409C10.6678 9.56493 10.8231 9.90191 10.9138 10.2577C11.0161 10.6591 11.3426 11 11.7568 11L14.2509 11C14.6651 11 15.005 10.6632 14.9608 10.2514C14.8087 8.83759 14.229 7.50093 13.2967 6.42237Z" fill="currentColor"/>
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"/>
3432
3576
  </symbol>
3433
- <symbol id="lg-bulb" viewBox="0 0 16 16">
3434
- <path d="M12.3311 8.5C12.7565 7.76457 13 6.91072 13 6C13 3.23858 10.7614 1 8 1C5.23858 1 3 3.23858 3 6C3 6.94628 3.26287 7.83117 3.71958 8.58561L5.40749 11.501C5.58628 11.8099 5.91607 12 6.27291 12H6.5V6C6.5 5.17157 7.17157 4.5 8 4.5C8.82843 4.5 9.5 5.17157 9.5 6V12H9.72368C10.0793 12 10.4082 11.8111 10.5874 11.5039L12.34 8.5H12.3311Z" fill="currentColor"/>
3435
- <path d="M7.5 6V12H8.5V6C8.5 5.72386 8.27614 5.5 8 5.5C7.72386 5.5 7.5 5.72386 7.5 6Z" fill="currentColor"/>
3436
- <path d="M10 14V13H6V14C6 14.5523 6.44772 15 7 15H9C9.55228 15 10 14.5523 10 14Z" fill="currentColor"/>
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"/>
3437
3580
  </symbol>
3438
- <symbol id="lg-info" viewBox="0 0 16 16">
3439
- <path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM9 4C9 4.55228 8.55228 5 8 5C7.44772 5 7 4.55228 7 4C7 3.44772 7.44772 3 8 3C8.55228 3 9 3.44772 9 4ZM8 6C8.55228 6 9 6.44772 9 7V11H9.5C9.77614 11 10 11.2239 10 11.5C10 11.7761 9.77614 12 9.5 12H6.5C6.22386 12 6 11.7761 6 11.5C6 11.2239 6.22386 11 6.5 11H7V7H6.5C6.22386 7 6 6.77614 6 6.5C6 6.22386 6.22386 6 6.5 6H8Z" fill="currentColor"/>
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"/>
3440
3584
  </symbol>
3441
- <symbol id="lg-image" viewBox="0 0 16 16">
3442
- <path fill-rule="evenodd" clip-rule="evenodd" d="M2 3C2 2.44772 2.44772 2 3 2H13C13.5523 2 14 2.44772 14 3V13C14 13.5523 13.5523 14 13 14H3C2.44772 14 2 13.5523 2 13V3ZM4 4V9.586L5.793 7.793C6.183 7.403 6.817 7.403 7.207 7.793L8.5 9.086L10.293 7.293C10.683 6.903 11.317 6.903 11.707 7.293L12 7.586V4H4ZM12 10.414L10.5 8.914L8.207 11.207C7.817 11.597 7.183 11.597 6.793 11.207L5.5 9.914L4 11.414V12H12V10.414ZM10 5.5C10 6.328 10.672 7 11.5 7C11.776 7 12 6.776 12 6.5V5C12 4.724 11.776 4.5 11.5 4.5H10.5C10.224 4.5 10 4.724 10 5V5.5Z" fill="currentColor"/>
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"/>
3443
3588
  </symbol>
3444
- <symbol id="lg-config" viewBox="0 0 16 16">
3445
- <path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 1.5A.5.5 0 0 1 7 1h2a.5.5 0 0 1 .5.5v1.05a5 5 0 0 1 1.37.57l.74-.74a.5.5 0 0 1 .7 0l1.42 1.42a.5.5 0 0 1 0 .7l-.74.74c.25.43.44.89.57 1.37H14.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-1.05a5 5 0 0 1-.57 1.37l.74.74a.5.5 0 0 1 0 .7l-1.42 1.42a.5.5 0 0 1-.7 0l-.74-.74a5 5 0 0 1-1.37.57V14.5a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5v-1.05a5 5 0 0 1-1.37-.57l-.74.74a.5.5 0 0 1-.7 0L2.27 12.2a.5.5 0 0 1 0-.7l.74-.74a5 5 0 0 1-.57-1.37H1.5A.5.5 0 0 1 1 9V7a.5.5 0 0 1 .5-.5h1.05c.13-.48.32-.94.57-1.37l-.74-.74a.5.5 0 0 1 0-.7L3.8 2.27a.5.5 0 0 1 .7 0l.74.74A5 5 0 0 1 6.6 2.44V1.5zM8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z" fill="currentColor"/>
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"/>
3446
3592
  </symbol>
3447
- <symbol id="lg-code" viewBox="0 0 16 16">
3448
- <path fill="currentColor" d="M5.854 4.146a.5.5 0 0 1 0 .708L2.707 8l3.147 3.146a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 0 1 .708 0zm4.292 0a.5.5 0 0 0 0 .708L13.293 8l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0z"/>
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"/>
3449
3596
  </symbol>
3450
- <symbol id="lg-palette" viewBox="0 0 16 16">
3451
- <path fill-rule="evenodd" clip-rule="evenodd" d="M8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15C8.55228 15 9 14.5523 9 14V12.5C9 11.6716 9.67157 11 10.5 11H12C13.6569 11 15 9.65685 15 8C15 4.13401 11.866 1 8 1ZM4.5 9C5.32843 9 6 8.32843 6 7.5C6 6.67157 5.32843 6 4.5 6C3.67157 6 3 6.67157 3 7.5C3 8.32843 3.67157 9 4.5 9ZM7 5.5C7 6.32843 6.32843 7 5.5 7C4.67157 7 4 6.32843 4 5.5C4 4.67157 4.67157 4 5.5 4C6.32843 4 7 4.67157 7 5.5ZM9.5 6C10.3284 6 11 5.32843 11 4.5C11 3.67157 10.3284 3 9.5 3C8.67157 3 8 3.67157 8 4.5C8 5.32843 8.67157 6 9.5 6ZM13 7.5C13 8.32843 12.3284 9 11.5 9C10.6716 9 10 8.32843 10 7.5C10 6.67157 10.6716 6 11.5 6C12.3284 6 13 6.67157 13 7.5Z" fill="currentColor"/>
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"/>
3452
3600
  </symbol>
3453
- <symbol id="lg-cube" viewBox="0 0 16 16">
3454
- <path fill-rule="evenodd" clip-rule="evenodd" d="M8.35 1.18a.75.75 0 0 0-.7 0l-6 3.25A.75.75 0 0 0 1.25 5v6a.75.75 0 0 0 .4.66l6 3.25a.75.75 0 0 0 .7 0l6-3.25a.75.75 0 0 0 .4-.66V5a.75.75 0 0 0-.4-.66l-6-3.16zM8 3.2 3.47 5.65 8 7.82l4.53-2.17L8 3.2zM2.75 6.8v3.82L7.25 12.8V8.97L2.75 6.8zm5.5 6 4.5-2.18V6.8L8.25 8.97V12.8z" fill="currentColor"/>
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"/>
3455
3604
  </symbol>
3456
- <symbol id="lg-chat" viewBox="0 0 16 16">
3457
- <path d="M2 2h12v9H5l-3 3V2z" fill="none" stroke="currentColor" stroke-width="1.5"/>
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"/>
3458
3608
  </symbol>
3459
- <symbol id="lg-shield" viewBox="0 0 16 16">
3460
- <path fill-rule="evenodd" clip-rule="evenodd" d="M8.35 1.18a.75.75 0 0 0-.7 0l-5 2.7A.75.75 0 0 0 2.25 4.5V8c0 2.9 2.1 5.5 5.5 6.95a.75.75 0 0 0 .5 0C11.65 13.5 13.75 10.9 13.75 8V4.5a.75.75 0 0 0-.4-.62l-5-2.7zM8 3.2 3.75 5.5V8c0 2.2 1.6 4.2 4.25 5.45C10.65 12.2 12.25 10.2 12.25 8V5.5L8 3.2z" fill="currentColor"/>
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"/>
3461
3612
  </symbol>
3462
- <symbol id="lg-pulse" viewBox="0 0 16 16">
3463
- <path d="M1 8h3l2-5 2 10 2-5h5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
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"/>
3464
3616
  </symbol>
3465
3617
  </svg>
3466
3618
 
@@ -3471,7 +3623,7 @@ select:focus { outline: none; border-color: var(--accent); }
3471
3623
  <div class="sidebar-drag-region">
3472
3624
  <img class="sidebar-logo" id="sidebarLogo" src="/icons/dark/64.png" alt="Vai">
3473
3625
  <span class="sidebar-title">Vai</span>
3474
- <button class="sidebar-settings-btn" data-tab="settings" title="Settings"><svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-config"/></svg></button>
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>
3475
3627
  </div>
3476
3628
  <nav class="sidebar-nav">
3477
3629
  <div class="sidebar-nav-group" role="tablist" aria-label="Tools">
@@ -3481,8 +3633,8 @@ select:focus { outline: none; border-color: var(--accent); }
3481
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>
3482
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>
3483
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>
3484
- <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 width="16" height="16" viewBox="0 0 16 16"><path d="M2 2h12v9H5l-3 3V2z" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></span><span>Chat</span></button>
3485
- <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 width="16" height="16" viewBox="0 0 16 16"><circle cx="3" cy="8" r="2" fill="none" stroke="currentColor" stroke-width="1.3"/><circle cx="13" cy="4" r="2" fill="none" stroke="currentColor" stroke-width="1.3"/><circle cx="13" cy="12" r="2" fill="none" stroke="currentColor" stroke-width="1.3"/><line x1="5" y1="7" x2="11" y2="4.5" stroke="currentColor" stroke-width="1.3"/><line x1="5" y1="9" x2="11" y2="11.5" stroke="currentColor" stroke-width="1.3"/></svg></span><span>Workflows</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>
3486
3638
  </div>
3487
3639
  <div class="sidebar-nav-divider"></div>
3488
3640
  <div class="sidebar-nav-group" role="tablist" aria-label="Learn">
@@ -3581,7 +3733,7 @@ select:focus { outline: none; border-color: var(--accent); }
3581
3733
  <option value="ubinary">ubinary (32× smaller)</option>
3582
3734
  </select>
3583
3735
  </div>
3584
- <button class="btn" id="embedBtn" onclick="doEmbed()">⚡ Embed</button>
3736
+ <button class="btn" id="embedBtn" onclick="doEmbed()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>Embed</button>
3585
3737
  </div>
3586
3738
 
3587
3739
  <div class="error-msg" id="embedError"></div>
@@ -3592,7 +3744,7 @@ select:focus { outline: none; border-color: var(--accent); }
3592
3744
  <div id="embedStats"></div>
3593
3745
  <div class="vector-preview" id="embedVector"></div>
3594
3746
  <div style="margin-top:8px;">
3595
- <button class="btn btn-secondary btn-small" onclick="copyVector()">📋 Copy Full Vector</button>
3747
+ <button class="btn btn-secondary btn-small" onclick="copyVector()"><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:-1px;margin-right:3px;"><path d="M9 9V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-3M3 15a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3z"/></svg>Copy Full Vector</button>
3596
3748
  </div>
3597
3749
  <div class="card-title" style="margin-top:16px;">Vector Heatmap</div>
3598
3750
  <div class="heatmap" id="embedHeatmap"></div>
@@ -3634,7 +3786,7 @@ select:focus { outline: none; border-color: var(--accent); }
3634
3786
  <option value="2048">2048</option>
3635
3787
  </select>
3636
3788
  </div>
3637
- <button class="btn" id="compareBtn" onclick="doCompare()">⚖️ Compare</button>
3789
+ <button class="btn" id="compareBtn" onclick="doCompare()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4"/></svg>Compare</button>
3638
3790
  </div>
3639
3791
 
3640
3792
  <div class="error-msg" id="compareError"></div>
@@ -3696,8 +3848,8 @@ Semantic search understands meaning beyond keyword matching</textarea>
3696
3848
  <option value="10">10</option>
3697
3849
  </select>
3698
3850
  </div>
3699
- <button class="btn" id="searchBtn" onclick="doSearch(false)">🔍 Search</button>
3700
- <button class="btn btn-secondary" id="searchRerankBtn" onclick="doSearch(true)">🔍+ Rerank</button>
3851
+ <button class="btn" id="searchBtn" onclick="doSearch(false)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/></svg>Search</button>
3852
+ <button class="btn btn-secondary" id="searchRerankBtn" onclick="doSearch(true)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M3 6h18M7 12h10M10 18h4"/></svg>Rerank</button>
3701
3853
  </div>
3702
3854
 
3703
3855
  <div class="error-msg" id="searchError"></div>
@@ -3756,7 +3908,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
3756
3908
  <option value="2048">2048</option>
3757
3909
  </select>
3758
3910
  </div>
3759
- <button class="btn" id="mmCompareBtn" onclick="doMultimodalCompare()">🔮 Compare</button>
3911
+ <button class="btn" id="mmCompareBtn" onclick="doMultimodalCompare()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4"/></svg>Compare</button>
3760
3912
  </div>
3761
3913
 
3762
3914
  <div class="error-msg" id="mmError"></div>
@@ -3794,20 +3946,20 @@ Semantic search understands meaning beyond keyword matching</textarea>
3794
3946
  <div class="card" style="margin-top:12px;">
3795
3947
  <div class="card-title">Search Query</div>
3796
3948
  <div class="mm-search-mode" id="mmSearchMode">
3797
- <button class="active" data-mode="text" onclick="setMmSearchMode('text')">📝 Text Query</button>
3798
- <button data-mode="image" onclick="setMmSearchMode('image')">🖼️ Image Query</button>
3949
+ <button class="active" data-mode="text" onclick="setMmSearchMode('text')"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M17 6.1H3M21 12.1H3M15.1 18H3"/></svg>Text Query</button>
3950
+ <button data-mode="image" onclick="setMmSearchMode('image')"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><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"/></svg>Image Query</button>
3799
3951
  </div>
3800
3952
  <div id="mmSearchTextWrap">
3801
3953
  <div class="mm-search-row">
3802
3954
  <input type="text" id="mmSearchQuery" placeholder="Enter a search query...">
3803
- <button class="btn" id="mmSearchBtn" onclick="doMultimodalSearch()">🔮 Search Corpus</button>
3955
+ <button class="btn" id="mmSearchBtn" onclick="doMultimodalSearch()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/></svg>Search Corpus</button>
3804
3956
  </div>
3805
3957
  </div>
3806
3958
  <div id="mmSearchImageWrap" style="display:none;">
3807
3959
  <p style="font-size:13px;color:var(--text-dim);margin-bottom:8px;">Click an image in the corpus above to use it as the search query, then:</p>
3808
3960
  <div style="display:flex;gap:8px;align-items:center;">
3809
3961
  <span id="mmSearchImageLabel" style="font-size:13px;color:var(--text-muted);">No image selected</span>
3810
- <button class="btn" id="mmSearchImgBtn" onclick="doMultimodalSearch()">🔮 Search Corpus</button>
3962
+ <button class="btn" id="mmSearchImgBtn" onclick="doMultimodalSearch()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/></svg>Search Corpus</button>
3811
3963
  </div>
3812
3964
  </div>
3813
3965
  </div>
@@ -3834,12 +3986,12 @@ Semantic search understands meaning beyond keyword matching</textarea>
3834
3986
 
3835
3987
  <!-- Sub-panel switcher -->
3836
3988
  <div class="bench-panels">
3837
- <button class="bench-panel-btn active" data-bench="latency">⚡ Latency</button>
3838
- <button class="bench-panel-btn" data-bench="ranking">🏆 Ranking</button>
3839
- <button class="bench-panel-btn" data-bench="competitors">⚔️ vs Competitors</button>
3840
- <button class="bench-panel-btn" data-bench="quantization">⚗️ Quantization</button>
3841
- <button class="bench-panel-btn" data-bench="cost">💰 Cost</button>
3842
- <button class="bench-panel-btn" data-bench="history">📊 History</button>
3989
+ <button class="bench-panel-btn active" data-bench="latency"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>Latency</button>
3990
+ <button class="bench-panel-btn" data-bench="ranking"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>Ranking</button>
3991
+ <button class="bench-panel-btn" data-bench="competitors"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="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"/></svg>vs Competitors</button>
3992
+ <button class="bench-panel-btn" data-bench="quantization"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="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"/></svg>Quantization</button>
3993
+ <button class="bench-panel-btn" data-bench="cost"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>Cost</button>
3994
+ <button class="bench-panel-btn" data-bench="history"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>History</button>
3843
3995
  </div>
3844
3996
 
3845
3997
  <!-- ── Latency Panel ── -->
@@ -3861,7 +4013,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
3861
4013
  <option value="10">10</option>
3862
4014
  </select>
3863
4015
  </div>
3864
- <button class="btn" id="benchLatencyBtn" onclick="doBenchLatency()">⚡ Run Benchmark</button>
4016
+ <button class="btn" id="benchLatencyBtn" onclick="doBenchLatency()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>Run Benchmark</button>
3865
4017
  </div>
3866
4018
  </div>
3867
4019
 
@@ -3915,7 +4067,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
3915
4067
  <option value="8">8</option>
3916
4068
  </select>
3917
4069
  </div>
3918
- <button class="btn" id="benchRankBtn" onclick="doBenchRanking()">🏆 Compare Rankings</button>
4070
+ <button class="btn" id="benchRankBtn" onclick="doBenchRanking()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>Compare Rankings</button>
3919
4071
  </div>
3920
4072
  </div>
3921
4073
 
@@ -3991,7 +4143,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
3991
4143
 
3992
4144
  <!-- Cost Comparison -->
3993
4145
  <div class="card" style="margin-top:16px;">
3994
- <div class="card-title">💰 Cost per Million Tokens</div>
4146
+ <div class="card-title"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>Cost per Million Tokens</div>
3995
4147
  <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
3996
4148
  Voyage AI offers significant cost savings, especially with asymmetric retrieval strategies.
3997
4149
  </p>
@@ -4152,7 +4304,7 @@ Approximate nearest neighbor algorithms like HNSW enable fast similarity search
4152
4304
  Reranking models rescore initial search results to improve relevance ordering.</textarea>
4153
4305
  </div>
4154
4306
  <div style="margin-top:12px;">
4155
- <button class="btn" id="quantBtn" onclick="doBenchQuantization()">⚗️ Run Quantization Benchmark</button>
4307
+ <button class="btn" id="quantBtn" onclick="doBenchQuantization()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="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"/></svg>Run Quantization Benchmark</button>
4156
4308
  </div>
4157
4309
  </div>
4158
4310
 
@@ -4180,7 +4332,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4180
4332
  <!-- ── Cost Panel ── -->
4181
4333
  <div class="bench-view" id="bench-cost">
4182
4334
  <div class="card">
4183
- <div class="card-title">💰 RAG Cost Calculator <button class="cost-help-btn" id="costHelpBtn" title="How the math works">?</button></div>
4335
+ <div class="card-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>RAG Cost Calculator <button class="cost-help-btn" id="costHelpBtn" title="How the math works">?</button></div>
4184
4336
 
4185
4337
  <!-- Mode toggle -->
4186
4338
  <div style="margin-bottom: 20px;">
@@ -4301,7 +4453,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4301
4453
  <div class="history-empty">No benchmarks recorded yet. Run a latency benchmark to start tracking.</div>
4302
4454
  </div>
4303
4455
  <div style="margin-top:12px;text-align:right;">
4304
- <button class="btn btn-secondary btn-small" onclick="clearHistory()">🗑 Clear History</button>
4456
+ <button class="btn btn-secondary btn-small" onclick="clearHistory()"><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:-1px;margin-right:3px;"><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>Clear History</button>
4305
4457
  </div>
4306
4458
  </div>
4307
4459
  </div>
@@ -4318,7 +4470,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4318
4470
  <span id="chatStatusDb">No database</span>
4319
4471
  </div>
4320
4472
  <button class="chat-config-toggle" id="chatOpenSettings" onclick="openChatSettings()" title="Configure in Settings">
4321
- <svg width="14" height="14" viewBox="0 0 16 16"><use href="#lg-config"/></svg>
4473
+ <svg width="14" height="14" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
4322
4474
  Configure
4323
4475
  </button>
4324
4476
  </div>
@@ -4342,11 +4494,15 @@ Reranking models rescore initial search results to improve relevance ordering.</
4342
4494
  <div class="wf-container">
4343
4495
  <div class="wf-library">
4344
4496
  <div class="wf-library-header">
4345
- <span>Workflows</span>
4497
+ <div class="wf-library-tabs">
4498
+ <button class="wf-lib-tab active" data-lib-tab="library" onclick="wfSwitchLibTab('library')">Library</button>
4499
+ <button class="wf-lib-tab" data-lib-tab="palette" onclick="wfSwitchLibTab('palette')">Palette</button>
4500
+ </div>
4346
4501
  </div>
4347
4502
  <div class="wf-library-list" id="wfLibraryList">
4348
4503
  <div style="padding: 16px; color: var(--text-muted); font-size: 12px;">Loading...</div>
4349
4504
  </div>
4505
+ <div class="wf-palette-list" id="wfPaletteList" style="display:none; flex:1; overflow-y:auto; padding:8px;"></div>
4350
4506
  <div class="wf-library-footer">
4351
4507
  <button class="wf-load-file-btn" onclick="wfLoadFromFile()" title="Load workflow JSON from file">
4352
4508
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 14h10M8 2v9M5 8l3 3 3-3"/></svg>
@@ -4357,6 +4513,9 @@ Reranking models rescore initial search results to improve relevance ordering.</
4357
4513
  </div>
4358
4514
  <div class="wf-canvas-area">
4359
4515
  <div class="wf-canvas-toolbar" id="wfToolbar">
4516
+ <button class="wf-new-btn" onclick="wfNewWorkflow()" title="Create new workflow">+ New</button>
4517
+ <button class="wf-edit-btn" id="wfEditBtn" onclick="wfEditWorkflow()" disabled title="Edit current workflow">&#9998; Edit</button>
4518
+ <span class="wf-toolbar-sep"></span>
4360
4519
  <button onclick="wfZoom(1)" title="Zoom in">+</button>
4361
4520
  <button onclick="wfZoom(-1)" title="Zoom out">&minus;</button>
4362
4521
  <button onclick="wfFitToView()" title="Fit to view">&#8862;</button>
@@ -4377,10 +4536,11 @@ Reranking models rescore initial search results to improve relevance ordering.</
4377
4536
  <span class="wf-exec-status-time" id="wfExecStatusTime"></span>
4378
4537
  </div>
4379
4538
  <div class="wf-canvas-empty" id="wfCanvasEmpty">
4380
- <div class="wf-canvas-empty-icon">&#9881;</div>
4539
+ <div class="wf-canvas-empty-icon"><img src="/icons/watermark.png" alt="Vai"></div>
4381
4540
  <div class="wf-canvas-empty-text">Select a workflow from the library</div>
4382
4541
  </div>
4383
- <svg id="wf-canvas" xmlns="http://www.w3.org/2000/svg"></svg>
4542
+ <div class="wf-canvas-watermark" id="wfCanvasWatermark"><img src="/icons/watermark.png" alt=""></div>
4543
+ <svg id="wf-canvas" xmlns="http://www.w3.org/2000/svg" ondragover="event.preventDefault()" ondrop="wfCanvasDrop(event)"></svg>
4384
4544
  </div>
4385
4545
  <div class="wf-inspector collapsed" id="wfInspector">
4386
4546
  <button class="wf-inspector-toggle" id="wfInspectorToggle" onclick="wfToggleInspector()" title="Toggle inspector">&lsaquo;</button>
@@ -4411,6 +4571,21 @@ Reranking models rescore initial search results to improve relevance ordering.</
4411
4571
  </div>
4412
4572
  </div>
4413
4573
 
4574
+ <!-- ── Workflow Input Modal (pre-execution) ── -->
4575
+ <div class="wf-input-modal-backdrop" id="wfInputModalBackdrop" style="display:none;" onclick="wfCloseInputModal()">
4576
+ <div class="wf-input-modal" onclick="event.stopPropagation()">
4577
+ <div class="wf-input-modal-header">
4578
+ <span class="wf-input-modal-title" id="wfInputModalTitle">Workflow Inputs</span>
4579
+ <button class="wf-output-modal-btn close" onclick="wfCloseInputModal()" title="Close">&times;</button>
4580
+ </div>
4581
+ <div class="wf-input-modal-body" id="wfInputModalBody"></div>
4582
+ <div class="wf-input-modal-footer">
4583
+ <button class="wf-input-modal-cancel" onclick="wfCloseInputModal()">Cancel</button>
4584
+ <button class="wf-input-modal-run" onclick="wfInputModalSubmit()">Run Workflow</button>
4585
+ </div>
4586
+ </div>
4587
+ </div>
4588
+
4414
4589
  <!-- ========== ABOUT TAB ========== -->
4415
4590
  <div class="tab-panel" id="tab-about" role="tabpanel" aria-labelledby="tab-btn-about" tabindex="0">
4416
4591
  <div class="about-container">
@@ -4421,9 +4596,9 @@ Reranking models rescore initial search results to improve relevance ordering.</
4421
4596
  <div class="about-name">Michael Lynn</div>
4422
4597
  <div class="about-role">Principal Staff Developer Advocate · MongoDB</div>
4423
4598
  <div class="about-links">
4424
- <a href="https://github.com/mrlynn" target="_blank" rel="noopener">🔗 GitHub</a>
4425
- <a href="https://mlynn.org" target="_blank" rel="noopener">🌐 mlynn.org</a>
4426
- <a href="https://www.npmjs.com/package/voyageai-cli" target="_blank" rel="noopener">📦 npm</a>
4599
+ <a href="https://github.com/mrlynn" target="_blank" rel="noopener"><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:-1px;margin-right:3px;"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>GitHub</a>
4600
+ <a href="https://mlynn.org" target="_blank" rel="noopener"><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:-1px;margin-right:3px;"><circle cx="12" cy="12" r="10"/><path d="M2 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"/></svg>mlynn.org</a>
4601
+ <a href="https://www.npmjs.com/package/voyageai-cli" target="_blank" rel="noopener"><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:-1px;margin-right:3px;"><path d="M16.5 9.4l-9-5.19M21 16V8a2 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 16zM3.27 6.96 12 12.01l8.73-5.05M12 22.08V12"/></svg>npm</a>
4427
4602
  </div>
4428
4603
  </div>
4429
4604
  </div>
@@ -4519,11 +4694,11 @@ Reranking models rescore initial search results to improve relevance ordering.</
4519
4694
  <!-- Mode Toggle -->
4520
4695
  <div class="subtabs" role="tablist">
4521
4696
  <button class="subtab active" id="genModeCode" onclick="setGenerateMode('code')" role="tab" aria-selected="true">
4522
- <svg viewBox="0 0 16 16"><path fill="currentColor" d="M5.854 4.854a.5.5 0 1 0-.708-.708l-3.5 3.5a.5.5 0 0 0 0 .708l3.5 3.5a.5.5 0 0 0 .708-.708L2.707 8l3.147-3.146zm4.292 0a.5.5 0 0 1 .708-.708l3.5 3.5a.5.5 0 0 1 0 .708l-3.5 3.5a.5.5 0 0 1-.708-.708L13.293 8l-3.147-3.146z"/></svg>
4697
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
4523
4698
  Generate Code
4524
4699
  </button>
4525
4700
  <button class="subtab" id="genModeScaffold" onclick="setGenerateMode('scaffold')" role="tab" aria-selected="false">
4526
- <svg viewBox="0 0 16 16"><path fill="currentColor" d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h13zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13z"/><path fill="currentColor" d="M5 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 5 8zm0-2.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0 5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-1-5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zM4 8a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm0 2.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"/></svg>
4701
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 13h4"/><path d="M10 17h4"/><path d="M10 9h1"/></svg>
4527
4702
  Scaffold Project
4528
4703
  </button>
4529
4704
  </div>
@@ -4665,7 +4840,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4665
4840
  </div>
4666
4841
  <div id="scaffoldWebButtons" style="display:none;">
4667
4842
  <button class="btn btn-primary" onclick="downloadScaffoldZip()" id="scaffoldDownloadBtn">
4668
- <svg width="16" height="16" viewBox="0 0 16 16" style="margin-right:6px;"><path fill="currentColor" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path fill="currentColor" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg>
4843
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
4669
4844
  Download ZIP
4670
4845
  </button>
4671
4846
  </div>
@@ -4722,31 +4897,31 @@ Reranking models rescore initial search results to improve relevance ordering.</
4722
4897
  <nav class="settings-nav">
4723
4898
  <div class="settings-nav-header">Settings</div>
4724
4899
  <button class="settings-nav-item active" data-settings-section="general">
4725
- <svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-config"/></svg>
4900
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
4726
4901
  <span>General</span>
4727
4902
  </button>
4728
4903
  <button class="settings-nav-item" data-settings-section="appearance">
4729
- <svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-palette"/></svg>
4904
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-palette"/></svg>
4730
4905
  <span>Appearance</span>
4731
4906
  </button>
4732
4907
  <button class="settings-nav-item" data-settings-section="models">
4733
- <svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-cube"/></svg>
4908
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-cube"/></svg>
4734
4909
  <span>Models</span>
4735
4910
  </button>
4736
4911
  <button class="settings-nav-item" data-settings-section="chat">
4737
- <svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-chat"/></svg>
4912
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-chat"/></svg>
4738
4913
  <span>Chat</span>
4739
4914
  </button>
4740
4915
  <button class="settings-nav-item" data-settings-section="benchmark">
4741
- <svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-gauge"/></svg>
4916
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-gauge"/></svg>
4742
4917
  <span>Benchmark</span>
4743
4918
  </button>
4744
4919
  <button class="settings-nav-item" data-settings-section="privacy">
4745
- <svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-shield"/></svg>
4920
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-shield"/></svg>
4746
4921
  <span>Data &amp; Privacy</span>
4747
4922
  </button>
4748
4923
  <button class="settings-nav-item" data-settings-section="health">
4749
- <svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-pulse"/></svg>
4924
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-pulse"/></svg>
4750
4925
  <span>Health Check</span>
4751
4926
  </button>
4752
4927
  </nav>
@@ -4768,12 +4943,12 @@ Reranking models rescore initial search results to improve relevance ordering.</
4768
4943
  <div class="settings-row">
4769
4944
  <div class="settings-label">
4770
4945
  <span class="settings-label-text">API Key <span class="settings-origin" data-origin-key="apiKey"></span></span>
4771
- <span class="settings-label-hint">Encrypted via OS keychain · <a href="https://dash.voyageai.com" target="_blank" class="settings-key-link">🔑 Get a key</a></span>
4946
+ <span class="settings-label-hint">Encrypted via OS keychain · <a href="https://dash.voyageai.com" target="_blank" class="settings-key-link"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:2px;"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/></svg>Get a key</a></span>
4772
4947
  </div>
4773
4948
  <div class="settings-control" style="min-width:260px;">
4774
4949
  <div class="settings-api-field">
4775
4950
  <input type="password" id="settingsApiKey" placeholder="pa-..." autocomplete="off" spellcheck="false">
4776
- <button type="button" id="settingsApiKeyToggle" title="Show/hide key">👁</button>
4951
+ <button type="button" id="settingsApiKeyToggle" title="Show/hide key"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg></button>
4777
4952
  <button type="button" id="settingsApiKeySave" class="save-btn" title="Save key">Save</button>
4778
4953
  </div>
4779
4954
  </div>
@@ -4927,7 +5102,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4927
5102
  title="Show/hide API key"
4928
5103
  style="padding:8px 12px;min-width:auto"
4929
5104
  >
4930
- 👁️
5105
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
4931
5106
  </button>
4932
5107
  <button
4933
5108
  class="btn"
@@ -4936,7 +5111,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4936
5111
  title="Save API key"
4937
5112
  style="padding:8px 12px;min-width:auto"
4938
5113
  >
4939
- 💾
5114
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg>
4940
5115
  </button>
4941
5116
  </div>
4942
5117
  </div>
@@ -5952,12 +6127,12 @@ async function downloadScaffoldZip() {
5952
6127
  // Show success
5953
6128
  btn.innerHTML = '<span style="margin-right:6px;">✓</span> Downloaded!';
5954
6129
  setTimeout(() => {
5955
- btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" style="margin-right:6px;"><path fill="currentColor" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path fill="currentColor" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg> Download ZIP';
6130
+ btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Download ZIP';
5956
6131
  btn.disabled = false;
5957
6132
  }, 2000);
5958
6133
  } catch (err) {
5959
6134
  alert('Error: ' + err.message);
5960
- btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" style="margin-right:6px;"><path fill="currentColor" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path fill="currentColor" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg> Download ZIP';
6135
+ btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Download ZIP';
5961
6136
  btn.disabled = false;
5962
6137
  }
5963
6138
  }
@@ -6053,37 +6228,73 @@ document.addEventListener('DOMContentLoaded', () => {
6053
6228
  });
6054
6229
 
6055
6230
  // ── Explore: icons and tab mappings per concept ──
6231
+ // Lucide SVG icon helper — returns an inline <svg> string
6232
+ function lucideIcon(d, size = 16) {
6233
+ 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="${d}"/></svg>`;
6234
+ }
6235
+
6236
+ // Lucide path constants for concept/button icons
6237
+ const LI = {
6238
+ zap: 'M13 2 3 14h9l-1 8 10-12h-9l1-8z',
6239
+ search: 'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
6240
+ trophy: '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',
6241
+ bot: 'M12 8V4H8M8 2h8M2 14a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zM6 14v4M10 14v4M14 14v4M18 14v4',
6242
+ ruler: 'M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z M15 5l4 4',
6243
+ target: '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',
6244
+ tag: 'M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42zM7.5 7.5m-.5 0a.5.5 0 1 0 1 0 .5.5 0 1 0-1 0',
6245
+ brain: 'M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2zM14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z',
6246
+ key: 'M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4zM16.5 7.5m-.5 0a.5.5 0 1 0 1 0 .5.5 0 1 0-1 0',
6247
+ globe: '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',
6248
+ package: 'M16.5 9.4l-9-5.19M21 16V8a2 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 16zM3.27 6.96 12 12.01l8.73-5.05M12 22.08V12',
6249
+ timer: 'M10 2h4M12 14l3-3M12 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
6250
+ flask: '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',
6251
+ puzzle: 'M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.077.877.528 1.073 1.01a2.5 2.5 0 1 0 3.259-3.259c-.482-.196-.933-.558-1.01-1.073-.05-.336.062-.676.303-.917l1.525-1.525A2.402 2.402 0 0 1 12 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.878.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.968 1.02z',
6252
+ link: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',
6253
+ barChart: 'M12 20V10M18 20V4M6 20v-4',
6254
+ microscope: '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',
6255
+ image: 'M3 3h18a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM21 15l-5-5L5 21',
6256
+ shuffle: 'M2 18h1.4c1.3 0 2.5-.6 3.3-1.7l6.1-8.6c.7-1.1 2-1.7 3.3-1.7H22M18 2l4 4-4 4M2 6h1.9c1.5 0 2.9.9 3.6 2.2M22 18l-4 4-4-4M19 14h3',
6257
+ circle: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0',
6258
+ fileTxt: 'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7zM14 2v4a2 2 0 0 0 2 2h4M10 13h4M10 17h4M10 9h1',
6259
+ scale: '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',
6260
+ laptop: 'M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9M2 20h20M12 12h.01',
6261
+ blocks: 'M2 12h10v10H2zM14 4l6 3.5v7L14 18l-6-3.5v-7zM12 2l10 6',
6262
+ refresh: 'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16',
6263
+ sparkle: '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',
6264
+ download: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3',
6265
+ copy: 'M9 9V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-3M3 15a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3z',
6266
+ filter: 'M3 6h18M7 12h10M10 18h4',
6267
+ };
6268
+
6056
6269
  const CONCEPT_META = {
6057
- embeddings: { icon: '🧮', tab: 'embed' },
6058
- reranking: { icon: '🏆', tab: 'search' },
6059
- 'vector-search': { icon: '🔎', tab: 'search' },
6060
- rag: { icon: '🤖', tab: 'search' },
6061
- 'cosine-similarity': { icon: '📐', tab: 'compare' },
6062
- 'two-stage-retrieval': { icon: '🎯', tab: 'search' },
6063
- 'input-type': { icon: '🏷️', tab: 'embed' },
6064
- models: { icon: '🧠', tab: 'embed' },
6065
- 'api-keys': { icon: '🔑', tab: 'embed' },
6066
- 'api-access': { icon: '🌐', tab: 'embed' },
6067
- 'batch-processing': { icon: '📦', tab: 'embed' },
6068
- benchmarking: { icon: '⏱', tab: 'benchmark' },
6069
- quantization: { icon: '⚗️', tab: 'benchmark' },
6070
- 'mixture-of-experts': { icon: '🧩', tab: 'embed' },
6071
- 'shared-embedding-space': { icon: '🔗', tab: 'compare' },
6072
- 'rteb-benchmarks': { icon: '📊', tab: 'benchmark' },
6073
- 'voyage-4-nano': { icon: '🔬', tab: 'embed' },
6074
- 'rerank-eval': { icon: '📐', tab: 'benchmark' },
6075
- 'multimodal-embeddings': { icon: '🖼️', tab: 'multimodal' },
6076
- 'cross-modal-search': { icon: '🔀', tab: 'multimodal' },
6077
- 'modality-gap': { icon: '🕳️', tab: 'multimodal' },
6078
- 'multimodal-rag': { icon: '📄', tab: 'multimodal' },
6079
- 'provider-comparison': { icon: '⚖️', tab: 'explore' },
6080
- // Code generation & scaffolding
6081
- 'code-generation': { icon: '💻', tab: 'explore' },
6082
- scaffolding: { icon: '🏗️', tab: 'explore' },
6083
- 'eval-comparison': { icon: '📊', tab: 'benchmark' },
6084
- // MongoDB Auto-Embedding
6085
- 'auto-embedding': { icon: '⚡', tab: 'explore' },
6086
- 'vai-vs-auto-embedding': { icon: '🔄', tab: 'explore' },
6270
+ embeddings: { icon: LI.zap, tab: 'embed' },
6271
+ reranking: { icon: LI.trophy, tab: 'search' },
6272
+ 'vector-search': { icon: LI.search, tab: 'search' },
6273
+ rag: { icon: LI.bot, tab: 'search' },
6274
+ 'cosine-similarity': { icon: LI.ruler, tab: 'compare' },
6275
+ 'two-stage-retrieval': { icon: LI.target, tab: 'search' },
6276
+ 'input-type': { icon: LI.tag, tab: 'embed' },
6277
+ models: { icon: LI.brain, tab: 'embed' },
6278
+ 'api-keys': { icon: LI.key, tab: 'embed' },
6279
+ 'api-access': { icon: LI.globe, tab: 'embed' },
6280
+ 'batch-processing': { icon: LI.package, tab: 'embed' },
6281
+ benchmarking: { icon: LI.timer, tab: 'benchmark' },
6282
+ quantization: { icon: LI.flask, tab: 'benchmark' },
6283
+ 'mixture-of-experts': { icon: LI.puzzle, tab: 'embed' },
6284
+ 'shared-embedding-space': { icon: LI.link, tab: 'compare' },
6285
+ 'rteb-benchmarks': { icon: LI.barChart, tab: 'benchmark' },
6286
+ 'voyage-4-nano': { icon: LI.microscope, tab: 'embed' },
6287
+ 'rerank-eval': { icon: LI.ruler, tab: 'benchmark' },
6288
+ 'multimodal-embeddings': { icon: LI.image, tab: 'multimodal' },
6289
+ 'cross-modal-search': { icon: LI.shuffle, tab: 'multimodal' },
6290
+ 'modality-gap': { icon: LI.circle, tab: 'multimodal' },
6291
+ 'multimodal-rag': { icon: LI.fileTxt, tab: 'multimodal' },
6292
+ 'provider-comparison': { icon: LI.scale, tab: 'explore' },
6293
+ 'code-generation': { icon: LI.laptop, tab: 'explore' },
6294
+ scaffolding: { icon: LI.blocks, tab: 'explore' },
6295
+ 'eval-comparison': { icon: LI.barChart, tab: 'benchmark' },
6296
+ 'auto-embedding': { icon: LI.zap, tab: 'explore' },
6297
+ 'vai-vs-auto-embedding': { icon: LI.refresh, tab: 'explore' },
6087
6298
  };
6088
6299
 
6089
6300
  let exploreConcepts = {};
@@ -6109,13 +6320,13 @@ function buildExploreCards() {
6109
6320
  grid.innerHTML = '';
6110
6321
 
6111
6322
  for (const [key, concept] of Object.entries(exploreConcepts)) {
6112
- const meta = CONCEPT_META[key] || { icon: '📚', tab: 'embed' };
6323
+ const meta = CONCEPT_META[key] || { icon: LI.package, tab: 'embed' };
6113
6324
  const card = document.createElement('div');
6114
6325
  card.className = 'explore-card';
6115
6326
  card.dataset.key = key;
6116
6327
 
6117
6328
  card.innerHTML = `
6118
- <div class="explore-card-icon">${meta.icon}</div>
6329
+ <div class="explore-card-icon">${lucideIcon(meta.icon, 28)}</div>
6119
6330
  <div class="explore-card-title">${escapeHtml(concept.title)}</div>
6120
6331
  <div class="explore-card-summary">${escapeHtml(concept.summary)}</div>
6121
6332
  `;
@@ -6142,12 +6353,12 @@ let exploreModalPreviousFocus = null;
6142
6353
  function openExploreModal(key) {
6143
6354
  const concept = exploreConcepts[key];
6144
6355
  if (!concept) return;
6145
- const meta = CONCEPT_META[key] || { icon: '📚', tab: 'embed' };
6356
+ const meta = CONCEPT_META[key] || { icon: LI.package, tab: 'embed' };
6146
6357
 
6147
6358
  // Save the currently focused element to restore when modal closes
6148
6359
  exploreModalPreviousFocus = document.activeElement;
6149
6360
 
6150
- document.getElementById('exploreModalIcon').textContent = meta.icon;
6361
+ document.getElementById('exploreModalIcon').innerHTML = lucideIcon(meta.icon, 32);
6151
6362
  document.getElementById('exploreModalTitle').textContent = concept.title;
6152
6363
  document.getElementById('exploreModalSummary').textContent = concept.summary;
6153
6364
 
@@ -9790,19 +10001,26 @@ const WF_NODE_META = {
9790
10001
  search: { icon: '\u{1F50E}', label: 'Vector Search', color: '#40E0FF', category: 'retrieval' },
9791
10002
  rerank: { icon: '\u{1F3C6}', label: 'Rerank', color: '#40E0FF', category: 'retrieval' },
9792
10003
  ingest: { icon: '\u{1F4E5}', label: 'Ingest', color: '#40E0FF', category: 'retrieval' },
9793
- embed: { icon: '\u{1F4D0}', label: 'Embed', color: '#B388FF', category: 'embedding' },
9794
- similarity: { icon: '\u{1F517}', label: 'Similarity', color: '#B388FF', category: 'embedding' },
9795
- collections: { icon: '\u{1F5C4}', label: 'Collections', color: '#00D4AA', category: 'management' },
9796
- models: { icon: '\u{1F4CB}', label: 'Models', color: '#00D4AA', category: 'management' },
9797
- estimate: { icon: '\u{1F4B0}', label: 'Cost Estimate', color: '#FFB74D', category: 'utility' },
9798
- explain: { icon: '\u{1F4D6}', label: 'Explain', color: '#FFB74D', category: 'utility' },
9799
- topics: { icon: '\u{1F5C2}', label: 'Topics', color: '#FFB74D', category: 'utility' },
9800
- merge: { icon: '\u{1F500}', label: 'Merge', color: '#90A4AE', category: 'control' },
9801
- filter: { icon: '\u{1F9F9}', label: 'Filter', color: '#90A4AE', category: 'control' },
9802
- transform: { icon: '\u{1F504}', label: 'Transform', color: '#90A4AE', category: 'control' },
9803
- generate: { icon: '\u2728', label: 'Generate', color: '#69F0AE', category: 'generation' },
10004
+ embed: { icon: 'M13 2 3 14h9l-1 8 10-12h-9l1-8z', label: 'Embed', color: '#B388FF', category: 'embedding' },
10005
+ similarity: { icon: 'M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4', label: 'Similarity', color: '#B388FF', category: 'embedding' },
10006
+ collections: { icon: 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z', label: 'Collections', color: '#00D4AA', category: 'management' },
10007
+ models: { icon: 'M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2M9 5h6M9 14l2 2 4-4', label: 'Models', color: '#00D4AA', category: 'management' },
10008
+ estimate: { icon: 'M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6', label: 'Cost Estimate', color: '#FFB74D', category: 'utility' },
10009
+ explain: { icon: 'M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z', label: 'Explain', color: '#FFB74D', category: 'utility' },
10010
+ topics: { icon: 'M12 6V2M8 18H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-4M12 18v4M8 22h8', label: 'Topics', color: '#FFB74D', category: 'utility' },
10011
+ merge: { icon: 'M18 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 9v6M18 15l-6-6-6 6', label: 'Merge', color: '#90A4AE', category: 'control' },
10012
+ filter: { icon: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z', label: 'Filter', color: '#90A4AE', category: 'control' },
10013
+ transform: { icon: 'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16', label: 'Transform', color: '#90A4AE', category: 'control' },
10014
+ generate: { icon: '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', label: 'Generate', color: '#69F0AE', category: 'generation' },
10015
+ query: { icon: 'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', label: 'Query', color: '#64B5F6', category: 'retrieval' },
10016
+ rerank: { icon: 'M3 6h18M7 12h10M10 18h4', label: 'Rerank', color: '#CE93D8', category: 'retrieval' },
10017
+ 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
+ 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' },
9804
10019
  };
9805
10020
 
10021
+ // Fallback icon (gear) for unknown workflow node types
10022
+ const WF_FALLBACK_ICON = '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-2zM12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0';
10023
+
9806
10024
  const WF_NODE_W = 180;
9807
10025
  const WF_NODE_H = 64;
9808
10026
  const WF_LAYER_GAP = 260;
@@ -9812,6 +10030,7 @@ const WF_PORT_R = 5; // Port circle radius
9812
10030
 
9813
10031
  let wfState = {
9814
10032
  workflows: [],
10033
+ examples: [],
9815
10034
  activeWorkflow: null,
9816
10035
  selectedNodeId: null,
9817
10036
  executionState: {},
@@ -9825,14 +10044,24 @@ let wfState = {
9825
10044
  isPanning: false,
9826
10045
  panStart: { x: 0, y: 0 },
9827
10046
  executing: false,
10047
+ // Builder state
10048
+ builderMode: false,
10049
+ draggingEdge: null,
10050
+ dragNode: null,
10051
+ dirtyFlag: false,
9828
10052
  };
9829
10053
 
9830
10054
  // ── Library ──
9831
10055
  async function wfLoadLibrary() {
9832
10056
  try {
9833
- const res = await fetch('/api/workflows');
9834
- const data = await res.json();
9835
- wfState.workflows = data.workflows || [];
10057
+ const [wfRes, exRes] = await Promise.all([
10058
+ fetch('/api/workflows'),
10059
+ fetch('/api/workflows/examples'),
10060
+ ]);
10061
+ const wfData = await wfRes.json();
10062
+ const exData = await exRes.json();
10063
+ wfState.workflows = wfData.workflows || [];
10064
+ wfState.examples = exData.examples || [];
9836
10065
  wfRenderLibrary();
9837
10066
  } catch (err) {
9838
10067
  const list = document.getElementById('wfLibraryList');
@@ -9843,19 +10072,53 @@ async function wfLoadLibrary() {
9843
10072
  function wfRenderLibrary() {
9844
10073
  const list = document.getElementById('wfLibraryList');
9845
10074
  if (!list) return;
9846
- if (wfState.workflows.length === 0) {
10075
+ if (wfState.workflows.length === 0 && (!wfState.examples || wfState.examples.length === 0)) {
9847
10076
  list.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;">No workflows found</div>';
9848
10077
  return;
9849
10078
  }
9850
- list.innerHTML = wfState.workflows.map(w => {
9851
- // w.name is the file stem (e.g., "multi-collection-search")
9852
- // w.description comes from the workflow JSON's description field
10079
+
10080
+ // Built-in templates
10081
+ let html = wfState.workflows.map(w => {
9853
10082
  const displayName = w.name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
9854
10083
  return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
9855
10084
  <div class="wf-library-item-name">${displayName}</div>
9856
10085
  <div class="wf-library-item-desc">${w.description || ''}</div>
9857
10086
  </div>`;
9858
10087
  }).join('');
10088
+
10089
+ // Collapsible examples section
10090
+ const examples = wfState.examples || [];
10091
+ 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>';
10112
+ }
10113
+
10114
+ list.innerHTML = html;
10115
+ }
10116
+
10117
+ function wfToggleExamples(btn) {
10118
+ const content = btn.nextElementSibling;
10119
+ const isOpen = content.style.display !== 'none';
10120
+ content.style.display = isOpen ? 'none' : '';
10121
+ btn.classList.toggle('open', !isOpen);
9859
10122
  }
9860
10123
 
9861
10124
  async function wfSelectWorkflow(name) {
@@ -9868,12 +10131,14 @@ async function wfSelectWorkflow(name) {
9868
10131
  const res = await fetch('/api/workflows/' + encodeURIComponent(name));
9869
10132
  const data = await res.json();
9870
10133
  wfState.activeWorkflow = data.definition;
10134
+ wfState.builderMode = false;
9871
10135
  wfState.selectedNodeId = null;
9872
10136
  wfState.executionState = {};
9873
10137
  wfState.executionResults = {};
9874
10138
  wfSetToolbarEnabled(true);
9875
10139
  document.getElementById('wfCanvasEmpty').style.display = 'none';
9876
10140
  await wfRenderWorkflow(data.definition);
10141
+ wfSwitchLibTab('library');
9877
10142
  wfOpenInspector();
9878
10143
  wfUpdateInspector();
9879
10144
  } catch (err) {
@@ -10024,7 +10289,7 @@ async function wfRenderWorkflow(definition) {
10024
10289
  }
10025
10290
 
10026
10291
  function wfDrawNode(step, x, y, state, hasDeps, hasDependents) {
10027
- const meta = WF_NODE_META[step.tool] || { icon: '\u2699', label: step.tool, color: '#666', category: 'unknown' };
10292
+ const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666', category: 'unknown' };
10028
10293
  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
10029
10294
  g.classList.add('wf-node');
10030
10295
  if (state !== 'idle') g.classList.add('wf-node--' + state);
@@ -10036,6 +10301,39 @@ function wfDrawNode(step, x, y, state, hasDeps, hasDependents) {
10036
10301
  wfSelectNode(step.id);
10037
10302
  });
10038
10303
 
10304
+ // Builder: node drag
10305
+ if (wfState.builderMode) {
10306
+ g.style.cursor = 'move';
10307
+ g.addEventListener('mousedown', (e) => {
10308
+ // Don't drag if clicking a port
10309
+ if (e.target.classList.contains('wf-port')) return;
10310
+ e.stopPropagation();
10311
+ const svg = document.getElementById('wf-canvas');
10312
+ const rect = svg.getBoundingClientRect();
10313
+ const startSvgX = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
10314
+ const startSvgY = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
10315
+ const pos = wfState.nodePositions[step.id];
10316
+ if (!pos) return;
10317
+ const offX = startSvgX - pos.x;
10318
+ const offY = startSvgY - pos.y;
10319
+ wfState.dragNode = step.id;
10320
+
10321
+ function onMove(ev) {
10322
+ const mx = (ev.clientX - rect.left) / wfState.zoom + wfState.panX;
10323
+ const my = (ev.clientY - rect.top) / wfState.zoom + wfState.panY;
10324
+ wfState.nodePositions[step.id] = { x: mx - offX, y: my - offY };
10325
+ wfRefreshNodes();
10326
+ }
10327
+ function onUp() {
10328
+ wfState.dragNode = null;
10329
+ document.removeEventListener('mousemove', onMove);
10330
+ document.removeEventListener('mouseup', onUp);
10331
+ }
10332
+ document.addEventListener('mousemove', onMove);
10333
+ document.addEventListener('mouseup', onUp);
10334
+ });
10335
+ }
10336
+
10039
10337
  // Background rect
10040
10338
  const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
10041
10339
  rect.setAttribute('width', WF_NODE_W);
@@ -10045,37 +10343,66 @@ function wfDrawNode(step, x, y, state, hasDeps, hasDependents) {
10045
10343
  rect.setAttribute('opacity', '0.85');
10046
10344
  g.appendChild(rect);
10047
10345
 
10048
- // Input port (left side): only if this node has dependencies
10049
- if (hasDeps) {
10346
+ // Input port (left side): show in builder mode always, otherwise only if has deps
10347
+ const showInPort = wfState.builderMode || hasDeps;
10348
+ if (showInPort) {
10050
10349
  const inPort = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
10051
10350
  inPort.classList.add('wf-port', 'wf-port-in');
10351
+ if (wfState.builderMode) inPort.classList.add('wf-port-builder');
10052
10352
  inPort.setAttribute('cx', 0);
10053
10353
  inPort.setAttribute('cy', WF_NODE_H / 2);
10054
- inPort.setAttribute('r', WF_PORT_R);
10354
+ inPort.setAttribute('r', wfState.builderMode ? 7 : WF_PORT_R);
10055
10355
  inPort.setAttribute('stroke', meta.color);
10056
10356
  inPort.setAttribute('stroke-width', '2');
10357
+ inPort.setAttribute('pointer-events', 'all');
10358
+ if (wfState.builderMode) {
10359
+ inPort.addEventListener('mouseup', () => {
10360
+ if (wfState.draggingEdge) wfEdgeDropOnInput(step.id);
10361
+ });
10362
+ }
10057
10363
  g.appendChild(inPort);
10058
10364
  }
10059
10365
 
10060
- // Output port (right side): only if other nodes depend on this one
10061
- if (hasDependents) {
10366
+ // Output port (right side): show in builder mode always, otherwise only if has dependents
10367
+ const showOutPort = wfState.builderMode || hasDependents;
10368
+ if (showOutPort) {
10062
10369
  const outPort = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
10063
10370
  outPort.classList.add('wf-port', 'wf-port-out');
10371
+ if (wfState.builderMode) outPort.classList.add('wf-port-builder');
10064
10372
  outPort.setAttribute('cx', WF_NODE_W);
10065
10373
  outPort.setAttribute('cy', WF_NODE_H / 2);
10066
- outPort.setAttribute('r', WF_PORT_R);
10374
+ outPort.setAttribute('r', wfState.builderMode ? 7 : WF_PORT_R);
10067
10375
  outPort.setAttribute('fill', meta.color);
10068
10376
  outPort.setAttribute('stroke-width', '2');
10377
+ outPort.setAttribute('pointer-events', 'all');
10378
+ if (wfState.builderMode) {
10379
+ outPort.addEventListener('mousedown', (e) => {
10380
+ e.stopPropagation();
10381
+ const pos = wfState.nodePositions[step.id];
10382
+ if (!pos) return;
10383
+ wfEdgeDragStart(step.id, pos.x + WF_NODE_W, pos.y + WF_NODE_H / 2);
10384
+ });
10385
+ }
10069
10386
  g.appendChild(outPort);
10070
10387
  }
10071
10388
 
10072
- // Icon
10073
- const icon = document.createElementNS('http://www.w3.org/2000/svg', 'text');
10074
- icon.classList.add('wf-node-icon');
10075
- icon.setAttribute('x', 22);
10076
- icon.setAttribute('y', WF_NODE_H / 2);
10077
- icon.textContent = meta.icon;
10078
- g.appendChild(icon);
10389
+ // Icon (Lucide SVG)
10390
+ const iconG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
10391
+ iconG.classList.add('wf-node-icon');
10392
+ const iconSize = 18;
10393
+ const ix = 22 - iconSize / 2;
10394
+ const iy = WF_NODE_H / 2 - iconSize / 2;
10395
+ iconG.setAttribute('transform', `translate(${ix},${iy}) scale(${iconSize / 24})`);
10396
+ const fullD = meta.icon || WF_FALLBACK_ICON;
10397
+ const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
10398
+ iconPath.setAttribute('d', fullD);
10399
+ iconPath.setAttribute('fill', 'none');
10400
+ iconPath.setAttribute('stroke', 'currentColor');
10401
+ iconPath.setAttribute('stroke-width', '1.75');
10402
+ iconPath.setAttribute('stroke-linecap', 'round');
10403
+ iconPath.setAttribute('stroke-linejoin', 'round');
10404
+ iconG.appendChild(iconPath);
10405
+ g.appendChild(iconG);
10079
10406
 
10080
10407
  // Label (step name, truncated)
10081
10408
  const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
@@ -10242,44 +10569,69 @@ function wfUpdateInspector() {
10242
10569
  header.textContent = def.name || 'Workflow';
10243
10570
  let html = '';
10244
10571
 
10245
- // Description
10246
- if (def.description) {
10572
+ if (wfState.builderMode) {
10573
+ // Builder: editable workflow fields
10574
+ html += `<div class="wf-inspector-section">
10575
+ <div class="wf-inspector-section-title">Name</div>
10576
+ <input class="wf-inspector-input" value="${escapeHtml(def.name || '')}" onchange="wfEditWorkflowField('name', this.value); document.getElementById('wfInspectorHeader').textContent = this.value || 'Workflow';">
10577
+ </div>`;
10247
10578
  html += `<div class="wf-inspector-section">
10248
10579
  <div class="wf-inspector-section-title">Description</div>
10249
- <div style="font-size:12px;color:var(--text);line-height:1.4;">${escapeHtml(def.description)}</div>
10580
+ <textarea class="wf-inspector-input" rows="2" style="resize:vertical;" onchange="wfEditWorkflowField('description', this.value)">${escapeHtml(def.description || '')}</textarea>
10250
10581
  </div>`;
10251
- }
10252
-
10253
- // Inputs
10254
- if (def.inputs && Object.keys(def.inputs).length > 0) {
10255
- html += '<div class="wf-inspector-section"><div class="wf-inspector-section-title">Inputs</div>';
10256
- for (const [key, spec] of Object.entries(def.inputs)) {
10257
- const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
10258
- const defVal = spec.default !== undefined ? ` (default: ${spec.default})` : '';
10259
- html += `<div style="margin-bottom:8px;">
10260
- <div style="font-size:12px;font-weight:600;color:var(--text);">${escapeHtml(key)}${req}</div>
10261
- <div style="font-size:11px;color:var(--text-muted);">${escapeHtml(spec.description || spec.type || '')}${defVal}</div>
10262
- <input class="wf-inspector-input" id="wf-input-${key}" placeholder="${escapeHtml(key)}" value="${spec.default !== undefined ? spec.default : ''}">
10582
+ html += `<div class="wf-inspector-section">
10583
+ <div class="wf-inspector-section-title">Steps</div>
10584
+ <div style="font-size:12px;color:var(--text);">${def.steps.length} step${def.steps.length !== 1 ? 's' : ''}</div>
10585
+ </div>`;
10586
+ if (def.steps.length > 0) {
10587
+ html += `<div class="wf-inspector-section">
10588
+ <div class="wf-inspector-section-title">Output Mapping</div>
10589
+ <textarea class="wf-inspector-input" rows="3" style="resize:vertical;font-family:monospace;font-size:11px;" onchange="try { wfEditWorkflowField('output', JSON.parse(this.value)); } catch(e) {}">${escapeHtml(JSON.stringify(def.output || {}, null, 2))}</textarea>
10590
+ </div>`;
10591
+ }
10592
+ html += `<div class="wf-inspector-section" style="margin-top:8px;">
10593
+ <div style="font-size:10px;color:var(--text-muted);">Add steps from the Palette tab, then drag between ports to connect them.</div>
10594
+ </div>`;
10595
+ } else {
10596
+ // Read-only: Description
10597
+ if (def.description) {
10598
+ html += `<div class="wf-inspector-section">
10599
+ <div class="wf-inspector-section-title">Description</div>
10600
+ <div style="font-size:12px;color:var(--text);line-height:1.4;">${escapeHtml(def.description)}</div>
10263
10601
  </div>`;
10264
10602
  }
10265
- html += '</div>';
10266
- }
10267
10603
 
10268
- // Steps summary
10269
- html += `<div class="wf-inspector-section">
10270
- <div class="wf-inspector-section-title">Steps</div>
10271
- <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>
10272
- </div>`;
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>';
10607
+ for (const [key, spec] of Object.entries(def.inputs)) {
10608
+ 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;">
10611
+ <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 : ''}">
10614
+ </div>`;
10615
+ }
10616
+ html += '</div>';
10617
+ }
10273
10618
 
10274
- // Output mapping
10275
- if (def.output) {
10619
+ // Steps summary
10276
10620
  html += `<div class="wf-inspector-section">
10277
- <div class="wf-inspector-section-title">Output</div>
10278
- <div class="wf-inspector-code">${escapeHtml(JSON.stringify(def.output, null, 2))}</div>
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>
10279
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>
10630
+ </div>`;
10631
+ }
10280
10632
  }
10281
10633
 
10282
- // Execution result
10634
+ // Execution result (shown in both modes)
10283
10635
  if (wfState.executionResults._done) {
10284
10636
  const r = wfState.executionResults._done;
10285
10637
  const doneJson = JSON.stringify(r.output, null, 2);
@@ -10305,54 +10657,109 @@ function wfUpdateInspector() {
10305
10657
  return;
10306
10658
  }
10307
10659
 
10308
- const meta = WF_NODE_META[step.tool] || { icon: '\u2699', label: step.tool, color: '#666' };
10660
+ const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666' };
10309
10661
  header.textContent = step.name || step.id;
10310
10662
 
10311
10663
  let html = '';
10312
10664
 
10313
- // Tool badge
10665
+ // Tool badge (always shown)
10314
10666
  html += `<div class="wf-inspector-section">
10315
10667
  <div class="wf-inspector-section-title">Tool</div>
10316
- <span class="wf-tool-badge" style="background:${meta.color}">${meta.icon} ${meta.label}</span>
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>
10317
10669
  </div>`;
10318
10670
 
10319
- // Step ID
10320
- html += `<div class="wf-inspector-section">
10321
- <div class="wf-inspector-section-title">ID</div>
10322
- <div style="font-size:12px;color:var(--text);font-family:monospace;">${escapeHtml(step.id)}</div>
10323
- </div>`;
10324
-
10325
- // Inputs
10326
- if (step.inputs) {
10671
+ if (wfState.builderMode) {
10672
+ // Builder: editable step fields
10673
+ const sid = escapeHtml(step.id);
10327
10674
  html += `<div class="wf-inspector-section">
10328
- <div class="wf-inspector-section-title">Inputs</div>`;
10329
- for (const [key, val] of Object.entries(step.inputs)) {
10330
- const display = typeof val === 'string' ? val : JSON.stringify(val);
10331
- html += `<div class="wf-inspector-field">
10332
- <span class="wf-inspector-field-label">${escapeHtml(key)}</span>
10333
- <span class="wf-inspector-field-value" style="font-family:monospace;font-size:11px;">${escapeHtml(display)}</span>
10334
- </div>`;
10675
+ <div class="wf-inspector-section-title">Step ID</div>
10676
+ <input class="wf-inspector-input" value="${sid}" style="font-family:monospace;" onchange="wfEditStepId('${sid}', this.value)">
10677
+ </div>`;
10678
+ html += `<div class="wf-inspector-section">
10679
+ <div class="wf-inspector-section-title">Name</div>
10680
+ <input class="wf-inspector-input" value="${escapeHtml(step.name || '')}" onchange="wfEditStepField('${sid}', 'name', this.value)">
10681
+ </div>`;
10682
+
10683
+ // Inputs from WF_INPUT_DEFS
10684
+ const inputDefs = WF_INPUT_DEFS[step.tool] || [];
10685
+ if (inputDefs.length > 0) {
10686
+ html += `<div class="wf-inspector-section"><div class="wf-inspector-section-title">Inputs</div>`;
10687
+ for (const d of inputDefs) {
10688
+ const val = step.inputs?.[d.key] ?? '';
10689
+ const display = typeof val === 'string' ? val : JSON.stringify(val);
10690
+ const req = d.required ? ' <span style="color:#e74c3c;font-size:10px;">required</span>' : '';
10691
+ html += `<div style="margin-bottom:8px;">
10692
+ <div style="font-size:11px;font-weight:600;color:var(--text);">${escapeHtml(d.key)}${req}</div>`;
10693
+
10694
+ if (d.type === 'textarea' || d.type === 'json') {
10695
+ html += `<textarea class="wf-inspector-input" rows="2" style="resize:vertical;font-family:monospace;font-size:11px;" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">${escapeHtml(display)}</textarea>`;
10696
+ } else if (d.type === 'select' && d.options) {
10697
+ html += `<select class="wf-inspector-input" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">
10698
+ <option value="">--</option>`;
10699
+ for (const opt of d.options) {
10700
+ html += `<option value="${escapeHtml(opt)}" ${val === opt ? 'selected' : ''}>${escapeHtml(opt)}</option>`;
10701
+ }
10702
+ html += `</select>`;
10703
+ } else if (d.type === 'number') {
10704
+ html += `<input class="wf-inspector-input" type="number" value="${escapeHtml(String(val))}" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">`;
10705
+ } else {
10706
+ html += `<input class="wf-inspector-input" value="${escapeHtml(display)}" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">`;
10707
+ }
10708
+ html += `</div>`;
10709
+ }
10710
+ html += '</div>';
10335
10711
  }
10336
- html += '</div>';
10337
- }
10338
10712
 
10339
- // Condition
10340
- if (step.condition) {
10713
+ // Condition
10341
10714
  html += `<div class="wf-inspector-section">
10342
- <div class="wf-inspector-section-title">Condition \u26A1</div>
10343
- <div class="wf-inspector-code">${escapeHtml(step.condition)}</div>
10715
+ <div class="wf-inspector-section-title">Condition <span style="font-size:10px;color:var(--text-muted)">(optional)</span></div>
10716
+ <input class="wf-inspector-input" value="${escapeHtml(step.condition || '')}" placeholder="e.g. results.length > 0" onchange="wfEditStepField('${sid}', 'condition', this.value || undefined)">
10344
10717
  </div>`;
10345
- }
10346
10718
 
10347
- // ForEach
10348
- if (step.forEach) {
10719
+ // continueOnError toggle
10720
+ html += `<div class="wf-inspector-section" style="display:flex;align-items:center;gap:8px;">
10721
+ <input type="checkbox" id="wf-coe-${sid}" ${step.continueOnError ? 'checked' : ''} onchange="wfEditStepField('${sid}', 'continueOnError', this.checked)">
10722
+ <label for="wf-coe-${sid}" style="font-size:11px;color:var(--text);cursor:pointer;">Continue on error</label>
10723
+ </div>`;
10724
+
10725
+ // Delete button
10726
+ html += `<button class="wf-inspector-delete-btn" onclick="if(confirm('Delete step ${sid}?')) wfDeleteStep('${sid}')">Delete Step</button>`;
10727
+ } else {
10728
+ // Read-only step view
10349
10729
  html += `<div class="wf-inspector-section">
10350
- <div class="wf-inspector-section-title">ForEach</div>
10351
- <div class="wf-inspector-code">${escapeHtml(JSON.stringify(step.forEach, null, 2))}</div>
10730
+ <div class="wf-inspector-section-title">ID</div>
10731
+ <div style="font-size:12px;color:var(--text);font-family:monospace;">${escapeHtml(step.id)}</div>
10352
10732
  </div>`;
10733
+
10734
+ if (step.inputs) {
10735
+ html += `<div class="wf-inspector-section">
10736
+ <div class="wf-inspector-section-title">Inputs</div>`;
10737
+ for (const [key, val] of Object.entries(step.inputs)) {
10738
+ const display = typeof val === 'string' ? val : JSON.stringify(val);
10739
+ html += `<div class="wf-inspector-field">
10740
+ <span class="wf-inspector-field-label">${escapeHtml(key)}</span>
10741
+ <span class="wf-inspector-field-value" style="font-family:monospace;font-size:11px;">${escapeHtml(display)}</span>
10742
+ </div>`;
10743
+ }
10744
+ html += '</div>';
10745
+ }
10746
+
10747
+ if (step.condition) {
10748
+ html += `<div class="wf-inspector-section">
10749
+ <div class="wf-inspector-section-title">Condition \u26A1</div>
10750
+ <div class="wf-inspector-code">${escapeHtml(step.condition)}</div>
10751
+ </div>`;
10752
+ }
10753
+
10754
+ if (step.forEach) {
10755
+ html += `<div class="wf-inspector-section">
10756
+ <div class="wf-inspector-section-title">ForEach</div>
10757
+ <div class="wf-inspector-code">${escapeHtml(JSON.stringify(step.forEach, null, 2))}</div>
10758
+ </div>`;
10759
+ }
10353
10760
  }
10354
10761
 
10355
- // Execution result for this step
10762
+ // Execution result for this step (shown in both modes)
10356
10763
  const result = wfState.executionResults[step.id];
10357
10764
  const state = wfState.executionState[step.id];
10358
10765
  if (state === 'completed' && result) {
@@ -10416,6 +10823,7 @@ function wfSetToolbarEnabled(enabled) {
10416
10823
  document.getElementById('wfRunBtn').disabled = !enabled;
10417
10824
  document.getElementById('wfDryRunBtn').disabled = !enabled;
10418
10825
  document.getElementById('wfExportBtn').disabled = !enabled;
10826
+ document.getElementById('wfEditBtn').disabled = !enabled;
10419
10827
  }
10420
10828
 
10421
10829
  // ── Export workflow JSON ──
@@ -10461,9 +10869,9 @@ function wfDryRun() {
10461
10869
  const step = stepMap[stepId];
10462
10870
  if (!step) return;
10463
10871
  totalSteps++;
10464
- const meta = WF_NODE_META[step.tool] || { icon: '\u2699', label: step.tool, color: '#666' };
10872
+ const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666' };
10465
10873
  layersHtml += '<div class="wf-dryrun-step">';
10466
- layersHtml += '<span class="wf-dryrun-step-icon">' + meta.icon + '</span>';
10874
+ layersHtml += '<span class="wf-dryrun-step-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="' + (meta.icon || WF_FALLBACK_ICON) + '"/></svg></span>';
10467
10875
  layersHtml += '<div class="wf-dryrun-step-info">';
10468
10876
  layersHtml += '<div class="wf-dryrun-step-name">' + escapeHtml(step.name || step.id) + '</div>';
10469
10877
  layersHtml += '<div class="wf-dryrun-step-tool">' + escapeHtml(meta.label) + ' (' + escapeHtml(step.id) + ')</div>';
@@ -10601,27 +11009,128 @@ function wfStopExecution(reason) {
10601
11009
  wfUpdateInspector();
10602
11010
  }
10603
11011
 
10604
- async function wfExecute() {
11012
+ // ── Input Modal (pre-execution) ──
11013
+
11014
+ function wfShowInputModal() {
10605
11015
  const def = wfState.activeWorkflow;
10606
- if (!def || wfState.executing) return;
11016
+ if (!def || !def.inputs) return;
11017
+ const entries = Object.entries(def.inputs);
11018
+ if (entries.length === 0) { wfExecuteWithInputs({}); return; }
11019
+
11020
+ document.getElementById('wfInputModalTitle').textContent = (def.name || 'Workflow') + ' Inputs';
11021
+ let html = '';
11022
+ for (const [key, spec] of entries) {
11023
+ const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
11024
+ const desc = spec.description ? `<div class="wf-input-modal-desc">${escapeHtml(spec.description)}</div>` : '';
11025
+ // Pre-fill from inspector fields if available, then from defaults
11026
+ const inspectorEl = document.getElementById('wf-input-' + key);
11027
+ let prefill = inspectorEl ? inspectorEl.value : '';
11028
+ if (!prefill && spec.default !== undefined) prefill = String(spec.default);
11029
+ const placeholder = spec.type === 'number' ? 'number' : (spec.type || 'string');
11030
+ html += `<div class="wf-input-modal-field">
11031
+ <div class="wf-input-modal-label">${escapeHtml(key)}${req}</div>
11032
+ ${desc}
11033
+ <input class="wf-input-modal-input" id="wf-modal-input-${key}" placeholder="${escapeHtml(placeholder)}" value="${escapeHtml(prefill)}" data-key="${escapeHtml(key)}" data-type="${spec.type || 'string'}" data-required="${!!spec.required}">
11034
+ <div class="wf-input-modal-error" id="wf-modal-err-${key}">This field is required</div>
11035
+ </div>`;
11036
+ }
11037
+ document.getElementById('wfInputModalBody').innerHTML = html;
11038
+ document.getElementById('wfInputModalBackdrop').style.display = '';
11039
+
11040
+ // Focus first empty required field, or first field
11041
+ const firstEmpty = entries.find(([k, s]) => {
11042
+ const el = document.getElementById('wf-modal-input-' + k);
11043
+ return s.required && el && !el.value;
11044
+ });
11045
+ const focusKey = firstEmpty ? firstEmpty[0] : entries[0][0];
11046
+ const focusEl = document.getElementById('wf-modal-input-' + focusKey);
11047
+ if (focusEl) setTimeout(() => focusEl.focus(), 50);
11048
+ }
11049
+
11050
+ function wfCloseInputModal() {
11051
+ document.getElementById('wfInputModalBackdrop').style.display = 'none';
11052
+ }
11053
+
11054
+ function wfInputModalSubmit() {
11055
+ const def = wfState.activeWorkflow;
11056
+ if (!def || !def.inputs) return;
10607
11057
 
10608
- // Collect inputs
10609
11058
  const inputs = {};
10610
- if (def.inputs) {
10611
- for (const [key, spec] of Object.entries(def.inputs)) {
10612
- const el = document.getElementById('wf-input-' + key);
10613
- let val = el ? el.value : (spec.default !== undefined ? spec.default : undefined);
10614
- if (val === '' && spec.default !== undefined) val = spec.default;
10615
- if (val === '' && spec.required) {
10616
- alert('Input "' + key + '" is required');
10617
- return;
10618
- }
10619
- // Type coerce
10620
- if (spec.type === 'number' && val !== undefined && val !== '') val = Number(val);
10621
- if (val !== undefined && val !== '') inputs[key] = val;
11059
+ let hasError = false;
11060
+
11061
+ for (const [key, spec] of Object.entries(def.inputs)) {
11062
+ const el = document.getElementById('wf-modal-input-' + key);
11063
+ const errEl = document.getElementById('wf-modal-err-' + key);
11064
+ if (!el) continue;
11065
+
11066
+ let val = el.value.trim();
11067
+
11068
+ // Clear previous error
11069
+ el.classList.remove('error');
11070
+ if (errEl) errEl.style.display = 'none';
11071
+
11072
+ // Required check
11073
+ if (!val && spec.required && spec.default === undefined) {
11074
+ el.classList.add('error');
11075
+ if (errEl) { errEl.textContent = 'This field is required'; errEl.style.display = ''; }
11076
+ hasError = true;
11077
+ continue;
11078
+ }
11079
+
11080
+ // Use default if empty
11081
+ if (!val && spec.default !== undefined) val = String(spec.default);
11082
+
11083
+ // Type validation
11084
+ if (spec.type === 'number' && val && isNaN(Number(val))) {
11085
+ el.classList.add('error');
11086
+ if (errEl) { errEl.textContent = 'Must be a number'; errEl.style.display = ''; }
11087
+ hasError = true;
11088
+ continue;
10622
11089
  }
11090
+
11091
+ // Coerce
11092
+ if (spec.type === 'number' && val) val = Number(val);
11093
+ if (val !== undefined && val !== '') inputs[key] = val;
11094
+ }
11095
+
11096
+ if (hasError) return;
11097
+
11098
+ // Also update inspector fields to keep them in sync
11099
+ for (const [key, val] of Object.entries(inputs)) {
11100
+ const inspEl = document.getElementById('wf-input-' + key);
11101
+ if (inspEl) inspEl.value = typeof val === 'number' ? String(val) : val;
11102
+ }
11103
+
11104
+ wfCloseInputModal();
11105
+ wfExecuteWithInputs(inputs);
11106
+ }
11107
+
11108
+ // Keyboard handler for input modal
11109
+ document.addEventListener('keydown', (e) => {
11110
+ const backdrop = document.getElementById('wfInputModalBackdrop');
11111
+ if (!backdrop || backdrop.style.display === 'none') return;
11112
+ if (e.key === 'Escape') { wfCloseInputModal(); e.preventDefault(); }
11113
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { wfInputModalSubmit(); e.preventDefault(); }
11114
+ });
11115
+
11116
+ async function wfExecute() {
11117
+ const def = wfState.activeWorkflow;
11118
+ if (!def || wfState.executing) return;
11119
+
11120
+ // If workflow has inputs, show the input modal instead of executing directly
11121
+ if (def.inputs && Object.keys(def.inputs).length > 0) {
11122
+ wfShowInputModal();
11123
+ return;
10623
11124
  }
10624
11125
 
11126
+ // No inputs needed, execute directly
11127
+ wfExecuteWithInputs({});
11128
+ }
11129
+
11130
+ async function wfExecuteWithInputs(inputs) {
11131
+ const def = wfState.activeWorkflow;
11132
+ if (!def || wfState.executing) return;
11133
+
10625
11134
  wfState.executing = true;
10626
11135
  wfState.executionResults = {};
10627
11136
  wfAbortController = new AbortController();
@@ -10907,7 +11416,7 @@ function wfInitPan() {
10907
11416
  // Scroll wheel zoom, centered on cursor position
10908
11417
  svg.addEventListener('wheel', (e) => {
10909
11418
  e.preventDefault();
10910
- const factor = e.deltaY < 0 ? 1.08 : 1 / 1.08;
11419
+ const factor = e.deltaY < 0 ? 1.03 : 1 / 1.03;
10911
11420
  const oldZoom = wfState.zoom;
10912
11421
  const newZoom = Math.max(0.2, Math.min(5, oldZoom * factor));
10913
11422
  // Zoom toward cursor: keep the SVG point under the mouse fixed
@@ -10928,6 +11437,446 @@ function wfInitPan() {
10928
11437
  });
10929
11438
  }
10930
11439
 
11440
+ // ── Builder: Input Definitions ──
11441
+ const WF_INPUT_DEFS = {
11442
+ query: [{ key: 'query', type: 'text', required: true, placeholder: 'Search query' }, { key: 'collection', type: 'text', required: false, placeholder: 'Collection name' }, { key: 'db', type: 'text', required: false, placeholder: 'Database name' }, { key: 'limit', type: 'number', required: false, placeholder: '5' }, { key: 'filter', type: 'json', required: false, placeholder: '{}' }],
11443
+ search: [{ key: 'query', type: 'text', required: true, placeholder: 'Search query' }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }, { key: 'limit', type: 'number', required: false, placeholder: '10' }, { key: 'filter', type: 'json', required: false, placeholder: '{}' }],
11444
+ rerank: [{ key: 'query', type: 'text', required: true }, { key: 'documents', type: 'json', required: true, placeholder: '["doc1","doc2"]' }, { key: 'model', type: 'text', required: false, placeholder: 'rerank-2.5' }],
11445
+ ingest: [{ key: 'text', type: 'textarea', required: true }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }, { key: 'source', type: 'text', required: false }, { key: 'chunkSize', type: 'number', required: false, placeholder: '512' }, { key: 'chunkStrategy', type: 'select', required: false, options: ['fixed','sentence','paragraph','recursive','markdown'] }],
11446
+ embed: [{ key: 'text', type: 'text', required: true, placeholder: 'Text to embed' }, { key: 'model', type: 'text', required: false, placeholder: 'voyage-3-large' }, { key: 'inputType', type: 'select', required: false, options: ['document','query'] }],
11447
+ similarity: [{ key: 'text1', type: 'text', required: true }, { key: 'text2', type: 'text', required: true }, { key: 'model', type: 'text', required: false }],
11448
+ collections: [{ key: 'db', type: 'text', required: false }],
11449
+ models: [{ key: 'category', type: 'select', required: false, options: ['embedding','rerank','all'] }],
11450
+ estimate: [{ key: 'docs', type: 'number', required: true, placeholder: '1000' }, { key: 'queries', type: 'number', required: false, placeholder: '0' }, { key: 'months', type: 'number', required: false, placeholder: '12' }],
11451
+ explain: [{ key: 'topic', type: 'text', required: true }],
11452
+ topics: [{ key: 'search', type: 'text', required: false }],
11453
+ merge: [{ key: 'sources', type: 'json', required: true, placeholder: '["step1.output","step2.output"]' }, { key: 'strategy', type: 'select', required: false, options: ['concat','interleave','unique'] }],
11454
+ filter: [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'condition', type: 'text', required: true, placeholder: 'item.score > 0.5' }],
11455
+ transform: [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'expression', type: 'text', required: true, placeholder: 'item.text' }],
11456
+ generate: [{ key: 'prompt', type: 'textarea', required: true, placeholder: 'Generate a summary of...' }, { key: 'context', type: 'text', required: false, placeholder: '{{ step.output }}' }],
11457
+ };
11458
+
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' };
11461
+
11462
+ // ── Builder: Library/Palette tab toggle ──
11463
+ function wfSwitchLibTab(tab) {
11464
+ const libraryList = document.getElementById('wfLibraryList');
11465
+ const paletteList = document.getElementById('wfPaletteList');
11466
+ document.querySelectorAll('.wf-lib-tab').forEach(b => b.classList.toggle('active', b.dataset.libTab === tab));
11467
+ if (libraryList) libraryList.style.display = tab === 'library' ? '' : 'none';
11468
+ if (paletteList) paletteList.style.display = tab === 'palette' ? '' : 'none';
11469
+ if (tab === 'palette') wfRenderPalette();
11470
+ }
11471
+
11472
+ // ── Builder: Palette rendering ──
11473
+ function wfRenderPalette() {
11474
+ const container = document.getElementById('wfPaletteList');
11475
+ if (!container) return;
11476
+ const grouped = {};
11477
+ for (const [tool, meta] of Object.entries(WF_NODE_META)) {
11478
+ const cat = meta.category || 'unknown';
11479
+ if (!grouped[cat]) grouped[cat] = [];
11480
+ grouped[cat].push({ tool, ...meta });
11481
+ }
11482
+ let html = '';
11483
+ for (const cat of WF_CATEGORY_ORDER) {
11484
+ const items = grouped[cat];
11485
+ if (!items) continue;
11486
+ html += `<div class="wf-palette-category"><div class="wf-palette-category-title">${WF_CATEGORY_LABELS[cat] || cat}</div>`;
11487
+ for (const item of items) {
11488
+ html += `<div class="wf-palette-item" draggable="true" ondragstart="event.dataTransfer.setData('text/plain','${item.tool}')" onclick="wfAddNodeFromPalette('${item.tool}')">
11489
+ <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
+ <span class="wf-palette-label">${item.label}</span>
11491
+ </div>`;
11492
+ }
11493
+ html += '</div>';
11494
+ }
11495
+ container.innerHTML = html;
11496
+ }
11497
+
11498
+ // ── Builder: New Workflow ──
11499
+ function wfNewWorkflow() {
11500
+ wfState.activeWorkflow = {
11501
+ name: 'Untitled Workflow',
11502
+ description: '',
11503
+ steps: [],
11504
+ inputs: {},
11505
+ defaults: {},
11506
+ output: {}
11507
+ };
11508
+ wfState.builderMode = true;
11509
+ wfState.dirtyFlag = false;
11510
+ wfState.selectedNodeId = null;
11511
+ wfState.executionState = {};
11512
+ wfState.executionResults = {};
11513
+ wfState.nodePositions = {};
11514
+ wfState.layers = [];
11515
+ wfState.graph = {};
11516
+ wfState.zoom = 1;
11517
+ wfState.panX = 0;
11518
+ wfState.panY = 0;
11519
+ wfState.draggingEdge = null;
11520
+ wfState.dragNode = null;
11521
+
11522
+ // Clear canvas
11523
+ const svg = document.getElementById('wf-canvas');
11524
+ if (svg) svg.querySelectorAll('.wf-node, .wf-edge-group').forEach(el => el.remove());
11525
+ document.getElementById('wfCanvasEmpty').style.display = 'none';
11526
+ svg.style.display = '';
11527
+
11528
+ // Enable toolbar buttons
11529
+ document.getElementById('wfDryRunBtn').disabled = false;
11530
+ document.getElementById('wfRunBtn').disabled = false;
11531
+ document.getElementById('wfExportBtn').disabled = false;
11532
+
11533
+ // Switch to palette tab
11534
+ wfSwitchLibTab('palette');
11535
+
11536
+ // Open inspector for workflow-level editing
11537
+ wfOpenInspector();
11538
+ wfUpdateInspector();
11539
+
11540
+ // Set viewBox
11541
+ wfApplyViewBox();
11542
+ }
11543
+
11544
+ // ── Builder: Edit current workflow ──
11545
+ function wfEditWorkflow() {
11546
+ if (!wfState.activeWorkflow) return;
11547
+ wfState.builderMode = true;
11548
+ wfState.dirtyFlag = false;
11549
+ wfState.selectedNodeId = null;
11550
+ wfState.draggingEdge = null;
11551
+ wfState.dragNode = null;
11552
+
11553
+ // Re-render nodes with builder ports and drag handles
11554
+ wfRefreshNodes();
11555
+
11556
+ // Switch to palette tab and open inspector for workflow-level editing
11557
+ wfSwitchLibTab('palette');
11558
+ wfOpenInspector();
11559
+ wfUpdateInspector();
11560
+ }
11561
+
11562
+ // ── Builder: Add node from palette ──
11563
+ function wfAddNodeFromPalette(tool) {
11564
+ if (!wfState.activeWorkflow) wfNewWorkflow();
11565
+ wfState.builderMode = true;
11566
+ const def = wfState.activeWorkflow;
11567
+
11568
+ // Generate unique step ID
11569
+ let baseId = tool;
11570
+ let id = baseId;
11571
+ let counter = 2;
11572
+ const existingIds = new Set(def.steps.map(s => s.id));
11573
+ while (existingIds.has(id)) { id = baseId + '_' + counter; counter++; }
11574
+
11575
+ // Build default inputs
11576
+ const inputDefs = WF_INPUT_DEFS[tool] || [];
11577
+ const inputs = {};
11578
+ for (const d of inputDefs) {
11579
+ if (d.required) inputs[d.key] = '';
11580
+ }
11581
+
11582
+ const meta = WF_NODE_META[tool] || {};
11583
+ const step = {
11584
+ id,
11585
+ name: meta.label || tool,
11586
+ tool,
11587
+ inputs,
11588
+ };
11589
+ def.steps.push(step);
11590
+
11591
+ // Position: place to the right of all existing nodes
11592
+ let maxX = WF_PAD;
11593
+ for (const pos of Object.values(wfState.nodePositions)) {
11594
+ if (pos.x + WF_NODE_W + WF_LAYER_GAP > maxX) maxX = pos.x + WF_NODE_W + WF_LAYER_GAP;
11595
+ }
11596
+ let y = WF_PAD;
11597
+ // Stack vertically if there are nodes in the same column
11598
+ const nodesAtX = Object.values(wfState.nodePositions).filter(p => Math.abs(p.x - maxX) < 20);
11599
+ if (nodesAtX.length > 0) {
11600
+ y = Math.max(...nodesAtX.map(p => p.y)) + WF_NODE_H + WF_NODE_GAP;
11601
+ }
11602
+ wfState.nodePositions[id] = { x: maxX, y };
11603
+
11604
+ // Rebuild graph
11605
+ wfBuildGraph();
11606
+ wfRefreshNodes();
11607
+ wfSelectNode(id);
11608
+ wfState.dirtyFlag = true;
11609
+ }
11610
+
11611
+ // ── Builder: Build graph from step inputs (template references) ──
11612
+ function wfBuildGraph() {
11613
+ const def = wfState.activeWorkflow;
11614
+ if (!def) return;
11615
+ const graph = {};
11616
+ const stepIds = new Set(def.steps.map(s => s.id));
11617
+ for (const step of def.steps) {
11618
+ const deps = new Set();
11619
+ // Scan all input values for {{ stepId.xxx }} references
11620
+ for (const val of Object.values(step.inputs || {})) {
11621
+ const str = typeof val === 'string' ? val : JSON.stringify(val);
11622
+ const matches = str.matchAll(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\./g);
11623
+ for (const m of matches) {
11624
+ if (stepIds.has(m[1]) && m[1] !== step.id) deps.add(m[1]);
11625
+ }
11626
+ }
11627
+ graph[step.id] = Array.from(deps);
11628
+ }
11629
+ wfState.graph = graph;
11630
+ }
11631
+
11632
+ // ── Builder: Canvas drop (from palette drag) ──
11633
+ function wfCanvasDrop(e) {
11634
+ e.preventDefault();
11635
+ const tool = e.dataTransfer.getData('text/plain');
11636
+ if (!tool || !WF_NODE_META[tool]) return;
11637
+ if (!wfState.activeWorkflow) wfNewWorkflow();
11638
+ wfState.builderMode = true;
11639
+ const def = wfState.activeWorkflow;
11640
+
11641
+ // Generate unique ID
11642
+ let id = tool;
11643
+ let counter = 2;
11644
+ const existingIds = new Set(def.steps.map(s => s.id));
11645
+ while (existingIds.has(id)) { id = tool + '_' + counter; counter++; }
11646
+
11647
+ // Inputs
11648
+ const inputDefs = WF_INPUT_DEFS[tool] || [];
11649
+ const inputs = {};
11650
+ for (const d of inputDefs) { if (d.required) inputs[d.key] = ''; }
11651
+
11652
+ const meta = WF_NODE_META[tool] || {};
11653
+ const step = { id, name: meta.label || tool, tool, inputs };
11654
+ def.steps.push(step);
11655
+
11656
+ // Convert drop coordinates to SVG space
11657
+ const svg = document.getElementById('wf-canvas');
11658
+ const rect = svg.getBoundingClientRect();
11659
+ const svgX = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
11660
+ const svgY = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
11661
+ wfState.nodePositions[id] = { x: svgX, y: svgY };
11662
+
11663
+ wfBuildGraph();
11664
+ wfRefreshNodes();
11665
+ wfSelectNode(id);
11666
+ wfState.dirtyFlag = true;
11667
+ }
11668
+
11669
+ // ── Builder: Mutation helpers ──
11670
+ function wfEditStepField(stepId, field, value) {
11671
+ const step = wfState.activeWorkflow?.steps.find(s => s.id === stepId);
11672
+ if (!step) return;
11673
+ step[field] = value;
11674
+ wfState.dirtyFlag = true;
11675
+ if (field === 'name') wfRefreshNodes();
11676
+ }
11677
+
11678
+ function wfEditStepInput(stepId, key, value) {
11679
+ const step = wfState.activeWorkflow?.steps.find(s => s.id === stepId);
11680
+ if (!step) return;
11681
+ if (!step.inputs) step.inputs = {};
11682
+ step.inputs[key] = value;
11683
+ wfState.dirtyFlag = true;
11684
+ // Rebuild graph in case template refs changed
11685
+ wfBuildGraph();
11686
+ wfRefreshNodes();
11687
+ }
11688
+
11689
+ function wfEditStepId(oldId, newId) {
11690
+ const def = wfState.activeWorkflow;
11691
+ if (!def) return;
11692
+ newId = newId.trim().replace(/[^a-zA-Z0-9_]/g, '_');
11693
+ if (!newId || newId === oldId) return;
11694
+ if (def.steps.some(s => s.id === newId)) return; // duplicate
11695
+
11696
+ const step = def.steps.find(s => s.id === oldId);
11697
+ if (!step) return;
11698
+ step.id = newId;
11699
+
11700
+ // Update position map
11701
+ if (wfState.nodePositions[oldId]) {
11702
+ wfState.nodePositions[newId] = wfState.nodePositions[oldId];
11703
+ delete wfState.nodePositions[oldId];
11704
+ }
11705
+ // Update template references in other steps
11706
+ for (const s of def.steps) {
11707
+ for (const [k, v] of Object.entries(s.inputs || {})) {
11708
+ if (typeof v === 'string' && v.includes('{{ ' + oldId + '.')) {
11709
+ s.inputs[k] = v.replaceAll('{{ ' + oldId + '.', '{{ ' + newId + '.');
11710
+ }
11711
+ }
11712
+ }
11713
+ if (wfState.selectedNodeId === oldId) wfState.selectedNodeId = newId;
11714
+ wfState.dirtyFlag = true;
11715
+ wfBuildGraph();
11716
+ wfRefreshNodes();
11717
+ wfUpdateInspector();
11718
+ }
11719
+
11720
+ function wfDeleteStep(stepId) {
11721
+ const def = wfState.activeWorkflow;
11722
+ if (!def) return;
11723
+ def.steps = def.steps.filter(s => s.id !== stepId);
11724
+ delete wfState.nodePositions[stepId];
11725
+ if (wfState.selectedNodeId === stepId) wfState.selectedNodeId = null;
11726
+ wfState.dirtyFlag = true;
11727
+ wfBuildGraph();
11728
+ wfRefreshNodes();
11729
+ wfUpdateInspector();
11730
+ }
11731
+
11732
+ function wfEditWorkflowField(field, value) {
11733
+ if (!wfState.activeWorkflow) return;
11734
+ wfState.activeWorkflow[field] = value;
11735
+ wfState.dirtyFlag = true;
11736
+ }
11737
+
11738
+ // ── Builder: Validate ──
11739
+ async function wfValidateBuilder() {
11740
+ const def = wfState.activeWorkflow;
11741
+ if (!def) return null;
11742
+ try {
11743
+ const res = await fetch('/api/workflows/validate', {
11744
+ method: 'POST',
11745
+ headers: { 'Content-Type': 'application/json' },
11746
+ body: JSON.stringify(def),
11747
+ });
11748
+ return await res.json();
11749
+ } catch (err) {
11750
+ return { valid: false, errors: [err.message] };
11751
+ }
11752
+ }
11753
+
11754
+ // ── Builder: Edge drag ──
11755
+ function wfEdgeDragStart(fromId, fromX, fromY) {
11756
+ const svg = document.getElementById('wf-canvas');
11757
+ if (!svg) return;
11758
+ wfState.draggingEdge = { fromId, fromX, fromY };
11759
+
11760
+ // Create temp edge (dashed bezier), pointer-events: none so it doesn't block port hit-testing
11761
+ const tempPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
11762
+ tempPath.setAttribute('id', 'wf-temp-edge');
11763
+ tempPath.setAttribute('fill', 'none');
11764
+ tempPath.setAttribute('stroke', 'var(--accent)');
11765
+ tempPath.setAttribute('stroke-width', '2');
11766
+ tempPath.setAttribute('stroke-dasharray', '6 4');
11767
+ tempPath.setAttribute('pointer-events', 'none');
11768
+ tempPath.setAttribute('d', `M${fromX},${fromY} L${fromX},${fromY}`);
11769
+ svg.appendChild(tempPath);
11770
+
11771
+ function onMove(e) {
11772
+ const rect = svg.getBoundingClientRect();
11773
+ const mx = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
11774
+ const my = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
11775
+ const dx = Math.abs(mx - fromX) * 0.5;
11776
+ tempPath.setAttribute('d', `M${fromX},${fromY} C${fromX + dx},${fromY} ${mx - dx},${my} ${mx},${my}`);
11777
+ }
11778
+
11779
+ function onUp() {
11780
+ document.removeEventListener('mousemove', onMove);
11781
+ document.removeEventListener('mouseup', onUp);
11782
+ const el = document.getElementById('wf-temp-edge');
11783
+ if (el) el.remove();
11784
+ wfState.draggingEdge = null;
11785
+ }
11786
+
11787
+ document.addEventListener('mousemove', onMove);
11788
+ document.addEventListener('mouseup', onUp);
11789
+ }
11790
+
11791
+ function wfEdgeDropOnInput(toId) {
11792
+ if (!wfState.draggingEdge) return;
11793
+ const fromId = wfState.draggingEdge.fromId;
11794
+ if (fromId === toId) return; // no self-connections
11795
+
11796
+ // Add template reference to the target step's first empty required input
11797
+ const def = wfState.activeWorkflow;
11798
+ if (!def) return;
11799
+ const targetStep = def.steps.find(s => s.id === toId);
11800
+ if (!targetStep) return;
11801
+
11802
+ const inputDefs = WF_INPUT_DEFS[targetStep.tool] || [];
11803
+ let connected = false;
11804
+
11805
+ // Try to fill the first empty required input with a template reference
11806
+ for (const d of inputDefs) {
11807
+ if (!targetStep.inputs) targetStep.inputs = {};
11808
+ const current = targetStep.inputs[d.key];
11809
+ if (!current || current === '') {
11810
+ targetStep.inputs[d.key] = `{{ ${fromId}.output }}`;
11811
+ connected = true;
11812
+ break;
11813
+ }
11814
+ }
11815
+
11816
+ // If no empty required input, try first empty optional input
11817
+ if (!connected) {
11818
+ for (const d of inputDefs) {
11819
+ const current = targetStep.inputs?.[d.key];
11820
+ if (!current || current === '') {
11821
+ if (!targetStep.inputs) targetStep.inputs = {};
11822
+ targetStep.inputs[d.key] = `{{ ${fromId}.output }}`;
11823
+ connected = true;
11824
+ break;
11825
+ }
11826
+ }
11827
+ }
11828
+
11829
+ if (connected) {
11830
+ wfState.dirtyFlag = true;
11831
+ wfBuildGraph();
11832
+ wfRelayout();
11833
+ wfRefreshNodes();
11834
+ if (wfState.selectedNodeId === toId) wfUpdateInspector();
11835
+ }
11836
+
11837
+ // Clean up drag state
11838
+ wfState.draggingEdge = null;
11839
+ const el = document.getElementById('wf-temp-edge');
11840
+ if (el) el.remove();
11841
+ }
11842
+
11843
+ // ── Builder: Relayout via topological sort ──
11844
+ async function wfRelayout() {
11845
+ const def = wfState.activeWorkflow;
11846
+ if (!def || def.steps.length === 0) return;
11847
+
11848
+ try {
11849
+ const res = await fetch('/api/workflows/plan', {
11850
+ method: 'POST',
11851
+ headers: { 'Content-Type': 'application/json' },
11852
+ body: JSON.stringify(def),
11853
+ });
11854
+ const data = await res.json();
11855
+ if (data.layers && data.layers.length > 0) {
11856
+ wfState.layers = data.layers;
11857
+ // Reposition nodes based on layers
11858
+ const positions = {};
11859
+ data.layers.forEach((layer, li) => {
11860
+ layer.forEach((stepId, ni) => {
11861
+ positions[stepId] = {
11862
+ x: WF_PAD + li * WF_LAYER_GAP,
11863
+ y: WF_PAD + ni * (WF_NODE_H + WF_NODE_GAP),
11864
+ };
11865
+ });
11866
+ });
11867
+ // Keep orphan nodes (not in any layer) at their current position
11868
+ for (const step of def.steps) {
11869
+ if (!positions[step.id] && wfState.nodePositions[step.id]) {
11870
+ positions[step.id] = wfState.nodePositions[step.id];
11871
+ }
11872
+ }
11873
+ wfState.nodePositions = positions;
11874
+ }
11875
+ } catch (err) {
11876
+ console.warn('Relayout failed:', err.message);
11877
+ }
11878
+ }
11879
+
10931
11880
  // ── Docs shortcut (F1) ──
10932
11881
  const DOCS_URLS = {
10933
11882
  embed: 'https://docs.vaicli.com/docs/commands/embeddings/embed',
@@ -10956,6 +11905,22 @@ document.addEventListener('keydown', (e) => {
10956
11905
  document.addEventListener('keydown', (e) => {
10957
11906
  const activeTab = document.querySelector('.tab-btn.active');
10958
11907
  if (!activeTab || activeTab.dataset.tab !== 'workflows') return;
11908
+
11909
+ // Ctrl/Cmd+S to save/export in builder mode (works even in inputs)
11910
+ if ((e.ctrlKey || e.metaKey) && e.key === 's' && wfState.builderMode) {
11911
+ e.preventDefault();
11912
+ wfExportJson();
11913
+ return;
11914
+ }
11915
+
11916
+ // Delete/Backspace to remove selected node in builder mode
11917
+ if ((e.key === 'Delete' || e.key === 'Backspace') && wfState.builderMode && wfState.selectedNodeId) {
11918
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
11919
+ e.preventDefault();
11920
+ wfDeleteStep(wfState.selectedNodeId);
11921
+ return;
11922
+ }
11923
+
10959
11924
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
10960
11925
 
10961
11926
  const PAN_STEP = 40;