voyageai-cli 1.28.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.
@@ -1779,6 +1779,43 @@ select:focus { outline: none; border-color: var(--accent); }
1779
1779
  margin-bottom: 8px;
1780
1780
  }
1781
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); }
1782
1819
  .about-text a { color: var(--blue); text-decoration: none; }
1783
1820
  .about-text a:hover { text-decoration: underline; }
1784
1821
  .about-disclaimer {
@@ -3127,6 +3164,199 @@ select:focus { outline: none; border-color: var(--accent); }
3127
3164
  .wf-palette-item:active { cursor: grabbing; }
3128
3165
  .wf-palette-icon { width: 20px; text-align: center; display: flex; align-items: center; justify-content: center; }
3129
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
+
3130
3360
  /* Builder port styles */
3131
3361
  .wf-port-builder { cursor: crosshair; transition: r 0.15s; pointer-events: all !important; }
3132
3362
  .wf-port-builder:hover { r: 9; filter: brightness(1.4); }
@@ -3400,6 +3630,8 @@ select:focus { outline: none; border-color: var(--accent); }
3400
3630
  }
3401
3631
  @keyframes wf-flow { to { stroke-dashoffset: -12; } }
3402
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; }
3403
3635
  /* Inspector */
3404
3636
  .wf-inspector {
3405
3637
  flex-shrink: 0; position: relative;
@@ -3550,8 +3782,682 @@ select:focus { outline: none; border-color: var(--accent); }
3550
3782
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
3551
3783
  pointer-events: none; z-index: 0; opacity: 0.04;
3552
3784
  }
3553
- .wf-canvas-watermark img { width: 300px; height: 300px; filter: invert(1); }
3554
- [data-theme="light"] .wf-canvas-watermark img { filter: none; }
3785
+ .wf-canvas-watermark img { width: 300px; height: 300px; filter: invert(1); }
3786
+ [data-theme="light"] .wf-canvas-watermark img { filter: none; }
3787
+
3788
+ /* ── Home Page Styles ── */
3789
+
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
+ }
3799
+
3800
+ .home-header-left {
3801
+ display: flex;
3802
+ align-items: center;
3803
+ gap: 12px;
3804
+ }
3805
+
3806
+ .home-logo {
3807
+ width: 32px;
3808
+ height: 32px;
3809
+ }
3810
+
3811
+ .home-header-title {
3812
+ font-size: 16px;
3813
+ font-weight: 600;
3814
+ color: var(--text);
3815
+ letter-spacing: -0.01em;
3816
+ }
3817
+
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
+ }
3827
+
3828
+ .home-header-right {
3829
+ display: flex;
3830
+ align-items: center;
3831
+ gap: 12px;
3832
+ }
3833
+
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
+ }
3843
+
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
+ }
3555
4461
  </style>
3556
4462
  </head>
3557
4463
  <body>
@@ -3628,7 +4534,8 @@ select:focus { outline: none; border-color: var(--accent); }
3628
4534
  <nav class="sidebar-nav">
3629
4535
  <div class="sidebar-nav-group" role="tablist" aria-label="Tools">
3630
4536
  <div class="sidebar-nav-label" id="nav-tools-label">Tools</div>
3631
- <button class="tab-btn active" data-tab="embed" role="tab" aria-selected="true" aria-controls="tab-embed" id="tab-btn-embed"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-lightning"/></svg></span><span>Embed</span></button>
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>
3632
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>
3633
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>
3634
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>
@@ -3687,8 +4594,169 @@ select:focus { outline: none; border-color: var(--accent); }
3687
4594
  </div>
3688
4595
  <div class="main">
3689
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
+
3690
4758
  <!-- ========== EMBED TAB ========== -->
3691
- <div class="tab-panel active" id="tab-embed" role="tabpanel" aria-labelledby="tab-btn-embed" tabindex="0">
4759
+ <div class="tab-panel" id="tab-embed" role="tabpanel" aria-labelledby="tab-btn-embed" tabindex="0">
3692
4760
  <div class="page-header">
3693
4761
  <h2 class="page-header-title">Embed</h2>
3694
4762
  <p class="page-header-subtitle">Generate vector embeddings for text</p>
@@ -4494,10 +5562,13 @@ Reranking models rescore initial search results to improve relevance ordering.</
4494
5562
  <div class="wf-container">
4495
5563
  <div class="wf-library">
4496
5564
  <div class="wf-library-header">
4497
- <div class="wf-library-tabs">
5565
+ <div class="wf-library-tabs" style="flex:1;">
4498
5566
  <button class="wf-lib-tab active" data-lib-tab="library" onclick="wfSwitchLibTab('library')">Library</button>
4499
5567
  <button class="wf-lib-tab" data-lib-tab="palette" onclick="wfSwitchLibTab('palette')">Palette</button>
4500
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>
4501
5572
  </div>
4502
5573
  <div class="wf-library-list" id="wfLibraryList">
4503
5574
  <div style="padding: 16px; color: var(--text-muted); font-size: 12px;">Loading...</div>
@@ -4541,6 +5612,30 @@ Reranking models rescore initial search results to improve relevance ordering.</
4541
5612
  </div>
4542
5613
  <div class="wf-canvas-watermark" id="wfCanvasWatermark"><img src="/icons/watermark.png" alt=""></div>
4543
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>
4544
5639
  </div>
4545
5640
  <div class="wf-inspector collapsed" id="wfInspector">
4546
5641
  <button class="wf-inspector-toggle" id="wfInspectorToggle" onclick="wfToggleInspector()" title="Toggle inspector">&lsaquo;</button>
@@ -4665,13 +5760,39 @@ Reranking models rescore initial search results to improve relevance ordering.</
4665
5760
  <div class="card" style="margin-top:16px;">
4666
5761
  <div class="about-section" style="padding-bottom:0;">
4667
5762
  <div class="about-section-title">What's New</div>
4668
- <div class="about-text" style="font-size:13px;">
4669
- <strong>v1.26</strong> — Agent workflows with thinking panel &amp; markdown rendering, multi-step tool orchestration<br>
4670
- <strong>v1.25</strong> — Code generation &amp; project scaffolding tabs<br>
4671
- <strong>v1.24</strong> MCP server install/uninstall/status commands, Electron app v1.5<br>
4672
- <strong>v1.23</strong> — MCP server (expose vai tools to AI agents), HTTP transport, bearer auth, 71+ MCP tests<br>
4673
- <strong>v1.22</strong> — RAG chat with smart source labels, configurable system prompts, streaming responses<br>
4674
- <strong>v1.2</strong> — Multimodal tab, 4 new Explore concepts, auto-update, hidden easter egg 🕹️
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>
4675
5796
  </div>
4676
5797
  </div>
4677
5798
  </div>
@@ -4976,6 +6097,26 @@ Reranking models rescore initial search results to improve relevance ordering.</
4976
6097
  </select>
4977
6098
  </div>
4978
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>
4979
6120
  </div>
4980
6121
  </div>
4981
6122
 
@@ -5453,89 +6594,463 @@ function initTelemetryToggle() {
5453
6594
  });
5454
6595
  }
5455
6596
 
5456
- async function init() {
5457
- // Mark body for Electron vs Web styling
5458
- if (window.vai && window.vai.isElectron) {
5459
- document.body.classList.add('is-electron');
6597
+ async function init() {
6598
+ // Mark body for Electron vs Web styling
6599
+ if (window.vai && window.vai.isElectron) {
6600
+ document.body.classList.add('is-electron');
6601
+ }
6602
+ setupTabs();
6603
+ await loadConfig();
6604
+ await Promise.all([loadModels(), loadConcepts()]);
6605
+ populateModelSelects();
6606
+ buildExploreCards();
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
+
6613
+ // Telemetry
6614
+ initTelemetryToggle();
6615
+ sendTelemetry('app_launch');
6616
+ }
6617
+
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;
5460
6968
  }
5461
- setupTabs();
5462
- await loadConfig();
5463
- await Promise.all([loadModels(), loadConcepts()]);
5464
- populateModelSelects();
5465
- buildExploreCards();
6969
+
6970
+ let workflows = [...homeData.communityWorkflows];
5466
6971
 
5467
- // Telemetry
5468
- initTelemetryToggle();
5469
- sendTelemetry('app_launch');
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('');
5470
7001
  }
5471
7002
 
5472
- // ── Tabs ──
5473
- function setupTabs() {
5474
- const tabBtns = document.querySelectorAll('.tab-btn');
5475
- const tabList = Array.from(tabBtns);
7003
+ function sortCommunityWorkflows(mode) {
7004
+ homeData.sortMode = mode;
5476
7005
 
5477
- tabBtns.forEach(btn => {
5478
- btn.addEventListener('click', () => {
5479
- switchTab(btn.dataset.tab);
5480
- });
5481
-
5482
- // Keyboard navigation for accessibility
5483
- btn.addEventListener('keydown', (e) => {
5484
- const currentIndex = tabList.indexOf(btn);
5485
- let nextIndex = -1;
5486
-
5487
- if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
5488
- e.preventDefault();
5489
- nextIndex = (currentIndex + 1) % tabList.length;
5490
- } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
5491
- e.preventDefault();
5492
- nextIndex = (currentIndex - 1 + tabList.length) % tabList.length;
5493
- } else if (e.key === 'Home') {
5494
- e.preventDefault();
5495
- nextIndex = 0;
5496
- } else if (e.key === 'End') {
5497
- e.preventDefault();
5498
- nextIndex = tabList.length - 1;
5499
- }
5500
-
5501
- if (nextIndex >= 0) {
5502
- tabList[nextIndex].focus();
5503
- switchTab(tabList[nextIndex].dataset.tab);
5504
- }
5505
- });
7006
+ // Update tab appearance
7007
+ document.querySelectorAll('.home-sort-tab').forEach(tab => {
7008
+ tab.classList.toggle('active', tab.dataset.sort === mode);
5506
7009
  });
7010
+
7011
+ renderCommunityWorkflows();
7012
+ }
5507
7013
 
5508
- // Settings cog in header
5509
- const settingsCog = document.querySelector('.sidebar-settings-btn');
5510
- if (settingsCog) {
5511
- settingsCog.addEventListener('click', () => {
5512
- switchTab(settingsCog.dataset.tab);
5513
- });
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');
5514
7036
  }
5515
7037
  }
5516
7038
 
5517
- function switchTab(tab) {
5518
- document.querySelectorAll('.tab-btn').forEach(b => {
5519
- const isActive = b.dataset.tab === tab;
5520
- b.classList.toggle('active', isActive);
5521
- // Update ARIA attributes for accessibility
5522
- b.setAttribute('aria-selected', isActive ? 'true' : 'false');
5523
- b.setAttribute('tabindex', isActive ? '0' : '-1');
5524
- });
5525
- document.querySelectorAll('.tab-panel').forEach(p => {
5526
- const isActive = p.id === 'tab-' + tab;
5527
- p.classList.toggle('active', isActive);
5528
- // Hide inactive panels from screen readers
5529
- p.setAttribute('aria-hidden', isActive ? 'false' : 'true');
5530
- });
5531
- // Sync settings cog highlight
5532
- const settingsBtn = document.querySelector('.sidebar-settings-btn');
5533
- if (settingsBtn) settingsBtn.classList.toggle('active', tab === 'settings');
5534
- // Track tab views
5535
- 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
+ }
5536
7053
  }
5537
- // Expose globally so Electron main process can call it
5538
- window.switchTab = switchTab;
5539
7054
 
5540
7055
  // ── Config ──
5541
7056
  async function loadConfig() {
@@ -7461,6 +8976,15 @@ function initSettings() {
7461
8976
  timeoutSel.addEventListener('change', () => saveSetting('timeout', timeoutSel.value));
7462
8977
  }
7463
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
+
7464
8988
  // Benchmark iterations
7465
8989
  const benchIterSel = document.getElementById('settingsBenchIter');
7466
8990
  if (benchIterSel) {
@@ -10016,6 +11540,13 @@ const WF_NODE_META = {
10016
11540
  rerank: { icon: 'M3 6h18M7 12h10M10 18h4', label: 'Rerank', color: '#CE93D8', category: 'retrieval' },
10017
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' },
10018
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' },
10019
11550
  };
10020
11551
 
10021
11552
  // Fallback icon (gear) for unknown workflow node types
@@ -10061,6 +11592,8 @@ async function wfLoadLibrary() {
10061
11592
  const wfData = await wfRes.json();
10062
11593
  const exData = await exRes.json();
10063
11594
  wfState.workflows = wfData.workflows || [];
11595
+ wfState.official = wfData.official || [];
11596
+ wfState.community = wfData.community || [];
10064
11597
  wfState.examples = exData.examples || [];
10065
11598
  wfRenderLibrary();
10066
11599
  } catch (err) {
@@ -10111,9 +11644,169 @@ function wfRenderLibrary() {
10111
11644
  html += '</div></div>';
10112
11645
  }
10113
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
+
10114
11697
  list.innerHTML = html;
10115
11698
  }
10116
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
+
10117
11810
  function wfToggleExamples(btn) {
10118
11811
  const content = btn.nextElementSibling;
10119
11812
  const isOpen = content.style.display !== 'none';
@@ -10257,6 +11950,19 @@ async function wfRenderWorkflow(definition) {
10257
11950
  });
10258
11951
  }
10259
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
+
10260
11966
  // Draw edges first (behind nodes)
10261
11967
  const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
10262
11968
  edgeGroup.classList.add('wf-edge-group');
@@ -10267,6 +11973,15 @@ async function wfRenderWorkflow(definition) {
10267
11973
  const depId = rawDepId.replace(/^!/, '');
10268
11974
  if (positions[depId] && positions[stepId]) {
10269
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
+ }
10270
11985
  edgeGroup.appendChild(edge);
10271
11986
  }
10272
11987
  });
@@ -10334,14 +12049,27 @@ function wfDrawNode(step, x, y, state, hasDeps, hasDependents) {
10334
12049
  });
10335
12050
  }
10336
12051
 
10337
- // Background rect
10338
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
10339
- rect.setAttribute('width', WF_NODE_W);
10340
- rect.setAttribute('height', WF_NODE_H);
10341
- rect.setAttribute('fill', meta.color);
10342
- rect.setAttribute('stroke', meta.color);
10343
- rect.setAttribute('opacity', '0.85');
10344
- g.appendChild(rect);
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
+ }
10345
12073
 
10346
12074
  // Input port (left side): show in builder mode always, otherwise only if has deps
10347
12075
  const showInPort = wfState.builderMode || hasDeps;
@@ -11454,10 +13182,16 @@ const WF_INPUT_DEFS = {
11454
13182
  filter: [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'condition', type: 'text', required: true, placeholder: 'item.score > 0.5' }],
11455
13183
  transform: [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'expression', type: 'text', required: true, placeholder: 'item.text' }],
11456
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' }],
11457
13191
  };
11458
13192
 
11459
- const WF_CATEGORY_ORDER = ['retrieval', 'embedding', 'management', 'utility', 'control', 'generation'];
11460
- const WF_CATEGORY_LABELS = { retrieval: 'Retrieval', embedding: 'Embedding', management: 'Management', utility: 'Utility', control: 'Control Flow', generation: 'Generation' };
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' };
11461
13195
 
11462
13196
  // ── Builder: Library/Palette tab toggle ──
11463
13197
  function wfSwitchLibTab(tab) {
@@ -11935,6 +13669,382 @@ document.addEventListener('keydown', (e) => {
11935
13669
  });
11936
13670
 
11937
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
+
11938
14048
  function wfInit() {
11939
14049
  wfLoadLibrary();
11940
14050
  wfInitPan();