voyageai-cli 1.30.1 → 1.30.3
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.
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/about.js +3 -3
- package/src/commands/code-search.js +751 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/embed.js +121 -2
- package/src/commands/index-workspace.js +9 -5
- package/src/commands/playground.js +65 -4
- package/src/commands/quickstart.js +4 -4
- package/src/commands/workflow.js +132 -65
- package/src/lib/api.js +31 -0
- package/src/lib/catalog.js +4 -2
- package/src/lib/code-search.js +315 -0
- package/src/lib/codegen.js +1 -1
- package/src/lib/explanations.js +3 -3
- package/src/lib/github.js +226 -0
- package/src/lib/input.js +92 -1
- package/src/lib/template-engine.js +154 -20
- package/src/lib/workflow-builder.js +753 -0
- package/src/lib/workflow-formatters.js +454 -0
- package/src/lib/workflow-input-cache.js +111 -0
- package/src/lib/workflow-scaffold.js +1 -1
- package/src/lib/workflow.js +124 -8
- package/src/mcp/schemas/index.js +142 -0
- package/src/mcp/server.js +17 -4
- package/src/mcp/tools/authoring.js +662 -0
- package/src/mcp/tools/code-search.js +620 -0
- package/src/mcp/tools/embedding.js +72 -3
- package/src/mcp/tools/ingest.js +2 -5
- package/src/mcp/tools/retrieval.js +2 -15
- package/src/mcp/tools/workspace.js +1 -12
- package/src/mcp/utils.js +20 -0
- package/src/playground/help/workflow-nodes.js +127 -2
- package/src/playground/index.html +2013 -139
- package/src/workflows/code-review.json +110 -0
- package/src/workflows/cost-analysis.json +5 -0
- package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
- package/src/workflows/tests/code-review.happy-path.test.json +121 -0
- package/src/workflows/tests/code-review.no-question.test.json +70 -0
- 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;
|
|
@@ -4029,24 +4517,39 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
4029
4517
|
cursor: help;
|
|
4030
4518
|
}
|
|
4031
4519
|
.wf-validation-bar {
|
|
4032
|
-
position:
|
|
4033
|
-
top:
|
|
4034
|
-
left:
|
|
4035
|
-
right:
|
|
4520
|
+
position: absolute;
|
|
4521
|
+
top: 52px;
|
|
4522
|
+
left: 12px;
|
|
4523
|
+
right: 12px;
|
|
4036
4524
|
height: 28px;
|
|
4037
4525
|
background: var(--bg-surface);
|
|
4038
|
-
border
|
|
4526
|
+
border: 1px solid var(--border);
|
|
4527
|
+
border-radius: 6px;
|
|
4039
4528
|
display: none;
|
|
4040
4529
|
align-items: center;
|
|
4041
|
-
padding: 0
|
|
4530
|
+
padding: 0 12px;
|
|
4042
4531
|
font-size: 12px;
|
|
4043
|
-
z-index:
|
|
4532
|
+
z-index: 9;
|
|
4044
4533
|
cursor: pointer;
|
|
4045
4534
|
transition: background-color 0.15s;
|
|
4046
4535
|
}
|
|
4047
4536
|
.wf-validation-bar:hover {
|
|
4048
4537
|
background: var(--bg-card);
|
|
4049
4538
|
}
|
|
4539
|
+
.wf-validation-bar .wf-validation-close {
|
|
4540
|
+
margin-left: auto;
|
|
4541
|
+
background: none;
|
|
4542
|
+
border: none;
|
|
4543
|
+
color: inherit;
|
|
4544
|
+
cursor: pointer;
|
|
4545
|
+
font-size: 14px;
|
|
4546
|
+
padding: 0 4px;
|
|
4547
|
+
opacity: 0.6;
|
|
4548
|
+
line-height: 1;
|
|
4549
|
+
}
|
|
4550
|
+
.wf-validation-bar .wf-validation-close:hover {
|
|
4551
|
+
opacity: 1;
|
|
4552
|
+
}
|
|
4050
4553
|
.wf-validation-bar.warning {
|
|
4051
4554
|
color: #FFB74D;
|
|
4052
4555
|
border-bottom-color: #FFB74D;
|
|
@@ -4200,6 +4703,54 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
4200
4703
|
border-radius: 6px; padding: 8px; overflow-x: auto;
|
|
4201
4704
|
white-space: pre-wrap; color: var(--text); margin-top: 4px;
|
|
4202
4705
|
}
|
|
4706
|
+
/* Output format selector and rich rendering */
|
|
4707
|
+
.wf-output-format-select {
|
|
4708
|
+
font-size: 11px; padding: 2px 6px; border-radius: 4px;
|
|
4709
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
4710
|
+
color: var(--text); cursor: pointer; margin-left: 8px;
|
|
4711
|
+
}
|
|
4712
|
+
.wf-output-format-select:hover { border-color: var(--accent); }
|
|
4713
|
+
.wf-output-table {
|
|
4714
|
+
width: 100%; border-collapse: collapse; font-size: 11px;
|
|
4715
|
+
color: var(--text); margin-top: 8px;
|
|
4716
|
+
}
|
|
4717
|
+
.wf-output-table th {
|
|
4718
|
+
text-align: left; padding: 6px 8px; font-weight: 600;
|
|
4719
|
+
border-bottom: 1px solid var(--accent); color: var(--accent);
|
|
4720
|
+
font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
4721
|
+
}
|
|
4722
|
+
.wf-output-table td {
|
|
4723
|
+
padding: 5px 8px; border-bottom: 1px solid var(--border);
|
|
4724
|
+
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px;
|
|
4725
|
+
}
|
|
4726
|
+
.wf-output-table tr:hover td { background: rgba(0,212,170,0.05); }
|
|
4727
|
+
.wf-output-markdown {
|
|
4728
|
+
font-size: 12px; line-height: 1.5; color: var(--text); padding: 8px;
|
|
4729
|
+
}
|
|
4730
|
+
.wf-output-markdown h2, .wf-output-markdown h3 {
|
|
4731
|
+
color: var(--accent); margin-top: 12px; margin-bottom: 4px;
|
|
4732
|
+
}
|
|
4733
|
+
.wf-output-markdown table {
|
|
4734
|
+
border-collapse: collapse; width: 100%; margin: 8px 0;
|
|
4735
|
+
}
|
|
4736
|
+
.wf-output-markdown th, .wf-output-markdown td {
|
|
4737
|
+
border: 1px solid var(--border); padding: 4px 8px; font-size: 11px;
|
|
4738
|
+
}
|
|
4739
|
+
.wf-output-text-field { margin-bottom: 12px; }
|
|
4740
|
+
.wf-output-text-label {
|
|
4741
|
+
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
4742
|
+
color: var(--accent); letter-spacing: 0.5px; margin-bottom: 2px;
|
|
4743
|
+
}
|
|
4744
|
+
.wf-output-text-value {
|
|
4745
|
+
font-size: 12px; color: var(--text); line-height: 1.5; white-space: pre-wrap;
|
|
4746
|
+
}
|
|
4747
|
+
.wf-output-metric {
|
|
4748
|
+
display: inline-block; margin-right: 16px; margin-bottom: 6px;
|
|
4749
|
+
}
|
|
4750
|
+
.wf-output-metric-val {
|
|
4751
|
+
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px;
|
|
4752
|
+
color: #40E0FF; font-weight: 600;
|
|
4753
|
+
}
|
|
4203
4754
|
.wf-tool-badge {
|
|
4204
4755
|
display: inline-flex; align-items: center; gap: 4px;
|
|
4205
4756
|
padding: 2px 8px; border-radius: 10px; font-size: 11px;
|
|
@@ -5754,6 +6305,7 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
5754
6305
|
<div class="sidebar-nav-divider"></div>
|
|
5755
6306
|
<div class="sidebar-nav-group" role="tablist" aria-label="Learn">
|
|
5756
6307
|
<div class="sidebar-nav-label" id="nav-learn-label">Learn</div>
|
|
6308
|
+
<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
6309
|
<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
6310
|
<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
6311
|
<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>
|
|
@@ -6150,12 +6702,12 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
6150
6702
|
<div class="tab-panel" id="tab-multimodal" role="tabpanel" aria-labelledby="tab-btn-multimodal" tabindex="0">
|
|
6151
6703
|
<div class="page-header">
|
|
6152
6704
|
<h2 class="page-header-title">Multimodal</h2>
|
|
6153
|
-
<p class="page-header-subtitle">Compare images and text in the same vector space</p>
|
|
6154
|
-
<p class="page-header-hint">Voyage AI's multimodal models embed images and text into a unified vector space, so you can compare them directly with cosine similarity.</p>
|
|
6705
|
+
<p class="page-header-subtitle">Compare images, video, and text in the same vector space</p>
|
|
6706
|
+
<p class="page-header-hint">Voyage AI's multimodal models embed images, video, and text into a unified vector space, so you can compare them directly with cosine similarity.</p>
|
|
6155
6707
|
<a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/embed" target="_blank" rel="noopener" title="Multimodal embedding documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
|
|
6156
6708
|
</div>
|
|
6157
6709
|
|
|
6158
|
-
<!-- Section A: Image ↔ Text Similarity -->
|
|
6710
|
+
<!-- Section A: Image / Video ↔ Text Similarity -->
|
|
6159
6711
|
<div class="mm-grid">
|
|
6160
6712
|
<div class="card">
|
|
6161
6713
|
<div class="card-title">Image</div>
|
|
@@ -6171,9 +6723,23 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
6171
6723
|
<button class="mm-clear-btn" onclick="clearMultimodalImage()">✕ Clear</button>
|
|
6172
6724
|
</div>
|
|
6173
6725
|
</div>
|
|
6726
|
+
<div class="card">
|
|
6727
|
+
<div class="card-title">Video</div>
|
|
6728
|
+
<div class="mm-drop-zone" id="mmVideoDropZone">
|
|
6729
|
+
<div class="mm-drop-icon">🎬</div>
|
|
6730
|
+
<div class="mm-drop-text">Drop a video here or click to browse</div>
|
|
6731
|
+
<div class="mm-drop-hint">MP4, WebM, MOV, max 20 MB</div>
|
|
6732
|
+
</div>
|
|
6733
|
+
<input type="file" id="mmVideoFileInput" accept="video/mp4,video/webm,video/quicktime,video/x-msvideo,video/x-matroska" style="display:none">
|
|
6734
|
+
<div class="mm-preview" id="mmVideoPreview">
|
|
6735
|
+
<video id="mmPreviewVideo" controls style="max-width:100%;max-height:240px;border-radius:8px;background:#000;"></video>
|
|
6736
|
+
<div class="mm-file-info" id="mmVideoFileInfo"></div>
|
|
6737
|
+
<button class="mm-clear-btn" onclick="clearMultimodalVideo()">✕ Clear</button>
|
|
6738
|
+
</div>
|
|
6739
|
+
</div>
|
|
6174
6740
|
<div class="card">
|
|
6175
6741
|
<div class="card-title">Text</div>
|
|
6176
|
-
<textarea id="mmText" rows="8" placeholder="Describe what you see, or enter any text to compare against the
|
|
6742
|
+
<textarea id="mmText" rows="8" placeholder="Describe what you see, or enter any text to compare against the media..."></textarea>
|
|
6177
6743
|
</div>
|
|
6178
6744
|
</div>
|
|
6179
6745
|
|
|
@@ -6474,9 +7040,9 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
6474
7040
|
<div style="margin-top:16px;padding:12px;background:var(--accent-glow);border-radius:8px;border-left:3px solid var(--accent);">
|
|
6475
7041
|
<strong style="color:var(--accent);">💡 Pro Tip: Asymmetric Retrieval</strong>
|
|
6476
7042
|
<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-
|
|
6478
|
-
query with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-
|
|
6479
|
-
Because all Voyage
|
|
7043
|
+
Embed your document corpus with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-4-lite</code> ($0.02/M) and
|
|
7044
|
+
query with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-4</code> ($0.06/M).
|
|
7045
|
+
Because all Voyage 4 models share the same embedding space, you get top-tier retrieval quality at a fraction of the cost.
|
|
6480
7046
|
</p>
|
|
6481
7047
|
</div>
|
|
6482
7048
|
</div>
|
|
@@ -6489,8 +7055,8 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
6489
7055
|
<div style="font-size:20px;margin-bottom:8px;">🔗</div>
|
|
6490
7056
|
<div style="font-weight:600;margin-bottom:4px;">Shared Embedding Space</div>
|
|
6491
7057
|
<div style="font-size:13px;color:var(--text-dim);">
|
|
6492
|
-
All Voyage
|
|
6493
|
-
with <code>voyage-
|
|
7058
|
+
All Voyage 4 models produce compatible embeddings. Mix <code>voyage-4-large</code> for queries
|
|
7059
|
+
with <code>voyage-4-lite</code> for documents, and they work together seamlessly.
|
|
6494
7060
|
</div>
|
|
6495
7061
|
</div>
|
|
6496
7062
|
<div style="padding:16px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
|
|
@@ -6818,6 +7384,9 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
6818
7384
|
<button onclick="wfZoom(1)" title="Zoom in">+</button>
|
|
6819
7385
|
<button onclick="wfZoom(-1)" title="Zoom out">−</button>
|
|
6820
7386
|
<button onclick="wfFitToView()" title="Fit to view">⊞</button>
|
|
7387
|
+
<button onclick="wfRelayout()" title="Auto-layout (reorder nodes to minimize crossings)">
|
|
7388
|
+
<svg width="14" height="14" 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="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><line x1="10" y1="6.5" x2="14" y2="6.5"/><line x1="10" y1="17.5" x2="14" y2="17.5"/></svg>
|
|
7389
|
+
</button>
|
|
6821
7390
|
<button onclick="wfResetExecution()" title="Reset">↻</button>
|
|
6822
7391
|
<span class="wf-toolbar-sep"></span>
|
|
6823
7392
|
<button class="wf-plan-btn" onclick="wfDryRun()" id="wfDryRunBtn" disabled title="Dry run: show execution plan">⚙ Plan</button>
|
|
@@ -6837,6 +7406,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
6837
7406
|
<!-- Draft validation status bar -->
|
|
6838
7407
|
<div class="wf-validation-bar" id="wfValidationBar">
|
|
6839
7408
|
<span id="wfValidationBarText"></span>
|
|
7409
|
+
<button class="wf-validation-close" onclick="event.stopPropagation(); document.getElementById('wfValidationBar').style.display='none';" title="Dismiss">×</button>
|
|
6840
7410
|
</div>
|
|
6841
7411
|
<!-- Edge handles for collapsed panels -->
|
|
6842
7412
|
<div class="wf-edge-handle wf-edge-handle--left" id="wfEdgeHandleLeft" onclick="wfToggleLibrary()" title="Expand library">›</div>
|
|
@@ -7112,7 +7682,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
7112
7682
|
<option value="voyage-4-large">voyage-4-large</option>
|
|
7113
7683
|
<option value="voyage-4">voyage-4</option>
|
|
7114
7684
|
<option value="voyage-4-lite">voyage-4-lite</option>
|
|
7115
|
-
<option value="voyage-3
|
|
7685
|
+
<option value="voyage-code-3">voyage-code-3</option>
|
|
7116
7686
|
</select>
|
|
7117
7687
|
</label>
|
|
7118
7688
|
<label>
|
|
@@ -7233,6 +7803,54 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
7233
7803
|
</div>
|
|
7234
7804
|
</div>
|
|
7235
7805
|
|
|
7806
|
+
<!-- ========== MODELS TAB ========== -->
|
|
7807
|
+
<div class="tab-panel" id="tab-models" role="tabpanel" aria-labelledby="tab-btn-models" tabindex="0">
|
|
7808
|
+
<div class="page-header">
|
|
7809
|
+
<h2 class="page-header-title">Models</h2>
|
|
7810
|
+
<p class="page-header-subtitle">Voyage AI model showcase</p>
|
|
7811
|
+
<p class="page-header-hint">Explore all Voyage AI models, compare specs, and find the right model for your use case.</p>
|
|
7812
|
+
<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>
|
|
7813
|
+
</div>
|
|
7814
|
+
|
|
7815
|
+
<!-- Search + Filter -->
|
|
7816
|
+
<div class="models-toolbar">
|
|
7817
|
+
<input type="text" id="modelsSearch" placeholder="Search models..." oninput="filterModels()" style="max-width:280px;" aria-label="Search models">
|
|
7818
|
+
<div class="models-filter-pills" id="modelsFilterPills">
|
|
7819
|
+
<button class="models-filter-pill active" data-filter="all" onclick="setModelFilter('all')">All</button>
|
|
7820
|
+
<button class="models-filter-pill" data-filter="embedding" onclick="setModelFilter('embedding')">Embedding</button>
|
|
7821
|
+
<button class="models-filter-pill" data-filter="reranking" onclick="setModelFilter('reranking')">Reranking</button>
|
|
7822
|
+
<button class="models-filter-pill" data-filter="multimodal" onclick="setModelFilter('multimodal')">Multimodal</button>
|
|
7823
|
+
</div>
|
|
7824
|
+
</div>
|
|
7825
|
+
|
|
7826
|
+
<!-- Shared Embedding Space Hero -->
|
|
7827
|
+
<div class="models-hero card" id="modelsHero">
|
|
7828
|
+
<div class="models-hero-shape" id="modelsHeroShape"></div>
|
|
7829
|
+
<div class="models-hero-content">
|
|
7830
|
+
<div class="models-hero-badge">Voyage 4 Family</div>
|
|
7831
|
+
<div class="models-hero-title">Shared Embedding Space</div>
|
|
7832
|
+
<div class="models-hero-desc">
|
|
7833
|
+
voyage-4-large, voyage-4, and voyage-4-lite produce compatible embeddings in the same vector space.
|
|
7834
|
+
Index documents with the large model for best quality, then query with the lite model at a fraction of the cost.
|
|
7835
|
+
No re-indexing required.
|
|
7836
|
+
</div>
|
|
7837
|
+
<div class="models-hero-models" id="modelsHeroModels"></div>
|
|
7838
|
+
</div>
|
|
7839
|
+
</div>
|
|
7840
|
+
|
|
7841
|
+
<!-- Model Groups (dynamically rendered) -->
|
|
7842
|
+
<div id="modelsGroups"></div>
|
|
7843
|
+
|
|
7844
|
+
<!-- RTEB Benchmark Chart -->
|
|
7845
|
+
<div class="card" id="modelsRtebCard" style="margin-top:24px;">
|
|
7846
|
+
<div class="card-title">RTEB Retrieval Benchmark (NDCG@10)</div>
|
|
7847
|
+
<p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
|
|
7848
|
+
Industry-standard benchmark for retrieval quality across 29 datasets. Higher is better.
|
|
7849
|
+
</p>
|
|
7850
|
+
<div id="modelsRtebChart"></div>
|
|
7851
|
+
</div>
|
|
7852
|
+
</div>
|
|
7853
|
+
|
|
7236
7854
|
<!-- ========== EXPLORE TAB ========== -->
|
|
7237
7855
|
<div class="tab-panel" id="tab-explore" role="tabpanel" aria-labelledby="tab-btn-explore" tabindex="0">
|
|
7238
7856
|
<div class="page-header">
|
|
@@ -7263,9 +7881,30 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
7263
7881
|
</div>
|
|
7264
7882
|
</div>
|
|
7265
7883
|
|
|
7266
|
-
<!-- Hidden global model select (used by JS for model sync) -->
|
|
7267
|
-
<select id="globalModel" style="display:none;"></select>
|
|
7268
|
-
|
|
7884
|
+
<!-- Hidden global model select (used by JS for model sync) -->
|
|
7885
|
+
<select id="globalModel" style="display:none;"></select>
|
|
7886
|
+
|
|
7887
|
+
<!-- ========== MODEL PICKER OVERLAY ========== -->
|
|
7888
|
+
<div class="model-picker-overlay" id="modelPickerOverlay" role="dialog" aria-modal="true" aria-labelledby="modelPickerTitle">
|
|
7889
|
+
<div class="model-picker">
|
|
7890
|
+
<div class="model-picker-header">
|
|
7891
|
+
<div>
|
|
7892
|
+
<div class="model-picker-title" id="modelPickerTitle">Choose a Model</div>
|
|
7893
|
+
<div class="model-picker-context" id="modelPickerContext"></div>
|
|
7894
|
+
</div>
|
|
7895
|
+
<button class="model-picker-close" id="modelPickerClose" aria-label="Close">×</button>
|
|
7896
|
+
</div>
|
|
7897
|
+
<div class="model-picker-search">
|
|
7898
|
+
<input type="text" id="modelPickerSearch" placeholder="Search models..." oninput="filterModelPicker()" aria-label="Search models">
|
|
7899
|
+
</div>
|
|
7900
|
+
<div class="model-picker-recommendation" id="modelPickerRecommendation"></div>
|
|
7901
|
+
<div class="model-picker-list" id="modelPickerList" role="listbox"></div>
|
|
7902
|
+
<div class="model-picker-footer">
|
|
7903
|
+
<a href="#" onclick="event.preventDefault(); closeModelPicker(); switchTab('models');">View all models in catalog</a>
|
|
7904
|
+
</div>
|
|
7905
|
+
</div>
|
|
7906
|
+
</div>
|
|
7907
|
+
|
|
7269
7908
|
<!-- ========== SETTINGS TAB ========== -->
|
|
7270
7909
|
<div class="tab-panel" id="tab-settings">
|
|
7271
7910
|
<div class="settings-layout">
|
|
@@ -8063,6 +8702,7 @@ async function init() {
|
|
|
8063
8702
|
await loadConfig();
|
|
8064
8703
|
await Promise.all([loadModels(), loadConcepts()]);
|
|
8065
8704
|
populateModelSelects();
|
|
8705
|
+
enhanceModelSelects();
|
|
8066
8706
|
buildExploreCards();
|
|
8067
8707
|
|
|
8068
8708
|
// Apply default tab setting
|
|
@@ -8146,6 +8786,9 @@ function switchTab(tab) {
|
|
|
8146
8786
|
if (tab === 'home') {
|
|
8147
8787
|
homeInit();
|
|
8148
8788
|
}
|
|
8789
|
+
if (tab === 'models') {
|
|
8790
|
+
modelsInit();
|
|
8791
|
+
}
|
|
8149
8792
|
}
|
|
8150
8793
|
// Expose globally so Electron main process can call it
|
|
8151
8794
|
window.switchTab = switchTab;
|
|
@@ -9541,6 +10184,393 @@ window.filterExplore = function() {
|
|
|
9541
10184
|
});
|
|
9542
10185
|
};
|
|
9543
10186
|
|
|
10187
|
+
// ── Models Tab ──
|
|
10188
|
+
let modelsTabData = {
|
|
10189
|
+
catalog: null,
|
|
10190
|
+
benchmarks: null,
|
|
10191
|
+
currentFilter: 'all',
|
|
10192
|
+
initialized: false
|
|
10193
|
+
};
|
|
10194
|
+
|
|
10195
|
+
async function loadModelsCatalog() {
|
|
10196
|
+
if (modelsTabData.catalog) return;
|
|
10197
|
+
try {
|
|
10198
|
+
const res = await fetch('/api/models/catalog');
|
|
10199
|
+
const data = await res.json();
|
|
10200
|
+
modelsTabData.catalog = data.models || [];
|
|
10201
|
+
modelsTabData.benchmarks = data.benchmarks || [];
|
|
10202
|
+
} catch {
|
|
10203
|
+
modelsTabData.catalog = allModels;
|
|
10204
|
+
modelsTabData.benchmarks = [];
|
|
10205
|
+
}
|
|
10206
|
+
}
|
|
10207
|
+
|
|
10208
|
+
function modelsInit() {
|
|
10209
|
+
if (modelsTabData.initialized) return;
|
|
10210
|
+
modelsTabData.initialized = true;
|
|
10211
|
+
loadModelsCatalog().then(() => {
|
|
10212
|
+
renderModelsHero();
|
|
10213
|
+
renderModelsGroups();
|
|
10214
|
+
renderModelsRtebChart();
|
|
10215
|
+
});
|
|
10216
|
+
}
|
|
10217
|
+
|
|
10218
|
+
function renderModelsHero() {
|
|
10219
|
+
const heroModels = document.getElementById('modelsHeroModels');
|
|
10220
|
+
const sharedModels = (modelsTabData.catalog || []).filter(
|
|
10221
|
+
m => m.sharedSpace === 'voyage-4' && !m.legacy && !m.unreleased
|
|
10222
|
+
);
|
|
10223
|
+
heroModels.innerHTML = sharedModels.map(m =>
|
|
10224
|
+
'<div class="model-chip" onclick="scrollToModelCard(\'' + m.name + '\')">' +
|
|
10225
|
+
'<span>' + escapeHtml(m.name) + '</span>' +
|
|
10226
|
+
'<span class="model-chip-price">' + escapeHtml(m.price) + '</span>' +
|
|
10227
|
+
'</div>'
|
|
10228
|
+
).join('');
|
|
10229
|
+
|
|
10230
|
+
const shapeEl = document.getElementById('modelsHeroShape');
|
|
10231
|
+
if (shapeEl && !shapeEl.innerHTML) {
|
|
10232
|
+
shapeEl.innerHTML = generateNebulaSVG(200, 200, 900, {variant: 'rich', opacity: 0.08});
|
|
10233
|
+
}
|
|
10234
|
+
}
|
|
10235
|
+
|
|
10236
|
+
function getModelGroups(models) {
|
|
10237
|
+
const groups = [
|
|
10238
|
+
{
|
|
10239
|
+
id: 'voyage-4',
|
|
10240
|
+
title: 'Voyage 4 Flagship',
|
|
10241
|
+
icon: LI.zap,
|
|
10242
|
+
models: models.filter(m => m.family === 'voyage-4' && !m.legacy)
|
|
10243
|
+
},
|
|
10244
|
+
{
|
|
10245
|
+
id: 'domain',
|
|
10246
|
+
title: 'Domain-Specific',
|
|
10247
|
+
icon: LI.target,
|
|
10248
|
+
models: models.filter(m => !m.family && m.type === 'embedding' && !m.legacy && !m.multimodal)
|
|
10249
|
+
},
|
|
10250
|
+
{
|
|
10251
|
+
id: 'multimodal',
|
|
10252
|
+
title: 'Multimodal',
|
|
10253
|
+
icon: LI.image,
|
|
10254
|
+
models: models.filter(m => m.multimodal && !m.legacy)
|
|
10255
|
+
},
|
|
10256
|
+
{
|
|
10257
|
+
id: 'reranking',
|
|
10258
|
+
title: 'Reranking',
|
|
10259
|
+
icon: LI.shuffle,
|
|
10260
|
+
models: models.filter(m => m.type === 'reranking' && !m.legacy)
|
|
10261
|
+
},
|
|
10262
|
+
{
|
|
10263
|
+
id: 'legacy',
|
|
10264
|
+
title: 'Previous Generation',
|
|
10265
|
+
icon: LI.timer,
|
|
10266
|
+
models: models.filter(m => m.legacy === true)
|
|
10267
|
+
}
|
|
10268
|
+
];
|
|
10269
|
+
return groups.filter(g => g.models.length > 0);
|
|
10270
|
+
}
|
|
10271
|
+
|
|
10272
|
+
function renderModelsGroups() {
|
|
10273
|
+
const container = document.getElementById('modelsGroups');
|
|
10274
|
+
const catalog = modelsTabData.catalog || [];
|
|
10275
|
+
const groups = getModelGroups(catalog);
|
|
10276
|
+
|
|
10277
|
+
container.innerHTML = groups.map(function(group, gi) {
|
|
10278
|
+
return '<div class="models-group" id="models-group-' + group.id + '">' +
|
|
10279
|
+
'<div class="models-group-header">' +
|
|
10280
|
+
'<span class="models-group-icon">' + lucideIcon(group.icon, 18) + '</span>' +
|
|
10281
|
+
'<span class="models-group-title">' + escapeHtml(group.title) + '</span>' +
|
|
10282
|
+
'<span class="models-group-count">' + group.models.length + '</span>' +
|
|
10283
|
+
'</div>' +
|
|
10284
|
+
'<div class="models-group-grid">' +
|
|
10285
|
+
group.models.map(function(m, mi) { return renderModelCard(m, gi * 100 + mi); }).join('') +
|
|
10286
|
+
'</div>' +
|
|
10287
|
+
'</div>';
|
|
10288
|
+
}).join('');
|
|
10289
|
+
}
|
|
10290
|
+
|
|
10291
|
+
function renderModelCard(model, seedOffset) {
|
|
10292
|
+
var typeClass = model.multimodal ? 'multimodal' :
|
|
10293
|
+
model.type === 'reranking' ? 'reranking' : 'embedding';
|
|
10294
|
+
var typeLabel = model.multimodal ? 'Multimodal' :
|
|
10295
|
+
model.type === 'reranking' ? 'Reranker' : 'Embedding';
|
|
10296
|
+
|
|
10297
|
+
var specs = [];
|
|
10298
|
+
if (model.context) specs.push({label: 'Context', value: model.context});
|
|
10299
|
+
if (model.dimensions && model.dimensions !== '\u2014') {
|
|
10300
|
+
var dimDefault = model.dimensions.split('(')[0].trim().split(',')[0];
|
|
10301
|
+
specs.push({label: 'Dims', value: dimDefault});
|
|
10302
|
+
}
|
|
10303
|
+
specs.push({label: 'Price', value: model.price});
|
|
10304
|
+
if (model.architecture) specs.push({label: 'Arch', value: model.architecture === 'moe' ? 'MoE' : 'Dense'});
|
|
10305
|
+
|
|
10306
|
+
var sharedBadge = model.sharedSpace ?
|
|
10307
|
+
'<div class="model-card-shared-badge">' + lucideIcon(LI.link, 12) + ' Shared: ' + model.sharedSpace + '</div>' : '';
|
|
10308
|
+
|
|
10309
|
+
var rtebBadge = model.rtebScore ?
|
|
10310
|
+
'<div class="model-card-rteb">' + model.rtebScore.toFixed(1) + '</div>' : '';
|
|
10311
|
+
|
|
10312
|
+
var tryActions = '';
|
|
10313
|
+
if (model.multimodal) {
|
|
10314
|
+
tryActions = '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'multimodal\')">Try in Multimodal</button>';
|
|
10315
|
+
} else if (model.type === 'reranking') {
|
|
10316
|
+
tryActions = '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'search\')">Try in Search</button>';
|
|
10317
|
+
} else if (!model.legacy) {
|
|
10318
|
+
tryActions = '<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'embed\')">Try in Embed</button>' +
|
|
10319
|
+
'<button class="model-card-action" onclick="event.stopPropagation(); switchTab(\'search\')">Try in Search</button>';
|
|
10320
|
+
}
|
|
10321
|
+
|
|
10322
|
+
return '<div class="model-card ' + (model.legacy ? 'legacy' : '') + '" data-model="' + model.name + '" data-type="' + typeClass + '" id="model-card-' + model.name + '">' +
|
|
10323
|
+
'<div class="model-card-nebula">' + generateNebulaSVG(140, 140, 500 + seedOffset, {variant: 'subtle', opacity: 0.05, sparkles: false}) + '</div>' +
|
|
10324
|
+
rtebBadge +
|
|
10325
|
+
'<div class="model-card-header">' +
|
|
10326
|
+
'<span class="model-card-name">' + escapeHtml(model.name) + '</span>' +
|
|
10327
|
+
'<span class="model-card-type-badge ' + typeClass + '">' + typeLabel + '</span>' +
|
|
10328
|
+
'</div>' +
|
|
10329
|
+
sharedBadge +
|
|
10330
|
+
'<div class="model-card-desc">' + escapeHtml(model.bestFor) + '</div>' +
|
|
10331
|
+
'<div class="model-card-specs">' +
|
|
10332
|
+
specs.map(function(s) {
|
|
10333
|
+
return '<div class="model-card-spec">' +
|
|
10334
|
+
'<span class="model-card-spec-label">' + s.label + '</span>' +
|
|
10335
|
+
'<span class="model-card-spec-value">' + escapeHtml(s.value) + '</span>' +
|
|
10336
|
+
'</div>';
|
|
10337
|
+
}).join('') +
|
|
10338
|
+
'</div>' +
|
|
10339
|
+
(tryActions ? '<div class="model-card-actions">' + tryActions + '</div>' : '') +
|
|
10340
|
+
'</div>';
|
|
10341
|
+
}
|
|
10342
|
+
|
|
10343
|
+
function renderModelsRtebChart() {
|
|
10344
|
+
var chart = document.getElementById('modelsRtebChart');
|
|
10345
|
+
var benchmarks = modelsTabData.benchmarks || [];
|
|
10346
|
+
if (!benchmarks.length) {
|
|
10347
|
+
chart.innerHTML = '<p style="color:var(--text-muted);font-size:13px;">Benchmark data unavailable.</p>';
|
|
10348
|
+
return;
|
|
10349
|
+
}
|
|
10350
|
+
var maxScore = Math.max.apply(null, benchmarks.map(function(b) { return b.score; }));
|
|
10351
|
+
var minDisplay = 55;
|
|
10352
|
+
|
|
10353
|
+
chart.innerHTML = benchmarks.map(function(b) {
|
|
10354
|
+
var isVoyage = b.provider === 'Voyage AI';
|
|
10355
|
+
var barPct = ((b.score - minDisplay) / (maxScore - minDisplay)) * 100;
|
|
10356
|
+
var color = isVoyage ? 'var(--accent)' : '#555';
|
|
10357
|
+
var labelColor = isVoyage ? 'var(--accent)' : 'var(--text-dim)';
|
|
10358
|
+
var fontWeight = isVoyage ? '600' : '400';
|
|
10359
|
+
|
|
10360
|
+
return '<div class="models-rteb-bar">' +
|
|
10361
|
+
'<span class="models-rteb-label" style="color:' + labelColor + ';font-weight:' + fontWeight + ';">' + escapeHtml(b.model) + '</span>' +
|
|
10362
|
+
'<div class="models-rteb-track">' +
|
|
10363
|
+
'<div class="models-rteb-fill" style="width:' + barPct.toFixed(1) + '%;background:' + color + ';' + (isVoyage ? '' : 'opacity:0.6;') + '"></div>' +
|
|
10364
|
+
'</div>' +
|
|
10365
|
+
'<span class="models-rteb-score" style="color:' + labelColor + ';">' + b.score.toFixed(2) + '</span>' +
|
|
10366
|
+
'</div>';
|
|
10367
|
+
}).join('');
|
|
10368
|
+
}
|
|
10369
|
+
|
|
10370
|
+
function scrollToModelCard(name) {
|
|
10371
|
+
var card = document.getElementById('model-card-' + name);
|
|
10372
|
+
if (card) {
|
|
10373
|
+
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
10374
|
+
card.style.outline = '2px solid var(--accent)';
|
|
10375
|
+
setTimeout(function() { card.style.outline = ''; }, 2000);
|
|
10376
|
+
}
|
|
10377
|
+
}
|
|
10378
|
+
|
|
10379
|
+
window.filterModels = function() {
|
|
10380
|
+
var q = document.getElementById('modelsSearch').value.toLowerCase().trim();
|
|
10381
|
+
document.querySelectorAll('.model-card').forEach(function(card) {
|
|
10382
|
+
if (!q) { card.style.display = ''; return; }
|
|
10383
|
+
var text = card.textContent.toLowerCase();
|
|
10384
|
+
card.style.display = text.includes(q) ? '' : 'none';
|
|
10385
|
+
});
|
|
10386
|
+
document.querySelectorAll('.models-group').forEach(function(group) {
|
|
10387
|
+
var visible = group.querySelectorAll('.model-card:not([style*="display: none"])');
|
|
10388
|
+
group.style.display = visible.length ? '' : 'none';
|
|
10389
|
+
});
|
|
10390
|
+
};
|
|
10391
|
+
|
|
10392
|
+
window.setModelFilter = function(filter) {
|
|
10393
|
+
modelsTabData.currentFilter = filter;
|
|
10394
|
+
document.querySelectorAll('.models-filter-pill').forEach(function(p) {
|
|
10395
|
+
p.classList.toggle('active', p.dataset.filter === filter);
|
|
10396
|
+
});
|
|
10397
|
+
document.querySelectorAll('.model-card').forEach(function(card) {
|
|
10398
|
+
if (filter === 'all') { card.style.display = ''; return; }
|
|
10399
|
+
var cardType = card.dataset.type;
|
|
10400
|
+
card.style.display = cardType === filter ? '' : 'none';
|
|
10401
|
+
});
|
|
10402
|
+
document.querySelectorAll('.models-group').forEach(function(group) {
|
|
10403
|
+
var visible = group.querySelectorAll('.model-card:not([style*="display: none"])');
|
|
10404
|
+
group.style.display = visible.length ? '' : 'none';
|
|
10405
|
+
});
|
|
10406
|
+
};
|
|
10407
|
+
|
|
10408
|
+
// ── Model Picker ──
|
|
10409
|
+
var modelPickerState = {
|
|
10410
|
+
isOpen: false,
|
|
10411
|
+
targetSelectId: null,
|
|
10412
|
+
context: null,
|
|
10413
|
+
selectedModel: null,
|
|
10414
|
+
previousFocus: null,
|
|
10415
|
+
modelType: 'embedding'
|
|
10416
|
+
};
|
|
10417
|
+
|
|
10418
|
+
function openModelPicker(selectId, context) {
|
|
10419
|
+
modelPickerState.isOpen = true;
|
|
10420
|
+
modelPickerState.targetSelectId = selectId;
|
|
10421
|
+
modelPickerState.context = context;
|
|
10422
|
+
modelPickerState.previousFocus = document.activeElement;
|
|
10423
|
+
|
|
10424
|
+
var sel = document.getElementById(selectId);
|
|
10425
|
+
modelPickerState.selectedModel = sel ? sel.value : null;
|
|
10426
|
+
modelPickerState.modelType = selectId === 'searchRerankModel' ? 'reranking' : 'embedding';
|
|
10427
|
+
|
|
10428
|
+
var contextHints = {
|
|
10429
|
+
embed: 'Choose an embedding model for vectorizing text.',
|
|
10430
|
+
compare: 'Choose a model for similarity scoring.',
|
|
10431
|
+
search: 'Choose an embedding model for document search.',
|
|
10432
|
+
multimodal: 'Only multimodal models are available here.'
|
|
10433
|
+
};
|
|
10434
|
+
document.getElementById('modelPickerContext').textContent = contextHints[context] || '';
|
|
10435
|
+
|
|
10436
|
+
var rec = document.getElementById('modelPickerRecommendation');
|
|
10437
|
+
var recommendations = {
|
|
10438
|
+
embed: 'Tip: voyage-4 offers balanced quality and cost.',
|
|
10439
|
+
compare: 'Tip: voyage-4-large gives highest accuracy comparisons.',
|
|
10440
|
+
search: 'Tip: Use voyage-4-large for docs, voyage-4-lite for queries (shared space saves cost).',
|
|
10441
|
+
multimodal: 'Tip: voyage-multimodal-3.5 supports text, images, and video.'
|
|
10442
|
+
};
|
|
10443
|
+
if (recommendations[context]) {
|
|
10444
|
+
rec.textContent = recommendations[context];
|
|
10445
|
+
rec.classList.add('visible');
|
|
10446
|
+
} else {
|
|
10447
|
+
rec.classList.remove('visible');
|
|
10448
|
+
}
|
|
10449
|
+
|
|
10450
|
+
renderModelPickerList();
|
|
10451
|
+
document.getElementById('modelPickerOverlay').classList.add('open');
|
|
10452
|
+
setTimeout(function() {
|
|
10453
|
+
document.getElementById('modelPickerSearch').focus();
|
|
10454
|
+
}, 100);
|
|
10455
|
+
}
|
|
10456
|
+
|
|
10457
|
+
function closeModelPicker() {
|
|
10458
|
+
modelPickerState.isOpen = false;
|
|
10459
|
+
document.getElementById('modelPickerOverlay').classList.remove('open');
|
|
10460
|
+
document.getElementById('modelPickerSearch').value = '';
|
|
10461
|
+
if (modelPickerState.previousFocus) {
|
|
10462
|
+
modelPickerState.previousFocus.focus();
|
|
10463
|
+
modelPickerState.previousFocus = null;
|
|
10464
|
+
}
|
|
10465
|
+
}
|
|
10466
|
+
|
|
10467
|
+
function selectModelFromPicker(name) {
|
|
10468
|
+
var sel = document.getElementById(modelPickerState.targetSelectId);
|
|
10469
|
+
if (sel) {
|
|
10470
|
+
sel.value = name;
|
|
10471
|
+
sel.dispatchEvent(new Event('change'));
|
|
10472
|
+
localStorage.setItem('vai-playground-model', name);
|
|
10473
|
+
if (modelPickerState.modelType === 'embedding') {
|
|
10474
|
+
['embedModel', 'compareModel', 'searchEmbedModel'].forEach(function(id) {
|
|
10475
|
+
var otherSel = document.getElementById(id);
|
|
10476
|
+
if (otherSel && otherSel !== sel) otherSel.value = name;
|
|
10477
|
+
});
|
|
10478
|
+
}
|
|
10479
|
+
}
|
|
10480
|
+
closeModelPicker();
|
|
10481
|
+
sendTelemetry('model_picker_select', { model: name, context: modelPickerState.context });
|
|
10482
|
+
}
|
|
10483
|
+
|
|
10484
|
+
function renderModelPickerList() {
|
|
10485
|
+
var list = document.getElementById('modelPickerList');
|
|
10486
|
+
var type = modelPickerState.modelType;
|
|
10487
|
+
var context = modelPickerState.context;
|
|
10488
|
+
var selected = modelPickerState.selectedModel;
|
|
10489
|
+
|
|
10490
|
+
var models;
|
|
10491
|
+
if (context === 'multimodal') {
|
|
10492
|
+
models = allModels.filter(function(m) { return m.multimodal; });
|
|
10493
|
+
} else if (type === 'reranking') {
|
|
10494
|
+
models = rerankModels;
|
|
10495
|
+
} else {
|
|
10496
|
+
models = embedModels;
|
|
10497
|
+
}
|
|
10498
|
+
|
|
10499
|
+
list.innerHTML = models.map(function(m) {
|
|
10500
|
+
var isSelected = m.name === selected;
|
|
10501
|
+
var tags = [];
|
|
10502
|
+
if (m.architecture === 'moe') tags.push({text: 'MoE', cls: 'model-picker-item-tag'});
|
|
10503
|
+
if (m.sharedSpace) tags.push({text: 'Shared Space', cls: 'model-picker-item-shared'});
|
|
10504
|
+
if (m.rtebScore) tags.push({text: 'RTEB ' + m.rtebScore, cls: 'model-picker-item-tag'});
|
|
10505
|
+
|
|
10506
|
+
var meta = [m.bestFor, m.context, m.price].filter(Boolean).join(' \u00b7 ');
|
|
10507
|
+
|
|
10508
|
+
return '<div class="model-picker-item ' + (isSelected ? 'selected' : '') + '" ' +
|
|
10509
|
+
'role="option" aria-selected="' + isSelected + '" tabindex="0" ' +
|
|
10510
|
+
'data-model="' + m.name + '" ' +
|
|
10511
|
+
'onclick="selectModelFromPicker(\'' + m.name + '\')" ' +
|
|
10512
|
+
'onkeydown="if(event.key===\'Enter\') selectModelFromPicker(\'' + m.name + '\')">' +
|
|
10513
|
+
'<div class="model-picker-item-radio"></div>' +
|
|
10514
|
+
'<div class="model-picker-item-info">' +
|
|
10515
|
+
'<div class="model-picker-item-name">' + escapeHtml(m.name) + '</div>' +
|
|
10516
|
+
'<div class="model-picker-item-meta">' + escapeHtml(meta) + '</div>' +
|
|
10517
|
+
(tags.length ? '<div class="model-picker-item-tags">' +
|
|
10518
|
+
tags.map(function(t) { return '<span class="' + t.cls + '">' + t.text + '</span>'; }).join('') +
|
|
10519
|
+
'</div>' : '') +
|
|
10520
|
+
'</div>' +
|
|
10521
|
+
'</div>';
|
|
10522
|
+
}).join('');
|
|
10523
|
+
}
|
|
10524
|
+
|
|
10525
|
+
window.filterModelPicker = function() {
|
|
10526
|
+
var q = document.getElementById('modelPickerSearch').value.toLowerCase().trim();
|
|
10527
|
+
document.querySelectorAll('#modelPickerList .model-picker-item').forEach(function(item) {
|
|
10528
|
+
if (!q) { item.style.display = ''; return; }
|
|
10529
|
+
var text = item.textContent.toLowerCase();
|
|
10530
|
+
item.style.display = text.includes(q) ? '' : 'none';
|
|
10531
|
+
});
|
|
10532
|
+
};
|
|
10533
|
+
|
|
10534
|
+
function enhanceModelSelects() {
|
|
10535
|
+
var selectConfigs = [
|
|
10536
|
+
{ id: 'embedModel', context: 'embed' },
|
|
10537
|
+
{ id: 'compareModel', context: 'compare' },
|
|
10538
|
+
{ id: 'searchEmbedModel', context: 'search' },
|
|
10539
|
+
{ id: 'searchRerankModel', context: 'search' }
|
|
10540
|
+
];
|
|
10541
|
+
|
|
10542
|
+
selectConfigs.forEach(function(cfg) {
|
|
10543
|
+
var sel = document.getElementById(cfg.id);
|
|
10544
|
+
if (!sel || sel.dataset.pickerEnhanced) return;
|
|
10545
|
+
sel.dataset.pickerEnhanced = 'true';
|
|
10546
|
+
|
|
10547
|
+
var pickerBtn = document.createElement('button');
|
|
10548
|
+
pickerBtn.className = 'model-picker-trigger';
|
|
10549
|
+
pickerBtn.type = 'button';
|
|
10550
|
+
pickerBtn.title = 'Browse models';
|
|
10551
|
+
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>';
|
|
10552
|
+
pickerBtn.addEventListener('click', function(e) {
|
|
10553
|
+
e.preventDefault();
|
|
10554
|
+
e.stopPropagation();
|
|
10555
|
+
openModelPicker(cfg.id, cfg.context);
|
|
10556
|
+
});
|
|
10557
|
+
|
|
10558
|
+
sel.parentNode.insertBefore(pickerBtn, sel.nextSibling);
|
|
10559
|
+
});
|
|
10560
|
+
}
|
|
10561
|
+
|
|
10562
|
+
// Model picker close handlers
|
|
10563
|
+
document.getElementById('modelPickerClose').addEventListener('click', closeModelPicker);
|
|
10564
|
+
document.getElementById('modelPickerOverlay').addEventListener('click', function(e) {
|
|
10565
|
+
if (e.target === document.getElementById('modelPickerOverlay')) closeModelPicker();
|
|
10566
|
+
});
|
|
10567
|
+
document.addEventListener('keydown', function(e) {
|
|
10568
|
+
if (e.key === 'Escape' && modelPickerState.isOpen) {
|
|
10569
|
+
e.preventDefault();
|
|
10570
|
+
closeModelPicker();
|
|
10571
|
+
}
|
|
10572
|
+
});
|
|
10573
|
+
|
|
9544
10574
|
// ── Benchmark: Sub-panel switching ──
|
|
9545
10575
|
document.querySelectorAll('.bench-panel-btn').forEach(btn => {
|
|
9546
10576
|
btn.addEventListener('click', () => {
|
|
@@ -11065,7 +12095,23 @@ function checkForAppUpdate() {
|
|
|
11065
12095
|
downloadBtn.style.display = 'none';
|
|
11066
12096
|
bannerText.style.display = 'none';
|
|
11067
12097
|
progressWrap.style.display = 'flex';
|
|
12098
|
+
// Timeout: if no progress after 30s, offer manual download
|
|
12099
|
+
let gotProgress = false;
|
|
12100
|
+
const dlTimeout = setTimeout(() => {
|
|
12101
|
+
if (!gotProgress) {
|
|
12102
|
+
progressWrap.style.display = 'none';
|
|
12103
|
+
bannerText.innerHTML = 'Download stalled. <a href="#" onclick="window.vai.updates.openRelease(\'' + result.releaseUrl + '\');return false;" style="color:var(--accent);">Download manually</a>';
|
|
12104
|
+
bannerText.style.display = '';
|
|
12105
|
+
}
|
|
12106
|
+
}, 30000);
|
|
12107
|
+
// Listen for first progress event to cancel timeout
|
|
12108
|
+
const unsub = window.vai.updates.onEvent((d) => {
|
|
12109
|
+
if (d.event === 'download-progress') { gotProgress = true; clearTimeout(dlTimeout); unsub(); }
|
|
12110
|
+
if (d.event === 'update-error') { clearTimeout(dlTimeout); unsub(); }
|
|
12111
|
+
if (d.event === 'update-downloaded') { clearTimeout(dlTimeout); unsub(); }
|
|
12112
|
+
});
|
|
11068
12113
|
window.vai.updates.download().catch(() => {
|
|
12114
|
+
clearTimeout(dlTimeout);
|
|
11069
12115
|
// Fallback to manual download
|
|
11070
12116
|
window.vai.updates.openRelease(result.releaseUrl);
|
|
11071
12117
|
});
|
|
@@ -11344,6 +12390,7 @@ function initOnboarding() {
|
|
|
11344
12390
|
|
|
11345
12391
|
// ── Multimodal Tab ──
|
|
11346
12392
|
let mmImageData = null; // base64 data URL of the uploaded image
|
|
12393
|
+
let mmVideoData = null; // base64 data URL of the uploaded video
|
|
11347
12394
|
let mmGalleryImages = []; // array of { dataUrl, name, size }
|
|
11348
12395
|
let mmSearchMode = 'text';
|
|
11349
12396
|
let mmSearchImageIndex = -1;
|
|
@@ -11414,6 +12461,59 @@ function initMultimodal() {
|
|
|
11414
12461
|
}
|
|
11415
12462
|
});
|
|
11416
12463
|
|
|
12464
|
+
// Video drop zone — the hidden <input type="file"> lives inside the same
|
|
12465
|
+
// card, so its programmatic .click() bubbles back up through the DOM and
|
|
12466
|
+
// re-triggers the dropzone click handler, creating an infinite dialog loop.
|
|
12467
|
+
// Fix: stopPropagation on the input so its click never reaches the dropzone,
|
|
12468
|
+
// plus a re-trigger guard identical to the image dropzone.
|
|
12469
|
+
const videoDropZone = document.getElementById('mmVideoDropZone');
|
|
12470
|
+
const videoFileInput = document.getElementById('mmVideoFileInput');
|
|
12471
|
+
|
|
12472
|
+
videoFileInput.addEventListener('click', (e) => e.stopPropagation());
|
|
12473
|
+
|
|
12474
|
+
let videoDialogOpen = false;
|
|
12475
|
+
videoDropZone.addEventListener('click', async () => {
|
|
12476
|
+
if (videoDialogOpen) return;
|
|
12477
|
+
videoDialogOpen = true;
|
|
12478
|
+
try {
|
|
12479
|
+
if (window.vai && window.vai.isElectron && window.vai.openVideoDialog) {
|
|
12480
|
+
const result = await window.vai.openVideoDialog();
|
|
12481
|
+
if (!result.canceled && result.dataUrl) {
|
|
12482
|
+
handleMultimodalVideoFromData(result.dataUrl, result.name, result.size);
|
|
12483
|
+
}
|
|
12484
|
+
} else {
|
|
12485
|
+
videoFileInput.click();
|
|
12486
|
+
}
|
|
12487
|
+
} finally {
|
|
12488
|
+
setTimeout(() => { videoDialogOpen = false; }, 300);
|
|
12489
|
+
}
|
|
12490
|
+
});
|
|
12491
|
+
videoFileInput.addEventListener('change', (e) => {
|
|
12492
|
+
videoDialogOpen = false;
|
|
12493
|
+
if (e.target.files && e.target.files[0]) handleMultimodalVideo(e.target.files[0]);
|
|
12494
|
+
videoFileInput.value = '';
|
|
12495
|
+
});
|
|
12496
|
+
|
|
12497
|
+
['dragenter', 'dragover'].forEach(evt => {
|
|
12498
|
+
videoDropZone.addEventListener(evt, (e) => {
|
|
12499
|
+
e.preventDefault();
|
|
12500
|
+
e.stopPropagation();
|
|
12501
|
+
videoDropZone.classList.add('drag-active');
|
|
12502
|
+
});
|
|
12503
|
+
});
|
|
12504
|
+
['dragleave', 'drop'].forEach(evt => {
|
|
12505
|
+
videoDropZone.addEventListener(evt, (e) => {
|
|
12506
|
+
e.preventDefault();
|
|
12507
|
+
e.stopPropagation();
|
|
12508
|
+
videoDropZone.classList.remove('drag-active');
|
|
12509
|
+
});
|
|
12510
|
+
});
|
|
12511
|
+
videoDropZone.addEventListener('drop', (e) => {
|
|
12512
|
+
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
|
12513
|
+
handleMultimodalVideo(e.dataTransfer.files[0]);
|
|
12514
|
+
}
|
|
12515
|
+
});
|
|
12516
|
+
|
|
11417
12517
|
// Gallery
|
|
11418
12518
|
renderGalleryGrid();
|
|
11419
12519
|
|
|
@@ -11484,22 +12584,155 @@ window.clearMultimodalImage = function() {
|
|
|
11484
12584
|
document.getElementById('mmFileInput').value = '';
|
|
11485
12585
|
};
|
|
11486
12586
|
|
|
12587
|
+
async function handleMultimodalVideoFromData(dataUrl, name, size) {
|
|
12588
|
+
mmVideoData = dataUrl;
|
|
12589
|
+
const video = document.getElementById('mmPreviewVideo');
|
|
12590
|
+
video.src = mmVideoData;
|
|
12591
|
+
|
|
12592
|
+
const info = document.getElementById('mmVideoFileInfo');
|
|
12593
|
+
const sizeStr = size > 1024 * 1024
|
|
12594
|
+
? (size / (1024 * 1024)).toFixed(1) + ' MB'
|
|
12595
|
+
: (size / 1024).toFixed(0) + ' KB';
|
|
12596
|
+
|
|
12597
|
+
document.getElementById('mmVideoDropZone').style.display = 'none';
|
|
12598
|
+
document.getElementById('mmVideoPreview').classList.add('visible');
|
|
12599
|
+
hideError('mmError');
|
|
12600
|
+
|
|
12601
|
+
// Estimate tokens from video metadata
|
|
12602
|
+
const meta = await estimateVideoTokens(video);
|
|
12603
|
+
if (meta) {
|
|
12604
|
+
const durStr = meta.duration.toFixed(1) + 's';
|
|
12605
|
+
const resStr = meta.width + '×' + meta.height;
|
|
12606
|
+
info.textContent = `${name} · ${sizeStr} · ${resStr} · ${durStr} · ~${meta.tokens.toLocaleString()} tokens`;
|
|
12607
|
+
if (meta.tokens > 32000) {
|
|
12608
|
+
showError('mmError',
|
|
12609
|
+
'This video is estimated at ~' + meta.tokens.toLocaleString() + ' tokens, which exceeds the 32,000 token context window. ' +
|
|
12610
|
+
'Try a shorter clip, lower resolution, or smaller dimensions. ' +
|
|
12611
|
+
'(' + resStr + ', ' + durStr + ')'
|
|
12612
|
+
);
|
|
12613
|
+
}
|
|
12614
|
+
} else {
|
|
12615
|
+
info.textContent = `${name} · ${sizeStr}`;
|
|
12616
|
+
}
|
|
12617
|
+
}
|
|
12618
|
+
|
|
12619
|
+
// Estimate video tokens: total pixels across all frames / 1120 pixels per token
|
|
12620
|
+
// The playground server downsamples video to 1fps before sending to Voyage AI,
|
|
12621
|
+
// so we estimate based on 1fps which matches what will actually be sent.
|
|
12622
|
+
function estimateVideoTokens(videoEl) {
|
|
12623
|
+
return new Promise((resolve) => {
|
|
12624
|
+
const checkMeta = () => {
|
|
12625
|
+
const w = videoEl.videoWidth;
|
|
12626
|
+
const h = videoEl.videoHeight;
|
|
12627
|
+
const dur = videoEl.duration;
|
|
12628
|
+
if (w && h && dur && isFinite(dur)) {
|
|
12629
|
+
// Server downsamples to 1fps before API call
|
|
12630
|
+
const assumedFps = 1;
|
|
12631
|
+
const frames = Math.ceil(dur * assumedFps);
|
|
12632
|
+
const totalPixels = w * h * frames;
|
|
12633
|
+
const tokens = Math.ceil(totalPixels / 1120);
|
|
12634
|
+
resolve({ width: w, height: h, duration: dur, frames, totalPixels, tokens });
|
|
12635
|
+
} else {
|
|
12636
|
+
resolve(null);
|
|
12637
|
+
}
|
|
12638
|
+
};
|
|
12639
|
+
if (videoEl.readyState >= 1) {
|
|
12640
|
+
checkMeta();
|
|
12641
|
+
} else {
|
|
12642
|
+
videoEl.addEventListener('loadedmetadata', checkMeta, { once: true });
|
|
12643
|
+
// Timeout fallback
|
|
12644
|
+
setTimeout(() => resolve(null), 3000);
|
|
12645
|
+
}
|
|
12646
|
+
});
|
|
12647
|
+
}
|
|
12648
|
+
|
|
12649
|
+
function handleMultimodalVideo(file) {
|
|
12650
|
+
const VALID_TYPES = ['video/mp4', 'video/webm', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska'];
|
|
12651
|
+
if (!VALID_TYPES.includes(file.type) && !file.name.match(/\.(mp4|webm|mov|avi|mkv)$/i)) {
|
|
12652
|
+
showError('mmError', 'Unsupported video type. Use MP4, WebM, MOV, AVI, or MKV.');
|
|
12653
|
+
return;
|
|
12654
|
+
}
|
|
12655
|
+
if (file.size > 20 * 1024 * 1024) {
|
|
12656
|
+
showError('mmError', 'Video too large. Maximum size is 20 MB.');
|
|
12657
|
+
return;
|
|
12658
|
+
}
|
|
12659
|
+
hideError('mmError');
|
|
12660
|
+
|
|
12661
|
+
const reader = new FileReader();
|
|
12662
|
+
reader.onload = async (e) => {
|
|
12663
|
+
mmVideoData = e.target.result;
|
|
12664
|
+
const video = document.getElementById('mmPreviewVideo');
|
|
12665
|
+
video.src = mmVideoData;
|
|
12666
|
+
|
|
12667
|
+
const info = document.getElementById('mmVideoFileInfo');
|
|
12668
|
+
const sizeStr = file.size > 1024 * 1024
|
|
12669
|
+
? (file.size / (1024 * 1024)).toFixed(1) + ' MB'
|
|
12670
|
+
: (file.size / 1024).toFixed(0) + ' KB';
|
|
12671
|
+
|
|
12672
|
+
document.getElementById('mmVideoDropZone').style.display = 'none';
|
|
12673
|
+
document.getElementById('mmVideoPreview').classList.add('visible');
|
|
12674
|
+
|
|
12675
|
+
// Estimate tokens from video metadata and warn if likely to exceed limit
|
|
12676
|
+
const meta = await estimateVideoTokens(video);
|
|
12677
|
+
if (meta) {
|
|
12678
|
+
const durStr = meta.duration.toFixed(1) + 's';
|
|
12679
|
+
const resStr = meta.width + '×' + meta.height;
|
|
12680
|
+
info.textContent = `${file.name} · ${sizeStr} · ${resStr} · ${durStr} · ~${meta.tokens.toLocaleString()} tokens`;
|
|
12681
|
+
if (meta.tokens > 32000) {
|
|
12682
|
+
showError('mmError',
|
|
12683
|
+
'This video is estimated at ~' + meta.tokens.toLocaleString() + ' tokens, which exceeds the 32,000 token context window. ' +
|
|
12684
|
+
'Try a shorter clip, lower resolution, or smaller dimensions. ' +
|
|
12685
|
+
'(' + resStr + ', ' + durStr + ')'
|
|
12686
|
+
);
|
|
12687
|
+
}
|
|
12688
|
+
} else {
|
|
12689
|
+
info.textContent = `${file.name} · ${sizeStr}`;
|
|
12690
|
+
}
|
|
12691
|
+
};
|
|
12692
|
+
reader.readAsDataURL(file);
|
|
12693
|
+
}
|
|
12694
|
+
|
|
12695
|
+
window.clearMultimodalVideo = function() {
|
|
12696
|
+
mmVideoData = null;
|
|
12697
|
+
const video = document.getElementById('mmPreviewVideo');
|
|
12698
|
+
video.pause();
|
|
12699
|
+
video.src = '';
|
|
12700
|
+
document.getElementById('mmVideoFileInfo').textContent = '';
|
|
12701
|
+
document.getElementById('mmVideoPreview').classList.remove('visible');
|
|
12702
|
+
document.getElementById('mmVideoDropZone').style.display = '';
|
|
12703
|
+
document.getElementById('mmVideoFileInput').value = '';
|
|
12704
|
+
};
|
|
12705
|
+
|
|
11487
12706
|
window.doMultimodalCompare = async function() {
|
|
11488
12707
|
hideError('mmError');
|
|
11489
12708
|
sendTelemetry('api_call', { endpoint: 'multimodal-compare', model: document.getElementById('mmModel').value });
|
|
11490
12709
|
const text = document.getElementById('mmText').value.trim();
|
|
11491
|
-
|
|
11492
|
-
if (!text) { showError('mmError', '
|
|
12710
|
+
const hasMedia = mmImageData || mmVideoData;
|
|
12711
|
+
if (!hasMedia && !text) { showError('mmError', 'Upload an image or video, and enter text to compare'); return; }
|
|
12712
|
+
|
|
12713
|
+
// Need at least 2 inputs to compare
|
|
12714
|
+
const mediaInputs = [];
|
|
12715
|
+
if (mmImageData) {
|
|
12716
|
+
mediaInputs.push({ content: [{ type: 'image_base64', image_base64: mmImageData }], label: 'Image' });
|
|
12717
|
+
}
|
|
12718
|
+
if (mmVideoData) {
|
|
12719
|
+
mediaInputs.push({ content: [{ type: 'video_base64', video_base64: mmVideoData }], label: 'Video' });
|
|
12720
|
+
}
|
|
12721
|
+
if (text) {
|
|
12722
|
+
mediaInputs.push({ content: [{ type: 'text', text: text }], label: 'Text' });
|
|
12723
|
+
}
|
|
12724
|
+
|
|
12725
|
+
if (mediaInputs.length < 2) {
|
|
12726
|
+
showError('mmError', 'Provide at least 2 inputs to compare (e.g., image + text, video + text, or image + video)');
|
|
12727
|
+
return;
|
|
12728
|
+
}
|
|
11493
12729
|
|
|
11494
12730
|
setLoading('mmCompareBtn', true);
|
|
11495
12731
|
try {
|
|
11496
12732
|
const model = document.getElementById('mmModel').value;
|
|
11497
12733
|
const dimsVal = document.getElementById('mmDimensions').value;
|
|
11498
12734
|
const body = {
|
|
11499
|
-
inputs:
|
|
11500
|
-
{ content: [{ type: 'image_base64', image_base64: mmImageData }] },
|
|
11501
|
-
{ content: [{ type: 'text', text: text }] }
|
|
11502
|
-
],
|
|
12735
|
+
inputs: mediaInputs.map(m => m.content ? { content: m.content } : m),
|
|
11503
12736
|
model: model,
|
|
11504
12737
|
input_type: 'document'
|
|
11505
12738
|
};
|
|
@@ -11507,6 +12740,7 @@ window.doMultimodalCompare = async function() {
|
|
|
11507
12740
|
|
|
11508
12741
|
const data = await apiPost('/api/multimodal-embed', body);
|
|
11509
12742
|
|
|
12743
|
+
// Compare first two inputs (primary comparison)
|
|
11510
12744
|
const vecA = data.data[0].embedding;
|
|
11511
12745
|
const vecB = data.data[1].embedding;
|
|
11512
12746
|
const cosine = cosineSim(vecA, vecB);
|
|
@@ -11528,29 +12762,46 @@ window.doMultimodalCompare = async function() {
|
|
|
11528
12762
|
|
|
11529
12763
|
// Stats
|
|
11530
12764
|
const usage = data.usage || {};
|
|
11531
|
-
const
|
|
11532
|
-
|
|
12765
|
+
const pairLabel = mediaInputs[0].label + ' vs ' + mediaInputs[1].label;
|
|
12766
|
+
let statsHtml = `
|
|
11533
12767
|
<span class="stat"><span class="stat-label">Model</span><span class="stat-value">${data.model || model}</span></span>
|
|
12768
|
+
<span class="stat"><span class="stat-label">Comparing</span><span class="stat-value">${pairLabel}</span></span>
|
|
11534
12769
|
<span class="stat"><span class="stat-label">Dimensions</span><span class="stat-value">${vecA.length}</span></span>
|
|
11535
|
-
<span class="stat"><span class="stat-label">Text Tokens</span><span class="stat-value">${usage.text_tokens || '—'}</span></span>
|
|
11536
|
-
<span class="stat"><span class="stat-label">Image Pixels</span><span class="stat-value">${usage.image_pixels ? usage.image_pixels.toLocaleString() : '—'}</span></span>
|
|
11537
|
-
<span class="stat"><span class="stat-label">Total Tokens</span><span class="stat-value">${usage.total_tokens || '—'}</span></span>
|
|
11538
12770
|
`;
|
|
12771
|
+
if (usage.text_tokens) statsHtml += `<span class="stat"><span class="stat-label">Text Tokens</span><span class="stat-value">${usage.text_tokens}</span></span>`;
|
|
12772
|
+
if (usage.image_pixels) statsHtml += `<span class="stat"><span class="stat-label">Image Pixels</span><span class="stat-value">${usage.image_pixels.toLocaleString()}</span></span>`;
|
|
12773
|
+
statsHtml += `<span class="stat"><span class="stat-label">Total Tokens</span><span class="stat-value">${usage.total_tokens || '—'}</span></span>`;
|
|
12774
|
+
|
|
12775
|
+
// If 3 inputs, show all pairwise similarities
|
|
12776
|
+
if (mediaInputs.length === 3) {
|
|
12777
|
+
const vecC = data.data[2].embedding;
|
|
12778
|
+
const simAC = cosineSim(vecA, vecC);
|
|
12779
|
+
const simBC = cosineSim(vecB, vecC);
|
|
12780
|
+
statsHtml += `<br><span class="stat"><span class="stat-label">${mediaInputs[0].label} vs ${mediaInputs[2].label}</span><span class="stat-value">${simAC.toFixed(4)}</span></span>`;
|
|
12781
|
+
statsHtml += `<span class="stat"><span class="stat-label">${mediaInputs[1].label} vs ${mediaInputs[2].label}</span><span class="stat-value">${simBC.toFixed(4)}</span></span>`;
|
|
12782
|
+
}
|
|
12783
|
+
|
|
12784
|
+
document.getElementById('mmStats').innerHTML = statsHtml;
|
|
11539
12785
|
|
|
11540
12786
|
// Insight note
|
|
11541
12787
|
const noteEl = document.getElementById('mmNote');
|
|
11542
12788
|
if (cosine > 0.7) {
|
|
11543
|
-
noteEl.innerHTML = '💡 <strong>High similarity!</strong> The
|
|
12789
|
+
noteEl.innerHTML = '💡 <strong>High similarity!</strong> The inputs are closely related in Voyage AI\'s multimodal embedding space.';
|
|
11544
12790
|
} else if (cosine > 0.4) {
|
|
11545
|
-
noteEl.innerHTML = '💡 <strong>Moderate similarity.</strong> The
|
|
12791
|
+
noteEl.innerHTML = '💡 <strong>Moderate similarity.</strong> The inputs share some semantic overlap but are not a direct match.';
|
|
11546
12792
|
} else {
|
|
11547
|
-
noteEl.innerHTML = '💡 <strong>Low similarity.</strong> The
|
|
12793
|
+
noteEl.innerHTML = '💡 <strong>Low similarity.</strong> The inputs are semantically distant. Try content that is more closely related.';
|
|
11548
12794
|
}
|
|
11549
12795
|
|
|
11550
12796
|
document.getElementById('mmResult').classList.add('visible');
|
|
11551
12797
|
CostTracker.addOperation('multimodal-compare', data.model || model, usage.total_tokens || 0);
|
|
11552
12798
|
} catch (err) {
|
|
11553
|
-
|
|
12799
|
+
let msg = err.message;
|
|
12800
|
+
// Provide actionable guidance for context window errors
|
|
12801
|
+
if (msg.includes('context window') || msg.includes('exceed')) {
|
|
12802
|
+
msg += ' Try reducing video resolution, trimming to a shorter clip, or using a smaller image.';
|
|
12803
|
+
}
|
|
12804
|
+
showError('mmError', msg);
|
|
11554
12805
|
} finally {
|
|
11555
12806
|
setLoading('mmCompareBtn', false);
|
|
11556
12807
|
}
|
|
@@ -11754,7 +13005,11 @@ window.doMultimodalSearch = async function() {
|
|
|
11754
13005
|
document.getElementById('mmSearchResult').classList.add('visible');
|
|
11755
13006
|
CostTracker.addOperation('multimodal-search', model, data.usage?.total_tokens || 0);
|
|
11756
13007
|
} catch (err) {
|
|
11757
|
-
|
|
13008
|
+
let msg = err.message;
|
|
13009
|
+
if (msg.includes('context window') || msg.includes('exceed')) {
|
|
13010
|
+
msg += ' Try using smaller images or fewer corpus items per batch.';
|
|
13011
|
+
}
|
|
13012
|
+
showError('mmSearchError', msg);
|
|
11758
13013
|
} finally {
|
|
11759
13014
|
setLoading(btnId, false);
|
|
11760
13015
|
}
|
|
@@ -13345,6 +14600,12 @@ const WF_NODE_META = {
|
|
|
13345
14600
|
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
14601
|
aggregate: { icon: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z', label: 'Aggregate', color: '#00D4AA', category: 'processing' },
|
|
13347
14602
|
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' },
|
|
14603
|
+
// Code search tools (voyage-code-3)
|
|
14604
|
+
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' },
|
|
14605
|
+
code_search: { icon: 'M10 20l4-16M18 8l4 4-4 4M6 16l-4-4 4-4', label: 'Code Search', color: '#06B6D4', category: 'code' },
|
|
14606
|
+
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' },
|
|
14607
|
+
code_find_similar: { icon: 'M16 3h5v5M8 3H3v5M3 16v5h5M21 16v5h-5M12 8v8M8 12h8', label: 'Find Similar', color: '#06B6D4', category: 'code' },
|
|
14608
|
+
code_status: { icon: 'M22 12h-4l-3 9L9 3l-3 9H2', label: 'Code Status', color: '#06B6D4', category: 'code' },
|
|
13348
14609
|
};
|
|
13349
14610
|
|
|
13350
14611
|
// Fallback icon (gear) for unknown workflow node types
|
|
@@ -13378,6 +14639,7 @@ let wfState = {
|
|
|
13378
14639
|
draggingEdge: null,
|
|
13379
14640
|
dragNode: null,
|
|
13380
14641
|
dirtyFlag: false,
|
|
14642
|
+
outputFormat: 'auto',
|
|
13381
14643
|
};
|
|
13382
14644
|
|
|
13383
14645
|
// ── Library ──
|
|
@@ -13559,6 +14821,16 @@ function wfCloseInstallDialog() {
|
|
|
13559
14821
|
if (modal) modal.style.display = 'none';
|
|
13560
14822
|
}
|
|
13561
14823
|
|
|
14824
|
+
function wfCompareVersions(a, b) {
|
|
14825
|
+
const pa = (a || '0.0.0').split('.').map(Number);
|
|
14826
|
+
const pb = (b || '0.0.0').split('.').map(Number);
|
|
14827
|
+
for (let i = 0; i < 3; i++) {
|
|
14828
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
14829
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
14830
|
+
}
|
|
14831
|
+
return 0;
|
|
14832
|
+
}
|
|
14833
|
+
|
|
13562
14834
|
async function wfSearchNpm() {
|
|
13563
14835
|
const query = document.getElementById('wfInstallSearch').value.trim();
|
|
13564
14836
|
const results = document.getElementById('wfInstallResults');
|
|
@@ -13574,15 +14846,27 @@ async function wfSearchNpm() {
|
|
|
13574
14846
|
results.innerHTML = data.results.map(r => {
|
|
13575
14847
|
const isOfficial = r.name.startsWith('@vaicli/');
|
|
13576
14848
|
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
|
|
13578
|
-
const
|
|
13579
|
-
|
|
13580
|
-
|
|
14849
|
+
const installedPkg = [...(wfState.official||[]), ...(wfState.community||[])].find(w => w.name === r.name);
|
|
14850
|
+
const installed = !!installedPkg;
|
|
14851
|
+
const installedVersion = installedPkg?.version;
|
|
14852
|
+
const npmVersion = r.version;
|
|
14853
|
+
const hasUpdate = installed && npmVersion && installedVersion && npmVersion !== installedVersion && wfCompareVersions(npmVersion, installedVersion) > 0;
|
|
14854
|
+
let btn;
|
|
14855
|
+
if (hasUpdate) {
|
|
14856
|
+
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>`;
|
|
14857
|
+
} else if (installed) {
|
|
14858
|
+
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>';
|
|
14859
|
+
} else {
|
|
14860
|
+
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>`;
|
|
14861
|
+
}
|
|
14862
|
+
const versionInfo = hasUpdate
|
|
14863
|
+
? `v${installedVersion} → <span style="color:#F59E0B;font-weight:500;">v${npmVersion}</span>`
|
|
14864
|
+
: `v${npmVersion || '?'}`;
|
|
13581
14865
|
return `<div style="padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;justify-content:space-between;">
|
|
13582
14866
|
<div style="flex:1;min-width:0;">
|
|
13583
14867
|
<div style="font-size:13px;font-weight:500;">${r.name}${badge}</div>
|
|
13584
14868
|
<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;"
|
|
14869
|
+
<div style="font-size:10px;color:var(--text-muted);margin-top:2px;">${versionInfo}</div>
|
|
13586
14870
|
</div>
|
|
13587
14871
|
<div style="margin-left:12px;flex-shrink:0;">${btn}</div>
|
|
13588
14872
|
</div>`;
|
|
@@ -13598,7 +14882,7 @@ async function wfInstallPkg(name) {
|
|
|
13598
14882
|
const btns = results.querySelectorAll('button');
|
|
13599
14883
|
let clickedBtn = null;
|
|
13600
14884
|
btns.forEach(b => {
|
|
13601
|
-
if (b.textContent === 'Install' && !clickedBtn) {
|
|
14885
|
+
if ((b.textContent === 'Install' || b.textContent === 'Update') && !clickedBtn) {
|
|
13602
14886
|
// Find the button in the same row as the package name
|
|
13603
14887
|
const row = b.closest('div[style*="border-bottom"]');
|
|
13604
14888
|
if (row && row.textContent.includes(name)) {
|
|
@@ -13685,40 +14969,192 @@ function wfHandleFileLoad(event) {
|
|
|
13685
14969
|
alert('Invalid workflow: missing "steps" array');
|
|
13686
14970
|
return;
|
|
13687
14971
|
}
|
|
13688
|
-
// Validate via server
|
|
13689
|
-
const valRes = await fetch('/api/workflows/validate', {
|
|
13690
|
-
method: 'POST',
|
|
13691
|
-
headers: { 'Content-Type': 'application/json' },
|
|
13692
|
-
body: JSON.stringify({ definition }),
|
|
13693
|
-
});
|
|
13694
|
-
const valData = await valRes.json();
|
|
13695
|
-
if (!valData.valid) {
|
|
13696
|
-
alert('Workflow validation failed:\n' + (valData.errors || []).join('\n'));
|
|
13697
|
-
return;
|
|
14972
|
+
// Validate via server
|
|
14973
|
+
const valRes = await fetch('/api/workflows/validate', {
|
|
14974
|
+
method: 'POST',
|
|
14975
|
+
headers: { 'Content-Type': 'application/json' },
|
|
14976
|
+
body: JSON.stringify({ definition }),
|
|
14977
|
+
});
|
|
14978
|
+
const valData = await valRes.json();
|
|
14979
|
+
if (!valData.valid) {
|
|
14980
|
+
alert('Workflow validation failed:\n' + (valData.errors || []).join('\n'));
|
|
14981
|
+
return;
|
|
14982
|
+
}
|
|
14983
|
+
// Deselect any library item
|
|
14984
|
+
document.querySelectorAll('.wf-library-item.active').forEach(el => el.classList.remove('active'));
|
|
14985
|
+
// Load the workflow
|
|
14986
|
+
wfState.activeWorkflow = definition;
|
|
14987
|
+
wfState.selectedNodeId = null;
|
|
14988
|
+
wfState.executionState = {};
|
|
14989
|
+
wfState.executionResults = {};
|
|
14990
|
+
wfSetToolbarEnabled(true);
|
|
14991
|
+
document.getElementById('wfCanvasEmpty').style.display = 'none';
|
|
14992
|
+
wfHideExecStatus();
|
|
14993
|
+
await wfRenderWorkflow(definition);
|
|
14994
|
+
wfOpenInspector();
|
|
14995
|
+
wfUpdateInspector();
|
|
14996
|
+
} catch (err) {
|
|
14997
|
+
alert('Failed to parse workflow file: ' + err.message);
|
|
14998
|
+
}
|
|
14999
|
+
};
|
|
15000
|
+
reader.readAsText(file);
|
|
15001
|
+
// Reset file input so the same file can be re-loaded
|
|
15002
|
+
event.target.value = '';
|
|
15003
|
+
}
|
|
15004
|
+
|
|
15005
|
+
// ── DAG Layout + SVG Rendering ──
|
|
15006
|
+
/**
|
|
15007
|
+
* Sugiyama-style DAG auto-layout: barycenter crossing minimization + neighbor-aware placement.
|
|
15008
|
+
* Takes layers (from Kahn's topological sort) and graph (dependency map),
|
|
15009
|
+
* reorders nodes within each layer to minimize edge crossings,
|
|
15010
|
+
* then assigns Y coordinates so nodes sit near their connected neighbors.
|
|
15011
|
+
*
|
|
15012
|
+
* @param {string[][]} layers - layers[i] = [stepIds that can run in parallel]
|
|
15013
|
+
* @param {Object} graph - { stepId: [depStepIds...] } (incoming edges)
|
|
15014
|
+
* @returns {Object} positions - { stepId: { x, y } }
|
|
15015
|
+
*/
|
|
15016
|
+
function wfAutoLayout(layers, graph) {
|
|
15017
|
+
if (!layers || layers.length === 0) return {};
|
|
15018
|
+
|
|
15019
|
+
// Build adjacency: forward (outgoing) and backward (incoming) neighbor lists
|
|
15020
|
+
const incoming = {}; // stepId -> [ids in previous layers that connect to it]
|
|
15021
|
+
const outgoing = {}; // stepId -> [ids in next layers it connects to]
|
|
15022
|
+
const allIds = new Set(layers.flat());
|
|
15023
|
+
for (const id of allIds) { incoming[id] = []; outgoing[id] = []; }
|
|
15024
|
+
|
|
15025
|
+
for (const [stepId, deps] of Object.entries(graph)) {
|
|
15026
|
+
if (!deps || !Array.isArray(deps)) continue;
|
|
15027
|
+
for (const rawDep of deps) {
|
|
15028
|
+
const dep = rawDep.replace(/^!/, '');
|
|
15029
|
+
if (allIds.has(dep) && allIds.has(stepId)) {
|
|
15030
|
+
incoming[stepId].push(dep);
|
|
15031
|
+
outgoing[dep].push(stepId);
|
|
15032
|
+
}
|
|
15033
|
+
}
|
|
15034
|
+
}
|
|
15035
|
+
|
|
15036
|
+
// Index: which layer is each node in?
|
|
15037
|
+
const layerOf = {};
|
|
15038
|
+
layers.forEach((layer, li) => layer.forEach(id => { layerOf[id] = li; }));
|
|
15039
|
+
|
|
15040
|
+
// Work on mutable copies of each layer's ordering
|
|
15041
|
+
const ordered = layers.map(l => [...l]);
|
|
15042
|
+
|
|
15043
|
+
// Barycenter crossing minimization (multi-pass, alternating direction)
|
|
15044
|
+
// Barycenter = average position of a node's neighbors in the adjacent layer.
|
|
15045
|
+
// Sorting by barycenter within each layer reduces crossings.
|
|
15046
|
+
const NUM_PASSES = 4;
|
|
15047
|
+
|
|
15048
|
+
for (let pass = 0; pass < NUM_PASSES; pass++) {
|
|
15049
|
+
if (pass % 2 === 0) {
|
|
15050
|
+
// Forward pass: for each layer (left to right), sort by barycenter of incoming neighbors
|
|
15051
|
+
for (let li = 1; li < ordered.length; li++) {
|
|
15052
|
+
const prevOrder = {};
|
|
15053
|
+
ordered[li - 1].forEach((id, idx) => { prevOrder[id] = idx; });
|
|
15054
|
+
|
|
15055
|
+
ordered[li].sort((a, b) => {
|
|
15056
|
+
const baryA = wfBarycenter(a, incoming, prevOrder);
|
|
15057
|
+
const baryB = wfBarycenter(b, incoming, prevOrder);
|
|
15058
|
+
return baryA - baryB;
|
|
15059
|
+
});
|
|
15060
|
+
}
|
|
15061
|
+
} else {
|
|
15062
|
+
// Backward pass: for each layer (right to left), sort by barycenter of outgoing neighbors
|
|
15063
|
+
for (let li = ordered.length - 2; li >= 0; li--) {
|
|
15064
|
+
const nextOrder = {};
|
|
15065
|
+
ordered[li + 1].forEach((id, idx) => { nextOrder[id] = idx; });
|
|
15066
|
+
|
|
15067
|
+
ordered[li].sort((a, b) => {
|
|
15068
|
+
const baryA = wfBarycenter(a, outgoing, nextOrder);
|
|
15069
|
+
const baryB = wfBarycenter(b, outgoing, nextOrder);
|
|
15070
|
+
return baryA - baryB;
|
|
15071
|
+
});
|
|
15072
|
+
}
|
|
15073
|
+
}
|
|
15074
|
+
}
|
|
15075
|
+
|
|
15076
|
+
// Assign X coordinates (fixed per layer)
|
|
15077
|
+
// Assign Y coordinates using neighbor-aware placement:
|
|
15078
|
+
// First pass: assign evenly spaced Y (centered).
|
|
15079
|
+
// Second pass: nudge each node toward the average Y of its neighbors.
|
|
15080
|
+
const positions = {};
|
|
15081
|
+
const maxLayerSize = Math.max(...ordered.map(l => l.length));
|
|
15082
|
+
const totalH = maxLayerSize * (WF_NODE_H + WF_NODE_GAP);
|
|
15083
|
+
|
|
15084
|
+
// Initial even spacing (centered vertically)
|
|
15085
|
+
ordered.forEach((layer, li) => {
|
|
15086
|
+
const x = WF_PAD + li * WF_LAYER_GAP;
|
|
15087
|
+
const layerH = layer.length * WF_NODE_H + (layer.length - 1) * WF_NODE_GAP;
|
|
15088
|
+
const startY = WF_PAD + (totalH - layerH) / 2;
|
|
15089
|
+
layer.forEach((stepId, ni) => {
|
|
15090
|
+
positions[stepId] = { x, y: startY + ni * (WF_NODE_H + WF_NODE_GAP) };
|
|
15091
|
+
});
|
|
15092
|
+
});
|
|
15093
|
+
|
|
15094
|
+
// Neighbor-aware Y nudging (iterative relaxation)
|
|
15095
|
+
// Pulls nodes toward the average Y of their connected neighbors while maintaining order and minimum gap.
|
|
15096
|
+
const RELAX_PASSES = 3;
|
|
15097
|
+
const RELAX_STRENGTH = 0.4; // How much to move toward neighbor average (0=none, 1=full)
|
|
15098
|
+
|
|
15099
|
+
for (let rp = 0; rp < RELAX_PASSES; rp++) {
|
|
15100
|
+
for (let li = 0; li < ordered.length; li++) {
|
|
15101
|
+
const layer = ordered[li];
|
|
15102
|
+
if (layer.length <= 1) continue;
|
|
15103
|
+
|
|
15104
|
+
// Compute ideal Y for each node (average Y of all neighbors)
|
|
15105
|
+
const idealY = {};
|
|
15106
|
+
for (const id of layer) {
|
|
15107
|
+
const neighbors = [...(incoming[id] || []), ...(outgoing[id] || [])];
|
|
15108
|
+
if (neighbors.length === 0) { idealY[id] = positions[id].y; continue; }
|
|
15109
|
+
const avgY = neighbors.reduce((sum, nid) => sum + (positions[nid]?.y || 0), 0) / neighbors.length;
|
|
15110
|
+
idealY[id] = avgY;
|
|
15111
|
+
}
|
|
15112
|
+
|
|
15113
|
+
// Nudge toward ideal, then enforce minimum gap and original order
|
|
15114
|
+
for (const id of layer) {
|
|
15115
|
+
const current = positions[id].y;
|
|
15116
|
+
const target = idealY[id];
|
|
15117
|
+
positions[id].y = current + (target - current) * RELAX_STRENGTH;
|
|
15118
|
+
}
|
|
15119
|
+
|
|
15120
|
+
// Re-sort by Y to maintain the barycenter ordering, then enforce minimum gaps
|
|
15121
|
+
const sortedLayer = [...layer].sort((a, b) => positions[a].y - positions[b].y);
|
|
15122
|
+
const minGap = WF_NODE_H + WF_NODE_GAP;
|
|
15123
|
+
|
|
15124
|
+
// Push apart any overlaps (top-down)
|
|
15125
|
+
for (let i = 1; i < sortedLayer.length; i++) {
|
|
15126
|
+
const prev = positions[sortedLayer[i - 1]].y;
|
|
15127
|
+
const curr = positions[sortedLayer[i]].y;
|
|
15128
|
+
if (curr - prev < minGap) {
|
|
15129
|
+
positions[sortedLayer[i]].y = prev + minGap;
|
|
15130
|
+
}
|
|
15131
|
+
}
|
|
15132
|
+
|
|
15133
|
+
// Re-center the layer vertically to keep it balanced
|
|
15134
|
+
const layerTop = positions[sortedLayer[0]].y;
|
|
15135
|
+
const layerBottom = positions[sortedLayer[sortedLayer.length - 1]].y + WF_NODE_H;
|
|
15136
|
+
const layerMid = (layerTop + layerBottom) / 2;
|
|
15137
|
+
const canvasMid = WF_PAD + totalH / 2;
|
|
15138
|
+
const shift = canvasMid - layerMid;
|
|
15139
|
+
for (const id of sortedLayer) {
|
|
15140
|
+
positions[id].y += shift;
|
|
13698
15141
|
}
|
|
13699
|
-
// Deselect any library item
|
|
13700
|
-
document.querySelectorAll('.wf-library-item.active').forEach(el => el.classList.remove('active'));
|
|
13701
|
-
// Load the workflow
|
|
13702
|
-
wfState.activeWorkflow = definition;
|
|
13703
|
-
wfState.selectedNodeId = null;
|
|
13704
|
-
wfState.executionState = {};
|
|
13705
|
-
wfState.executionResults = {};
|
|
13706
|
-
wfSetToolbarEnabled(true);
|
|
13707
|
-
document.getElementById('wfCanvasEmpty').style.display = 'none';
|
|
13708
|
-
wfHideExecStatus();
|
|
13709
|
-
await wfRenderWorkflow(definition);
|
|
13710
|
-
wfOpenInspector();
|
|
13711
|
-
wfUpdateInspector();
|
|
13712
|
-
} catch (err) {
|
|
13713
|
-
alert('Failed to parse workflow file: ' + err.message);
|
|
13714
15142
|
}
|
|
13715
|
-
}
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
15143
|
+
}
|
|
15144
|
+
|
|
15145
|
+
return positions;
|
|
15146
|
+
}
|
|
15147
|
+
|
|
15148
|
+
/**
|
|
15149
|
+
* Compute barycenter (average position) of a node's neighbors in an adjacent layer.
|
|
15150
|
+
* If a node has no neighbors in the given orderMap, returns Infinity to push it to the end.
|
|
15151
|
+
*/
|
|
15152
|
+
function wfBarycenter(nodeId, neighborMap, orderMap) {
|
|
15153
|
+
const neighbors = (neighborMap[nodeId] || []).filter(n => orderMap[n] !== undefined);
|
|
15154
|
+
if (neighbors.length === 0) return Infinity;
|
|
15155
|
+
return neighbors.reduce((sum, n) => sum + orderMap[n], 0) / neighbors.length;
|
|
13719
15156
|
}
|
|
13720
15157
|
|
|
13721
|
-
// ── DAG Layout + SVG Rendering ──
|
|
13722
15158
|
async function wfRenderWorkflow(definition) {
|
|
13723
15159
|
const svg = document.getElementById('wf-canvas');
|
|
13724
15160
|
// Clear previous nodes and edges (keep defs)
|
|
@@ -13747,23 +15183,8 @@ async function wfRenderWorkflow(definition) {
|
|
|
13747
15183
|
const stepMap = {};
|
|
13748
15184
|
definition.steps.forEach(s => { stepMap[s.id] = s; });
|
|
13749
15185
|
|
|
13750
|
-
//
|
|
13751
|
-
const positions =
|
|
13752
|
-
const maxLayerSize = Math.max(...layers.map(l => l.length));
|
|
13753
|
-
const totalW = layers.length * WF_LAYER_GAP;
|
|
13754
|
-
const totalH = maxLayerSize * (WF_NODE_H + WF_NODE_GAP);
|
|
13755
|
-
|
|
13756
|
-
layers.forEach((layer, li) => {
|
|
13757
|
-
const x = WF_PAD + li * WF_LAYER_GAP;
|
|
13758
|
-
const layerH = layer.length * WF_NODE_H + (layer.length - 1) * WF_NODE_GAP;
|
|
13759
|
-
const startY = WF_PAD + (totalH - layerH) / 2;
|
|
13760
|
-
layer.forEach((stepId, ni) => {
|
|
13761
|
-
positions[stepId] = {
|
|
13762
|
-
x,
|
|
13763
|
-
y: startY + ni * (WF_NODE_H + WF_NODE_GAP),
|
|
13764
|
-
};
|
|
13765
|
-
});
|
|
13766
|
-
});
|
|
15186
|
+
// Auto-layout: crossing minimization + neighbor-aware placement
|
|
15187
|
+
const positions = wfAutoLayout(layers, graph);
|
|
13767
15188
|
wfState.nodePositions = positions;
|
|
13768
15189
|
|
|
13769
15190
|
// Build port-visibility maps: which nodes have input deps, which have dependents
|
|
@@ -14388,18 +15809,27 @@ function wfUpdateInspector() {
|
|
|
14388
15809
|
|
|
14389
15810
|
// ── OUTPUT Accordion ──
|
|
14390
15811
|
const outputExpanded = hasDone || accStates.output === true;
|
|
15812
|
+
const fmtSelectorHtml = hasDone ? `<select class="wf-output-format-select" onchange="wfChangeOutputFormat(this.value)" onclick="event.stopPropagation()">
|
|
15813
|
+
<option value="auto"${wfState.outputFormat === 'auto' ? ' selected' : ''}>Auto</option>
|
|
15814
|
+
<option value="json"${wfState.outputFormat === 'json' ? ' selected' : ''}>JSON</option>
|
|
15815
|
+
<option value="table"${wfState.outputFormat === 'table' ? ' selected' : ''}>Table</option>
|
|
15816
|
+
<option value="markdown"${wfState.outputFormat === 'markdown' ? ' selected' : ''}>Markdown</option>
|
|
15817
|
+
<option value="text"${wfState.outputFormat === 'text' ? ' selected' : ''}>Text</option>
|
|
15818
|
+
</select>` : '';
|
|
14391
15819
|
html += `<div class="wf-accordion-header ${outputExpanded ? 'expanded' : ''}" onclick="wfToggleAccordion('output', this)">
|
|
14392
15820
|
<span class="wf-acc-left"><span class="wf-chevron">▶</span> OUTPUT</span>
|
|
14393
|
-
<span class="wf-acc-summary"
|
|
15821
|
+
<span class="wf-acc-summary">${fmtSelectorHtml}</span>
|
|
14394
15822
|
</div>
|
|
14395
15823
|
<div class="wf-accordion-body" data-acc="output" style="max-height:${outputExpanded ? 'none' : '0px'};">
|
|
14396
|
-
<div class="wf-accordion-body-inner">`;
|
|
15824
|
+
<div class="wf-accordion-body-inner" id="wfOutputContent">`;
|
|
14397
15825
|
if (hasDone) {
|
|
14398
15826
|
const r = wfState.executionResults._done;
|
|
14399
|
-
const
|
|
14400
|
-
|
|
14401
|
-
|
|
14402
|
-
|
|
15827
|
+
const hints = r.formatters || {};
|
|
15828
|
+
const fmt = wfState.outputFormat || 'auto';
|
|
15829
|
+
const effectiveFmt = fmt === 'auto' ? wfAutoFormat(r.output) : fmt;
|
|
15830
|
+
html += `<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;">Completed in ${r.totalTimeMs}ms</div>`;
|
|
15831
|
+
html += wfRenderOutputAs(r.output, effectiveFmt, hints);
|
|
15832
|
+
html += `<button class="wf-output-expand-btn" data-expand-step="_done">⤢ Expand</button>`;
|
|
14403
15833
|
} else if (def.output) {
|
|
14404
15834
|
html += `<div class="wf-inspector-code" style="color:#40E0FF;">${escapeHtml(JSON.stringify(def.output, null, 2))}</div>`;
|
|
14405
15835
|
} else {
|
|
@@ -14531,11 +15961,23 @@ function wfUpdateInspector() {
|
|
|
14531
15961
|
if (state === 'completed' && result) {
|
|
14532
15962
|
const outputJson = result.output ? JSON.stringify(result.output, null, 2) : '';
|
|
14533
15963
|
const stepTitle = step.name || step.id;
|
|
15964
|
+
const stepShape = result.output ? wfDetectOutputShape(result.output) : null;
|
|
15965
|
+
const showStepFmtToggle = stepShape && (stepShape.type === 'array' || stepShape.type === 'comparison');
|
|
15966
|
+
const stepFmtToggle = showStepFmtToggle ?
|
|
15967
|
+
`<div style="margin-bottom:4px;"><select class="wf-output-format-select" style="width:auto;margin:0;" onchange="wfChangeStepFormat('${escapeHtml(step.id)}', this.value)">
|
|
15968
|
+
<option value="json">JSON</option>
|
|
15969
|
+
<option value="table" selected>Table</option>
|
|
15970
|
+
<option value="text">Text</option>
|
|
15971
|
+
</select></div>` : '';
|
|
15972
|
+
const stepResultContent = showStepFmtToggle
|
|
15973
|
+
? wfRenderOutputAs(result.output, 'table', {})
|
|
15974
|
+
: (outputJson ? '<div class="wf-inspector-code" style="max-height:120px;overflow:auto;">' + escapeHtml(outputJson) + '</div>' : '');
|
|
14534
15975
|
html += `<div class="wf-inspector-section">
|
|
14535
15976
|
<div class="wf-inspector-section-title">Result</div>
|
|
14536
15977
|
<div class="wf-inspector-result success">
|
|
14537
15978
|
<div style="font-size:11px;color:var(--text-muted);margin-bottom:4px;">${result.timeMs}ms${result.summary ? ', ' + result.summary : ''}</div>
|
|
14538
|
-
${
|
|
15979
|
+
${stepFmtToggle}
|
|
15980
|
+
<div id="wf-step-result-${escapeHtml(step.id)}">${stepResultContent}</div>
|
|
14539
15981
|
</div>
|
|
14540
15982
|
${outputJson ? '<button class="wf-output-expand-btn" data-expand-step="' + escapeHtml(step.id) + '">⤢ Expand</button>' : ''}
|
|
14541
15983
|
</div>`;
|
|
@@ -14571,17 +16013,274 @@ function wfBindExpandButtons(container) {
|
|
|
14571
16013
|
if (stepId === '_done') {
|
|
14572
16014
|
title = 'Workflow Output';
|
|
14573
16015
|
content = JSON.stringify(result.output, null, 2);
|
|
16016
|
+
// Rich modal rendering for workflow output
|
|
16017
|
+
const hints = result.formatters || {};
|
|
16018
|
+
const fmt = wfState.outputFormat || hints.default || 'auto';
|
|
16019
|
+
const effectiveFmt = fmt === 'auto' ? wfAutoFormat(result.output) : fmt;
|
|
16020
|
+
wfOpenOutputModal(title, content, effectiveFmt, result.output, hints);
|
|
16021
|
+
return;
|
|
14574
16022
|
} else {
|
|
14575
16023
|
const def = wfState.activeWorkflow;
|
|
14576
16024
|
const step = def ? def.steps.find(s => s.id === stepId) : null;
|
|
14577
16025
|
title = (step ? step.name || step.id : stepId) + ' Output';
|
|
14578
16026
|
content = JSON.stringify(result.output, null, 2);
|
|
16027
|
+
// Rich modal for step outputs with array/comparison shapes
|
|
16028
|
+
const stepShape = result.output ? wfDetectOutputShape(result.output) : null;
|
|
16029
|
+
if (stepShape && (stepShape.type === 'array' || stepShape.type === 'comparison')) {
|
|
16030
|
+
wfOpenOutputModal(title, content, 'table', result.output, {});
|
|
16031
|
+
return;
|
|
16032
|
+
}
|
|
14579
16033
|
}
|
|
14580
16034
|
wfOpenOutputModal(title, content);
|
|
14581
16035
|
});
|
|
14582
16036
|
});
|
|
14583
16037
|
}
|
|
14584
16038
|
|
|
16039
|
+
// ── Output Format Rendering ──
|
|
16040
|
+
|
|
16041
|
+
function wfStringify(val) {
|
|
16042
|
+
if (val == null) return '';
|
|
16043
|
+
if (typeof val === 'number') return val % 1 === 0 ? String(val) : val.toFixed(4);
|
|
16044
|
+
if (typeof val === 'boolean') return val ? 'true' : 'false';
|
|
16045
|
+
if (typeof val === 'object') {
|
|
16046
|
+
const s = JSON.stringify(val);
|
|
16047
|
+
return s.length > 80 ? s.slice(0, 77) + '...' : s;
|
|
16048
|
+
}
|
|
16049
|
+
return String(val);
|
|
16050
|
+
}
|
|
16051
|
+
|
|
16052
|
+
function wfDetectOutputShape(output) {
|
|
16053
|
+
if (output == null || typeof output !== 'object') return { type: 'scalar', value: output };
|
|
16054
|
+
const keys = Object.keys(output);
|
|
16055
|
+
for (const key of keys) {
|
|
16056
|
+
const val = output[key];
|
|
16057
|
+
if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object' && val[0] !== null) {
|
|
16058
|
+
return { type: 'array', arrayKey: key, columns: Object.keys(val[0]), totalRows: val.length };
|
|
16059
|
+
}
|
|
16060
|
+
}
|
|
16061
|
+
const objKeys = keys.filter(k => output[k] != null && typeof output[k] === 'object' && !Array.isArray(output[k]));
|
|
16062
|
+
if (objKeys.length >= 2) {
|
|
16063
|
+
return { type: 'comparison', objectKeys: objKeys, metricKeys: keys.filter(k => !objKeys.includes(k)) };
|
|
16064
|
+
}
|
|
16065
|
+
const textKeys = keys.filter(k => typeof output[k] === 'string' && output[k].length > 100);
|
|
16066
|
+
if (textKeys.length > 0) {
|
|
16067
|
+
return { type: 'text', textKeys, metricKeys: keys.filter(k => !textKeys.includes(k)) };
|
|
16068
|
+
}
|
|
16069
|
+
return { type: 'metrics', keys };
|
|
16070
|
+
}
|
|
16071
|
+
|
|
16072
|
+
function wfAutoFormat(output) {
|
|
16073
|
+
const shape = wfDetectOutputShape(output);
|
|
16074
|
+
if (shape.type === 'array' || shape.type === 'comparison') return 'table';
|
|
16075
|
+
return 'text';
|
|
16076
|
+
}
|
|
16077
|
+
|
|
16078
|
+
function wfRenderOutputAs(output, format, hints) {
|
|
16079
|
+
hints = hints || {};
|
|
16080
|
+
if (!output) return '<div class="wf-inspector-code" style="color:#40E0FF;">(no output)</div>';
|
|
16081
|
+
switch (format) {
|
|
16082
|
+
case 'json':
|
|
16083
|
+
return '<div class="wf-inspector-code" style="max-height:200px;overflow:auto;color:#40E0FF;">' + escapeHtml(JSON.stringify(output, null, 2)) + '</div>';
|
|
16084
|
+
case 'table':
|
|
16085
|
+
return wfRenderTable(output, hints);
|
|
16086
|
+
case 'markdown':
|
|
16087
|
+
return '<div class="wf-output-markdown">' + renderMarkdown(wfOutputToMarkdown(output, hints)) + '</div>';
|
|
16088
|
+
case 'text':
|
|
16089
|
+
return wfRenderText(output, hints);
|
|
16090
|
+
default:
|
|
16091
|
+
return '<div class="wf-inspector-code" style="max-height:200px;overflow:auto;color:#40E0FF;">' + escapeHtml(JSON.stringify(output, null, 2)) + '</div>';
|
|
16092
|
+
}
|
|
16093
|
+
}
|
|
16094
|
+
|
|
16095
|
+
function wfRenderTable(output, hints) {
|
|
16096
|
+
const shape = wfDetectOutputShape(output);
|
|
16097
|
+
let html = '';
|
|
16098
|
+
|
|
16099
|
+
if (shape.type === 'array') {
|
|
16100
|
+
const key = hints.arrayField || shape.arrayKey;
|
|
16101
|
+
const data = output[key] || [];
|
|
16102
|
+
if (data.length === 0) return '<div style="color:var(--text-muted);font-size:12px;">(empty results)</div>';
|
|
16103
|
+
const columns = hints.columns || shape.columns;
|
|
16104
|
+
|
|
16105
|
+
// Show non-array metrics above
|
|
16106
|
+
const metricKeys = Object.keys(output).filter(k => k !== key && !Array.isArray(output[k]));
|
|
16107
|
+
if (metricKeys.length > 0) {
|
|
16108
|
+
html += '<div style="margin-bottom:8px;">';
|
|
16109
|
+
metricKeys.forEach(k => {
|
|
16110
|
+
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>';
|
|
16111
|
+
});
|
|
16112
|
+
html += '</div>';
|
|
16113
|
+
}
|
|
16114
|
+
|
|
16115
|
+
html += '<table class="wf-output-table"><thead><tr>';
|
|
16116
|
+
columns.forEach(c => { html += '<th>' + escapeHtml(c) + '</th>'; });
|
|
16117
|
+
html += '</tr></thead><tbody>';
|
|
16118
|
+
data.forEach(row => {
|
|
16119
|
+
html += '<tr>';
|
|
16120
|
+
columns.forEach(c => { html += '<td>' + escapeHtml(wfStringify(row[c])) + '</td>'; });
|
|
16121
|
+
html += '</tr>';
|
|
16122
|
+
});
|
|
16123
|
+
html += '</tbody></table>';
|
|
16124
|
+
return html;
|
|
16125
|
+
}
|
|
16126
|
+
|
|
16127
|
+
if (shape.type === 'comparison') {
|
|
16128
|
+
const keys = shape.objectKeys;
|
|
16129
|
+
const allFields = new Set();
|
|
16130
|
+
keys.forEach(k => { if (output[k]) Object.keys(output[k]).forEach(f => allFields.add(f)); });
|
|
16131
|
+
|
|
16132
|
+
// Show non-object metrics above
|
|
16133
|
+
const metricKeys = (shape.metricKeys || []).filter(k => output[k] != null);
|
|
16134
|
+
if (metricKeys.length > 0) {
|
|
16135
|
+
html += '<div style="margin-bottom:8px;">';
|
|
16136
|
+
metricKeys.forEach(k => {
|
|
16137
|
+
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>';
|
|
16138
|
+
});
|
|
16139
|
+
html += '</div>';
|
|
16140
|
+
}
|
|
16141
|
+
|
|
16142
|
+
html += '<table class="wf-output-table"><thead><tr><th></th>';
|
|
16143
|
+
keys.forEach(k => { html += '<th>' + escapeHtml(String(k)) + '</th>'; });
|
|
16144
|
+
html += '</tr></thead><tbody>';
|
|
16145
|
+
allFields.forEach(f => {
|
|
16146
|
+
html += '<tr><td style="font-weight:600;">' + escapeHtml(f) + '</td>';
|
|
16147
|
+
keys.forEach(k => { html += '<td>' + escapeHtml(wfStringify(output[k] && output[k][f])) + '</td>'; });
|
|
16148
|
+
html += '</tr>';
|
|
16149
|
+
});
|
|
16150
|
+
html += '</tbody></table>';
|
|
16151
|
+
return html;
|
|
16152
|
+
}
|
|
16153
|
+
|
|
16154
|
+
// Fallback: key-value table
|
|
16155
|
+
html += '<table class="wf-output-table"><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody>';
|
|
16156
|
+
Object.entries(output).forEach(function(entry) {
|
|
16157
|
+
html += '<tr><td>' + escapeHtml(entry[0]) + '</td><td>' + escapeHtml(wfStringify(entry[1])) + '</td></tr>';
|
|
16158
|
+
});
|
|
16159
|
+
html += '</tbody></table>';
|
|
16160
|
+
return html;
|
|
16161
|
+
}
|
|
16162
|
+
|
|
16163
|
+
function wfRenderText(output, hints) {
|
|
16164
|
+
const shape = wfDetectOutputShape(output);
|
|
16165
|
+
if (shape.type === 'scalar') return '<div style="font-size:12px;color:var(--text);">' + escapeHtml(String(output || '')) + '</div>';
|
|
16166
|
+
let html = '';
|
|
16167
|
+
if (hints.title) {
|
|
16168
|
+
html += '<div style="font-weight:600;font-size:13px;color:var(--accent);margin-bottom:8px;">' + escapeHtml(hints.title) + '</div>';
|
|
16169
|
+
}
|
|
16170
|
+
|
|
16171
|
+
// Metrics
|
|
16172
|
+
const metricKeys = Object.keys(output).filter(k =>
|
|
16173
|
+
output[k] != null && (typeof output[k] !== 'object' || typeof output[k] === 'boolean') &&
|
|
16174
|
+
(typeof output[k] !== 'string' || output[k].length <= 100)
|
|
16175
|
+
);
|
|
16176
|
+
if (metricKeys.length > 0) {
|
|
16177
|
+
html += '<div style="margin-bottom:10px;">';
|
|
16178
|
+
metricKeys.forEach(k => {
|
|
16179
|
+
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>';
|
|
16180
|
+
});
|
|
16181
|
+
html += '</div>';
|
|
16182
|
+
}
|
|
16183
|
+
|
|
16184
|
+
// Object fields (comparison-like)
|
|
16185
|
+
Object.keys(output).forEach(k => {
|
|
16186
|
+
const v = output[k];
|
|
16187
|
+
if (v != null && typeof v === 'object' && !Array.isArray(v)) {
|
|
16188
|
+
html += '<div class="wf-output-text-field"><div class="wf-output-text-label">' + escapeHtml(k) + '</div>';
|
|
16189
|
+
Object.entries(v).forEach(function(entry) {
|
|
16190
|
+
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>';
|
|
16191
|
+
});
|
|
16192
|
+
html += '</div>';
|
|
16193
|
+
}
|
|
16194
|
+
});
|
|
16195
|
+
|
|
16196
|
+
// Text fields
|
|
16197
|
+
Object.keys(output).forEach(k => {
|
|
16198
|
+
if (typeof output[k] === 'string' && output[k].length > 100) {
|
|
16199
|
+
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>';
|
|
16200
|
+
}
|
|
16201
|
+
});
|
|
16202
|
+
|
|
16203
|
+
// Arrays
|
|
16204
|
+
Object.keys(output).forEach(k => {
|
|
16205
|
+
if (Array.isArray(output[k])) {
|
|
16206
|
+
html += '<div class="wf-output-text-field"><div class="wf-output-text-label">' + escapeHtml(k) + ' (' + output[k].length + ' items)</div>';
|
|
16207
|
+
html += '<div class="wf-inspector-code" style="max-height:100px;overflow:auto;">' + escapeHtml(JSON.stringify(output[k], null, 2)) + '</div></div>';
|
|
16208
|
+
}
|
|
16209
|
+
});
|
|
16210
|
+
|
|
16211
|
+
return html || '<div class="wf-inspector-code" style="color:#40E0FF;">' + escapeHtml(JSON.stringify(output, null, 2)) + '</div>';
|
|
16212
|
+
}
|
|
16213
|
+
|
|
16214
|
+
function wfOutputToMarkdown(output, hints) {
|
|
16215
|
+
let md = '';
|
|
16216
|
+
if (hints.title) md += '## ' + hints.title + '\n\n';
|
|
16217
|
+
else md += '## Workflow Output\n\n';
|
|
16218
|
+
Object.entries(output).forEach(function(entry) {
|
|
16219
|
+
const k = entry[0], v = entry[1];
|
|
16220
|
+
if (v == null) return;
|
|
16221
|
+
if (typeof v !== 'object' || typeof v === 'boolean') {
|
|
16222
|
+
if (typeof v !== 'string' || v.length <= 100) {
|
|
16223
|
+
md += '- **' + k + ':** ' + wfStringify(v) + '\n';
|
|
16224
|
+
}
|
|
16225
|
+
}
|
|
16226
|
+
});
|
|
16227
|
+
md += '\n';
|
|
16228
|
+
// Arrays as tables
|
|
16229
|
+
Object.entries(output).forEach(function(entry) {
|
|
16230
|
+
const k = entry[0], v = entry[1];
|
|
16231
|
+
if (!Array.isArray(v) || v.length === 0 || typeof v[0] !== 'object') return;
|
|
16232
|
+
const cols = hints.columns || Object.keys(v[0]);
|
|
16233
|
+
md += '### ' + k + '\n\n| ' + cols.join(' | ') + ' |\n| ' + cols.map(function() { return '---'; }).join(' | ') + ' |\n';
|
|
16234
|
+
v.forEach(function(row) { md += '| ' + cols.map(function(c) { return wfStringify(row[c]); }).join(' | ') + ' |\n'; });
|
|
16235
|
+
md += '\n';
|
|
16236
|
+
});
|
|
16237
|
+
// Comparison objects
|
|
16238
|
+
const objKeys = Object.keys(output).filter(function(k) { return output[k] != null && typeof output[k] === 'object' && !Array.isArray(output[k]); });
|
|
16239
|
+
if (objKeys.length >= 2) {
|
|
16240
|
+
const allFields = new Set();
|
|
16241
|
+
objKeys.forEach(function(k) { Object.keys(output[k]).forEach(function(f) { allFields.add(f); }); });
|
|
16242
|
+
md += '| | ' + objKeys.join(' | ') + ' |\n| --- | ' + objKeys.map(function() { return '---'; }).join(' | ') + ' |\n';
|
|
16243
|
+
allFields.forEach(function(f) {
|
|
16244
|
+
md += '| **' + f + '** | ' + objKeys.map(function(k) { return wfStringify(output[k][f]); }).join(' | ') + ' |\n';
|
|
16245
|
+
});
|
|
16246
|
+
md += '\n';
|
|
16247
|
+
}
|
|
16248
|
+
// Long text
|
|
16249
|
+
Object.entries(output).forEach(function(entry) {
|
|
16250
|
+
if (typeof entry[1] === 'string' && entry[1].length > 100) {
|
|
16251
|
+
md += '### ' + entry[0] + '\n\n' + entry[1] + '\n\n';
|
|
16252
|
+
}
|
|
16253
|
+
});
|
|
16254
|
+
return md;
|
|
16255
|
+
}
|
|
16256
|
+
|
|
16257
|
+
function wfChangeOutputFormat(format) {
|
|
16258
|
+
wfState.outputFormat = format;
|
|
16259
|
+
const r = wfState.executionResults._done;
|
|
16260
|
+
if (!r) return;
|
|
16261
|
+
const hints = r.formatters || {};
|
|
16262
|
+
const effectiveFormat = format === 'auto' ? wfAutoFormat(r.output) : format;
|
|
16263
|
+
const container = document.getElementById('wfOutputContent');
|
|
16264
|
+
if (!container) return;
|
|
16265
|
+
let html = '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;">Completed in ' + r.totalTimeMs + 'ms</div>';
|
|
16266
|
+
html += wfRenderOutputAs(r.output, effectiveFormat, hints);
|
|
16267
|
+
html += '<button class="wf-output-expand-btn" data-expand-step="_done">⤢ Expand</button>';
|
|
16268
|
+
container.innerHTML = html;
|
|
16269
|
+
wfBindExpandButtons(container);
|
|
16270
|
+
}
|
|
16271
|
+
|
|
16272
|
+
function wfChangeStepFormat(stepId, format) {
|
|
16273
|
+
const result = wfState.executionResults[stepId];
|
|
16274
|
+
if (!result || !result.output) return;
|
|
16275
|
+
const container = document.getElementById('wf-step-result-' + stepId);
|
|
16276
|
+
if (!container) return;
|
|
16277
|
+
if (format === 'json') {
|
|
16278
|
+
container.innerHTML = '<div class="wf-inspector-code" style="max-height:120px;overflow:auto;">' + escapeHtml(JSON.stringify(result.output, null, 2)) + '</div>';
|
|
16279
|
+
} else {
|
|
16280
|
+
container.innerHTML = wfRenderOutputAs(result.output, format, {});
|
|
16281
|
+
}
|
|
16282
|
+
}
|
|
16283
|
+
|
|
14585
16284
|
// escapeHtml is already defined globally — reuse it
|
|
14586
16285
|
|
|
14587
16286
|
// ── Toolbar helpers ──
|
|
@@ -14783,6 +16482,31 @@ function wfStopExecution(reason) {
|
|
|
14783
16482
|
wfUpdateInspector();
|
|
14784
16483
|
}
|
|
14785
16484
|
|
|
16485
|
+
// ── Workflow Input Cache (localStorage) ──
|
|
16486
|
+
|
|
16487
|
+
function wfSlugify(name) {
|
|
16488
|
+
return String(name).toLowerCase().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
16489
|
+
}
|
|
16490
|
+
|
|
16491
|
+
function wfGetInputCache(workflowName) {
|
|
16492
|
+
try {
|
|
16493
|
+
const slug = wfSlugify(workflowName);
|
|
16494
|
+
if (!slug) return {};
|
|
16495
|
+
const raw = localStorage.getItem('vai-workflow-inputs-' + slug);
|
|
16496
|
+
if (!raw) return {};
|
|
16497
|
+
const parsed = JSON.parse(raw);
|
|
16498
|
+
return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {};
|
|
16499
|
+
} catch { return {}; }
|
|
16500
|
+
}
|
|
16501
|
+
|
|
16502
|
+
function wfSaveInputCache(workflowName, inputs) {
|
|
16503
|
+
try {
|
|
16504
|
+
const slug = wfSlugify(workflowName);
|
|
16505
|
+
if (!slug || !inputs) return;
|
|
16506
|
+
localStorage.setItem('vai-workflow-inputs-' + slug, JSON.stringify(inputs));
|
|
16507
|
+
} catch { /* localStorage may be full or disabled */ }
|
|
16508
|
+
}
|
|
16509
|
+
|
|
14786
16510
|
// ── Input Modal (pre-execution) ──
|
|
14787
16511
|
|
|
14788
16512
|
function wfShowInputModal() {
|
|
@@ -14792,13 +16516,15 @@ function wfShowInputModal() {
|
|
|
14792
16516
|
if (entries.length === 0) { wfExecuteWithInputs({}); return; }
|
|
14793
16517
|
|
|
14794
16518
|
document.getElementById('wfInputModalTitle').textContent = (def.name || 'Workflow') + ' Inputs';
|
|
16519
|
+
const cached = wfGetInputCache(def.name || '');
|
|
14795
16520
|
let html = '';
|
|
14796
16521
|
for (const [key, spec] of entries) {
|
|
14797
16522
|
const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
|
|
14798
16523
|
const desc = spec.description ? `<div class="wf-input-modal-desc">${escapeHtml(spec.description)}</div>` : '';
|
|
14799
|
-
// Pre-fill
|
|
16524
|
+
// Pre-fill priority: 1) inspector field, 2) cached from last run, 3) spec default
|
|
14800
16525
|
const inspectorEl = document.getElementById('wf-input-' + key);
|
|
14801
16526
|
let prefill = inspectorEl ? inspectorEl.value : '';
|
|
16527
|
+
if (!prefill && key in cached) prefill = String(cached[key]);
|
|
14802
16528
|
if (!prefill && spec.default !== undefined) prefill = String(spec.default);
|
|
14803
16529
|
const placeholder = spec.type === 'number' ? 'number' : (spec.type || 'string');
|
|
14804
16530
|
html += `<div class="wf-input-modal-field">
|
|
@@ -14869,6 +16595,9 @@ function wfInputModalSubmit() {
|
|
|
14869
16595
|
|
|
14870
16596
|
if (hasError) return;
|
|
14871
16597
|
|
|
16598
|
+
// Cache inputs for next run
|
|
16599
|
+
wfSaveInputCache(def.name || '', inputs);
|
|
16600
|
+
|
|
14872
16601
|
// Also update inspector fields to keep them in sync
|
|
14873
16602
|
for (const [key, val] of Object.entries(inputs)) {
|
|
14874
16603
|
const inspEl = document.getElementById('wf-input-' + key);
|
|
@@ -14897,10 +16626,131 @@ async function wfExecute() {
|
|
|
14897
16626
|
return;
|
|
14898
16627
|
}
|
|
14899
16628
|
|
|
16629
|
+
// Check for empty required step inputs and prompt the user
|
|
16630
|
+
const missing = wfFindMissingStepInputs(def);
|
|
16631
|
+
if (missing.length > 0) {
|
|
16632
|
+
wfShowStepInputModal(missing);
|
|
16633
|
+
return;
|
|
16634
|
+
}
|
|
16635
|
+
|
|
14900
16636
|
// No inputs needed, execute directly
|
|
14901
16637
|
wfExecuteWithInputs({});
|
|
14902
16638
|
}
|
|
14903
16639
|
|
|
16640
|
+
/**
|
|
16641
|
+
* Scan all steps for required inputs that are empty (and not filled by template refs).
|
|
16642
|
+
* Returns array of { stepId, stepName, tool, key, placeholder, type } for each missing input.
|
|
16643
|
+
*/
|
|
16644
|
+
function wfFindMissingStepInputs(def) {
|
|
16645
|
+
const missing = [];
|
|
16646
|
+
for (const step of (def.steps || [])) {
|
|
16647
|
+
const inputDefs = WF_INPUT_DEFS[step.tool] || [];
|
|
16648
|
+
for (const d of inputDefs) {
|
|
16649
|
+
if (!d.required) continue;
|
|
16650
|
+
const val = step.inputs?.[d.key];
|
|
16651
|
+
// Skip if already filled (non-empty string, or any non-string value)
|
|
16652
|
+
if (val !== undefined && val !== null && val !== '') continue;
|
|
16653
|
+
missing.push({
|
|
16654
|
+
stepId: step.id,
|
|
16655
|
+
stepName: step.name || step.id,
|
|
16656
|
+
tool: step.tool,
|
|
16657
|
+
key: d.key,
|
|
16658
|
+
placeholder: d.placeholder || '',
|
|
16659
|
+
type: d.type || 'text',
|
|
16660
|
+
});
|
|
16661
|
+
}
|
|
16662
|
+
}
|
|
16663
|
+
return missing;
|
|
16664
|
+
}
|
|
16665
|
+
|
|
16666
|
+
/**
|
|
16667
|
+
* Show a modal prompting the user for missing required step inputs before execution.
|
|
16668
|
+
* Reuses the existing input modal UI.
|
|
16669
|
+
*/
|
|
16670
|
+
function wfShowStepInputModal(missing) {
|
|
16671
|
+
document.getElementById('wfInputModalTitle').textContent = 'Required Inputs';
|
|
16672
|
+
let html = '';
|
|
16673
|
+
let lastStepId = '';
|
|
16674
|
+
|
|
16675
|
+
for (const m of missing) {
|
|
16676
|
+
// Group header per step
|
|
16677
|
+
if (m.stepId !== lastStepId) {
|
|
16678
|
+
const meta = WF_NODE_META[m.tool] || {};
|
|
16679
|
+
html += `<div style="margin-top:${lastStepId ? '16' : '0'}px;margin-bottom:8px;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);display:flex;align-items:center;gap:6px;">
|
|
16680
|
+
<span style="color:${meta.color || 'var(--text-muted)'}">
|
|
16681
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${meta.icon || WF_FALLBACK_ICON}"/></svg>
|
|
16682
|
+
</span>
|
|
16683
|
+
${escapeHtml(m.stepName)}
|
|
16684
|
+
</div>`;
|
|
16685
|
+
lastStepId = m.stepId;
|
|
16686
|
+
}
|
|
16687
|
+
|
|
16688
|
+
const inputId = 'wf-stepin-' + m.stepId + '-' + m.key;
|
|
16689
|
+
html += `<div class="wf-input-modal-field">
|
|
16690
|
+
<div class="wf-input-modal-label">${escapeHtml(m.key)} <span style="color:#e74c3c">*</span></div>
|
|
16691
|
+
<input class="wf-input-modal-input" id="${inputId}" placeholder="${escapeHtml(m.placeholder)}" data-step-id="${escapeHtml(m.stepId)}" data-key="${escapeHtml(m.key)}" data-type="${m.type}">
|
|
16692
|
+
<div class="wf-input-modal-error" id="${inputId}-err">This field is required</div>
|
|
16693
|
+
</div>`;
|
|
16694
|
+
}
|
|
16695
|
+
|
|
16696
|
+
document.getElementById('wfInputModalBody').innerHTML = html;
|
|
16697
|
+
|
|
16698
|
+
// Swap the footer button handler temporarily
|
|
16699
|
+
const runBtn = document.querySelector('.wf-input-modal-run');
|
|
16700
|
+
runBtn.textContent = 'Run Workflow';
|
|
16701
|
+
runBtn.onclick = () => wfStepInputModalSubmit(missing);
|
|
16702
|
+
|
|
16703
|
+
document.getElementById('wfInputModalBackdrop').style.display = '';
|
|
16704
|
+
|
|
16705
|
+
// Focus first input
|
|
16706
|
+
const firstId = 'wf-stepin-' + missing[0].stepId + '-' + missing[0].key;
|
|
16707
|
+
const firstEl = document.getElementById(firstId);
|
|
16708
|
+
if (firstEl) setTimeout(() => firstEl.focus(), 50);
|
|
16709
|
+
}
|
|
16710
|
+
|
|
16711
|
+
function wfStepInputModalSubmit(missing) {
|
|
16712
|
+
let hasError = false;
|
|
16713
|
+
|
|
16714
|
+
for (const m of missing) {
|
|
16715
|
+
const inputId = 'wf-stepin-' + m.stepId + '-' + m.key;
|
|
16716
|
+
const el = document.getElementById(inputId);
|
|
16717
|
+
const errEl = document.getElementById(inputId + '-err');
|
|
16718
|
+
if (!el) continue;
|
|
16719
|
+
|
|
16720
|
+
el.classList.remove('error');
|
|
16721
|
+
if (errEl) errEl.style.display = 'none';
|
|
16722
|
+
|
|
16723
|
+
const val = el.value.trim();
|
|
16724
|
+
if (!val) {
|
|
16725
|
+
el.classList.add('error');
|
|
16726
|
+
if (errEl) { errEl.textContent = 'This field is required'; errEl.style.display = ''; }
|
|
16727
|
+
hasError = true;
|
|
16728
|
+
continue;
|
|
16729
|
+
}
|
|
16730
|
+
|
|
16731
|
+
// Write the value into the step definition
|
|
16732
|
+
const def = wfState.activeWorkflow;
|
|
16733
|
+
const step = def.steps.find(s => s.id === m.stepId);
|
|
16734
|
+
if (step) {
|
|
16735
|
+
if (!step.inputs) step.inputs = {};
|
|
16736
|
+
step.inputs[m.key] = val;
|
|
16737
|
+
}
|
|
16738
|
+
}
|
|
16739
|
+
|
|
16740
|
+
if (hasError) return;
|
|
16741
|
+
|
|
16742
|
+
// Restore the default modal handler
|
|
16743
|
+
const runBtn = document.querySelector('.wf-input-modal-run');
|
|
16744
|
+
runBtn.onclick = () => wfInputModalSubmit();
|
|
16745
|
+
|
|
16746
|
+
wfCloseInputModal();
|
|
16747
|
+
|
|
16748
|
+
// Refresh inspector if a step is selected
|
|
16749
|
+
if (wfState.selectedNodeId) wfUpdateInspector();
|
|
16750
|
+
|
|
16751
|
+
wfExecuteWithInputs({});
|
|
16752
|
+
}
|
|
16753
|
+
|
|
14904
16754
|
async function wfExecuteWithInputs(inputs) {
|
|
14905
16755
|
const def = wfState.activeWorkflow;
|
|
14906
16756
|
if (!def || wfState.executing) return;
|
|
@@ -14972,6 +16822,16 @@ async function wfExecuteWithInputs(inputs) {
|
|
|
14972
16822
|
timeMs: data.timeMs,
|
|
14973
16823
|
summary: data.summary || '',
|
|
14974
16824
|
};
|
|
16825
|
+
// Update cost tracker with usage data from this step
|
|
16826
|
+
if (data._usage && Array.isArray(data._usage)) {
|
|
16827
|
+
data._usage.forEach(u => {
|
|
16828
|
+
if (u.op === 'llm') {
|
|
16829
|
+
CostTracker.addLLMOperation('wf-' + u.op, u.model, u.inputTokens || 0, u.outputTokens || 0);
|
|
16830
|
+
} else {
|
|
16831
|
+
CostTracker.addOperation('wf-' + u.op, u.model, u.tokens || 0);
|
|
16832
|
+
}
|
|
16833
|
+
});
|
|
16834
|
+
}
|
|
14975
16835
|
wfRefreshNodes();
|
|
14976
16836
|
if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
|
|
14977
16837
|
} else if (currentEvent === 'step_skip') {
|
|
@@ -14988,6 +16848,7 @@ async function wfExecuteWithInputs(inputs) {
|
|
|
14988
16848
|
if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
|
|
14989
16849
|
} else if (currentEvent === 'done') {
|
|
14990
16850
|
wfState.executionResults._done = data;
|
|
16851
|
+
wfState.outputFormat = 'auto';
|
|
14991
16852
|
if (!wfState.selectedNodeId) wfUpdateInspector();
|
|
14992
16853
|
} else if (currentEvent === 'error') {
|
|
14993
16854
|
hasError = true;
|
|
@@ -15074,10 +16935,49 @@ function wfResetExecution() {
|
|
|
15074
16935
|
wfUpdateInspector();
|
|
15075
16936
|
}
|
|
15076
16937
|
|
|
16938
|
+
async function wfRelayout() {
|
|
16939
|
+
const def = wfState.activeWorkflow;
|
|
16940
|
+
if (!def) return;
|
|
16941
|
+
|
|
16942
|
+
// If layers/graph aren't populated yet (e.g. builder mode), fetch them
|
|
16943
|
+
if (!wfState.layers || !wfState.graph || wfState.layers.length === 0) {
|
|
16944
|
+
try {
|
|
16945
|
+
const res = await fetch('/api/workflows/plan', {
|
|
16946
|
+
method: 'POST',
|
|
16947
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16948
|
+
body: JSON.stringify({ definition: def }),
|
|
16949
|
+
});
|
|
16950
|
+
const data = await res.json();
|
|
16951
|
+
if (data.layers) wfState.layers = data.layers;
|
|
16952
|
+
if (data.graph) wfState.graph = data.graph;
|
|
16953
|
+
} catch (err) {
|
|
16954
|
+
console.warn('Relayout: failed to fetch plan:', err.message);
|
|
16955
|
+
return;
|
|
16956
|
+
}
|
|
16957
|
+
}
|
|
16958
|
+
|
|
16959
|
+
if (!wfState.layers || wfState.layers.length === 0) return;
|
|
16960
|
+
|
|
16961
|
+
const positions = wfAutoLayout(wfState.layers, wfState.graph || {});
|
|
16962
|
+
|
|
16963
|
+
// Preserve positions for orphan nodes not in any layer (builder mode)
|
|
16964
|
+
if (def.steps) {
|
|
16965
|
+
for (const step of def.steps) {
|
|
16966
|
+
if (!positions[step.id] && wfState.nodePositions[step.id]) {
|
|
16967
|
+
positions[step.id] = wfState.nodePositions[step.id];
|
|
16968
|
+
}
|
|
16969
|
+
}
|
|
16970
|
+
}
|
|
16971
|
+
|
|
16972
|
+
wfState.nodePositions = positions;
|
|
16973
|
+
wfRefreshNodes();
|
|
16974
|
+
wfFitToView();
|
|
16975
|
+
}
|
|
16976
|
+
|
|
15077
16977
|
// ── Output Modal ──
|
|
15078
16978
|
let wfOutputModalData = '';
|
|
15079
16979
|
|
|
15080
|
-
function wfOpenOutputModal(title, content) {
|
|
16980
|
+
function wfOpenOutputModal(title, content, format, outputObj, hints) {
|
|
15081
16981
|
wfOutputModalData = content;
|
|
15082
16982
|
const backdrop = document.getElementById('wfOutputModalBackdrop');
|
|
15083
16983
|
const titleEl = document.getElementById('wfOutputModalTitle');
|
|
@@ -15085,8 +16985,14 @@ function wfOpenOutputModal(title, content) {
|
|
|
15085
16985
|
const copyLabel = document.getElementById('wfOutputCopyLabel');
|
|
15086
16986
|
if (!backdrop || !bodyEl) return;
|
|
15087
16987
|
titleEl.textContent = title || 'Output';
|
|
15088
|
-
bodyEl.textContent = content;
|
|
15089
16988
|
copyLabel.textContent = 'Copy';
|
|
16989
|
+
|
|
16990
|
+
// Rich rendering if format + output object provided
|
|
16991
|
+
if (format && outputObj && format !== 'json') {
|
|
16992
|
+
bodyEl.innerHTML = wfRenderOutputAs(outputObj, format, hints || {});
|
|
16993
|
+
} else {
|
|
16994
|
+
bodyEl.textContent = content;
|
|
16995
|
+
}
|
|
15090
16996
|
backdrop.style.display = 'flex';
|
|
15091
16997
|
}
|
|
15092
16998
|
|
|
@@ -15325,7 +17231,7 @@ const WF_INPUT_DEFS = {
|
|
|
15325
17231
|
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
17232
|
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
17233
|
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-
|
|
17234
|
+
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
17235
|
similarity: [{ key: 'text1', type: 'text', required: true }, { key: 'text2', type: 'text', required: true }, { key: 'model', type: 'text', required: false }],
|
|
15330
17236
|
collections: [{ key: 'db', type: 'text', required: false }],
|
|
15331
17237
|
models: [{ key: 'category', type: 'select', required: false, options: ['embedding','rerank','all'] }],
|
|
@@ -16111,10 +18017,9 @@ function wfEdgeDropOnInput(toId) {
|
|
|
16111
18017
|
if (connected) {
|
|
16112
18018
|
wfState.dirtyFlag = true;
|
|
16113
18019
|
wfBuildGraph();
|
|
16114
|
-
wfRelayout();
|
|
16115
18020
|
wfRefreshNodes();
|
|
16116
18021
|
if (wfState.selectedNodeId === toId) wfUpdateInspector();
|
|
16117
|
-
|
|
18022
|
+
|
|
16118
18023
|
// Trigger draft validation immediately on edge connection
|
|
16119
18024
|
wfTriggerDraftValidation(0);
|
|
16120
18025
|
}
|
|
@@ -16125,42 +18030,9 @@ function wfEdgeDropOnInput(toId) {
|
|
|
16125
18030
|
if (el) el.remove();
|
|
16126
18031
|
}
|
|
16127
18032
|
|
|
16128
|
-
|
|
16129
|
-
|
|
16130
|
-
|
|
16131
|
-
if (!def || def.steps.length === 0) return;
|
|
16132
|
-
|
|
16133
|
-
try {
|
|
16134
|
-
const res = await fetch('/api/workflows/plan', {
|
|
16135
|
-
method: 'POST',
|
|
16136
|
-
headers: { 'Content-Type': 'application/json' },
|
|
16137
|
-
body: JSON.stringify(def),
|
|
16138
|
-
});
|
|
16139
|
-
const data = await res.json();
|
|
16140
|
-
if (data.layers && data.layers.length > 0) {
|
|
16141
|
-
wfState.layers = data.layers;
|
|
16142
|
-
// Reposition nodes based on layers
|
|
16143
|
-
const positions = {};
|
|
16144
|
-
data.layers.forEach((layer, li) => {
|
|
16145
|
-
layer.forEach((stepId, ni) => {
|
|
16146
|
-
positions[stepId] = {
|
|
16147
|
-
x: WF_PAD + li * WF_LAYER_GAP,
|
|
16148
|
-
y: WF_PAD + ni * (WF_NODE_H + WF_NODE_GAP),
|
|
16149
|
-
};
|
|
16150
|
-
});
|
|
16151
|
-
});
|
|
16152
|
-
// Keep orphan nodes (not in any layer) at their current position
|
|
16153
|
-
for (const step of def.steps) {
|
|
16154
|
-
if (!positions[step.id] && wfState.nodePositions[step.id]) {
|
|
16155
|
-
positions[step.id] = wfState.nodePositions[step.id];
|
|
16156
|
-
}
|
|
16157
|
-
}
|
|
16158
|
-
wfState.nodePositions = positions;
|
|
16159
|
-
}
|
|
16160
|
-
} catch (err) {
|
|
16161
|
-
console.warn('Relayout failed:', err.message);
|
|
16162
|
-
}
|
|
16163
|
-
}
|
|
18033
|
+
/// ── Builder: Relayout via auto-layout algorithm ──
|
|
18034
|
+
// (delegates to wfRelayout defined earlier, which uses wfAutoLayout for
|
|
18035
|
+
// barycenter crossing minimization + neighbor-aware placement)
|
|
16164
18036
|
|
|
16165
18037
|
// ── Docs shortcut (F1) ──
|
|
16166
18038
|
const DOCS_URLS = {
|
|
@@ -16212,6 +18084,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
16212
18084
|
if (e.key === '+' || e.key === '=') { wfZoom(1); e.preventDefault(); }
|
|
16213
18085
|
else if (e.key === '-') { wfZoom(-1); e.preventDefault(); }
|
|
16214
18086
|
else if (e.key === '0') { wfFitToView(); e.preventDefault(); }
|
|
18087
|
+
else if (e.key === 'l' || e.key === 'L') { wfRelayout(); e.preventDefault(); }
|
|
16215
18088
|
else if (e.key === 'Escape') { wfDeselectNode(); e.preventDefault(); }
|
|
16216
18089
|
else if (e.key === 'ArrowLeft') { wfState.panX -= PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
|
|
16217
18090
|
else if (e.key === 'ArrowRight') { wfState.panX += PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
|
|
@@ -18184,6 +20057,7 @@ const staticCommandRegistry = [
|
|
|
18184
20057
|
{ 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
20058
|
{ 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
20059
|
{ id: 'nav-workflows', label: 'Workflows', description: 'Visual workflow canvas', category: 'navigation', keywords: ['pipeline', 'dag', 'canvas'], icon: '🔄', shortcut: '⌘8', priority: 1, execute: () => switchTab('workflows') },
|
|
20060
|
+
{ 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
20061
|
{ 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
20062
|
{ 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
20063
|
{ id: 'nav-about', label: 'About', description: 'Version and project information', category: 'navigation', keywords: ['version', 'info', 'credits'], icon: 'ℹ️', priority: 5, execute: () => switchTab('about') },
|
|
@@ -18239,7 +20113,7 @@ const staticCommandRegistry = [
|
|
|
18239
20113
|
{ 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
20114
|
{ 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
20115
|
{ 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('
|
|
20116
|
+
{ 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
20117
|
{ 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
20118
|
];
|
|
18245
20119
|
|