voyageai-cli 1.19.0 → 1.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voyageai-cli",
3
- "version": "1.19.0",
3
+ "version": "1.19.2",
4
4
  "description": "CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search",
5
5
  "bin": {
6
6
  "vai": "./src/cli.js"
@@ -8,27 +8,85 @@
8
8
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
9
 
10
10
  :root {
11
- --bg: #1a1a2e;
12
- --bg-surface: #16213e;
13
- --bg-card: #1e2a47;
14
- --bg-input: #0f1629;
15
- --accent: #00d4aa;
16
- --accent-dim: #00a88a;
17
- --accent-glow: rgba(0, 212, 170, 0.15);
18
- --text: #e0e0e0;
19
- --text-dim: #8892a4;
20
- --text-muted: #5a6478;
21
- --border: #2a3550;
22
- --error: #ff6b6b;
23
- --warning: #ffd93d;
24
- --success: #00d4aa;
25
- --red: #ff6b6b;
26
- --yellow: #ffd93d;
27
- --green: #00d4aa;
11
+ /* MongoDB Design System — Dark Mode Palette (default) */
12
+ --bg: #001E2B; /* MDB Black */
13
+ --bg-surface: #112733; /* Gray Dark 4 */
14
+ --bg-card: #1C2D38; /* Gray Dark 3 */
15
+ --bg-input: #112733; /* Gray Dark 4 */
16
+ --accent: #00ED64; /* Green Base */
17
+ --accent-dim: #00A35C; /* Green Dark 1 */
18
+ --accent-glow: rgba(0, 237, 100, 0.12);
19
+ --text: #E8EDEB; /* Gray Light 2 */
20
+ --text-dim: #C1C7C6; /* Gray Light 1 */
21
+ --text-muted: #889397; /* Gray Base */
22
+ --border: #3D4F58; /* Gray Dark 2 */
23
+ --error: #FF6960; /* Red Light 1 */
24
+ --warning: #FFC010; /* Yellow Base */
25
+ --success: #00ED64; /* Green Base */
26
+ --red: #FF6960; /* Red Light 1 */
27
+ --yellow: #FFC010; /* Yellow Base */
28
+ --green: #00ED64; /* Green Base */
29
+ --blue: #0498EC; /* Blue Light 1 (links) */
30
+ --purple: #B45AF2; /* Purple Base */
31
+ --green-dark: #023430; /* Green Dark 3 */
28
32
  --radius: 8px;
29
- --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
30
- --mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
31
- }
33
+ --font: 'Euclid Circular A', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
34
+ --mono: 'Source Code Pro', 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
35
+ }
36
+
37
+ /* MongoDB Design System — Light Mode Palette */
38
+ [data-theme="light"] {
39
+ --bg: #FFFFFF; /* White */
40
+ --bg-surface: #F9FBFA; /* Gray Light 3 */
41
+ --bg-card: #FFFFFF; /* White */
42
+ --bg-input: #F9FBFA; /* Gray Light 3 */
43
+ --accent: #00A35C; /* Green Dark 1 (better contrast on white) */
44
+ --accent-dim: #00684A; /* Green Dark 2 */
45
+ --accent-glow: rgba(0, 163, 92, 0.08);
46
+ --text: #001E2B; /* MDB Black */
47
+ --text-dim: #5C6C75; /* Gray Dark 1 */
48
+ --text-muted: #889397; /* Gray Base */
49
+ --border: #E8EDEB; /* Gray Light 2 */
50
+ --error: #DB3030; /* Red Base */
51
+ --warning: #944F01; /* Yellow Dark 2 */
52
+ --success: #00684A; /* Green Dark 2 */
53
+ --red: #DB3030; /* Red Base */
54
+ --yellow: #944F01; /* Yellow Dark 2 */
55
+ --green: #00684A; /* Green Dark 2 */
56
+ --blue: #016BF8; /* Blue Base */
57
+ --purple: #5E0C9E; /* Purple Dark 2 */
58
+ --green-dark: #023430; /* Green Dark 3 */
59
+ }
60
+ /* Light mode shadow + card adjustments */
61
+ [data-theme="light"] .explore-card,
62
+ [data-theme="light"] .card,
63
+ [data-theme="light"] .cost-strategy,
64
+ [data-theme="light"] .cost-summary-card {
65
+ box-shadow: 0 1px 4px rgba(0, 30, 43, 0.08);
66
+ }
67
+ [data-theme="light"] .explore-card:hover {
68
+ box-shadow: 0 4px 16px rgba(0, 163, 92, 0.12);
69
+ }
70
+ [data-theme="light"] .cost-modal,
71
+ [data-theme="light"] .explore-modal {
72
+ box-shadow: 0 20px 60px rgba(0, 30, 43, 0.2);
73
+ }
74
+ [data-theme="light"] .cost-modal-overlay,
75
+ [data-theme="light"] .explore-modal-overlay {
76
+ background: rgba(0, 30, 43, 0.4);
77
+ }
78
+ [data-theme="light"] .nav {
79
+ box-shadow: 0 1px 3px rgba(0, 30, 43, 0.06);
80
+ }
81
+ /* Light mode gradient overrides */
82
+ [data-theme="light"] .quant-bar-fill.storage { background: linear-gradient(90deg, #00A35C, #00ED64); }
83
+ [data-theme="light"] .quant-bar-fill.latency { background: linear-gradient(90deg, #016BF8, #0498EC); }
84
+ [data-theme="light"] .quant-meter-fill.perfect { background: linear-gradient(90deg, #00A35C, #00ED64); }
85
+ [data-theme="light"] .quant-meter-fill.good { background: linear-gradient(90deg, #944F01, #FFC010); }
86
+ [data-theme="light"] .quant-meter-fill.degraded { background: linear-gradient(90deg, #DB3030, #FF6960); }
87
+ /* Light mode button text */
88
+ [data-theme="light"] .btn { color: #FFFFFF; }
89
+ [data-theme="light"] .btn:hover { background: #00684A; }
32
90
 
33
91
  html, body { height: 100%; }
34
92
 
@@ -63,6 +121,24 @@ body {
63
121
 
64
122
  .nav-spacer { flex: 1; }
65
123
 
124
+ .theme-toggle {
125
+ background: none;
126
+ border: 1px solid var(--border);
127
+ border-radius: 20px;
128
+ padding: 5px 10px;
129
+ cursor: pointer;
130
+ font-size: 16px;
131
+ line-height: 1;
132
+ transition: all 0.2s;
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 4px;
136
+ }
137
+ .theme-toggle:hover {
138
+ border-color: var(--accent);
139
+ background: var(--accent-glow);
140
+ }
141
+
66
142
  .status-dot {
67
143
  width: 8px; height: 8px;
68
144
  border-radius: 50%;
@@ -168,7 +244,7 @@ select:focus { outline: none; border-color: var(--accent); }
168
244
 
169
245
  .btn {
170
246
  background: var(--accent);
171
- color: #0a0a1a;
247
+ color: var(--green-dark);
172
248
  border: none;
173
249
  padding: 10px 24px;
174
250
  border-radius: var(--radius);
@@ -181,12 +257,12 @@ select:focus { outline: none; border-color: var(--accent); }
181
257
  align-items: center;
182
258
  gap: 8px;
183
259
  }
184
- .btn:hover { background: #00eabb; transform: translateY(-1px); }
260
+ .btn:hover { background: #71F6BA; transform: translateY(-1px); }
185
261
  .btn:active { transform: translateY(0); }
186
262
  .btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
187
263
 
188
264
  .btn-secondary {
189
- background: var(--bg-input);
265
+ background: transparent;
190
266
  color: var(--accent);
191
267
  border: 1px solid var(--accent-dim);
192
268
  }
@@ -319,6 +395,65 @@ select:focus { outline: none; border-color: var(--accent); }
319
395
  border-radius: 4px;
320
396
  transition: width 0.6s ease, background 0.6s ease;
321
397
  }
398
+ .metrics-grid {
399
+ display: grid;
400
+ grid-template-columns: repeat(3, 1fr);
401
+ gap: 16px;
402
+ margin-top: 24px;
403
+ }
404
+ .metric-card {
405
+ background: var(--bg-input);
406
+ border: 1px solid var(--border);
407
+ border-radius: 10px;
408
+ padding: 20px 16px;
409
+ text-align: center;
410
+ transition: border-color 0.2s;
411
+ }
412
+ .metric-card.primary {
413
+ border-color: var(--accent);
414
+ background: var(--accent-glow);
415
+ }
416
+ .metric-card-value {
417
+ font-family: var(--mono);
418
+ font-size: 28px;
419
+ font-weight: 700;
420
+ line-height: 1;
421
+ }
422
+ .metric-card-name {
423
+ font-size: 13px;
424
+ font-weight: 600;
425
+ color: var(--text);
426
+ margin-top: 10px;
427
+ }
428
+ .metric-card-desc {
429
+ font-size: 11px;
430
+ color: var(--text-muted);
431
+ margin-top: 4px;
432
+ line-height: 1.4;
433
+ }
434
+ .metric-bar {
435
+ width: 100%;
436
+ height: 6px;
437
+ background: var(--bg);
438
+ border-radius: 3px;
439
+ margin-top: 12px;
440
+ overflow: hidden;
441
+ }
442
+ .metric-bar-fill {
443
+ height: 100%;
444
+ border-radius: 3px;
445
+ transition: width 0.6s ease, background 0.6s ease;
446
+ }
447
+ .metric-note {
448
+ text-align: center;
449
+ font-size: 12px;
450
+ color: var(--text-muted);
451
+ margin-top: 16px;
452
+ padding: 10px 16px;
453
+ background: var(--bg-input);
454
+ border-radius: 8px;
455
+ line-height: 1.6;
456
+ }
322
457
 
323
458
  /* Search tab */
324
459
  .search-results {
@@ -400,11 +535,10 @@ select:focus { outline: none; border-color: var(--accent); }
400
535
  .explore-card:hover {
401
536
  border-color: var(--accent);
402
537
  transform: translateY(-2px);
403
- box-shadow: 0 4px 20px rgba(0, 212, 170, 0.1);
538
+ box-shadow: 0 4px 20px rgba(0, 237, 100, 0.1);
404
539
  }
405
540
  .explore-card.expanded {
406
- grid-column: 1 / -1;
407
- cursor: default;
541
+ border-color: var(--accent);
408
542
  }
409
543
 
410
544
  .explore-card-icon {
@@ -423,20 +557,136 @@ select:focus { outline: none; border-color: var(--accent); }
423
557
  }
424
558
  .explore-card-content {
425
559
  display: none;
426
- margin-top: 16px;
560
+ }
561
+ .explore-card-actions {
562
+ display: none;
563
+ }
564
+
565
+ /* Explore modal */
566
+ .explore-modal-overlay {
567
+ position: fixed;
568
+ inset: 0;
569
+ background: rgba(0, 0, 0, 0.7);
570
+ backdrop-filter: blur(4px);
571
+ z-index: 1000;
572
+ display: flex;
573
+ align-items: center;
574
+ justify-content: center;
575
+ opacity: 0;
576
+ pointer-events: none;
577
+ transition: opacity 0.25s ease;
578
+ }
579
+ .explore-modal-overlay.open {
580
+ opacity: 1;
581
+ pointer-events: auto;
582
+ }
583
+ .explore-modal {
584
+ background: var(--bg-surface);
585
+ border: 1px solid var(--border);
586
+ border-radius: 14px;
587
+ max-width: 720px;
588
+ width: 92%;
589
+ max-height: 85vh;
590
+ overflow-y: auto;
591
+ padding: 0;
592
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
593
+ position: relative;
594
+ animation: exploreModalIn 0.2s ease-out;
595
+ }
596
+ @keyframes exploreModalIn {
597
+ from { opacity: 0; transform: scale(0.95) translateY(10px); }
598
+ to { opacity: 1; transform: scale(1) translateY(0); }
599
+ }
600
+ .explore-modal::-webkit-scrollbar { width: 6px; }
601
+ .explore-modal::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
602
+ .explore-modal-header {
603
+ display: flex;
604
+ align-items: center;
605
+ gap: 14px;
606
+ padding: 24px 28px 16px;
607
+ border-bottom: 1px solid var(--border);
608
+ }
609
+ .explore-modal-icon { font-size: 32px; }
610
+ .explore-modal-title {
611
+ font-size: 18px;
612
+ font-weight: 600;
613
+ color: var(--text);
614
+ }
615
+ .explore-modal-summary {
616
+ font-size: 13px;
617
+ color: var(--text-dim);
618
+ margin-top: 2px;
619
+ }
620
+ .explore-modal-close {
621
+ position: absolute;
622
+ top: 16px; right: 18px;
623
+ background: none;
624
+ border: none;
625
+ color: var(--text-dim);
626
+ font-size: 22px;
627
+ cursor: pointer;
628
+ padding: 4px 8px;
629
+ border-radius: 6px;
630
+ transition: all 0.15s;
631
+ z-index: 1;
632
+ }
633
+ .explore-modal-close:hover { background: rgba(255,255,255,0.05); color: var(--text); }
634
+ .explore-modal-body {
635
+ padding: 20px 28px 24px;
427
636
  font-size: 14px;
428
- line-height: 1.7;
637
+ line-height: 1.75;
429
638
  color: var(--text);
430
639
  white-space: pre-wrap;
431
640
  }
432
- .explore-card.expanded .explore-card-content { display: block; }
433
-
434
- .explore-card-actions {
435
- display: none;
641
+ .explore-modal-links {
436
642
  margin-top: 16px;
643
+ padding-top: 14px;
644
+ border-top: 1px solid var(--border);
645
+ }
646
+ .explore-modal-links-title {
647
+ font-size: 11px;
648
+ font-weight: 600;
649
+ color: var(--accent);
650
+ text-transform: uppercase;
651
+ letter-spacing: 0.5px;
652
+ margin-bottom: 6px;
653
+ }
654
+ .explore-modal-links a {
655
+ display: block;
656
+ color: var(--blue);
657
+ font-size: 12px;
658
+ word-break: break-all;
659
+ margin-bottom: 4px;
660
+ text-decoration: none;
661
+ }
662
+ .explore-modal-links a:hover { text-decoration: underline; }
663
+ .explore-modal-tryit {
664
+ margin-top: 14px;
665
+ padding-top: 14px;
666
+ border-top: 1px solid var(--border);
667
+ }
668
+ .explore-modal-tryit-title {
669
+ font-size: 11px;
670
+ font-weight: 600;
671
+ color: var(--accent);
672
+ text-transform: uppercase;
673
+ letter-spacing: 0.5px;
674
+ margin-bottom: 8px;
675
+ }
676
+ .explore-modal-tryit-cmd {
677
+ font-family: var(--mono);
678
+ font-size: 12px;
679
+ color: var(--text-dim);
680
+ background: var(--bg);
681
+ padding: 6px 10px;
682
+ border-radius: 5px;
683
+ margin-bottom: 4px;
684
+ }
685
+ .explore-modal-actions {
686
+ display: flex;
437
687
  gap: 8px;
688
+ padding: 0 28px 24px;
438
689
  }
439
- .explore-card.expanded .explore-card-actions { display: flex; }
440
690
 
441
691
  /* Benchmark tab */
442
692
  .bench-panels { display: flex; gap: 8px; margin-bottom: 16px; }
@@ -490,7 +740,7 @@ select:focus { outline: none; border-color: var(--accent); }
490
740
  padding: 0 10px;
491
741
  font-family: var(--mono);
492
742
  font-size: 12px;
493
- color: #0a0a1a;
743
+ color: var(--green-dark);
494
744
  font-weight: 600;
495
745
  white-space: nowrap;
496
746
  }
@@ -571,10 +821,10 @@ select:focus { outline: none; border-color: var(--accent); }
571
821
  transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
572
822
  display: flex; align-items: center; padding: 0 10px;
573
823
  font-family: var(--mono); font-size: 12px; font-weight: 600;
574
- color: #0a0a1a; white-space: nowrap; min-width: fit-content;
824
+ color: var(--green-dark); white-space: nowrap; min-width: fit-content;
575
825
  }
576
- .quant-bar-fill.storage { background: linear-gradient(90deg, #00d4aa, #4ecdc4); }
577
- .quant-bar-fill.latency { background: linear-gradient(90deg, #45b7d1, #82aaff); }
826
+ .quant-bar-fill.storage { background: linear-gradient(90deg, #00ED64, #71F6BA); }
827
+ .quant-bar-fill.latency { background: linear-gradient(90deg, #0498EC, #016BF8); }
578
828
  .quant-bar-badge {
579
829
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
580
830
  font-size: 12px; color: var(--text-dim); font-family: var(--mono);
@@ -599,9 +849,9 @@ select:focus { outline: none; border-color: var(--accent); }
599
849
  height: 100%; border-radius: 5px;
600
850
  transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
601
851
  }
602
- .quant-meter-fill.perfect { background: linear-gradient(90deg, #00d4aa, #00e4ba); }
603
- .quant-meter-fill.good { background: linear-gradient(90deg, #ffd93d, #ffe066); }
604
- .quant-meter-fill.degraded { background: linear-gradient(90deg, #ff6b6b, #ff8e8e); }
852
+ .quant-meter-fill.perfect { background: linear-gradient(90deg, #00ED64, #71F6BA); }
853
+ .quant-meter-fill.good { background: linear-gradient(90deg, #FFC010, #FFEC9E); }
854
+ .quant-meter-fill.degraded { background: linear-gradient(90deg, #FF6960, #FFCDC7); }
605
855
  .quant-meter-detail { font-size: 11px; color: var(--text-muted); margin-top: 4px; font-family: var(--mono); }
606
856
 
607
857
  .quant-rank-cols {
@@ -695,7 +945,7 @@ select:focus { outline: none; border-color: var(--accent); }
695
945
  font-weight: 600;
696
946
  }
697
947
  .cost-mode-btn:hover:not(.active) {
698
- background: rgba(0, 212, 170, 0.1);
948
+ background: rgba(0, 237, 100, 0.1);
699
949
  color: var(--text);
700
950
  }
701
951
  .cost-select {
@@ -809,7 +1059,7 @@ select:focus { outline: none; border-color: var(--accent); }
809
1059
  .cost-strategy-total-value { font-family: var(--mono); font-size: 18px; font-weight: 700; color: var(--accent); }
810
1060
  .cost-savings {
811
1061
  font-size: 11px;
812
- color: #4ade80;
1062
+ color: var(--success);
813
1063
  font-weight: 600;
814
1064
  margin-top: 6px;
815
1065
  text-align: right;
@@ -835,7 +1085,7 @@ select:focus { outline: none; border-color: var(--accent); }
835
1085
  border-bottom: 1px solid rgba(42, 53, 80, 0.3);
836
1086
  font-family: var(--mono);
837
1087
  }
838
- .cost-table tr:hover { background: rgba(0, 212, 170, 0.03); }
1088
+ .cost-table tr:hover { background: rgba(0, 237, 100, 0.03); }
839
1089
  .cost-highlight {
840
1090
  color: var(--accent);
841
1091
  font-weight: 600;
@@ -862,12 +1112,136 @@ select:focus { outline: none; border-color: var(--accent); }
862
1112
  .cost-tip {
863
1113
  font-size: 12px;
864
1114
  color: var(--text-muted);
865
- background: rgba(0, 212, 170, 0.05);
1115
+ background: rgba(0, 237, 100, 0.05);
866
1116
  border-left: 3px solid var(--accent);
867
1117
  padding: 10px 14px;
868
1118
  border-radius: 0 6px 6px 0;
869
1119
  margin-top: 16px;
870
1120
  }
1121
+ .cost-help-btn {
1122
+ display: inline-flex;
1123
+ align-items: center;
1124
+ justify-content: center;
1125
+ width: 22px; height: 22px;
1126
+ border-radius: 50%;
1127
+ border: 1.5px solid var(--accent);
1128
+ background: transparent;
1129
+ color: var(--accent);
1130
+ font-size: 13px;
1131
+ font-weight: 700;
1132
+ cursor: pointer;
1133
+ margin-left: 8px;
1134
+ transition: all 0.2s;
1135
+ vertical-align: middle;
1136
+ font-family: var(--mono);
1137
+ line-height: 1;
1138
+ }
1139
+ .cost-help-btn:hover {
1140
+ background: var(--accent);
1141
+ color: var(--bg);
1142
+ box-shadow: 0 0 10px var(--accent-glow);
1143
+ }
1144
+ .cost-modal-overlay {
1145
+ position: fixed;
1146
+ inset: 0;
1147
+ background: rgba(0, 0, 0, 0.7);
1148
+ backdrop-filter: blur(4px);
1149
+ z-index: 1000;
1150
+ display: flex;
1151
+ align-items: center;
1152
+ justify-content: center;
1153
+ opacity: 0;
1154
+ pointer-events: none;
1155
+ transition: opacity 0.25s ease;
1156
+ }
1157
+ .cost-modal-overlay.open {
1158
+ opacity: 1;
1159
+ pointer-events: auto;
1160
+ }
1161
+ .cost-modal {
1162
+ background: var(--bg-surface);
1163
+ border: 1px solid var(--border);
1164
+ border-radius: 14px;
1165
+ max-width: 680px;
1166
+ width: 90%;
1167
+ max-height: 85vh;
1168
+ overflow-y: auto;
1169
+ padding: 32px;
1170
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
1171
+ position: relative;
1172
+ }
1173
+ .cost-modal::-webkit-scrollbar { width: 6px; }
1174
+ .cost-modal::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
1175
+ .cost-modal-close {
1176
+ position: absolute;
1177
+ top: 14px; right: 16px;
1178
+ background: none;
1179
+ border: none;
1180
+ color: var(--text-dim);
1181
+ font-size: 22px;
1182
+ cursor: pointer;
1183
+ padding: 4px 8px;
1184
+ border-radius: 6px;
1185
+ transition: all 0.15s;
1186
+ }
1187
+ .cost-modal-close:hover { background: rgba(255,255,255,0.05); color: var(--text); }
1188
+ .cost-modal h2 {
1189
+ font-size: 18px;
1190
+ color: var(--text);
1191
+ margin: 0 0 20px;
1192
+ display: flex;
1193
+ align-items: center;
1194
+ gap: 10px;
1195
+ }
1196
+ .cost-modal h3 {
1197
+ font-size: 14px;
1198
+ color: var(--accent);
1199
+ margin: 22px 0 10px;
1200
+ text-transform: uppercase;
1201
+ letter-spacing: 0.5px;
1202
+ }
1203
+ .cost-modal p, .cost-modal li {
1204
+ font-size: 13px;
1205
+ color: var(--text-dim);
1206
+ line-height: 1.7;
1207
+ }
1208
+ .cost-modal ul { padding-left: 20px; margin: 6px 0; }
1209
+ .cost-modal li { margin-bottom: 4px; }
1210
+ .cost-modal code {
1211
+ background: var(--bg-input);
1212
+ padding: 2px 7px;
1213
+ border-radius: 4px;
1214
+ font-size: 12px;
1215
+ color: var(--accent);
1216
+ font-family: var(--mono);
1217
+ }
1218
+ .cost-modal .formula {
1219
+ background: var(--bg-input);
1220
+ border: 1px solid var(--border);
1221
+ border-radius: 8px;
1222
+ padding: 14px 18px;
1223
+ margin: 10px 0;
1224
+ font-family: var(--mono);
1225
+ font-size: 13px;
1226
+ color: var(--text);
1227
+ line-height: 1.8;
1228
+ }
1229
+ .cost-modal .formula .label {
1230
+ color: var(--text-muted);
1231
+ font-size: 11px;
1232
+ }
1233
+ .cost-modal .formula .accent { color: var(--accent); font-weight: 600; }
1234
+ .cost-modal .example {
1235
+ background: rgba(0, 237, 100, 0.05);
1236
+ border-left: 3px solid var(--accent);
1237
+ border-radius: 0 8px 8px 0;
1238
+ padding: 12px 16px;
1239
+ margin: 12px 0;
1240
+ font-size: 12px;
1241
+ color: var(--text-dim);
1242
+ font-family: var(--mono);
1243
+ line-height: 1.8;
1244
+ }
871
1245
 
872
1246
  /* History chart */
873
1247
  .history-empty {
@@ -954,7 +1328,7 @@ select:focus { outline: none; border-color: var(--accent); }
954
1328
  margin-bottom: 8px;
955
1329
  }
956
1330
  .about-text { font-size: 14px; line-height: 1.8; color: var(--text); }
957
- .about-text a { color: var(--accent); text-decoration: none; }
1331
+ .about-text a { color: var(--blue); text-decoration: none; }
958
1332
  .about-text a:hover { text-decoration: underline; }
959
1333
  .about-disclaimer {
960
1334
  background: rgba(255, 215, 61, 0.08);
@@ -993,6 +1367,7 @@ select:focus { outline: none; border-color: var(--accent); }
993
1367
  <span class="option-label">Default Model</span>
994
1368
  <select id="globalModel" class="nav-model-select"></select>
995
1369
  </div>
1370
+ <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">🌙</button>
996
1371
  <div style="display:flex;align-items:center;gap:6px;">
997
1372
  <div class="status-dot" id="statusDot"></div>
998
1373
  <span class="status-label" id="statusLabel">Checking...</span>
@@ -1112,7 +1487,9 @@ select:focus { outline: none; border-color: var(--accent); }
1112
1487
  <div class="similarity-bar-inner" id="simBar" style="width:0%"></div>
1113
1488
  </div>
1114
1489
  </div>
1115
- <div id="compareStats" style="text-align:center;"></div>
1490
+ <div class="metrics-grid" id="metricsGrid"></div>
1491
+ <div class="metric-note" id="metricNote"></div>
1492
+ <div id="compareStats" style="text-align:center;margin-top:16px;"></div>
1116
1493
  </div>
1117
1494
  </div>
1118
1495
  </div>
@@ -1351,13 +1728,13 @@ Reranking models rescore initial search results to improve relevance ordering.</
1351
1728
  <!-- ── Cost Panel ── -->
1352
1729
  <div class="bench-view" id="bench-cost">
1353
1730
  <div class="card">
1354
- <div class="card-title">💰 RAG Cost Calculator</div>
1731
+ <div class="card-title">💰 RAG Cost Calculator <button class="cost-help-btn" id="costHelpBtn" title="How the math works">?</button></div>
1355
1732
 
1356
1733
  <!-- Mode toggle -->
1357
1734
  <div style="margin-bottom: 20px;">
1358
1735
  <div class="cost-mode-toggle">
1359
- <button class="cost-mode-btn active" data-mode="simple" onclick="setCostMode('simple')">Simple</button>
1360
- <button class="cost-mode-btn" data-mode="rag" onclick="setCostMode('rag')">RAG Planner</button>
1736
+ <button class="cost-mode-btn active" data-mode="simple" id="costModeSimple">Simple</button>
1737
+ <button class="cost-mode-btn" data-mode="rag" id="costModeRag">RAG Planner</button>
1361
1738
  </div>
1362
1739
  </div>
1363
1740
 
@@ -1521,7 +1898,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
1521
1898
  <div class="about-section-title">What You Can Do Here</div>
1522
1899
  <div class="about-text">
1523
1900
  <strong>⚡ Embed</strong> — Generate vector embeddings for any text<br>
1524
- <strong>⚖️ Compare</strong> — Measure cosine similarity between texts<br>
1901
+ <strong>⚖️ Compare</strong> — Measure similarity with cosine, dot product &amp; euclidean distance<br>
1525
1902
  <strong>🔍 Search</strong> — Semantic search with optional reranking<br>
1526
1903
  <strong>⏱ Benchmark</strong> — Compare model latency, ranking quality, and costs<br>
1527
1904
  <strong>📚 Explore</strong> — Learn about embeddings, vector search, RAG, and more
@@ -1554,12 +1931,61 @@ Reranking models rescore initial search results to improve relevance ordering.</
1554
1931
  <div class="explore-grid" id="exploreGrid"></div>
1555
1932
  </div>
1556
1933
 
1934
+ <!-- Explore Concept Modal -->
1935
+ <div class="explore-modal-overlay" id="exploreModal">
1936
+ <div class="explore-modal">
1937
+ <button class="explore-modal-close" id="exploreModalClose">&times;</button>
1938
+ <div class="explore-modal-header">
1939
+ <div class="explore-modal-icon" id="exploreModalIcon"></div>
1940
+ <div>
1941
+ <div class="explore-modal-title" id="exploreModalTitle"></div>
1942
+ <div class="explore-modal-summary" id="exploreModalSummary"></div>
1943
+ </div>
1944
+ </div>
1945
+ <div class="explore-modal-body" id="exploreModalBody"></div>
1946
+ <div class="explore-modal-actions" id="exploreModalActions"></div>
1947
+ </div>
1948
+ </div>
1949
+
1557
1950
  </div><!-- .main -->
1558
1951
 
1952
+ <script>
1953
+ // Apply saved theme immediately to prevent flash
1954
+ (function() {
1955
+ var t = localStorage.getItem('vai-theme') || 'dark';
1956
+ if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
1957
+ })();
1958
+ </script>
1559
1959
  <script>
1560
1960
  (function() {
1561
1961
  'use strict';
1562
1962
 
1963
+ // ── Theme Toggle ──
1964
+ function initThemeToggle() {
1965
+ const toggle = document.getElementById('themeToggle');
1966
+ const saved = localStorage.getItem('vai-theme') || 'dark';
1967
+ let current = saved;
1968
+
1969
+ function applyTheme(theme) {
1970
+ current = theme;
1971
+ if (theme === 'light') {
1972
+ document.documentElement.setAttribute('data-theme', 'light');
1973
+ toggle.textContent = '☀️';
1974
+ toggle.title = 'Switch to dark mode';
1975
+ } else {
1976
+ document.documentElement.removeAttribute('data-theme');
1977
+ toggle.textContent = '🌙';
1978
+ toggle.title = 'Switch to light mode';
1979
+ }
1980
+ localStorage.setItem('vai-theme', theme);
1981
+ }
1982
+
1983
+ applyTheme(current);
1984
+ toggle.addEventListener('click', () => {
1985
+ applyTheme(current === 'dark' ? 'light' : 'dark');
1986
+ });
1987
+ }
1988
+
1563
1989
  // ── State ──
1564
1990
  let allModels = [];
1565
1991
  let embedModels = [];
@@ -1801,27 +2227,73 @@ window.doCompare = async function() {
1801
2227
  const dimensions = dims ? parseInt(dims, 10) : undefined;
1802
2228
 
1803
2229
  const data = await apiPost('/api/similarity', { texts: [a, b], model, dimensions });
1804
- const sim = data.matrix[0][1];
1805
- const pct = Math.max(0, sim * 100);
1806
2230
 
1807
- // Color
1808
- let color;
1809
- if (sim > 0.7) color = 'var(--green)';
1810
- else if (sim > 0.4) color = 'var(--yellow)';
1811
- else color = 'var(--red)';
2231
+ // Get raw embeddings for all metrics
2232
+ const vecA = data.embeddings[0].embedding;
2233
+ const vecB = data.embeddings[1].embedding;
2234
+
2235
+ const cosine = cosineSim(vecA, vecB);
2236
+ const dot = dotProduct(vecA, vecB);
2237
+ const euclid = euclideanDist(vecA, vecB);
2238
+
2239
+ // Hero display — cosine similarity
2240
+ const cosinePct = Math.max(0, cosine * 100);
2241
+ let cosineColor;
2242
+ if (cosine > 0.7) cosineColor = 'var(--green)';
2243
+ else if (cosine > 0.4) cosineColor = 'var(--yellow)';
2244
+ else cosineColor = 'var(--red)';
1812
2245
 
1813
2246
  const scoreEl = document.getElementById('simScore');
1814
- scoreEl.textContent = sim.toFixed(4);
1815
- scoreEl.style.color = color;
2247
+ scoreEl.textContent = cosine.toFixed(4);
2248
+ scoreEl.style.color = cosineColor;
1816
2249
 
1817
2250
  const barEl = document.getElementById('simBar');
1818
- barEl.style.width = pct + '%';
1819
- barEl.style.background = color;
2251
+ barEl.style.width = cosinePct + '%';
2252
+ barEl.style.background = cosineColor;
2253
+
2254
+ // Metric cards — all three
2255
+ const dotColor = dot > 0.7 ? 'var(--green)' : dot > 0.4 ? 'var(--yellow)' : 'var(--red)';
2256
+ // Euclidean: 0 = identical, ~2 = max for normalized vectors. Invert for color.
2257
+ const euclidColor = euclid < 0.6 ? 'var(--green)' : euclid < 1.0 ? 'var(--yellow)' : 'var(--red)';
2258
+ // For euclidean bar, invert: 0 dist = 100% bar, 2.0 dist = 0%
2259
+ const euclidPct = Math.max(0, Math.min(100, (1 - euclid / 2) * 100));
2260
+
2261
+ const metricsEl = document.getElementById('metricsGrid');
2262
+ metricsEl.innerHTML = `
2263
+ <div class="metric-card primary">
2264
+ <div class="metric-card-value" style="color:${cosineColor}">${cosine.toFixed(4)}</div>
2265
+ <div class="metric-card-name">Cosine Similarity</div>
2266
+ <div class="metric-card-desc">Angle between vectors (−1 to 1). Standard for semantic search.</div>
2267
+ <div class="metric-bar"><div class="metric-bar-fill" style="width:${cosinePct}%;background:${cosineColor}"></div></div>
2268
+ </div>
2269
+ <div class="metric-card">
2270
+ <div class="metric-card-value" style="color:${dotColor}">${dot.toFixed(4)}</div>
2271
+ <div class="metric-card-name">Dot Product</div>
2272
+ <div class="metric-card-desc">Sum of element-wise products. Equals cosine for normalized vectors.</div>
2273
+ <div class="metric-bar"><div class="metric-bar-fill" style="width:${Math.max(0, dot * 100)}%;background:${dotColor}"></div></div>
2274
+ </div>
2275
+ <div class="metric-card">
2276
+ <div class="metric-card-value" style="color:${euclidColor}">${euclid.toFixed(4)}</div>
2277
+ <div class="metric-card-name">Euclidean Distance</div>
2278
+ <div class="metric-card-desc">Straight-line distance (0 = identical). Lower is more similar.</div>
2279
+ <div class="metric-bar"><div class="metric-bar-fill" style="width:${euclidPct}%;background:${euclidColor}"></div></div>
2280
+ </div>
2281
+ `;
2282
+
2283
+ // Insight note
2284
+ const noteEl = document.getElementById('metricNote');
2285
+ const diff = Math.abs(cosine - dot);
2286
+ if (diff < 0.001) {
2287
+ noteEl.innerHTML = '💡 <strong>Cosine ≈ Dot Product</strong> — these vectors are L2-normalized (as Voyage AI models produce), so cosine similarity and dot product give identical results. Euclidean distance is <code>√(2 − 2·cosine)</code> for normalized vectors.';
2288
+ } else {
2289
+ noteEl.innerHTML = '💡 Cosine and dot product differ because these vectors are not perfectly L2-normalized. Atlas Vector Search uses cosine by default.';
2290
+ }
1820
2291
 
1821
2292
  // Stats
1822
2293
  const statsEl = document.getElementById('compareStats');
1823
2294
  statsEl.innerHTML = `
1824
2295
  <span class="stat"><span class="stat-label">Model</span><span class="stat-value">${data.model}</span></span>
2296
+ <span class="stat"><span class="stat-label">Dimensions</span><span class="stat-value">${vecA.length}</span></span>
1825
2297
  <span class="stat"><span class="stat-label">Tokens</span><span class="stat-value">${data.usage?.total_tokens || '—'}</span></span>
1826
2298
  `;
1827
2299
 
@@ -1898,6 +2370,18 @@ function cosineSim(a, b) {
1898
2370
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
1899
2371
  }
1900
2372
 
2373
+ function dotProduct(a, b) {
2374
+ let sum = 0;
2375
+ for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
2376
+ return sum;
2377
+ }
2378
+
2379
+ function euclideanDist(a, b) {
2380
+ let sum = 0;
2381
+ for (let i = 0; i < a.length; i++) sum += (a[i] - b[i]) ** 2;
2382
+ return Math.sqrt(sum);
2383
+ }
2384
+
1901
2385
  function renderSearchResults(embResults, rerankResults) {
1902
2386
  const grid = document.getElementById('searchResultGrid');
1903
2387
  grid.innerHTML = '';
@@ -2003,40 +2487,64 @@ function buildExploreCards() {
2003
2487
  card.className = 'explore-card';
2004
2488
  card.dataset.key = key;
2005
2489
 
2006
- // Build links HTML
2007
- let linksHtml = '';
2008
- if (concept.links && concept.links.length > 0) {
2009
- linksHtml = '<div style="margin-top:12px;"><strong style="color:var(--accent);font-size:12px;">LEARN MORE</strong><br>' +
2010
- concept.links.map(url => `<a href="${escapeHtml(url)}" target="_blank" rel="noopener" style="color:var(--accent);font-size:12px;word-break:break-all;">${escapeHtml(url)}</a>`).join('<br>') +
2011
- '</div>';
2012
- }
2013
-
2014
- // Build try-it HTML
2015
- let tryItHtml = '';
2016
- if (concept.tryIt && concept.tryIt.length > 0) {
2017
- tryItHtml = '<div style="margin-top:12px;"><strong style="color:var(--accent);font-size:12px;">TRY IT</strong>' +
2018
- concept.tryIt.map(cmd => `<div style="font-family:var(--mono);font-size:12px;color:var(--text-dim);background:var(--bg);padding:4px 8px;border-radius:4px;margin-top:4px;">$ ${escapeHtml(cmd)}</div>`).join('') +
2019
- '</div>';
2020
- }
2021
-
2022
2490
  card.innerHTML = `
2023
2491
  <div class="explore-card-icon">${meta.icon}</div>
2024
2492
  <div class="explore-card-title">${escapeHtml(concept.title)}</div>
2025
2493
  <div class="explore-card-summary">${escapeHtml(concept.summary)}</div>
2026
- <div class="explore-card-content">${escapeHtml(concept.content)}${linksHtml}${tryItHtml}</div>
2027
- <div class="explore-card-actions">
2028
- <button class="btn btn-small" onclick="tryTopic('${escapeHtml(key)}')">Try it in playground →</button>
2029
- <button class="btn btn-secondary btn-small" onclick="collapseTopic(this)">Collapse</button>
2030
- </div>
2031
2494
  `;
2032
- card.addEventListener('click', function(e) {
2033
- if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A') return;
2034
- if (!this.classList.contains('expanded')) {
2035
- this.classList.add('expanded');
2036
- }
2037
- });
2495
+ card.addEventListener('click', () => openExploreModal(key));
2038
2496
  grid.appendChild(card);
2039
2497
  }
2498
+
2499
+ // Modal close handlers
2500
+ const modal = document.getElementById('exploreModal');
2501
+ document.getElementById('exploreModalClose').addEventListener('click', closeExploreModal);
2502
+ modal.addEventListener('click', (e) => { if (e.target === modal) closeExploreModal(); });
2503
+ }
2504
+
2505
+ function openExploreModal(key) {
2506
+ const concept = exploreConcepts[key];
2507
+ if (!concept) return;
2508
+ const meta = CONCEPT_META[key] || { icon: '📚', tab: 'embed' };
2509
+
2510
+ document.getElementById('exploreModalIcon').textContent = meta.icon;
2511
+ document.getElementById('exploreModalTitle').textContent = concept.title;
2512
+ document.getElementById('exploreModalSummary').textContent = concept.summary;
2513
+
2514
+ // Build body: content + links + tryIt
2515
+ let bodyHtml = escapeHtml(concept.content);
2516
+
2517
+ if (concept.links && concept.links.length > 0) {
2518
+ bodyHtml += '<div class="explore-modal-links">' +
2519
+ '<div class="explore-modal-links-title">Learn More</div>' +
2520
+ concept.links.map(url =>
2521
+ `<a href="${escapeHtml(url)}" target="_blank" rel="noopener">${escapeHtml(url)}</a>`
2522
+ ).join('') + '</div>';
2523
+ }
2524
+
2525
+ if (concept.tryIt && concept.tryIt.length > 0) {
2526
+ bodyHtml += '<div class="explore-modal-tryit">' +
2527
+ '<div class="explore-modal-tryit-title">Try It</div>' +
2528
+ concept.tryIt.map(cmd =>
2529
+ `<div class="explore-modal-tryit-cmd">$ ${escapeHtml(cmd)}</div>`
2530
+ ).join('') + '</div>';
2531
+ }
2532
+
2533
+ document.getElementById('exploreModalBody').innerHTML = bodyHtml;
2534
+
2535
+ // Actions
2536
+ const actionsEl = document.getElementById('exploreModalActions');
2537
+ actionsEl.innerHTML = `<button class="btn btn-small" id="exploreModalTry">Try it in playground →</button>`;
2538
+ actionsEl.querySelector('#exploreModalTry').addEventListener('click', () => {
2539
+ closeExploreModal();
2540
+ if (meta.tab) switchTab(meta.tab);
2541
+ });
2542
+
2543
+ document.getElementById('exploreModal').classList.add('open');
2544
+ }
2545
+
2546
+ function closeExploreModal() {
2547
+ document.getElementById('exploreModal').classList.remove('open');
2040
2548
  }
2041
2549
 
2042
2550
  window.tryTopic = function(key) {
@@ -2044,10 +2552,6 @@ window.tryTopic = function(key) {
2044
2552
  if (meta) switchTab(meta.tab);
2045
2553
  };
2046
2554
 
2047
- window.collapseTopic = function(btn) {
2048
- btn.closest('.explore-card').classList.remove('expanded');
2049
- };
2050
-
2051
2555
  window.filterExplore = function() {
2052
2556
  const q = document.getElementById('exploreSearch').value.toLowerCase().trim();
2053
2557
  document.querySelectorAll('#exploreGrid .explore-card').forEach(card => {
@@ -2115,8 +2619,8 @@ const BENCH_SAMPLE_TEXTS = [
2115
2619
  ];
2116
2620
 
2117
2621
  const MODEL_COLORS = [
2118
- '#00d4aa', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffd93d',
2119
- '#ff6b6b', '#c792ea', '#f78c6c', '#82aaff', '#c3e88d',
2622
+ '#00ED64', '#71F6BA', '#0498EC', '#B45AF2', '#FFC010',
2623
+ '#FF6960', '#B45AF2', '#FFC010', '#016BF8', '#C0FAE6',
2120
2624
  ];
2121
2625
 
2122
2626
  window.doBenchLatency = async function() {
@@ -2457,7 +2961,7 @@ window.doBenchQuantization = async function() {
2457
2961
  const baseline = completed.find(r => r.dtype === 'float') || completed[0];
2458
2962
  const maxBytes = Math.max(...completed.map(r => r.bytesPerVec));
2459
2963
  const maxLatency = Math.max(...completed.map(r => r.latency));
2460
- const DTYPE_COLORS = { float: '#00d4aa', int8: '#4ecdc4', uint8: '#45b7d1', ubinary: '#ffd93d', binary: '#ff6b6b' };
2964
+ const DTYPE_COLORS = { float: '#00ED64', int8: '#71F6BA', uint8: '#0498EC', ubinary: '#FFC010', binary: '#FF6960' };
2461
2965
 
2462
2966
  // ── Storage Bar Chart ──
2463
2967
  let storageHTML = '';
@@ -2468,7 +2972,7 @@ window.doBenchQuantization = async function() {
2468
2972
  const savings = r.bytesPerVec < baseline.bytesPerVec
2469
2973
  ? `${(baseline.bytesPerVec / r.bytesPerVec).toFixed(0)}× smaller`
2470
2974
  : 'baseline';
2471
- const color = DTYPE_COLORS[r.dtype] || '#82aaff';
2975
+ const color = DTYPE_COLORS[r.dtype] || '#0498EC';
2472
2976
  storageHTML += `<div class="quant-bar-group">
2473
2977
  <div class="quant-bar-label">
2474
2978
  <span class="dtype-name">${r.dtype}</span>
@@ -2486,7 +2990,7 @@ window.doBenchQuantization = async function() {
2486
2990
  const minLatency = Math.min(...completed.map(r => r.latency));
2487
2991
  for (const r of completed) {
2488
2992
  const pct = Math.max(8, (r.latency / maxLatency) * 100);
2489
- const color = DTYPE_COLORS[r.dtype] || '#82aaff';
2993
+ const color = DTYPE_COLORS[r.dtype] || '#0498EC';
2490
2994
  const badge = r.latency === minLatency ? ' ⚡' : '';
2491
2995
  latencyHTML += `<div class="quant-bar-group">
2492
2996
  <div class="quant-bar-label">
@@ -2621,6 +3125,22 @@ function setCostMode(mode) {
2621
3125
  // ── Simple Mode (query-only comparison) ──
2622
3126
 
2623
3127
  function initCostCalculator() {
3128
+ // Mode toggle buttons
3129
+ document.getElementById('costModeSimple').addEventListener('click', () => setCostMode('simple'));
3130
+ document.getElementById('costModeRag').addEventListener('click', () => setCostMode('rag'));
3131
+
3132
+ // Help modal
3133
+ const helpModal = document.getElementById('costHelpModal');
3134
+ document.getElementById('costHelpBtn').addEventListener('click', () => helpModal.classList.add('open'));
3135
+ document.getElementById('costHelpClose').addEventListener('click', () => helpModal.classList.remove('open'));
3136
+ helpModal.addEventListener('click', (e) => { if (e.target === helpModal) helpModal.classList.remove('open'); });
3137
+ document.addEventListener('keydown', (e) => {
3138
+ if (e.key === 'Escape') {
3139
+ helpModal.classList.remove('open');
3140
+ closeExploreModal();
3141
+ }
3142
+ });
3143
+
2624
3144
  // Simple mode sliders
2625
3145
  const tokSlider = document.getElementById('costTokens');
2626
3146
  const qSlider = document.getElementById('costQueries');
@@ -2829,7 +3349,7 @@ function updateRagCalculator() {
2829
3349
  </div>
2830
3350
  <div class="cost-summary-card">
2831
3351
  <div class="cost-summary-label">Max potential savings</div>
2832
- <div class="cost-summary-value" style="color:#4ade80">${maxCost > 0 ? ((1 - strategies[0].totalCost / maxCost) * 100).toFixed(0) + '%' : '0%'}</div>
3352
+ <div class="cost-summary-value" style="color:var(--success)">${maxCost > 0 ? ((1 - strategies[0].totalCost / maxCost) * 100).toFixed(0) + '%' : '0%'}</div>
2833
3353
  <div class="cost-summary-detail">vs ${strategies[strategies.length - 1].name.split(':')[1]?.trim() || 'most expensive'}</div>
2834
3354
  </div>
2835
3355
  `;
@@ -2985,6 +3505,7 @@ window.clearHistory = function() {
2985
3505
  const _origInit = init;
2986
3506
  init = async function() {
2987
3507
  await _origInit();
3508
+ initThemeToggle();
2988
3509
  buildModelCheckboxes();
2989
3510
  populateBenchRankSelects();
2990
3511
  populateQuantModelSelect();
@@ -2996,5 +3517,84 @@ init = async function() {
2996
3517
  init();
2997
3518
  })();
2998
3519
  </script>
3520
+ <!-- Cost Help Modal -->
3521
+ <div class="cost-modal-overlay" id="costHelpModal">
3522
+ <div class="cost-modal">
3523
+ <button class="cost-modal-close" id="costHelpClose">&times;</button>
3524
+
3525
+ <h2>📐 How the Cost Calculator Works</h2>
3526
+
3527
+ <p>Voyage AI charges per <strong>million tokens</strong> processed. A token is roughly ¾ of a word.
3528
+ The calculator estimates your total embedding cost based on how many documents you embed
3529
+ and how many queries you run over time.</p>
3530
+
3531
+ <h3>💡 Simple Mode</h3>
3532
+ <p>Compares the per-model query cost for a given volume. Useful for quick "which model is cheapest?" checks.</p>
3533
+ <div class="formula">
3534
+ <span class="label">Daily cost =</span><br>
3535
+ <span class="accent">tokens_per_query</span> × <span class="accent">queries_per_day</span> ÷ 1,000,000 × <span class="accent">price_per_M_tokens</span><br><br>
3536
+ <span class="label">Monthly cost =</span> daily cost × 30
3537
+ </div>
3538
+
3539
+ <h3>📊 RAG Planner Mode</h3>
3540
+ <p>Models the full cost of a Retrieval-Augmented Generation (RAG) pipeline, separating
3541
+ the <strong>one-time</strong> document ingestion cost from the <strong>recurring</strong> query cost.</p>
3542
+
3543
+ <div class="formula">
3544
+ <span class="label">Document embedding (one-time):</span><br>
3545
+ <span class="accent">doc_cost</span> = num_docs × tokens_per_doc ÷ 1,000,000 × <span class="accent">doc_model_price</span><br><br>
3546
+ <span class="label">Query embedding (monthly):</span><br>
3547
+ <span class="accent">query_cost/mo</span> = queries_per_month × tokens_per_query ÷ 1,000,000 × <span class="accent">query_model_price</span><br><br>
3548
+ <span class="label">Projected total:</span><br>
3549
+ <span class="accent">total</span> = doc_cost + (query_cost/mo × <span class="accent">months</span>)
3550
+ </div>
3551
+
3552
+ <h3>⚖️ Three Strategies Compared</h3>
3553
+ <ul>
3554
+ <li><strong>Symmetric</strong> — same model for documents and queries. Simple but expensive at scale,
3555
+ because query-heavy workloads pay the full model price on every request.</li>
3556
+ <li><strong>Asymmetric (★ Recommended)</strong> — use a high-quality model (e.g. <code>voyage-4-large</code>)
3557
+ for documents and a cheaper model (e.g. <code>voyage-4-lite</code>) for queries.
3558
+ This works because Voyage 4 models share the same embedding space — vectors from different
3559
+ models are directly comparable.</li>
3560
+ <li><strong>Asymmetric + Local</strong> — embed documents via the API, but run queries locally using
3561
+ <code>voyage-4-nano</code> on HuggingFace (free). Query cost drops to $0.</li>
3562
+ </ul>
3563
+
3564
+ <h3>🔗 Shared Embedding Space</h3>
3565
+ <p>The Voyage 4 family (<code>voyage-4-large</code>, <code>voyage-4</code>, <code>voyage-4-lite</code>,
3566
+ <code>voyage-4-nano</code>) all produce vectors in the <em>same geometric space</em>.
3567
+ A document embedded with <code>voyage-4-large</code> can be searched with a query embedded by
3568
+ <code>voyage-4-lite</code> — cosine similarity still works correctly. This is what makes
3569
+ asymmetric strategies possible.</p>
3570
+
3571
+ <div class="example">
3572
+ <strong>Example:</strong> 100K docs × 500 tok = 50M doc tokens<br>
3573
+ 1M queries/mo × 30 tok = 30M query tokens/mo<br><br>
3574
+ <strong>Symmetric</strong> (voyage-4-large @ $0.18/1M):<br>
3575
+ &nbsp;&nbsp;Docs: $9.00 + Queries: $5.40/mo × 12 = <strong>$73.80</strong><br><br>
3576
+ <strong>Asymmetric</strong> (large docs + lite queries @ $0.05/1M):<br>
3577
+ &nbsp;&nbsp;Docs: $9.00 + Queries: $1.50/mo × 12 = <strong>$27.00</strong><br><br>
3578
+ &nbsp;&nbsp;Savings: <strong>63%</strong> — same document quality, cheaper queries.
3579
+ </div>
3580
+
3581
+ <h3>📋 Per-Model Table</h3>
3582
+ <p>The bottom table shows what it would cost to use each model symmetrically (same model for
3583
+ docs and queries). The relative bar shows cost compared to the most expensive option.
3584
+ Use this to understand the price spread across the full model lineup.</p>
3585
+
3586
+ <h3>🎯 Key Assumptions</h3>
3587
+ <ul>
3588
+ <li>Token counts are estimates — actual counts depend on your text. Use <code>vai chunk --stats</code> to measure real token counts.</li>
3589
+ <li>Document embedding is a one-time cost (you embed once, search many times).</li>
3590
+ <li>Re-embedding (e.g. updated docs) is not modeled — add a buffer if your corpus changes frequently.</li>
3591
+ <li>Reranking costs are separate and not included here. Reranking is priced per query pair, not per token.</li>
3592
+ </ul>
3593
+
3594
+ <p style="margin-top:20px;font-size:12px;color:var(--text-muted);">
3595
+ CLI equivalent: <code>vai estimate --docs 100K --queries 1M --doc-model voyage-4-large --query-model voyage-4-lite</code>
3596
+ </p>
3597
+ </div>
3598
+ </div>
2999
3599
  </body>
3000
3600
  </html>