voyageai-cli 1.18.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.
@@ -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 {
@@ -629,16 +879,22 @@ select:focus { outline: none; border-color: var(--accent); }
629
879
  .quant-rank-score { color: var(--text-muted); font-size: 11px; font-family: var(--mono); margin-top: 3px; }
630
880
 
631
881
  /* Cost calculator */
882
+ .cost-controls {
883
+ display: grid;
884
+ grid-template-columns: 1fr 1fr;
885
+ gap: 16px 32px;
886
+ margin-bottom: 20px;
887
+ }
888
+ .cost-controls-full { grid-column: 1 / -1; }
632
889
  .cost-slider-row {
633
890
  display: flex;
634
891
  align-items: center;
635
- gap: 16px;
636
- margin-bottom: 16px;
892
+ gap: 12px;
637
893
  }
638
894
  .cost-slider-label {
639
- font-size: 13px;
895
+ font-size: 12px;
640
896
  color: var(--text-dim);
641
- min-width: 130px;
897
+ min-width: 110px;
642
898
  }
643
899
  .cost-slider {
644
900
  flex: 1;
@@ -659,12 +915,155 @@ select:focus { outline: none; border-color: var(--accent); }
659
915
  }
660
916
  .cost-slider-value {
661
917
  font-family: var(--mono);
662
- font-size: 14px;
918
+ font-size: 13px;
663
919
  color: var(--accent);
664
- min-width: 80px;
920
+ min-width: 70px;
665
921
  text-align: right;
666
922
  font-weight: 600;
667
923
  }
924
+ .cost-mode-toggle {
925
+ display: flex;
926
+ gap: 0;
927
+ border: 1px solid var(--border);
928
+ border-radius: 8px;
929
+ overflow: hidden;
930
+ width: fit-content;
931
+ }
932
+ .cost-mode-btn {
933
+ padding: 8px 20px;
934
+ background: transparent;
935
+ border: none;
936
+ color: var(--text-dim);
937
+ font-size: 13px;
938
+ cursor: pointer;
939
+ transition: all 0.2s;
940
+ font-family: var(--mono);
941
+ }
942
+ .cost-mode-btn.active {
943
+ background: var(--accent);
944
+ color: var(--bg);
945
+ font-weight: 600;
946
+ }
947
+ .cost-mode-btn:hover:not(.active) {
948
+ background: rgba(0, 237, 100, 0.1);
949
+ color: var(--text);
950
+ }
951
+ .cost-select {
952
+ background: var(--bg-input);
953
+ border: 1px solid var(--border);
954
+ border-radius: 6px;
955
+ color: var(--text);
956
+ font-family: var(--mono);
957
+ font-size: 12px;
958
+ padding: 6px 10px;
959
+ min-width: 160px;
960
+ }
961
+ .cost-select:focus { border-color: var(--accent); outline: none; }
962
+ .cost-model-row {
963
+ display: flex;
964
+ align-items: center;
965
+ gap: 12px;
966
+ margin-bottom: 12px;
967
+ }
968
+ .cost-model-label {
969
+ font-size: 12px;
970
+ color: var(--text-dim);
971
+ min-width: 110px;
972
+ }
973
+ .cost-summary {
974
+ display: grid;
975
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
976
+ gap: 12px;
977
+ margin-bottom: 20px;
978
+ }
979
+ .cost-summary-card {
980
+ background: var(--bg-input);
981
+ border-radius: 8px;
982
+ padding: 14px 16px;
983
+ border: 1px solid var(--border);
984
+ }
985
+ .cost-summary-label {
986
+ font-size: 11px;
987
+ color: var(--text-muted);
988
+ text-transform: uppercase;
989
+ letter-spacing: 0.5px;
990
+ margin-bottom: 4px;
991
+ }
992
+ .cost-summary-value {
993
+ font-family: var(--mono);
994
+ font-size: 20px;
995
+ font-weight: 700;
996
+ color: var(--accent);
997
+ }
998
+ .cost-summary-detail {
999
+ font-size: 11px;
1000
+ color: var(--text-dim);
1001
+ margin-top: 4px;
1002
+ font-family: var(--mono);
1003
+ }
1004
+ .cost-strategy-cards {
1005
+ display: grid;
1006
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
1007
+ gap: 16px;
1008
+ margin-top: 16px;
1009
+ }
1010
+ .cost-strategy {
1011
+ background: var(--bg-input);
1012
+ border-radius: 10px;
1013
+ padding: 18px;
1014
+ border: 1px solid var(--border);
1015
+ transition: border-color 0.2s, box-shadow 0.2s;
1016
+ position: relative;
1017
+ }
1018
+ .cost-strategy.recommended {
1019
+ border-color: var(--accent);
1020
+ box-shadow: 0 0 16px var(--accent-glow);
1021
+ }
1022
+ .cost-strategy-badge {
1023
+ position: absolute;
1024
+ top: -10px;
1025
+ right: 16px;
1026
+ background: var(--accent);
1027
+ color: var(--bg);
1028
+ font-size: 10px;
1029
+ font-weight: 700;
1030
+ padding: 3px 10px;
1031
+ border-radius: 10px;
1032
+ text-transform: uppercase;
1033
+ letter-spacing: 0.5px;
1034
+ }
1035
+ .cost-strategy-name {
1036
+ font-size: 14px;
1037
+ font-weight: 600;
1038
+ color: var(--text);
1039
+ margin-bottom: 12px;
1040
+ }
1041
+ .cost-strategy-row {
1042
+ display: flex;
1043
+ justify-content: space-between;
1044
+ align-items: center;
1045
+ padding: 4px 0;
1046
+ font-size: 12px;
1047
+ }
1048
+ .cost-strategy-row-label { color: var(--text-dim); }
1049
+ .cost-strategy-row-value { font-family: var(--mono); color: var(--text); font-weight: 500; }
1050
+ .cost-strategy-total {
1051
+ border-top: 1px solid var(--border);
1052
+ margin-top: 10px;
1053
+ padding-top: 10px;
1054
+ display: flex;
1055
+ justify-content: space-between;
1056
+ align-items: center;
1057
+ }
1058
+ .cost-strategy-total-label { font-size: 13px; font-weight: 600; color: var(--text); }
1059
+ .cost-strategy-total-value { font-family: var(--mono); font-size: 18px; font-weight: 700; color: var(--accent); }
1060
+ .cost-savings {
1061
+ font-size: 11px;
1062
+ color: var(--success);
1063
+ font-weight: 600;
1064
+ margin-top: 6px;
1065
+ text-align: right;
1066
+ }
668
1067
  .cost-table {
669
1068
  width: 100%;
670
1069
  border-collapse: collapse;
@@ -686,7 +1085,7 @@ select:focus { outline: none; border-color: var(--accent); }
686
1085
  border-bottom: 1px solid rgba(42, 53, 80, 0.3);
687
1086
  font-family: var(--mono);
688
1087
  }
689
- .cost-table tr:hover { background: rgba(0, 212, 170, 0.03); }
1088
+ .cost-table tr:hover { background: rgba(0, 237, 100, 0.03); }
690
1089
  .cost-highlight {
691
1090
  color: var(--accent);
692
1091
  font-weight: 600;
@@ -701,6 +1100,148 @@ select:focus { outline: none; border-color: var(--accent); }
701
1100
  border-radius: 3px;
702
1101
  transition: width 0.4s ease;
703
1102
  }
1103
+ .cost-section-title {
1104
+ font-size: 13px;
1105
+ font-weight: 600;
1106
+ color: var(--text);
1107
+ margin: 20px 0 8px;
1108
+ display: flex;
1109
+ align-items: center;
1110
+ gap: 8px;
1111
+ }
1112
+ .cost-tip {
1113
+ font-size: 12px;
1114
+ color: var(--text-muted);
1115
+ background: rgba(0, 237, 100, 0.05);
1116
+ border-left: 3px solid var(--accent);
1117
+ padding: 10px 14px;
1118
+ border-radius: 0 6px 6px 0;
1119
+ margin-top: 16px;
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
+ }
704
1245
 
705
1246
  /* History chart */
706
1247
  .history-empty {
@@ -787,7 +1328,7 @@ select:focus { outline: none; border-color: var(--accent); }
787
1328
  margin-bottom: 8px;
788
1329
  }
789
1330
  .about-text { font-size: 14px; line-height: 1.8; color: var(--text); }
790
- .about-text a { color: var(--accent); text-decoration: none; }
1331
+ .about-text a { color: var(--blue); text-decoration: none; }
791
1332
  .about-text a:hover { text-decoration: underline; }
792
1333
  .about-disclaimer {
793
1334
  background: rgba(255, 215, 61, 0.08);
@@ -826,6 +1367,7 @@ select:focus { outline: none; border-color: var(--accent); }
826
1367
  <span class="option-label">Default Model</span>
827
1368
  <select id="globalModel" class="nav-model-select"></select>
828
1369
  </div>
1370
+ <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">🌙</button>
829
1371
  <div style="display:flex;align-items:center;gap:6px;">
830
1372
  <div class="status-dot" id="statusDot"></div>
831
1373
  <span class="status-label" id="statusLabel">Checking...</span>
@@ -945,7 +1487,9 @@ select:focus { outline: none; border-color: var(--accent); }
945
1487
  <div class="similarity-bar-inner" id="simBar" style="width:0%"></div>
946
1488
  </div>
947
1489
  </div>
948
- <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>
949
1493
  </div>
950
1494
  </div>
951
1495
  </div>
@@ -1184,30 +1728,116 @@ Reranking models rescore initial search results to improve relevance ordering.</
1184
1728
  <!-- ── Cost Panel ── -->
1185
1729
  <div class="bench-view" id="bench-cost">
1186
1730
  <div class="card">
1187
- <div class="card-title">Cost Calculator</div>
1188
- <div class="cost-slider-row">
1189
- <span class="cost-slider-label">Tokens per query</span>
1190
- <input type="range" class="cost-slider" id="costTokens" min="50" max="5000" value="500" step="50">
1191
- <span class="cost-slider-value" id="costTokensValue">500</span>
1731
+ <div class="card-title">💰 RAG Cost Calculator <button class="cost-help-btn" id="costHelpBtn" title="How the math works">?</button></div>
1732
+
1733
+ <!-- Mode toggle -->
1734
+ <div style="margin-bottom: 20px;">
1735
+ <div class="cost-mode-toggle">
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>
1738
+ </div>
1739
+ </div>
1740
+
1741
+ <!-- Simple mode (query cost comparison) -->
1742
+ <div id="costSimpleMode">
1743
+ <div class="cost-controls cost-controls-full">
1744
+ <div class="cost-slider-row">
1745
+ <span class="cost-slider-label">Tokens / query</span>
1746
+ <input type="range" class="cost-slider" id="costTokens" min="50" max="5000" value="500" step="50">
1747
+ <span class="cost-slider-value" id="costTokensValue">500</span>
1748
+ </div>
1749
+ <div class="cost-slider-row">
1750
+ <span class="cost-slider-label">Queries / day</span>
1751
+ <input type="range" class="cost-slider" id="costQueries" min="10" max="500000" value="1000" step="10">
1752
+ <span class="cost-slider-value" id="costQueriesValue">1,000</span>
1753
+ </div>
1754
+ </div>
1755
+ <table class="cost-table" id="costTable">
1756
+ <thead>
1757
+ <tr>
1758
+ <th>Model</th>
1759
+ <th>Type</th>
1760
+ <th>$/1M tokens</th>
1761
+ <th>Daily Cost</th>
1762
+ <th>Monthly Cost</th>
1763
+ <th style="width:30%">Relative</th>
1764
+ </tr>
1765
+ </thead>
1766
+ <tbody id="costTableBody"></tbody>
1767
+ </table>
1192
1768
  </div>
1193
- <div class="cost-slider-row">
1194
- <span class="cost-slider-label">Queries per day</span>
1195
- <input type="range" class="cost-slider" id="costQueries" min="10" max="500000" value="1000" step="10">
1196
- <span class="cost-slider-value" id="costQueriesValue">1,000</span>
1769
+
1770
+ <!-- RAG Planner mode (full TCO) -->
1771
+ <div id="costRagMode" style="display:none;">
1772
+ <div class="cost-section-title">📄 Documents (one-time ingestion)</div>
1773
+ <div class="cost-controls">
1774
+ <div class="cost-slider-row">
1775
+ <span class="cost-slider-label">Documents</span>
1776
+ <input type="range" class="cost-slider" id="ragDocs" min="1000" max="10000000" value="100000" step="1000">
1777
+ <span class="cost-slider-value" id="ragDocsValue">100K</span>
1778
+ </div>
1779
+ <div class="cost-slider-row">
1780
+ <span class="cost-slider-label">Tokens / doc</span>
1781
+ <input type="range" class="cost-slider" id="ragDocTokens" min="50" max="5000" value="500" step="50">
1782
+ <span class="cost-slider-value" id="ragDocTokensValue">500</span>
1783
+ </div>
1784
+ </div>
1785
+
1786
+ <div class="cost-section-title">🔍 Queries (recurring)</div>
1787
+ <div class="cost-controls">
1788
+ <div class="cost-slider-row">
1789
+ <span class="cost-slider-label">Queries / month</span>
1790
+ <input type="range" class="cost-slider" id="ragQueries" min="1000" max="50000000" value="1000000" step="1000">
1791
+ <span class="cost-slider-value" id="ragQueriesValue">1M</span>
1792
+ </div>
1793
+ <div class="cost-slider-row">
1794
+ <span class="cost-slider-label">Tokens / query</span>
1795
+ <input type="range" class="cost-slider" id="ragQueryTokens" min="10" max="500" value="30" step="5">
1796
+ <span class="cost-slider-value" id="ragQueryTokensValue">30</span>
1797
+ </div>
1798
+ </div>
1799
+
1800
+ <div class="cost-section-title">⚙️ Configuration</div>
1801
+ <div class="cost-controls">
1802
+ <div class="cost-model-row">
1803
+ <span class="cost-model-label">Doc model</span>
1804
+ <select class="cost-select" id="ragDocModel"></select>
1805
+ </div>
1806
+ <div class="cost-model-row">
1807
+ <span class="cost-model-label">Query model</span>
1808
+ <select class="cost-select" id="ragQueryModel"></select>
1809
+ </div>
1810
+ <div class="cost-slider-row">
1811
+ <span class="cost-slider-label">Projection</span>
1812
+ <input type="range" class="cost-slider" id="ragMonths" min="1" max="36" value="12" step="1">
1813
+ <span class="cost-slider-value" id="ragMonthsValue">12 mo</span>
1814
+ </div>
1815
+ </div>
1816
+
1817
+ <!-- Summary cards -->
1818
+ <div class="cost-summary" id="ragSummary"></div>
1819
+
1820
+ <!-- Strategy comparison -->
1821
+ <div class="cost-section-title">📊 Strategy Comparison</div>
1822
+ <div class="cost-strategy-cards" id="ragStrategies"></div>
1823
+
1824
+ <!-- Per-model table -->
1825
+ <div class="cost-section-title">📋 Per-Model Breakdown</div>
1826
+ <table class="cost-table" id="ragTable">
1827
+ <thead>
1828
+ <tr>
1829
+ <th>Model</th>
1830
+ <th>Doc Cost</th>
1831
+ <th>Query $/mo</th>
1832
+ <th>Total (projected)</th>
1833
+ <th style="width:25%">Relative</th>
1834
+ </tr>
1835
+ </thead>
1836
+ <tbody id="ragTableBody"></tbody>
1837
+ </table>
1838
+
1839
+ <div class="cost-tip" id="ragTip"></div>
1197
1840
  </div>
1198
- <table class="cost-table" id="costTable">
1199
- <thead>
1200
- <tr>
1201
- <th>Model</th>
1202
- <th>Type</th>
1203
- <th>$/1M tokens</th>
1204
- <th>Daily Cost</th>
1205
- <th>Monthly Cost</th>
1206
- <th style="width:30%">Relative</th>
1207
- </tr>
1208
- </thead>
1209
- <tbody id="costTableBody"></tbody>
1210
- </table>
1211
1841
  </div>
1212
1842
  </div>
1213
1843
 
@@ -1268,7 +1898,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
1268
1898
  <div class="about-section-title">What You Can Do Here</div>
1269
1899
  <div class="about-text">
1270
1900
  <strong>⚡ Embed</strong> — Generate vector embeddings for any text<br>
1271
- <strong>⚖️ Compare</strong> — Measure cosine similarity between texts<br>
1901
+ <strong>⚖️ Compare</strong> — Measure similarity with cosine, dot product &amp; euclidean distance<br>
1272
1902
  <strong>🔍 Search</strong> — Semantic search with optional reranking<br>
1273
1903
  <strong>⏱ Benchmark</strong> — Compare model latency, ranking quality, and costs<br>
1274
1904
  <strong>📚 Explore</strong> — Learn about embeddings, vector search, RAG, and more
@@ -1301,12 +1931,61 @@ Reranking models rescore initial search results to improve relevance ordering.</
1301
1931
  <div class="explore-grid" id="exploreGrid"></div>
1302
1932
  </div>
1303
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
+
1304
1950
  </div><!-- .main -->
1305
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>
1306
1959
  <script>
1307
1960
  (function() {
1308
1961
  'use strict';
1309
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
+
1310
1989
  // ── State ──
1311
1990
  let allModels = [];
1312
1991
  let embedModels = [];
@@ -1548,27 +2227,73 @@ window.doCompare = async function() {
1548
2227
  const dimensions = dims ? parseInt(dims, 10) : undefined;
1549
2228
 
1550
2229
  const data = await apiPost('/api/similarity', { texts: [a, b], model, dimensions });
1551
- const sim = data.matrix[0][1];
1552
- const pct = Math.max(0, sim * 100);
1553
2230
 
1554
- // Color
1555
- let color;
1556
- if (sim > 0.7) color = 'var(--green)';
1557
- else if (sim > 0.4) color = 'var(--yellow)';
1558
- 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)';
1559
2245
 
1560
2246
  const scoreEl = document.getElementById('simScore');
1561
- scoreEl.textContent = sim.toFixed(4);
1562
- scoreEl.style.color = color;
2247
+ scoreEl.textContent = cosine.toFixed(4);
2248
+ scoreEl.style.color = cosineColor;
1563
2249
 
1564
2250
  const barEl = document.getElementById('simBar');
1565
- barEl.style.width = pct + '%';
1566
- 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
+ }
1567
2291
 
1568
2292
  // Stats
1569
2293
  const statsEl = document.getElementById('compareStats');
1570
2294
  statsEl.innerHTML = `
1571
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>
1572
2297
  <span class="stat"><span class="stat-label">Tokens</span><span class="stat-value">${data.usage?.total_tokens || '—'}</span></span>
1573
2298
  `;
1574
2299
 
@@ -1645,6 +2370,18 @@ function cosineSim(a, b) {
1645
2370
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
1646
2371
  }
1647
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
+
1648
2385
  function renderSearchResults(embResults, rerankResults) {
1649
2386
  const grid = document.getElementById('searchResultGrid');
1650
2387
  grid.innerHTML = '';
@@ -1750,40 +2487,64 @@ function buildExploreCards() {
1750
2487
  card.className = 'explore-card';
1751
2488
  card.dataset.key = key;
1752
2489
 
1753
- // Build links HTML
1754
- let linksHtml = '';
1755
- if (concept.links && concept.links.length > 0) {
1756
- linksHtml = '<div style="margin-top:12px;"><strong style="color:var(--accent);font-size:12px;">LEARN MORE</strong><br>' +
1757
- 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>') +
1758
- '</div>';
1759
- }
1760
-
1761
- // Build try-it HTML
1762
- let tryItHtml = '';
1763
- if (concept.tryIt && concept.tryIt.length > 0) {
1764
- tryItHtml = '<div style="margin-top:12px;"><strong style="color:var(--accent);font-size:12px;">TRY IT</strong>' +
1765
- 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('') +
1766
- '</div>';
1767
- }
1768
-
1769
2490
  card.innerHTML = `
1770
2491
  <div class="explore-card-icon">${meta.icon}</div>
1771
2492
  <div class="explore-card-title">${escapeHtml(concept.title)}</div>
1772
2493
  <div class="explore-card-summary">${escapeHtml(concept.summary)}</div>
1773
- <div class="explore-card-content">${escapeHtml(concept.content)}${linksHtml}${tryItHtml}</div>
1774
- <div class="explore-card-actions">
1775
- <button class="btn btn-small" onclick="tryTopic('${escapeHtml(key)}')">Try it in playground →</button>
1776
- <button class="btn btn-secondary btn-small" onclick="collapseTopic(this)">Collapse</button>
1777
- </div>
1778
2494
  `;
1779
- card.addEventListener('click', function(e) {
1780
- if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A') return;
1781
- if (!this.classList.contains('expanded')) {
1782
- this.classList.add('expanded');
1783
- }
1784
- });
2495
+ card.addEventListener('click', () => openExploreModal(key));
1785
2496
  grid.appendChild(card);
1786
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');
1787
2548
  }
1788
2549
 
1789
2550
  window.tryTopic = function(key) {
@@ -1791,10 +2552,6 @@ window.tryTopic = function(key) {
1791
2552
  if (meta) switchTab(meta.tab);
1792
2553
  };
1793
2554
 
1794
- window.collapseTopic = function(btn) {
1795
- btn.closest('.explore-card').classList.remove('expanded');
1796
- };
1797
-
1798
2555
  window.filterExplore = function() {
1799
2556
  const q = document.getElementById('exploreSearch').value.toLowerCase().trim();
1800
2557
  document.querySelectorAll('#exploreGrid .explore-card').forEach(card => {
@@ -1862,8 +2619,8 @@ const BENCH_SAMPLE_TEXTS = [
1862
2619
  ];
1863
2620
 
1864
2621
  const MODEL_COLORS = [
1865
- '#00d4aa', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffd93d',
1866
- '#ff6b6b', '#c792ea', '#f78c6c', '#82aaff', '#c3e88d',
2622
+ '#00ED64', '#71F6BA', '#0498EC', '#B45AF2', '#FFC010',
2623
+ '#FF6960', '#B45AF2', '#FFC010', '#016BF8', '#C0FAE6',
1867
2624
  ];
1868
2625
 
1869
2626
  window.doBenchLatency = async function() {
@@ -2204,7 +2961,7 @@ window.doBenchQuantization = async function() {
2204
2961
  const baseline = completed.find(r => r.dtype === 'float') || completed[0];
2205
2962
  const maxBytes = Math.max(...completed.map(r => r.bytesPerVec));
2206
2963
  const maxLatency = Math.max(...completed.map(r => r.latency));
2207
- 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' };
2208
2965
 
2209
2966
  // ── Storage Bar Chart ──
2210
2967
  let storageHTML = '';
@@ -2215,7 +2972,7 @@ window.doBenchQuantization = async function() {
2215
2972
  const savings = r.bytesPerVec < baseline.bytesPerVec
2216
2973
  ? `${(baseline.bytesPerVec / r.bytesPerVec).toFixed(0)}× smaller`
2217
2974
  : 'baseline';
2218
- const color = DTYPE_COLORS[r.dtype] || '#82aaff';
2975
+ const color = DTYPE_COLORS[r.dtype] || '#0498EC';
2219
2976
  storageHTML += `<div class="quant-bar-group">
2220
2977
  <div class="quant-bar-label">
2221
2978
  <span class="dtype-name">${r.dtype}</span>
@@ -2233,7 +2990,7 @@ window.doBenchQuantization = async function() {
2233
2990
  const minLatency = Math.min(...completed.map(r => r.latency));
2234
2991
  for (const r of completed) {
2235
2992
  const pct = Math.max(8, (r.latency / maxLatency) * 100);
2236
- const color = DTYPE_COLORS[r.dtype] || '#82aaff';
2993
+ const color = DTYPE_COLORS[r.dtype] || '#0498EC';
2237
2994
  const badge = r.latency === minLatency ? ' ⚡' : '';
2238
2995
  latencyHTML += `<div class="quant-bar-group">
2239
2996
  <div class="quant-bar-label">
@@ -2326,7 +3083,65 @@ window.doBenchQuantization = async function() {
2326
3083
  };
2327
3084
 
2328
3085
  // ── Benchmark: Cost Calculator ──
3086
+ // ── Cost Calculator: Shared Helpers ──
3087
+
3088
+ function costFormatDollars(n) {
3089
+ if (n === 0) return '$0.00';
3090
+ if (n < 0.01 && n > 0) return '$' + n.toFixed(4);
3091
+ if (n < 1) return '$' + n.toFixed(2);
3092
+ return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
3093
+ }
3094
+
3095
+ function costShortNum(n) {
3096
+ if (n >= 1e9) return (n / 1e9).toFixed(n % 1e9 === 0 ? 0 : 1) + 'B';
3097
+ if (n >= 1e6) return (n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1) + 'M';
3098
+ if (n >= 1e3) return (n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1) + 'K';
3099
+ return String(n);
3100
+ }
3101
+
3102
+ function costGetPricePerM(model) {
3103
+ const match = model.price.match(/\$([0-9.]+)\/1M/);
3104
+ return match ? parseFloat(match[1]) : null;
3105
+ }
3106
+
3107
+ function costGetV4Models() {
3108
+ return allModels.filter(m => !m.legacy && !m.unreleased && costGetPricePerM(m) !== null);
3109
+ }
3110
+
3111
+ // ── Cost Mode Toggle ──
3112
+
3113
+ let currentCostMode = 'simple';
3114
+
3115
+ function setCostMode(mode) {
3116
+ currentCostMode = mode;
3117
+ document.querySelectorAll('.cost-mode-btn').forEach(btn => {
3118
+ btn.classList.toggle('active', btn.dataset.mode === mode);
3119
+ });
3120
+ document.getElementById('costSimpleMode').style.display = mode === 'simple' ? '' : 'none';
3121
+ document.getElementById('costRagMode').style.display = mode === 'rag' ? '' : 'none';
3122
+ if (mode === 'rag') updateRagCalculator();
3123
+ }
3124
+
3125
+ // ── Simple Mode (query-only comparison) ──
3126
+
2329
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
+
3144
+ // Simple mode sliders
2330
3145
  const tokSlider = document.getElementById('costTokens');
2331
3146
  const qSlider = document.getElementById('costQueries');
2332
3147
  const tokValue = document.getElementById('costTokensValue');
@@ -2342,22 +3157,22 @@ function initCostCalculator() {
2342
3157
 
2343
3158
  tokSlider.addEventListener('input', updateCost);
2344
3159
  qSlider.addEventListener('input', updateCost);
2345
-
2346
- // Initialize
2347
3160
  updateCost();
3161
+
3162
+ // RAG mode init
3163
+ initRagCalculator();
2348
3164
  }
2349
3165
 
2350
3166
  function renderCostTable(tokensPerQuery, queriesPerDay) {
2351
3167
  const tbody = document.getElementById('costTableBody');
2352
3168
  tbody.innerHTML = '';
2353
3169
 
2354
- const models = allModels.filter(m => !m.legacy);
3170
+ const models = allModels.filter(m => !m.legacy && !m.unreleased);
2355
3171
  const rows = [];
2356
3172
 
2357
3173
  models.forEach(m => {
2358
- const match = m.price.match(/\$([0-9.]+)\/1M/);
2359
- if (!match) return;
2360
- const pricePerM = parseFloat(match[1]);
3174
+ const pricePerM = costGetPricePerM(m);
3175
+ if (pricePerM === null) return;
2361
3176
  const dailyTokens = tokensPerQuery * queriesPerDay;
2362
3177
  const dailyCost = (dailyTokens / 1_000_000) * pricePerM;
2363
3178
  const monthlyCost = dailyCost * 30;
@@ -2388,6 +3203,234 @@ function renderCostTable(tokensPerQuery, queriesPerDay) {
2388
3203
  });
2389
3204
  }
2390
3205
 
3206
+ // ── RAG Planner Mode (full TCO with strategies) ──
3207
+
3208
+ function initRagCalculator() {
3209
+ // Populate model dropdowns
3210
+ const embeddingModels = allModels.filter(m =>
3211
+ m.type === 'embedding' && !m.legacy && !m.unreleased && costGetPricePerM(m) !== null
3212
+ );
3213
+
3214
+ const docSelect = document.getElementById('ragDocModel');
3215
+ const querySelect = document.getElementById('ragQueryModel');
3216
+
3217
+ embeddingModels.forEach(m => {
3218
+ const pricePerM = costGetPricePerM(m);
3219
+ const opt1 = document.createElement('option');
3220
+ opt1.value = m.name;
3221
+ opt1.textContent = `${m.name} ($${pricePerM.toFixed(2)}/1M)`;
3222
+ docSelect.appendChild(opt1);
3223
+
3224
+ const opt2 = document.createElement('option');
3225
+ opt2.value = m.name;
3226
+ opt2.textContent = `${m.name} ($${pricePerM.toFixed(2)}/1M)`;
3227
+ querySelect.appendChild(opt2);
3228
+ });
3229
+
3230
+ // Set defaults: voyage-4-large for docs, voyage-4-lite for queries
3231
+ docSelect.value = 'voyage-4-large';
3232
+ querySelect.value = 'voyage-4-lite';
3233
+
3234
+ // Bind all sliders and selects
3235
+ const ids = ['ragDocs', 'ragDocTokens', 'ragQueries', 'ragQueryTokens', 'ragMonths'];
3236
+ ids.forEach(id => {
3237
+ document.getElementById(id).addEventListener('input', updateRagCalculator);
3238
+ });
3239
+ docSelect.addEventListener('change', updateRagCalculator);
3240
+ querySelect.addEventListener('change', updateRagCalculator);
3241
+
3242
+ updateRagCalculator();
3243
+ }
3244
+
3245
+ function updateRagCalculator() {
3246
+ const numDocs = parseInt(document.getElementById('ragDocs').value, 10);
3247
+ const docTokens = parseInt(document.getElementById('ragDocTokens').value, 10);
3248
+ const numQueries = parseInt(document.getElementById('ragQueries').value, 10);
3249
+ const queryTokens = parseInt(document.getElementById('ragQueryTokens').value, 10);
3250
+ const months = parseInt(document.getElementById('ragMonths').value, 10);
3251
+ const docModelName = document.getElementById('ragDocModel').value;
3252
+ const queryModelName = document.getElementById('ragQueryModel').value;
3253
+
3254
+ // Update slider display values
3255
+ document.getElementById('ragDocsValue').textContent = costShortNum(numDocs);
3256
+ document.getElementById('ragDocTokensValue').textContent = docTokens.toLocaleString();
3257
+ document.getElementById('ragQueriesValue').textContent = costShortNum(numQueries);
3258
+ document.getElementById('ragQueryTokensValue').textContent = queryTokens.toLocaleString();
3259
+ document.getElementById('ragMonthsValue').textContent = months + ' mo';
3260
+
3261
+ const docTotalTokens = numDocs * docTokens;
3262
+ const queryTotalTokensPerMonth = numQueries * queryTokens;
3263
+
3264
+ // Get model prices
3265
+ const docModel = allModels.find(m => m.name === docModelName);
3266
+ const queryModel = allModels.find(m => m.name === queryModelName);
3267
+ const docPrice = docModel ? costGetPricePerM(docModel) : 0;
3268
+ const queryPrice = queryModel ? costGetPricePerM(queryModel) : 0;
3269
+
3270
+ // ── Build strategies (same logic as CLI) ──
3271
+ const strategies = [];
3272
+ const v4Embedding = allModels.filter(m =>
3273
+ m.type === 'embedding' && !m.legacy && !m.unreleased &&
3274
+ (m.sharedSpace === 'voyage-4' || m.name.startsWith('voyage-4')) &&
3275
+ costGetPricePerM(m) !== null && costGetPricePerM(m) > 0
3276
+ );
3277
+
3278
+ // Strategy group 1: Symmetric with each V4 model
3279
+ v4Embedding.forEach(m => {
3280
+ const price = costGetPricePerM(m);
3281
+ const docCost = (docTotalTokens / 1e6) * price;
3282
+ const queryCostPerMonth = (queryTotalTokensPerMonth / 1e6) * price;
3283
+ const totalCost = docCost + (queryCostPerMonth * months);
3284
+ strategies.push({
3285
+ name: `Symmetric: ${m.name}`,
3286
+ type: 'symmetric',
3287
+ docModel: m.name,
3288
+ queryModel: m.name,
3289
+ docCost,
3290
+ queryCostPerMonth,
3291
+ totalCost,
3292
+ months,
3293
+ });
3294
+ });
3295
+
3296
+ // Strategy 2: Asymmetric — user-selected combo
3297
+ const asymDocCost = (docTotalTokens / 1e6) * docPrice;
3298
+ const asymQueryCostPerMonth = (queryTotalTokensPerMonth / 1e6) * queryPrice;
3299
+ const asymTotalCost = asymDocCost + (asymQueryCostPerMonth * months);
3300
+
3301
+ // Only add if it's actually asymmetric (different models)
3302
+ if (docModelName !== queryModelName) {
3303
+ strategies.push({
3304
+ name: `Asymmetric: ${docModelName} + ${queryModelName}`,
3305
+ type: 'asymmetric',
3306
+ docModel: docModelName,
3307
+ queryModel: queryModelName,
3308
+ docCost: asymDocCost,
3309
+ queryCostPerMonth: asymQueryCostPerMonth,
3310
+ totalCost: asymTotalCost,
3311
+ months,
3312
+ recommended: true,
3313
+ });
3314
+ }
3315
+
3316
+ // Strategy 3: Asymmetric with nano (local, free queries)
3317
+ strategies.push({
3318
+ name: `Asymmetric: ${docModelName} + nano (local)`,
3319
+ type: 'asymmetric-local',
3320
+ docModel: docModelName,
3321
+ queryModel: 'voyage-4-nano',
3322
+ docCost: asymDocCost,
3323
+ queryCostPerMonth: 0,
3324
+ totalCost: asymDocCost,
3325
+ months,
3326
+ localNote: 'Query cost = $0 (runs locally via HuggingFace)',
3327
+ });
3328
+
3329
+ strategies.sort((a, b) => a.totalCost - b.totalCost);
3330
+ const maxCost = Math.max(...strategies.map(s => s.totalCost), 0.01);
3331
+
3332
+ // ── Render summary cards ──
3333
+ const summaryEl = document.getElementById('ragSummary');
3334
+ summaryEl.innerHTML = `
3335
+ <div class="cost-summary-card">
3336
+ <div class="cost-summary-label">Document tokens</div>
3337
+ <div class="cost-summary-value">${costShortNum(docTotalTokens)}</div>
3338
+ <div class="cost-summary-detail">${costShortNum(numDocs)} docs × ${docTokens.toLocaleString()} tok</div>
3339
+ </div>
3340
+ <div class="cost-summary-card">
3341
+ <div class="cost-summary-label">Query tokens / mo</div>
3342
+ <div class="cost-summary-value">${costShortNum(queryTotalTokensPerMonth)}</div>
3343
+ <div class="cost-summary-detail">${costShortNum(numQueries)} queries × ${queryTokens} tok</div>
3344
+ </div>
3345
+ <div class="cost-summary-card">
3346
+ <div class="cost-summary-label">Best ${months}-mo total</div>
3347
+ <div class="cost-summary-value">${costFormatDollars(strategies[0].totalCost)}</div>
3348
+ <div class="cost-summary-detail">${strategies[0].name}</div>
3349
+ </div>
3350
+ <div class="cost-summary-card">
3351
+ <div class="cost-summary-label">Max potential savings</div>
3352
+ <div class="cost-summary-value" style="color:var(--success)">${maxCost > 0 ? ((1 - strategies[0].totalCost / maxCost) * 100).toFixed(0) + '%' : '0%'}</div>
3353
+ <div class="cost-summary-detail">vs ${strategies[strategies.length - 1].name.split(':')[1]?.trim() || 'most expensive'}</div>
3354
+ </div>
3355
+ `;
3356
+
3357
+ // ── Render strategy cards ──
3358
+ const stratEl = document.getElementById('ragStrategies');
3359
+ stratEl.innerHTML = strategies.map(s => {
3360
+ const savings = maxCost > 0 ? ((1 - s.totalCost / maxCost) * 100) : 0;
3361
+ const savingsHtml = savings > 0 ? `<div class="cost-savings">↓ ${savings.toFixed(0)}% savings</div>` : '';
3362
+ const badgeHtml = s.recommended ? '<div class="cost-strategy-badge">★ Recommended</div>' : '';
3363
+ const localHtml = s.localNote ? `<div style="font-size:11px;color:var(--text-muted);margin-top:6px;">⚡ ${s.localNote}</div>` : '';
3364
+
3365
+ return `
3366
+ <div class="cost-strategy${s.recommended ? ' recommended' : ''}">
3367
+ ${badgeHtml}
3368
+ <div class="cost-strategy-name">${s.name}</div>
3369
+ <div class="cost-strategy-row">
3370
+ <span class="cost-strategy-row-label">Doc embedding</span>
3371
+ <span class="cost-strategy-row-value">${costFormatDollars(s.docCost)} <span style="color:var(--text-muted);font-size:11px">(one-time)</span></span>
3372
+ </div>
3373
+ <div class="cost-strategy-row">
3374
+ <span class="cost-strategy-row-label">Query cost</span>
3375
+ <span class="cost-strategy-row-value">${costFormatDollars(s.queryCostPerMonth)}/mo</span>
3376
+ </div>
3377
+ <div class="cost-strategy-total">
3378
+ <span class="cost-strategy-total-label">${s.months}-month total</span>
3379
+ <span class="cost-strategy-total-value">${costFormatDollars(s.totalCost)}</span>
3380
+ </div>
3381
+ ${savingsHtml}
3382
+ ${localHtml}
3383
+ </div>
3384
+ `;
3385
+ }).join('');
3386
+
3387
+ // ── Render per-model table ──
3388
+ const ragTbody = document.getElementById('ragTableBody');
3389
+ ragTbody.innerHTML = '';
3390
+
3391
+ const allEmbedding = allModels.filter(m =>
3392
+ m.type === 'embedding' && !m.legacy && !m.unreleased && costGetPricePerM(m) !== null && costGetPricePerM(m) > 0
3393
+ );
3394
+
3395
+ const tableRows = allEmbedding.map(m => {
3396
+ const price = costGetPricePerM(m);
3397
+ const dCost = (docTotalTokens / 1e6) * price;
3398
+ const qCostMo = (queryTotalTokensPerMonth / 1e6) * price;
3399
+ const total = dCost + (qCostMo * months);
3400
+ return { name: m.name, dCost, qCostMo, total };
3401
+ }).sort((a, b) => a.total - b.total);
3402
+
3403
+ const maxTable = Math.max(...tableRows.map(r => r.total), 0.01);
3404
+
3405
+ tableRows.forEach(r => {
3406
+ const tr = document.createElement('tr');
3407
+ const barPct = Math.max(2, (r.total / maxTable) * 100);
3408
+ tr.innerHTML = `
3409
+ <td style="color:var(--text)">${r.name}</td>
3410
+ <td>${costFormatDollars(r.dCost)}</td>
3411
+ <td>${costFormatDollars(r.qCostMo)}</td>
3412
+ <td class="cost-highlight">${costFormatDollars(r.total)}</td>
3413
+ <td class="cost-bar-cell" style="position:relative;padding-left:8px;">
3414
+ <div class="cost-bar" style="width:${barPct}%;"></div>
3415
+ <span style="position:relative;z-index:1;font-size:12px;color:var(--text-dim);">${costFormatDollars(r.total)}</span>
3416
+ </td>
3417
+ `;
3418
+ ragTbody.appendChild(tr);
3419
+ });
3420
+
3421
+ // ── Tip ──
3422
+ const bestSym = strategies.find(s => s.type === 'symmetric' && s.docModel === 'voyage-4-large');
3423
+ const bestAsym = strategies.find(s => s.recommended);
3424
+ const tipEl = document.getElementById('ragTip');
3425
+ if (bestSym && bestAsym && bestSym.totalCost > bestAsym.totalCost) {
3426
+ const saved = bestSym.totalCost - bestAsym.totalCost;
3427
+ const pct = ((saved / bestSym.totalCost) * 100).toFixed(0);
3428
+ tipEl.innerHTML = `💡 <strong>Asymmetric retrieval saves ${costFormatDollars(saved)} (${pct}%)</strong> over symmetric voyage-4-large — same document quality, lower query costs. The shared embedding space makes this possible. <code>vai explain shared-space</code>`;
3429
+ } else {
3430
+ tipEl.innerHTML = '💡 Try selecting different doc and query models to see asymmetric cost savings. <code>vai explain shared-space</code>';
3431
+ }
3432
+ }
3433
+
2391
3434
  // ── Benchmark: History ──
2392
3435
  const HISTORY_KEY = 'vai-bench-history';
2393
3436
 
@@ -2462,6 +3505,7 @@ window.clearHistory = function() {
2462
3505
  const _origInit = init;
2463
3506
  init = async function() {
2464
3507
  await _origInit();
3508
+ initThemeToggle();
2465
3509
  buildModelCheckboxes();
2466
3510
  populateBenchRankSelects();
2467
3511
  populateQuantModelSelect();
@@ -2473,5 +3517,84 @@ init = async function() {
2473
3517
  init();
2474
3518
  })();
2475
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>
2476
3599
  </body>
2477
3600
  </html>