voyageai-cli 1.30.1 → 1.30.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -1
  3. package/src/cli.js +2 -0
  4. package/src/commands/about.js +3 -3
  5. package/src/commands/code-search.js +751 -0
  6. package/src/commands/doctor.js +1 -1
  7. package/src/commands/index-workspace.js +9 -5
  8. package/src/commands/playground.js +9 -1
  9. package/src/commands/quickstart.js +4 -4
  10. package/src/commands/workflow.js +132 -65
  11. package/src/lib/catalog.js +4 -2
  12. package/src/lib/code-search.js +315 -0
  13. package/src/lib/codegen.js +1 -1
  14. package/src/lib/explanations.js +3 -3
  15. package/src/lib/github.js +226 -0
  16. package/src/lib/template-engine.js +154 -20
  17. package/src/lib/workflow-builder.js +753 -0
  18. package/src/lib/workflow-formatters.js +454 -0
  19. package/src/lib/workflow-input-cache.js +111 -0
  20. package/src/lib/workflow-scaffold.js +1 -1
  21. package/src/lib/workflow.js +91 -1
  22. package/src/mcp/schemas/index.js +130 -0
  23. package/src/mcp/server.js +17 -4
  24. package/src/mcp/tools/authoring.js +662 -0
  25. package/src/mcp/tools/code-search.js +620 -0
  26. package/src/mcp/tools/ingest.js +2 -5
  27. package/src/mcp/tools/retrieval.js +2 -15
  28. package/src/mcp/tools/workspace.js +1 -12
  29. package/src/mcp/utils.js +20 -0
  30. package/src/playground/help/workflow-nodes.js +127 -2
  31. package/src/playground/index.html +1366 -24
  32. package/src/workflows/code-review.json +110 -0
  33. package/src/workflows/cost-analysis.json +5 -0
  34. package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
  35. package/src/workflows/tests/code-review.happy-path.test.json +121 -0
  36. package/src/workflows/tests/code-review.no-question.test.json +70 -0
  37. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +2 -2
@@ -1089,6 +1089,494 @@ select:focus { outline: none; border-color: var(--accent); }
1089
1089
  display: none;
1090
1090
  }
1091
1091
 
1092
+ /* ── Models Tab ── */
1093
+ .models-toolbar {
1094
+ display: flex;
1095
+ align-items: center;
1096
+ gap: 12px;
1097
+ margin-bottom: 20px;
1098
+ flex-wrap: wrap;
1099
+ }
1100
+ .models-filter-pills {
1101
+ display: flex;
1102
+ gap: 6px;
1103
+ }
1104
+ .models-filter-pill {
1105
+ background: var(--bg-card);
1106
+ border: 1px solid var(--border);
1107
+ color: var(--text-dim);
1108
+ padding: 4px 12px;
1109
+ border-radius: 20px;
1110
+ font-size: 12px;
1111
+ font-family: var(--font);
1112
+ cursor: pointer;
1113
+ transition: all 0.15s;
1114
+ }
1115
+ .models-filter-pill:hover {
1116
+ border-color: var(--accent);
1117
+ color: var(--text);
1118
+ }
1119
+ .models-filter-pill.active {
1120
+ background: rgba(0, 212, 170, 0.12);
1121
+ border-color: var(--accent);
1122
+ color: var(--accent);
1123
+ }
1124
+ .models-hero {
1125
+ position: relative;
1126
+ overflow: hidden;
1127
+ background: linear-gradient(135deg, rgba(0, 212, 170, 0.06), rgba(64, 224, 255, 0.06));
1128
+ border: 1px solid rgba(0, 212, 170, 0.2);
1129
+ margin-bottom: 24px;
1130
+ }
1131
+ .models-hero-shape {
1132
+ position: absolute;
1133
+ top: -30px;
1134
+ right: -30px;
1135
+ width: 200px;
1136
+ height: 200px;
1137
+ pointer-events: none;
1138
+ z-index: 0;
1139
+ }
1140
+ .models-hero-content {
1141
+ position: relative;
1142
+ z-index: 1;
1143
+ }
1144
+ .models-hero-badge {
1145
+ display: inline-block;
1146
+ background: linear-gradient(135deg, #00D4AA, #40E0FF);
1147
+ color: #001E2B;
1148
+ font-size: 11px;
1149
+ font-weight: 700;
1150
+ padding: 3px 10px;
1151
+ border-radius: 12px;
1152
+ text-transform: uppercase;
1153
+ letter-spacing: 0.5px;
1154
+ margin-bottom: 10px;
1155
+ }
1156
+ .models-hero-title {
1157
+ font-size: 18px;
1158
+ font-weight: 700;
1159
+ color: var(--text);
1160
+ margin-bottom: 8px;
1161
+ }
1162
+ .models-hero-desc {
1163
+ font-size: 13px;
1164
+ color: var(--text-dim);
1165
+ line-height: 1.6;
1166
+ max-width: 700px;
1167
+ margin-bottom: 14px;
1168
+ }
1169
+ .models-hero-models {
1170
+ display: flex;
1171
+ gap: 8px;
1172
+ flex-wrap: wrap;
1173
+ }
1174
+ .model-chip {
1175
+ display: inline-flex;
1176
+ align-items: center;
1177
+ gap: 6px;
1178
+ padding: 6px 12px;
1179
+ background: var(--bg-card);
1180
+ border: 1px solid var(--border);
1181
+ border-radius: 8px;
1182
+ font-size: 12px;
1183
+ font-family: var(--mono);
1184
+ color: var(--text);
1185
+ cursor: pointer;
1186
+ transition: all 0.15s;
1187
+ }
1188
+ .model-chip:hover {
1189
+ border-color: var(--accent);
1190
+ background: rgba(0, 212, 170, 0.06);
1191
+ }
1192
+ .model-chip-price {
1193
+ color: var(--text-muted);
1194
+ font-size: 11px;
1195
+ }
1196
+ .models-group {
1197
+ margin-bottom: 24px;
1198
+ }
1199
+ .models-group-header {
1200
+ display: flex;
1201
+ align-items: center;
1202
+ gap: 8px;
1203
+ margin-bottom: 12px;
1204
+ }
1205
+ .models-group-icon {
1206
+ color: var(--accent);
1207
+ opacity: 0.8;
1208
+ }
1209
+ .models-group-title {
1210
+ font-size: 15px;
1211
+ font-weight: 600;
1212
+ color: var(--text);
1213
+ }
1214
+ .models-group-count {
1215
+ font-size: 11px;
1216
+ color: var(--text-muted);
1217
+ background: var(--bg-card);
1218
+ border: 1px solid var(--border);
1219
+ border-radius: 10px;
1220
+ padding: 1px 8px;
1221
+ }
1222
+ .models-group-grid {
1223
+ display: grid;
1224
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1225
+ gap: 14px;
1226
+ }
1227
+ .model-card {
1228
+ background: var(--bg-card);
1229
+ border: 1px solid var(--border);
1230
+ border-radius: var(--radius);
1231
+ padding: 18px;
1232
+ cursor: default;
1233
+ transition: all 0.2s;
1234
+ position: relative;
1235
+ overflow: hidden;
1236
+ }
1237
+ .model-card:hover {
1238
+ border-color: var(--accent);
1239
+ transform: translateY(-2px);
1240
+ box-shadow: 0 4px 20px rgba(0, 212, 170, 0.1);
1241
+ }
1242
+ .model-card.legacy {
1243
+ opacity: 0.5;
1244
+ }
1245
+ .model-card.legacy:hover {
1246
+ opacity: 0.7;
1247
+ }
1248
+ .model-card-nebula {
1249
+ position: absolute;
1250
+ top: -40px;
1251
+ right: -40px;
1252
+ width: 140px;
1253
+ height: 140px;
1254
+ pointer-events: none;
1255
+ z-index: 0;
1256
+ }
1257
+ .model-card-header {
1258
+ display: flex;
1259
+ align-items: center;
1260
+ justify-content: space-between;
1261
+ margin-bottom: 6px;
1262
+ position: relative;
1263
+ z-index: 1;
1264
+ }
1265
+ .model-card-name {
1266
+ font-size: 15px;
1267
+ font-weight: 600;
1268
+ color: var(--text);
1269
+ font-family: var(--mono);
1270
+ }
1271
+ .model-card-type-badge {
1272
+ font-size: 10px;
1273
+ padding: 2px 8px;
1274
+ border-radius: 10px;
1275
+ text-transform: uppercase;
1276
+ letter-spacing: 0.3px;
1277
+ font-weight: 600;
1278
+ }
1279
+ .model-card-type-badge.embedding {
1280
+ background: rgba(0, 212, 170, 0.15);
1281
+ color: #00D4AA;
1282
+ }
1283
+ .model-card-type-badge.reranking {
1284
+ background: rgba(64, 224, 255, 0.15);
1285
+ color: #40E0FF;
1286
+ }
1287
+ .model-card-type-badge.multimodal {
1288
+ background: rgba(255, 170, 64, 0.15);
1289
+ color: #FFAA40;
1290
+ }
1291
+ .model-card-shared-badge {
1292
+ display: inline-flex;
1293
+ align-items: center;
1294
+ gap: 4px;
1295
+ font-size: 10px;
1296
+ color: var(--blue, #40E0FF);
1297
+ margin-bottom: 6px;
1298
+ position: relative;
1299
+ z-index: 1;
1300
+ }
1301
+ .model-card-rteb {
1302
+ position: absolute;
1303
+ top: 14px;
1304
+ right: 14px;
1305
+ font-size: 11px;
1306
+ font-weight: 700;
1307
+ color: var(--accent);
1308
+ font-family: var(--mono);
1309
+ z-index: 1;
1310
+ }
1311
+ .model-card-desc {
1312
+ font-size: 13px;
1313
+ color: var(--text-dim);
1314
+ margin-bottom: 12px;
1315
+ position: relative;
1316
+ z-index: 1;
1317
+ }
1318
+ .model-card-specs {
1319
+ display: grid;
1320
+ grid-template-columns: 1fr 1fr;
1321
+ gap: 6px 16px;
1322
+ font-size: 12px;
1323
+ position: relative;
1324
+ z-index: 1;
1325
+ }
1326
+ .model-card-spec {
1327
+ display: flex;
1328
+ justify-content: space-between;
1329
+ }
1330
+ .model-card-spec-label {
1331
+ color: var(--text-muted);
1332
+ }
1333
+ .model-card-spec-value {
1334
+ color: var(--text);
1335
+ font-family: var(--mono);
1336
+ font-weight: 500;
1337
+ }
1338
+ .model-card-actions {
1339
+ display: flex;
1340
+ gap: 6px;
1341
+ margin-top: 12px;
1342
+ position: relative;
1343
+ z-index: 1;
1344
+ }
1345
+ .model-card-action {
1346
+ font-size: 11px;
1347
+ padding: 4px 10px;
1348
+ border-radius: 6px;
1349
+ border: 1px solid var(--border);
1350
+ background: transparent;
1351
+ color: var(--accent);
1352
+ cursor: pointer;
1353
+ font-family: var(--font);
1354
+ transition: all 0.15s;
1355
+ }
1356
+ .model-card-action:hover {
1357
+ background: rgba(0, 212, 170, 0.1);
1358
+ border-color: var(--accent);
1359
+ }
1360
+ .models-rteb-bar {
1361
+ display: flex;
1362
+ align-items: center;
1363
+ gap: 12px;
1364
+ margin-bottom: 8px;
1365
+ }
1366
+ .models-rteb-label {
1367
+ min-width: 160px;
1368
+ font-size: 13px;
1369
+ white-space: nowrap;
1370
+ }
1371
+ .models-rteb-track {
1372
+ flex: 1;
1373
+ height: 24px;
1374
+ background: var(--bg-surface);
1375
+ border-radius: 4px;
1376
+ overflow: hidden;
1377
+ }
1378
+ .models-rteb-fill {
1379
+ height: 100%;
1380
+ border-radius: 4px;
1381
+ transition: width 0.6s ease-out;
1382
+ }
1383
+ .models-rteb-score {
1384
+ min-width: 50px;
1385
+ text-align: right;
1386
+ font-family: var(--mono);
1387
+ font-weight: 600;
1388
+ font-size: 13px;
1389
+ }
1390
+
1391
+ /* ── Model Picker Overlay ── */
1392
+ .model-picker-overlay {
1393
+ position: fixed;
1394
+ inset: 0;
1395
+ background: rgba(0, 0, 0, 0.6);
1396
+ backdrop-filter: blur(4px);
1397
+ z-index: 1001;
1398
+ display: flex;
1399
+ align-items: center;
1400
+ justify-content: center;
1401
+ opacity: 0;
1402
+ pointer-events: none;
1403
+ transition: opacity 0.2s ease;
1404
+ }
1405
+ .model-picker-overlay.open {
1406
+ opacity: 1;
1407
+ pointer-events: auto;
1408
+ }
1409
+ .model-picker {
1410
+ background: var(--bg-card);
1411
+ border: 1px solid var(--border);
1412
+ border-radius: 12px;
1413
+ width: 480px;
1414
+ max-width: 90vw;
1415
+ max-height: 70vh;
1416
+ display: flex;
1417
+ flex-direction: column;
1418
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
1419
+ }
1420
+ .model-picker-header {
1421
+ display: flex;
1422
+ align-items: center;
1423
+ justify-content: space-between;
1424
+ padding: 16px 20px 12px;
1425
+ border-bottom: 1px solid var(--border);
1426
+ }
1427
+ .model-picker-title {
1428
+ font-size: 16px;
1429
+ font-weight: 600;
1430
+ color: var(--text);
1431
+ }
1432
+ .model-picker-context {
1433
+ font-size: 12px;
1434
+ color: var(--text-muted);
1435
+ margin-top: 2px;
1436
+ }
1437
+ .model-picker-close {
1438
+ background: none;
1439
+ border: none;
1440
+ color: var(--text-dim);
1441
+ font-size: 22px;
1442
+ cursor: pointer;
1443
+ padding: 0 4px;
1444
+ line-height: 1;
1445
+ }
1446
+ .model-picker-close:hover {
1447
+ color: var(--text);
1448
+ }
1449
+ .model-picker-search {
1450
+ padding: 12px 20px;
1451
+ }
1452
+ .model-picker-search input {
1453
+ width: 100%;
1454
+ background: var(--bg-input, var(--bg-surface));
1455
+ border: 1px solid var(--border);
1456
+ color: var(--text);
1457
+ padding: 8px 12px;
1458
+ border-radius: var(--radius);
1459
+ font-size: 13px;
1460
+ font-family: var(--font);
1461
+ }
1462
+ .model-picker-recommendation {
1463
+ padding: 0 20px 8px;
1464
+ font-size: 12px;
1465
+ color: var(--blue, #40E0FF);
1466
+ display: none;
1467
+ }
1468
+ .model-picker-recommendation.visible {
1469
+ display: block;
1470
+ }
1471
+ .model-picker-list {
1472
+ flex: 1;
1473
+ overflow-y: auto;
1474
+ padding: 0 12px 12px;
1475
+ }
1476
+ .model-picker-item {
1477
+ display: flex;
1478
+ align-items: flex-start;
1479
+ gap: 12px;
1480
+ padding: 10px 12px;
1481
+ border-radius: 8px;
1482
+ cursor: pointer;
1483
+ transition: background 0.1s;
1484
+ border: 1px solid transparent;
1485
+ }
1486
+ .model-picker-item:hover,
1487
+ .model-picker-item:focus {
1488
+ background: rgba(0, 212, 170, 0.06);
1489
+ border-color: var(--border);
1490
+ outline: none;
1491
+ }
1492
+ .model-picker-item.selected {
1493
+ background: rgba(0, 212, 170, 0.1);
1494
+ border-color: var(--accent);
1495
+ }
1496
+ .model-picker-item-radio {
1497
+ width: 16px;
1498
+ height: 16px;
1499
+ border: 2px solid var(--border);
1500
+ border-radius: 50%;
1501
+ flex-shrink: 0;
1502
+ margin-top: 2px;
1503
+ transition: all 0.15s;
1504
+ }
1505
+ .model-picker-item.selected .model-picker-item-radio {
1506
+ border-color: var(--accent);
1507
+ background: var(--accent);
1508
+ box-shadow: inset 0 0 0 3px var(--bg-card);
1509
+ }
1510
+ .model-picker-item-info {
1511
+ flex: 1;
1512
+ min-width: 0;
1513
+ }
1514
+ .model-picker-item-name {
1515
+ font-size: 13px;
1516
+ font-weight: 600;
1517
+ color: var(--text);
1518
+ font-family: var(--mono);
1519
+ }
1520
+ .model-picker-item-meta {
1521
+ font-size: 11px;
1522
+ color: var(--text-dim);
1523
+ margin-top: 2px;
1524
+ }
1525
+ .model-picker-item-tags {
1526
+ display: flex;
1527
+ gap: 4px;
1528
+ margin-top: 4px;
1529
+ flex-wrap: wrap;
1530
+ }
1531
+ .model-picker-item-tag {
1532
+ font-size: 10px;
1533
+ padding: 1px 6px;
1534
+ border-radius: 8px;
1535
+ background: rgba(0, 212, 170, 0.08);
1536
+ color: var(--accent);
1537
+ }
1538
+ .model-picker-item-shared {
1539
+ font-size: 10px;
1540
+ padding: 1px 6px;
1541
+ border-radius: 8px;
1542
+ background: rgba(64, 224, 255, 0.1);
1543
+ color: var(--blue, #40E0FF);
1544
+ }
1545
+ .model-picker-footer {
1546
+ padding: 10px 20px;
1547
+ border-top: 1px solid var(--border);
1548
+ text-align: center;
1549
+ font-size: 12px;
1550
+ }
1551
+ .model-picker-footer a {
1552
+ color: var(--accent);
1553
+ text-decoration: none;
1554
+ }
1555
+ .model-picker-footer a:hover {
1556
+ text-decoration: underline;
1557
+ }
1558
+ .model-picker-trigger {
1559
+ background: transparent;
1560
+ border: 1px solid var(--border);
1561
+ color: var(--text-muted);
1562
+ cursor: pointer;
1563
+ padding: 4px 6px;
1564
+ border-radius: 4px;
1565
+ transition: color 0.15s, background 0.15s, border-color 0.15s;
1566
+ display: inline-flex;
1567
+ align-items: center;
1568
+ vertical-align: middle;
1569
+ margin-left: 4px;
1570
+ }
1571
+ .model-picker-trigger:hover {
1572
+ color: var(--accent);
1573
+ background: rgba(0, 212, 170, 0.08);
1574
+ border-color: var(--accent);
1575
+ }
1576
+ [data-theme="light"] .model-card:hover {
1577
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
1578
+ }
1579
+
1092
1580
  /* Explore modal */
1093
1581
  .explore-modal-overlay {
1094
1582
  position: fixed;
@@ -4200,6 +4688,54 @@ select:focus { outline: none; border-color: var(--accent); }
4200
4688
  border-radius: 6px; padding: 8px; overflow-x: auto;
4201
4689
  white-space: pre-wrap; color: var(--text); margin-top: 4px;
4202
4690
  }
4691
+ /* Output format selector and rich rendering */
4692
+ .wf-output-format-select {
4693
+ font-size: 11px; padding: 2px 6px; border-radius: 4px;
4694
+ background: var(--bg-card); border: 1px solid var(--border);
4695
+ color: var(--text); cursor: pointer; margin-left: 8px;
4696
+ }
4697
+ .wf-output-format-select:hover { border-color: var(--accent); }
4698
+ .wf-output-table {
4699
+ width: 100%; border-collapse: collapse; font-size: 11px;
4700
+ color: var(--text); margin-top: 8px;
4701
+ }
4702
+ .wf-output-table th {
4703
+ text-align: left; padding: 6px 8px; font-weight: 600;
4704
+ border-bottom: 1px solid var(--accent); color: var(--accent);
4705
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;
4706
+ }
4707
+ .wf-output-table td {
4708
+ padding: 5px 8px; border-bottom: 1px solid var(--border);
4709
+ font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px;
4710
+ }
4711
+ .wf-output-table tr:hover td { background: rgba(0,212,170,0.05); }
4712
+ .wf-output-markdown {
4713
+ font-size: 12px; line-height: 1.5; color: var(--text); padding: 8px;
4714
+ }
4715
+ .wf-output-markdown h2, .wf-output-markdown h3 {
4716
+ color: var(--accent); margin-top: 12px; margin-bottom: 4px;
4717
+ }
4718
+ .wf-output-markdown table {
4719
+ border-collapse: collapse; width: 100%; margin: 8px 0;
4720
+ }
4721
+ .wf-output-markdown th, .wf-output-markdown td {
4722
+ border: 1px solid var(--border); padding: 4px 8px; font-size: 11px;
4723
+ }
4724
+ .wf-output-text-field { margin-bottom: 12px; }
4725
+ .wf-output-text-label {
4726
+ font-size: 10px; font-weight: 600; text-transform: uppercase;
4727
+ color: var(--accent); letter-spacing: 0.5px; margin-bottom: 2px;
4728
+ }
4729
+ .wf-output-text-value {
4730
+ font-size: 12px; color: var(--text); line-height: 1.5; white-space: pre-wrap;
4731
+ }
4732
+ .wf-output-metric {
4733
+ display: inline-block; margin-right: 16px; margin-bottom: 6px;
4734
+ }
4735
+ .wf-output-metric-val {
4736
+ font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px;
4737
+ color: #40E0FF; font-weight: 600;
4738
+ }
4203
4739
  .wf-tool-badge {
4204
4740
  display: inline-flex; align-items: center; gap: 4px;
4205
4741
  padding: 2px 8px; border-radius: 10px; font-size: 11px;
@@ -5754,6 +6290,7 @@ select:focus { outline: none; border-color: var(--accent); }
5754
6290
  <div class="sidebar-nav-divider"></div>
5755
6291
  <div class="sidebar-nav-group" role="tablist" aria-label="Learn">
5756
6292
  <div class="sidebar-nav-label" id="nav-learn-label">Learn</div>
6293
+ <button class="tab-btn" data-tab="models" data-tooltip="Models" role="tab" aria-selected="false" aria-controls="tab-models" id="tab-btn-models"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-bot"/></svg></span><span>Models</span></button>
5757
6294
  <button class="tab-btn" data-tab="benchmark" data-tooltip="Benchmark" role="tab" aria-selected="false" aria-controls="tab-benchmark" id="tab-btn-benchmark"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-gauge"/></svg></span><span>Benchmark</span></button>
5758
6295
  <button class="tab-btn" data-tab="explore" data-tooltip="Explore" role="tab" aria-selected="false" aria-controls="tab-explore" id="tab-btn-explore"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-bulb"/></svg></span><span>Explore</span></button>
5759
6296
  <button class="tab-btn" data-tab="about" data-tooltip="About" role="tab" aria-selected="false" aria-controls="tab-about" id="tab-btn-about"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-info"/></svg></span><span>About</span></button>
@@ -6474,9 +7011,9 @@ Reranking models rescore initial search results to improve relevance ordering.</
6474
7011
  <div style="margin-top:16px;padding:12px;background:var(--accent-glow);border-radius:8px;border-left:3px solid var(--accent);">
6475
7012
  <strong style="color:var(--accent);">💡 Pro Tip: Asymmetric Retrieval</strong>
6476
7013
  <p style="margin:8px 0 0 0;font-size:13px;color:var(--text);">
6477
- Embed your document corpus with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-3-lite</code> ($0.02/M) and
6478
- query with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-3</code> ($0.06/M).
6479
- Because all Voyage-3 models share the same embedding space, you get top-tier retrieval quality at a fraction of the cost.
7014
+ Embed your document corpus with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-4-lite</code> ($0.02/M) and
7015
+ query with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-4</code> ($0.06/M).
7016
+ Because all Voyage 4 models share the same embedding space, you get top-tier retrieval quality at a fraction of the cost.
6480
7017
  </p>
6481
7018
  </div>
6482
7019
  </div>
@@ -6489,8 +7026,8 @@ Reranking models rescore initial search results to improve relevance ordering.</
6489
7026
  <div style="font-size:20px;margin-bottom:8px;">🔗</div>
6490
7027
  <div style="font-weight:600;margin-bottom:4px;">Shared Embedding Space</div>
6491
7028
  <div style="font-size:13px;color:var(--text-dim);">
6492
- All Voyage-3 models produce compatible embeddings. Mix <code>voyage-3-large</code> for queries
6493
- with <code>voyage-3-lite</code> for documents they work together seamlessly.
7029
+ All Voyage 4 models produce compatible embeddings. Mix <code>voyage-4-large</code> for queries
7030
+ with <code>voyage-4-lite</code> for documents, and they work together seamlessly.
6494
7031
  </div>
6495
7032
  </div>
6496
7033
  <div style="padding:16px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
@@ -7112,7 +7649,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
7112
7649
  <option value="voyage-4-large">voyage-4-large</option>
7113
7650
  <option value="voyage-4">voyage-4</option>
7114
7651
  <option value="voyage-4-lite">voyage-4-lite</option>
7115
- <option value="voyage-3-large">voyage-3-large</option>
7652
+ <option value="voyage-code-3">voyage-code-3</option>
7116
7653
  </select>
7117
7654
  </label>
7118
7655
  <label>
@@ -7233,6 +7770,54 @@ Reranking models rescore initial search results to improve relevance ordering.</
7233
7770
  </div>
7234
7771
  </div>
7235
7772
 
7773
+ <!-- ========== MODELS TAB ========== -->
7774
+ <div class="tab-panel" id="tab-models" role="tabpanel" aria-labelledby="tab-btn-models" tabindex="0">
7775
+ <div class="page-header">
7776
+ <h2 class="page-header-title">Models</h2>
7777
+ <p class="page-header-subtitle">Voyage AI model showcase</p>
7778
+ <p class="page-header-hint">Explore all Voyage AI models, compare specs, and find the right model for your use case.</p>
7779
+ <a class="page-header-docs" href="https://docs.voyageai.com/docs/embeddings" target="_blank" rel="noopener" title="Voyage AI model docs"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
7780
+ </div>
7781
+
7782
+ <!-- Search + Filter -->
7783
+ <div class="models-toolbar">
7784
+ <input type="text" id="modelsSearch" placeholder="Search models..." oninput="filterModels()" style="max-width:280px;" aria-label="Search models">
7785
+ <div class="models-filter-pills" id="modelsFilterPills">
7786
+ <button class="models-filter-pill active" data-filter="all" onclick="setModelFilter('all')">All</button>
7787
+ <button class="models-filter-pill" data-filter="embedding" onclick="setModelFilter('embedding')">Embedding</button>
7788
+ <button class="models-filter-pill" data-filter="reranking" onclick="setModelFilter('reranking')">Reranking</button>
7789
+ <button class="models-filter-pill" data-filter="multimodal" onclick="setModelFilter('multimodal')">Multimodal</button>
7790
+ </div>
7791
+ </div>
7792
+
7793
+ <!-- Shared Embedding Space Hero -->
7794
+ <div class="models-hero card" id="modelsHero">
7795
+ <div class="models-hero-shape" id="modelsHeroShape"></div>
7796
+ <div class="models-hero-content">
7797
+ <div class="models-hero-badge">Voyage 4 Family</div>
7798
+ <div class="models-hero-title">Shared Embedding Space</div>
7799
+ <div class="models-hero-desc">
7800
+ voyage-4-large, voyage-4, and voyage-4-lite produce compatible embeddings in the same vector space.
7801
+ Index documents with the large model for best quality, then query with the lite model at a fraction of the cost.
7802
+ No re-indexing required.
7803
+ </div>
7804
+ <div class="models-hero-models" id="modelsHeroModels"></div>
7805
+ </div>
7806
+ </div>
7807
+
7808
+ <!-- Model Groups (dynamically rendered) -->
7809
+ <div id="modelsGroups"></div>
7810
+
7811
+ <!-- RTEB Benchmark Chart -->
7812
+ <div class="card" id="modelsRtebCard" style="margin-top:24px;">
7813
+ <div class="card-title">RTEB Retrieval Benchmark (NDCG@10)</div>
7814
+ <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
7815
+ Industry-standard benchmark for retrieval quality across 29 datasets. Higher is better.
7816
+ </p>
7817
+ <div id="modelsRtebChart"></div>
7818
+ </div>
7819
+ </div>
7820
+
7236
7821
  <!-- ========== EXPLORE TAB ========== -->
7237
7822
  <div class="tab-panel" id="tab-explore" role="tabpanel" aria-labelledby="tab-btn-explore" tabindex="0">
7238
7823
  <div class="page-header">
@@ -7266,6 +7851,27 @@ Reranking models rescore initial search results to improve relevance ordering.</
7266
7851
  <!-- Hidden global model select (used by JS for model sync) -->
7267
7852
  <select id="globalModel" style="display:none;"></select>
7268
7853
 
7854
+ <!-- ========== MODEL PICKER OVERLAY ========== -->
7855
+ <div class="model-picker-overlay" id="modelPickerOverlay" role="dialog" aria-modal="true" aria-labelledby="modelPickerTitle">
7856
+ <div class="model-picker">
7857
+ <div class="model-picker-header">
7858
+ <div>
7859
+ <div class="model-picker-title" id="modelPickerTitle">Choose a Model</div>
7860
+ <div class="model-picker-context" id="modelPickerContext"></div>
7861
+ </div>
7862
+ <button class="model-picker-close" id="modelPickerClose" aria-label="Close">&times;</button>
7863
+ </div>
7864
+ <div class="model-picker-search">
7865
+ <input type="text" id="modelPickerSearch" placeholder="Search models..." oninput="filterModelPicker()" aria-label="Search models">
7866
+ </div>
7867
+ <div class="model-picker-recommendation" id="modelPickerRecommendation"></div>
7868
+ <div class="model-picker-list" id="modelPickerList" role="listbox"></div>
7869
+ <div class="model-picker-footer">
7870
+ <a href="#" onclick="event.preventDefault(); closeModelPicker(); switchTab('models');">View all models in catalog</a>
7871
+ </div>
7872
+ </div>
7873
+ </div>
7874
+
7269
7875
  <!-- ========== SETTINGS TAB ========== -->
7270
7876
  <div class="tab-panel" id="tab-settings">
7271
7877
  <div class="settings-layout">
@@ -8063,6 +8669,7 @@ async function init() {
8063
8669
  await loadConfig();
8064
8670
  await Promise.all([loadModels(), loadConcepts()]);
8065
8671
  populateModelSelects();
8672
+ enhanceModelSelects();
8066
8673
  buildExploreCards();
8067
8674
 
8068
8675
  // Apply default tab setting
@@ -8146,6 +8753,9 @@ function switchTab(tab) {
8146
8753
  if (tab === 'home') {
8147
8754
  homeInit();
8148
8755
  }
8756
+ if (tab === 'models') {
8757
+ modelsInit();
8758
+ }
8149
8759
  }
8150
8760
  // Expose globally so Electron main process can call it
8151
8761
  window.switchTab = switchTab;
@@ -9541,6 +10151,393 @@ window.filterExplore = function() {
9541
10151
  });
9542
10152
  };
9543
10153
 
10154
+ // ── Models Tab ──
10155
+ let modelsTabData = {
10156
+ catalog: null,
10157
+ benchmarks: null,
10158
+ currentFilter: 'all',
10159
+ initialized: false
10160
+ };
10161
+
10162
+ async function loadModelsCatalog() {
10163
+ if (modelsTabData.catalog) return;
10164
+ try {
10165
+ const res = await fetch('/api/models/catalog');
10166
+ const data = await res.json();
10167
+ modelsTabData.catalog = data.models || [];
10168
+ modelsTabData.benchmarks = data.benchmarks || [];
10169
+ } catch {
10170
+ modelsTabData.catalog = allModels;
10171
+ modelsTabData.benchmarks = [];
10172
+ }
10173
+ }
10174
+
10175
+ function modelsInit() {
10176
+ if (modelsTabData.initialized) return;
10177
+ modelsTabData.initialized = true;
10178
+ loadModelsCatalog().then(() => {
10179
+ renderModelsHero();
10180
+ renderModelsGroups();
10181
+ renderModelsRtebChart();
10182
+ });
10183
+ }
10184
+
10185
+ function renderModelsHero() {
10186
+ const heroModels = document.getElementById('modelsHeroModels');
10187
+ const sharedModels = (modelsTabData.catalog || []).filter(
10188
+ m => m.sharedSpace === 'voyage-4' && !m.legacy && !m.unreleased
10189
+ );
10190
+ heroModels.innerHTML = sharedModels.map(m =>
10191
+ '<div class="model-chip" onclick="scrollToModelCard(\'' + m.name + '\')">' +
10192
+ '<span>' + escapeHtml(m.name) + '</span>' +
10193
+ '<span class="model-chip-price">' + escapeHtml(m.price) + '</span>' +
10194
+ '</div>'
10195
+ ).join('');
10196
+
10197
+ const shapeEl = document.getElementById('modelsHeroShape');
10198
+ if (shapeEl && !shapeEl.innerHTML) {
10199
+ shapeEl.innerHTML = generateNebulaSVG(200, 200, 900, {variant: 'rich', opacity: 0.08});
10200
+ }
10201
+ }
10202
+
10203
+ function getModelGroups(models) {
10204
+ const groups = [
10205
+ {
10206
+ id: 'voyage-4',
10207
+ title: 'Voyage 4 Flagship',
10208
+ icon: LI.zap,
10209
+ models: models.filter(m => m.family === 'voyage-4' && !m.legacy)
10210
+ },
10211
+ {
10212
+ id: 'domain',
10213
+ title: 'Domain-Specific',
10214
+ icon: LI.target,
10215
+ models: models.filter(m => !m.family && m.type === 'embedding' && !m.legacy && !m.multimodal)
10216
+ },
10217
+ {
10218
+ id: 'multimodal',
10219
+ title: 'Multimodal',
10220
+ icon: LI.image,
10221
+ models: models.filter(m => m.multimodal && !m.legacy)
10222
+ },
10223
+ {
10224
+ id: 'reranking',
10225
+ title: 'Reranking',
10226
+ icon: LI.shuffle,
10227
+ models: models.filter(m => m.type === 'reranking' && !m.legacy)
10228
+ },
10229
+ {
10230
+ id: 'legacy',
10231
+ title: 'Previous Generation',
10232
+ icon: LI.timer,
10233
+ models: models.filter(m => m.legacy === true)
10234
+ }
10235
+ ];
10236
+ return groups.filter(g => g.models.length > 0);
10237
+ }
10238
+
10239
+ function renderModelsGroups() {
10240
+ const container = document.getElementById('modelsGroups');
10241
+ const catalog = modelsTabData.catalog || [];
10242
+ const groups = getModelGroups(catalog);
10243
+
10244
+ container.innerHTML = groups.map(function(group, gi) {
10245
+ return '<div class="models-group" id="models-group-' + group.id + '">' +
10246
+ '<div class="models-group-header">' +
10247
+ '<span class="models-group-icon">' + lucideIcon(group.icon, 18) + '</span>' +
10248
+ '<span class="models-group-title">' + escapeHtml(group.title) + '</span>' +
10249
+ '<span class="models-group-count">' + group.models.length + '</span>' +
10250
+ '</div>' +
10251
+ '<div class="models-group-grid">' +
10252
+ group.models.map(function(m, mi) { return renderModelCard(m, gi * 100 + mi); }).join('') +
10253
+ '</div>' +
10254
+ '</div>';
10255
+ }).join('');
10256
+ }
10257
+
10258
+ function renderModelCard(model, seedOffset) {
10259
+ var typeClass = model.multimodal ? 'multimodal' :
10260
+ model.type === 'reranking' ? 'reranking' : 'embedding';
10261
+ var typeLabel = model.multimodal ? 'Multimodal' :
10262
+ model.type === 'reranking' ? 'Reranker' : 'Embedding';
10263
+
10264
+ var specs = [];
10265
+ if (model.context) specs.push({label: 'Context', value: model.context});
10266
+ if (model.dimensions && model.dimensions !== '\u2014') {
10267
+ var dimDefault = model.dimensions.split('(')[0].trim().split(',')[0];
10268
+ specs.push({label: 'Dims', value: dimDefault});
10269
+ }
10270
+ specs.push({label: 'Price', value: model.price});
10271
+ if (model.architecture) specs.push({label: 'Arch', value: model.architecture === 'moe' ? 'MoE' : 'Dense'});
10272
+
10273
+ var sharedBadge = model.sharedSpace ?
10274
+ '<div class="model-card-shared-badge">' + lucideIcon(LI.link, 12) + ' Shared: ' + model.sharedSpace + '</div>' : '';
10275
+
10276
+ var rtebBadge = model.rtebScore ?
10277
+ '<div class="model-card-rteb">' + model.rtebScore.toFixed(1) + '</div>' : '';
10278
+
10279
+ var tryActions = '';
10280
+ if (model.multimodal) {
10281
+ tryActions = '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'multimodal\')">Try in Multimodal</button>';
10282
+ } else if (model.type === 'reranking') {
10283
+ tryActions = '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'search\')">Try in Search</button>';
10284
+ } else if (!model.legacy) {
10285
+ tryActions = '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'embed\')">Try in Embed</button>' +
10286
+ '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'search\')">Try in Search</button>';
10287
+ }
10288
+
10289
+ return '<div class="model-card ' + (model.legacy ? 'legacy' : '') + '" data-model="' + model.name + '" data-type="' + typeClass + '" id="model-card-' + model.name + '">' +
10290
+ '<div class="model-card-nebula">' + generateNebulaSVG(140, 140, 500 + seedOffset, {variant: 'subtle', opacity: 0.05, sparkles: false}) + '</div>' +
10291
+ rtebBadge +
10292
+ '<div class="model-card-header">' +
10293
+ '<span class="model-card-name">' + escapeHtml(model.name) + '</span>' +
10294
+ '<span class="model-card-type-badge ' + typeClass + '">' + typeLabel + '</span>' +
10295
+ '</div>' +
10296
+ sharedBadge +
10297
+ '<div class="model-card-desc">' + escapeHtml(model.bestFor) + '</div>' +
10298
+ '<div class="model-card-specs">' +
10299
+ specs.map(function(s) {
10300
+ return '<div class="model-card-spec">' +
10301
+ '<span class="model-card-spec-label">' + s.label + '</span>' +
10302
+ '<span class="model-card-spec-value">' + escapeHtml(s.value) + '</span>' +
10303
+ '</div>';
10304
+ }).join('') +
10305
+ '</div>' +
10306
+ (tryActions ? '<div class="model-card-actions">' + tryActions + '</div>' : '') +
10307
+ '</div>';
10308
+ }
10309
+
10310
+ function renderModelsRtebChart() {
10311
+ var chart = document.getElementById('modelsRtebChart');
10312
+ var benchmarks = modelsTabData.benchmarks || [];
10313
+ if (!benchmarks.length) {
10314
+ chart.innerHTML = '<p style="color:var(--text-muted);font-size:13px;">Benchmark data unavailable.</p>';
10315
+ return;
10316
+ }
10317
+ var maxScore = Math.max.apply(null, benchmarks.map(function(b) { return b.score; }));
10318
+ var minDisplay = 55;
10319
+
10320
+ chart.innerHTML = benchmarks.map(function(b) {
10321
+ var isVoyage = b.provider === 'Voyage AI';
10322
+ var barPct = ((b.score - minDisplay) / (maxScore - minDisplay)) * 100;
10323
+ var color = isVoyage ? 'var(--accent)' : '#555';
10324
+ var labelColor = isVoyage ? 'var(--accent)' : 'var(--text-dim)';
10325
+ var fontWeight = isVoyage ? '600' : '400';
10326
+
10327
+ return '<div class="models-rteb-bar">' +
10328
+ '<span class="models-rteb-label" style="color:' + labelColor + ';font-weight:' + fontWeight + ';">' + escapeHtml(b.model) + '</span>' +
10329
+ '<div class="models-rteb-track">' +
10330
+ '<div class="models-rteb-fill" style="width:' + barPct.toFixed(1) + '%;background:' + color + ';' + (isVoyage ? '' : 'opacity:0.6;') + '"></div>' +
10331
+ '</div>' +
10332
+ '<span class="models-rteb-score" style="color:' + labelColor + ';">' + b.score.toFixed(2) + '</span>' +
10333
+ '</div>';
10334
+ }).join('');
10335
+ }
10336
+
10337
+ function scrollToModelCard(name) {
10338
+ var card = document.getElementById('model-card-' + name);
10339
+ if (card) {
10340
+ card.scrollIntoView({ behavior: 'smooth', block: 'center' });
10341
+ card.style.outline = '2px solid var(--accent)';
10342
+ setTimeout(function() { card.style.outline = ''; }, 2000);
10343
+ }
10344
+ }
10345
+
10346
+ window.filterModels = function() {
10347
+ var q = document.getElementById('modelsSearch').value.toLowerCase().trim();
10348
+ document.querySelectorAll('.model-card').forEach(function(card) {
10349
+ if (!q) { card.style.display = ''; return; }
10350
+ var text = card.textContent.toLowerCase();
10351
+ card.style.display = text.includes(q) ? '' : 'none';
10352
+ });
10353
+ document.querySelectorAll('.models-group').forEach(function(group) {
10354
+ var visible = group.querySelectorAll('.model-card:not([style*="display: none"])');
10355
+ group.style.display = visible.length ? '' : 'none';
10356
+ });
10357
+ };
10358
+
10359
+ window.setModelFilter = function(filter) {
10360
+ modelsTabData.currentFilter = filter;
10361
+ document.querySelectorAll('.models-filter-pill').forEach(function(p) {
10362
+ p.classList.toggle('active', p.dataset.filter === filter);
10363
+ });
10364
+ document.querySelectorAll('.model-card').forEach(function(card) {
10365
+ if (filter === 'all') { card.style.display = ''; return; }
10366
+ var cardType = card.dataset.type;
10367
+ card.style.display = cardType === filter ? '' : 'none';
10368
+ });
10369
+ document.querySelectorAll('.models-group').forEach(function(group) {
10370
+ var visible = group.querySelectorAll('.model-card:not([style*="display: none"])');
10371
+ group.style.display = visible.length ? '' : 'none';
10372
+ });
10373
+ };
10374
+
10375
+ // ── Model Picker ──
10376
+ var modelPickerState = {
10377
+ isOpen: false,
10378
+ targetSelectId: null,
10379
+ context: null,
10380
+ selectedModel: null,
10381
+ previousFocus: null,
10382
+ modelType: 'embedding'
10383
+ };
10384
+
10385
+ function openModelPicker(selectId, context) {
10386
+ modelPickerState.isOpen = true;
10387
+ modelPickerState.targetSelectId = selectId;
10388
+ modelPickerState.context = context;
10389
+ modelPickerState.previousFocus = document.activeElement;
10390
+
10391
+ var sel = document.getElementById(selectId);
10392
+ modelPickerState.selectedModel = sel ? sel.value : null;
10393
+ modelPickerState.modelType = selectId === 'searchRerankModel' ? 'reranking' : 'embedding';
10394
+
10395
+ var contextHints = {
10396
+ embed: 'Choose an embedding model for vectorizing text.',
10397
+ compare: 'Choose a model for similarity scoring.',
10398
+ search: 'Choose an embedding model for document search.',
10399
+ multimodal: 'Only multimodal models are available here.'
10400
+ };
10401
+ document.getElementById('modelPickerContext').textContent = contextHints[context] || '';
10402
+
10403
+ var rec = document.getElementById('modelPickerRecommendation');
10404
+ var recommendations = {
10405
+ embed: 'Tip: voyage-4 offers balanced quality and cost.',
10406
+ compare: 'Tip: voyage-4-large gives highest accuracy comparisons.',
10407
+ search: 'Tip: Use voyage-4-large for docs, voyage-4-lite for queries (shared space saves cost).',
10408
+ multimodal: 'Tip: voyage-multimodal-3.5 supports text, images, and video.'
10409
+ };
10410
+ if (recommendations[context]) {
10411
+ rec.textContent = recommendations[context];
10412
+ rec.classList.add('visible');
10413
+ } else {
10414
+ rec.classList.remove('visible');
10415
+ }
10416
+
10417
+ renderModelPickerList();
10418
+ document.getElementById('modelPickerOverlay').classList.add('open');
10419
+ setTimeout(function() {
10420
+ document.getElementById('modelPickerSearch').focus();
10421
+ }, 100);
10422
+ }
10423
+
10424
+ function closeModelPicker() {
10425
+ modelPickerState.isOpen = false;
10426
+ document.getElementById('modelPickerOverlay').classList.remove('open');
10427
+ document.getElementById('modelPickerSearch').value = '';
10428
+ if (modelPickerState.previousFocus) {
10429
+ modelPickerState.previousFocus.focus();
10430
+ modelPickerState.previousFocus = null;
10431
+ }
10432
+ }
10433
+
10434
+ function selectModelFromPicker(name) {
10435
+ var sel = document.getElementById(modelPickerState.targetSelectId);
10436
+ if (sel) {
10437
+ sel.value = name;
10438
+ sel.dispatchEvent(new Event('change'));
10439
+ localStorage.setItem('vai-playground-model', name);
10440
+ if (modelPickerState.modelType === 'embedding') {
10441
+ ['embedModel', 'compareModel', 'searchEmbedModel'].forEach(function(id) {
10442
+ var otherSel = document.getElementById(id);
10443
+ if (otherSel && otherSel !== sel) otherSel.value = name;
10444
+ });
10445
+ }
10446
+ }
10447
+ closeModelPicker();
10448
+ sendTelemetry('model_picker_select', { model: name, context: modelPickerState.context });
10449
+ }
10450
+
10451
+ function renderModelPickerList() {
10452
+ var list = document.getElementById('modelPickerList');
10453
+ var type = modelPickerState.modelType;
10454
+ var context = modelPickerState.context;
10455
+ var selected = modelPickerState.selectedModel;
10456
+
10457
+ var models;
10458
+ if (context === 'multimodal') {
10459
+ models = allModels.filter(function(m) { return m.multimodal; });
10460
+ } else if (type === 'reranking') {
10461
+ models = rerankModels;
10462
+ } else {
10463
+ models = embedModels;
10464
+ }
10465
+
10466
+ list.innerHTML = models.map(function(m) {
10467
+ var isSelected = m.name === selected;
10468
+ var tags = [];
10469
+ if (m.architecture === 'moe') tags.push({text: 'MoE', cls: 'model-picker-item-tag'});
10470
+ if (m.sharedSpace) tags.push({text: 'Shared Space', cls: 'model-picker-item-shared'});
10471
+ if (m.rtebScore) tags.push({text: 'RTEB ' + m.rtebScore, cls: 'model-picker-item-tag'});
10472
+
10473
+ var meta = [m.bestFor, m.context, m.price].filter(Boolean).join(' \u00b7 ');
10474
+
10475
+ return '<div class="model-picker-item ' + (isSelected ? 'selected' : '') + '" ' +
10476
+ 'role="option" aria-selected="' + isSelected + '" tabindex="0" ' +
10477
+ 'data-model="' + m.name + '" ' +
10478
+ 'onclick="selectModelFromPicker(\'' + m.name + '\')" ' +
10479
+ 'onkeydown="if(event.key===\'Enter\') selectModelFromPicker(\'' + m.name + '\')">' +
10480
+ '<div class="model-picker-item-radio"></div>' +
10481
+ '<div class="model-picker-item-info">' +
10482
+ '<div class="model-picker-item-name">' + escapeHtml(m.name) + '</div>' +
10483
+ '<div class="model-picker-item-meta">' + escapeHtml(meta) + '</div>' +
10484
+ (tags.length ? '<div class="model-picker-item-tags">' +
10485
+ tags.map(function(t) { return '<span class="' + t.cls + '">' + t.text + '</span>'; }).join('') +
10486
+ '</div>' : '') +
10487
+ '</div>' +
10488
+ '</div>';
10489
+ }).join('');
10490
+ }
10491
+
10492
+ window.filterModelPicker = function() {
10493
+ var q = document.getElementById('modelPickerSearch').value.toLowerCase().trim();
10494
+ document.querySelectorAll('#modelPickerList .model-picker-item').forEach(function(item) {
10495
+ if (!q) { item.style.display = ''; return; }
10496
+ var text = item.textContent.toLowerCase();
10497
+ item.style.display = text.includes(q) ? '' : 'none';
10498
+ });
10499
+ };
10500
+
10501
+ function enhanceModelSelects() {
10502
+ var selectConfigs = [
10503
+ { id: 'embedModel', context: 'embed' },
10504
+ { id: 'compareModel', context: 'compare' },
10505
+ { id: 'searchEmbedModel', context: 'search' },
10506
+ { id: 'searchRerankModel', context: 'search' }
10507
+ ];
10508
+
10509
+ selectConfigs.forEach(function(cfg) {
10510
+ var sel = document.getElementById(cfg.id);
10511
+ if (!sel || sel.dataset.pickerEnhanced) return;
10512
+ sel.dataset.pickerEnhanced = 'true';
10513
+
10514
+ var pickerBtn = document.createElement('button');
10515
+ pickerBtn.className = 'model-picker-trigger';
10516
+ pickerBtn.type = 'button';
10517
+ pickerBtn.title = 'Browse models';
10518
+ pickerBtn.innerHTML = '<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="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>';
10519
+ pickerBtn.addEventListener('click', function(e) {
10520
+ e.preventDefault();
10521
+ e.stopPropagation();
10522
+ openModelPicker(cfg.id, cfg.context);
10523
+ });
10524
+
10525
+ sel.parentNode.insertBefore(pickerBtn, sel.nextSibling);
10526
+ });
10527
+ }
10528
+
10529
+ // Model picker close handlers
10530
+ document.getElementById('modelPickerClose').addEventListener('click', closeModelPicker);
10531
+ document.getElementById('modelPickerOverlay').addEventListener('click', function(e) {
10532
+ if (e.target === document.getElementById('modelPickerOverlay')) closeModelPicker();
10533
+ });
10534
+ document.addEventListener('keydown', function(e) {
10535
+ if (e.key === 'Escape' && modelPickerState.isOpen) {
10536
+ e.preventDefault();
10537
+ closeModelPicker();
10538
+ }
10539
+ });
10540
+
9544
10541
  // ── Benchmark: Sub-panel switching ──
9545
10542
  document.querySelectorAll('.bench-panel-btn').forEach(btn => {
9546
10543
  btn.addEventListener('click', () => {
@@ -13345,6 +14342,12 @@ const WF_NODE_META = {
13345
14342
  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' },
13346
14343
  aggregate: { icon: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z', label: 'Aggregate', color: '#00D4AA', category: 'processing' },
13347
14344
  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' },
14345
+ // Code search tools (voyage-code-3)
14346
+ code_index: { icon: 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2zM12 11v6M9 14h6', label: 'Code Index', color: '#06B6D4', category: 'code' },
14347
+ code_search: { icon: 'M10 20l4-16M18 8l4 4-4 4M6 16l-4-4 4-4', label: 'Code Search', color: '#06B6D4', category: 'code' },
14348
+ code_query: { icon: 'M10 20l4-16M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', label: 'Code Query', color: '#06B6D4', category: 'code' },
14349
+ code_find_similar: { icon: 'M16 3h5v5M8 3H3v5M3 16v5h5M21 16v5h-5M12 8v8M8 12h8', label: 'Find Similar', color: '#06B6D4', category: 'code' },
14350
+ code_status: { icon: 'M22 12h-4l-3 9L9 3l-3 9H2', label: 'Code Status', color: '#06B6D4', category: 'code' },
13348
14351
  };
13349
14352
 
13350
14353
  // Fallback icon (gear) for unknown workflow node types
@@ -13378,6 +14381,7 @@ let wfState = {
13378
14381
  draggingEdge: null,
13379
14382
  dragNode: null,
13380
14383
  dirtyFlag: false,
14384
+ outputFormat: 'auto',
13381
14385
  };
13382
14386
 
13383
14387
  // ── Library ──
@@ -13559,6 +14563,16 @@ function wfCloseInstallDialog() {
13559
14563
  if (modal) modal.style.display = 'none';
13560
14564
  }
13561
14565
 
14566
+ function wfCompareVersions(a, b) {
14567
+ const pa = (a || '0.0.0').split('.').map(Number);
14568
+ const pb = (b || '0.0.0').split('.').map(Number);
14569
+ for (let i = 0; i < 3; i++) {
14570
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
14571
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
14572
+ }
14573
+ return 0;
14574
+ }
14575
+
13562
14576
  async function wfSearchNpm() {
13563
14577
  const query = document.getElementById('wfInstallSearch').value.trim();
13564
14578
  const results = document.getElementById('wfInstallResults');
@@ -13574,15 +14588,27 @@ async function wfSearchNpm() {
13574
14588
  results.innerHTML = data.results.map(r => {
13575
14589
  const isOfficial = r.name.startsWith('@vaicli/');
13576
14590
  const badge = isOfficial ? '<span style="background:var(--accent);color:#fff;font-size:9px;padding:1px 5px;border-radius:3px;margin-left:6px;">OFFICIAL</span>' : '';
13577
- const installed = [...(wfState.official||[]), ...(wfState.community||[])].some(w => w.name === r.name);
13578
- const btn = installed
13579
- ? '<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>'
13580
- : `<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>`;
14591
+ const installedPkg = [...(wfState.official||[]), ...(wfState.community||[])].find(w => w.name === r.name);
14592
+ const installed = !!installedPkg;
14593
+ const installedVersion = installedPkg?.version;
14594
+ const npmVersion = r.version;
14595
+ const hasUpdate = installed && npmVersion && installedVersion && npmVersion !== installedVersion && wfCompareVersions(npmVersion, installedVersion) > 0;
14596
+ let btn;
14597
+ if (hasUpdate) {
14598
+ btn = `<button onclick="wfInstallPkg('${r.name.replace(/'/g,"\\'")}')" style="padding:4px 10px;background:#F59E0B;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;" title="Installed: v${installedVersion}">Update</button>`;
14599
+ } else if (installed) {
14600
+ btn = '<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>';
14601
+ } else {
14602
+ btn = `<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>`;
14603
+ }
14604
+ const versionInfo = hasUpdate
14605
+ ? `v${installedVersion} → <span style="color:#F59E0B;font-weight:500;">v${npmVersion}</span>`
14606
+ : `v${npmVersion || '?'}`;
13581
14607
  return `<div style="padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;justify-content:space-between;">
13582
14608
  <div style="flex:1;min-width:0;">
13583
14609
  <div style="font-size:13px;font-weight:500;">${r.name}${badge}</div>
13584
14610
  <div style="font-size:11px;color:var(--text-muted);margin-top:2px;">${r.description || ''}</div>
13585
- <div style="font-size:10px;color:var(--text-muted);margin-top:2px;">v${r.version || '?'}</div>
14611
+ <div style="font-size:10px;color:var(--text-muted);margin-top:2px;">${versionInfo}</div>
13586
14612
  </div>
13587
14613
  <div style="margin-left:12px;flex-shrink:0;">${btn}</div>
13588
14614
  </div>`;
@@ -13598,7 +14624,7 @@ async function wfInstallPkg(name) {
13598
14624
  const btns = results.querySelectorAll('button');
13599
14625
  let clickedBtn = null;
13600
14626
  btns.forEach(b => {
13601
- if (b.textContent === 'Install' && !clickedBtn) {
14627
+ if ((b.textContent === 'Install' || b.textContent === 'Update') && !clickedBtn) {
13602
14628
  // Find the button in the same row as the package name
13603
14629
  const row = b.closest('div[style*="border-bottom"]');
13604
14630
  if (row && row.textContent.includes(name)) {
@@ -14388,18 +15414,27 @@ function wfUpdateInspector() {
14388
15414
 
14389
15415
  // ── OUTPUT Accordion ──
14390
15416
  const outputExpanded = hasDone || accStates.output === true;
15417
+ const fmtSelectorHtml = hasDone ? `<select class="wf-output-format-select" onchange="wfChangeOutputFormat(this.value)" onclick="event.stopPropagation()">
15418
+ <option value="auto"${wfState.outputFormat === 'auto' ? ' selected' : ''}>Auto</option>
15419
+ <option value="json"${wfState.outputFormat === 'json' ? ' selected' : ''}>JSON</option>
15420
+ <option value="table"${wfState.outputFormat === 'table' ? ' selected' : ''}>Table</option>
15421
+ <option value="markdown"${wfState.outputFormat === 'markdown' ? ' selected' : ''}>Markdown</option>
15422
+ <option value="text"${wfState.outputFormat === 'text' ? ' selected' : ''}>Text</option>
15423
+ </select>` : '';
14391
15424
  html += `<div class="wf-accordion-header ${outputExpanded ? 'expanded' : ''}" onclick="wfToggleAccordion('output', this)">
14392
15425
  <span class="wf-acc-left"><span class="wf-chevron">&#9654;</span> OUTPUT</span>
14393
- <span class="wf-acc-summary"></span>
15426
+ <span class="wf-acc-summary">${fmtSelectorHtml}</span>
14394
15427
  </div>
14395
15428
  <div class="wf-accordion-body" data-acc="output" style="max-height:${outputExpanded ? 'none' : '0px'};">
14396
- <div class="wf-accordion-body-inner">`;
15429
+ <div class="wf-accordion-body-inner" id="wfOutputContent">`;
14397
15430
  if (hasDone) {
14398
15431
  const r = wfState.executionResults._done;
14399
- const doneJson = JSON.stringify(r.output, null, 2);
14400
- html += `<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;">Completed in ${r.totalTimeMs}ms</div>
14401
- <div class="wf-inspector-code" style="max-height:200px;overflow:auto;color:#40E0FF;">${escapeHtml(doneJson)}</div>
14402
- <button class="wf-output-expand-btn" data-expand-step="_done">&#x2922; Expand</button>`;
15432
+ const hints = r.formatters || {};
15433
+ const fmt = wfState.outputFormat || 'auto';
15434
+ const effectiveFmt = fmt === 'auto' ? wfAutoFormat(r.output) : fmt;
15435
+ html += `<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;">Completed in ${r.totalTimeMs}ms</div>`;
15436
+ html += wfRenderOutputAs(r.output, effectiveFmt, hints);
15437
+ html += `<button class="wf-output-expand-btn" data-expand-step="_done">&#x2922; Expand</button>`;
14403
15438
  } else if (def.output) {
14404
15439
  html += `<div class="wf-inspector-code" style="color:#40E0FF;">${escapeHtml(JSON.stringify(def.output, null, 2))}</div>`;
14405
15440
  } else {
@@ -14531,11 +15566,23 @@ function wfUpdateInspector() {
14531
15566
  if (state === 'completed' && result) {
14532
15567
  const outputJson = result.output ? JSON.stringify(result.output, null, 2) : '';
14533
15568
  const stepTitle = step.name || step.id;
15569
+ const stepShape = result.output ? wfDetectOutputShape(result.output) : null;
15570
+ const showStepFmtToggle = stepShape && (stepShape.type === 'array' || stepShape.type === 'comparison');
15571
+ const stepFmtToggle = showStepFmtToggle ?
15572
+ `<div style="margin-bottom:4px;"><select class="wf-output-format-select" style="width:auto;margin:0;" onchange="wfChangeStepFormat('${escapeHtml(step.id)}', this.value)">
15573
+ <option value="json">JSON</option>
15574
+ <option value="table" selected>Table</option>
15575
+ <option value="text">Text</option>
15576
+ </select></div>` : '';
15577
+ const stepResultContent = showStepFmtToggle
15578
+ ? wfRenderOutputAs(result.output, 'table', {})
15579
+ : (outputJson ? '<div class="wf-inspector-code" style="max-height:120px;overflow:auto;">' + escapeHtml(outputJson) + '</div>' : '');
14534
15580
  html += `<div class="wf-inspector-section">
14535
15581
  <div class="wf-inspector-section-title">Result</div>
14536
15582
  <div class="wf-inspector-result success">
14537
15583
  <div style="font-size:11px;color:var(--text-muted);margin-bottom:4px;">${result.timeMs}ms${result.summary ? ', ' + result.summary : ''}</div>
14538
- ${outputJson ? '<div class="wf-inspector-code" style="max-height:120px;overflow:auto;">' + escapeHtml(outputJson) + '</div>' : ''}
15584
+ ${stepFmtToggle}
15585
+ <div id="wf-step-result-${escapeHtml(step.id)}">${stepResultContent}</div>
14539
15586
  </div>
14540
15587
  ${outputJson ? '<button class="wf-output-expand-btn" data-expand-step="' + escapeHtml(step.id) + '">&#x2922; Expand</button>' : ''}
14541
15588
  </div>`;
@@ -14571,17 +15618,274 @@ function wfBindExpandButtons(container) {
14571
15618
  if (stepId === '_done') {
14572
15619
  title = 'Workflow Output';
14573
15620
  content = JSON.stringify(result.output, null, 2);
15621
+ // Rich modal rendering for workflow output
15622
+ const hints = result.formatters || {};
15623
+ const fmt = wfState.outputFormat || hints.default || 'auto';
15624
+ const effectiveFmt = fmt === 'auto' ? wfAutoFormat(result.output) : fmt;
15625
+ wfOpenOutputModal(title, content, effectiveFmt, result.output, hints);
15626
+ return;
14574
15627
  } else {
14575
15628
  const def = wfState.activeWorkflow;
14576
15629
  const step = def ? def.steps.find(s => s.id === stepId) : null;
14577
15630
  title = (step ? step.name || step.id : stepId) + ' Output';
14578
15631
  content = JSON.stringify(result.output, null, 2);
15632
+ // Rich modal for step outputs with array/comparison shapes
15633
+ const stepShape = result.output ? wfDetectOutputShape(result.output) : null;
15634
+ if (stepShape && (stepShape.type === 'array' || stepShape.type === 'comparison')) {
15635
+ wfOpenOutputModal(title, content, 'table', result.output, {});
15636
+ return;
15637
+ }
14579
15638
  }
14580
15639
  wfOpenOutputModal(title, content);
14581
15640
  });
14582
15641
  });
14583
15642
  }
14584
15643
 
15644
+ // ── Output Format Rendering ──
15645
+
15646
+ function wfStringify(val) {
15647
+ if (val == null) return '';
15648
+ if (typeof val === 'number') return val % 1 === 0 ? String(val) : val.toFixed(4);
15649
+ if (typeof val === 'boolean') return val ? 'true' : 'false';
15650
+ if (typeof val === 'object') {
15651
+ const s = JSON.stringify(val);
15652
+ return s.length > 80 ? s.slice(0, 77) + '...' : s;
15653
+ }
15654
+ return String(val);
15655
+ }
15656
+
15657
+ function wfDetectOutputShape(output) {
15658
+ if (output == null || typeof output !== 'object') return { type: 'scalar', value: output };
15659
+ const keys = Object.keys(output);
15660
+ for (const key of keys) {
15661
+ const val = output[key];
15662
+ if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object' && val[0] !== null) {
15663
+ return { type: 'array', arrayKey: key, columns: Object.keys(val[0]), totalRows: val.length };
15664
+ }
15665
+ }
15666
+ const objKeys = keys.filter(k => output[k] != null && typeof output[k] === 'object' && !Array.isArray(output[k]));
15667
+ if (objKeys.length >= 2) {
15668
+ return { type: 'comparison', objectKeys: objKeys, metricKeys: keys.filter(k => !objKeys.includes(k)) };
15669
+ }
15670
+ const textKeys = keys.filter(k => typeof output[k] === 'string' && output[k].length > 100);
15671
+ if (textKeys.length > 0) {
15672
+ return { type: 'text', textKeys, metricKeys: keys.filter(k => !textKeys.includes(k)) };
15673
+ }
15674
+ return { type: 'metrics', keys };
15675
+ }
15676
+
15677
+ function wfAutoFormat(output) {
15678
+ const shape = wfDetectOutputShape(output);
15679
+ if (shape.type === 'array' || shape.type === 'comparison') return 'table';
15680
+ return 'text';
15681
+ }
15682
+
15683
+ function wfRenderOutputAs(output, format, hints) {
15684
+ hints = hints || {};
15685
+ if (!output) return '<div class="wf-inspector-code" style="color:#40E0FF;">(no output)</div>';
15686
+ switch (format) {
15687
+ case 'json':
15688
+ return '<div class="wf-inspector-code" style="max-height:200px;overflow:auto;color:#40E0FF;">' + escapeHtml(JSON.stringify(output, null, 2)) + '</div>';
15689
+ case 'table':
15690
+ return wfRenderTable(output, hints);
15691
+ case 'markdown':
15692
+ return '<div class="wf-output-markdown">' + renderMarkdown(wfOutputToMarkdown(output, hints)) + '</div>';
15693
+ case 'text':
15694
+ return wfRenderText(output, hints);
15695
+ default:
15696
+ return '<div class="wf-inspector-code" style="max-height:200px;overflow:auto;color:#40E0FF;">' + escapeHtml(JSON.stringify(output, null, 2)) + '</div>';
15697
+ }
15698
+ }
15699
+
15700
+ function wfRenderTable(output, hints) {
15701
+ const shape = wfDetectOutputShape(output);
15702
+ let html = '';
15703
+
15704
+ if (shape.type === 'array') {
15705
+ const key = hints.arrayField || shape.arrayKey;
15706
+ const data = output[key] || [];
15707
+ if (data.length === 0) return '<div style="color:var(--text-muted);font-size:12px;">(empty results)</div>';
15708
+ const columns = hints.columns || shape.columns;
15709
+
15710
+ // Show non-array metrics above
15711
+ const metricKeys = Object.keys(output).filter(k => k !== key && !Array.isArray(output[k]));
15712
+ if (metricKeys.length > 0) {
15713
+ html += '<div style="margin-bottom:8px;">';
15714
+ metricKeys.forEach(k => {
15715
+ html += '<div class="wf-output-metric"><div class="wf-output-text-label">' + escapeHtml(k) + '</div><div class="wf-output-metric-val">' + escapeHtml(wfStringify(output[k])) + '</div></div>';
15716
+ });
15717
+ html += '</div>';
15718
+ }
15719
+
15720
+ html += '<table class="wf-output-table"><thead><tr>';
15721
+ columns.forEach(c => { html += '<th>' + escapeHtml(c) + '</th>'; });
15722
+ html += '</tr></thead><tbody>';
15723
+ data.forEach(row => {
15724
+ html += '<tr>';
15725
+ columns.forEach(c => { html += '<td>' + escapeHtml(wfStringify(row[c])) + '</td>'; });
15726
+ html += '</tr>';
15727
+ });
15728
+ html += '</tbody></table>';
15729
+ return html;
15730
+ }
15731
+
15732
+ if (shape.type === 'comparison') {
15733
+ const keys = shape.objectKeys;
15734
+ const allFields = new Set();
15735
+ keys.forEach(k => { if (output[k]) Object.keys(output[k]).forEach(f => allFields.add(f)); });
15736
+
15737
+ // Show non-object metrics above
15738
+ const metricKeys = (shape.metricKeys || []).filter(k => output[k] != null);
15739
+ if (metricKeys.length > 0) {
15740
+ html += '<div style="margin-bottom:8px;">';
15741
+ metricKeys.forEach(k => {
15742
+ html += '<div class="wf-output-metric"><div class="wf-output-text-label">' + escapeHtml(k) + '</div><div class="wf-output-metric-val">' + escapeHtml(wfStringify(output[k])) + '</div></div>';
15743
+ });
15744
+ html += '</div>';
15745
+ }
15746
+
15747
+ html += '<table class="wf-output-table"><thead><tr><th></th>';
15748
+ keys.forEach(k => { html += '<th>' + escapeHtml(String(k)) + '</th>'; });
15749
+ html += '</tr></thead><tbody>';
15750
+ allFields.forEach(f => {
15751
+ html += '<tr><td style="font-weight:600;">' + escapeHtml(f) + '</td>';
15752
+ keys.forEach(k => { html += '<td>' + escapeHtml(wfStringify(output[k] && output[k][f])) + '</td>'; });
15753
+ html += '</tr>';
15754
+ });
15755
+ html += '</tbody></table>';
15756
+ return html;
15757
+ }
15758
+
15759
+ // Fallback: key-value table
15760
+ html += '<table class="wf-output-table"><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody>';
15761
+ Object.entries(output).forEach(function(entry) {
15762
+ html += '<tr><td>' + escapeHtml(entry[0]) + '</td><td>' + escapeHtml(wfStringify(entry[1])) + '</td></tr>';
15763
+ });
15764
+ html += '</tbody></table>';
15765
+ return html;
15766
+ }
15767
+
15768
+ function wfRenderText(output, hints) {
15769
+ const shape = wfDetectOutputShape(output);
15770
+ if (shape.type === 'scalar') return '<div style="font-size:12px;color:var(--text);">' + escapeHtml(String(output || '')) + '</div>';
15771
+ let html = '';
15772
+ if (hints.title) {
15773
+ html += '<div style="font-weight:600;font-size:13px;color:var(--accent);margin-bottom:8px;">' + escapeHtml(hints.title) + '</div>';
15774
+ }
15775
+
15776
+ // Metrics
15777
+ const metricKeys = Object.keys(output).filter(k =>
15778
+ output[k] != null && (typeof output[k] !== 'object' || typeof output[k] === 'boolean') &&
15779
+ (typeof output[k] !== 'string' || output[k].length <= 100)
15780
+ );
15781
+ if (metricKeys.length > 0) {
15782
+ html += '<div style="margin-bottom:10px;">';
15783
+ metricKeys.forEach(k => {
15784
+ html += '<div class="wf-output-metric"><div class="wf-output-text-label">' + escapeHtml(k) + '</div><div class="wf-output-metric-val">' + escapeHtml(wfStringify(output[k])) + '</div></div>';
15785
+ });
15786
+ html += '</div>';
15787
+ }
15788
+
15789
+ // Object fields (comparison-like)
15790
+ Object.keys(output).forEach(k => {
15791
+ const v = output[k];
15792
+ if (v != null && typeof v === 'object' && !Array.isArray(v)) {
15793
+ html += '<div class="wf-output-text-field"><div class="wf-output-text-label">' + escapeHtml(k) + '</div>';
15794
+ Object.entries(v).forEach(function(entry) {
15795
+ html += '<div style="font-size:12px;margin-left:8px;"><span style="color:var(--text-muted);">' + escapeHtml(entry[0]) + ':</span> <span style="color:#40E0FF;">' + escapeHtml(wfStringify(entry[1])) + '</span></div>';
15796
+ });
15797
+ html += '</div>';
15798
+ }
15799
+ });
15800
+
15801
+ // Text fields
15802
+ Object.keys(output).forEach(k => {
15803
+ if (typeof output[k] === 'string' && output[k].length > 100) {
15804
+ html += '<div class="wf-output-text-field"><div class="wf-output-text-label">' + escapeHtml(k) + '</div><div class="wf-output-text-value">' + escapeHtml(output[k]) + '</div></div>';
15805
+ }
15806
+ });
15807
+
15808
+ // Arrays
15809
+ Object.keys(output).forEach(k => {
15810
+ if (Array.isArray(output[k])) {
15811
+ html += '<div class="wf-output-text-field"><div class="wf-output-text-label">' + escapeHtml(k) + ' (' + output[k].length + ' items)</div>';
15812
+ html += '<div class="wf-inspector-code" style="max-height:100px;overflow:auto;">' + escapeHtml(JSON.stringify(output[k], null, 2)) + '</div></div>';
15813
+ }
15814
+ });
15815
+
15816
+ return html || '<div class="wf-inspector-code" style="color:#40E0FF;">' + escapeHtml(JSON.stringify(output, null, 2)) + '</div>';
15817
+ }
15818
+
15819
+ function wfOutputToMarkdown(output, hints) {
15820
+ let md = '';
15821
+ if (hints.title) md += '## ' + hints.title + '\n\n';
15822
+ else md += '## Workflow Output\n\n';
15823
+ Object.entries(output).forEach(function(entry) {
15824
+ const k = entry[0], v = entry[1];
15825
+ if (v == null) return;
15826
+ if (typeof v !== 'object' || typeof v === 'boolean') {
15827
+ if (typeof v !== 'string' || v.length <= 100) {
15828
+ md += '- **' + k + ':** ' + wfStringify(v) + '\n';
15829
+ }
15830
+ }
15831
+ });
15832
+ md += '\n';
15833
+ // Arrays as tables
15834
+ Object.entries(output).forEach(function(entry) {
15835
+ const k = entry[0], v = entry[1];
15836
+ if (!Array.isArray(v) || v.length === 0 || typeof v[0] !== 'object') return;
15837
+ const cols = hints.columns || Object.keys(v[0]);
15838
+ md += '### ' + k + '\n\n| ' + cols.join(' | ') + ' |\n| ' + cols.map(function() { return '---'; }).join(' | ') + ' |\n';
15839
+ v.forEach(function(row) { md += '| ' + cols.map(function(c) { return wfStringify(row[c]); }).join(' | ') + ' |\n'; });
15840
+ md += '\n';
15841
+ });
15842
+ // Comparison objects
15843
+ const objKeys = Object.keys(output).filter(function(k) { return output[k] != null && typeof output[k] === 'object' && !Array.isArray(output[k]); });
15844
+ if (objKeys.length >= 2) {
15845
+ const allFields = new Set();
15846
+ objKeys.forEach(function(k) { Object.keys(output[k]).forEach(function(f) { allFields.add(f); }); });
15847
+ md += '| | ' + objKeys.join(' | ') + ' |\n| --- | ' + objKeys.map(function() { return '---'; }).join(' | ') + ' |\n';
15848
+ allFields.forEach(function(f) {
15849
+ md += '| **' + f + '** | ' + objKeys.map(function(k) { return wfStringify(output[k][f]); }).join(' | ') + ' |\n';
15850
+ });
15851
+ md += '\n';
15852
+ }
15853
+ // Long text
15854
+ Object.entries(output).forEach(function(entry) {
15855
+ if (typeof entry[1] === 'string' && entry[1].length > 100) {
15856
+ md += '### ' + entry[0] + '\n\n' + entry[1] + '\n\n';
15857
+ }
15858
+ });
15859
+ return md;
15860
+ }
15861
+
15862
+ function wfChangeOutputFormat(format) {
15863
+ wfState.outputFormat = format;
15864
+ const r = wfState.executionResults._done;
15865
+ if (!r) return;
15866
+ const hints = r.formatters || {};
15867
+ const effectiveFormat = format === 'auto' ? wfAutoFormat(r.output) : format;
15868
+ const container = document.getElementById('wfOutputContent');
15869
+ if (!container) return;
15870
+ let html = '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;">Completed in ' + r.totalTimeMs + 'ms</div>';
15871
+ html += wfRenderOutputAs(r.output, effectiveFormat, hints);
15872
+ html += '<button class="wf-output-expand-btn" data-expand-step="_done">&#x2922; Expand</button>';
15873
+ container.innerHTML = html;
15874
+ wfBindExpandButtons(container);
15875
+ }
15876
+
15877
+ function wfChangeStepFormat(stepId, format) {
15878
+ const result = wfState.executionResults[stepId];
15879
+ if (!result || !result.output) return;
15880
+ const container = document.getElementById('wf-step-result-' + stepId);
15881
+ if (!container) return;
15882
+ if (format === 'json') {
15883
+ container.innerHTML = '<div class="wf-inspector-code" style="max-height:120px;overflow:auto;">' + escapeHtml(JSON.stringify(result.output, null, 2)) + '</div>';
15884
+ } else {
15885
+ container.innerHTML = wfRenderOutputAs(result.output, format, {});
15886
+ }
15887
+ }
15888
+
14585
15889
  // escapeHtml is already defined globally — reuse it
14586
15890
 
14587
15891
  // ── Toolbar helpers ──
@@ -14783,6 +16087,31 @@ function wfStopExecution(reason) {
14783
16087
  wfUpdateInspector();
14784
16088
  }
14785
16089
 
16090
+ // ── Workflow Input Cache (localStorage) ──
16091
+
16092
+ function wfSlugify(name) {
16093
+ return String(name).toLowerCase().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
16094
+ }
16095
+
16096
+ function wfGetInputCache(workflowName) {
16097
+ try {
16098
+ const slug = wfSlugify(workflowName);
16099
+ if (!slug) return {};
16100
+ const raw = localStorage.getItem('vai-workflow-inputs-' + slug);
16101
+ if (!raw) return {};
16102
+ const parsed = JSON.parse(raw);
16103
+ return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {};
16104
+ } catch { return {}; }
16105
+ }
16106
+
16107
+ function wfSaveInputCache(workflowName, inputs) {
16108
+ try {
16109
+ const slug = wfSlugify(workflowName);
16110
+ if (!slug || !inputs) return;
16111
+ localStorage.setItem('vai-workflow-inputs-' + slug, JSON.stringify(inputs));
16112
+ } catch { /* localStorage may be full or disabled */ }
16113
+ }
16114
+
14786
16115
  // ── Input Modal (pre-execution) ──
14787
16116
 
14788
16117
  function wfShowInputModal() {
@@ -14792,13 +16121,15 @@ function wfShowInputModal() {
14792
16121
  if (entries.length === 0) { wfExecuteWithInputs({}); return; }
14793
16122
 
14794
16123
  document.getElementById('wfInputModalTitle').textContent = (def.name || 'Workflow') + ' Inputs';
16124
+ const cached = wfGetInputCache(def.name || '');
14795
16125
  let html = '';
14796
16126
  for (const [key, spec] of entries) {
14797
16127
  const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
14798
16128
  const desc = spec.description ? `<div class="wf-input-modal-desc">${escapeHtml(spec.description)}</div>` : '';
14799
- // Pre-fill from inspector fields if available, then from defaults
16129
+ // Pre-fill priority: 1) inspector field, 2) cached from last run, 3) spec default
14800
16130
  const inspectorEl = document.getElementById('wf-input-' + key);
14801
16131
  let prefill = inspectorEl ? inspectorEl.value : '';
16132
+ if (!prefill && key in cached) prefill = String(cached[key]);
14802
16133
  if (!prefill && spec.default !== undefined) prefill = String(spec.default);
14803
16134
  const placeholder = spec.type === 'number' ? 'number' : (spec.type || 'string');
14804
16135
  html += `<div class="wf-input-modal-field">
@@ -14869,6 +16200,9 @@ function wfInputModalSubmit() {
14869
16200
 
14870
16201
  if (hasError) return;
14871
16202
 
16203
+ // Cache inputs for next run
16204
+ wfSaveInputCache(def.name || '', inputs);
16205
+
14872
16206
  // Also update inspector fields to keep them in sync
14873
16207
  for (const [key, val] of Object.entries(inputs)) {
14874
16208
  const inspEl = document.getElementById('wf-input-' + key);
@@ -14988,6 +16322,7 @@ async function wfExecuteWithInputs(inputs) {
14988
16322
  if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
14989
16323
  } else if (currentEvent === 'done') {
14990
16324
  wfState.executionResults._done = data;
16325
+ wfState.outputFormat = 'auto';
14991
16326
  if (!wfState.selectedNodeId) wfUpdateInspector();
14992
16327
  } else if (currentEvent === 'error') {
14993
16328
  hasError = true;
@@ -15077,7 +16412,7 @@ function wfResetExecution() {
15077
16412
  // ── Output Modal ──
15078
16413
  let wfOutputModalData = '';
15079
16414
 
15080
- function wfOpenOutputModal(title, content) {
16415
+ function wfOpenOutputModal(title, content, format, outputObj, hints) {
15081
16416
  wfOutputModalData = content;
15082
16417
  const backdrop = document.getElementById('wfOutputModalBackdrop');
15083
16418
  const titleEl = document.getElementById('wfOutputModalTitle');
@@ -15085,8 +16420,14 @@ function wfOpenOutputModal(title, content) {
15085
16420
  const copyLabel = document.getElementById('wfOutputCopyLabel');
15086
16421
  if (!backdrop || !bodyEl) return;
15087
16422
  titleEl.textContent = title || 'Output';
15088
- bodyEl.textContent = content;
15089
16423
  copyLabel.textContent = 'Copy';
16424
+
16425
+ // Rich rendering if format + output object provided
16426
+ if (format && outputObj && format !== 'json') {
16427
+ bodyEl.innerHTML = wfRenderOutputAs(outputObj, format, hints || {});
16428
+ } else {
16429
+ bodyEl.textContent = content;
16430
+ }
15090
16431
  backdrop.style.display = 'flex';
15091
16432
  }
15092
16433
 
@@ -15325,7 +16666,7 @@ const WF_INPUT_DEFS = {
15325
16666
  search: [{ key: 'query', type: 'text', required: true, placeholder: 'Search query' }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }, { key: 'limit', type: 'number', required: false, placeholder: '10' }, { key: 'filter', type: 'json', required: false, placeholder: '{}' }],
15326
16667
  rerank: [{ key: 'query', type: 'text', required: true }, { key: 'documents', type: 'json', required: true, placeholder: '["doc1","doc2"]' }, { key: 'model', type: 'text', required: false, placeholder: 'rerank-2.5' }],
15327
16668
  ingest: [{ key: 'text', type: 'textarea', required: true }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }, { key: 'source', type: 'text', required: false }, { key: 'chunkSize', type: 'number', required: false, placeholder: '512' }, { key: 'chunkStrategy', type: 'select', required: false, options: ['fixed','sentence','paragraph','recursive','markdown'] }],
15328
- embed: [{ key: 'text', type: 'text', required: true, placeholder: 'Text to embed' }, { key: 'model', type: 'text', required: false, placeholder: 'voyage-3-large' }, { key: 'inputType', type: 'select', required: false, options: ['document','query'] }],
16669
+ embed: [{ key: 'text', type: 'text', required: true, placeholder: 'Text to embed' }, { key: 'model', type: 'text', required: false, placeholder: 'voyage-4-large' }, { key: 'inputType', type: 'select', required: false, options: ['document','query'] }],
15329
16670
  similarity: [{ key: 'text1', type: 'text', required: true }, { key: 'text2', type: 'text', required: true }, { key: 'model', type: 'text', required: false }],
15330
16671
  collections: [{ key: 'db', type: 'text', required: false }],
15331
16672
  models: [{ key: 'category', type: 'select', required: false, options: ['embedding','rerank','all'] }],
@@ -18184,6 +19525,7 @@ const staticCommandRegistry = [
18184
19525
  { id: 'nav-generate', label: 'Generate', description: 'Code generation and templates', category: 'navigation', keywords: ['code', 'template', 'scaffold'], icon: '💻', shortcut: '⌘6', priority: 1, execute: () => switchTab('generate') },
18185
19526
  { id: 'nav-chat', label: 'Chat', description: 'Chat with your knowledge base', category: 'navigation', keywords: ['conversation', 'ask', 'rag', 'talk'], icon: '💬', shortcut: '⌘7', priority: 1, execute: () => switchTab('chat') },
18186
19527
  { id: 'nav-workflows', label: 'Workflows', description: 'Visual workflow canvas', category: 'navigation', keywords: ['pipeline', 'dag', 'canvas'], icon: '🔄', shortcut: '⌘8', priority: 1, execute: () => switchTab('workflows') },
19528
+ { id: 'nav-models', label: 'Models', description: 'Voyage AI model catalog and specs', category: 'navigation', keywords: ['model', 'voyage', 'catalog', 'specs', 'embedding', 'rerank', 'pricing'], icon: '🤖', priority: 1, execute: () => switchTab('models') },
18187
19529
  { id: 'nav-benchmark', label: 'Benchmark', description: 'Model comparison on your data', category: 'navigation', keywords: ['performance', 'test', 'evaluate', 'benchmark'], icon: '📊', shortcut: '⌘9', priority: 1, execute: () => switchTab('benchmark') },
18188
19530
  { id: 'nav-explore', label: 'Explore', description: 'Embedding space visualization', category: 'navigation', keywords: ['pca', 'tsne', 'visualize', 'scatter'], icon: '🎯', shortcut: '⌘0', priority: 1, execute: () => switchTab('explore') },
18189
19531
  { id: 'nav-about', label: 'About', description: 'Version and project information', category: 'navigation', keywords: ['version', 'info', 'credits'], icon: 'ℹ️', priority: 5, execute: () => switchTab('about') },
@@ -18239,7 +19581,7 @@ const staticCommandRegistry = [
18239
19581
  { id: 'explain-reranking', label: 'Learn: Reranking', description: 'Two-stage retrieval with rerankers', category: 'explainer', keywords: ['reranking', 'two-stage', 'retrieval', 'precision'], icon: '📚', priority: 7, execute: () => switchTab('explore') },
18240
19582
  { id: 'explain-vector-search', label: 'Learn: Vector Search', description: 'MongoDB Atlas Vector Search', category: 'explainer', keywords: ['vector', 'search', 'atlas', 'similarity', 'knn'], icon: '📚', priority: 7, execute: () => switchTab('explore') },
18241
19583
  { id: 'explain-rag', label: 'Learn: RAG Pipelines', description: 'Retrieval-Augmented Generation', category: 'explainer', keywords: ['rag', 'retrieval', 'augmented', 'generation', 'pipeline'], icon: '📚', priority: 7, execute: () => switchTab('explore') },
18242
- { id: 'explain-models', label: 'Learn: Voyage AI Models', description: 'Choosing the right model', category: 'explainer', keywords: ['models', 'voyage-4', 'large', 'lite', 'domain'], icon: '📚', priority: 7, execute: () => switchTab('explore') },
19584
+ { id: 'explain-models', label: 'Learn: Voyage AI Models', description: 'Choosing the right model', category: 'explainer', keywords: ['models', 'voyage-4', 'large', 'lite', 'domain'], icon: '📚', priority: 7, execute: () => switchTab('models') },
18243
19585
  { id: 'explain-quantization', label: 'Learn: Quantization', description: 'Reduce storage costs with lower-precision embeddings', category: 'explainer', keywords: ['quantization', 'int8', 'ubinary', 'compression', 'storage'], icon: '📚', priority: 7, execute: () => switchTab('explore') }
18244
19586
  ];
18245
19587