voyageai-cli 1.27.0 → 1.29.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;
@@ -1778,6 +1779,43 @@ select:focus { outline: none; border-color: var(--accent); }
1778
1779
  margin-bottom: 8px;
1779
1780
  }
1780
1781
  .about-text { font-size: 14px; line-height: 1.8; color: var(--text); }
1782
+ .about-changelog { font-size: 13px; line-height: 1.6; }
1783
+ .about-changelog details {
1784
+ border-bottom: 1px solid rgba(255,255,255,0.06);
1785
+ padding: 0;
1786
+ }
1787
+ .about-changelog details:last-child { border-bottom: none; }
1788
+ .about-changelog summary {
1789
+ padding: 10px 0;
1790
+ cursor: pointer;
1791
+ color: var(--text);
1792
+ list-style: none;
1793
+ display: flex;
1794
+ align-items: center;
1795
+ gap: 8px;
1796
+ }
1797
+ .about-changelog summary::before {
1798
+ content: '';
1799
+ display: inline-block;
1800
+ width: 5px; height: 5px;
1801
+ border-right: 1.5px solid var(--accent, #00D4AA);
1802
+ border-bottom: 1.5px solid var(--accent, #00D4AA);
1803
+ transform: rotate(-45deg);
1804
+ transition: transform 0.15s ease;
1805
+ flex-shrink: 0;
1806
+ }
1807
+ .about-changelog details[open] > summary::before {
1808
+ transform: rotate(45deg);
1809
+ }
1810
+ .about-changelog summary::-webkit-details-marker { display: none; }
1811
+ .about-changelog summary:hover { color: var(--accent-text); }
1812
+ .about-changelog p {
1813
+ margin: 0 0 10px 13px;
1814
+ color: var(--text-muted);
1815
+ font-size: 12.5px;
1816
+ line-height: 1.6;
1817
+ }
1818
+ [data-theme="light"] .about-changelog details { border-color: rgba(0,0,0,0.08); }
1781
1819
  .about-text a { color: var(--blue); text-decoration: none; }
1782
1820
  .about-text a:hover { text-decoration: underline; }
1783
1821
  .about-disclaimer {
@@ -3030,6 +3068,24 @@ select:focus { outline: none; border-color: var(--accent); }
3030
3068
  font-size: 10px; color: var(--text-muted); margin-top: 4px;
3031
3069
  display: flex; gap: 8px;
3032
3070
  }
3071
+ /* Collapsible examples section */
3072
+ .wf-library-section { padding: 0; margin-top: 4px; }
3073
+ .wf-library-section-toggle {
3074
+ display: flex; align-items: center; gap: 6px;
3075
+ width: 100%; padding: 8px 12px; border: none; background: none;
3076
+ color: var(--text-muted); font-size: 11px; font-weight: 600;
3077
+ text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer;
3078
+ transition: color 0.15s;
3079
+ }
3080
+ .wf-library-section-toggle:hover { color: var(--text); }
3081
+ .wf-library-section-toggle .arrow {
3082
+ font-size: 8px; transition: transform 0.2s; display: inline-block;
3083
+ }
3084
+ .wf-library-section-toggle.open .arrow { transform: rotate(90deg); }
3085
+ .wf-library-category {
3086
+ font-size: 10px; font-weight: 600; color: var(--text-muted);
3087
+ padding: 8px 12px 2px; text-transform: uppercase; letter-spacing: 0.5px;
3088
+ }
3033
3089
  .wf-canvas-area {
3034
3090
  flex: 1; position: relative; overflow: hidden;
3035
3091
  background: var(--bg);
@@ -3060,9 +3116,263 @@ select:focus { outline: none; border-color: var(--accent); }
3060
3116
  }
3061
3117
  .wf-canvas-toolbar .wf-run-btn:hover { opacity: 0.9; }
3062
3118
  .wf-canvas-toolbar .wf-run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
3119
+ .wf-canvas-toolbar .wf-new-btn {
3120
+ width: auto; padding: 0 12px; gap: 4px;
3121
+ font-weight: 600; font-size: 12px;
3122
+ background: var(--bg-card); border: 1px solid var(--accent);
3123
+ color: var(--accent);
3124
+ }
3125
+ .wf-canvas-toolbar .wf-new-btn:hover { background: var(--accent); color: #fff; }
3126
+ .wf-canvas-toolbar .wf-edit-btn {
3127
+ width: auto; padding: 0 12px; gap: 4px;
3128
+ font-weight: 600; font-size: 12px;
3129
+ background: var(--bg-card); border: 1px solid var(--border);
3130
+ color: var(--text-muted);
3131
+ }
3132
+ .wf-canvas-toolbar .wf-edit-btn:hover { border-color: var(--accent); color: var(--accent); }
3133
+ .wf-canvas-toolbar .wf-edit-btn:disabled { opacity: 0.4; cursor: not-allowed; }
3134
+ .wf-canvas-toolbar .wf-edit-btn:disabled:hover { border-color: var(--border); color: var(--text-muted); }
3063
3135
  .wf-toolbar-sep {
3064
3136
  width: 1px; height: 20px; background: var(--border); margin: 0 2px;
3065
3137
  }
3138
+ /* Library / Palette tabs */
3139
+ .wf-library-tabs {
3140
+ display: flex; gap: 2px; width: 100%;
3141
+ background: var(--bg-input); border-radius: 6px; padding: 2px;
3142
+ }
3143
+ .wf-lib-tab {
3144
+ flex: 1; padding: 5px 0; border: none; border-radius: 4px;
3145
+ background: transparent; color: var(--text-muted); cursor: pointer;
3146
+ font-size: 11px; font-weight: 600; font-family: var(--font);
3147
+ transition: background 0.15s, color 0.15s; text-align: center;
3148
+ }
3149
+ .wf-lib-tab.active { background: var(--accent); color: #fff; }
3150
+ .wf-lib-tab:hover:not(.active) { color: var(--text); }
3151
+ /* Palette items */
3152
+ .wf-palette-category { margin-bottom: 8px; }
3153
+ .wf-palette-category-title {
3154
+ font-size: 10px; font-weight: 700; text-transform: uppercase;
3155
+ letter-spacing: 0.5px; color: var(--text-muted);
3156
+ padding: 4px 12px; margin-bottom: 2px;
3157
+ }
3158
+ .wf-palette-item {
3159
+ display: flex; align-items: center; gap: 8px;
3160
+ padding: 7px 12px; border-radius: 6px; cursor: grab;
3161
+ font-size: 12px; color: var(--text); transition: background 0.15s;
3162
+ }
3163
+ .wf-palette-item:hover { background: var(--bg-card); }
3164
+ .wf-palette-item:active { cursor: grabbing; }
3165
+ .wf-palette-icon { width: 20px; text-align: center; display: flex; align-items: center; justify-content: center; }
3166
+ .wf-palette-label { font-weight: 500; }
3167
+
3168
+ /* ── Workflow Store ── */
3169
+ .wf-store-overlay {
3170
+ position: absolute; inset: 0; background: var(--bg); z-index: 50;
3171
+ display: flex; flex-direction: column;
3172
+ opacity: 0; transform: scale(0.98); pointer-events: none;
3173
+ transition: opacity 0.2s ease, transform 0.2s ease;
3174
+ }
3175
+ .wf-store-overlay.open {
3176
+ opacity: 1; transform: scale(1); pointer-events: all;
3177
+ }
3178
+ .wf-store-overlay::before {
3179
+ content: ''; position: absolute; top: -150px; left: 50%; transform: translateX(-50%);
3180
+ width: 700px; height: 500px;
3181
+ background: radial-gradient(ellipse, rgba(0,212,170,.05) 0%, rgba(64,224,255,.02) 40%, transparent 70%);
3182
+ pointer-events: none; z-index: 0;
3183
+ }
3184
+ .wf-store-header {
3185
+ padding: 14px 20px; border-bottom: 1px solid var(--border);
3186
+ display: flex; align-items: center; gap: 14px; position: relative; z-index: 1; flex-shrink: 0;
3187
+ }
3188
+ .wf-store-back {
3189
+ display: flex; align-items: center; gap: 6px; background: none; border: none;
3190
+ color: var(--text-muted); font-family: var(--font); font-size: 13px; cursor: pointer;
3191
+ padding: 6px 10px; border-radius: 6px; transition: all 0.15s;
3192
+ }
3193
+ .wf-store-back:hover { background: var(--bg-surface); color: var(--accent); }
3194
+ .wf-store-title-area { display: flex; align-items: center; gap: 8px; flex: 1; }
3195
+ .wf-store-title { font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--text); letter-spacing: 0.02em; }
3196
+ .wf-store-badge { font-family: var(--mono); font-size: 9px; padding: 3px 8px; background: rgba(0,212,170,.15); border: 1px solid rgba(0,212,170,.2); border-radius: 100px; color: var(--accent); letter-spacing: 0.04em; text-transform: uppercase; }
3197
+ .wf-store-search-wrap { position: relative; width: 220px; }
3198
+ .wf-store-search {
3199
+ width: 100%; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px;
3200
+ padding: 8px 12px 8px 30px; font-family: var(--font); font-size: 13px; color: var(--text); outline: none;
3201
+ transition: border-color 0.15s;
3202
+ }
3203
+ .wf-store-search:focus { border-color: var(--accent); }
3204
+ .wf-store-search::placeholder { color: var(--text-muted); }
3205
+ .wf-store-search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 13px; pointer-events: none; }
3206
+ .wf-store-sort {
3207
+ background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px;
3208
+ padding: 8px 10px; font-family: var(--font); font-size: 12px; color: var(--text-muted); outline: none; cursor: pointer;
3209
+ }
3210
+ .wf-store-body { flex: 1; overflow-y: auto; position: relative; z-index: 1; }
3211
+ .wf-store-body::-webkit-scrollbar { width: 6px; }
3212
+ .wf-store-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
3213
+ .wf-store-inner { max-width: 1100px; margin: 0 auto; padding: 20px 24px 40px; }
3214
+ .wf-store-chips { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 20px; }
3215
+ .wf-store-chip {
3216
+ padding: 6px 14px; background: none; border: 1px solid var(--border); border-radius: 100px;
3217
+ font-family: var(--font); font-size: 12px; color: var(--text-muted); cursor: pointer;
3218
+ transition: all 0.15s; white-space: nowrap;
3219
+ }
3220
+ .wf-store-chip:hover { border-color: var(--text-dim); color: var(--text-dim); }
3221
+ .wf-store-chip.active { background: rgba(0,212,170,.15); border-color: rgba(0,212,170,.3); color: var(--accent); }
3222
+ .wf-store-section-label {
3223
+ font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
3224
+ text-transform: uppercase; color: var(--text-muted); margin-bottom: 12px;
3225
+ display: flex; align-items: center; gap: 8px;
3226
+ }
3227
+ .wf-store-count { background: var(--bg-surface); padding: 2px 7px; border-radius: 100px; font-size: 10px; }
3228
+ /* Featured cards */
3229
+ .wf-store-featured-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 24px; }
3230
+ .wf-store-featured-card {
3231
+ border-radius: 14px; padding: 22px 20px 18px; position: relative; overflow: hidden;
3232
+ cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; min-height: 150px;
3233
+ display: flex; flex-direction: column; justify-content: flex-end;
3234
+ }
3235
+ .wf-store-featured-card:hover { transform: translateY(-3px); box-shadow: 0 16px 48px -8px rgba(0,0,0,.5); }
3236
+ .wf-store-featured-card::before {
3237
+ content: ''; position: absolute; inset: 0;
3238
+ background: linear-gradient(180deg, transparent 15%, rgba(0,0,0,.65) 100%); z-index: 1;
3239
+ }
3240
+ .wf-store-featured-content { position: relative; z-index: 2; }
3241
+ .wf-store-featured-content h3 { font-family: var(--mono); font-size: 14px; font-weight: 700; margin-bottom: 4px; color: #fff; }
3242
+ .wf-store-featured-content p { font-size: 12px; color: rgba(255,255,255,.65); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
3243
+ .wf-store-featured-icon { position: absolute; top: 14px; left: 16px; z-index: 2; width: 36px; height: 36px; border-radius: 10px; background: rgba(255,255,255,.18); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; }
3244
+ .wf-store-featured-icon svg { width: 20px; height: 20px; color: #fff; }
3245
+ .wf-store-featured-dl { position: absolute; top: 12px; right: 14px; z-index: 2; font-family: var(--mono); font-size: 10px; color: rgba(255,255,255,.5); }
3246
+ .wf-store-featured-author { position: relative; z-index: 2; display: flex; align-items: center; gap: 6px; margin-top: 6px; font-size: 11px; color: rgba(255,255,255,.55); }
3247
+ .wf-store-featured-avatar { width: 18px; height: 18px; border-radius: 50%; background: rgba(255,255,255,.2); display: flex; align-items: center; justify-content: center; font-size: 9px; color: #fff; font-weight: 700; overflow: hidden; }
3248
+ .wf-store-featured-avatar img { width: 100%; height: 100%; object-fit: cover; }
3249
+ /* Workflow cards grid */
3250
+ .wf-store-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; }
3251
+ .wf-store-card {
3252
+ background: var(--bg-surface); border: 1px solid var(--border); border-radius: 14px;
3253
+ padding: 16px; cursor: pointer; transition: all 0.15s; position: relative; overflow: hidden;
3254
+ }
3255
+ .wf-store-card:hover { border-color: var(--text-dim); background: var(--bg-card); transform: translateY(-1px); }
3256
+ .wf-store-card-bar { position: absolute; top: 0; left: 0; right: 0; height: 2px; opacity: 0; transition: opacity 0.15s; }
3257
+ .wf-store-card:hover .wf-store-card-bar { opacity: 1; }
3258
+ .wf-store-card-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 6px; gap: 8px; }
3259
+ .wf-store-card-icon { width: 28px; height: 28px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
3260
+ .wf-store-card-icon svg { width: 16px; height: 16px; }
3261
+ .wf-store-card-name-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
3262
+ .wf-store-card-name { font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--text); }
3263
+ .wf-store-card-badges { display: flex; gap: 4px; }
3264
+ .wf-store-badge-official { font-family: var(--mono); font-size: 9px; padding: 2px 7px; border-radius: 100px; background: rgba(0,212,170,.1); color: var(--accent); border: 1px solid rgba(0,212,170,.15); white-space: nowrap; }
3265
+ .wf-store-badge-installed { font-family: var(--mono); font-size: 9px; padding: 2px 7px; border-radius: 100px; background: rgba(16,185,129,.1); color: #10B981; border: 1px solid rgba(16,185,129,.15); white-space: nowrap; }
3266
+ .wf-store-card-desc { font-size: 12px; color: var(--text-dim); line-height: 1.5; margin-bottom: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
3267
+ .wf-store-card-author { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; }
3268
+ .wf-store-card-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
3269
+ .wf-store-card-cat { font-family: var(--mono); font-size: 10px; padding: 2px 8px; background: var(--bg-card); border-radius: 100px; color: var(--text-muted); }
3270
+ .wf-store-card-dots { display: flex; gap: 2px; }
3271
+ .wf-store-card-dot { width: 7px; height: 7px; border-radius: 50%; opacity: 0.65; }
3272
+ .wf-store-card-complexity { font-family: var(--mono); font-size: 10px; color: var(--text-muted); }
3273
+ .wf-store-card-downloads { margin-left: auto; font-family: var(--mono); font-size: 10px; color: var(--text-muted); }
3274
+ .wf-store-empty { text-align: center; padding: 40px 0; color: var(--text-muted); }
3275
+ .wf-store-empty p { font-family: var(--mono); font-size: 13px; }
3276
+ /* Detail modal */
3277
+ .wf-store-detail-bg {
3278
+ position: fixed; inset: 0; background: rgba(0,0,0,.55); backdrop-filter: blur(6px);
3279
+ z-index: 200; display: flex; align-items: center; justify-content: center;
3280
+ animation: wfStoreDetailFadeIn 0.12s ease-out; padding: 20px;
3281
+ }
3282
+ @keyframes wfStoreDetailFadeIn { from { opacity: 0; } }
3283
+ @keyframes wfStoreDetailSlideIn { from { transform: translateY(20px); opacity: 0; } }
3284
+ .wf-store-detail-panel {
3285
+ background: var(--bg-surface); border: 1px solid var(--border); border-radius: 18px;
3286
+ width: 100%; max-width: 580px; max-height: 82vh; overflow-y: auto;
3287
+ animation: wfStoreDetailSlideIn 0.18s ease-out;
3288
+ }
3289
+ .wf-store-detail-panel::-webkit-scrollbar { width: 4px; }
3290
+ .wf-store-detail-panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
3291
+ .wf-store-detail-hero { padding: 28px 28px 20px; border-radius: 18px 18px 0 0; position: relative; }
3292
+ .wf-store-detail-hero::before { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 25%, var(--bg-surface) 100%); border-radius: 18px 18px 0 0; }
3293
+ .wf-store-detail-hero-inner { position: relative; z-index: 1; display: flex; align-items: center; gap: 14px; }
3294
+ .wf-store-detail-hero-icon { width: 48px; height: 48px; border-radius: 12px; background: rgba(255,255,255,.18); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
3295
+ .wf-store-detail-hero-icon svg { width: 26px; height: 26px; color: #fff; }
3296
+ .wf-store-detail-close {
3297
+ position: absolute; top: 14px; right: 14px; z-index: 2; width: 28px; height: 28px;
3298
+ border-radius: 50%; background: rgba(0,0,0,.35); border: none; color: #fff; font-size: 16px;
3299
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
3300
+ }
3301
+ .wf-store-detail-close:hover { background: rgba(0,0,0,.55); }
3302
+ .wf-store-detail-name { font-family: var(--mono); font-size: 20px; font-weight: 700; color: #fff; margin-bottom: 4px; }
3303
+ .wf-store-detail-pkg { font-family: var(--mono); font-size: 11px; color: rgba(255,255,255,.45); }
3304
+ .wf-store-detail-body { padding: 0 28px 28px; }
3305
+ .wf-store-detail-desc { font-size: 14px; color: var(--text-dim); line-height: 1.65; margin-bottom: 20px; }
3306
+ .wf-store-detail-section { margin-bottom: 16px; }
3307
+ .wf-store-detail-label { font-family: var(--mono); font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 8px; }
3308
+ .wf-store-detail-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
3309
+ .wf-store-detail-stat { background: var(--bg-card); border-radius: 8px; padding: 12px; text-align: center; }
3310
+ .wf-store-detail-stat-val { font-family: var(--mono); font-size: 20px; font-weight: 700; color: var(--text); }
3311
+ .wf-store-detail-stat-lbl { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
3312
+ .wf-store-detail-tools { display: flex; gap: 5px; flex-wrap: wrap; }
3313
+ .wf-store-detail-tool { padding: 4px 10px; border-radius: 100px; font-size: 11px; font-family: var(--mono); border: 1px solid; }
3314
+ .wf-store-detail-tags { display: flex; gap: 5px; flex-wrap: wrap; }
3315
+ .wf-store-detail-tag { padding: 4px 10px; background: var(--bg-card); border-radius: 100px; font-size: 11px; color: var(--text-dim); font-family: var(--mono); }
3316
+ .wf-store-detail-install { margin-top: 20px; padding: 16px; background: var(--bg-card); border-radius: 8px; border: 1px solid var(--border); }
3317
+ .wf-store-detail-cmd {
3318
+ font-family: var(--mono); font-size: 12px; color: var(--accent); background: var(--bg);
3319
+ padding: 10px 14px; border-radius: 6px; margin-top: 6px; display: flex; align-items: center;
3320
+ justify-content: space-between; cursor: pointer; border: 1px solid var(--border);
3321
+ transition: border-color 0.15s; overflow-x: auto; white-space: nowrap; gap: 10px;
3322
+ }
3323
+ .wf-store-detail-cmd:hover { border-color: var(--accent); }
3324
+ .wf-store-detail-cmd-hint { font-size: 10px; color: var(--text-muted); flex-shrink: 0; }
3325
+ .wf-store-detail-author { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
3326
+ .wf-store-detail-avatar { width: 24px; height: 24px; border-radius: 50%; background: var(--bg-card); display: flex; align-items: center; justify-content: center; font-size: 11px; color: var(--text-muted); font-weight: 700; overflow: hidden; flex-shrink: 0; }
3327
+ .wf-store-detail-avatar img { width: 100%; height: 100%; object-fit: cover; }
3328
+ .wf-store-detail-author-name { font-size: 13px; color: var(--text-dim); }
3329
+ .wf-store-detail-author-name a { color: var(--accent); text-decoration: none; }
3330
+ .wf-store-detail-author-name a:hover { text-decoration: underline; }
3331
+ .wf-store-detail-inputs-table { width: 100%; border-collapse: collapse; font-size: 12px; font-family: var(--mono); }
3332
+ .wf-store-detail-inputs-table th { text-align: left; padding: 6px 10px; color: var(--text-muted); font-weight: 600; font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); }
3333
+ .wf-store-detail-inputs-table td { padding: 6px 10px; color: var(--text-dim); border-bottom: 1px solid var(--border); }
3334
+ .wf-store-detail-screenshots { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 6px; }
3335
+ .wf-store-detail-screenshots img { height: 180px; border-radius: 8px; border: 1px solid var(--border); flex-shrink: 0; }
3336
+ .wf-store-detail-actions { display: flex; gap: 8px; margin-top: 16px; }
3337
+ .wf-store-detail-btn {
3338
+ flex: 1; padding: 10px; border-radius: 8px; font-family: var(--mono); font-size: 12px;
3339
+ font-weight: 700; cursor: pointer; border: none; transition: all 0.15s; text-align: center;
3340
+ }
3341
+ .wf-store-detail-btn-primary { background: var(--accent); color: var(--bg); }
3342
+ .wf-store-detail-btn-primary:hover { filter: brightness(1.1); }
3343
+ .wf-store-detail-btn-secondary { background: var(--bg-card); color: var(--text-dim); border: 1px solid var(--border); }
3344
+ .wf-store-detail-btn-secondary:hover { background: var(--border); color: var(--text); }
3345
+ .wf-store-detail-btn-installed { background: rgba(16,185,129,.1); color: #10B981; border: 1px solid rgba(16,185,129,.2); cursor: default; }
3346
+ /* Store button in library header */
3347
+ .wf-store-btn {
3348
+ width: 28px; height: 28px; border-radius: 6px; border: 1px solid transparent;
3349
+ background: none; color: var(--text-muted); font-size: 14px; cursor: pointer;
3350
+ display: flex; align-items: center; justify-content: center; transition: all 0.15s;
3351
+ flex-shrink: 0;
3352
+ }
3353
+ .wf-store-btn:hover { background: var(--bg-card); color: var(--accent); border-color: rgba(0,212,170,.15); }
3354
+ .wf-store-btn.active { color: var(--accent); }
3355
+ @media (max-width: 700px) {
3356
+ .wf-store-featured-grid { grid-template-columns: 1fr; }
3357
+ .wf-store-grid { grid-template-columns: 1fr; }
3358
+ }
3359
+
3360
+ /* Builder port styles */
3361
+ .wf-port-builder { cursor: crosshair; transition: r 0.15s; pointer-events: all !important; }
3362
+ .wf-port-builder:hover { r: 9; filter: brightness(1.4); }
3363
+ .wf-inspector-delete-btn {
3364
+ width: 100%; padding: 8px 0; border: none; border-radius: 6px;
3365
+ background: var(--error); color: #fff; cursor: pointer;
3366
+ font-size: 12px; font-weight: 600; margin-top: 12px;
3367
+ transition: opacity 0.15s;
3368
+ }
3369
+ .wf-inspector-delete-btn:hover { opacity: 0.85; }
3370
+ .wf-validation-errors {
3371
+ background: rgba(255,105,96,0.08); border: 1px solid var(--error);
3372
+ border-radius: 6px; padding: 10px 12px; margin-bottom: 12px;
3373
+ font-size: 11px; color: var(--error);
3374
+ }
3375
+ .wf-validation-errors ul { margin: 4px 0 0 16px; }
3066
3376
  /* Dry-run plan overlay */
3067
3377
  .wf-dryrun-overlay {
3068
3378
  position: absolute; top: 0; left: 0; right: 0; bottom: 0;
@@ -3107,7 +3417,7 @@ select:focus { outline: none; border-color: var(--accent); }
3107
3417
  background: var(--bg-card); border: 1px solid var(--border);
3108
3418
  display: flex; align-items: center; gap: 10px; font-size: 12px;
3109
3419
  }
3110
- .wf-dryrun-step-icon { font-size: 16px; flex-shrink: 0; }
3420
+ .wf-dryrun-step-icon { flex-shrink: 0; display: flex; align-items: center; }
3111
3421
  .wf-dryrun-step-info { flex: 1; min-width: 0; }
3112
3422
  .wf-dryrun-step-name { font-weight: 600; color: var(--text); }
3113
3423
  .wf-dryrun-step-tool { color: var(--text-muted); font-size: 11px; }
@@ -3195,8 +3505,61 @@ select:focus { outline: none; border-color: var(--accent); }
3195
3505
  transition: color 0.15s, border-color 0.15s;
3196
3506
  }
3197
3507
  .wf-output-expand-btn:hover { color: var(--text); border-color: var(--text-muted); }
3508
+ /* Input modal (pre-execution) */
3509
+ .wf-input-modal-backdrop {
3510
+ position: fixed; inset: 0; z-index: 9999;
3511
+ background: rgba(0,0,0,0.55); display: flex;
3512
+ align-items: center; justify-content: center;
3513
+ }
3514
+ .wf-input-modal {
3515
+ background: var(--bg-surface, var(--bg)); border: 1px solid var(--border);
3516
+ border-radius: 10px; width: 440px; max-width: 90vw;
3517
+ max-height: 80vh; display: flex; flex-direction: column;
3518
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
3519
+ }
3520
+ .wf-input-modal-header {
3521
+ display: flex; align-items: center; justify-content: space-between;
3522
+ padding: 14px 20px; border-bottom: 1px solid var(--border);
3523
+ }
3524
+ .wf-input-modal-title { font-weight: 700; font-size: 14px; color: var(--text); }
3525
+ .wf-input-modal-body {
3526
+ flex: 1; overflow-y: auto; padding: 16px 20px;
3527
+ }
3528
+ .wf-input-modal-field { margin-bottom: 14px; }
3529
+ .wf-input-modal-label {
3530
+ font-size: 12px; font-weight: 600; color: var(--text); margin-bottom: 4px;
3531
+ }
3532
+ .wf-input-modal-desc {
3533
+ font-size: 11px; color: var(--text-muted); margin-bottom: 4px; line-height: 1.3;
3534
+ }
3535
+ .wf-input-modal-input {
3536
+ width: 100%; box-sizing: border-box; padding: 6px 10px;
3537
+ border: 1px solid var(--border); border-radius: 6px;
3538
+ background: var(--bg); color: var(--text); font-size: 13px;
3539
+ font-family: inherit; outline: none;
3540
+ transition: border-color 0.15s;
3541
+ }
3542
+ .wf-input-modal-input:focus { border-color: var(--accent, #6c63ff); }
3543
+ .wf-input-modal-input.error { border-color: #e74c3c; }
3544
+ .wf-input-modal-error { font-size: 11px; color: #e74c3c; margin-top: 3px; display: none; }
3545
+ .wf-input-modal-footer {
3546
+ display: flex; align-items: center; justify-content: flex-end; gap: 8px;
3547
+ padding: 12px 20px; border-top: 1px solid var(--border);
3548
+ }
3549
+ .wf-input-modal-cancel {
3550
+ padding: 6px 16px; border: 1px solid var(--border); border-radius: 6px;
3551
+ background: transparent; color: var(--text); font-size: 12px; cursor: pointer;
3552
+ transition: background 0.15s, border-color 0.15s;
3553
+ }
3554
+ .wf-input-modal-cancel:hover { background: var(--bg); border-color: var(--text-muted); }
3555
+ .wf-input-modal-run {
3556
+ padding: 6px 16px; border: none; border-radius: 6px;
3557
+ background: var(--accent, #6c63ff); color: #fff; font-size: 12px;
3558
+ font-weight: 600; cursor: pointer; transition: filter 0.15s;
3559
+ }
3560
+ .wf-input-modal-run:hover { filter: brightness(1.15); }
3198
3561
  #wf-canvas {
3199
- width: 100%; height: 100%; display: block;
3562
+ width: 100%; height: 100%; display: block; position: relative; z-index: 1;
3200
3563
  }
3201
3564
  /* SVG node styles */
3202
3565
  .wf-node { cursor: pointer; }
@@ -3211,8 +3574,8 @@ select:focus { outline: none; border-color: var(--accent); }
3211
3574
  pointer-events: none; text-anchor: middle; dominant-baseline: central;
3212
3575
  }
3213
3576
  .wf-node-icon {
3214
- font-size: 16px; pointer-events: none;
3215
- text-anchor: middle; dominant-baseline: central;
3577
+ pointer-events: none;
3578
+ color: #fff;
3216
3579
  }
3217
3580
  .wf-node-badge {
3218
3581
  font-size: 10px; fill: rgba(255,255,255,0.7);
@@ -3267,6 +3630,8 @@ select:focus { outline: none; border-color: var(--accent); }
3267
3630
  }
3268
3631
  @keyframes wf-flow { to { stroke-dashoffset: -12; } }
3269
3632
  .wf-edge--complete { stroke: var(--accent, #6c63ff); opacity: 0.6; stroke-dasharray: none; }
3633
+ .wf-edge--else { stroke-dasharray: 6 4; opacity: 0.4; }
3634
+ .wf-edge--skipped { opacity: 0.12; stroke-dasharray: 4 4; }
3270
3635
  /* Inspector */
3271
3636
  .wf-inspector {
3272
3637
  flex-shrink: 0; position: relative;
@@ -3374,7 +3739,7 @@ select:focus { outline: none; border-color: var(--accent); }
3374
3739
  }
3375
3740
  /* Workflow visualizer light mode */
3376
3741
  [data-theme="light"] .wf-node-label { fill: #001E2B; }
3377
- [data-theme="light"] .wf-node-icon { fill: #001E2B; }
3742
+ [data-theme="light"] .wf-node-icon { color: #001E2B; }
3378
3743
  [data-theme="light"] .wf-node-badge { fill: rgba(0,30,43,0.55); }
3379
3744
  [data-theme="light"] .wf-node-time { fill: rgba(0,30,43,0.5); }
3380
3745
  [data-theme="light"] .wf-node-condition { fill: #944F01; }
@@ -3396,6 +3761,8 @@ select:focus { outline: none; border-color: var(--accent); }
3396
3761
  [data-theme="light"] .wf-exec-status { box-shadow: 0 2px 8px rgba(0,30,43,0.08); }
3397
3762
  [data-theme="light"] .wf-run-btn { background: var(--accent); }
3398
3763
  [data-theme="light"] .wf-output-modal { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
3764
+ [data-theme="light"] .wf-input-modal { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
3765
+ [data-theme="light"] .wf-input-modal-backdrop { background: rgba(0,0,0,0.3); }
3399
3766
  @keyframes wf-pulse-light {
3400
3767
  0%, 100% { filter: drop-shadow(0 0 4px rgba(0,158,128,0.3)); }
3401
3768
  50% { filter: drop-shadow(0 0 12px rgba(0,158,128,0.6)); }
@@ -3406,207 +3773,1060 @@ select:focus { outline: none; border-color: var(--accent); }
3406
3773
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
3407
3774
  text-align: center; color: var(--text-muted); pointer-events: none;
3408
3775
  }
3409
- .wf-canvas-empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.5; }
3776
+ .wf-canvas-empty-icon { margin-bottom: 16px; opacity: 0.15; }
3777
+ .wf-canvas-empty-icon img { width: 200px; height: 200px; filter: invert(1); }
3778
+ [data-theme="light"] .wf-canvas-empty-icon img { filter: none; opacity: 0.1; }
3410
3779
  .wf-canvas-empty-text { font-size: 14px; }
3411
- </style>
3412
- </head>
3413
- <body>
3780
+ /* Persistent watermark behind workflows */
3781
+ .wf-canvas-watermark {
3782
+ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
3783
+ pointer-events: none; z-index: 0; opacity: 0.04;
3784
+ }
3785
+ .wf-canvas-watermark img { width: 300px; height: 300px; filter: invert(1); }
3786
+ [data-theme="light"] .wf-canvas-watermark img { filter: none; }
3414
3787
 
3415
- <!-- LeafyGreen Icon Sprites (mongodb.design) -->
3416
- <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"/>
3419
- </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"/>
3425
- </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"/>
3428
- </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"/>
3432
- </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"/>
3437
- </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"/>
3440
- </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"/>
3443
- </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"/>
3446
- </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"/>
3449
- </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"/>
3452
- </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"/>
3455
- </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"/>
3458
- </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"/>
3461
- </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"/>
3464
- </symbol>
3465
- </svg>
3788
+ /* ── Home Page Styles ── */
3466
3789
 
3467
- <div class="app-shell">
3790
+ /* Home Header */
3791
+ .home-header {
3792
+ display: flex;
3793
+ align-items: center;
3794
+ justify-content: space-between;
3795
+ padding: 24px 32px 16px;
3796
+ border-bottom: 1px solid var(--border);
3797
+ background: var(--bg);
3798
+ }
3468
3799
 
3469
- <!-- Sidebar -->
3470
- <aside class="sidebar">
3471
- <div class="sidebar-drag-region">
3472
- <img class="sidebar-logo" id="sidebarLogo" src="/icons/dark/64.png" alt="Vai">
3473
- <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>
3475
- </div>
3476
- <nav class="sidebar-nav">
3477
- <div class="sidebar-nav-group" role="tablist" aria-label="Tools">
3478
- <div class="sidebar-nav-label" id="nav-tools-label">Tools</div>
3479
- <button class="tab-btn active" data-tab="embed" role="tab" aria-selected="true" aria-controls="tab-embed" id="tab-btn-embed"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-lightning"/></svg></span><span>Embed</span></button>
3480
- <button class="tab-btn" data-tab="compare" role="tab" aria-selected="false" aria-controls="tab-compare" id="tab-btn-compare"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-arrows"/></svg></span><span>Compare</span></button>
3481
- <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
- <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
- <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>
3486
- </div>
3487
- <div class="sidebar-nav-divider"></div>
3488
- <div class="sidebar-nav-group" role="tablist" aria-label="Learn">
3489
- <div class="sidebar-nav-label" id="nav-learn-label">Learn</div>
3490
- <button class="tab-btn" data-tab="benchmark" role="tab" aria-selected="false" aria-controls="tab-benchmark" id="tab-btn-benchmark"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-gauge"/></svg></span><span>Benchmark</span></button>
3491
- <button class="tab-btn" data-tab="explore" role="tab" aria-selected="false" aria-controls="tab-explore" id="tab-btn-explore"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-bulb"/></svg></span><span>Explore</span></button>
3492
- <button class="tab-btn" data-tab="about" role="tab" aria-selected="false" aria-controls="tab-about" id="tab-btn-about"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-info"/></svg></span><span>About</span></button>
3493
- </div>
3494
- </nav>
3495
- <div class="sidebar-footer">
3496
- <div style="display:flex;align-items:center;gap:8px;justify-content:space-between;">
3497
- <div style="display:flex;align-items:center;gap:5px;">
3498
- <div class="status-dot" id="statusDot"></div>
3499
- <span class="status-label" id="statusLabel">Checking...</span>
3500
- </div>
3501
- <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">🌙</button>
3502
- </div>
3503
- <div style="display:flex;align-items:center;justify-content:space-between;">
3504
- <div id="appVersionLabel" style="font-size:10px;color:var(--text-muted);"></div>
3505
- <div style="display:flex;align-items:center;gap:6px;">
3506
- <a class="sidebar-docs-link" href="https://docs.vaicli.com" target="_blank" rel="noopener" title="Documentation (F1)">
3507
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/><path d="M6 8h4M6 10.5h4"/></svg>
3508
- <span>Docs</span>
3509
- </a>
3510
- <button class="sidebar-bug-link" id="bugButton" title="Report a Bug">
3511
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="9" r="4.5"/><path d="M5.5 5.5L3 3M10.5 5.5L13 3M3 9H1M15 9h-2M5.5 12.5L4 15M10.5 12.5L12 15"/></svg>
3512
- <span class="sidebar-bug-label">Bug</span>
3513
- </button>
3514
- </div>
3515
- </div>
3516
- </div>
3517
- </aside>
3800
+ .home-header-left {
3801
+ display: flex;
3802
+ align-items: center;
3803
+ gap: 12px;
3804
+ }
3518
3805
 
3519
- <!-- Content -->
3520
- <div class="content-area">
3521
- <div class="content-drag-region"></div>
3522
- <div class="update-banner" id="updateBanner">
3523
- <span class="update-banner-icon">🚀</span>
3524
- <span class="update-banner-text" id="updateBannerText">
3525
- <strong>Vai <span id="updateVersion"></span></strong> is available.
3526
- You're on <span id="currentVersion"></span>.
3527
- </span>
3528
- <div class="update-progress" id="updateProgress" style="display:none;">
3529
- <div class="update-progress-bar"><div class="update-progress-fill" id="updateProgressFill"></div></div>
3530
- <span class="update-progress-label" id="updateProgressLabel">0%</span>
3531
- </div>
3532
- <button class="update-banner-btn" id="updateDownloadBtn">Download &amp; Install</button>
3533
- <button class="update-banner-btn" id="updateInstallBtn" style="display:none;">Restart &amp; Update</button>
3534
- <button class="update-banner-dismiss" id="updateDismissBtn" title="Dismiss">×</button>
3535
- </div>
3536
- <div class="main">
3806
+ .home-logo {
3807
+ width: 32px;
3808
+ height: 32px;
3809
+ }
3537
3810
 
3538
- <!-- ========== EMBED TAB ========== -->
3539
- <div class="tab-panel active" id="tab-embed" role="tabpanel" aria-labelledby="tab-btn-embed" tabindex="0">
3540
- <div class="page-header">
3541
- <h2 class="page-header-title">Embed</h2>
3542
- <p class="page-header-subtitle">Generate vector embeddings for text</p>
3543
- <p class="page-header-hint">Paste or type text below, choose a model, and hit Embed to see the raw vectors and token usage.</p>
3544
- <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/embed" target="_blank" rel="noopener" title="Embed documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
3545
- </div>
3546
- <div class="card">
3547
- <div class="card-title">Input Text</div>
3548
- <textarea id="embedInput" rows="5" placeholder="Enter text to embed..." aria-label="Text to embed">MongoDB Atlas provides powerful vector search capabilities for AI applications.</textarea>
3549
- </div>
3811
+ .home-header-title {
3812
+ font-size: 16px;
3813
+ font-weight: 600;
3814
+ color: var(--text);
3815
+ letter-spacing: -0.01em;
3816
+ }
3550
3817
 
3551
- <div class="options-row">
3552
- <div class="option-group">
3553
- <span class="option-label">Model</span>
3554
- <select id="embedModel"></select>
3555
- </div>
3556
- <div class="option-group">
3557
- <span class="option-label">Input Type</span>
3558
- <select id="embedInputType">
3559
- <option value="">None</option>
3560
- <option value="query">Query</option>
3561
- <option value="document">Document</option>
3562
- </select>
3563
- </div>
3564
- <div class="option-group">
3565
- <span class="option-label">Dimensions</span>
3566
- <select id="embedDimensions">
3567
- <option value="">Default</option>
3568
- <option value="256">256</option>
3569
- <option value="512">512</option>
3570
- <option value="1024">1024</option>
3571
- <option value="2048">2048</option>
3572
- </select>
3573
- </div>
3574
- <div class="option-group">
3575
- <span class="option-label">Output Type</span>
3576
- <select id="embedOutputDtype">
3577
- <option value="float">float (32-bit)</option>
3578
- <option value="int8">int8 (4× smaller)</option>
3579
- <option value="uint8">uint8 (4× smaller)</option>
3580
- <option value="binary">binary (32× smaller)</option>
3581
- <option value="ubinary">ubinary (32× smaller)</option>
3582
- </select>
3583
- </div>
3584
- <button class="btn" id="embedBtn" onclick="doEmbed()">⚡ Embed</button>
3585
- </div>
3818
+ .home-version-badge {
3819
+ background: var(--bg-card);
3820
+ border: 1px solid var(--border);
3821
+ color: var(--text-dim);
3822
+ padding: 4px 8px;
3823
+ border-radius: 12px;
3824
+ font-size: 11px;
3825
+ font-weight: 500;
3826
+ }
3586
3827
 
3587
- <div class="error-msg" id="embedError"></div>
3828
+ .home-header-right {
3829
+ display: flex;
3830
+ align-items: center;
3831
+ gap: 12px;
3832
+ }
3588
3833
 
3589
- <div class="result-section" id="embedResult">
3590
- <div class="card">
3591
- <div class="card-title">Result</div>
3592
- <div id="embedStats"></div>
3593
- <div class="vector-preview" id="embedVector"></div>
3594
- <div style="margin-top:8px;">
3595
- <button class="btn btn-secondary btn-small" onclick="copyVector()">📋 Copy Full Vector</button>
3596
- </div>
3597
- <div class="card-title" style="margin-top:16px;">Vector Heatmap</div>
3598
- <div class="heatmap" id="embedHeatmap"></div>
3599
- </div>
3600
- </div>
3601
- </div>
3834
+ .home-settings-btn {
3835
+ background: none;
3836
+ border: 1px solid var(--border);
3837
+ color: var(--text-dim);
3838
+ padding: 8px;
3839
+ border-radius: 8px;
3840
+ cursor: pointer;
3841
+ transition: all 0.2s;
3842
+ }
3602
3843
 
3603
- <!-- ========== COMPARE TAB ========== -->
3604
- <div class="tab-panel" id="tab-compare" role="tabpanel" aria-labelledby="tab-btn-compare" tabindex="0">
3605
- <div class="page-header">
3606
- <h2 class="page-header-title">Compare</h2>
3607
- <p class="page-header-subtitle">Visualize similarity between text pairs</p>
3608
- <p class="page-header-hint">Enter two texts and compare their embeddings: see cosine similarity, a heatmap of vector dimensions, and a visual diff.</p>
3609
- <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/similarity" target="_blank" rel="noopener" title="Similarity documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
3844
+ .home-settings-btn:hover {
3845
+ background: var(--bg-card);
3846
+ color: var(--text);
3847
+ border-color: var(--accent);
3848
+ }
3849
+
3850
+ .home-settings-btn svg {
3851
+ width: 16px;
3852
+ height: 16px;
3853
+ }
3854
+
3855
+ /* API Key Warning Banner */
3856
+ .home-api-warning {
3857
+ margin: 16px 32px;
3858
+ background: linear-gradient(135deg, rgba(255, 201, 16, 0.1), rgba(255, 105, 96, 0.1));
3859
+ border: 1px solid rgba(255, 201, 16, 0.3);
3860
+ border-radius: 12px;
3861
+ padding: 16px;
3862
+ animation: slideDown 0.3s ease-out;
3863
+ }
3864
+
3865
+ .home-api-warning-content {
3866
+ display: flex;
3867
+ align-items: center;
3868
+ gap: 12px;
3869
+ color: var(--warning);
3870
+ }
3871
+
3872
+ .home-api-warning svg {
3873
+ width: 20px;
3874
+ height: 20px;
3875
+ flex-shrink: 0;
3876
+ }
3877
+
3878
+ .home-api-warning button {
3879
+ margin-left: auto;
3880
+ background: none;
3881
+ border: 1px solid var(--warning);
3882
+ color: var(--warning);
3883
+ padding: 6px 12px;
3884
+ border-radius: 6px;
3885
+ cursor: pointer;
3886
+ font-size: 12px;
3887
+ transition: all 0.2s;
3888
+ }
3889
+
3890
+ .home-api-warning button:hover {
3891
+ background: var(--warning);
3892
+ color: var(--bg);
3893
+ }
3894
+
3895
+ /* Home Content */
3896
+ .home-content {
3897
+ padding: 32px;
3898
+ max-width: 1200px;
3899
+ margin: 0 auto;
3900
+ overflow-y: auto;
3901
+ height: calc(100vh - 120px);
3902
+ }
3903
+
3904
+ /* Announcements Banner */
3905
+ .home-announcements {
3906
+ margin-bottom: 48px;
3907
+ animation: fadeInUp 0.6s ease-out;
3908
+ }
3909
+
3910
+ .home-announcements-carousel {
3911
+ position: relative;
3912
+ overflow: hidden;
3913
+ border-radius: 16px;
3914
+ background: linear-gradient(135deg, var(--bg-card), var(--bg-surface));
3915
+ border: 1px solid var(--border);
3916
+ }
3917
+
3918
+ .home-announcement-card {
3919
+ display: none;
3920
+ padding: 32px;
3921
+ text-align: center;
3922
+ position: relative;
3923
+ }
3924
+
3925
+ .home-announcement-card.active {
3926
+ display: block;
3927
+ animation: fadeIn 0.5s ease-out;
3928
+ }
3929
+
3930
+ .home-announcement-card .badge {
3931
+ display: inline-block;
3932
+ background: linear-gradient(135deg, #00ED64, #00C2FF);
3933
+ color: white;
3934
+ padding: 4px 12px;
3935
+ border-radius: 12px;
3936
+ font-size: 11px;
3937
+ font-weight: 600;
3938
+ margin-bottom: 16px;
3939
+ text-transform: uppercase;
3940
+ letter-spacing: 0.5px;
3941
+ }
3942
+
3943
+ .home-announcement-card h3 {
3944
+ font-size: 24px;
3945
+ font-weight: 700;
3946
+ color: var(--accent-text);
3947
+ margin-bottom: 12px;
3948
+ }
3949
+
3950
+ .home-announcement-card p {
3951
+ color: var(--text-dim);
3952
+ line-height: 1.6;
3953
+ margin-bottom: 24px;
3954
+ max-width: 600px;
3955
+ margin-left: auto;
3956
+ margin-right: auto;
3957
+ }
3958
+
3959
+ .home-announcement-card .cta {
3960
+ background: linear-gradient(135deg, #00ED64, #00C2FF);
3961
+ color: white;
3962
+ border: none;
3963
+ padding: 12px 24px;
3964
+ border-radius: 8px;
3965
+ font-weight: 600;
3966
+ cursor: pointer;
3967
+ transition: all 0.2s;
3968
+ }
3969
+
3970
+ .home-announcement-card .cta:hover {
3971
+ transform: translateY(-2px);
3972
+ box-shadow: 0 8px 32px rgba(0, 237, 100, 0.3);
3973
+ }
3974
+
3975
+ .home-announcement-dismiss {
3976
+ position: absolute;
3977
+ top: 16px;
3978
+ right: 16px;
3979
+ background: none;
3980
+ border: none;
3981
+ color: var(--text-muted);
3982
+ cursor: pointer;
3983
+ padding: 4px;
3984
+ border-radius: 4px;
3985
+ transition: all 0.2s;
3986
+ }
3987
+
3988
+ .home-announcement-dismiss:hover {
3989
+ background: var(--bg-card);
3990
+ color: var(--text);
3991
+ }
3992
+
3993
+ .home-announcements-dots {
3994
+ display: flex;
3995
+ justify-content: center;
3996
+ gap: 8px;
3997
+ margin-top: 16px;
3998
+ }
3999
+
4000
+ .home-announcement-dot {
4001
+ width: 8px;
4002
+ height: 8px;
4003
+ border-radius: 50%;
4004
+ background: var(--border);
4005
+ cursor: pointer;
4006
+ transition: all 0.2s;
4007
+ }
4008
+
4009
+ .home-announcement-dot.active {
4010
+ background: linear-gradient(135deg, #00ED64, #00C2FF);
4011
+ }
4012
+
4013
+ /* Section Headers */
4014
+ .home-section {
4015
+ margin-bottom: 48px;
4016
+ }
4017
+
4018
+ .home-section-header {
4019
+ display: flex;
4020
+ align-items: center;
4021
+ justify-content: space-between;
4022
+ margin-bottom: 24px;
4023
+ }
4024
+
4025
+ .home-section-header h3 {
4026
+ font-size: 20px;
4027
+ font-weight: 700;
4028
+ color: var(--accent-text);
4029
+ }
4030
+
4031
+ .home-section-header a,
4032
+ .home-marketplace-cta {
4033
+ color: var(--blue);
4034
+ text-decoration: none;
4035
+ font-weight: 500;
4036
+ transition: all 0.2s;
4037
+ }
4038
+
4039
+ .home-marketplace-cta {
4040
+ background: linear-gradient(135deg, #00ED64, #00C2FF);
4041
+ color: white;
4042
+ border: none;
4043
+ padding: 10px 20px;
4044
+ border-radius: 8px;
4045
+ cursor: pointer;
4046
+ font-weight: 600;
4047
+ transition: all 0.2s;
4048
+ }
4049
+
4050
+ .home-marketplace-cta:hover {
4051
+ transform: translateY(-1px);
4052
+ box-shadow: 0 4px 16px rgba(0, 237, 100, 0.3);
4053
+ }
4054
+
4055
+ .home-section-header a:hover {
4056
+ color: var(--accent);
4057
+ }
4058
+
4059
+ /* Release Notes Timeline */
4060
+ .home-releases-timeline {
4061
+ display: flex;
4062
+ flex-direction: column;
4063
+ gap: 24px;
4064
+ }
4065
+
4066
+ .home-release-item {
4067
+ display: flex;
4068
+ gap: 16px;
4069
+ padding: 24px;
4070
+ background: var(--bg-card);
4071
+ border: 1px solid var(--border);
4072
+ border-radius: 12px;
4073
+ border-left: 4px solid transparent;
4074
+ border-left-color: #00ED64;
4075
+ background: linear-gradient(90deg, transparent, var(--bg-card) 4px);
4076
+ transition: all 0.2s;
4077
+ animation: fadeInUp 0.6s ease-out;
4078
+ }
4079
+
4080
+ .home-release-item:hover {
4081
+ background: var(--bg-surface);
4082
+ border-color: var(--accent);
4083
+ }
4084
+
4085
+ .home-release-version {
4086
+ font-size: 16px;
4087
+ font-weight: 700;
4088
+ color: var(--accent-text);
4089
+ margin-bottom: 4px;
4090
+ }
4091
+
4092
+ .home-release-date {
4093
+ font-size: 12px;
4094
+ color: var(--text-muted);
4095
+ margin-bottom: 12px;
4096
+ }
4097
+
4098
+ .home-release-highlights {
4099
+ list-style: none;
4100
+ display: flex;
4101
+ flex-direction: column;
4102
+ gap: 8px;
4103
+ }
4104
+
4105
+ .home-release-highlights li {
4106
+ display: flex;
4107
+ align-items: flex-start;
4108
+ gap: 8px;
4109
+ color: var(--text-dim);
4110
+ line-height: 1.5;
4111
+ }
4112
+
4113
+ .home-release-highlights li:before {
4114
+ content: "•";
4115
+ color: var(--accent);
4116
+ font-weight: bold;
4117
+ flex-shrink: 0;
4118
+ margin-top: 2px;
4119
+ }
4120
+
4121
+ /* Subsections */
4122
+ .home-subsection {
4123
+ margin-bottom: 32px;
4124
+ }
4125
+
4126
+ .home-subsection h4 {
4127
+ font-size: 16px;
4128
+ font-weight: 600;
4129
+ color: var(--text);
4130
+ margin-bottom: 16px;
4131
+ }
4132
+
4133
+ .home-subsection-header {
4134
+ display: flex;
4135
+ align-items: center;
4136
+ justify-content: space-between;
4137
+ margin-bottom: 16px;
4138
+ }
4139
+
4140
+ /* Workflow Carousels */
4141
+ .home-workflows-carousel {
4142
+ display: flex;
4143
+ gap: 16px;
4144
+ overflow-x: auto;
4145
+ padding-bottom: 8px;
4146
+ scroll-snap-type: x mandatory;
4147
+ }
4148
+
4149
+ .home-workflows-carousel::-webkit-scrollbar {
4150
+ height: 6px;
4151
+ }
4152
+
4153
+ .home-workflows-carousel::-webkit-scrollbar-track {
4154
+ background: var(--bg-surface);
4155
+ border-radius: 3px;
4156
+ }
4157
+
4158
+ .home-workflows-carousel::-webkit-scrollbar-thumb {
4159
+ background: var(--border);
4160
+ border-radius: 3px;
4161
+ }
4162
+
4163
+ .home-workflow-card {
4164
+ flex: 0 0 280px;
4165
+ background: var(--bg-card);
4166
+ border: 1px solid var(--border);
4167
+ border-radius: 12px;
4168
+ padding: 20px;
4169
+ scroll-snap-align: start;
4170
+ transition: all 0.2s;
4171
+ animation: fadeInUp 0.6s ease-out;
4172
+ }
4173
+
4174
+ .home-workflow-card:hover {
4175
+ background: var(--bg-surface);
4176
+ border-color: var(--accent);
4177
+ transform: translateY(-2px);
4178
+ box-shadow: 0 8px 32px rgba(0, 212, 170, 0.1);
4179
+ }
4180
+
4181
+ .home-workflow-card h5 {
4182
+ font-size: 14px;
4183
+ font-weight: 600;
4184
+ color: var(--accent-text);
4185
+ margin-bottom: 8px;
4186
+ }
4187
+
4188
+ .home-workflow-card p {
4189
+ font-size: 12px;
4190
+ color: var(--text-dim);
4191
+ line-height: 1.5;
4192
+ margin-bottom: 12px;
4193
+ display: -webkit-box;
4194
+ -webkit-line-clamp: 2;
4195
+ -webkit-box-orient: vertical;
4196
+ overflow: hidden;
4197
+ }
4198
+
4199
+ .home-workflow-meta {
4200
+ display: flex;
4201
+ align-items: center;
4202
+ gap: 8px;
4203
+ margin-bottom: 16px;
4204
+ }
4205
+
4206
+ .home-workflow-domain {
4207
+ background: rgba(0, 237, 100, 0.1);
4208
+ color: var(--accent);
4209
+ padding: 4px 8px;
4210
+ border-radius: 6px;
4211
+ font-size: 10px;
4212
+ font-weight: 500;
4213
+ }
4214
+
4215
+ .home-workflow-author {
4216
+ font-size: 11px;
4217
+ color: var(--text-muted);
4218
+ }
4219
+
4220
+ .home-workflow-actions {
4221
+ display: flex;
4222
+ gap: 8px;
4223
+ }
4224
+
4225
+ .home-workflow-btn {
4226
+ flex: 1;
4227
+ background: none;
4228
+ border: 1px solid var(--border);
4229
+ color: var(--text-dim);
4230
+ padding: 8px 12px;
4231
+ border-radius: 6px;
4232
+ font-size: 11px;
4233
+ cursor: pointer;
4234
+ transition: all 0.2s;
4235
+ }
4236
+
4237
+ .home-workflow-btn:hover {
4238
+ background: var(--accent);
4239
+ color: white;
4240
+ border-color: var(--accent);
4241
+ }
4242
+
4243
+ /* Domain Pills */
4244
+ .home-domain-pills {
4245
+ display: flex;
4246
+ flex-wrap: wrap;
4247
+ gap: 8px;
4248
+ }
4249
+
4250
+ .home-domain-pill {
4251
+ background: var(--bg-card);
4252
+ border: 1px solid var(--border);
4253
+ color: var(--text-dim);
4254
+ padding: 8px 16px;
4255
+ border-radius: 20px;
4256
+ font-size: 12px;
4257
+ font-weight: 500;
4258
+ cursor: pointer;
4259
+ transition: all 0.2s;
4260
+ }
4261
+
4262
+ .home-domain-pill:hover {
4263
+ background: linear-gradient(135deg, #00ED64, #00C2FF);
4264
+ color: white;
4265
+ border-color: transparent;
4266
+ transform: translateY(-1px);
4267
+ }
4268
+
4269
+ /* Sort Tabs */
4270
+ .home-sort-tabs {
4271
+ display: flex;
4272
+ gap: 2px;
4273
+ background: var(--bg-surface);
4274
+ border: 1px solid var(--border);
4275
+ border-radius: 8px;
4276
+ padding: 4px;
4277
+ }
4278
+
4279
+ .home-sort-tab {
4280
+ background: none;
4281
+ border: none;
4282
+ color: var(--text-muted);
4283
+ padding: 6px 12px;
4284
+ border-radius: 6px;
4285
+ font-size: 12px;
4286
+ cursor: pointer;
4287
+ transition: all 0.2s;
4288
+ }
4289
+
4290
+ .home-sort-tab.active {
4291
+ background: var(--accent);
4292
+ color: white;
4293
+ }
4294
+
4295
+ .home-sort-tab:hover:not(.active) {
4296
+ background: var(--bg-card);
4297
+ color: var(--text);
4298
+ }
4299
+
4300
+ /* Community Grid */
4301
+ .home-community-grid {
4302
+ display: grid;
4303
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
4304
+ gap: 16px;
4305
+ }
4306
+
4307
+ .home-community-empty {
4308
+ grid-column: 1 / -1;
4309
+ text-align: center;
4310
+ padding: 48px;
4311
+ color: var(--text-muted);
4312
+ }
4313
+
4314
+ .home-community-empty h4 {
4315
+ font-size: 16px;
4316
+ margin-bottom: 8px;
4317
+ color: var(--text);
4318
+ }
4319
+
4320
+ /* Quick Actions */
4321
+ .home-quick-actions {
4322
+ display: grid;
4323
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
4324
+ gap: 16px;
4325
+ }
4326
+
4327
+ .home-quick-action {
4328
+ display: flex;
4329
+ flex-direction: column;
4330
+ align-items: center;
4331
+ gap: 12px;
4332
+ background: var(--bg-card);
4333
+ border: 1px solid var(--border);
4334
+ border-radius: 12px;
4335
+ padding: 24px;
4336
+ cursor: pointer;
4337
+ transition: all 0.2s;
4338
+ text-decoration: none;
4339
+ color: var(--text);
4340
+ animation: fadeInUp 0.6s ease-out;
4341
+ }
4342
+
4343
+ .home-quick-action:hover {
4344
+ background: var(--bg-surface);
4345
+ border-color: var(--accent);
4346
+ transform: translateY(-4px);
4347
+ box-shadow: 0 12px 40px rgba(0, 212, 170, 0.1);
4348
+ color: var(--text);
4349
+ }
4350
+
4351
+ .home-quick-action svg {
4352
+ width: 24px;
4353
+ height: 24px;
4354
+ color: var(--accent);
4355
+ }
4356
+
4357
+ .home-quick-action span {
4358
+ font-size: 14px;
4359
+ font-weight: 500;
4360
+ text-align: center;
4361
+ }
4362
+
4363
+ /* Footer */
4364
+ .home-footer {
4365
+ margin-top: 64px;
4366
+ padding-top: 32px;
4367
+ border-top: 1px solid var(--border);
4368
+ text-align: center;
4369
+ }
4370
+
4371
+ .home-footer-links {
4372
+ display: flex;
4373
+ justify-content: center;
4374
+ gap: 24px;
4375
+ margin-bottom: 16px;
4376
+ flex-wrap: wrap;
4377
+ }
4378
+
4379
+ .home-footer-links a {
4380
+ color: var(--text-dim);
4381
+ text-decoration: none;
4382
+ font-size: 12px;
4383
+ transition: color 0.2s;
4384
+ }
4385
+
4386
+ .home-footer-links a:hover {
4387
+ color: var(--accent);
4388
+ }
4389
+
4390
+ .home-footer-branding {
4391
+ display: flex;
4392
+ flex-direction: column;
4393
+ gap: 4px;
4394
+ color: var(--text-muted);
4395
+ font-size: 11px;
4396
+ }
4397
+
4398
+ /* Animations */
4399
+ @keyframes fadeIn {
4400
+ from { opacity: 0; }
4401
+ to { opacity: 1; }
4402
+ }
4403
+
4404
+ @keyframes fadeInUp {
4405
+ from {
4406
+ opacity: 0;
4407
+ transform: translateY(20px);
4408
+ }
4409
+ to {
4410
+ opacity: 1;
4411
+ transform: translateY(0);
4412
+ }
4413
+ }
4414
+
4415
+ @keyframes slideDown {
4416
+ from {
4417
+ opacity: 0;
4418
+ transform: translateY(-10px);
4419
+ }
4420
+ to {
4421
+ opacity: 1;
4422
+ transform: translateY(0);
4423
+ }
4424
+ }
4425
+
4426
+ /* Responsive */
4427
+ @media (max-width: 1024px) {
4428
+ .home-content {
4429
+ padding: 24px;
4430
+ }
4431
+
4432
+ .home-quick-actions {
4433
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
4434
+ }
4435
+
4436
+ .home-community-grid {
4437
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
4438
+ }
4439
+ }
4440
+
4441
+ @media (max-width: 768px) {
4442
+ .home-header {
4443
+ padding: 16px 24px 12px;
4444
+ }
4445
+
4446
+ .home-content {
4447
+ padding: 16px;
4448
+ }
4449
+
4450
+ .home-section-header {
4451
+ flex-direction: column;
4452
+ align-items: flex-start;
4453
+ gap: 12px;
4454
+ }
4455
+
4456
+ .home-footer-links {
4457
+ flex-direction: column;
4458
+ gap: 12px;
4459
+ }
4460
+ }
4461
+ </style>
4462
+ </head>
4463
+ <body>
4464
+
4465
+ <!-- Lucide Icons (lucide.dev) — stroke-based, 16×16 -->
4466
+ <svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
4467
+ <!-- Zap (Embed) -->
4468
+ <symbol id="lg-lightning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4469
+ <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>
4470
+ </symbol>
4471
+ <!-- Arrow Left Right (Compare) -->
4472
+ <symbol id="lg-arrows" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4473
+ <path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>
4474
+ </symbol>
4475
+ <!-- Search -->
4476
+ <symbol id="lg-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4477
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
4478
+ </symbol>
4479
+ <!-- Gauge (Benchmark) -->
4480
+ <symbol id="lg-gauge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4481
+ <path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>
4482
+ </symbol>
4483
+ <!-- Lightbulb (Explore) -->
4484
+ <symbol id="lg-bulb" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4485
+ <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"/>
4486
+ </symbol>
4487
+ <!-- Info (About) -->
4488
+ <symbol id="lg-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4489
+ <circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>
4490
+ </symbol>
4491
+ <!-- Image (Multimodal) -->
4492
+ <symbol id="lg-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4493
+ <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"/>
4494
+ </symbol>
4495
+ <!-- Settings (Config) -->
4496
+ <symbol id="lg-config" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4497
+ <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"/>
4498
+ </symbol>
4499
+ <!-- Code (Generate) -->
4500
+ <symbol id="lg-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4501
+ <polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
4502
+ </symbol>
4503
+ <!-- Palette (Theme) -->
4504
+ <symbol id="lg-palette" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4505
+ <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"/>
4506
+ </symbol>
4507
+ <!-- Box (Cube) -->
4508
+ <symbol id="lg-cube" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4509
+ <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"/>
4510
+ </symbol>
4511
+ <!-- Message Square (Chat) -->
4512
+ <symbol id="lg-chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4513
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
4514
+ </symbol>
4515
+ <!-- Shield -->
4516
+ <symbol id="lg-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4517
+ <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"/>
4518
+ </symbol>
4519
+ <!-- Activity (Pulse) -->
4520
+ <symbol id="lg-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
4521
+ <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"/>
4522
+ </symbol>
4523
+ </svg>
4524
+
4525
+ <div class="app-shell">
4526
+
4527
+ <!-- Sidebar -->
4528
+ <aside class="sidebar">
4529
+ <div class="sidebar-drag-region">
4530
+ <img class="sidebar-logo" id="sidebarLogo" src="/icons/dark/64.png" alt="Vai">
4531
+ <span class="sidebar-title">Vai</span>
4532
+ <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>
4533
+ </div>
4534
+ <nav class="sidebar-nav">
4535
+ <div class="sidebar-nav-group" role="tablist" aria-label="Tools">
4536
+ <div class="sidebar-nav-label" id="nav-tools-label">Tools</div>
4537
+ <button class="tab-btn active" data-tab="home" role="tab" aria-selected="true" aria-controls="tab-home" id="tab-btn-home"><span class="tab-btn-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></span><span>Home</span></button>
4538
+ <button class="tab-btn" data-tab="embed" role="tab" aria-selected="false" aria-controls="tab-embed" id="tab-btn-embed"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-lightning"/></svg></span><span>Embed</span></button>
4539
+ <button class="tab-btn" data-tab="compare" role="tab" aria-selected="false" aria-controls="tab-compare" id="tab-btn-compare"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-arrows"/></svg></span><span>Compare</span></button>
4540
+ <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>
4541
+ <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>
4542
+ <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>
4543
+ <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>
4544
+ <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>
4545
+ </div>
4546
+ <div class="sidebar-nav-divider"></div>
4547
+ <div class="sidebar-nav-group" role="tablist" aria-label="Learn">
4548
+ <div class="sidebar-nav-label" id="nav-learn-label">Learn</div>
4549
+ <button class="tab-btn" data-tab="benchmark" role="tab" aria-selected="false" aria-controls="tab-benchmark" id="tab-btn-benchmark"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-gauge"/></svg></span><span>Benchmark</span></button>
4550
+ <button class="tab-btn" data-tab="explore" role="tab" aria-selected="false" aria-controls="tab-explore" id="tab-btn-explore"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-bulb"/></svg></span><span>Explore</span></button>
4551
+ <button class="tab-btn" data-tab="about" role="tab" aria-selected="false" aria-controls="tab-about" id="tab-btn-about"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-info"/></svg></span><span>About</span></button>
4552
+ </div>
4553
+ </nav>
4554
+ <div class="sidebar-footer">
4555
+ <div style="display:flex;align-items:center;gap:8px;justify-content:space-between;">
4556
+ <div style="display:flex;align-items:center;gap:5px;">
4557
+ <div class="status-dot" id="statusDot"></div>
4558
+ <span class="status-label" id="statusLabel">Checking...</span>
4559
+ </div>
4560
+ <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">🌙</button>
4561
+ </div>
4562
+ <div style="display:flex;align-items:center;justify-content:space-between;">
4563
+ <div id="appVersionLabel" style="font-size:10px;color:var(--text-muted);"></div>
4564
+ <div style="display:flex;align-items:center;gap:6px;">
4565
+ <a class="sidebar-docs-link" href="https://docs.vaicli.com" target="_blank" rel="noopener" title="Documentation (F1)">
4566
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/><path d="M6 8h4M6 10.5h4"/></svg>
4567
+ <span>Docs</span>
4568
+ </a>
4569
+ <button class="sidebar-bug-link" id="bugButton" title="Report a Bug">
4570
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="9" r="4.5"/><path d="M5.5 5.5L3 3M10.5 5.5L13 3M3 9H1M15 9h-2M5.5 12.5L4 15M10.5 12.5L12 15"/></svg>
4571
+ <span class="sidebar-bug-label">Bug</span>
4572
+ </button>
4573
+ </div>
4574
+ </div>
4575
+ </div>
4576
+ </aside>
4577
+
4578
+ <!-- Content -->
4579
+ <div class="content-area">
4580
+ <div class="content-drag-region"></div>
4581
+ <div class="update-banner" id="updateBanner">
4582
+ <span class="update-banner-icon">🚀</span>
4583
+ <span class="update-banner-text" id="updateBannerText">
4584
+ <strong>Vai <span id="updateVersion"></span></strong> is available.
4585
+ You're on <span id="currentVersion"></span>.
4586
+ </span>
4587
+ <div class="update-progress" id="updateProgress" style="display:none;">
4588
+ <div class="update-progress-bar"><div class="update-progress-fill" id="updateProgressFill"></div></div>
4589
+ <span class="update-progress-label" id="updateProgressLabel">0%</span>
4590
+ </div>
4591
+ <button class="update-banner-btn" id="updateDownloadBtn">Download &amp; Install</button>
4592
+ <button class="update-banner-btn" id="updateInstallBtn" style="display:none;">Restart &amp; Update</button>
4593
+ <button class="update-banner-dismiss" id="updateDismissBtn" title="Dismiss">×</button>
4594
+ </div>
4595
+ <div class="main">
4596
+
4597
+ <!-- ========== HOME TAB ========== -->
4598
+ <div class="tab-panel active" id="tab-home" role="tabpanel" aria-labelledby="tab-btn-home" tabindex="0">
4599
+ <!-- Header Bar -->
4600
+ <div class="home-header">
4601
+ <div class="home-header-left">
4602
+ <img class="home-logo" id="homeLogo" src="/icons/V.png" alt="Vai" onerror="this.src='/icons/dark/64.png'">
4603
+ <span class="home-header-title">Voyage AI Playground</span>
4604
+ <span class="home-version-badge" id="homeVersionBadge">v1.0.0</span>
4605
+ </div>
4606
+ <div class="home-header-right">
4607
+ <button class="home-settings-btn" onclick="switchTab('settings')" title="Settings">
4608
+ <svg width="20" height="20" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
4609
+ </button>
4610
+ </div>
4611
+ </div>
4612
+
4613
+ <!-- API Key Warning Banner (hidden by default) -->
4614
+ <div class="home-api-warning" id="homeApiWarning" style="display: none;">
4615
+ <div class="home-api-warning-content">
4616
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
4617
+ <triangle cx="12" cy="12" r="10"/>
4618
+ <line x1="12" y1="8" x2="12" y2="12"/>
4619
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
4620
+ </svg>
4621
+ <span>Set up your Voyage AI API key to get started</span>
4622
+ <button onclick="switchTab('settings')">Settings →</button>
4623
+ </div>
4624
+ </div>
4625
+
4626
+ <div class="home-content">
4627
+ <!-- Announcements Banner -->
4628
+ <div class="home-announcements" id="homeAnnouncements" style="display: none;">
4629
+ <div class="home-announcements-carousel" id="announcementsCarousel">
4630
+ <!-- Cards will be inserted here -->
4631
+ </div>
4632
+ <div class="home-announcements-dots" id="announcementsDots">
4633
+ <!-- Dots will be inserted here -->
4634
+ </div>
4635
+ </div>
4636
+
4637
+ <!-- What's New (Release Notes) -->
4638
+ <div class="home-section" id="homeReleases">
4639
+ <div class="home-section-header">
4640
+ <h3>What's New</h3>
4641
+ <a href="https://github.com/mrlynn/voyageai-cli/releases" target="_blank" rel="noopener">View All →</a>
4642
+ </div>
4643
+ <div class="home-releases-timeline" id="releasesTimeline">
4644
+ <!-- Release items will be inserted here -->
4645
+ </div>
4646
+ </div>
4647
+
4648
+ <!-- Marketplace Spotlight -->
4649
+ <div class="home-section">
4650
+ <div class="home-section-header">
4651
+ <h3>Marketplace Spotlight</h3>
4652
+ <button class="home-marketplace-cta" onclick="switchTab('workflows')">
4653
+ Explore the Marketplace →
4654
+ </button>
4655
+ </div>
4656
+
4657
+ <!-- Featured Workflows -->
4658
+ <div class="home-subsection">
4659
+ <h4>Featured Workflows</h4>
4660
+ <div class="home-workflows-carousel" id="featuredWorkflows">
4661
+ <!-- Workflow cards will be inserted here -->
4662
+ </div>
4663
+ </div>
4664
+
4665
+ <!-- Browse by Domain -->
4666
+ <div class="home-subsection">
4667
+ <h4>Browse by Domain</h4>
4668
+ <div class="home-domain-pills">
4669
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('Healthcare')">Healthcare</button>
4670
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('Finance')">Finance</button>
4671
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('Legal')">Legal</button>
4672
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('DevOps')">DevOps</button>
4673
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('Education')">Education</button>
4674
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('E-Commerce')">E-Commerce</button>
4675
+ <button class="home-domain-pill" onclick="filterAndSwitchWorkflows('Marketing')">Marketing</button>
4676
+ </div>
4677
+ </div>
4678
+
4679
+ <!-- Community Workflows -->
4680
+ <div class="home-subsection">
4681
+ <div class="home-subsection-header">
4682
+ <h4>Community Workflows</h4>
4683
+ <div class="home-sort-tabs">
4684
+ <button class="home-sort-tab active" data-sort="trending" onclick="sortCommunityWorkflows('trending')">Trending</button>
4685
+ <button class="home-sort-tab" data-sort="newest" onclick="sortCommunityWorkflows('newest')">Newest</button>
4686
+ <button class="home-sort-tab" data-sort="installs" onclick="sortCommunityWorkflows('installs')">Most Installed</button>
4687
+ </div>
4688
+ </div>
4689
+ <div class="home-community-grid" id="communityWorkflows">
4690
+ <!-- Community workflow cards will be inserted here -->
4691
+ </div>
4692
+ </div>
4693
+ </div>
4694
+
4695
+ <!-- Quick Actions -->
4696
+ <div class="home-section">
4697
+ <div class="home-section-header">
4698
+ <h3>Quick Actions</h3>
4699
+ </div>
4700
+ <div class="home-quick-actions">
4701
+ <button class="home-quick-action" onclick="switchTab('embed')" title="Generate embeddings for text">
4702
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
4703
+ <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>
4704
+ </svg>
4705
+ <span>New Embedding</span>
4706
+ </button>
4707
+ <button class="home-quick-action" onclick="switchTab('compare')" title="Compare model similarities">
4708
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
4709
+ <path d="M21 12c0 1.2-4.03 6-9 6s-9-4.8-9-6c0-1.2 4.03-6 9-6s9 4.8 9 6"/>
4710
+ <circle cx="12" cy="12" r="3"/>
4711
+ </svg>
4712
+ <span>Compare Models</span>
4713
+ </button>
4714
+ <button class="home-quick-action" onclick="switchTab('search')" title="Search through vectors">
4715
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
4716
+ <circle cx="11" cy="11" r="8"/>
4717
+ <path d="m21 21-4.35-4.35"/>
4718
+ </svg>
4719
+ <span>Search Vectors</span>
4720
+ </button>
4721
+ <button class="home-quick-action" onclick="switchTab('workflows')" title="View installed workflows">
4722
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
4723
+ <circle cx="5" cy="12" r="3"/>
4724
+ <circle cx="19" cy="6" r="3"/>
4725
+ <circle cx="19" cy="18" r="3"/>
4726
+ <line x1="7.7" y1="10.7" x2="16.3" y2="7.3"/>
4727
+ <line x1="7.7" y1="13.3" x2="16.3" y2="16.7"/>
4728
+ </svg>
4729
+ <span>My Workflows</span>
4730
+ </button>
4731
+ <button class="home-quick-action" onclick="publishWorkflow()" title="Publish a new workflow">
4732
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
4733
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
4734
+ <polyline points="17,8 12,3 7,8"/>
4735
+ <line x1="12" y1="3" x2="12" y2="15"/>
4736
+ </svg>
4737
+ <span>Publish Workflow</span>
4738
+ </button>
4739
+ </div>
4740
+ </div>
4741
+
4742
+ <!-- Footer -->
4743
+ <div class="home-footer">
4744
+ <div class="home-footer-links">
4745
+ <a href="https://docs.vaicli.com" target="_blank" rel="noopener">Documentation</a>
4746
+ <a href="https://github.com/mrlynn/voyageai-cli" target="_blank" rel="noopener">GitHub</a>
4747
+ <a href="#" onclick="reportBug()">Report Bug</a>
4748
+ <!-- <a href="#" target="_blank" rel="noopener">Community / Discord</a> -->
4749
+ </div>
4750
+ <div class="home-footer-branding">
4751
+ <span>Built with Voyage AI + MongoDB Atlas</span>
4752
+ <span id="homeFooterVersion">Version 1.0.0 • © 2026 VAI</span>
4753
+ </div>
4754
+ </div>
4755
+ </div>
4756
+ </div>
4757
+
4758
+ <!-- ========== EMBED TAB ========== -->
4759
+ <div class="tab-panel" id="tab-embed" role="tabpanel" aria-labelledby="tab-btn-embed" tabindex="0">
4760
+ <div class="page-header">
4761
+ <h2 class="page-header-title">Embed</h2>
4762
+ <p class="page-header-subtitle">Generate vector embeddings for text</p>
4763
+ <p class="page-header-hint">Paste or type text below, choose a model, and hit Embed to see the raw vectors and token usage.</p>
4764
+ <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/embed" target="_blank" rel="noopener" title="Embed documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
4765
+ </div>
4766
+ <div class="card">
4767
+ <div class="card-title">Input Text</div>
4768
+ <textarea id="embedInput" rows="5" placeholder="Enter text to embed..." aria-label="Text to embed">MongoDB Atlas provides powerful vector search capabilities for AI applications.</textarea>
4769
+ </div>
4770
+
4771
+ <div class="options-row">
4772
+ <div class="option-group">
4773
+ <span class="option-label">Model</span>
4774
+ <select id="embedModel"></select>
4775
+ </div>
4776
+ <div class="option-group">
4777
+ <span class="option-label">Input Type</span>
4778
+ <select id="embedInputType">
4779
+ <option value="">None</option>
4780
+ <option value="query">Query</option>
4781
+ <option value="document">Document</option>
4782
+ </select>
4783
+ </div>
4784
+ <div class="option-group">
4785
+ <span class="option-label">Dimensions</span>
4786
+ <select id="embedDimensions">
4787
+ <option value="">Default</option>
4788
+ <option value="256">256</option>
4789
+ <option value="512">512</option>
4790
+ <option value="1024">1024</option>
4791
+ <option value="2048">2048</option>
4792
+ </select>
4793
+ </div>
4794
+ <div class="option-group">
4795
+ <span class="option-label">Output Type</span>
4796
+ <select id="embedOutputDtype">
4797
+ <option value="float">float (32-bit)</option>
4798
+ <option value="int8">int8 (4× smaller)</option>
4799
+ <option value="uint8">uint8 (4× smaller)</option>
4800
+ <option value="binary">binary (32× smaller)</option>
4801
+ <option value="ubinary">ubinary (32× smaller)</option>
4802
+ </select>
4803
+ </div>
4804
+ <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>
4805
+ </div>
4806
+
4807
+ <div class="error-msg" id="embedError"></div>
4808
+
4809
+ <div class="result-section" id="embedResult">
4810
+ <div class="card">
4811
+ <div class="card-title">Result</div>
4812
+ <div id="embedStats"></div>
4813
+ <div class="vector-preview" id="embedVector"></div>
4814
+ <div style="margin-top:8px;">
4815
+ <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>
4816
+ </div>
4817
+ <div class="card-title" style="margin-top:16px;">Vector Heatmap</div>
4818
+ <div class="heatmap" id="embedHeatmap"></div>
4819
+ </div>
4820
+ </div>
4821
+ </div>
4822
+
4823
+ <!-- ========== COMPARE TAB ========== -->
4824
+ <div class="tab-panel" id="tab-compare" role="tabpanel" aria-labelledby="tab-btn-compare" tabindex="0">
4825
+ <div class="page-header">
4826
+ <h2 class="page-header-title">Compare</h2>
4827
+ <p class="page-header-subtitle">Visualize similarity between text pairs</p>
4828
+ <p class="page-header-hint">Enter two texts and compare their embeddings: see cosine similarity, a heatmap of vector dimensions, and a visual diff.</p>
4829
+ <a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/similarity" target="_blank" rel="noopener" title="Similarity documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
3610
4830
  </div>
3611
4831
  <div class="compare-grid">
3612
4832
  <div class="card">
@@ -3634,7 +4854,7 @@ select:focus { outline: none; border-color: var(--accent); }
3634
4854
  <option value="2048">2048</option>
3635
4855
  </select>
3636
4856
  </div>
3637
- <button class="btn" id="compareBtn" onclick="doCompare()">⚖️ Compare</button>
4857
+ <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
4858
  </div>
3639
4859
 
3640
4860
  <div class="error-msg" id="compareError"></div>
@@ -3696,8 +4916,8 @@ Semantic search understands meaning beyond keyword matching</textarea>
3696
4916
  <option value="10">10</option>
3697
4917
  </select>
3698
4918
  </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>
4919
+ <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>
4920
+ <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
4921
  </div>
3702
4922
 
3703
4923
  <div class="error-msg" id="searchError"></div>
@@ -3756,7 +4976,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
3756
4976
  <option value="2048">2048</option>
3757
4977
  </select>
3758
4978
  </div>
3759
- <button class="btn" id="mmCompareBtn" onclick="doMultimodalCompare()">🔮 Compare</button>
4979
+ <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
4980
  </div>
3761
4981
 
3762
4982
  <div class="error-msg" id="mmError"></div>
@@ -3794,20 +5014,20 @@ Semantic search understands meaning beyond keyword matching</textarea>
3794
5014
  <div class="card" style="margin-top:12px;">
3795
5015
  <div class="card-title">Search Query</div>
3796
5016
  <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>
5017
+ <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>
5018
+ <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
5019
  </div>
3800
5020
  <div id="mmSearchTextWrap">
3801
5021
  <div class="mm-search-row">
3802
5022
  <input type="text" id="mmSearchQuery" placeholder="Enter a search query...">
3803
- <button class="btn" id="mmSearchBtn" onclick="doMultimodalSearch()">🔮 Search Corpus</button>
5023
+ <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
5024
  </div>
3805
5025
  </div>
3806
5026
  <div id="mmSearchImageWrap" style="display:none;">
3807
5027
  <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
5028
  <div style="display:flex;gap:8px;align-items:center;">
3809
5029
  <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>
5030
+ <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
5031
  </div>
3812
5032
  </div>
3813
5033
  </div>
@@ -3834,12 +5054,12 @@ Semantic search understands meaning beyond keyword matching</textarea>
3834
5054
 
3835
5055
  <!-- Sub-panel switcher -->
3836
5056
  <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>
5057
+ <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>
5058
+ <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>
5059
+ <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>
5060
+ <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>
5061
+ <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>
5062
+ <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
5063
  </div>
3844
5064
 
3845
5065
  <!-- ── Latency Panel ── -->
@@ -3861,7 +5081,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
3861
5081
  <option value="10">10</option>
3862
5082
  </select>
3863
5083
  </div>
3864
- <button class="btn" id="benchLatencyBtn" onclick="doBenchLatency()">⚡ Run Benchmark</button>
5084
+ <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
5085
  </div>
3866
5086
  </div>
3867
5087
 
@@ -3915,7 +5135,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
3915
5135
  <option value="8">8</option>
3916
5136
  </select>
3917
5137
  </div>
3918
- <button class="btn" id="benchRankBtn" onclick="doBenchRanking()">🏆 Compare Rankings</button>
5138
+ <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
5139
  </div>
3920
5140
  </div>
3921
5141
 
@@ -3991,7 +5211,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
3991
5211
 
3992
5212
  <!-- Cost Comparison -->
3993
5213
  <div class="card" style="margin-top:16px;">
3994
- <div class="card-title">💰 Cost per Million Tokens</div>
5214
+ <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
5215
  <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
3996
5216
  Voyage AI offers significant cost savings, especially with asymmetric retrieval strategies.
3997
5217
  </p>
@@ -4152,7 +5372,7 @@ Approximate nearest neighbor algorithms like HNSW enable fast similarity search
4152
5372
  Reranking models rescore initial search results to improve relevance ordering.</textarea>
4153
5373
  </div>
4154
5374
  <div style="margin-top:12px;">
4155
- <button class="btn" id="quantBtn" onclick="doBenchQuantization()">⚗️ Run Quantization Benchmark</button>
5375
+ <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
5376
  </div>
4157
5377
  </div>
4158
5378
 
@@ -4180,7 +5400,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4180
5400
  <!-- ── Cost Panel ── -->
4181
5401
  <div class="bench-view" id="bench-cost">
4182
5402
  <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>
5403
+ <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
5404
 
4185
5405
  <!-- Mode toggle -->
4186
5406
  <div style="margin-bottom: 20px;">
@@ -4301,7 +5521,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4301
5521
  <div class="history-empty">No benchmarks recorded yet. Run a latency benchmark to start tracking.</div>
4302
5522
  </div>
4303
5523
  <div style="margin-top:12px;text-align:right;">
4304
- <button class="btn btn-secondary btn-small" onclick="clearHistory()">🗑 Clear History</button>
5524
+ <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
5525
  </div>
4306
5526
  </div>
4307
5527
  </div>
@@ -4318,7 +5538,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4318
5538
  <span id="chatStatusDb">No database</span>
4319
5539
  </div>
4320
5540
  <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>
5541
+ <svg width="14" height="14" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
4322
5542
  Configure
4323
5543
  </button>
4324
5544
  </div>
@@ -4342,11 +5562,18 @@ Reranking models rescore initial search results to improve relevance ordering.</
4342
5562
  <div class="wf-container">
4343
5563
  <div class="wf-library">
4344
5564
  <div class="wf-library-header">
4345
- <span>Workflows</span>
5565
+ <div class="wf-library-tabs" style="flex:1;">
5566
+ <button class="wf-lib-tab active" data-lib-tab="library" onclick="wfSwitchLibTab('library')">Library</button>
5567
+ <button class="wf-lib-tab" data-lib-tab="palette" onclick="wfSwitchLibTab('palette')">Palette</button>
5568
+ </div>
5569
+ <button class="wf-store-btn" id="wfStoreBtn" onclick="wfStoreOpen()" title="Browse Workflow Store">
5570
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
5571
+ </button>
4346
5572
  </div>
4347
5573
  <div class="wf-library-list" id="wfLibraryList">
4348
5574
  <div style="padding: 16px; color: var(--text-muted); font-size: 12px;">Loading...</div>
4349
5575
  </div>
5576
+ <div class="wf-palette-list" id="wfPaletteList" style="display:none; flex:1; overflow-y:auto; padding:8px;"></div>
4350
5577
  <div class="wf-library-footer">
4351
5578
  <button class="wf-load-file-btn" onclick="wfLoadFromFile()" title="Load workflow JSON from file">
4352
5579
  <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 +5584,9 @@ Reranking models rescore initial search results to improve relevance ordering.</
4357
5584
  </div>
4358
5585
  <div class="wf-canvas-area">
4359
5586
  <div class="wf-canvas-toolbar" id="wfToolbar">
5587
+ <button class="wf-new-btn" onclick="wfNewWorkflow()" title="Create new workflow">+ New</button>
5588
+ <button class="wf-edit-btn" id="wfEditBtn" onclick="wfEditWorkflow()" disabled title="Edit current workflow">&#9998; Edit</button>
5589
+ <span class="wf-toolbar-sep"></span>
4360
5590
  <button onclick="wfZoom(1)" title="Zoom in">+</button>
4361
5591
  <button onclick="wfZoom(-1)" title="Zoom out">&minus;</button>
4362
5592
  <button onclick="wfFitToView()" title="Fit to view">&#8862;</button>
@@ -4377,10 +5607,35 @@ Reranking models rescore initial search results to improve relevance ordering.</
4377
5607
  <span class="wf-exec-status-time" id="wfExecStatusTime"></span>
4378
5608
  </div>
4379
5609
  <div class="wf-canvas-empty" id="wfCanvasEmpty">
4380
- <div class="wf-canvas-empty-icon">&#9881;</div>
5610
+ <div class="wf-canvas-empty-icon"><img src="/icons/watermark.png" alt="Vai"></div>
4381
5611
  <div class="wf-canvas-empty-text">Select a workflow from the library</div>
4382
5612
  </div>
4383
- <svg id="wf-canvas" xmlns="http://www.w3.org/2000/svg"></svg>
5613
+ <div class="wf-canvas-watermark" id="wfCanvasWatermark"><img src="/icons/watermark.png" alt=""></div>
5614
+ <svg id="wf-canvas" xmlns="http://www.w3.org/2000/svg" ondragover="event.preventDefault()" ondrop="wfCanvasDrop(event)"></svg>
5615
+ <!-- Workflow Store Overlay -->
5616
+ <div class="wf-store-overlay" id="wfStoreOverlay">
5617
+ <div class="wf-store-header">
5618
+ <button class="wf-store-back" onclick="wfStoreClose()">← Library</button>
5619
+ <div class="wf-store-title-area">
5620
+ <span class="wf-store-title">Workflow Store</span>
5621
+ <span class="wf-store-badge">@vaicli</span>
5622
+ </div>
5623
+ <div class="wf-store-search-wrap">
5624
+ <span class="wf-store-search-icon">⌕</span>
5625
+ <input class="wf-store-search" id="wfStoreSearch" placeholder="Search..." oninput="wfStoreRender()">
5626
+ </div>
5627
+ <select class="wf-store-sort" id="wfStoreSort" onchange="wfStoreRender()">
5628
+ <option value="downloads">Popular</option>
5629
+ <option value="name">Name A–Z</option>
5630
+ <option value="complexity">Complex</option>
5631
+ </select>
5632
+ </div>
5633
+ <div class="wf-store-body">
5634
+ <div class="wf-store-inner" id="wfStoreContent">
5635
+ <div style="padding:40px;text-align:center;color:var(--text-muted);">Loading catalog...</div>
5636
+ </div>
5637
+ </div>
5638
+ </div>
4384
5639
  </div>
4385
5640
  <div class="wf-inspector collapsed" id="wfInspector">
4386
5641
  <button class="wf-inspector-toggle" id="wfInspectorToggle" onclick="wfToggleInspector()" title="Toggle inspector">&lsaquo;</button>
@@ -4411,6 +5666,21 @@ Reranking models rescore initial search results to improve relevance ordering.</
4411
5666
  </div>
4412
5667
  </div>
4413
5668
 
5669
+ <!-- ── Workflow Input Modal (pre-execution) ── -->
5670
+ <div class="wf-input-modal-backdrop" id="wfInputModalBackdrop" style="display:none;" onclick="wfCloseInputModal()">
5671
+ <div class="wf-input-modal" onclick="event.stopPropagation()">
5672
+ <div class="wf-input-modal-header">
5673
+ <span class="wf-input-modal-title" id="wfInputModalTitle">Workflow Inputs</span>
5674
+ <button class="wf-output-modal-btn close" onclick="wfCloseInputModal()" title="Close">&times;</button>
5675
+ </div>
5676
+ <div class="wf-input-modal-body" id="wfInputModalBody"></div>
5677
+ <div class="wf-input-modal-footer">
5678
+ <button class="wf-input-modal-cancel" onclick="wfCloseInputModal()">Cancel</button>
5679
+ <button class="wf-input-modal-run" onclick="wfInputModalSubmit()">Run Workflow</button>
5680
+ </div>
5681
+ </div>
5682
+ </div>
5683
+
4414
5684
  <!-- ========== ABOUT TAB ========== -->
4415
5685
  <div class="tab-panel" id="tab-about" role="tabpanel" aria-labelledby="tab-btn-about" tabindex="0">
4416
5686
  <div class="about-container">
@@ -4421,9 +5691,9 @@ Reranking models rescore initial search results to improve relevance ordering.</
4421
5691
  <div class="about-name">Michael Lynn</div>
4422
5692
  <div class="about-role">Principal Staff Developer Advocate · MongoDB</div>
4423
5693
  <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>
5694
+ <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>
5695
+ <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>
5696
+ <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
5697
  </div>
4428
5698
  </div>
4429
5699
  </div>
@@ -4490,13 +5760,39 @@ Reranking models rescore initial search results to improve relevance ordering.</
4490
5760
  <div class="card" style="margin-top:16px;">
4491
5761
  <div class="about-section" style="padding-bottom:0;">
4492
5762
  <div class="about-section-title">What's New</div>
4493
- <div class="about-text" style="font-size:13px;">
4494
- <strong>v1.26</strong> — Agent workflows with thinking panel &amp; markdown rendering, multi-step tool orchestration<br>
4495
- <strong>v1.25</strong> — Code generation &amp; project scaffolding tabs<br>
4496
- <strong>v1.24</strong> MCP server install/uninstall/status commands, Electron app v1.5<br>
4497
- <strong>v1.23</strong> — MCP server (expose vai tools to AI agents), HTTP transport, bearer auth, 71+ MCP tests<br>
4498
- <strong>v1.22</strong> — RAG chat with smart source labels, configurable system prompts, streaming responses<br>
4499
- <strong>v1.2</strong> — Multimodal tab, 4 new Explore concepts, auto-update, hidden easter egg 🕹️
5763
+ <div class="about-changelog">
5764
+ <details open>
5765
+ <summary><strong>v1.28</strong> — Lucide icon overhaul</summary>
5766
+ <p>Replaced all emoji and filled LeafyGreen icons with clean, stroke-based Lucide SVGs across the entire app — tabs, workflow nodes, action buttons, benchmark page, explore cards, settings, and more. Added reusable icon helper and path library.</p>
5767
+ </details>
5768
+ <details>
5769
+ <summary><strong>v1.27</strong> — Workflow builder improvements</summary>
5770
+ <p>Workflow palette with drag-and-drop node creation, edge drawing, dry-run preview, and builder mode toggle. New workflow node types: query, rerank, search, ingest.</p>
5771
+ </details>
5772
+ <details>
5773
+ <summary><strong>v1.26</strong> — Agent workflows</summary>
5774
+ <p>Agent workflows with thinking panel &amp; markdown rendering, multi-step tool orchestration, interactive DAG visualization with execution animation.</p>
5775
+ </details>
5776
+ <details>
5777
+ <summary><strong>v1.25</strong> — Code generation &amp; scaffolding</summary>
5778
+ <p>Generate embedding/search code snippets and scaffold full project directories with best-practice structure, ready-to-run RAG pipelines.</p>
5779
+ </details>
5780
+ <details>
5781
+ <summary><strong>v1.24</strong> — MCP server management</summary>
5782
+ <p>MCP server install/uninstall/status commands. Electron app v1.5 with signed macOS builds.</p>
5783
+ </details>
5784
+ <details>
5785
+ <summary><strong>v1.23</strong> — MCP server</summary>
5786
+ <p>Expose vai tools to AI agents via MCP protocol. HTTP transport, bearer auth, 71+ MCP tests.</p>
5787
+ </details>
5788
+ <details>
5789
+ <summary><strong>v1.22</strong> — RAG chat</summary>
5790
+ <p>RAG chat with smart source labels, configurable system prompts, streaming responses.</p>
5791
+ </details>
5792
+ <details>
5793
+ <summary><strong>v1.2</strong> — Multimodal &amp; more</summary>
5794
+ <p>Multimodal tab, 4 new Explore concepts, auto-update, hidden easter egg <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/></svg></p>
5795
+ </details>
4500
5796
  </div>
4501
5797
  </div>
4502
5798
  </div>
@@ -4519,11 +5815,11 @@ Reranking models rescore initial search results to improve relevance ordering.</
4519
5815
  <!-- Mode Toggle -->
4520
5816
  <div class="subtabs" role="tablist">
4521
5817
  <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>
5818
+ <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
5819
  Generate Code
4524
5820
  </button>
4525
5821
  <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>
5822
+ <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
5823
  Scaffold Project
4528
5824
  </button>
4529
5825
  </div>
@@ -4665,7 +5961,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4665
5961
  </div>
4666
5962
  <div id="scaffoldWebButtons" style="display:none;">
4667
5963
  <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>
5964
+ <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
5965
  Download ZIP
4670
5966
  </button>
4671
5967
  </div>
@@ -4722,31 +6018,31 @@ Reranking models rescore initial search results to improve relevance ordering.</
4722
6018
  <nav class="settings-nav">
4723
6019
  <div class="settings-nav-header">Settings</div>
4724
6020
  <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>
6021
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
4726
6022
  <span>General</span>
4727
6023
  </button>
4728
6024
  <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>
6025
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-palette"/></svg>
4730
6026
  <span>Appearance</span>
4731
6027
  </button>
4732
6028
  <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>
6029
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-cube"/></svg>
4734
6030
  <span>Models</span>
4735
6031
  </button>
4736
6032
  <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>
6033
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-chat"/></svg>
4738
6034
  <span>Chat</span>
4739
6035
  </button>
4740
6036
  <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>
6037
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-gauge"/></svg>
4742
6038
  <span>Benchmark</span>
4743
6039
  </button>
4744
6040
  <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>
6041
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-shield"/></svg>
4746
6042
  <span>Data &amp; Privacy</span>
4747
6043
  </button>
4748
6044
  <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>
6045
+ <svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-pulse"/></svg>
4750
6046
  <span>Health Check</span>
4751
6047
  </button>
4752
6048
  </nav>
@@ -4768,12 +6064,12 @@ Reranking models rescore initial search results to improve relevance ordering.</
4768
6064
  <div class="settings-row">
4769
6065
  <div class="settings-label">
4770
6066
  <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>
6067
+ <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
6068
  </div>
4773
6069
  <div class="settings-control" style="min-width:260px;">
4774
6070
  <div class="settings-api-field">
4775
6071
  <input type="password" id="settingsApiKey" placeholder="pa-..." autocomplete="off" spellcheck="false">
4776
- <button type="button" id="settingsApiKeyToggle" title="Show/hide key">👁</button>
6072
+ <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
6073
  <button type="button" id="settingsApiKeySave" class="save-btn" title="Save key">Save</button>
4778
6074
  </div>
4779
6075
  </div>
@@ -4801,6 +6097,26 @@ Reranking models rescore initial search results to improve relevance ordering.</
4801
6097
  </select>
4802
6098
  </div>
4803
6099
  </div>
6100
+ <div class="settings-row">
6101
+ <div class="settings-label">
6102
+ <span class="settings-label-text">Default Tab</span>
6103
+ <span class="settings-label-hint">Which tab to open when the app starts</span>
6104
+ </div>
6105
+ <div class="settings-control">
6106
+ <select class="settings-select" id="settingsDefaultTab">
6107
+ <option value="home" selected>Home</option>
6108
+ <option value="embed">Embed</option>
6109
+ <option value="compare">Compare</option>
6110
+ <option value="search">Search</option>
6111
+ <option value="multimodal">Multimodal</option>
6112
+ <option value="generate">Generate</option>
6113
+ <option value="chat">Chat</option>
6114
+ <option value="workflows">Workflows</option>
6115
+ <option value="benchmark">Benchmark</option>
6116
+ <option value="explore">Explore</option>
6117
+ </select>
6118
+ </div>
6119
+ </div>
4804
6120
  </div>
4805
6121
  </div>
4806
6122
 
@@ -4927,7 +6243,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4927
6243
  title="Show/hide API key"
4928
6244
  style="padding:8px 12px;min-width:auto"
4929
6245
  >
4930
- 👁️
6246
+ <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
6247
  </button>
4932
6248
  <button
4933
6249
  class="btn"
@@ -4936,7 +6252,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
4936
6252
  title="Save API key"
4937
6253
  style="padding:8px 12px;min-width:auto"
4938
6254
  >
4939
- 💾
6255
+ <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
6256
  </button>
4941
6257
  </div>
4942
6258
  </div>
@@ -5289,78 +6605,452 @@ async function init() {
5289
6605
  populateModelSelects();
5290
6606
  buildExploreCards();
5291
6607
 
6608
+ // Apply default tab setting
6609
+ const settings = JSON.parse(localStorage.getItem('vai-settings') || '{}');
6610
+ const defaultTab = settings.defaultTab || 'home';
6611
+ switchTab(defaultTab);
6612
+
5292
6613
  // Telemetry
5293
6614
  initTelemetryToggle();
5294
6615
  sendTelemetry('app_launch');
5295
6616
  }
5296
6617
 
5297
- // ── Tabs ──
5298
- function setupTabs() {
5299
- const tabBtns = document.querySelectorAll('.tab-btn');
5300
- const tabList = Array.from(tabBtns);
6618
+ // ── Tabs ──
6619
+ function setupTabs() {
6620
+ const tabBtns = document.querySelectorAll('.tab-btn');
6621
+ const tabList = Array.from(tabBtns);
6622
+
6623
+ tabBtns.forEach(btn => {
6624
+ btn.addEventListener('click', () => {
6625
+ switchTab(btn.dataset.tab);
6626
+ });
6627
+
6628
+ // Keyboard navigation for accessibility
6629
+ btn.addEventListener('keydown', (e) => {
6630
+ const currentIndex = tabList.indexOf(btn);
6631
+ let nextIndex = -1;
6632
+
6633
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
6634
+ e.preventDefault();
6635
+ nextIndex = (currentIndex + 1) % tabList.length;
6636
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
6637
+ e.preventDefault();
6638
+ nextIndex = (currentIndex - 1 + tabList.length) % tabList.length;
6639
+ } else if (e.key === 'Home') {
6640
+ e.preventDefault();
6641
+ nextIndex = 0;
6642
+ } else if (e.key === 'End') {
6643
+ e.preventDefault();
6644
+ nextIndex = tabList.length - 1;
6645
+ }
6646
+
6647
+ if (nextIndex >= 0) {
6648
+ tabList[nextIndex].focus();
6649
+ switchTab(tabList[nextIndex].dataset.tab);
6650
+ }
6651
+ });
6652
+ });
6653
+
6654
+ // Settings cog in header
6655
+ const settingsCog = document.querySelector('.sidebar-settings-btn');
6656
+ if (settingsCog) {
6657
+ settingsCog.addEventListener('click', () => {
6658
+ switchTab(settingsCog.dataset.tab);
6659
+ });
6660
+ }
6661
+ }
6662
+
6663
+ function switchTab(tab) {
6664
+ document.querySelectorAll('.tab-btn').forEach(b => {
6665
+ const isActive = b.dataset.tab === tab;
6666
+ b.classList.toggle('active', isActive);
6667
+ // Update ARIA attributes for accessibility
6668
+ b.setAttribute('aria-selected', isActive ? 'true' : 'false');
6669
+ b.setAttribute('tabindex', isActive ? '0' : '-1');
6670
+ });
6671
+ document.querySelectorAll('.tab-panel').forEach(p => {
6672
+ const isActive = p.id === 'tab-' + tab;
6673
+ p.classList.toggle('active', isActive);
6674
+ // Hide inactive panels from screen readers
6675
+ p.setAttribute('aria-hidden', isActive ? 'false' : 'true');
6676
+ });
6677
+ // Sync settings cog highlight
6678
+ const settingsBtn = document.querySelector('.sidebar-settings-btn');
6679
+ if (settingsBtn) settingsBtn.classList.toggle('active', tab === 'settings');
6680
+ // Track tab views
6681
+ sendTelemetry('tab_view', { tab });
6682
+
6683
+ // Initialize Home page if switching to it
6684
+ if (tab === 'home') {
6685
+ homeInit();
6686
+ }
6687
+ }
6688
+ // Expose globally so Electron main process can call it
6689
+ window.switchTab = switchTab;
6690
+
6691
+ // ── Home Page ──
6692
+
6693
+ let homeData = {
6694
+ announcements: null,
6695
+ releases: null,
6696
+ featuredWorkflows: null,
6697
+ communityWorkflows: null,
6698
+ currentAnnouncementIndex: 0,
6699
+ sortMode: 'trending'
6700
+ };
6701
+
6702
+ async function homeInit() {
6703
+ // Only initialize once per session
6704
+ if (homeData.announcements !== null) return;
6705
+
6706
+ try {
6707
+ // Load version info
6708
+ await updateHomeVersionInfo();
6709
+
6710
+ // Check API key status and show warning if needed
6711
+ checkApiKeyStatus();
6712
+
6713
+ // Load announcements and releases first (fast, local data)
6714
+ await Promise.all([
6715
+ loadAnnouncements(),
6716
+ loadReleases(),
6717
+ ]);
6718
+
6719
+ // Render available sections immediately
6720
+ renderAnnouncements();
6721
+ renderReleases();
6722
+
6723
+ // Load marketplace data in background (slower, network-dependent)
6724
+ loadMarketplaceData().then(() => {
6725
+ renderFeaturedWorkflows();
6726
+ renderCommunityWorkflows();
6727
+ }).catch(() => {});
6728
+
6729
+ } catch (err) {
6730
+ console.error('Failed to initialize Home page:', err);
6731
+ }
6732
+ }
6733
+
6734
+ async function updateHomeVersionInfo() {
6735
+ let version = window.appVersion;
6736
+
6737
+ // If version not set yet, try to fetch from Electron
6738
+ if (!version && window.vai && window.vai.getVersion) {
6739
+ try {
6740
+ const v = await window.vai.getVersion();
6741
+ if (v) {
6742
+ version = typeof v === 'object' ? v.app : v;
6743
+ window.appVersion = version;
6744
+ }
6745
+ } catch (e) {
6746
+ console.warn('Failed to get version:', e);
6747
+ }
6748
+ }
6749
+
6750
+ // Fallback for web mode: try to get from CLI package.json via API
6751
+ if (!version) {
6752
+ try {
6753
+ const res = await fetch('/api/version');
6754
+ if (res.ok) {
6755
+ const data = await res.json();
6756
+ version = data.version || data.app;
6757
+ if (version) window.appVersion = version;
6758
+ }
6759
+ } catch (e) {
6760
+ // Ignore fetch errors in web mode
6761
+ }
6762
+ }
6763
+
6764
+ version = version || 'dev';
6765
+ document.getElementById('homeVersionBadge').textContent = `v${version}`;
6766
+ const footerEl = document.getElementById('homeFooterVersion');
6767
+ if (footerEl) footerEl.textContent = `Version ${version} • © 2026 VAI`;
6768
+ }
6769
+
6770
+ async function checkApiKeyStatus() {
6771
+ try {
6772
+ const res = await fetch('/api/config');
6773
+ const data = await res.json();
6774
+ const warningEl = document.getElementById('homeApiWarning');
6775
+
6776
+ if (!data.hasKey) {
6777
+ warningEl.style.display = 'block';
6778
+ } else {
6779
+ warningEl.style.display = 'none';
6780
+ }
6781
+ } catch {
6782
+ // If config check fails, assume no API key
6783
+ document.getElementById('homeApiWarning').style.display = 'block';
6784
+ }
6785
+ }
6786
+
6787
+ async function loadAnnouncements() {
6788
+ try {
6789
+ const res = await fetch('/api/home/announcements');
6790
+ const data = await res.json();
6791
+
6792
+ // Filter out dismissed announcements
6793
+ const dismissed = JSON.parse(localStorage.getItem('vai-dismissed-announcements') || '[]');
6794
+ homeData.announcements = data.announcements.filter(a => !dismissed.includes(a.id));
6795
+
6796
+ } catch (err) {
6797
+ console.error('Failed to load announcements:', err);
6798
+ homeData.announcements = [];
6799
+ }
6800
+ }
6801
+
6802
+ async function loadReleases() {
6803
+ try {
6804
+ // Try cache first
6805
+ const cached = localStorage.getItem('vai-releases-cache');
6806
+ if (cached) {
6807
+ const { data, timestamp } = JSON.parse(cached);
6808
+ // Use cache if less than 30 minutes old AND it has real data (not fallback)
6809
+ const hasRealData = data && data.length > 0 && !data[0].version?.includes('1.0.0');
6810
+ if (hasRealData && Date.now() - timestamp < 30 * 60 * 1000) {
6811
+ homeData.releases = data;
6812
+ return;
6813
+ }
6814
+ }
6815
+
6816
+ const res = await fetch('/api/home/releases');
6817
+ const data = await res.json();
6818
+ homeData.releases = data.releases;
6819
+
6820
+ // Only cache if we got real data (not fallback)
6821
+ const hasRealData = data.releases && data.releases.length > 0 && !data.releases[0].version?.includes('1.0.0');
6822
+ if (hasRealData) {
6823
+ localStorage.setItem('vai-releases-cache', JSON.stringify({
6824
+ data: data.releases,
6825
+ timestamp: Date.now()
6826
+ }));
6827
+ } else {
6828
+ // Clear stale cache if we got fallback data
6829
+ localStorage.removeItem('vai-releases-cache');
6830
+ }
6831
+
6832
+ } catch (err) {
6833
+ console.error('Failed to load releases:', err);
6834
+ homeData.releases = [];
6835
+ }
6836
+ }
6837
+
6838
+ async function loadMarketplaceData() {
6839
+ try {
6840
+ const res = await fetch('/api/workflows/catalog');
6841
+ const data = await res.json();
6842
+
6843
+ // Extract featured and community workflows
6844
+ homeData.featuredWorkflows = data.workflows.filter(w => w.featured).slice(0, 4);
6845
+ homeData.communityWorkflows = data.workflows.filter(w => !w.featured);
6846
+
6847
+ } catch (err) {
6848
+ console.error('Failed to load marketplace data:', err);
6849
+ homeData.featuredWorkflows = [];
6850
+ homeData.communityWorkflows = [];
6851
+ }
6852
+ }
6853
+
6854
+ function renderAnnouncements() {
6855
+ const container = document.getElementById('homeAnnouncements');
6856
+ const carousel = document.getElementById('announcementsCarousel');
6857
+ const dots = document.getElementById('announcementsDots');
6858
+
6859
+ if (!homeData.announcements.length) {
6860
+ container.style.display = 'none';
6861
+ return;
6862
+ }
6863
+
6864
+ // Render cards
6865
+ carousel.innerHTML = homeData.announcements.map((ann, i) => `
6866
+ <div class="home-announcement-card${i === 0 ? ' active' : ''}" data-id="${ann.id}">
6867
+ <button class="home-announcement-dismiss" onclick="dismissAnnouncement('${ann.id}')">×</button>
6868
+ ${ann.badge ? `<div class="badge">${ann.badge}</div>` : ''}
6869
+ <h3>${ann.title}</h3>
6870
+ <p>${ann.description}</p>
6871
+ ${ann.cta ? `<button class="cta" onclick="${ann.cta.action === 'navigate' ? `switchTab('${ann.cta.target.slice(1)}')` : 'void(0)'}">${ann.cta.label}</button>` : ''}
6872
+ </div>
6873
+ `).join('');
6874
+
6875
+ // Render dots
6876
+ if (homeData.announcements.length > 1) {
6877
+ dots.innerHTML = homeData.announcements.map((_, i) =>
6878
+ `<div class="home-announcement-dot${i === 0 ? ' active' : ''}" onclick="showAnnouncement(${i})"></div>`
6879
+ ).join('');
6880
+
6881
+ // Start rotation
6882
+ startAnnouncementRotation();
6883
+ }
6884
+
6885
+ container.style.display = 'block';
6886
+ }
6887
+
6888
+ function showAnnouncement(index) {
6889
+ homeData.currentAnnouncementIndex = index;
6890
+
6891
+ // Update cards
6892
+ document.querySelectorAll('.home-announcement-card').forEach((card, i) => {
6893
+ card.classList.toggle('active', i === index);
6894
+ });
6895
+
6896
+ // Update dots
6897
+ document.querySelectorAll('.home-announcement-dot').forEach((dot, i) => {
6898
+ dot.classList.toggle('active', i === index);
6899
+ });
6900
+ }
6901
+
6902
+ function startAnnouncementRotation() {
6903
+ setInterval(() => {
6904
+ if (homeData.announcements.length > 1) {
6905
+ homeData.currentAnnouncementIndex = (homeData.currentAnnouncementIndex + 1) % homeData.announcements.length;
6906
+ showAnnouncement(homeData.currentAnnouncementIndex);
6907
+ }
6908
+ }, 5000); // Rotate every 5 seconds
6909
+ }
6910
+
6911
+ function dismissAnnouncement(id) {
6912
+ const dismissed = JSON.parse(localStorage.getItem('vai-dismissed-announcements') || '[]');
6913
+ dismissed.push(id);
6914
+ localStorage.setItem('vai-dismissed-announcements', JSON.stringify(dismissed));
6915
+
6916
+ // Remove from current data and re-render
6917
+ homeData.announcements = homeData.announcements.filter(a => a.id !== id);
6918
+ renderAnnouncements();
6919
+ }
6920
+
6921
+ function renderReleases() {
6922
+ const container = document.getElementById('releasesTimeline');
6923
+
6924
+ const releases = homeData.releases.slice(0, 1); // Show only the latest release
6925
+ container.innerHTML = releases.map(release => `
6926
+ <div class="home-release-item">
6927
+ <div>
6928
+ <div class="home-release-version">${release.version}</div>
6929
+ <div class="home-release-date">${new Date(release.date).toLocaleDateString()}</div>
6930
+ <ul class="home-release-highlights">
6931
+ ${release.highlights.map(highlight => `<li>${highlight}</li>`).join('')}
6932
+ </ul>
6933
+ </div>
6934
+ </div>
6935
+ `).join('');
6936
+ }
6937
+
6938
+ function renderFeaturedWorkflows() {
6939
+ const container = document.getElementById('featuredWorkflows');
6940
+
6941
+ if (!homeData.featuredWorkflows.length) {
6942
+ container.innerHTML = '<div class="home-community-empty"><h4>Coming Soon</h4><p>Featured workflows launching soon</p></div>';
6943
+ return;
6944
+ }
6945
+
6946
+ container.innerHTML = homeData.featuredWorkflows.map(workflow => `
6947
+ <div class="home-workflow-card">
6948
+ <h5>${workflow.name || 'Untitled'}</h5>
6949
+ <p>${workflow.description || 'No description available'}</p>
6950
+ <div class="home-workflow-meta">
6951
+ <div class="home-workflow-domain">${workflow.category || 'utility'}</div>
6952
+ <div class="home-workflow-author">by ${workflow.author?.name || 'unknown'}</div>
6953
+ </div>
6954
+ <div class="home-workflow-actions">
6955
+ <button class="home-workflow-btn" onclick="installWorkflow('${workflow.packageName}')">Install</button>
6956
+ <button class="home-workflow-btn" onclick="viewWorkflowDetails('${workflow.packageName}')">Details</button>
6957
+ </div>
6958
+ </div>
6959
+ `).join('');
6960
+ }
6961
+
6962
+ function renderCommunityWorkflows() {
6963
+ const container = document.getElementById('communityWorkflows');
6964
+
6965
+ if (!homeData.communityWorkflows.length) {
6966
+ container.innerHTML = '<div class="home-community-empty"><h4>Be the First!</h4><p>Publish a workflow to get started</p></div>';
6967
+ return;
6968
+ }
6969
+
6970
+ let workflows = [...homeData.communityWorkflows];
6971
+
6972
+ // Sort based on current mode
6973
+ switch (homeData.sortMode) {
6974
+ case 'trending':
6975
+ // Use downloads as proxy for trending
6976
+ workflows.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
6977
+ break;
6978
+ case 'newest':
6979
+ // Sort by name alphabetically as fallback (no publish date available)
6980
+ workflows.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
6981
+ break;
6982
+ case 'installs':
6983
+ workflows.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
6984
+ break;
6985
+ }
6986
+
6987
+ container.innerHTML = workflows.slice(0, 6).map(workflow => `
6988
+ <div class="home-workflow-card">
6989
+ <h5>${workflow.name || 'Untitled'}</h5>
6990
+ <p>${workflow.description || 'No description available'}</p>
6991
+ <div class="home-workflow-meta">
6992
+ <div class="home-workflow-domain">${workflow.category || 'utility'}</div>
6993
+ <div class="home-workflow-author">by ${workflow.author?.name || 'unknown'}</div>
6994
+ </div>
6995
+ <div class="home-workflow-actions">
6996
+ <button class="home-workflow-btn" onclick="installWorkflow('${workflow.packageName}')">Install</button>
6997
+ <button class="home-workflow-btn" onclick="viewWorkflowDetails('${workflow.packageName}')">Details</button>
6998
+ </div>
6999
+ </div>
7000
+ `).join('');
7001
+ }
7002
+
7003
+ function sortCommunityWorkflows(mode) {
7004
+ homeData.sortMode = mode;
5301
7005
 
5302
- tabBtns.forEach(btn => {
5303
- btn.addEventListener('click', () => {
5304
- switchTab(btn.dataset.tab);
5305
- });
5306
-
5307
- // Keyboard navigation for accessibility
5308
- btn.addEventListener('keydown', (e) => {
5309
- const currentIndex = tabList.indexOf(btn);
5310
- let nextIndex = -1;
5311
-
5312
- if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
5313
- e.preventDefault();
5314
- nextIndex = (currentIndex + 1) % tabList.length;
5315
- } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
5316
- e.preventDefault();
5317
- nextIndex = (currentIndex - 1 + tabList.length) % tabList.length;
5318
- } else if (e.key === 'Home') {
5319
- e.preventDefault();
5320
- nextIndex = 0;
5321
- } else if (e.key === 'End') {
5322
- e.preventDefault();
5323
- nextIndex = tabList.length - 1;
5324
- }
5325
-
5326
- if (nextIndex >= 0) {
5327
- tabList[nextIndex].focus();
5328
- switchTab(tabList[nextIndex].dataset.tab);
5329
- }
5330
- });
7006
+ // Update tab appearance
7007
+ document.querySelectorAll('.home-sort-tab').forEach(tab => {
7008
+ tab.classList.toggle('active', tab.dataset.sort === mode);
5331
7009
  });
7010
+
7011
+ renderCommunityWorkflows();
7012
+ }
5332
7013
 
5333
- // Settings cog in header
5334
- const settingsCog = document.querySelector('.sidebar-settings-btn');
5335
- if (settingsCog) {
5336
- settingsCog.addEventListener('click', () => {
5337
- switchTab(settingsCog.dataset.tab);
5338
- });
7014
+ function filterAndSwitchWorkflows(domain) {
7015
+ // Switch to workflows tab with domain filter
7016
+ switchTab('workflows');
7017
+ // TODO: Apply domain filter when workflows tab supports it
7018
+ console.log('Filtering workflows by domain:', domain);
7019
+ }
7020
+
7021
+ function installWorkflow(packageName) {
7022
+ // Use the workflow store install function
7023
+ wfStoreInstall(packageName, null);
7024
+ }
7025
+
7026
+ function viewWorkflowDetails(packageName) {
7027
+ // Find the workflow by packageName and show its details
7028
+ const allWorkflows = [...(homeData.featuredWorkflows || []), ...(homeData.communityWorkflows || [])];
7029
+ const wf = allWorkflows.find(w => w.packageName === packageName);
7030
+ if (wf) {
7031
+ // Use the workflow store detail modal
7032
+ wfStoreShowDetail(wf.name);
7033
+ } else {
7034
+ // Fallback: switch to workflows tab
7035
+ switchTab('workflows');
5339
7036
  }
5340
7037
  }
5341
7038
 
5342
- function switchTab(tab) {
5343
- document.querySelectorAll('.tab-btn').forEach(b => {
5344
- const isActive = b.dataset.tab === tab;
5345
- b.classList.toggle('active', isActive);
5346
- // Update ARIA attributes for accessibility
5347
- b.setAttribute('aria-selected', isActive ? 'true' : 'false');
5348
- b.setAttribute('tabindex', isActive ? '0' : '-1');
5349
- });
5350
- document.querySelectorAll('.tab-panel').forEach(p => {
5351
- const isActive = p.id === 'tab-' + tab;
5352
- p.classList.toggle('active', isActive);
5353
- // Hide inactive panels from screen readers
5354
- p.setAttribute('aria-hidden', isActive ? 'false' : 'true');
5355
- });
5356
- // Sync settings cog highlight
5357
- const settingsBtn = document.querySelector('.sidebar-settings-btn');
5358
- if (settingsBtn) settingsBtn.classList.toggle('active', tab === 'settings');
5359
- // Track tab views
5360
- sendTelemetry('tab_view', { tab });
7039
+ function publishWorkflow() {
7040
+ // TODO: Open workflow publishing flow
7041
+ console.log('Opening workflow publisher');
7042
+ switchTab('workflows');
7043
+ }
7044
+
7045
+ function reportBug() {
7046
+ // Open bug report (reuse existing functionality if available)
7047
+ const bugButton = document.getElementById('bugButton');
7048
+ if (bugButton) {
7049
+ bugButton.click();
7050
+ } else {
7051
+ window.open('https://github.com/mrlynn/voyageai-cli/issues/new', '_blank');
7052
+ }
5361
7053
  }
5362
- // Expose globally so Electron main process can call it
5363
- window.switchTab = switchTab;
5364
7054
 
5365
7055
  // ── Config ──
5366
7056
  async function loadConfig() {
@@ -5952,12 +7642,12 @@ async function downloadScaffoldZip() {
5952
7642
  // Show success
5953
7643
  btn.innerHTML = '<span style="margin-right:6px;">✓</span> Downloaded!';
5954
7644
  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';
7645
+ 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
7646
  btn.disabled = false;
5957
7647
  }, 2000);
5958
7648
  } catch (err) {
5959
7649
  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';
7650
+ 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
7651
  btn.disabled = false;
5962
7652
  }
5963
7653
  }
@@ -6053,37 +7743,73 @@ document.addEventListener('DOMContentLoaded', () => {
6053
7743
  });
6054
7744
 
6055
7745
  // ── Explore: icons and tab mappings per concept ──
7746
+ // Lucide SVG icon helper — returns an inline <svg> string
7747
+ function lucideIcon(d, size = 16) {
7748
+ 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>`;
7749
+ }
7750
+
7751
+ // Lucide path constants for concept/button icons
7752
+ const LI = {
7753
+ zap: 'M13 2 3 14h9l-1 8 10-12h-9l1-8z',
7754
+ search: 'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
7755
+ 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',
7756
+ 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',
7757
+ 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',
7758
+ 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',
7759
+ 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',
7760
+ 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',
7761
+ 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',
7762
+ 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',
7763
+ 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',
7764
+ timer: 'M10 2h4M12 14l3-3M12 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
7765
+ 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',
7766
+ 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',
7767
+ 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',
7768
+ barChart: 'M12 20V10M18 20V4M6 20v-4',
7769
+ 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',
7770
+ 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',
7771
+ 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',
7772
+ circle: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0',
7773
+ 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',
7774
+ 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',
7775
+ laptop: 'M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9M2 20h20M12 12h.01',
7776
+ blocks: 'M2 12h10v10H2zM14 4l6 3.5v7L14 18l-6-3.5v-7zM12 2l10 6',
7777
+ refresh: 'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16',
7778
+ 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',
7779
+ download: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3',
7780
+ 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',
7781
+ filter: 'M3 6h18M7 12h10M10 18h4',
7782
+ };
7783
+
6056
7784
  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' },
7785
+ embeddings: { icon: LI.zap, tab: 'embed' },
7786
+ reranking: { icon: LI.trophy, tab: 'search' },
7787
+ 'vector-search': { icon: LI.search, tab: 'search' },
7788
+ rag: { icon: LI.bot, tab: 'search' },
7789
+ 'cosine-similarity': { icon: LI.ruler, tab: 'compare' },
7790
+ 'two-stage-retrieval': { icon: LI.target, tab: 'search' },
7791
+ 'input-type': { icon: LI.tag, tab: 'embed' },
7792
+ models: { icon: LI.brain, tab: 'embed' },
7793
+ 'api-keys': { icon: LI.key, tab: 'embed' },
7794
+ 'api-access': { icon: LI.globe, tab: 'embed' },
7795
+ 'batch-processing': { icon: LI.package, tab: 'embed' },
7796
+ benchmarking: { icon: LI.timer, tab: 'benchmark' },
7797
+ quantization: { icon: LI.flask, tab: 'benchmark' },
7798
+ 'mixture-of-experts': { icon: LI.puzzle, tab: 'embed' },
7799
+ 'shared-embedding-space': { icon: LI.link, tab: 'compare' },
7800
+ 'rteb-benchmarks': { icon: LI.barChart, tab: 'benchmark' },
7801
+ 'voyage-4-nano': { icon: LI.microscope, tab: 'embed' },
7802
+ 'rerank-eval': { icon: LI.ruler, tab: 'benchmark' },
7803
+ 'multimodal-embeddings': { icon: LI.image, tab: 'multimodal' },
7804
+ 'cross-modal-search': { icon: LI.shuffle, tab: 'multimodal' },
7805
+ 'modality-gap': { icon: LI.circle, tab: 'multimodal' },
7806
+ 'multimodal-rag': { icon: LI.fileTxt, tab: 'multimodal' },
7807
+ 'provider-comparison': { icon: LI.scale, tab: 'explore' },
7808
+ 'code-generation': { icon: LI.laptop, tab: 'explore' },
7809
+ scaffolding: { icon: LI.blocks, tab: 'explore' },
7810
+ 'eval-comparison': { icon: LI.barChart, tab: 'benchmark' },
7811
+ 'auto-embedding': { icon: LI.zap, tab: 'explore' },
7812
+ 'vai-vs-auto-embedding': { icon: LI.refresh, tab: 'explore' },
6087
7813
  };
6088
7814
 
6089
7815
  let exploreConcepts = {};
@@ -6109,13 +7835,13 @@ function buildExploreCards() {
6109
7835
  grid.innerHTML = '';
6110
7836
 
6111
7837
  for (const [key, concept] of Object.entries(exploreConcepts)) {
6112
- const meta = CONCEPT_META[key] || { icon: '📚', tab: 'embed' };
7838
+ const meta = CONCEPT_META[key] || { icon: LI.package, tab: 'embed' };
6113
7839
  const card = document.createElement('div');
6114
7840
  card.className = 'explore-card';
6115
7841
  card.dataset.key = key;
6116
7842
 
6117
7843
  card.innerHTML = `
6118
- <div class="explore-card-icon">${meta.icon}</div>
7844
+ <div class="explore-card-icon">${lucideIcon(meta.icon, 28)}</div>
6119
7845
  <div class="explore-card-title">${escapeHtml(concept.title)}</div>
6120
7846
  <div class="explore-card-summary">${escapeHtml(concept.summary)}</div>
6121
7847
  `;
@@ -6142,12 +7868,12 @@ let exploreModalPreviousFocus = null;
6142
7868
  function openExploreModal(key) {
6143
7869
  const concept = exploreConcepts[key];
6144
7870
  if (!concept) return;
6145
- const meta = CONCEPT_META[key] || { icon: '📚', tab: 'embed' };
7871
+ const meta = CONCEPT_META[key] || { icon: LI.package, tab: 'embed' };
6146
7872
 
6147
7873
  // Save the currently focused element to restore when modal closes
6148
7874
  exploreModalPreviousFocus = document.activeElement;
6149
7875
 
6150
- document.getElementById('exploreModalIcon').textContent = meta.icon;
7876
+ document.getElementById('exploreModalIcon').innerHTML = lucideIcon(meta.icon, 32);
6151
7877
  document.getElementById('exploreModalTitle').textContent = concept.title;
6152
7878
  document.getElementById('exploreModalSummary').textContent = concept.summary;
6153
7879
 
@@ -7250,6 +8976,15 @@ function initSettings() {
7250
8976
  timeoutSel.addEventListener('change', () => saveSetting('timeout', timeoutSel.value));
7251
8977
  }
7252
8978
 
8979
+ // Default Tab
8980
+ const defaultTabSel = document.getElementById('settingsDefaultTab');
8981
+ if (defaultTabSel) {
8982
+ defaultTabSel.value = s.defaultTab || 'home';
8983
+ defaultTabSel.addEventListener('change', () => {
8984
+ saveSetting('defaultTab', defaultTabSel.value);
8985
+ });
8986
+ }
8987
+
7253
8988
  // Benchmark iterations
7254
8989
  const benchIterSel = document.getElementById('settingsBenchIter');
7255
8990
  if (benchIterSel) {
@@ -9790,19 +11525,33 @@ const WF_NODE_META = {
9790
11525
  search: { icon: '\u{1F50E}', label: 'Vector Search', color: '#40E0FF', category: 'retrieval' },
9791
11526
  rerank: { icon: '\u{1F3C6}', label: 'Rerank', color: '#40E0FF', category: 'retrieval' },
9792
11527
  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' },
11528
+ embed: { icon: 'M13 2 3 14h9l-1 8 10-12h-9l1-8z', label: 'Embed', color: '#B388FF', category: 'embedding' },
11529
+ similarity: { icon: 'M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4', label: 'Similarity', color: '#B388FF', category: 'embedding' },
11530
+ 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' },
11531
+ 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' },
11532
+ 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' },
11533
+ 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' },
11534
+ 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' },
11535
+ 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' },
11536
+ filter: { icon: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z', label: 'Filter', color: '#90A4AE', category: 'control' },
11537
+ 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' },
11538
+ 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' },
11539
+ 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' },
11540
+ rerank: { icon: 'M3 6h18M7 12h10M10 18h4', label: 'Rerank', color: '#CE93D8', category: 'retrieval' },
11541
+ 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' },
11542
+ 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' },
11543
+ // Phase 1-3: New workflow nodes
11544
+ conditional: { icon: 'M12 3l9 9-9 9-9-9z', label: 'Conditional', color: '#90A4AE', category: 'control', shape: 'diamond' },
11545
+ loop: { icon: 'M17 1l4 4-4 4M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4M21 13v2a4 4 0 0 1-4 4H3', label: 'Loop', color: '#90A4AE', category: 'control' },
11546
+ template: { icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M16 13H8M16 17H8M10 9H8', label: 'Template', color: '#90A4AE', category: 'control' },
11547
+ chunk: { icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M9 15h6M9 11h6M9 19h4', label: 'Chunk', color: '#00D4AA', category: 'processing' },
11548
+ aggregate: { icon: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z', label: 'Aggregate', color: '#00D4AA', category: 'processing' },
11549
+ http: { icon: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z', label: 'HTTP Request', color: '#F5A623', category: 'integration' },
9804
11550
  };
9805
11551
 
11552
+ // Fallback icon (gear) for unknown workflow node types
11553
+ 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';
11554
+
9806
11555
  const WF_NODE_W = 180;
9807
11556
  const WF_NODE_H = 64;
9808
11557
  const WF_LAYER_GAP = 260;
@@ -9812,6 +11561,7 @@ const WF_PORT_R = 5; // Port circle radius
9812
11561
 
9813
11562
  let wfState = {
9814
11563
  workflows: [],
11564
+ examples: [],
9815
11565
  activeWorkflow: null,
9816
11566
  selectedNodeId: null,
9817
11567
  executionState: {},
@@ -9825,14 +11575,26 @@ let wfState = {
9825
11575
  isPanning: false,
9826
11576
  panStart: { x: 0, y: 0 },
9827
11577
  executing: false,
11578
+ // Builder state
11579
+ builderMode: false,
11580
+ draggingEdge: null,
11581
+ dragNode: null,
11582
+ dirtyFlag: false,
9828
11583
  };
9829
11584
 
9830
11585
  // ── Library ──
9831
11586
  async function wfLoadLibrary() {
9832
11587
  try {
9833
- const res = await fetch('/api/workflows');
9834
- const data = await res.json();
9835
- wfState.workflows = data.workflows || [];
11588
+ const [wfRes, exRes] = await Promise.all([
11589
+ fetch('/api/workflows'),
11590
+ fetch('/api/workflows/examples'),
11591
+ ]);
11592
+ const wfData = await wfRes.json();
11593
+ const exData = await exRes.json();
11594
+ wfState.workflows = wfData.workflows || [];
11595
+ wfState.official = wfData.official || [];
11596
+ wfState.community = wfData.community || [];
11597
+ wfState.examples = exData.examples || [];
9836
11598
  wfRenderLibrary();
9837
11599
  } catch (err) {
9838
11600
  const list = document.getElementById('wfLibraryList');
@@ -9843,19 +11605,213 @@ async function wfLoadLibrary() {
9843
11605
  function wfRenderLibrary() {
9844
11606
  const list = document.getElementById('wfLibraryList');
9845
11607
  if (!list) return;
9846
- if (wfState.workflows.length === 0) {
11608
+ if (wfState.workflows.length === 0 && (!wfState.examples || wfState.examples.length === 0)) {
9847
11609
  list.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;">No workflows found</div>';
9848
11610
  return;
9849
11611
  }
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
11612
+
11613
+ // Built-in templates
11614
+ let html = wfState.workflows.map(w => {
9853
11615
  const displayName = w.name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
9854
11616
  return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
9855
11617
  <div class="wf-library-item-name">${displayName}</div>
9856
11618
  <div class="wf-library-item-desc">${w.description || ''}</div>
9857
11619
  </div>`;
9858
11620
  }).join('');
11621
+
11622
+ // Collapsible examples section
11623
+ const examples = wfState.examples || [];
11624
+ if (examples.length > 0) {
11625
+ html += `<div class="wf-library-section">
11626
+ <button class="wf-library-section-toggle" onclick="wfToggleExamples(this)">
11627
+ <span class="arrow">&#9654;</span> Examples (${examples.length})
11628
+ </button>
11629
+ <div class="wf-examples-content" style="display:none;">`;
11630
+
11631
+ const categories = ['Retrieval', 'RAG', 'Ingestion', 'Analysis', 'Other'];
11632
+ for (const cat of categories) {
11633
+ const items = examples.filter(e => e.category === cat);
11634
+ if (items.length === 0) continue;
11635
+ html += `<div class="wf-library-category">${cat}</div>`;
11636
+ html += items.map(w => {
11637
+ const displayName = w.name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
11638
+ return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
11639
+ <div class="wf-library-item-name">${displayName}</div>
11640
+ <div class="wf-library-item-desc">${w.description || ''}</div>
11641
+ </div>`;
11642
+ }).join('');
11643
+ }
11644
+ html += '</div></div>';
11645
+ }
11646
+
11647
+ // Official catalog section
11648
+ const official = wfState.official || [];
11649
+ html += `<div class="wf-library-section">
11650
+ <div class="wf-library-category" style="display:flex;align-items:center;justify-content:space-between;">
11651
+ <span>Official Catalog (@vaicli) (${official.length})</span>
11652
+ </div>`;
11653
+ if (official.length === 0) {
11654
+ html += '<div style="padding:8px 12px;color:var(--text-muted);font-size:11px;">No official workflows installed.</div>';
11655
+ } else {
11656
+ html += official.map(w => {
11657
+ const displayName = w.name.replace(/^@vaicli\/vai-workflow-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
11658
+ const authorLine = w.author ? `<span style="color:var(--text-muted);font-size:10px;">by ${w.author}${w.version ? ' · v' + w.version : ''}</span>` : '';
11659
+ const tagLine = (w.tags || []).length ? `<div style="margin-top:2px;font-size:10px;color:var(--text-muted);">${w.tags.join(' · ')}</div>` : '';
11660
+ return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
11661
+ <div class="wf-library-item-name">✓ ${displayName}</div>
11662
+ <div class="wf-library-item-desc">${w.description || ''}</div>
11663
+ ${authorLine}${tagLine}
11664
+ </div>`;
11665
+ }).join('');
11666
+ }
11667
+ html += '</div>';
11668
+
11669
+ // Community workflows section
11670
+ const community = wfState.community || [];
11671
+ html += `<div class="wf-library-section">
11672
+ <div class="wf-library-category" style="display:flex;align-items:center;justify-content:space-between;">
11673
+ <span>Community (${community.length})</span>
11674
+ <button onclick="wfLoadLibrary()" title="Refresh" style="background:none;border:none;cursor:pointer;color:var(--text-muted);font-size:12px;display:flex;align-items:center;"><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="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg></button>
11675
+ </div>`;
11676
+ if (community.length === 0) {
11677
+ html += '<div style="padding:8px 12px;color:var(--text-muted);font-size:11px;">No community workflows installed.<br><code style="font-size:10px;">vai workflow install &lt;name&gt;</code></div>';
11678
+ } else {
11679
+ html += community.map(w => {
11680
+ const displayName = w.name.replace(/^vai-workflow-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
11681
+ const authorLine = w.author ? `<span style="color:var(--text-muted);font-size:10px;">by ${w.author}${w.version ? ' · v' + w.version : ''}</span>` : '';
11682
+ const tagLine = (w.tags || []).length ? `<div style="margin-top:2px;font-size:10px;color:var(--text-muted);">${w.tags.join(' · ')}</div>` : '';
11683
+ return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
11684
+ <div class="wf-library-item-name">🌐 ${displayName}</div>
11685
+ <div class="wf-library-item-desc">${w.description || ''}</div>
11686
+ ${authorLine}${tagLine}
11687
+ </div>`;
11688
+ }).join('');
11689
+ }
11690
+ html += '</div>';
11691
+
11692
+ // Install from npm button
11693
+ html += `<div style="padding:8px 12px;">
11694
+ <button onclick="wfShowInstallDialog()" style="background:none;border:1px dashed var(--border);color:var(--accent);cursor:pointer;padding:6px 12px;border-radius:6px;font-size:11px;width:100%;text-align:center;">+ Install from npm</button>
11695
+ </div>`;
11696
+
11697
+ list.innerHTML = html;
11698
+ }
11699
+
11700
+ // ── Install Dialog ──
11701
+ function wfShowInstallDialog() {
11702
+ let modal = document.getElementById('wfInstallModal');
11703
+ if (!modal) {
11704
+ modal = document.createElement('div');
11705
+ modal.id = 'wfInstallModal';
11706
+ modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:9999;';
11707
+ modal.innerHTML = `
11708
+ <div style="background:var(--bg-panel);border:1px solid var(--border);border-radius:12px;padding:24px;width:480px;max-height:70vh;display:flex;flex-direction:column;">
11709
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
11710
+ <h3 style="margin:0;font-size:16px;">Install Workflow Package</h3>
11711
+ <button onclick="wfCloseInstallDialog()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:18px;">✕</button>
11712
+ </div>
11713
+ <div style="display:flex;gap:8px;margin-bottom:12px;">
11714
+ <input id="wfInstallSearch" type="text" placeholder="Search vai workflows on npm..." style="flex:1;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;" onkeydown="if(event.key==='Enter')wfSearchNpm()">
11715
+ <button onclick="wfSearchNpm()" style="padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">Search</button>
11716
+ </div>
11717
+ <div id="wfInstallResults" style="overflow-y:auto;flex:1;min-height:100px;"></div>
11718
+ </div>`;
11719
+ document.body.appendChild(modal);
11720
+ modal.addEventListener('click', (e) => { if (e.target === modal) wfCloseInstallDialog(); });
11721
+ }
11722
+ modal.style.display = 'flex';
11723
+ document.getElementById('wfInstallSearch').value = '';
11724
+ document.getElementById('wfInstallResults').innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;text-align:center;">Search for vai-workflow packages on npm</div>';
11725
+ document.getElementById('wfInstallSearch').focus();
11726
+ }
11727
+
11728
+ function wfCloseInstallDialog() {
11729
+ const modal = document.getElementById('wfInstallModal');
11730
+ if (modal) modal.style.display = 'none';
11731
+ }
11732
+
11733
+ async function wfSearchNpm() {
11734
+ const query = document.getElementById('wfInstallSearch').value.trim();
11735
+ const results = document.getElementById('wfInstallResults');
11736
+ if (!query) return;
11737
+ results.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;text-align:center;">Searching...</div>';
11738
+ try {
11739
+ const res = await fetch('/api/workflows/community/search?q=' + encodeURIComponent(query) + '&limit=10');
11740
+ const data = await res.json();
11741
+ if (!data.results || data.results.length === 0) {
11742
+ results.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;text-align:center;">No packages found</div>';
11743
+ return;
11744
+ }
11745
+ results.innerHTML = data.results.map(r => {
11746
+ const isOfficial = r.name.startsWith('@vaicli/');
11747
+ const badge = isOfficial ? '<span style="background:var(--accent);color:#fff;font-size:9px;padding:1px 5px;border-radius:3px;margin-left:6px;">OFFICIAL</span>' : '';
11748
+ const installed = [...(wfState.official||[]), ...(wfState.community||[])].some(w => w.name === r.name);
11749
+ const btn = installed
11750
+ ? '<button disabled style="padding:4px 10px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text-muted);font-size:11px;cursor:default;">Installed</button>'
11751
+ : `<button onclick="wfInstallPkg('${r.name.replace(/'/g,"\\'")}')" style="padding:4px 10px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;">Install</button>`;
11752
+ return `<div style="padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;justify-content:space-between;">
11753
+ <div style="flex:1;min-width:0;">
11754
+ <div style="font-size:13px;font-weight:500;">${r.name}${badge}</div>
11755
+ <div style="font-size:11px;color:var(--text-muted);margin-top:2px;">${r.description || ''}</div>
11756
+ <div style="font-size:10px;color:var(--text-muted);margin-top:2px;">v${r.version || '?'}</div>
11757
+ </div>
11758
+ <div style="margin-left:12px;flex-shrink:0;">${btn}</div>
11759
+ </div>`;
11760
+ }).join('');
11761
+ } catch (err) {
11762
+ results.innerHTML = `<div style="padding:16px;color:#f44;font-size:12px;text-align:center;">Search failed: ${err.message}</div>`;
11763
+ }
11764
+ }
11765
+
11766
+ async function wfInstallPkg(name) {
11767
+ const results = document.getElementById('wfInstallResults');
11768
+ // Find the clicked button and show installing state
11769
+ const btns = results.querySelectorAll('button');
11770
+ let clickedBtn = null;
11771
+ btns.forEach(b => {
11772
+ if (b.textContent === 'Install' && !clickedBtn) {
11773
+ // Find the button in the same row as the package name
11774
+ const row = b.closest('div[style*="border-bottom"]');
11775
+ if (row && row.textContent.includes(name)) {
11776
+ clickedBtn = b;
11777
+ b.textContent = 'Installing...';
11778
+ b.disabled = true;
11779
+ b.style.opacity = '0.7';
11780
+ }
11781
+ }
11782
+ });
11783
+
11784
+ try {
11785
+ const res = await fetch('/api/workflows/community/install', {
11786
+ method: 'POST',
11787
+ headers: { 'Content-Type': 'application/json' },
11788
+ body: JSON.stringify({ name })
11789
+ });
11790
+ const text = await res.text();
11791
+ let data;
11792
+ try { data = JSON.parse(text); } catch { data = { error: text || 'Empty response' }; }
11793
+ if (data.success) {
11794
+ if (clickedBtn) { clickedBtn.textContent = '✓ Installed'; clickedBtn.style.opacity = '1'; }
11795
+ await wfLoadLibrary();
11796
+ await wfSearchNpm();
11797
+ } else {
11798
+ const errMsg = data.error || 'Unknown error';
11799
+ if (clickedBtn) { clickedBtn.textContent = 'Install'; clickedBtn.disabled = false; clickedBtn.style.opacity = '1'; }
11800
+ console.error('Install failed:', errMsg);
11801
+ alert('Install failed: ' + errMsg);
11802
+ }
11803
+ } catch (err) {
11804
+ if (clickedBtn) { clickedBtn.textContent = 'Install'; clickedBtn.disabled = false; clickedBtn.style.opacity = '1'; }
11805
+ console.error('Install error:', err);
11806
+ alert('Install failed: ' + err.message);
11807
+ }
11808
+ }
11809
+
11810
+ function wfToggleExamples(btn) {
11811
+ const content = btn.nextElementSibling;
11812
+ const isOpen = content.style.display !== 'none';
11813
+ content.style.display = isOpen ? 'none' : '';
11814
+ btn.classList.toggle('open', !isOpen);
9859
11815
  }
9860
11816
 
9861
11817
  async function wfSelectWorkflow(name) {
@@ -9868,12 +11824,14 @@ async function wfSelectWorkflow(name) {
9868
11824
  const res = await fetch('/api/workflows/' + encodeURIComponent(name));
9869
11825
  const data = await res.json();
9870
11826
  wfState.activeWorkflow = data.definition;
11827
+ wfState.builderMode = false;
9871
11828
  wfState.selectedNodeId = null;
9872
11829
  wfState.executionState = {};
9873
11830
  wfState.executionResults = {};
9874
11831
  wfSetToolbarEnabled(true);
9875
11832
  document.getElementById('wfCanvasEmpty').style.display = 'none';
9876
11833
  await wfRenderWorkflow(data.definition);
11834
+ wfSwitchLibTab('library');
9877
11835
  wfOpenInspector();
9878
11836
  wfUpdateInspector();
9879
11837
  } catch (err) {
@@ -9992,6 +11950,19 @@ async function wfRenderWorkflow(definition) {
9992
11950
  });
9993
11951
  }
9994
11952
 
11953
+ // Build conditional branch maps for edge styling
11954
+ const elseBranchEdges = new Set(); // "fromId->toId" for else branches
11955
+ if (definition.steps) {
11956
+ for (const step of definition.steps) {
11957
+ if (step.tool === 'conditional' && step.inputs) {
11958
+ const elseSteps = step.inputs.else || [];
11959
+ for (const eId of elseSteps) {
11960
+ elseBranchEdges.add(`${step.id}->${eId}`);
11961
+ }
11962
+ }
11963
+ }
11964
+ }
11965
+
9995
11966
  // Draw edges first (behind nodes)
9996
11967
  const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
9997
11968
  edgeGroup.classList.add('wf-edge-group');
@@ -10002,6 +11973,15 @@ async function wfRenderWorkflow(definition) {
10002
11973
  const depId = rawDepId.replace(/^!/, '');
10003
11974
  if (positions[depId] && positions[stepId]) {
10004
11975
  const edge = wfDrawEdge(depId, stepId, positions);
11976
+ // Mark else-branch edges with dashed style
11977
+ const edgeKey = `${depId}->${stepId}`;
11978
+ if (elseBranchEdges.has(edgeKey)) {
11979
+ edge.querySelector('.wf-edge')?.classList.add('wf-edge--else');
11980
+ }
11981
+ // Mark edges to skipped nodes
11982
+ if (wfState.executionState[stepId] === 'skipped') {
11983
+ edge.querySelector('.wf-edge')?.classList.add('wf-edge--skipped');
11984
+ }
10005
11985
  edgeGroup.appendChild(edge);
10006
11986
  }
10007
11987
  });
@@ -10024,7 +12004,7 @@ async function wfRenderWorkflow(definition) {
10024
12004
  }
10025
12005
 
10026
12006
  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' };
12007
+ const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666', category: 'unknown' };
10028
12008
  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
10029
12009
  g.classList.add('wf-node');
10030
12010
  if (state !== 'idle') g.classList.add('wf-node--' + state);
@@ -10036,46 +12016,121 @@ function wfDrawNode(step, x, y, state, hasDeps, hasDependents) {
10036
12016
  wfSelectNode(step.id);
10037
12017
  });
10038
12018
 
10039
- // Background rect
10040
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
10041
- rect.setAttribute('width', WF_NODE_W);
10042
- rect.setAttribute('height', WF_NODE_H);
10043
- rect.setAttribute('fill', meta.color);
10044
- rect.setAttribute('stroke', meta.color);
10045
- rect.setAttribute('opacity', '0.85');
10046
- g.appendChild(rect);
10047
-
10048
- // Input port (left side): only if this node has dependencies
10049
- if (hasDeps) {
12019
+ // Builder: node drag
12020
+ if (wfState.builderMode) {
12021
+ g.style.cursor = 'move';
12022
+ g.addEventListener('mousedown', (e) => {
12023
+ // Don't drag if clicking a port
12024
+ if (e.target.classList.contains('wf-port')) return;
12025
+ e.stopPropagation();
12026
+ const svg = document.getElementById('wf-canvas');
12027
+ const rect = svg.getBoundingClientRect();
12028
+ const startSvgX = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
12029
+ const startSvgY = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
12030
+ const pos = wfState.nodePositions[step.id];
12031
+ if (!pos) return;
12032
+ const offX = startSvgX - pos.x;
12033
+ const offY = startSvgY - pos.y;
12034
+ wfState.dragNode = step.id;
12035
+
12036
+ function onMove(ev) {
12037
+ const mx = (ev.clientX - rect.left) / wfState.zoom + wfState.panX;
12038
+ const my = (ev.clientY - rect.top) / wfState.zoom + wfState.panY;
12039
+ wfState.nodePositions[step.id] = { x: mx - offX, y: my - offY };
12040
+ wfRefreshNodes();
12041
+ }
12042
+ function onUp() {
12043
+ wfState.dragNode = null;
12044
+ document.removeEventListener('mousemove', onMove);
12045
+ document.removeEventListener('mouseup', onUp);
12046
+ }
12047
+ document.addEventListener('mousemove', onMove);
12048
+ document.addEventListener('mouseup', onUp);
12049
+ });
12050
+ }
12051
+
12052
+ // Background shape: diamond for conditional, rounded rect for others
12053
+ if (meta.shape === 'diamond') {
12054
+ const diamond = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
12055
+ const cx = WF_NODE_W / 2, cy = WF_NODE_H / 2;
12056
+ const rx = WF_NODE_W / 2 + 8, ry = WF_NODE_H / 2 + 4;
12057
+ diamond.setAttribute('points', `${cx},${cy - ry} ${cx + rx},${cy} ${cx},${cy + ry} ${cx - rx},${cy}`);
12058
+ diamond.setAttribute('fill', meta.color);
12059
+ diamond.setAttribute('stroke', meta.color);
12060
+ diamond.setAttribute('opacity', '0.85');
12061
+ g.appendChild(diamond);
12062
+ } else {
12063
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
12064
+ rect.setAttribute('width', WF_NODE_W);
12065
+ rect.setAttribute('height', WF_NODE_H);
12066
+ rect.setAttribute('rx', '6');
12067
+ rect.setAttribute('ry', '6');
12068
+ rect.setAttribute('fill', meta.color);
12069
+ rect.setAttribute('stroke', meta.color);
12070
+ rect.setAttribute('opacity', '0.85');
12071
+ g.appendChild(rect);
12072
+ }
12073
+
12074
+ // Input port (left side): show in builder mode always, otherwise only if has deps
12075
+ const showInPort = wfState.builderMode || hasDeps;
12076
+ if (showInPort) {
10050
12077
  const inPort = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
10051
12078
  inPort.classList.add('wf-port', 'wf-port-in');
12079
+ if (wfState.builderMode) inPort.classList.add('wf-port-builder');
10052
12080
  inPort.setAttribute('cx', 0);
10053
12081
  inPort.setAttribute('cy', WF_NODE_H / 2);
10054
- inPort.setAttribute('r', WF_PORT_R);
12082
+ inPort.setAttribute('r', wfState.builderMode ? 7 : WF_PORT_R);
10055
12083
  inPort.setAttribute('stroke', meta.color);
10056
12084
  inPort.setAttribute('stroke-width', '2');
12085
+ inPort.setAttribute('pointer-events', 'all');
12086
+ if (wfState.builderMode) {
12087
+ inPort.addEventListener('mouseup', () => {
12088
+ if (wfState.draggingEdge) wfEdgeDropOnInput(step.id);
12089
+ });
12090
+ }
10057
12091
  g.appendChild(inPort);
10058
12092
  }
10059
12093
 
10060
- // Output port (right side): only if other nodes depend on this one
10061
- if (hasDependents) {
12094
+ // Output port (right side): show in builder mode always, otherwise only if has dependents
12095
+ const showOutPort = wfState.builderMode || hasDependents;
12096
+ if (showOutPort) {
10062
12097
  const outPort = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
10063
12098
  outPort.classList.add('wf-port', 'wf-port-out');
12099
+ if (wfState.builderMode) outPort.classList.add('wf-port-builder');
10064
12100
  outPort.setAttribute('cx', WF_NODE_W);
10065
12101
  outPort.setAttribute('cy', WF_NODE_H / 2);
10066
- outPort.setAttribute('r', WF_PORT_R);
12102
+ outPort.setAttribute('r', wfState.builderMode ? 7 : WF_PORT_R);
10067
12103
  outPort.setAttribute('fill', meta.color);
10068
12104
  outPort.setAttribute('stroke-width', '2');
12105
+ outPort.setAttribute('pointer-events', 'all');
12106
+ if (wfState.builderMode) {
12107
+ outPort.addEventListener('mousedown', (e) => {
12108
+ e.stopPropagation();
12109
+ const pos = wfState.nodePositions[step.id];
12110
+ if (!pos) return;
12111
+ wfEdgeDragStart(step.id, pos.x + WF_NODE_W, pos.y + WF_NODE_H / 2);
12112
+ });
12113
+ }
10069
12114
  g.appendChild(outPort);
10070
12115
  }
10071
12116
 
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);
12117
+ // Icon (Lucide SVG)
12118
+ const iconG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
12119
+ iconG.classList.add('wf-node-icon');
12120
+ const iconSize = 18;
12121
+ const ix = 22 - iconSize / 2;
12122
+ const iy = WF_NODE_H / 2 - iconSize / 2;
12123
+ iconG.setAttribute('transform', `translate(${ix},${iy}) scale(${iconSize / 24})`);
12124
+ const fullD = meta.icon || WF_FALLBACK_ICON;
12125
+ const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
12126
+ iconPath.setAttribute('d', fullD);
12127
+ iconPath.setAttribute('fill', 'none');
12128
+ iconPath.setAttribute('stroke', 'currentColor');
12129
+ iconPath.setAttribute('stroke-width', '1.75');
12130
+ iconPath.setAttribute('stroke-linecap', 'round');
12131
+ iconPath.setAttribute('stroke-linejoin', 'round');
12132
+ iconG.appendChild(iconPath);
12133
+ g.appendChild(iconG);
10079
12134
 
10080
12135
  // Label (step name, truncated)
10081
12136
  const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
@@ -10242,44 +12297,69 @@ function wfUpdateInspector() {
10242
12297
  header.textContent = def.name || 'Workflow';
10243
12298
  let html = '';
10244
12299
 
10245
- // Description
10246
- if (def.description) {
12300
+ if (wfState.builderMode) {
12301
+ // Builder: editable workflow fields
12302
+ html += `<div class="wf-inspector-section">
12303
+ <div class="wf-inspector-section-title">Name</div>
12304
+ <input class="wf-inspector-input" value="${escapeHtml(def.name || '')}" onchange="wfEditWorkflowField('name', this.value); document.getElementById('wfInspectorHeader').textContent = this.value || 'Workflow';">
12305
+ </div>`;
10247
12306
  html += `<div class="wf-inspector-section">
10248
12307
  <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>
12308
+ <textarea class="wf-inspector-input" rows="2" style="resize:vertical;" onchange="wfEditWorkflowField('description', this.value)">${escapeHtml(def.description || '')}</textarea>
10250
12309
  </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 : ''}">
12310
+ html += `<div class="wf-inspector-section">
12311
+ <div class="wf-inspector-section-title">Steps</div>
12312
+ <div style="font-size:12px;color:var(--text);">${def.steps.length} step${def.steps.length !== 1 ? 's' : ''}</div>
12313
+ </div>`;
12314
+ if (def.steps.length > 0) {
12315
+ html += `<div class="wf-inspector-section">
12316
+ <div class="wf-inspector-section-title">Output Mapping</div>
12317
+ <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>
12318
+ </div>`;
12319
+ }
12320
+ html += `<div class="wf-inspector-section" style="margin-top:8px;">
12321
+ <div style="font-size:10px;color:var(--text-muted);">Add steps from the Palette tab, then drag between ports to connect them.</div>
12322
+ </div>`;
12323
+ } else {
12324
+ // Read-only: Description
12325
+ if (def.description) {
12326
+ html += `<div class="wf-inspector-section">
12327
+ <div class="wf-inspector-section-title">Description</div>
12328
+ <div style="font-size:12px;color:var(--text);line-height:1.4;">${escapeHtml(def.description)}</div>
10263
12329
  </div>`;
10264
12330
  }
10265
- html += '</div>';
10266
- }
10267
12331
 
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>`;
12332
+ // Read-only: Inputs
12333
+ if (def.inputs && Object.keys(def.inputs).length > 0) {
12334
+ html += '<div class="wf-inspector-section"><div class="wf-inspector-section-title">Inputs</div>';
12335
+ for (const [key, spec] of Object.entries(def.inputs)) {
12336
+ const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
12337
+ const defVal = spec.default !== undefined ? ` (default: ${spec.default})` : '';
12338
+ html += `<div style="margin-bottom:8px;">
12339
+ <div style="font-size:12px;font-weight:600;color:var(--text);">${escapeHtml(key)}${req}</div>
12340
+ <div style="font-size:11px;color:var(--text-muted);">${escapeHtml(spec.description || spec.type || '')}${defVal}</div>
12341
+ <input class="wf-inspector-input" id="wf-input-${key}" placeholder="${escapeHtml(key)}" value="${spec.default !== undefined ? spec.default : ''}">
12342
+ </div>`;
12343
+ }
12344
+ html += '</div>';
12345
+ }
10273
12346
 
10274
- // Output mapping
10275
- if (def.output) {
12347
+ // Steps summary
10276
12348
  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>
12349
+ <div class="wf-inspector-section-title">Steps</div>
12350
+ <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
12351
  </div>`;
12352
+
12353
+ // Output mapping
12354
+ if (def.output) {
12355
+ html += `<div class="wf-inspector-section">
12356
+ <div class="wf-inspector-section-title">Output</div>
12357
+ <div class="wf-inspector-code">${escapeHtml(JSON.stringify(def.output, null, 2))}</div>
12358
+ </div>`;
12359
+ }
10280
12360
  }
10281
12361
 
10282
- // Execution result
12362
+ // Execution result (shown in both modes)
10283
12363
  if (wfState.executionResults._done) {
10284
12364
  const r = wfState.executionResults._done;
10285
12365
  const doneJson = JSON.stringify(r.output, null, 2);
@@ -10305,54 +12385,109 @@ function wfUpdateInspector() {
10305
12385
  return;
10306
12386
  }
10307
12387
 
10308
- const meta = WF_NODE_META[step.tool] || { icon: '\u2699', label: step.tool, color: '#666' };
12388
+ const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666' };
10309
12389
  header.textContent = step.name || step.id;
10310
12390
 
10311
12391
  let html = '';
10312
12392
 
10313
- // Tool badge
12393
+ // Tool badge (always shown)
10314
12394
  html += `<div class="wf-inspector-section">
10315
12395
  <div class="wf-inspector-section-title">Tool</div>
10316
- <span class="wf-tool-badge" style="background:${meta.color}">${meta.icon} ${meta.label}</span>
10317
- </div>`;
10318
-
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>
12396
+ <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>
10323
12397
  </div>`;
10324
12398
 
10325
- // Inputs
10326
- if (step.inputs) {
12399
+ if (wfState.builderMode) {
12400
+ // Builder: editable step fields
12401
+ const sid = escapeHtml(step.id);
10327
12402
  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>`;
12403
+ <div class="wf-inspector-section-title">Step ID</div>
12404
+ <input class="wf-inspector-input" value="${sid}" style="font-family:monospace;" onchange="wfEditStepId('${sid}', this.value)">
12405
+ </div>`;
12406
+ html += `<div class="wf-inspector-section">
12407
+ <div class="wf-inspector-section-title">Name</div>
12408
+ <input class="wf-inspector-input" value="${escapeHtml(step.name || '')}" onchange="wfEditStepField('${sid}', 'name', this.value)">
12409
+ </div>`;
12410
+
12411
+ // Inputs from WF_INPUT_DEFS
12412
+ const inputDefs = WF_INPUT_DEFS[step.tool] || [];
12413
+ if (inputDefs.length > 0) {
12414
+ html += `<div class="wf-inspector-section"><div class="wf-inspector-section-title">Inputs</div>`;
12415
+ for (const d of inputDefs) {
12416
+ const val = step.inputs?.[d.key] ?? '';
12417
+ const display = typeof val === 'string' ? val : JSON.stringify(val);
12418
+ const req = d.required ? ' <span style="color:#e74c3c;font-size:10px;">required</span>' : '';
12419
+ html += `<div style="margin-bottom:8px;">
12420
+ <div style="font-size:11px;font-weight:600;color:var(--text);">${escapeHtml(d.key)}${req}</div>`;
12421
+
12422
+ if (d.type === 'textarea' || d.type === 'json') {
12423
+ 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>`;
12424
+ } else if (d.type === 'select' && d.options) {
12425
+ html += `<select class="wf-inspector-input" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">
12426
+ <option value="">--</option>`;
12427
+ for (const opt of d.options) {
12428
+ html += `<option value="${escapeHtml(opt)}" ${val === opt ? 'selected' : ''}>${escapeHtml(opt)}</option>`;
12429
+ }
12430
+ html += `</select>`;
12431
+ } else if (d.type === 'number') {
12432
+ html += `<input class="wf-inspector-input" type="number" value="${escapeHtml(String(val))}" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">`;
12433
+ } else {
12434
+ html += `<input class="wf-inspector-input" value="${escapeHtml(display)}" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">`;
12435
+ }
12436
+ html += `</div>`;
12437
+ }
12438
+ html += '</div>';
10335
12439
  }
10336
- html += '</div>';
10337
- }
10338
12440
 
10339
- // Condition
10340
- if (step.condition) {
12441
+ // Condition
10341
12442
  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>
12443
+ <div class="wf-inspector-section-title">Condition <span style="font-size:10px;color:var(--text-muted)">(optional)</span></div>
12444
+ <input class="wf-inspector-input" value="${escapeHtml(step.condition || '')}" placeholder="e.g. results.length > 0" onchange="wfEditStepField('${sid}', 'condition', this.value || undefined)">
12445
+ </div>`;
12446
+
12447
+ // continueOnError toggle
12448
+ html += `<div class="wf-inspector-section" style="display:flex;align-items:center;gap:8px;">
12449
+ <input type="checkbox" id="wf-coe-${sid}" ${step.continueOnError ? 'checked' : ''} onchange="wfEditStepField('${sid}', 'continueOnError', this.checked)">
12450
+ <label for="wf-coe-${sid}" style="font-size:11px;color:var(--text);cursor:pointer;">Continue on error</label>
10344
12451
  </div>`;
10345
- }
10346
12452
 
10347
- // ForEach
10348
- if (step.forEach) {
12453
+ // Delete button
12454
+ html += `<button class="wf-inspector-delete-btn" onclick="if(confirm('Delete step ${sid}?')) wfDeleteStep('${sid}')">Delete Step</button>`;
12455
+ } else {
12456
+ // Read-only step view
10349
12457
  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>
12458
+ <div class="wf-inspector-section-title">ID</div>
12459
+ <div style="font-size:12px;color:var(--text);font-family:monospace;">${escapeHtml(step.id)}</div>
10352
12460
  </div>`;
12461
+
12462
+ if (step.inputs) {
12463
+ html += `<div class="wf-inspector-section">
12464
+ <div class="wf-inspector-section-title">Inputs</div>`;
12465
+ for (const [key, val] of Object.entries(step.inputs)) {
12466
+ const display = typeof val === 'string' ? val : JSON.stringify(val);
12467
+ html += `<div class="wf-inspector-field">
12468
+ <span class="wf-inspector-field-label">${escapeHtml(key)}</span>
12469
+ <span class="wf-inspector-field-value" style="font-family:monospace;font-size:11px;">${escapeHtml(display)}</span>
12470
+ </div>`;
12471
+ }
12472
+ html += '</div>';
12473
+ }
12474
+
12475
+ if (step.condition) {
12476
+ html += `<div class="wf-inspector-section">
12477
+ <div class="wf-inspector-section-title">Condition \u26A1</div>
12478
+ <div class="wf-inspector-code">${escapeHtml(step.condition)}</div>
12479
+ </div>`;
12480
+ }
12481
+
12482
+ if (step.forEach) {
12483
+ html += `<div class="wf-inspector-section">
12484
+ <div class="wf-inspector-section-title">ForEach</div>
12485
+ <div class="wf-inspector-code">${escapeHtml(JSON.stringify(step.forEach, null, 2))}</div>
12486
+ </div>`;
12487
+ }
10353
12488
  }
10354
12489
 
10355
- // Execution result for this step
12490
+ // Execution result for this step (shown in both modes)
10356
12491
  const result = wfState.executionResults[step.id];
10357
12492
  const state = wfState.executionState[step.id];
10358
12493
  if (state === 'completed' && result) {
@@ -10416,6 +12551,7 @@ function wfSetToolbarEnabled(enabled) {
10416
12551
  document.getElementById('wfRunBtn').disabled = !enabled;
10417
12552
  document.getElementById('wfDryRunBtn').disabled = !enabled;
10418
12553
  document.getElementById('wfExportBtn').disabled = !enabled;
12554
+ document.getElementById('wfEditBtn').disabled = !enabled;
10419
12555
  }
10420
12556
 
10421
12557
  // ── Export workflow JSON ──
@@ -10461,9 +12597,9 @@ function wfDryRun() {
10461
12597
  const step = stepMap[stepId];
10462
12598
  if (!step) return;
10463
12599
  totalSteps++;
10464
- const meta = WF_NODE_META[step.tool] || { icon: '\u2699', label: step.tool, color: '#666' };
12600
+ const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666' };
10465
12601
  layersHtml += '<div class="wf-dryrun-step">';
10466
- layersHtml += '<span class="wf-dryrun-step-icon">' + meta.icon + '</span>';
12602
+ 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
12603
  layersHtml += '<div class="wf-dryrun-step-info">';
10468
12604
  layersHtml += '<div class="wf-dryrun-step-name">' + escapeHtml(step.name || step.id) + '</div>';
10469
12605
  layersHtml += '<div class="wf-dryrun-step-tool">' + escapeHtml(meta.label) + ' (' + escapeHtml(step.id) + ')</div>';
@@ -10601,27 +12737,128 @@ function wfStopExecution(reason) {
10601
12737
  wfUpdateInspector();
10602
12738
  }
10603
12739
 
10604
- async function wfExecute() {
12740
+ // ── Input Modal (pre-execution) ──
12741
+
12742
+ function wfShowInputModal() {
12743
+ const def = wfState.activeWorkflow;
12744
+ if (!def || !def.inputs) return;
12745
+ const entries = Object.entries(def.inputs);
12746
+ if (entries.length === 0) { wfExecuteWithInputs({}); return; }
12747
+
12748
+ document.getElementById('wfInputModalTitle').textContent = (def.name || 'Workflow') + ' Inputs';
12749
+ let html = '';
12750
+ for (const [key, spec] of entries) {
12751
+ const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
12752
+ const desc = spec.description ? `<div class="wf-input-modal-desc">${escapeHtml(spec.description)}</div>` : '';
12753
+ // Pre-fill from inspector fields if available, then from defaults
12754
+ const inspectorEl = document.getElementById('wf-input-' + key);
12755
+ let prefill = inspectorEl ? inspectorEl.value : '';
12756
+ if (!prefill && spec.default !== undefined) prefill = String(spec.default);
12757
+ const placeholder = spec.type === 'number' ? 'number' : (spec.type || 'string');
12758
+ html += `<div class="wf-input-modal-field">
12759
+ <div class="wf-input-modal-label">${escapeHtml(key)}${req}</div>
12760
+ ${desc}
12761
+ <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}">
12762
+ <div class="wf-input-modal-error" id="wf-modal-err-${key}">This field is required</div>
12763
+ </div>`;
12764
+ }
12765
+ document.getElementById('wfInputModalBody').innerHTML = html;
12766
+ document.getElementById('wfInputModalBackdrop').style.display = '';
12767
+
12768
+ // Focus first empty required field, or first field
12769
+ const firstEmpty = entries.find(([k, s]) => {
12770
+ const el = document.getElementById('wf-modal-input-' + k);
12771
+ return s.required && el && !el.value;
12772
+ });
12773
+ const focusKey = firstEmpty ? firstEmpty[0] : entries[0][0];
12774
+ const focusEl = document.getElementById('wf-modal-input-' + focusKey);
12775
+ if (focusEl) setTimeout(() => focusEl.focus(), 50);
12776
+ }
12777
+
12778
+ function wfCloseInputModal() {
12779
+ document.getElementById('wfInputModalBackdrop').style.display = 'none';
12780
+ }
12781
+
12782
+ function wfInputModalSubmit() {
12783
+ const def = wfState.activeWorkflow;
12784
+ if (!def || !def.inputs) return;
12785
+
12786
+ const inputs = {};
12787
+ let hasError = false;
12788
+
12789
+ for (const [key, spec] of Object.entries(def.inputs)) {
12790
+ const el = document.getElementById('wf-modal-input-' + key);
12791
+ const errEl = document.getElementById('wf-modal-err-' + key);
12792
+ if (!el) continue;
12793
+
12794
+ let val = el.value.trim();
12795
+
12796
+ // Clear previous error
12797
+ el.classList.remove('error');
12798
+ if (errEl) errEl.style.display = 'none';
12799
+
12800
+ // Required check
12801
+ if (!val && spec.required && spec.default === undefined) {
12802
+ el.classList.add('error');
12803
+ if (errEl) { errEl.textContent = 'This field is required'; errEl.style.display = ''; }
12804
+ hasError = true;
12805
+ continue;
12806
+ }
12807
+
12808
+ // Use default if empty
12809
+ if (!val && spec.default !== undefined) val = String(spec.default);
12810
+
12811
+ // Type validation
12812
+ if (spec.type === 'number' && val && isNaN(Number(val))) {
12813
+ el.classList.add('error');
12814
+ if (errEl) { errEl.textContent = 'Must be a number'; errEl.style.display = ''; }
12815
+ hasError = true;
12816
+ continue;
12817
+ }
12818
+
12819
+ // Coerce
12820
+ if (spec.type === 'number' && val) val = Number(val);
12821
+ if (val !== undefined && val !== '') inputs[key] = val;
12822
+ }
12823
+
12824
+ if (hasError) return;
12825
+
12826
+ // Also update inspector fields to keep them in sync
12827
+ for (const [key, val] of Object.entries(inputs)) {
12828
+ const inspEl = document.getElementById('wf-input-' + key);
12829
+ if (inspEl) inspEl.value = typeof val === 'number' ? String(val) : val;
12830
+ }
12831
+
12832
+ wfCloseInputModal();
12833
+ wfExecuteWithInputs(inputs);
12834
+ }
12835
+
12836
+ // Keyboard handler for input modal
12837
+ document.addEventListener('keydown', (e) => {
12838
+ const backdrop = document.getElementById('wfInputModalBackdrop');
12839
+ if (!backdrop || backdrop.style.display === 'none') return;
12840
+ if (e.key === 'Escape') { wfCloseInputModal(); e.preventDefault(); }
12841
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { wfInputModalSubmit(); e.preventDefault(); }
12842
+ });
12843
+
12844
+ async function wfExecute() {
12845
+ const def = wfState.activeWorkflow;
12846
+ if (!def || wfState.executing) return;
12847
+
12848
+ // If workflow has inputs, show the input modal instead of executing directly
12849
+ if (def.inputs && Object.keys(def.inputs).length > 0) {
12850
+ wfShowInputModal();
12851
+ return;
12852
+ }
12853
+
12854
+ // No inputs needed, execute directly
12855
+ wfExecuteWithInputs({});
12856
+ }
12857
+
12858
+ async function wfExecuteWithInputs(inputs) {
10605
12859
  const def = wfState.activeWorkflow;
10606
12860
  if (!def || wfState.executing) return;
10607
12861
 
10608
- // Collect inputs
10609
- 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;
10622
- }
10623
- }
10624
-
10625
12862
  wfState.executing = true;
10626
12863
  wfState.executionResults = {};
10627
12864
  wfAbortController = new AbortController();
@@ -10907,7 +13144,7 @@ function wfInitPan() {
10907
13144
  // Scroll wheel zoom, centered on cursor position
10908
13145
  svg.addEventListener('wheel', (e) => {
10909
13146
  e.preventDefault();
10910
- const factor = e.deltaY < 0 ? 1.08 : 1 / 1.08;
13147
+ const factor = e.deltaY < 0 ? 1.03 : 1 / 1.03;
10911
13148
  const oldZoom = wfState.zoom;
10912
13149
  const newZoom = Math.max(0.2, Math.min(5, oldZoom * factor));
10913
13150
  // Zoom toward cursor: keep the SVG point under the mouse fixed
@@ -10928,6 +13165,452 @@ function wfInitPan() {
10928
13165
  });
10929
13166
  }
10930
13167
 
13168
+ // ── Builder: Input Definitions ──
13169
+ const WF_INPUT_DEFS = {
13170
+ 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: '{}' }],
13171
+ 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: '{}' }],
13172
+ 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' }],
13173
+ 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'] }],
13174
+ 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'] }],
13175
+ similarity: [{ key: 'text1', type: 'text', required: true }, { key: 'text2', type: 'text', required: true }, { key: 'model', type: 'text', required: false }],
13176
+ collections: [{ key: 'db', type: 'text', required: false }],
13177
+ models: [{ key: 'category', type: 'select', required: false, options: ['embedding','rerank','all'] }],
13178
+ 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' }],
13179
+ explain: [{ key: 'topic', type: 'text', required: true }],
13180
+ topics: [{ key: 'search', type: 'text', required: false }],
13181
+ merge: [{ key: 'sources', type: 'json', required: true, placeholder: '["step1.output","step2.output"]' }, { key: 'strategy', type: 'select', required: false, options: ['concat','interleave','unique'] }],
13182
+ filter: [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'condition', type: 'text', required: true, placeholder: 'item.score > 0.5' }],
13183
+ transform: [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'expression', type: 'text', required: true, placeholder: 'item.text' }],
13184
+ generate: [{ key: 'prompt', type: 'textarea', required: true, placeholder: 'Generate a summary of...' }, { key: 'context', type: 'text', required: false, placeholder: '{{ step.output }}' }],
13185
+ conditional: [{ key: 'condition', type: 'text', required: true, placeholder: '{{ step.output.results.length > 0 }}' }, { key: 'then', type: 'json', required: true, placeholder: '["step_a"]' }, { key: 'else', type: 'json', required: false, placeholder: '["step_b"]' }],
13186
+ loop: [{ key: 'items', type: 'text', required: true, placeholder: '{{ step.output.results }}' }, { key: 'as', type: 'text', required: true, placeholder: 'item' }, { key: 'step', type: 'json', required: true, placeholder: '{"tool":"template","inputs":{"text":"{{ item }}"}}' }, { key: 'maxIterations', type: 'number', required: false, placeholder: '100' }],
13187
+ template: [{ key: 'text', type: 'textarea', required: true, placeholder: 'Compose text with {{ step.output }} references' }],
13188
+ chunk: [{ key: 'text', type: 'textarea', required: true, placeholder: '{{ step.output.text }}' }, { key: 'strategy', type: 'select', required: false, options: ['fixed','sentence','paragraph','recursive','markdown'] }, { key: 'size', type: 'number', required: false, placeholder: '512' }, { key: 'overlap', type: 'number', required: false, placeholder: '50' }, { key: 'source', type: 'text', required: false, placeholder: 'document.md' }],
13189
+ aggregate: [{ key: 'pipeline', type: 'json', required: true, placeholder: '[{"$group":{"_id":"$field","count":{"$sum":1}}}]' }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }],
13190
+ http: [{ key: 'url', type: 'text', required: true, placeholder: 'https://api.example.com/data' }, { key: 'method', type: 'select', required: false, options: ['GET','POST','PUT','PATCH','DELETE'] }, { key: 'headers', type: 'json', required: false, placeholder: '{"Authorization":"Bearer ..."}' }, { key: 'body', type: 'json', required: false, placeholder: '{}' }, { key: 'timeout', type: 'number', required: false, placeholder: '30000' }],
13191
+ };
13192
+
13193
+ const WF_CATEGORY_ORDER = ['retrieval', 'embedding', 'processing', 'control', 'generation', 'integration', 'management', 'utility'];
13194
+ const WF_CATEGORY_LABELS = { retrieval: 'Retrieval', embedding: 'Embedding', management: 'Management', utility: 'Utility', control: 'Control Flow', generation: 'Generation', processing: 'Processing', integration: 'Integration' };
13195
+
13196
+ // ── Builder: Library/Palette tab toggle ──
13197
+ function wfSwitchLibTab(tab) {
13198
+ const libraryList = document.getElementById('wfLibraryList');
13199
+ const paletteList = document.getElementById('wfPaletteList');
13200
+ document.querySelectorAll('.wf-lib-tab').forEach(b => b.classList.toggle('active', b.dataset.libTab === tab));
13201
+ if (libraryList) libraryList.style.display = tab === 'library' ? '' : 'none';
13202
+ if (paletteList) paletteList.style.display = tab === 'palette' ? '' : 'none';
13203
+ if (tab === 'palette') wfRenderPalette();
13204
+ }
13205
+
13206
+ // ── Builder: Palette rendering ──
13207
+ function wfRenderPalette() {
13208
+ const container = document.getElementById('wfPaletteList');
13209
+ if (!container) return;
13210
+ const grouped = {};
13211
+ for (const [tool, meta] of Object.entries(WF_NODE_META)) {
13212
+ const cat = meta.category || 'unknown';
13213
+ if (!grouped[cat]) grouped[cat] = [];
13214
+ grouped[cat].push({ tool, ...meta });
13215
+ }
13216
+ let html = '';
13217
+ for (const cat of WF_CATEGORY_ORDER) {
13218
+ const items = grouped[cat];
13219
+ if (!items) continue;
13220
+ html += `<div class="wf-palette-category"><div class="wf-palette-category-title">${WF_CATEGORY_LABELS[cat] || cat}</div>`;
13221
+ for (const item of items) {
13222
+ html += `<div class="wf-palette-item" draggable="true" ondragstart="event.dataTransfer.setData('text/plain','${item.tool}')" onclick="wfAddNodeFromPalette('${item.tool}')">
13223
+ <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>
13224
+ <span class="wf-palette-label">${item.label}</span>
13225
+ </div>`;
13226
+ }
13227
+ html += '</div>';
13228
+ }
13229
+ container.innerHTML = html;
13230
+ }
13231
+
13232
+ // ── Builder: New Workflow ──
13233
+ function wfNewWorkflow() {
13234
+ wfState.activeWorkflow = {
13235
+ name: 'Untitled Workflow',
13236
+ description: '',
13237
+ steps: [],
13238
+ inputs: {},
13239
+ defaults: {},
13240
+ output: {}
13241
+ };
13242
+ wfState.builderMode = true;
13243
+ wfState.dirtyFlag = false;
13244
+ wfState.selectedNodeId = null;
13245
+ wfState.executionState = {};
13246
+ wfState.executionResults = {};
13247
+ wfState.nodePositions = {};
13248
+ wfState.layers = [];
13249
+ wfState.graph = {};
13250
+ wfState.zoom = 1;
13251
+ wfState.panX = 0;
13252
+ wfState.panY = 0;
13253
+ wfState.draggingEdge = null;
13254
+ wfState.dragNode = null;
13255
+
13256
+ // Clear canvas
13257
+ const svg = document.getElementById('wf-canvas');
13258
+ if (svg) svg.querySelectorAll('.wf-node, .wf-edge-group').forEach(el => el.remove());
13259
+ document.getElementById('wfCanvasEmpty').style.display = 'none';
13260
+ svg.style.display = '';
13261
+
13262
+ // Enable toolbar buttons
13263
+ document.getElementById('wfDryRunBtn').disabled = false;
13264
+ document.getElementById('wfRunBtn').disabled = false;
13265
+ document.getElementById('wfExportBtn').disabled = false;
13266
+
13267
+ // Switch to palette tab
13268
+ wfSwitchLibTab('palette');
13269
+
13270
+ // Open inspector for workflow-level editing
13271
+ wfOpenInspector();
13272
+ wfUpdateInspector();
13273
+
13274
+ // Set viewBox
13275
+ wfApplyViewBox();
13276
+ }
13277
+
13278
+ // ── Builder: Edit current workflow ──
13279
+ function wfEditWorkflow() {
13280
+ if (!wfState.activeWorkflow) return;
13281
+ wfState.builderMode = true;
13282
+ wfState.dirtyFlag = false;
13283
+ wfState.selectedNodeId = null;
13284
+ wfState.draggingEdge = null;
13285
+ wfState.dragNode = null;
13286
+
13287
+ // Re-render nodes with builder ports and drag handles
13288
+ wfRefreshNodes();
13289
+
13290
+ // Switch to palette tab and open inspector for workflow-level editing
13291
+ wfSwitchLibTab('palette');
13292
+ wfOpenInspector();
13293
+ wfUpdateInspector();
13294
+ }
13295
+
13296
+ // ── Builder: Add node from palette ──
13297
+ function wfAddNodeFromPalette(tool) {
13298
+ if (!wfState.activeWorkflow) wfNewWorkflow();
13299
+ wfState.builderMode = true;
13300
+ const def = wfState.activeWorkflow;
13301
+
13302
+ // Generate unique step ID
13303
+ let baseId = tool;
13304
+ let id = baseId;
13305
+ let counter = 2;
13306
+ const existingIds = new Set(def.steps.map(s => s.id));
13307
+ while (existingIds.has(id)) { id = baseId + '_' + counter; counter++; }
13308
+
13309
+ // Build default inputs
13310
+ const inputDefs = WF_INPUT_DEFS[tool] || [];
13311
+ const inputs = {};
13312
+ for (const d of inputDefs) {
13313
+ if (d.required) inputs[d.key] = '';
13314
+ }
13315
+
13316
+ const meta = WF_NODE_META[tool] || {};
13317
+ const step = {
13318
+ id,
13319
+ name: meta.label || tool,
13320
+ tool,
13321
+ inputs,
13322
+ };
13323
+ def.steps.push(step);
13324
+
13325
+ // Position: place to the right of all existing nodes
13326
+ let maxX = WF_PAD;
13327
+ for (const pos of Object.values(wfState.nodePositions)) {
13328
+ if (pos.x + WF_NODE_W + WF_LAYER_GAP > maxX) maxX = pos.x + WF_NODE_W + WF_LAYER_GAP;
13329
+ }
13330
+ let y = WF_PAD;
13331
+ // Stack vertically if there are nodes in the same column
13332
+ const nodesAtX = Object.values(wfState.nodePositions).filter(p => Math.abs(p.x - maxX) < 20);
13333
+ if (nodesAtX.length > 0) {
13334
+ y = Math.max(...nodesAtX.map(p => p.y)) + WF_NODE_H + WF_NODE_GAP;
13335
+ }
13336
+ wfState.nodePositions[id] = { x: maxX, y };
13337
+
13338
+ // Rebuild graph
13339
+ wfBuildGraph();
13340
+ wfRefreshNodes();
13341
+ wfSelectNode(id);
13342
+ wfState.dirtyFlag = true;
13343
+ }
13344
+
13345
+ // ── Builder: Build graph from step inputs (template references) ──
13346
+ function wfBuildGraph() {
13347
+ const def = wfState.activeWorkflow;
13348
+ if (!def) return;
13349
+ const graph = {};
13350
+ const stepIds = new Set(def.steps.map(s => s.id));
13351
+ for (const step of def.steps) {
13352
+ const deps = new Set();
13353
+ // Scan all input values for {{ stepId.xxx }} references
13354
+ for (const val of Object.values(step.inputs || {})) {
13355
+ const str = typeof val === 'string' ? val : JSON.stringify(val);
13356
+ const matches = str.matchAll(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\./g);
13357
+ for (const m of matches) {
13358
+ if (stepIds.has(m[1]) && m[1] !== step.id) deps.add(m[1]);
13359
+ }
13360
+ }
13361
+ graph[step.id] = Array.from(deps);
13362
+ }
13363
+ wfState.graph = graph;
13364
+ }
13365
+
13366
+ // ── Builder: Canvas drop (from palette drag) ──
13367
+ function wfCanvasDrop(e) {
13368
+ e.preventDefault();
13369
+ const tool = e.dataTransfer.getData('text/plain');
13370
+ if (!tool || !WF_NODE_META[tool]) return;
13371
+ if (!wfState.activeWorkflow) wfNewWorkflow();
13372
+ wfState.builderMode = true;
13373
+ const def = wfState.activeWorkflow;
13374
+
13375
+ // Generate unique ID
13376
+ let id = tool;
13377
+ let counter = 2;
13378
+ const existingIds = new Set(def.steps.map(s => s.id));
13379
+ while (existingIds.has(id)) { id = tool + '_' + counter; counter++; }
13380
+
13381
+ // Inputs
13382
+ const inputDefs = WF_INPUT_DEFS[tool] || [];
13383
+ const inputs = {};
13384
+ for (const d of inputDefs) { if (d.required) inputs[d.key] = ''; }
13385
+
13386
+ const meta = WF_NODE_META[tool] || {};
13387
+ const step = { id, name: meta.label || tool, tool, inputs };
13388
+ def.steps.push(step);
13389
+
13390
+ // Convert drop coordinates to SVG space
13391
+ const svg = document.getElementById('wf-canvas');
13392
+ const rect = svg.getBoundingClientRect();
13393
+ const svgX = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
13394
+ const svgY = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
13395
+ wfState.nodePositions[id] = { x: svgX, y: svgY };
13396
+
13397
+ wfBuildGraph();
13398
+ wfRefreshNodes();
13399
+ wfSelectNode(id);
13400
+ wfState.dirtyFlag = true;
13401
+ }
13402
+
13403
+ // ── Builder: Mutation helpers ──
13404
+ function wfEditStepField(stepId, field, value) {
13405
+ const step = wfState.activeWorkflow?.steps.find(s => s.id === stepId);
13406
+ if (!step) return;
13407
+ step[field] = value;
13408
+ wfState.dirtyFlag = true;
13409
+ if (field === 'name') wfRefreshNodes();
13410
+ }
13411
+
13412
+ function wfEditStepInput(stepId, key, value) {
13413
+ const step = wfState.activeWorkflow?.steps.find(s => s.id === stepId);
13414
+ if (!step) return;
13415
+ if (!step.inputs) step.inputs = {};
13416
+ step.inputs[key] = value;
13417
+ wfState.dirtyFlag = true;
13418
+ // Rebuild graph in case template refs changed
13419
+ wfBuildGraph();
13420
+ wfRefreshNodes();
13421
+ }
13422
+
13423
+ function wfEditStepId(oldId, newId) {
13424
+ const def = wfState.activeWorkflow;
13425
+ if (!def) return;
13426
+ newId = newId.trim().replace(/[^a-zA-Z0-9_]/g, '_');
13427
+ if (!newId || newId === oldId) return;
13428
+ if (def.steps.some(s => s.id === newId)) return; // duplicate
13429
+
13430
+ const step = def.steps.find(s => s.id === oldId);
13431
+ if (!step) return;
13432
+ step.id = newId;
13433
+
13434
+ // Update position map
13435
+ if (wfState.nodePositions[oldId]) {
13436
+ wfState.nodePositions[newId] = wfState.nodePositions[oldId];
13437
+ delete wfState.nodePositions[oldId];
13438
+ }
13439
+ // Update template references in other steps
13440
+ for (const s of def.steps) {
13441
+ for (const [k, v] of Object.entries(s.inputs || {})) {
13442
+ if (typeof v === 'string' && v.includes('{{ ' + oldId + '.')) {
13443
+ s.inputs[k] = v.replaceAll('{{ ' + oldId + '.', '{{ ' + newId + '.');
13444
+ }
13445
+ }
13446
+ }
13447
+ if (wfState.selectedNodeId === oldId) wfState.selectedNodeId = newId;
13448
+ wfState.dirtyFlag = true;
13449
+ wfBuildGraph();
13450
+ wfRefreshNodes();
13451
+ wfUpdateInspector();
13452
+ }
13453
+
13454
+ function wfDeleteStep(stepId) {
13455
+ const def = wfState.activeWorkflow;
13456
+ if (!def) return;
13457
+ def.steps = def.steps.filter(s => s.id !== stepId);
13458
+ delete wfState.nodePositions[stepId];
13459
+ if (wfState.selectedNodeId === stepId) wfState.selectedNodeId = null;
13460
+ wfState.dirtyFlag = true;
13461
+ wfBuildGraph();
13462
+ wfRefreshNodes();
13463
+ wfUpdateInspector();
13464
+ }
13465
+
13466
+ function wfEditWorkflowField(field, value) {
13467
+ if (!wfState.activeWorkflow) return;
13468
+ wfState.activeWorkflow[field] = value;
13469
+ wfState.dirtyFlag = true;
13470
+ }
13471
+
13472
+ // ── Builder: Validate ──
13473
+ async function wfValidateBuilder() {
13474
+ const def = wfState.activeWorkflow;
13475
+ if (!def) return null;
13476
+ try {
13477
+ const res = await fetch('/api/workflows/validate', {
13478
+ method: 'POST',
13479
+ headers: { 'Content-Type': 'application/json' },
13480
+ body: JSON.stringify(def),
13481
+ });
13482
+ return await res.json();
13483
+ } catch (err) {
13484
+ return { valid: false, errors: [err.message] };
13485
+ }
13486
+ }
13487
+
13488
+ // ── Builder: Edge drag ──
13489
+ function wfEdgeDragStart(fromId, fromX, fromY) {
13490
+ const svg = document.getElementById('wf-canvas');
13491
+ if (!svg) return;
13492
+ wfState.draggingEdge = { fromId, fromX, fromY };
13493
+
13494
+ // Create temp edge (dashed bezier), pointer-events: none so it doesn't block port hit-testing
13495
+ const tempPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
13496
+ tempPath.setAttribute('id', 'wf-temp-edge');
13497
+ tempPath.setAttribute('fill', 'none');
13498
+ tempPath.setAttribute('stroke', 'var(--accent)');
13499
+ tempPath.setAttribute('stroke-width', '2');
13500
+ tempPath.setAttribute('stroke-dasharray', '6 4');
13501
+ tempPath.setAttribute('pointer-events', 'none');
13502
+ tempPath.setAttribute('d', `M${fromX},${fromY} L${fromX},${fromY}`);
13503
+ svg.appendChild(tempPath);
13504
+
13505
+ function onMove(e) {
13506
+ const rect = svg.getBoundingClientRect();
13507
+ const mx = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
13508
+ const my = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
13509
+ const dx = Math.abs(mx - fromX) * 0.5;
13510
+ tempPath.setAttribute('d', `M${fromX},${fromY} C${fromX + dx},${fromY} ${mx - dx},${my} ${mx},${my}`);
13511
+ }
13512
+
13513
+ function onUp() {
13514
+ document.removeEventListener('mousemove', onMove);
13515
+ document.removeEventListener('mouseup', onUp);
13516
+ const el = document.getElementById('wf-temp-edge');
13517
+ if (el) el.remove();
13518
+ wfState.draggingEdge = null;
13519
+ }
13520
+
13521
+ document.addEventListener('mousemove', onMove);
13522
+ document.addEventListener('mouseup', onUp);
13523
+ }
13524
+
13525
+ function wfEdgeDropOnInput(toId) {
13526
+ if (!wfState.draggingEdge) return;
13527
+ const fromId = wfState.draggingEdge.fromId;
13528
+ if (fromId === toId) return; // no self-connections
13529
+
13530
+ // Add template reference to the target step's first empty required input
13531
+ const def = wfState.activeWorkflow;
13532
+ if (!def) return;
13533
+ const targetStep = def.steps.find(s => s.id === toId);
13534
+ if (!targetStep) return;
13535
+
13536
+ const inputDefs = WF_INPUT_DEFS[targetStep.tool] || [];
13537
+ let connected = false;
13538
+
13539
+ // Try to fill the first empty required input with a template reference
13540
+ for (const d of inputDefs) {
13541
+ if (!targetStep.inputs) targetStep.inputs = {};
13542
+ const current = targetStep.inputs[d.key];
13543
+ if (!current || current === '') {
13544
+ targetStep.inputs[d.key] = `{{ ${fromId}.output }}`;
13545
+ connected = true;
13546
+ break;
13547
+ }
13548
+ }
13549
+
13550
+ // If no empty required input, try first empty optional input
13551
+ if (!connected) {
13552
+ for (const d of inputDefs) {
13553
+ const current = targetStep.inputs?.[d.key];
13554
+ if (!current || current === '') {
13555
+ if (!targetStep.inputs) targetStep.inputs = {};
13556
+ targetStep.inputs[d.key] = `{{ ${fromId}.output }}`;
13557
+ connected = true;
13558
+ break;
13559
+ }
13560
+ }
13561
+ }
13562
+
13563
+ if (connected) {
13564
+ wfState.dirtyFlag = true;
13565
+ wfBuildGraph();
13566
+ wfRelayout();
13567
+ wfRefreshNodes();
13568
+ if (wfState.selectedNodeId === toId) wfUpdateInspector();
13569
+ }
13570
+
13571
+ // Clean up drag state
13572
+ wfState.draggingEdge = null;
13573
+ const el = document.getElementById('wf-temp-edge');
13574
+ if (el) el.remove();
13575
+ }
13576
+
13577
+ // ── Builder: Relayout via topological sort ──
13578
+ async function wfRelayout() {
13579
+ const def = wfState.activeWorkflow;
13580
+ if (!def || def.steps.length === 0) return;
13581
+
13582
+ try {
13583
+ const res = await fetch('/api/workflows/plan', {
13584
+ method: 'POST',
13585
+ headers: { 'Content-Type': 'application/json' },
13586
+ body: JSON.stringify(def),
13587
+ });
13588
+ const data = await res.json();
13589
+ if (data.layers && data.layers.length > 0) {
13590
+ wfState.layers = data.layers;
13591
+ // Reposition nodes based on layers
13592
+ const positions = {};
13593
+ data.layers.forEach((layer, li) => {
13594
+ layer.forEach((stepId, ni) => {
13595
+ positions[stepId] = {
13596
+ x: WF_PAD + li * WF_LAYER_GAP,
13597
+ y: WF_PAD + ni * (WF_NODE_H + WF_NODE_GAP),
13598
+ };
13599
+ });
13600
+ });
13601
+ // Keep orphan nodes (not in any layer) at their current position
13602
+ for (const step of def.steps) {
13603
+ if (!positions[step.id] && wfState.nodePositions[step.id]) {
13604
+ positions[step.id] = wfState.nodePositions[step.id];
13605
+ }
13606
+ }
13607
+ wfState.nodePositions = positions;
13608
+ }
13609
+ } catch (err) {
13610
+ console.warn('Relayout failed:', err.message);
13611
+ }
13612
+ }
13613
+
10931
13614
  // ── Docs shortcut (F1) ──
10932
13615
  const DOCS_URLS = {
10933
13616
  embed: 'https://docs.vaicli.com/docs/commands/embeddings/embed',
@@ -10956,6 +13639,22 @@ document.addEventListener('keydown', (e) => {
10956
13639
  document.addEventListener('keydown', (e) => {
10957
13640
  const activeTab = document.querySelector('.tab-btn.active');
10958
13641
  if (!activeTab || activeTab.dataset.tab !== 'workflows') return;
13642
+
13643
+ // Ctrl/Cmd+S to save/export in builder mode (works even in inputs)
13644
+ if ((e.ctrlKey || e.metaKey) && e.key === 's' && wfState.builderMode) {
13645
+ e.preventDefault();
13646
+ wfExportJson();
13647
+ return;
13648
+ }
13649
+
13650
+ // Delete/Backspace to remove selected node in builder mode
13651
+ if ((e.key === 'Delete' || e.key === 'Backspace') && wfState.builderMode && wfState.selectedNodeId) {
13652
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
13653
+ e.preventDefault();
13654
+ wfDeleteStep(wfState.selectedNodeId);
13655
+ return;
13656
+ }
13657
+
10959
13658
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
10960
13659
 
10961
13660
  const PAN_STEP = 40;
@@ -10970,6 +13669,382 @@ document.addEventListener('keydown', (e) => {
10970
13669
  });
10971
13670
 
10972
13671
  // ── Init ──
13672
+ // ── Workflow Store ──
13673
+ const WF_STORE_TOOL_COLORS = {
13674
+ query:'#00D4AA',search:'#40E0FF',rerank:'#0EA5E9',embed:'#8B5CF6',similarity:'#A78BFA',
13675
+ ingest:'#10B981',collections:'#F59E0B',models:'#EF4444',explain:'#EC4899',estimate:'#F97316',
13676
+ merge:'#6366F1',filter:'#14B8A6',transform:'#D946EF',generate:'#F472B6'
13677
+ };
13678
+ const WF_STORE_CATEGORIES = [
13679
+ { id:'all', label:'All', icon:'◈' }, { id:'retrieval', label:'Retrieval', icon:'⊕' },
13680
+ { id:'analysis', label:'Analysis', icon:'◉' }, { id:'domain-specific', label:'Domain', icon:'◆' },
13681
+ { id:'ingestion', label:'Ingestion', icon:'⊞' }, { id:'utility', label:'Utility', icon:'⊡' },
13682
+ { id:'integration', label:'Integration', icon:'⊗' },
13683
+ ];
13684
+ // Hardcoded fallback data matching the spec's 20 workflows
13685
+ const WF_STORE_FALLBACK = [
13686
+ { name:'model-shootout', packageName:'@vaicli/vai-workflow-model-shootout', description:'Compare voyage-4-large, voyage-4, and voyage-4-lite side-by-side on your data.', category:'utility', tags:['benchmarking','model-comparison','cost','shared-space'], tools:['query','estimate','similarity','generate'], steps:7, tier:'official', downloads:3241, featured:true, installed:false, gradient:'linear-gradient(135deg, #0D9488, #06B6D4)' },
13687
+ { name:'asymmetric-search', packageName:'@vaicli/vai-workflow-asymmetric-search', description:'The canonical shared embedding space demo. Embed with voyage-4-large, query with voyage-4-lite. ~83% cost reduction.', category:'retrieval', tags:['asymmetric','shared-space','cost-savings'], tools:['embed','search','rerank'], steps:3, tier:'official', downloads:4812, featured:true, installed:false, gradient:'linear-gradient(135deg, #00D4AA, #40E0FF)' },
13688
+ { name:'cost-optimizer', packageName:'@vaicli/vai-workflow-cost-optimizer', description:'Quantify the exact cost savings of asymmetric retrieval on your collection.', category:'utility', tags:['cost','optimization','asymmetric'], tools:['query','estimate','similarity','generate'], steps:6, tier:'official', downloads:2918, featured:true, installed:false, gradient:'linear-gradient(135deg, #F59E0B, #EF4444)' },
13689
+ { name:'question-decomposition', packageName:'@vaicli/vai-workflow-question-decomposition', description:'Break complex questions into focused sub-queries, search each in parallel, merge and rerank.', category:'retrieval', tags:['decomposition','fan-out','synthesis'], tools:['generate','query','merge','rerank'], steps:6, tier:'official', downloads:1876, installed:false, gradient:'linear-gradient(135deg, #8B5CF6, #EC4899)' },
13690
+ { name:'contract-clause-finder', packageName:'@vaicli/vai-workflow-contract-clause-finder', description:'Search legal documents for specific clause types using voyage-law-2.', category:'domain-specific', tags:['legal','contracts','voyage-law-2'], tools:['query','rerank','generate'], steps:3, tier:'official', downloads:1543, installed:false, gradient:'linear-gradient(135deg, #1E40AF, #7C3AED)' },
13691
+ { name:'knowledge-base-bootstrap', packageName:'@vaicli/vai-workflow-knowledge-base-bootstrap', description:'End-to-end onboarding: ingest documents, verify, test a query, and generate a status report.', category:'integration', tags:['onboarding','bootstrap','end-to-end'], tools:['ingest','collections','query','generate'], steps:4, tier:'official', downloads:3654, installed:false, gradient:'linear-gradient(135deg, #059669, #10B981)' },
13692
+ { name:'embedding-drift-detector', packageName:'@vaicli/vai-workflow-embedding-drift-detector', description:'Monitor embedding quality over time. Re-embed sample documents and compare against stored vectors.', category:'analysis', tags:['monitoring','drift','operations'], tools:['search','embed','similarity','generate'], steps:5, tier:'official', downloads:987, installed:false, gradient:'linear-gradient(135deg, #DC2626, #F97316)' },
13693
+ { name:'multilingual-search', packageName:'@vaicli/vai-workflow-multilingual-search', description:'Translate your query into multiple languages, search each in parallel, merge and rerank.', category:'retrieval', tags:['multilingual','translation','cross-lingual'], tools:['generate','query','merge','rerank'], steps:5, tier:'official', downloads:1234, installed:false, gradient:'linear-gradient(135deg, #0EA5E9, #6366F1)' },
13694
+ { name:'financial-risk-scanner', packageName:'@vaicli/vai-workflow-financial-risk-scanner', description:'Scan financial documents for risk signals using voyage-finance-2.', category:'domain-specific', tags:['finance','risk','voyage-finance-2'], tools:['query','rerank','filter','generate'], steps:4, tier:'official', downloads:1102, installed:false, gradient:'linear-gradient(135deg, #B45309, #D97706)' },
13695
+ { name:'doc-freshness', packageName:'@vaicli/vai-workflow-doc-freshness', description:'Audit your knowledge base for stale content. Gathers metadata from 5 tools in parallel.', category:'utility', tags:['freshness','maintenance','monitoring'], tools:['collections','models','search','explain','estimate','generate'], steps:6, tier:'official', downloads:1456, installed:false, gradient:'linear-gradient(135deg, #4338CA, #7C3AED)' },
13696
+ { name:'incremental-sync', packageName:'@vaicli/vai-workflow-incremental-sync', description:'Smart ingestion with deduplication. Checks similarity before storing.', category:'ingestion', tags:['dedup','sync','conditional'], tools:['search','similarity','ingest','filter','generate'], steps:5, tier:'official', downloads:2103, installed:false, gradient:'linear-gradient(135deg, #15803D, #4ADE80)' },
13697
+ { name:'rag-ab-test', packageName:'@vaicli/vai-workflow-rag-ab-test', description:'Run the same query through two RAG configurations side-by-side.', category:'integration', tags:['ab-test','comparison','evaluation'], tools:['query','generate'], steps:5, tier:'official', downloads:1678, installed:false, gradient:'linear-gradient(135deg, #BE185D, #F472B6)' },
13698
+ { name:'hybrid-precision-search', packageName:'@vaicli/vai-workflow-hybrid-precision-search', description:'Three retrieval strategies in parallel — lite, large, and filtered — merged and reranked.', category:'retrieval', tags:['hybrid','parallel','precision'], tools:['query','search','merge','rerank'], steps:5, tier:'official', downloads:1890, installed:false, gradient:'linear-gradient(135deg, #0891B2, #22D3EE)' },
13699
+ { name:'code-migration-helper', packageName:'@vaicli/vai-workflow-code-migration-helper', description:'Find similar code patterns across your codebase using voyage-code-3.', category:'domain-specific', tags:['code','migration','voyage-code-3'], tools:['query','rerank','generate'], steps:3, tier:'official', downloads:932, installed:false, gradient:'linear-gradient(135deg, #475569, #94A3B8)' },
13700
+ { name:'meeting-action-items', packageName:'@vaicli/vai-workflow-meeting-action-items', description:'Ingest meeting notes and extract structured action items with owners, deadlines, and context.', category:'domain-specific', tags:['meetings','action-items','productivity'], tools:['ingest','query','generate'], steps:3, tier:'official', downloads:2567, installed:false, gradient:'linear-gradient(135deg, #7C2D12, #EA580C)' },
13701
+ { name:'collection-overlap-audit', packageName:'@vaicli/vai-workflow-collection-overlap-audit', description:'Detect duplicate content across two collections.', category:'analysis', tags:['dedup','overlap','audit'], tools:['search','similarity','merge','filter','generate'], steps:5, tier:'official', downloads:678, installed:false, gradient:'linear-gradient(135deg, #6D28D9, #A78BFA)' },
13702
+ { name:'query-quality-scorer', packageName:'@vaicli/vai-workflow-query-quality-scorer', description:'Evaluate retrieval quality without ground truth labels.', category:'analysis', tags:['evaluation','quality','scoring'], tools:['query','similarity','transform','generate'], steps:4, tier:'official', downloads:823, installed:false, gradient:'linear-gradient(135deg, #9333EA, #C084FC)' },
13703
+ { name:'clinical-protocol-match', packageName:'@vaicli/vai-workflow-clinical-protocol-match', description:'Match clinical presentations to treatment protocols.', category:'domain-specific', tags:['clinical','healthcare','protocols'], tools:['query','rerank','generate'], steps:3, tier:'official', downloads:712, installed:false, gradient:'linear-gradient(135deg, #0F766E, #2DD4BF)' },
13704
+ { name:'batch-quality-gate', packageName:'@vaicli/vai-workflow-batch-quality-gate', description:'Quality-filtered ingestion. Only ingest content that meets your similarity threshold.', category:'ingestion', tags:['quality','gate','filtering'], tools:['search','embed','similarity','ingest','filter','generate'], steps:5, tier:'official', downloads:543, installed:false, gradient:'linear-gradient(135deg, #166534, #86EFAC)' },
13705
+ { name:'index-health-check', packageName:'@vaicli/vai-workflow-index-health-check', description:'Diagnostic pipeline for your vector index.', category:'utility', tags:['diagnostics','health','index'], tools:['collections','search','query','estimate','generate'], steps:5, tier:'official', downloads:1345, installed:false, gradient:'linear-gradient(135deg, #1D4ED8, #60A5FA)' },
13706
+ ];
13707
+
13708
+ // Inject branding into fallback data (matches DEFAULT_BRANDING in catalog API)
13709
+ const _WF_FALLBACK_BRANDING = {
13710
+ 'model-shootout': { icon:'trophy', color:'#0D9488', iconPath:'M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0 0 12 0V2z' },
13711
+ 'asymmetric-search': { icon:'split', color:'#00D4AA', iconPath:'M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3M21 3l-7.828 7.828A4 4 0 0 0 12 13.7V22' },
13712
+ 'cost-optimizer': { icon:'dollar-sign', color:'#F59E0B', iconPath:'M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6' },
13713
+ 'question-decomposition': { icon:'sparkle', color:'#8B5CF6', iconPath:'M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z' },
13714
+ 'contract-clause-finder': { icon:'file-search', color:'#1E40AF', iconPath:'M14 2v4a2 2 0 0 0 2 2h4M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7zM9.5 12.5a2.5 2.5 0 1 0 5 0 2.5 2.5 0 1 0-5 0M13.3 14.3 15 16' },
13715
+ 'knowledge-base-bootstrap': { icon:'database', color:'#059669', iconPath:'M21 5c0 1.1-3.134 3-9 3S3 6.1 3 5M21 5c0-1.1-3.134-3-9-3S3 3.9 3 5M21 5v14c0 1.1-3.134 3-9 3s-9-1.9-9-3V5M21 12c0 1.1-3.134 3-9 3s-9-1.9-9-3' },
13716
+ 'embedding-drift-detector': { icon:'activity', color:'#DC2626', iconPath:'M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36-3.18-19.64A2 2 0 0 0 10.12 1h-.24a2 2 0 0 0-1.94 1.55L5.18 12H2' },
13717
+ 'multilingual-search': { icon:'globe', color:'#0EA5E9', iconPath:'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z' },
13718
+ 'financial-risk-scanner': { icon:'shield-alert', color:'#B45309', iconPath:'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 .5-.87l7-4a1 1 0 0 1 1 0l7 4A1 1 0 0 1 20 6zM12 8v4M12 16h.01' },
13719
+ 'doc-freshness': { icon:'timer', color:'#4338CA', iconPath:'M10 2h4M12 14l3-3M12 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16z' },
13720
+ 'incremental-sync': { icon:'refresh-cw', color:'#15803D', iconPath:'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16' },
13721
+ 'rag-ab-test': { icon:'flask-conical', color:'#BE185D', iconPath:'M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2M8.5 2h7M7 16h10' },
13722
+ 'hybrid-precision-search': { icon:'target', color:'#0891B2', iconPath:'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M12 12m-6 0a6 6 0 1 0 12 0 6 6 0 1 0-12 0M12 12m-2 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0' },
13723
+ 'code-migration-helper': { icon:'code', color:'#475569', iconPath:'M16 18l6-6-6-6M8 6l-6 6 6 6' },
13724
+ 'meeting-action-items': { icon:'clipboard-list',color:'#7C2D12', iconPath:'M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2M9 2h6a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1zM12 11h4M12 16h4M8 11h.01M8 16h.01' },
13725
+ 'collection-overlap-audit': { icon:'layers', color:'#6D28D9', iconPath:'M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.84zM2 12l8.58 3.91a2 2 0 0 0 1.66 0L22 12M2 17l8.58 3.91a2 2 0 0 0 1.66 0L22 17' },
13726
+ 'query-quality-scorer': { icon:'microscope', color:'#9333EA', iconPath:'M6 18h8M3 22h18M14 22a7 7 0 1 0 0-14h-1M9 14h2M9 12a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2z' },
13727
+ 'clinical-protocol-match': { icon:'heart-pulse', color:'#0F766E', iconPath:'M19.5 12.572l-7.5 7.428-7.5-7.428A5 5 0 0 1 7.5 5c1.8 0 3.3.9 4.5 2.7C13.2 5.9 14.7 5 16.5 5a5 5 0 0 1 3 9.572zM12 6l-1 4h4l-1 4' },
13728
+ 'batch-quality-gate': { icon:'check-circle', color:'#166534', iconPath:'M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4 12 14.01l-3-3' },
13729
+ 'index-health-check': { icon:'bar-chart-3', color:'#1D4ED8', iconPath:'M12 20V10M18 20V4M6 20v-4' },
13730
+ };
13731
+ WF_STORE_FALLBACK.forEach(wf => { wf.branding = _WF_FALLBACK_BRANDING[wf.name] || { icon:'zap', color:'#64748B', iconPath:'M13 2 3 14h9l-1 8 10-12h-9l1-8z' }; });
13732
+
13733
+ let _wfStoreCatalog = null;
13734
+ let _wfStoreCat = 'all';
13735
+
13736
+ function wfStoreOpen() {
13737
+ const overlay = document.getElementById('wfStoreOverlay');
13738
+ if (!overlay) return;
13739
+ overlay.classList.add('open');
13740
+ document.getElementById('wfStoreBtn').classList.add('active');
13741
+ if (!_wfStoreCatalog) wfStoreFetchCatalog();
13742
+ }
13743
+
13744
+ function wfStoreClose() {
13745
+ const overlay = document.getElementById('wfStoreOverlay');
13746
+ if (overlay) overlay.classList.remove('open');
13747
+ document.getElementById('wfStoreBtn').classList.remove('active');
13748
+ // Close detail modal if open
13749
+ const detail = document.getElementById('wfStoreDetail');
13750
+ if (detail) detail.remove();
13751
+ }
13752
+
13753
+ // Store icons map (populated from API response or used from inline fallback)
13754
+ let _wfStoreIcons = {};
13755
+
13756
+ // Render a branding icon as inline SVG
13757
+ function wfBrandingIcon(wf, size = 16) {
13758
+ const b = wf.branding || {};
13759
+ const iconPath = b.iconPath || _wfStoreIcons[b.icon] || '';
13760
+ if (!iconPath) return '';
13761
+ return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="${iconPath}"/></svg>`;
13762
+ }
13763
+
13764
+ async function wfStoreFetchCatalog() {
13765
+ try {
13766
+ const res = await fetch('/api/workflows/catalog');
13767
+ const data = await res.json();
13768
+ if (data.icons) _wfStoreIcons = data.icons;
13769
+ if (data.workflows && data.workflows.length > 0) {
13770
+ _wfStoreCatalog = data.workflows;
13771
+ } else {
13772
+ _wfStoreCatalog = WF_STORE_FALLBACK;
13773
+ }
13774
+ } catch {
13775
+ _wfStoreCatalog = WF_STORE_FALLBACK;
13776
+ }
13777
+ wfStoreRender();
13778
+ }
13779
+
13780
+ function wfStoreRender() {
13781
+ const container = document.getElementById('wfStoreContent');
13782
+ if (!container || !_wfStoreCatalog) return;
13783
+ const search = (document.getElementById('wfStoreSearch')?.value || '').toLowerCase();
13784
+ const sort = document.getElementById('wfStoreSort')?.value || 'downloads';
13785
+
13786
+ const filtered = _wfStoreCatalog.filter(wf => {
13787
+ const mc = _wfStoreCat === 'all' || wf.category === _wfStoreCat;
13788
+ const ms = !search || wf.name.includes(search) || (wf.description||'').toLowerCase().includes(search)
13789
+ || (wf.tags||[]).some(t => t.includes(search)) || (wf.tools||[]).some(t => t.includes(search));
13790
+ return mc && ms;
13791
+ }).sort((a, b) => sort === 'downloads' ? (b.downloads||0) - (a.downloads||0) : sort === 'name' ? a.name.localeCompare(b.name) : (b.steps||0) - (a.steps||0));
13792
+
13793
+ const featured = _wfStoreCatalog.filter(w => w.featured);
13794
+ const showFeat = _wfStoreCat === 'all' && !search;
13795
+
13796
+ let html = '';
13797
+ // Category chips
13798
+ html += '<div class="wf-store-chips">';
13799
+ WF_STORE_CATEGORIES.forEach(c => {
13800
+ html += `<button class="wf-store-chip ${_wfStoreCat===c.id?'active':''}" onclick="_wfStoreCat='${c.id}';wfStoreRender()">${c.icon} ${c.label}</button>`;
13801
+ });
13802
+ html += '</div>';
13803
+
13804
+ // Featured section
13805
+ if (showFeat && featured.length > 0) {
13806
+ html += '<div class="wf-store-section-label">Featured</div>';
13807
+ html += '<div class="wf-store-featured-grid">';
13808
+ featured.forEach(wf => {
13809
+ const featAuthor = wf.author && wf.author.name && wf.author.name !== 'unknown' ? wf.author : null;
13810
+ const featAvatarHtml = featAuthor ? (featAuthor.avatar
13811
+ ? `<div class="wf-store-featured-avatar"><img src="${featAuthor.avatar}" onerror="this.parentNode.textContent='${featAuthor.name.charAt(0).toUpperCase()}'"></div>`
13812
+ : `<div class="wf-store-featured-avatar">${featAuthor.name.charAt(0).toUpperCase()}</div>`) : '';
13813
+ const featAuthorHtml = featAuthor ? `<div class="wf-store-featured-author">${featAvatarHtml} by ${featAuthor.name}</div>` : '';
13814
+ const featIconHtml = wfBrandingIcon(wf, 20) ? `<div class="wf-store-featured-icon">${wfBrandingIcon(wf, 20)}</div>` : '';
13815
+ html += `<div class="wf-store-featured-card" style="background:${wf.gradient}" onclick="wfStoreShowDetail('${wf.name}')">
13816
+ ${featIconHtml}
13817
+ <div class="wf-store-featured-dl">↓ ${(wf.downloads||0).toLocaleString()}</div>
13818
+ <div class="wf-store-featured-content"><h3>${wf.name}</h3><p>${wf.description||''}</p>${featAuthorHtml}</div>
13819
+ </div>`;
13820
+ });
13821
+ html += '</div>';
13822
+ }
13823
+
13824
+ // Grid label
13825
+ const label = _wfStoreCat === 'all' ? 'All Workflows' : (WF_STORE_CATEGORIES.find(c=>c.id===_wfStoreCat)?.label || _wfStoreCat);
13826
+ html += `<div class="wf-store-section-label">${label} <span class="wf-store-count">${filtered.length}</span></div>`;
13827
+
13828
+ if (filtered.length === 0) {
13829
+ html += '<div class="wf-store-empty"><p>No workflows match your search</p></div>';
13830
+ } else {
13831
+ html += '<div class="wf-store-grid">';
13832
+ filtered.forEach(wf => {
13833
+ const dots = (wf.tools||[]).slice(0,5).map(t =>
13834
+ `<div class="wf-store-card-dot" style="background:${WF_STORE_TOOL_COLORS[t]||'#666'}"></div>`
13835
+ ).join('') + ((wf.tools||[]).length > 5 ? `<span style="font-family:var(--mono);font-size:9px;color:var(--text-muted);margin-left:2px">+${wf.tools.length-5}</span>` : '');
13836
+ const badges = (wf.installed ? '<span class="wf-store-badge-installed">installed</span>' : '') +
13837
+ `<span class="wf-store-badge-official">${wf.tier==='official' ? '✓ OFFICIAL' : 'COMMUNITY'}</span>`;
13838
+ const cardIconColor = (wf.branding && wf.branding.color) || '#64748B';
13839
+ const cardIconSvg = wfBrandingIcon(wf, 16);
13840
+ const cardIconHtml = cardIconSvg
13841
+ ? `<div class="wf-store-card-icon" style="background:${cardIconColor}18;color:${cardIconColor}">${cardIconSvg}</div>`
13842
+ : '';
13843
+ html += `<div class="wf-store-card" onclick="wfStoreShowDetail('${wf.name}')">
13844
+ <div class="wf-store-card-bar" style="background:${wf.gradient}"></div>
13845
+ <div class="wf-store-card-top">
13846
+ <div class="wf-store-card-name-wrap">${cardIconHtml}<span class="wf-store-card-name">${wf.name}</span></div>
13847
+ <div class="wf-store-card-badges">${badges}</div>
13848
+ </div>
13849
+ <div class="wf-store-card-desc">${wf.description||''}</div>
13850
+ ${wf.author && wf.author.name && wf.author.name !== 'unknown' ? `<div class="wf-store-card-author">by ${wf.author.name}</div>` : ''}
13851
+ <div class="wf-store-card-meta">
13852
+ <span class="wf-store-card-cat">${wf.category||'utility'}</span>
13853
+ <div class="wf-store-card-dots">${dots}</div>
13854
+ <span class="wf-store-card-complexity">${wf.steps||0}s·${wf.toolCount||(Array.isArray(wf.tools)?wf.tools.length:0)}t</span>
13855
+ <span class="wf-store-card-downloads">↓ ${(wf.downloads||0).toLocaleString()}</span>
13856
+ </div>
13857
+ </div>`;
13858
+ });
13859
+ html += '</div>';
13860
+ }
13861
+
13862
+ container.innerHTML = html;
13863
+ }
13864
+
13865
+ function wfStoreShowDetail(name) {
13866
+ const wf = (_wfStoreCatalog || []).find(w => w.name === name);
13867
+ if (!wf) return;
13868
+ // Remove existing detail
13869
+ const existing = document.getElementById('wfStoreDetail');
13870
+ if (existing) existing.remove();
13871
+
13872
+ const ic = `vai workflow install ${wf.packageName}`;
13873
+ const rc = `vai workflow run ${wf.packageName}`;
13874
+
13875
+ // Escape for use in HTML attributes (single-quoted onclick handlers)
13876
+ const icEsc = ic.replace(/'/g, "\\'");
13877
+ const rcEsc = rc.replace(/'/g, "\\'");
13878
+
13879
+ const toolsHtml = (wf.tools||[]).map(t => {
13880
+ const c = WF_STORE_TOOL_COLORS[t] || '#666';
13881
+ return `<span class="wf-store-detail-tool" style="color:${c};border-color:${c}44;background:${c}11">${t}</span>`;
13882
+ }).join('');
13883
+
13884
+ const tagsHtml = `<span class="wf-store-detail-tag">${wf.category||'utility'}</span>` +
13885
+ (wf.tags||[]).map(t => `<span class="wf-store-detail-tag">${t}</span>`).join('');
13886
+
13887
+ const installBtn = wf.installed
13888
+ ? '<button class="wf-store-detail-btn wf-store-detail-btn-installed">✓ Installed</button>'
13889
+ : `<button class="wf-store-detail-btn wf-store-detail-btn-primary" onclick="wfStoreInstall('${wf.packageName}',this)">Install</button>`;
13890
+
13891
+ const modal = document.createElement('div');
13892
+ modal.id = 'wfStoreDetail';
13893
+ modal.className = 'wf-store-detail-bg';
13894
+ modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
13895
+ modal.innerHTML = `<div class="wf-store-detail-panel" onclick="event.stopPropagation()">
13896
+ <div class="wf-store-detail-hero" style="background:${wf.gradient}">
13897
+ <button class="wf-store-detail-close" onclick="document.getElementById('wfStoreDetail').remove()">×</button>
13898
+ <div class="wf-store-detail-hero-inner">
13899
+ ${wfBrandingIcon(wf, 26) ? `<div class="wf-store-detail-hero-icon">${wfBrandingIcon(wf, 26)}</div>` : ''}
13900
+ <div>
13901
+ <div class="wf-store-detail-name">${wf.name}</div>
13902
+ <div class="wf-store-detail-pkg">${wf.packageName}</div>
13903
+ </div>
13904
+ </div>
13905
+ </div>
13906
+ <div class="wf-store-detail-body">
13907
+ ${(() => {
13908
+ const a = wf.author && wf.author.name && wf.author.name !== 'unknown' ? wf.author : null;
13909
+ if (!a) return '';
13910
+ const avatarInner = a.avatar
13911
+ ? `<img src="${a.avatar}" onerror="this.parentNode.textContent='${a.name.charAt(0).toUpperCase()}'">`
13912
+ : a.name.charAt(0).toUpperCase();
13913
+ const nameHtml = a.url
13914
+ ? `<a href="${a.url}" target="_blank" rel="noopener">${a.name}</a>`
13915
+ : a.name;
13916
+ return `<div class="wf-store-detail-author"><div class="wf-store-detail-avatar">${avatarInner}</div><div class="wf-store-detail-author-name">${nameHtml}</div></div>`;
13917
+ })()}
13918
+ <p class="wf-store-detail-desc">${wf.description||''}</p>
13919
+ ${wf.assets && wf.assets.screenshots && wf.assets.screenshots.length > 0 ? `
13920
+ <div class="wf-store-detail-section">
13921
+ <div class="wf-store-detail-label">Screenshots</div>
13922
+ <div class="wf-store-detail-screenshots">${wf.assets.screenshots.map(s => `<img src="${s}" alt="Screenshot">`).join('')}</div>
13923
+ </div>` : ''}
13924
+ <div class="wf-store-detail-section">
13925
+ <div class="wf-store-detail-label">Stats</div>
13926
+ <div class="wf-store-detail-stats">
13927
+ <div class="wf-store-detail-stat"><div class="wf-store-detail-stat-val">${wf.steps||0}</div><div class="wf-store-detail-stat-lbl">Steps</div></div>
13928
+ <div class="wf-store-detail-stat"><div class="wf-store-detail-stat-val">${wf.toolCount||(Array.isArray(wf.tools)?wf.tools.length:0)}</div><div class="wf-store-detail-stat-lbl">Tools</div></div>
13929
+ <div class="wf-store-detail-stat"><div class="wf-store-detail-stat-val">${(wf.downloads||0).toLocaleString()}</div><div class="wf-store-detail-stat-lbl">Downloads</div></div>
13930
+ </div>
13931
+ </div>
13932
+ <div class="wf-store-detail-section">
13933
+ <div class="wf-store-detail-label">Tools</div>
13934
+ <div class="wf-store-detail-tools">${toolsHtml}</div>
13935
+ </div>
13936
+ <div class="wf-store-detail-section">
13937
+ <div class="wf-store-detail-label">Tags</div>
13938
+ <div class="wf-store-detail-tags">${tagsHtml}</div>
13939
+ </div>
13940
+ ${wf.inputs && wf.inputs.length > 0 ? `
13941
+ <div class="wf-store-detail-section">
13942
+ <div class="wf-store-detail-label">Input Parameters</div>
13943
+ <table class="wf-store-detail-inputs-table">
13944
+ <thead><tr><th>Name</th><th>Type</th><th>Required / Default</th></tr></thead>
13945
+ <tbody>${wf.inputs.map(inp => `<tr><td>${inp.name}</td><td>${inp.type}</td><td>${inp.required ? 'required' : inp.default !== undefined ? 'default: ' + inp.default : 'optional'}</td></tr>`).join('')}</tbody>
13946
+ </table>
13947
+ </div>` : ''}
13948
+ <div class="wf-store-detail-install">
13949
+ <div class="wf-store-detail-label">Install</div>
13950
+ <div class="wf-store-detail-cmd" onclick="navigator.clipboard.writeText('${icEsc}');this.querySelector('.wf-store-detail-cmd-hint').textContent='✓ copied';setTimeout(()=>this.querySelector('.wf-store-detail-cmd-hint').textContent='copy',2000)">
13951
+ <span>$ ${ic}</span><span class="wf-store-detail-cmd-hint">copy</span>
13952
+ </div>
13953
+ <div class="wf-store-detail-label" style="margin-top:12px">Run</div>
13954
+ <div class="wf-store-detail-cmd" style="color:var(--text-dim)" onclick="navigator.clipboard.writeText('${rcEsc}');this.querySelector('.wf-store-detail-cmd-hint').textContent='✓ copied';setTimeout(()=>this.querySelector('.wf-store-detail-cmd-hint').textContent='copy',2000)">
13955
+ <span>$ ${rc}</span><span class="wf-store-detail-cmd-hint">copy</span>
13956
+ </div>
13957
+ </div>
13958
+ <div class="wf-store-detail-actions">
13959
+ ${installBtn}
13960
+ <button class="wf-store-detail-btn wf-store-detail-btn-secondary" onclick="wfStoreRun('${wf.name}')">Run</button>
13961
+ <button class="wf-store-detail-btn wf-store-detail-btn-secondary" onclick="wfStoreCanvas('${wf.name}')">Canvas</button>
13962
+ </div>
13963
+ </div>
13964
+ </div>`;
13965
+ document.body.appendChild(modal);
13966
+ }
13967
+
13968
+ async function wfStoreInstall(packageName, btn) {
13969
+ if (btn) { btn.textContent = 'Installing...'; btn.disabled = true; }
13970
+ try {
13971
+ const res = await fetch('/api/workflows/community/install', {
13972
+ method: 'POST',
13973
+ headers: { 'Content-Type': 'application/json' },
13974
+ body: JSON.stringify({ name: packageName })
13975
+ });
13976
+ const text = await res.text();
13977
+ let data;
13978
+ try { data = JSON.parse(text); } catch { data = { error: text || 'Empty response (status ' + res.status + ')' }; }
13979
+ console.log('[Store Install]', packageName, data);
13980
+ if (data.success) {
13981
+ if (btn) { btn.textContent = '✓ Installed'; btn.className = 'wf-store-detail-btn wf-store-detail-btn-installed'; btn.disabled = true; }
13982
+ // Update catalog state
13983
+ const wf = (_wfStoreCatalog||[]).find(w => w.packageName === packageName);
13984
+ if (wf) wf.installed = true;
13985
+ wfStoreRender();
13986
+ await wfLoadLibrary(); // Refresh library panel
13987
+ } else {
13988
+ const errMsg = data.error || 'Unknown error';
13989
+ console.error('[Store Install Error]', errMsg);
13990
+ if (btn) { btn.textContent = 'Failed'; btn.title = errMsg; setTimeout(() => { btn.textContent = 'Install'; btn.disabled = false; btn.title = ''; }, 4000); }
13991
+ }
13992
+ } catch (err) {
13993
+ console.error('[Store Install Exception]', err);
13994
+ if (btn) { btn.textContent = 'Failed'; btn.title = err.message; setTimeout(() => { btn.textContent = 'Install'; btn.disabled = false; btn.title = ''; }, 4000); }
13995
+ }
13996
+ }
13997
+
13998
+ async function wfStoreRun(name) {
13999
+ const detail = document.getElementById('wfStoreDetail');
14000
+ if (detail) detail.remove();
14001
+ wfStoreClose();
14002
+
14003
+ // Find the workflow in catalog
14004
+ const wf = (_wfStoreCatalog||[]).find(w => w.name === name);
14005
+ if (!wf) {
14006
+ alert('Workflow not found: ' + name);
14007
+ return;
14008
+ }
14009
+
14010
+ // Check if installed
14011
+ if (!wf.installed) {
14012
+ const doInstall = confirm(`"${wf.name}" is not installed. Install it now?`);
14013
+ if (doInstall) {
14014
+ await wfStoreInstall(wf.packageName, null);
14015
+ // Refresh catalog to update installed status
14016
+ await wfStoreFetchCatalog();
14017
+ // Check again
14018
+ const updated = (_wfStoreCatalog||[]).find(w => w.name === name);
14019
+ if (!updated || !updated.installed) {
14020
+ alert('Installation may have failed. Please try again.');
14021
+ return;
14022
+ }
14023
+ } else {
14024
+ return;
14025
+ }
14026
+ }
14027
+
14028
+ // Switch to workflows tab and load the workflow
14029
+ switchTab('workflows');
14030
+ // Use the full package name for resolution
14031
+ wfSelectWorkflow(wf.packageName);
14032
+ }
14033
+
14034
+ function wfStoreCanvas(name) {
14035
+ wfStoreRun(name); // Same behavior — load the workflow into canvas
14036
+ }
14037
+
14038
+ // Escape key handler for store
14039
+ document.addEventListener('keydown', (e) => {
14040
+ if (e.key === 'Escape') {
14041
+ const detail = document.getElementById('wfStoreDetail');
14042
+ if (detail) { detail.remove(); return; }
14043
+ const overlay = document.getElementById('wfStoreOverlay');
14044
+ if (overlay && overlay.classList.contains('open')) { wfStoreClose(); }
14045
+ }
14046
+ });
14047
+
10973
14048
  function wfInit() {
10974
14049
  wfLoadLibrary();
10975
14050
  wfInitPan();