voyageai-cli 1.19.0 โ†’ 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,32 +3,94 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>๐Ÿงญ Voyage AI Playground</title>
6
+ <title>Voyage AI Playground</title>
7
+ <link rel="icon" type="image/png" sizes="32x32" href="/icons/dark/32.png">
8
+ <link rel="icon" type="image/png" sizes="16x16" href="/icons/dark/16.png">
7
9
  <style>
8
10
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
11
 
10
12
  :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;
13
+ /* MongoDB Design System โ€” Dark Mode Palette (default) */
14
+ --bg: #001E2B; /* MDB Black */
15
+ --bg-surface: #112733; /* Gray Dark 4 */
16
+ --bg-card: #1C2D38; /* Gray Dark 3 */
17
+ --bg-input: #112733; /* Gray Dark 4 */
18
+ --accent: #00ED64; /* Green Base โ€” interactive elements only */
19
+ --accent-text: #FFFFFF; /* Bright white โ€” headings/labels in dark mode */
20
+ --accent-dim: #00A35C; /* Green Dark 1 */
21
+ --accent-glow: rgba(0, 237, 100, 0.12);
22
+ --text: #E8EDEB; /* Gray Light 2 */
23
+ --text-dim: #C1C7C6; /* Gray Light 1 */
24
+ --text-muted: #889397; /* Gray Base */
25
+ --border: #3D4F58; /* Gray Dark 2 */
26
+ --error: #FF6960; /* Red Light 1 */
27
+ --warning: #FFC010; /* Yellow Base */
28
+ --success: #00ED64; /* Green Base */
29
+ --red: #FF6960; /* Red Light 1 */
30
+ --yellow: #FFC010; /* Yellow Base */
31
+ --green: #00ED64; /* Green Base */
32
+ --blue: #0498EC; /* Blue Light 1 (links) */
33
+ --purple: #B45AF2; /* Purple Base */
34
+ --green-dark: #023430; /* Green Dark 3 */
28
35
  --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
- }
36
+ --font: 'Euclid Circular A', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
37
+ --mono: 'Source Code Pro', 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
38
+ }
39
+
40
+ /* MongoDB Design System โ€” Light Mode Palette */
41
+ [data-theme="light"] {
42
+ --bg: #FFFFFF; /* White */
43
+ --bg-surface: #F9FBFA; /* Gray Light 3 */
44
+ --bg-card: #FFFFFF; /* White */
45
+ --bg-input: #F9FBFA; /* Gray Light 3 */
46
+ --accent: #00A35C; /* Green Dark 1 โ€” interactive elements */
47
+ --accent-text: #001E2B; /* MDB Black โ€” headings/labels in light mode */
48
+ --accent-dim: #00684A; /* Green Dark 2 */
49
+ --accent-glow: rgba(0, 163, 92, 0.08);
50
+ --text: #001E2B; /* MDB Black */
51
+ --text-dim: #5C6C75; /* Gray Dark 1 */
52
+ --text-muted: #889397; /* Gray Base */
53
+ --border: #E8EDEB; /* Gray Light 2 */
54
+ --error: #DB3030; /* Red Base */
55
+ --warning: #944F01; /* Yellow Dark 2 */
56
+ --success: #00684A; /* Green Dark 2 */
57
+ --red: #DB3030; /* Red Base */
58
+ --yellow: #944F01; /* Yellow Dark 2 */
59
+ --green: #00684A; /* Green Dark 2 */
60
+ --blue: #016BF8; /* Blue Base */
61
+ --purple: #5E0C9E; /* Purple Dark 2 */
62
+ --green-dark: #023430; /* Green Dark 3 */
63
+ }
64
+ /* Light mode shadow + card adjustments */
65
+ [data-theme="light"] .explore-card,
66
+ [data-theme="light"] .card,
67
+ [data-theme="light"] .cost-strategy,
68
+ [data-theme="light"] .cost-summary-card {
69
+ box-shadow: 0 1px 4px rgba(0, 30, 43, 0.08);
70
+ }
71
+ [data-theme="light"] .explore-card:hover {
72
+ box-shadow: 0 4px 16px rgba(0, 163, 92, 0.12);
73
+ }
74
+ [data-theme="light"] .cost-modal,
75
+ [data-theme="light"] .explore-modal {
76
+ box-shadow: 0 20px 60px rgba(0, 30, 43, 0.2);
77
+ }
78
+ [data-theme="light"] .cost-modal-overlay,
79
+ [data-theme="light"] .explore-modal-overlay {
80
+ background: rgba(0, 30, 43, 0.4);
81
+ }
82
+ [data-theme="light"] .sidebar {
83
+ box-shadow: 1px 0 3px rgba(0, 30, 43, 0.06);
84
+ }
85
+ /* Light mode gradient overrides */
86
+ [data-theme="light"] .quant-bar-fill.storage { background: linear-gradient(90deg, #00A35C, #00ED64); }
87
+ [data-theme="light"] .quant-bar-fill.latency { background: linear-gradient(90deg, #016BF8, #0498EC); }
88
+ [data-theme="light"] .quant-meter-fill.perfect { background: linear-gradient(90deg, #00A35C, #00ED64); }
89
+ [data-theme="light"] .quant-meter-fill.good { background: linear-gradient(90deg, #944F01, #FFC010); }
90
+ [data-theme="light"] .quant-meter-fill.degraded { background: linear-gradient(90deg, #DB3030, #FF6960); }
91
+ /* Light mode button text */
92
+ [data-theme="light"] .btn { color: #FFFFFF; }
93
+ [data-theme="light"] .btn:hover { background: #00684A; }
32
94
 
33
95
  html, body { height: 100%; }
34
96
 
@@ -40,80 +102,447 @@ body {
40
102
  overflow-x: hidden;
41
103
  }
42
104
 
43
- /* Nav */
44
- .nav {
45
- background: var(--bg-surface);
46
- border-bottom: 1px solid var(--border);
47
- padding: 0 24px;
48
- height: 56px;
105
+ /* โ”€โ”€ Update banner โ”€โ”€ */
106
+ .update-banner {
107
+ display: none;
108
+ align-items: center;
109
+ gap: 10px;
110
+ padding: 8px 16px;
111
+ background: linear-gradient(135deg, rgba(0,163,92,0.12), rgba(4,152,236,0.12));
112
+ border-bottom: 1px solid rgba(0,237,100,0.2);
113
+ font-size: 13px;
114
+ color: var(--text);
115
+ flex-shrink: 0;
116
+ }
117
+ .update-banner.show { display: flex; }
118
+ .update-banner-icon { font-size: 16px; }
119
+ .update-banner-text { flex: 1; }
120
+ .update-banner-text strong { color: var(--accent); }
121
+ .update-banner-btn {
122
+ background: var(--accent);
123
+ color: var(--bg);
124
+ border: none;
125
+ border-radius: 4px;
126
+ padding: 5px 14px;
127
+ font-size: 12px;
128
+ font-weight: 600;
129
+ cursor: pointer;
130
+ font-family: var(--font);
131
+ transition: opacity 0.15s;
132
+ }
133
+ .update-banner-btn:hover { opacity: 0.85; }
134
+ .update-banner-dismiss {
135
+ background: none;
136
+ border: none;
137
+ color: var(--text-muted);
138
+ cursor: pointer;
139
+ font-size: 18px;
140
+ line-height: 1;
141
+ padding: 0 4px;
142
+ }
143
+ .update-banner-dismiss:hover { color: var(--text); }
144
+
145
+ /* โ”€โ”€ Onboarding Walkthrough โ”€โ”€ */
146
+ .onboarding-overlay {
147
+ display: none;
148
+ position: fixed;
149
+ inset: 0;
150
+ z-index: 10000;
151
+ }
152
+ .onboarding-overlay.active { display: block; }
153
+ .onboarding-backdrop {
154
+ position: absolute;
155
+ inset: 0;
156
+ background: rgba(0,0,0,0.6);
157
+ transition: opacity 0.3s;
158
+ }
159
+ .onboarding-spotlight {
160
+ position: absolute;
161
+ border-radius: var(--radius);
162
+ box-shadow: 0 0 0 9999px rgba(0,0,0,0.55);
163
+ transition: all 0.4s cubic-bezier(0.4,0,0.2,1);
164
+ z-index: 10001;
165
+ pointer-events: none;
166
+ }
167
+ .onboarding-tooltip {
168
+ position: absolute;
169
+ z-index: 10002;
170
+ background: var(--bg-card);
171
+ border: 1px solid var(--accent);
172
+ border-radius: 12px;
173
+ padding: 24px 28px 20px;
174
+ width: 380px;
175
+ max-width: 90vw;
176
+ box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(0,237,100,0.15);
177
+ transition: all 0.4s cubic-bezier(0.4,0,0.2,1);
178
+ opacity: 0;
179
+ transform: translateY(10px);
180
+ }
181
+ .onboarding-tooltip.visible {
182
+ opacity: 1;
183
+ transform: translateY(0);
184
+ }
185
+ .onboarding-tooltip-arrow {
186
+ position: absolute;
187
+ width: 12px;
188
+ height: 12px;
189
+ background: var(--bg-card);
190
+ border: 1px solid var(--accent);
191
+ transform: rotate(45deg);
192
+ }
193
+ .onboarding-tooltip-arrow.top {
194
+ top: -7px;
195
+ left: 30px;
196
+ border-right: none;
197
+ border-bottom: none;
198
+ }
199
+ .onboarding-tooltip-arrow.bottom {
200
+ bottom: -7px;
201
+ left: 30px;
202
+ border-left: none;
203
+ border-top: none;
204
+ }
205
+ .onboarding-tooltip-arrow.left {
206
+ left: -7px;
207
+ top: 24px;
208
+ border-right: none;
209
+ border-top: none;
210
+ }
211
+ .onboarding-step-icon {
212
+ font-size: 28px;
213
+ margin-bottom: 8px;
214
+ }
215
+ .onboarding-step-title {
216
+ font-size: 16px;
217
+ font-weight: 700;
218
+ color: var(--accent-text);
219
+ margin-bottom: 6px;
220
+ }
221
+ .onboarding-step-body {
222
+ font-size: 13px;
223
+ color: var(--text-dim);
224
+ line-height: 1.6;
225
+ margin-bottom: 18px;
226
+ }
227
+ .onboarding-step-body strong { color: var(--accent); }
228
+ .onboarding-footer {
49
229
  display: flex;
50
230
  align-items: center;
51
- gap: 16px;
52
- position: sticky;
53
- top: 0;
231
+ justify-content: space-between;
232
+ gap: 12px;
233
+ }
234
+ .onboarding-dots {
235
+ display: flex;
236
+ gap: 6px;
237
+ }
238
+ .onboarding-dot {
239
+ width: 8px;
240
+ height: 8px;
241
+ border-radius: 50%;
242
+ background: var(--border);
243
+ transition: background 0.2s;
244
+ }
245
+ .onboarding-dot.active { background: var(--accent); }
246
+ .onboarding-dot.completed { background: var(--accent-dim); }
247
+ .onboarding-actions {
248
+ display: flex;
249
+ gap: 8px;
250
+ align-items: center;
251
+ }
252
+ .onboarding-skip {
253
+ background: none;
254
+ border: none;
255
+ color: var(--text-muted);
256
+ font-size: 12px;
257
+ cursor: pointer;
258
+ font-family: var(--font);
259
+ padding: 6px 10px;
260
+ border-radius: 6px;
261
+ transition: color 0.15s;
262
+ }
263
+ .onboarding-skip:hover { color: var(--text); }
264
+ .onboarding-next {
265
+ background: var(--accent);
266
+ color: var(--bg);
267
+ border: none;
268
+ border-radius: 6px;
269
+ padding: 8px 20px;
270
+ font-size: 13px;
271
+ font-weight: 600;
272
+ cursor: pointer;
273
+ font-family: var(--font);
274
+ transition: opacity 0.15s;
275
+ }
276
+ .onboarding-next:hover { opacity: 0.85; }
277
+ .onboarding-welcome-center {
278
+ position: fixed;
279
+ inset: 0;
280
+ z-index: 10002;
281
+ display: flex;
282
+ align-items: center;
283
+ justify-content: center;
284
+ pointer-events: none;
285
+ }
286
+ .onboarding-welcome-card {
287
+ background: var(--bg-card);
288
+ border: 1px solid var(--accent);
289
+ border-radius: 16px;
290
+ padding: 40px 44px 32px;
291
+ width: 460px;
292
+ max-width: 90vw;
293
+ text-align: center;
294
+ box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 80px rgba(0,237,100,0.08);
295
+ pointer-events: auto;
296
+ opacity: 0;
297
+ transform: scale(0.95);
298
+ transition: all 0.4s cubic-bezier(0.4,0,0.2,1);
299
+ }
300
+ .onboarding-welcome-card.visible {
301
+ opacity: 1;
302
+ transform: scale(1);
303
+ }
304
+ .onboarding-welcome-logo {
305
+ width: 64px;
306
+ height: 64px;
307
+ margin-bottom: 16px;
308
+ }
309
+ .onboarding-welcome-title {
310
+ font-size: 24px;
311
+ font-weight: 700;
312
+ color: var(--accent-text);
313
+ margin-bottom: 8px;
314
+ }
315
+ .onboarding-welcome-sub {
316
+ font-size: 14px;
317
+ color: var(--text-dim);
318
+ line-height: 1.6;
319
+ margin-bottom: 28px;
320
+ }
321
+ [data-theme="light"] .onboarding-tooltip {
322
+ box-shadow: 0 12px 40px rgba(0,30,43,0.15), 0 0 0 1px rgba(0,163,92,0.2);
323
+ }
324
+ [data-theme="light"] .onboarding-welcome-card {
325
+ box-shadow: 0 20px 60px rgba(0,30,43,0.18), 0 0 80px rgba(0,163,92,0.06);
326
+ }
327
+ [data-theme="light"] .onboarding-backdrop {
328
+ background: rgba(0,30,43,0.45);
329
+ }
330
+
331
+ /* โ”€โ”€ App Shell: sidebar + content layout โ”€โ”€ */
332
+ .app-shell {
333
+ display: flex;
334
+ height: 100vh;
335
+ overflow: hidden;
336
+ }
337
+
338
+ /* โ”€โ”€ Sidebar โ”€โ”€ */
339
+ .sidebar {
340
+ width: 220px;
341
+ min-width: 220px;
342
+ background: var(--bg-surface);
343
+ border-right: 1px solid var(--border);
344
+ display: flex;
345
+ flex-direction: column;
346
+ overflow: hidden;
54
347
  z-index: 100;
55
348
  }
56
349
 
57
- .nav-title {
58
- font-size: 18px;
350
+ .sidebar-drag-region {
351
+ -webkit-app-region: drag;
352
+ height: 78px;
353
+ min-height: 78px;
354
+ padding: 0 16px;
355
+ padding-top: 52px; /* Clear macOS traffic lights fully */
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 10px;
359
+ }
360
+
361
+ .sidebar-logo {
362
+ width: 28px;
363
+ height: 28px;
364
+ border-radius: 6px;
365
+ flex-shrink: 0;
366
+ object-fit: contain;
367
+ }
368
+
369
+ .sidebar-title {
370
+ font-size: 14px;
59
371
  font-weight: 700;
60
- color: var(--accent);
372
+ color: var(--accent-text);
61
373
  white-space: nowrap;
374
+ letter-spacing: -0.2px;
62
375
  }
63
376
 
64
- .nav-spacer { flex: 1; }
377
+ .sidebar-nav {
378
+ flex: 1;
379
+ overflow-y: auto;
380
+ padding: 8px;
381
+ display: flex;
382
+ flex-direction: column;
383
+ gap: 1px;
384
+ }
65
385
 
66
- .status-dot {
67
- width: 8px; height: 8px;
68
- border-radius: 50%;
69
- background: var(--text-muted);
70
- transition: background 0.3s;
386
+ .sidebar-nav-group {
387
+ display: flex;
388
+ flex-direction: column;
389
+ gap: 1px;
390
+ }
391
+ .sidebar-nav-divider {
392
+ height: 1px;
393
+ background: var(--border);
394
+ margin: 8px 12px;
395
+ opacity: 0.5;
396
+ }
397
+ .sidebar-nav-label {
398
+ font-size: 10px;
399
+ font-weight: 600;
400
+ text-transform: uppercase;
401
+ letter-spacing: 0.8px;
402
+ color: var(--text-muted);
403
+ padding: 10px 12px 4px;
71
404
  }
72
- .status-dot.connected { background: var(--success); box-shadow: 0 0 8px var(--accent-glow); }
73
- .status-dot.error { background: var(--error); }
74
405
 
75
- .status-label {
76
- font-size: 12px;
406
+ .tab-btn {
407
+ display: flex;
408
+ align-items: center;
409
+ gap: 10px;
410
+ width: 100%;
411
+ background: none;
412
+ border: none;
413
+ border-left: 3px solid transparent;
414
+ border-radius: 0 6px 6px 0;
77
415
  color: var(--text-dim);
416
+ padding: 10px 12px;
417
+ font-size: 13px;
418
+ font-weight: 500;
419
+ font-family: var(--font);
420
+ cursor: pointer;
421
+ transition: all 0.15s;
422
+ white-space: nowrap;
423
+ text-align: left;
424
+ position: relative;
425
+ }
426
+ .tab-btn:hover {
427
+ color: var(--text);
428
+ background: rgba(255,255,255,0.05);
429
+ }
430
+ .tab-btn:hover .tab-btn-icon {
431
+ transform: scale(1.1);
432
+ }
433
+ .tab-btn.active {
434
+ color: var(--accent);
435
+ background: var(--accent-glow);
436
+ border-left-color: var(--accent);
437
+ font-weight: 600;
438
+ }
439
+ .tab-btn-icon {
440
+ width: 18px;
441
+ height: 18px;
442
+ flex-shrink: 0;
443
+ transition: transform 0.15s;
444
+ display: flex;
445
+ align-items: center;
446
+ justify-content: center;
447
+ }
448
+ .tab-btn-icon svg {
449
+ width: 16px;
450
+ height: 16px;
451
+ }
452
+ [data-theme="light"] .tab-btn:hover { background: rgba(0,30,43,0.04); }
453
+ [data-theme="light"] .tab-btn.active { background: rgba(0,163,92,0.08); }
454
+
455
+ .sidebar-footer {
456
+ padding: 12px 16px;
457
+ border-top: 1px solid var(--border);
458
+ display: flex;
459
+ flex-direction: column;
460
+ gap: 8px;
78
461
  }
79
462
 
463
+ .sidebar-model-group {
464
+ display: flex;
465
+ flex-direction: column;
466
+ gap: 4px;
467
+ }
468
+ .sidebar-model-label {
469
+ font-size: 10px;
470
+ font-weight: 600;
471
+ text-transform: uppercase;
472
+ letter-spacing: 0.5px;
473
+ color: var(--text-muted);
474
+ }
80
475
  .nav-model-select {
81
476
  background: var(--bg-input);
82
477
  border: 1px solid var(--border);
83
478
  color: var(--text);
84
- padding: 6px 12px;
479
+ padding: 6px 10px;
85
480
  border-radius: var(--radius);
86
- font-size: 13px;
481
+ font-size: 12px;
87
482
  font-family: var(--mono);
88
483
  cursor: pointer;
484
+ width: 100%;
89
485
  }
90
486
 
91
- /* Tabs */
92
- .tab-bar {
487
+ .sidebar-controls {
93
488
  display: flex;
94
- background: var(--bg-surface);
95
- border-bottom: 1px solid var(--border);
96
- padding: 0 24px;
97
- gap: 0;
489
+ align-items: center;
490
+ justify-content: space-between;
98
491
  }
99
492
 
100
- .tab-btn {
493
+ .theme-toggle {
101
494
  background: none;
102
- border: none;
103
- color: var(--text-dim);
104
- padding: 12px 20px;
105
- font-size: 14px;
106
- font-family: var(--font);
495
+ border: 1px solid var(--border);
496
+ border-radius: 16px;
497
+ padding: 4px 8px;
107
498
  cursor: pointer;
108
- border-bottom: 2px solid transparent;
499
+ font-size: 14px;
500
+ line-height: 1;
109
501
  transition: all 0.2s;
110
- white-space: nowrap;
502
+ display: flex;
503
+ align-items: center;
504
+ gap: 4px;
505
+ }
506
+ .theme-toggle:hover {
507
+ border-color: var(--accent);
508
+ background: var(--accent-glow);
509
+ }
510
+
511
+ .status-dot {
512
+ width: 8px; height: 8px;
513
+ border-radius: 50%;
514
+ background: var(--text-muted);
515
+ transition: background 0.3s;
516
+ }
517
+ .status-dot.connected { background: var(--success); box-shadow: 0 0 8px var(--accent-glow); }
518
+ .status-dot.error { background: var(--error); }
519
+
520
+ .status-label {
521
+ font-size: 11px;
522
+ color: var(--text-dim);
523
+ }
524
+
525
+ /* โ”€โ”€ Content area โ”€โ”€ */
526
+ .content-area {
527
+ flex: 1;
528
+ overflow-y: auto;
529
+ display: flex;
530
+ flex-direction: column;
531
+ }
532
+
533
+ .content-drag-region {
534
+ -webkit-app-region: drag;
535
+ height: 38px;
536
+ min-height: 38px;
537
+ flex-shrink: 0;
111
538
  }
112
- .tab-btn:hover { color: var(--text); background: rgba(255,255,255,0.03); }
113
- .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
114
539
 
115
540
  /* Main */
116
- .main { padding: 24px; max-width: 1200px; margin: 0 auto; }
541
+ .main { padding: 24px; max-width: 1200px; margin: 0 auto; width: 100%; }
542
+
543
+ /* Legacy compat โ€” hide old horizontal bar (replaced by sidebar) */
544
+ .nav { display: none; }
545
+ .tab-bar-horizontal { display: none; }
117
546
 
118
547
  .tab-panel { display: none; }
119
548
  .tab-panel.active { display: block; }
@@ -130,7 +559,7 @@ body {
130
559
  .card-title {
131
560
  font-size: 14px;
132
561
  font-weight: 600;
133
- color: var(--accent);
562
+ color: var(--accent-text);
134
563
  margin-bottom: 12px;
135
564
  text-transform: uppercase;
136
565
  letter-spacing: 0.5px;
@@ -168,7 +597,7 @@ select:focus { outline: none; border-color: var(--accent); }
168
597
 
169
598
  .btn {
170
599
  background: var(--accent);
171
- color: #0a0a1a;
600
+ color: var(--green-dark);
172
601
  border: none;
173
602
  padding: 10px 24px;
174
603
  border-radius: var(--radius);
@@ -181,12 +610,12 @@ select:focus { outline: none; border-color: var(--accent); }
181
610
  align-items: center;
182
611
  gap: 8px;
183
612
  }
184
- .btn:hover { background: #00eabb; transform: translateY(-1px); }
613
+ .btn:hover { background: #71F6BA; transform: translateY(-1px); }
185
614
  .btn:active { transform: translateY(0); }
186
615
  .btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
187
616
 
188
617
  .btn-secondary {
189
- background: var(--bg-input);
618
+ background: transparent;
190
619
  color: var(--accent);
191
620
  border: 1px solid var(--accent-dim);
192
621
  }
@@ -257,7 +686,7 @@ select:focus { outline: none; border-color: var(--accent); }
257
686
  margin-bottom: 8px;
258
687
  }
259
688
  .stat-label { color: var(--text-dim); }
260
- .stat-value { color: var(--accent); font-weight: 600; font-family: var(--mono); }
689
+ .stat-value { color: var(--accent-text); font-weight: 600; font-family: var(--mono); }
261
690
 
262
691
  .vector-preview {
263
692
  font-family: var(--mono);
@@ -319,34 +748,93 @@ select:focus { outline: none; border-color: var(--accent); }
319
748
  border-radius: 4px;
320
749
  transition: width 0.6s ease, background 0.6s ease;
321
750
  }
322
-
323
- /* Search tab */
324
- .search-results {
751
+ .metrics-grid {
325
752
  display: grid;
326
- grid-template-columns: 1fr 1fr;
753
+ grid-template-columns: repeat(3, 1fr);
327
754
  gap: 16px;
755
+ margin-top: 24px;
328
756
  }
329
- .search-results.single-col {
330
- grid-template-columns: 1fr;
331
- }
332
-
333
- .result-item {
334
- display: flex;
335
- gap: 12px;
336
- padding: 12px;
757
+ .metric-card {
337
758
  background: var(--bg-input);
338
- border-radius: var(--radius);
339
- margin-bottom: 8px;
340
- border-left: 3px solid var(--border);
341
- transition: border-color 0.3s;
342
- }
343
- .result-item.moved-up { border-left-color: var(--green); }
759
+ border: 1px solid var(--border);
760
+ border-radius: 10px;
761
+ padding: 20px 16px;
762
+ text-align: center;
763
+ transition: border-color 0.2s;
764
+ }
765
+ .metric-card.primary {
766
+ border-color: var(--accent);
767
+ background: var(--accent-glow);
768
+ }
769
+ .metric-card-value {
770
+ font-family: var(--mono);
771
+ font-size: 28px;
772
+ font-weight: 700;
773
+ line-height: 1;
774
+ }
775
+ .metric-card-name {
776
+ font-size: 13px;
777
+ font-weight: 600;
778
+ color: var(--text);
779
+ margin-top: 10px;
780
+ }
781
+ .metric-card-desc {
782
+ font-size: 11px;
783
+ color: var(--text-muted);
784
+ margin-top: 4px;
785
+ line-height: 1.4;
786
+ }
787
+ .metric-bar {
788
+ width: 100%;
789
+ height: 6px;
790
+ background: var(--bg);
791
+ border-radius: 3px;
792
+ margin-top: 12px;
793
+ overflow: hidden;
794
+ }
795
+ .metric-bar-fill {
796
+ height: 100%;
797
+ border-radius: 3px;
798
+ transition: width 0.6s ease, background 0.6s ease;
799
+ }
800
+ .metric-note {
801
+ text-align: center;
802
+ font-size: 12px;
803
+ color: var(--text-muted);
804
+ margin-top: 16px;
805
+ padding: 10px 16px;
806
+ background: var(--bg-input);
807
+ border-radius: 8px;
808
+ line-height: 1.6;
809
+ }
810
+
811
+ /* Search tab */
812
+ .search-results {
813
+ display: grid;
814
+ grid-template-columns: 1fr 1fr;
815
+ gap: 16px;
816
+ }
817
+ .search-results.single-col {
818
+ grid-template-columns: 1fr;
819
+ }
820
+
821
+ .result-item {
822
+ display: flex;
823
+ gap: 12px;
824
+ padding: 12px;
825
+ background: var(--bg-input);
826
+ border-radius: var(--radius);
827
+ margin-bottom: 8px;
828
+ border-left: 3px solid var(--border);
829
+ transition: border-color 0.3s;
830
+ }
831
+ .result-item.moved-up { border-left-color: var(--green); }
344
832
  .result-item.moved-down { border-left-color: var(--red); }
345
833
 
346
834
  .result-rank {
347
835
  font-size: 20px;
348
836
  font-weight: 700;
349
- color: var(--accent);
837
+ color: var(--accent-text);
350
838
  font-family: var(--mono);
351
839
  min-width: 30px;
352
840
  }
@@ -400,11 +888,10 @@ select:focus { outline: none; border-color: var(--accent); }
400
888
  .explore-card:hover {
401
889
  border-color: var(--accent);
402
890
  transform: translateY(-2px);
403
- box-shadow: 0 4px 20px rgba(0, 212, 170, 0.1);
891
+ box-shadow: 0 4px 20px rgba(0, 237, 100, 0.1);
404
892
  }
405
893
  .explore-card.expanded {
406
- grid-column: 1 / -1;
407
- cursor: default;
894
+ border-color: var(--accent);
408
895
  }
409
896
 
410
897
  .explore-card-icon {
@@ -423,20 +910,136 @@ select:focus { outline: none; border-color: var(--accent); }
423
910
  }
424
911
  .explore-card-content {
425
912
  display: none;
426
- margin-top: 16px;
913
+ }
914
+ .explore-card-actions {
915
+ display: none;
916
+ }
917
+
918
+ /* Explore modal */
919
+ .explore-modal-overlay {
920
+ position: fixed;
921
+ inset: 0;
922
+ background: rgba(0, 0, 0, 0.7);
923
+ backdrop-filter: blur(4px);
924
+ z-index: 1000;
925
+ display: flex;
926
+ align-items: center;
927
+ justify-content: center;
928
+ opacity: 0;
929
+ pointer-events: none;
930
+ transition: opacity 0.25s ease;
931
+ }
932
+ .explore-modal-overlay.open {
933
+ opacity: 1;
934
+ pointer-events: auto;
935
+ }
936
+ .explore-modal {
937
+ background: var(--bg-surface);
938
+ border: 1px solid var(--border);
939
+ border-radius: 14px;
940
+ max-width: 720px;
941
+ width: 92%;
942
+ max-height: 85vh;
943
+ overflow-y: auto;
944
+ padding: 0;
945
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
946
+ position: relative;
947
+ animation: exploreModalIn 0.2s ease-out;
948
+ }
949
+ @keyframes exploreModalIn {
950
+ from { opacity: 0; transform: scale(0.95) translateY(10px); }
951
+ to { opacity: 1; transform: scale(1) translateY(0); }
952
+ }
953
+ .explore-modal::-webkit-scrollbar { width: 6px; }
954
+ .explore-modal::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
955
+ .explore-modal-header {
956
+ display: flex;
957
+ align-items: center;
958
+ gap: 14px;
959
+ padding: 24px 28px 16px;
960
+ border-bottom: 1px solid var(--border);
961
+ }
962
+ .explore-modal-icon { font-size: 32px; }
963
+ .explore-modal-title {
964
+ font-size: 18px;
965
+ font-weight: 600;
966
+ color: var(--text);
967
+ }
968
+ .explore-modal-summary {
969
+ font-size: 13px;
970
+ color: var(--text-dim);
971
+ margin-top: 2px;
972
+ }
973
+ .explore-modal-close {
974
+ position: absolute;
975
+ top: 16px; right: 18px;
976
+ background: none;
977
+ border: none;
978
+ color: var(--text-dim);
979
+ font-size: 22px;
980
+ cursor: pointer;
981
+ padding: 4px 8px;
982
+ border-radius: 6px;
983
+ transition: all 0.15s;
984
+ z-index: 1;
985
+ }
986
+ .explore-modal-close:hover { background: rgba(255,255,255,0.05); color: var(--text); }
987
+ .explore-modal-body {
988
+ padding: 20px 28px 24px;
427
989
  font-size: 14px;
428
- line-height: 1.7;
990
+ line-height: 1.75;
429
991
  color: var(--text);
430
992
  white-space: pre-wrap;
431
993
  }
432
- .explore-card.expanded .explore-card-content { display: block; }
433
-
434
- .explore-card-actions {
435
- display: none;
994
+ .explore-modal-links {
436
995
  margin-top: 16px;
996
+ padding-top: 14px;
997
+ border-top: 1px solid var(--border);
998
+ }
999
+ .explore-modal-links-title {
1000
+ font-size: 11px;
1001
+ font-weight: 600;
1002
+ color: var(--accent-text);
1003
+ text-transform: uppercase;
1004
+ letter-spacing: 0.5px;
1005
+ margin-bottom: 6px;
1006
+ }
1007
+ .explore-modal-links a {
1008
+ display: block;
1009
+ color: var(--blue);
1010
+ font-size: 12px;
1011
+ word-break: break-all;
1012
+ margin-bottom: 4px;
1013
+ text-decoration: none;
1014
+ }
1015
+ .explore-modal-links a:hover { text-decoration: underline; }
1016
+ .explore-modal-tryit {
1017
+ margin-top: 14px;
1018
+ padding-top: 14px;
1019
+ border-top: 1px solid var(--border);
1020
+ }
1021
+ .explore-modal-tryit-title {
1022
+ font-size: 11px;
1023
+ font-weight: 600;
1024
+ color: var(--accent-text);
1025
+ text-transform: uppercase;
1026
+ letter-spacing: 0.5px;
1027
+ margin-bottom: 8px;
1028
+ }
1029
+ .explore-modal-tryit-cmd {
1030
+ font-family: var(--mono);
1031
+ font-size: 12px;
1032
+ color: var(--text-dim);
1033
+ background: var(--bg);
1034
+ padding: 6px 10px;
1035
+ border-radius: 5px;
1036
+ margin-bottom: 4px;
1037
+ }
1038
+ .explore-modal-actions {
1039
+ display: flex;
437
1040
  gap: 8px;
1041
+ padding: 0 28px 24px;
438
1042
  }
439
- .explore-card.expanded .explore-card-actions { display: flex; }
440
1043
 
441
1044
  /* Benchmark tab */
442
1045
  .bench-panels { display: flex; gap: 8px; margin-bottom: 16px; }
@@ -490,7 +1093,7 @@ select:focus { outline: none; border-color: var(--accent); }
490
1093
  padding: 0 10px;
491
1094
  font-family: var(--mono);
492
1095
  font-size: 12px;
493
- color: #0a0a1a;
1096
+ color: var(--green-dark);
494
1097
  font-weight: 600;
495
1098
  white-space: nowrap;
496
1099
  }
@@ -531,7 +1134,7 @@ select:focus { outline: none; border-color: var(--accent); }
531
1134
  .rank-num {
532
1135
  font-size: 16px;
533
1136
  font-weight: 700;
534
- color: var(--accent);
1137
+ color: var(--accent-text);
535
1138
  font-family: var(--mono);
536
1139
  text-align: center;
537
1140
  }
@@ -560,7 +1163,7 @@ select:focus { outline: none; border-color: var(--accent); }
560
1163
  display: flex; justify-content: space-between; align-items: baseline;
561
1164
  margin-bottom: 4px; font-size: 13px;
562
1165
  }
563
- .quant-bar-label .dtype-name { color: var(--accent); font-weight: 600; font-family: var(--mono); }
1166
+ .quant-bar-label .dtype-name { color: var(--accent-text); font-weight: 600; font-family: var(--mono); }
564
1167
  .quant-bar-label .dtype-value { color: var(--text-dim); font-family: var(--mono); font-size: 12px; }
565
1168
  .quant-bar-track {
566
1169
  height: 32px; background: var(--bg-input); border-radius: 6px;
@@ -571,10 +1174,10 @@ select:focus { outline: none; border-color: var(--accent); }
571
1174
  transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
572
1175
  display: flex; align-items: center; padding: 0 10px;
573
1176
  font-family: var(--mono); font-size: 12px; font-weight: 600;
574
- color: #0a0a1a; white-space: nowrap; min-width: fit-content;
1177
+ color: var(--green-dark); white-space: nowrap; min-width: fit-content;
575
1178
  }
576
- .quant-bar-fill.storage { background: linear-gradient(90deg, #00d4aa, #4ecdc4); }
577
- .quant-bar-fill.latency { background: linear-gradient(90deg, #45b7d1, #82aaff); }
1179
+ .quant-bar-fill.storage { background: linear-gradient(90deg, #00ED64, #71F6BA); }
1180
+ .quant-bar-fill.latency { background: linear-gradient(90deg, #0498EC, #016BF8); }
578
1181
  .quant-bar-badge {
579
1182
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
580
1183
  font-size: 12px; color: var(--text-dim); font-family: var(--mono);
@@ -585,7 +1188,7 @@ select:focus { outline: none; border-color: var(--accent); }
585
1188
  display: flex; justify-content: space-between; align-items: center;
586
1189
  margin-bottom: 6px;
587
1190
  }
588
- .quant-meter-header .dtype-name { color: var(--accent); font-weight: 600; font-family: var(--mono); font-size: 13px; }
1191
+ .quant-meter-header .dtype-name { color: var(--accent-text); font-weight: 600; font-family: var(--mono); font-size: 13px; }
589
1192
  .quant-meter-header .verdict-badge {
590
1193
  font-size: 12px; padding: 2px 8px; border-radius: 10px; font-weight: 600;
591
1194
  }
@@ -599,16 +1202,16 @@ select:focus { outline: none; border-color: var(--accent); }
599
1202
  height: 100%; border-radius: 5px;
600
1203
  transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
601
1204
  }
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); }
1205
+ .quant-meter-fill.perfect { background: linear-gradient(90deg, #00ED64, #71F6BA); }
1206
+ .quant-meter-fill.good { background: linear-gradient(90deg, #FFC010, #FFEC9E); }
1207
+ .quant-meter-fill.degraded { background: linear-gradient(90deg, #FF6960, #FFCDC7); }
605
1208
  .quant-meter-detail { font-size: 11px; color: var(--text-muted); margin-top: 4px; font-family: var(--mono); }
606
1209
 
607
1210
  .quant-rank-cols {
608
1211
  display: grid; gap: 12px;
609
1212
  }
610
1213
  .quant-rank-col-header {
611
- font-weight: 600; color: var(--accent); font-size: 13px; font-family: var(--mono);
1214
+ font-weight: 600; color: var(--accent-text); font-size: 13px; font-family: var(--mono);
612
1215
  margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--border);
613
1216
  }
614
1217
  .quant-rank-item {
@@ -623,7 +1226,7 @@ select:focus { outline: none; border-color: var(--accent); }
623
1226
  .quant-rank-pos {
624
1227
  display: inline-block; width: 22px; height: 22px; line-height: 22px;
625
1228
  text-align: center; border-radius: 50%; background: var(--bg-surface);
626
- color: var(--accent); font-weight: 700; font-size: 11px; font-family: var(--mono);
1229
+ color: var(--accent-text); font-weight: 700; font-size: 11px; font-family: var(--mono);
627
1230
  margin-right: 8px;
628
1231
  }
629
1232
  .quant-rank-score { color: var(--text-muted); font-size: 11px; font-family: var(--mono); margin-top: 3px; }
@@ -666,7 +1269,7 @@ select:focus { outline: none; border-color: var(--accent); }
666
1269
  .cost-slider-value {
667
1270
  font-family: var(--mono);
668
1271
  font-size: 13px;
669
- color: var(--accent);
1272
+ color: var(--accent-text);
670
1273
  min-width: 70px;
671
1274
  text-align: right;
672
1275
  font-weight: 600;
@@ -695,7 +1298,7 @@ select:focus { outline: none; border-color: var(--accent); }
695
1298
  font-weight: 600;
696
1299
  }
697
1300
  .cost-mode-btn:hover:not(.active) {
698
- background: rgba(0, 212, 170, 0.1);
1301
+ background: rgba(0, 237, 100, 0.1);
699
1302
  color: var(--text);
700
1303
  }
701
1304
  .cost-select {
@@ -743,7 +1346,7 @@ select:focus { outline: none; border-color: var(--accent); }
743
1346
  font-family: var(--mono);
744
1347
  font-size: 20px;
745
1348
  font-weight: 700;
746
- color: var(--accent);
1349
+ color: var(--accent-text);
747
1350
  }
748
1351
  .cost-summary-detail {
749
1352
  font-size: 11px;
@@ -806,10 +1409,10 @@ select:focus { outline: none; border-color: var(--accent); }
806
1409
  align-items: center;
807
1410
  }
808
1411
  .cost-strategy-total-label { font-size: 13px; font-weight: 600; color: var(--text); }
809
- .cost-strategy-total-value { font-family: var(--mono); font-size: 18px; font-weight: 700; color: var(--accent); }
1412
+ .cost-strategy-total-value { font-family: var(--mono); font-size: 18px; font-weight: 700; color: var(--accent-text); }
810
1413
  .cost-savings {
811
1414
  font-size: 11px;
812
- color: #4ade80;
1415
+ color: var(--success);
813
1416
  font-weight: 600;
814
1417
  margin-top: 6px;
815
1418
  text-align: right;
@@ -835,9 +1438,9 @@ select:focus { outline: none; border-color: var(--accent); }
835
1438
  border-bottom: 1px solid rgba(42, 53, 80, 0.3);
836
1439
  font-family: var(--mono);
837
1440
  }
838
- .cost-table tr:hover { background: rgba(0, 212, 170, 0.03); }
1441
+ .cost-table tr:hover { background: rgba(0, 237, 100, 0.03); }
839
1442
  .cost-highlight {
840
- color: var(--accent);
1443
+ color: var(--accent-text);
841
1444
  font-weight: 600;
842
1445
  }
843
1446
  .cost-bar-cell { position: relative; }
@@ -862,12 +1465,136 @@ select:focus { outline: none; border-color: var(--accent); }
862
1465
  .cost-tip {
863
1466
  font-size: 12px;
864
1467
  color: var(--text-muted);
865
- background: rgba(0, 212, 170, 0.05);
1468
+ background: rgba(0, 237, 100, 0.05);
866
1469
  border-left: 3px solid var(--accent);
867
1470
  padding: 10px 14px;
868
1471
  border-radius: 0 6px 6px 0;
869
1472
  margin-top: 16px;
870
1473
  }
1474
+ .cost-help-btn {
1475
+ display: inline-flex;
1476
+ align-items: center;
1477
+ justify-content: center;
1478
+ width: 22px; height: 22px;
1479
+ border-radius: 50%;
1480
+ border: 1.5px solid var(--accent);
1481
+ background: transparent;
1482
+ color: var(--accent);
1483
+ font-size: 13px;
1484
+ font-weight: 700;
1485
+ cursor: pointer;
1486
+ margin-left: 8px;
1487
+ transition: all 0.2s;
1488
+ vertical-align: middle;
1489
+ font-family: var(--mono);
1490
+ line-height: 1;
1491
+ }
1492
+ .cost-help-btn:hover {
1493
+ background: var(--accent);
1494
+ color: var(--bg);
1495
+ box-shadow: 0 0 10px var(--accent-glow);
1496
+ }
1497
+ .cost-modal-overlay {
1498
+ position: fixed;
1499
+ inset: 0;
1500
+ background: rgba(0, 0, 0, 0.7);
1501
+ backdrop-filter: blur(4px);
1502
+ z-index: 1000;
1503
+ display: flex;
1504
+ align-items: center;
1505
+ justify-content: center;
1506
+ opacity: 0;
1507
+ pointer-events: none;
1508
+ transition: opacity 0.25s ease;
1509
+ }
1510
+ .cost-modal-overlay.open {
1511
+ opacity: 1;
1512
+ pointer-events: auto;
1513
+ }
1514
+ .cost-modal {
1515
+ background: var(--bg-surface);
1516
+ border: 1px solid var(--border);
1517
+ border-radius: 14px;
1518
+ max-width: 680px;
1519
+ width: 90%;
1520
+ max-height: 85vh;
1521
+ overflow-y: auto;
1522
+ padding: 32px;
1523
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
1524
+ position: relative;
1525
+ }
1526
+ .cost-modal::-webkit-scrollbar { width: 6px; }
1527
+ .cost-modal::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
1528
+ .cost-modal-close {
1529
+ position: absolute;
1530
+ top: 14px; right: 16px;
1531
+ background: none;
1532
+ border: none;
1533
+ color: var(--text-dim);
1534
+ font-size: 22px;
1535
+ cursor: pointer;
1536
+ padding: 4px 8px;
1537
+ border-radius: 6px;
1538
+ transition: all 0.15s;
1539
+ }
1540
+ .cost-modal-close:hover { background: rgba(255,255,255,0.05); color: var(--text); }
1541
+ .cost-modal h2 {
1542
+ font-size: 18px;
1543
+ color: var(--text);
1544
+ margin: 0 0 20px;
1545
+ display: flex;
1546
+ align-items: center;
1547
+ gap: 10px;
1548
+ }
1549
+ .cost-modal h3 {
1550
+ font-size: 14px;
1551
+ color: var(--accent-text);
1552
+ margin: 22px 0 10px;
1553
+ text-transform: uppercase;
1554
+ letter-spacing: 0.5px;
1555
+ }
1556
+ .cost-modal p, .cost-modal li {
1557
+ font-size: 13px;
1558
+ color: var(--text-dim);
1559
+ line-height: 1.7;
1560
+ }
1561
+ .cost-modal ul { padding-left: 20px; margin: 6px 0; }
1562
+ .cost-modal li { margin-bottom: 4px; }
1563
+ .cost-modal code {
1564
+ background: var(--bg-input);
1565
+ padding: 2px 7px;
1566
+ border-radius: 4px;
1567
+ font-size: 12px;
1568
+ color: var(--accent-text);
1569
+ font-family: var(--mono);
1570
+ }
1571
+ .cost-modal .formula {
1572
+ background: var(--bg-input);
1573
+ border: 1px solid var(--border);
1574
+ border-radius: 8px;
1575
+ padding: 14px 18px;
1576
+ margin: 10px 0;
1577
+ font-family: var(--mono);
1578
+ font-size: 13px;
1579
+ color: var(--text);
1580
+ line-height: 1.8;
1581
+ }
1582
+ .cost-modal .formula .label {
1583
+ color: var(--text-muted);
1584
+ font-size: 11px;
1585
+ }
1586
+ .cost-modal .formula .accent { color: var(--accent); font-weight: 600; }
1587
+ .cost-modal .example {
1588
+ background: rgba(0, 237, 100, 0.05);
1589
+ border-left: 3px solid var(--accent);
1590
+ border-radius: 0 8px 8px 0;
1591
+ padding: 12px 16px;
1592
+ margin: 12px 0;
1593
+ font-size: 12px;
1594
+ color: var(--text-dim);
1595
+ font-family: var(--mono);
1596
+ line-height: 1.8;
1597
+ }
871
1598
 
872
1599
  /* History chart */
873
1600
  .history-empty {
@@ -909,110 +1636,694 @@ select:focus { outline: none; border-color: var(--accent); }
909
1636
  font-size: 12px;
910
1637
  color: var(--text-dim);
911
1638
  }
912
- .history-legend-dot {
913
- display: inline-block;
914
- width: 10px; height: 10px;
915
- border-radius: 2px;
916
- margin-right: 4px;
917
- vertical-align: middle;
1639
+ .history-legend-dot {
1640
+ display: inline-block;
1641
+ width: 10px; height: 10px;
1642
+ border-radius: 2px;
1643
+ margin-right: 4px;
1644
+ vertical-align: middle;
1645
+ }
1646
+ .history-labels {
1647
+ display: flex;
1648
+ justify-content: space-between;
1649
+ font-size: 10px;
1650
+ color: var(--text-muted);
1651
+ margin-top: 4px;
1652
+ }
1653
+
1654
+ /* About page */
1655
+ .about-container { max-width: 680px; margin: 0 auto; }
1656
+ .about-header { display: flex; gap: 24px; align-items: center; margin-bottom: 24px; }
1657
+ .about-avatar {
1658
+ width: 120px; height: 120px;
1659
+ border-radius: 50%;
1660
+ border: 3px solid var(--accent);
1661
+ box-shadow: 0 0 20px var(--accent-glow);
1662
+ flex-shrink: 0;
1663
+ }
1664
+ .about-name { font-size: 24px; font-weight: 700; color: var(--text); }
1665
+ .about-role { font-size: 14px; color: var(--accent-text); margin-top: 4px; }
1666
+ .about-links { display: flex; gap: 12px; margin-top: 8px; }
1667
+ .about-links a {
1668
+ color: var(--text-dim);
1669
+ font-size: 13px;
1670
+ text-decoration: none;
1671
+ transition: color 0.2s;
1672
+ }
1673
+ .about-links a:hover { color: var(--accent); }
1674
+ .about-section { margin-bottom: 24px; }
1675
+ .about-section-title {
1676
+ font-size: 13px;
1677
+ font-weight: 600;
1678
+ color: var(--accent-text);
1679
+ text-transform: uppercase;
1680
+ letter-spacing: 0.5px;
1681
+ margin-bottom: 8px;
1682
+ }
1683
+ .about-text { font-size: 14px; line-height: 1.8; color: var(--text); }
1684
+ .about-text a { color: var(--blue); text-decoration: none; }
1685
+ .about-text a:hover { text-decoration: underline; }
1686
+ .about-disclaimer {
1687
+ background: rgba(255, 215, 61, 0.08);
1688
+ border: 1px solid rgba(255, 215, 61, 0.2);
1689
+ border-radius: var(--radius);
1690
+ padding: 16px 20px;
1691
+ margin-top: 24px;
1692
+ }
1693
+ .about-disclaimer-title {
1694
+ font-size: 13px;
1695
+ font-weight: 600;
1696
+ color: var(--warning);
1697
+ margin-bottom: 6px;
1698
+ }
1699
+ .about-disclaimer-text {
1700
+ font-size: 13px;
1701
+ line-height: 1.7;
1702
+ color: var(--text-dim);
1703
+ }
1704
+
1705
+ /* โ”€โ”€ Page Headers โ”€โ”€ */
1706
+ .page-header {
1707
+ margin-bottom: 24px;
1708
+ padding-bottom: 16px;
1709
+ border-bottom: 1px solid var(--border);
1710
+ }
1711
+ .page-header-title {
1712
+ font-size: 22px;
1713
+ font-weight: 700;
1714
+ color: var(--accent-text, var(--text));
1715
+ margin: 0 0 4px;
1716
+ letter-spacing: -0.3px;
1717
+ }
1718
+ .page-header-subtitle {
1719
+ font-size: 14px;
1720
+ color: var(--text-dim);
1721
+ margin: 0 0 6px;
1722
+ font-weight: 500;
1723
+ }
1724
+ .page-header-hint {
1725
+ font-size: 12px;
1726
+ color: var(--text-muted);
1727
+ margin: 0;
1728
+ line-height: 1.5;
1729
+ }
1730
+
1731
+ /* โ”€โ”€ Settings page โ”€โ”€ */
1732
+ .settings-container { max-width: 640px; margin: 0 auto; }
1733
+ .settings-section {
1734
+ background: var(--bg-card);
1735
+ border: 1px solid var(--border);
1736
+ border-radius: var(--radius);
1737
+ padding: 24px;
1738
+ margin-bottom: 20px;
1739
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1740
+ }
1741
+ [data-theme="light"] .settings-section {
1742
+ box-shadow: 0 1px 4px rgba(0,30,43,0.07);
1743
+ border-color: #DDE4E2;
1744
+ }
1745
+ .settings-section-title {
1746
+ font-size: 11px;
1747
+ font-weight: 700;
1748
+ color: var(--text-muted);
1749
+ text-transform: uppercase;
1750
+ letter-spacing: 1px;
1751
+ margin-bottom: 16px;
1752
+ padding-bottom: 10px;
1753
+ border-bottom: 1px solid var(--border);
1754
+ }
1755
+ /* API key alert banner */
1756
+ .settings-api-banner {
1757
+ background: rgba(255,192,16,0.08);
1758
+ border: 1px solid rgba(255,192,16,0.25);
1759
+ border-radius: var(--radius);
1760
+ padding: 14px 18px;
1761
+ margin-bottom: 16px;
1762
+ display: flex;
1763
+ align-items: center;
1764
+ gap: 10px;
1765
+ font-size: 13px;
1766
+ color: var(--warning);
1767
+ line-height: 1.5;
1768
+ }
1769
+ .settings-api-banner.success {
1770
+ background: rgba(0,237,100,0.06);
1771
+ border-color: rgba(0,237,100,0.2);
1772
+ color: var(--success);
1773
+ }
1774
+ [data-theme="light"] .settings-api-banner {
1775
+ background: rgba(148,79,1,0.06);
1776
+ border-color: rgba(148,79,1,0.2);
1777
+ }
1778
+ [data-theme="light"] .settings-api-banner.success {
1779
+ background: rgba(0,104,74,0.06);
1780
+ border-color: rgba(0,104,74,0.2);
1781
+ }
1782
+ .settings-api-banner-icon { font-size: 20px; flex-shrink: 0; }
1783
+ /* Heatmap preview swatches */
1784
+ .heatmap-preview {
1785
+ display: inline-block;
1786
+ width: 80px;
1787
+ height: 12px;
1788
+ border-radius: 3px;
1789
+ vertical-align: middle;
1790
+ margin-left: 8px;
1791
+ border: 1px solid var(--border);
1792
+ }
1793
+ .heatmap-opt {
1794
+ display: flex;
1795
+ align-items: center;
1796
+ gap: 6px;
1797
+ }
1798
+ /* Get a key link as button */
1799
+ .settings-key-link {
1800
+ display: inline-flex;
1801
+ align-items: center;
1802
+ gap: 4px;
1803
+ color: var(--blue);
1804
+ font-size: 12px;
1805
+ font-weight: 600;
1806
+ text-decoration: none;
1807
+ padding: 4px 10px;
1808
+ border: 1px solid var(--blue);
1809
+ border-radius: 4px;
1810
+ transition: all 0.15s;
1811
+ white-space: nowrap;
1812
+ }
1813
+ .settings-key-link:hover {
1814
+ background: rgba(4,152,236,0.1);
1815
+ text-decoration: none;
1816
+ }
1817
+ /* API key field integration */
1818
+ .settings-api-field {
1819
+ display: flex;
1820
+ align-items: center;
1821
+ border: 1px solid var(--border);
1822
+ border-radius: var(--radius);
1823
+ background: var(--bg-input);
1824
+ overflow: hidden;
1825
+ transition: border-color 0.2s;
1826
+ }
1827
+ .settings-api-field:focus-within { border-color: var(--accent); }
1828
+ .settings-api-field input {
1829
+ flex: 1;
1830
+ border: none;
1831
+ background: transparent;
1832
+ padding: 8px 12px;
1833
+ font-size: 13px;
1834
+ font-family: var(--mono);
1835
+ color: var(--text);
1836
+ min-width: 0;
1837
+ }
1838
+ .settings-api-field input:focus { outline: none; }
1839
+ .settings-api-field button {
1840
+ background: none;
1841
+ border: none;
1842
+ border-left: 1px solid var(--border);
1843
+ padding: 8px 10px;
1844
+ cursor: pointer;
1845
+ font-size: 14px;
1846
+ color: var(--text-dim);
1847
+ transition: all 0.15s;
1848
+ flex-shrink: 0;
1849
+ }
1850
+ .settings-api-field button:hover { color: var(--text); background: rgba(255,255,255,0.05); }
1851
+ [data-theme="light"] .settings-api-field button:hover { background: rgba(0,0,0,0.03); }
1852
+ /* Version badges */
1853
+ .version-badge {
1854
+ display: inline-block;
1855
+ font-family: var(--mono);
1856
+ font-size: 13px;
1857
+ font-weight: 600;
1858
+ color: var(--accent);
1859
+ background: rgba(0,237,100,0.08);
1860
+ border: 1px solid rgba(0,237,100,0.2);
1861
+ border-radius: 6px;
1862
+ padding: 4px 12px;
1863
+ letter-spacing: 0.3px;
1864
+ }
1865
+ [data-theme="light"] .version-badge {
1866
+ background: rgba(0,104,74,0.06);
1867
+ border-color: rgba(0,104,74,0.2);
1868
+ color: #00684A;
1869
+ }
1870
+ .settings-api-field .save-btn {
1871
+ color: var(--accent);
1872
+ font-size: 12px;
1873
+ font-weight: 600;
1874
+ font-family: var(--font);
1875
+ padding: 8px 14px;
1876
+ }
1877
+ .settings-row {
1878
+ display: flex;
1879
+ align-items: center;
1880
+ justify-content: space-between;
1881
+ padding: 12px 0;
1882
+ border-bottom: 1px solid rgba(255,255,255,0.04);
1883
+ }
1884
+ .settings-row:last-child { border-bottom: none; }
1885
+ [data-theme="light"] .settings-row { border-bottom-color: rgba(0,0,0,0.04); }
1886
+ .settings-label {
1887
+ display: flex;
1888
+ flex-direction: column;
1889
+ gap: 2px;
1890
+ }
1891
+ .settings-label-text {
1892
+ font-size: 14px;
1893
+ font-weight: 500;
1894
+ color: var(--text);
1895
+ }
1896
+ .settings-label-hint {
1897
+ font-size: 12px;
1898
+ color: var(--text-muted);
1899
+ }
1900
+ .settings-control { flex-shrink: 0; }
1901
+ .settings-select {
1902
+ background: var(--bg-input);
1903
+ border: 1px solid var(--border);
1904
+ color: var(--text);
1905
+ padding: 8px 12px;
1906
+ border-radius: var(--radius);
1907
+ font-size: 13px;
1908
+ font-family: var(--mono);
1909
+ cursor: pointer;
1910
+ min-width: 180px;
1911
+ }
1912
+ .settings-select:focus { outline: none; border-color: var(--accent); }
1913
+ .settings-input {
1914
+ background: var(--bg-input);
1915
+ border: 1px solid var(--border);
1916
+ color: var(--text);
1917
+ padding: 8px 12px;
1918
+ border-radius: var(--radius);
1919
+ font-size: 13px;
1920
+ font-family: var(--mono);
1921
+ min-width: 180px;
1922
+ }
1923
+ .settings-input:focus { outline: none; border-color: var(--accent); }
1924
+ .settings-toggle {
1925
+ position: relative;
1926
+ width: 44px;
1927
+ height: 24px;
1928
+ background: var(--border);
1929
+ border-radius: 12px;
1930
+ border: none;
1931
+ cursor: pointer;
1932
+ transition: background 0.2s;
1933
+ padding: 0;
1934
+ }
1935
+ .settings-toggle.active { background: var(--accent); }
1936
+ .settings-toggle::after {
1937
+ content: '';
1938
+ position: absolute;
1939
+ top: 2px;
1940
+ left: 2px;
1941
+ width: 20px;
1942
+ height: 20px;
1943
+ background: white;
1944
+ border-radius: 50%;
1945
+ transition: transform 0.2s;
1946
+ }
1947
+ .settings-toggle.active::after { transform: translateX(20px); }
1948
+ .settings-saved {
1949
+ display: inline-flex;
1950
+ align-items: center;
1951
+ gap: 4px;
1952
+ font-size: 12px;
1953
+ color: var(--success);
1954
+ opacity: 0;
1955
+ transition: opacity 0.3s;
1956
+ }
1957
+ .settings-saved.show { opacity: 1; }
1958
+ .settings-reset-btn {
1959
+ background: transparent;
1960
+ border: 1px solid var(--border);
1961
+ color: var(--text-dim);
1962
+ padding: 8px 16px;
1963
+ border-radius: var(--radius);
1964
+ font-size: 13px;
1965
+ cursor: pointer;
1966
+ transition: all 0.2s;
1967
+ }
1968
+ .settings-reset-btn:hover { border-color: var(--error); color: var(--error); }
1969
+ .settings-toggle-vis {
1970
+ background: none;
1971
+ border: 1px solid var(--border);
1972
+ border-radius: var(--radius);
1973
+ padding: 6px 8px;
1974
+ cursor: pointer;
1975
+ font-size: 14px;
1976
+ line-height: 1;
1977
+ transition: all 0.2s;
1978
+ color: var(--text-dim);
1979
+ }
1980
+ .settings-toggle-vis:hover { border-color: var(--accent); }
1981
+ .settings-api-key-active { color: var(--success); }
1982
+ .settings-api-key-missing { color: var(--warning); }
1983
+ .settings-row .settings-select, .settings-row .settings-input { text-align: right; }
1984
+
1985
+ /* โ”€โ”€ Multimodal Tab โ”€โ”€ */
1986
+ .mm-grid {
1987
+ display: grid;
1988
+ grid-template-columns: 1fr 1fr;
1989
+ gap: 16px;
1990
+ }
1991
+ .mm-drop-zone {
1992
+ border: 2px dashed var(--border);
1993
+ border-radius: var(--radius);
1994
+ padding: 40px 24px;
1995
+ text-align: center;
1996
+ cursor: pointer;
1997
+ transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
1998
+ background: var(--bg-input);
1999
+ min-height: 200px;
2000
+ display: flex;
2001
+ flex-direction: column;
2002
+ align-items: center;
2003
+ justify-content: center;
2004
+ gap: 12px;
2005
+ }
2006
+ .mm-drop-zone:hover {
2007
+ border-color: var(--accent);
2008
+ box-shadow: 0 0 0 3px var(--accent-glow);
2009
+ }
2010
+ .mm-drop-zone.drag-active {
2011
+ border-color: var(--accent);
2012
+ background: var(--accent-glow);
2013
+ box-shadow: 0 0 0 4px var(--accent-glow);
2014
+ }
2015
+ .mm-drop-zone .mm-drop-icon {
2016
+ font-size: 40px;
2017
+ opacity: 0.6;
2018
+ }
2019
+ .mm-drop-zone .mm-drop-text {
2020
+ color: var(--text-dim);
2021
+ font-size: 14px;
2022
+ }
2023
+ .mm-drop-zone .mm-drop-hint {
2024
+ color: var(--text-muted);
2025
+ font-size: 12px;
2026
+ }
2027
+ .mm-preview {
2028
+ display: none;
2029
+ flex-direction: column;
2030
+ align-items: center;
2031
+ gap: 12px;
2032
+ padding: 16px;
2033
+ }
2034
+ .mm-preview.visible { display: flex; }
2035
+ .mm-preview img {
2036
+ max-height: 300px;
2037
+ max-width: 100%;
2038
+ object-fit: contain;
2039
+ border-radius: var(--radius);
2040
+ border: 1px solid var(--border);
2041
+ }
2042
+ .mm-preview .mm-file-info {
2043
+ font-size: 12px;
2044
+ color: var(--text-muted);
2045
+ font-family: var(--mono);
2046
+ }
2047
+ .mm-preview .mm-clear-btn {
2048
+ background: none;
2049
+ border: 1px solid var(--border);
2050
+ color: var(--text-dim);
2051
+ border-radius: 6px;
2052
+ padding: 4px 14px;
2053
+ font-size: 12px;
2054
+ cursor: pointer;
2055
+ font-family: var(--font);
2056
+ transition: border-color 0.15s, color 0.15s;
2057
+ }
2058
+ .mm-preview .mm-clear-btn:hover {
2059
+ border-color: var(--error);
2060
+ color: var(--error);
2061
+ }
2062
+ .mm-gallery-section {
2063
+ margin-top: 32px;
2064
+ padding-top: 24px;
2065
+ border-top: 1px solid var(--border);
2066
+ }
2067
+ .mm-gallery-grid {
2068
+ display: grid;
2069
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
2070
+ gap: 12px;
2071
+ margin-bottom: 16px;
2072
+ }
2073
+ .mm-gallery-slot {
2074
+ aspect-ratio: 1;
2075
+ border: 2px dashed var(--border);
2076
+ border-radius: var(--radius);
2077
+ display: flex;
2078
+ align-items: center;
2079
+ justify-content: center;
2080
+ cursor: pointer;
2081
+ transition: border-color 0.2s, background 0.2s;
2082
+ background: var(--bg-input);
2083
+ position: relative;
2084
+ overflow: hidden;
2085
+ }
2086
+ .mm-gallery-slot:hover {
2087
+ border-color: var(--accent);
2088
+ background: var(--accent-glow);
2089
+ }
2090
+ .mm-gallery-slot.filled {
2091
+ border-style: solid;
2092
+ cursor: default;
2093
+ padding: 0;
2094
+ }
2095
+ .mm-gallery-slot.filled img {
2096
+ width: 100%;
2097
+ height: 100%;
2098
+ object-fit: cover;
2099
+ border-radius: calc(var(--radius) - 2px);
2100
+ }
2101
+ .mm-gallery-slot .mm-slot-remove {
2102
+ position: absolute;
2103
+ top: 4px;
2104
+ right: 4px;
2105
+ background: rgba(0,0,0,0.6);
2106
+ color: #fff;
2107
+ border: none;
2108
+ border-radius: 50%;
2109
+ width: 22px;
2110
+ height: 22px;
2111
+ font-size: 13px;
2112
+ cursor: pointer;
2113
+ display: none;
2114
+ align-items: center;
2115
+ justify-content: center;
2116
+ line-height: 1;
2117
+ }
2118
+ .mm-gallery-slot.filled:hover .mm-slot-remove { display: flex; }
2119
+ .mm-gallery-slot .mm-slot-add {
2120
+ font-size: 28px;
2121
+ color: var(--text-muted);
2122
+ pointer-events: none;
2123
+ }
2124
+ .mm-gallery-slot.query-selected {
2125
+ border-color: var(--accent);
2126
+ box-shadow: 0 0 0 3px var(--accent-glow);
2127
+ }
2128
+ .mm-result-list {
2129
+ display: flex;
2130
+ flex-direction: column;
2131
+ gap: 8px;
918
2132
  }
919
- .history-labels {
2133
+ .mm-result-item {
920
2134
  display: flex;
921
- justify-content: space-between;
922
- font-size: 10px;
2135
+ align-items: center;
2136
+ gap: 12px;
2137
+ padding: 12px 16px;
2138
+ background: var(--bg-input);
2139
+ border: 1px solid var(--border);
2140
+ border-radius: var(--radius);
2141
+ transition: border-color 0.15s;
2142
+ }
2143
+ .mm-result-item:first-child {
2144
+ border-color: var(--accent);
2145
+ background: var(--accent-glow);
2146
+ }
2147
+ .mm-result-item .mm-result-rank {
2148
+ font-family: var(--mono);
2149
+ font-weight: 700;
2150
+ font-size: 18px;
923
2151
  color: var(--text-muted);
924
- margin-top: 4px;
2152
+ min-width: 28px;
2153
+ text-align: center;
925
2154
  }
926
-
927
- /* About page */
928
- .about-container { max-width: 680px; margin: 0 auto; }
929
- .about-header { display: flex; gap: 24px; align-items: center; margin-bottom: 24px; }
930
- .about-avatar {
931
- width: 120px; height: 120px;
932
- border-radius: 50%;
933
- border: 3px solid var(--accent);
934
- box-shadow: 0 0 20px var(--accent-glow);
2155
+ .mm-result-item:first-child .mm-result-rank { color: var(--accent); }
2156
+ .mm-result-item .mm-result-thumb {
2157
+ width: 48px;
2158
+ height: 48px;
2159
+ border-radius: 6px;
2160
+ object-fit: cover;
2161
+ border: 1px solid var(--border);
935
2162
  flex-shrink: 0;
936
2163
  }
937
- .about-name { font-size: 24px; font-weight: 700; color: var(--text); }
938
- .about-role { font-size: 14px; color: var(--accent); margin-top: 4px; }
939
- .about-links { display: flex; gap: 12px; margin-top: 8px; }
940
- .about-links a {
941
- color: var(--text-dim);
942
- font-size: 13px;
943
- text-decoration: none;
944
- transition: color 0.2s;
2164
+ .mm-result-item .mm-result-content {
2165
+ flex: 1;
2166
+ min-width: 0;
945
2167
  }
946
- .about-links a:hover { color: var(--accent); }
947
- .about-section { margin-bottom: 24px; }
948
- .about-section-title {
949
- font-size: 13px;
950
- font-weight: 600;
951
- color: var(--accent);
2168
+ .mm-result-item .mm-result-text {
2169
+ font-size: 14px;
2170
+ color: var(--text);
2171
+ white-space: nowrap;
2172
+ overflow: hidden;
2173
+ text-overflow: ellipsis;
2174
+ }
2175
+ .mm-result-item .mm-result-type {
2176
+ font-size: 11px;
2177
+ color: var(--text-muted);
952
2178
  text-transform: uppercase;
953
2179
  letter-spacing: 0.5px;
954
- margin-bottom: 8px;
955
- }
956
- .about-text { font-size: 14px; line-height: 1.8; color: var(--text); }
957
- .about-text a { color: var(--accent); text-decoration: none; }
958
- .about-text a:hover { text-decoration: underline; }
959
- .about-disclaimer {
960
- background: rgba(255, 215, 61, 0.08);
961
- border: 1px solid rgba(255, 215, 61, 0.2);
962
- border-radius: var(--radius);
963
- padding: 16px 20px;
964
- margin-top: 24px;
965
2180
  }
966
- .about-disclaimer-title {
967
- font-size: 13px;
2181
+ .mm-result-item .mm-result-score {
2182
+ font-family: var(--mono);
968
2183
  font-weight: 600;
969
- color: var(--warning);
970
- margin-bottom: 6px;
2184
+ font-size: 14px;
2185
+ flex-shrink: 0;
971
2186
  }
972
- .about-disclaimer-text {
973
- font-size: 13px;
974
- line-height: 1.7;
2187
+ .mm-search-row {
2188
+ display: flex;
2189
+ gap: 8px;
2190
+ align-items: center;
2191
+ margin-bottom: 12px;
2192
+ }
2193
+ .mm-search-row input[type="text"] {
2194
+ flex: 1;
2195
+ }
2196
+ .mm-search-mode {
2197
+ display: flex;
2198
+ gap: 6px;
2199
+ margin-bottom: 12px;
2200
+ }
2201
+ .mm-search-mode button {
2202
+ background: var(--bg-input);
2203
+ border: 1px solid var(--border);
975
2204
  color: var(--text-dim);
2205
+ border-radius: 6px;
2206
+ padding: 4px 12px;
2207
+ font-size: 12px;
2208
+ cursor: pointer;
2209
+ font-family: var(--font);
2210
+ transition: all 0.15s;
2211
+ }
2212
+ .mm-search-mode button.active {
2213
+ border-color: var(--accent);
2214
+ color: var(--accent);
2215
+ background: var(--accent-glow);
976
2216
  }
977
2217
 
978
2218
  @media (max-width: 768px) {
2219
+ .mm-grid { grid-template-columns: 1fr; }
2220
+ .mm-gallery-grid { grid-template-columns: repeat(3, 1fr); }
979
2221
  .compare-grid, .search-results { grid-template-columns: 1fr; }
980
- .nav { padding: 0 12px; }
2222
+ .sidebar { width: 56px; min-width: 56px; }
2223
+ .sidebar-title, .status-label { display: none; }
2224
+ .tab-btn { justify-content: center; padding: 10px; }
2225
+ .tab-btn span:not(.tab-btn-icon) { display: none; }
981
2226
  .main { padding: 16px; }
982
- .tab-btn { padding: 10px 14px; font-size: 13px; }
2227
+ .settings-row { flex-direction: column; align-items: flex-start; gap: 8px; }
2228
+ .settings-select, .settings-input { min-width: 100%; }
983
2229
  }
984
2230
  </style>
985
2231
  </head>
986
2232
  <body>
987
2233
 
988
- <!-- Nav -->
989
- <nav class="nav">
990
- <div class="nav-title">๐Ÿงญ Voyage AI Playground</div>
991
- <div class="nav-spacer"></div>
992
- <div class="option-group">
993
- <span class="option-label">Default Model</span>
994
- <select id="globalModel" class="nav-model-select"></select>
2234
+ <!-- LeafyGreen Icon Sprites (mongodb.design) -->
2235
+ <svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
2236
+ <symbol id="lg-lightning" viewBox="0 0 16 16">
2237
+ <path d="M9.22274 1.99296C9.22274 1.49561 8.56293 1.31233 8.30107 1.73667L4.07384 8.58696C3.87133 8.91513 4.10921 9.33717 4.49748 9.33717H6.77682L6.77682 14.0066C6.77682 14.504 7.43627 14.6879 7.69813 14.2635L11.9262 7.4118C12.1288 7.08363 11.8903 6.66244 11.5021 6.66244H9.22274V1.99296Z" fill="currentColor"/>
2238
+ </symbol>
2239
+ <symbol id="lg-arrows" viewBox="0 0 16 16">
2240
+ <path d="M5 8.57279V13.4272C5 13.9565 4.39241 14.2015 4.0721 13.8014L2.12898 11.3742C1.95701 11.1594 1.95701 10.8406 2.12898 10.6258L4.0721 8.19856C4.39241 7.79846 5 8.04351 5 8.57279Z" fill="currentColor"/>
2241
+ <path d="M5 10H12.5C12.7761 10 13 10.2239 13 10.5V11.5C13 11.7761 12.7761 12 12.5 12H5V10Z" fill="currentColor"/>
2242
+ <path d="M11 7.42721V2.57279C11 2.04351 11.6076 1.79846 11.9279 2.19856L13.871 4.62577C14.043 4.84058 14.043 5.15942 13.871 5.37423L11.9279 7.80144C11.6076 8.20154 11 7.95649 11 7.42721Z" fill="currentColor"/>
2243
+ <path d="M3 4.5C3 4.22386 3.22386 4 3.5 4H11V6H3.5C3.22386 6 3 5.77614 3 5.5V4.5Z" fill="currentColor"/>
2244
+ </symbol>
2245
+ <symbol id="lg-search" viewBox="0 0 16 16">
2246
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M2.3234 9.81874C4.07618 11.5715 6.75062 11.8398 8.78588 10.6244L12.93 14.7685C13.4377 15.2762 14.2608 15.2762 14.7685 14.7685C15.2762 14.2608 15.2762 13.4377 14.7685 12.93L10.6244 8.78588C11.8398 6.75062 11.5715 4.07619 9.81873 2.32341C7.74896 0.253628 4.39318 0.253628 2.3234 2.32341C0.253624 4.39319 0.253624 7.74896 2.3234 9.81874ZM7.98026 4.16188C9.03467 5.2163 9.03467 6.92585 7.98026 7.98026C6.92584 9.03468 5.2163 9.03468 4.16188 7.98026C3.10746 6.92585 3.10746 5.2163 4.16188 4.16188C5.2163 3.10747 6.92584 3.10747 7.98026 4.16188Z" fill="currentColor"/>
2247
+ </symbol>
2248
+ <symbol id="lg-gauge" viewBox="0 0 16 16">
2249
+ <path d="M1.041 10.2514C0.996713 10.6632 1.33666 11 1.75088 11H4.2449C4.65912 11 4.98569 10.6591 5.08798 10.2577C5.22027 9.73864 5.49013 9.25966 5.87533 8.87446C6.43906 8.31073 7.20364 7.99403 8.00088 7.99403C8.27011 7.99403 8.53562 8.03015 8.79093 8.0997L11.7818 5.10887C10.6623 4.39046 9.35172 4 8.00088 4C6.14436 4 4.36388 4.7375 3.05113 6.05025C1.91604 7.18534 1.21104 8.67012 1.041 10.2514Z" fill="currentColor"/>
2250
+ <path d="M13.2967 6.42237L10.455 9.26409C10.6678 9.56493 10.8231 9.90191 10.9138 10.2577C11.0161 10.6591 11.3426 11 11.7568 11L14.2509 11C14.6651 11 15.005 10.6632 14.9608 10.2514C14.8087 8.83759 14.229 7.50093 13.2967 6.42237Z" fill="currentColor"/>
2251
+ </symbol>
2252
+ <symbol id="lg-bulb" viewBox="0 0 16 16">
2253
+ <path d="M12.3311 8.5C12.7565 7.76457 13 6.91072 13 6C13 3.23858 10.7614 1 8 1C5.23858 1 3 3.23858 3 6C3 6.94628 3.26287 7.83117 3.71958 8.58561L5.40749 11.501C5.58628 11.8099 5.91607 12 6.27291 12H6.5V6C6.5 5.17157 7.17157 4.5 8 4.5C8.82843 4.5 9.5 5.17157 9.5 6V12H9.72368C10.0793 12 10.4082 11.8111 10.5874 11.5039L12.34 8.5H12.3311Z" fill="currentColor"/>
2254
+ <path d="M7.5 6V12H8.5V6C8.5 5.72386 8.27614 5.5 8 5.5C7.72386 5.5 7.5 5.72386 7.5 6Z" fill="currentColor"/>
2255
+ <path d="M10 14V13H6V14C6 14.5523 6.44772 15 7 15H9C9.55228 15 10 14.5523 10 14Z" fill="currentColor"/>
2256
+ </symbol>
2257
+ <symbol id="lg-info" viewBox="0 0 16 16">
2258
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM9 4C9 4.55228 8.55228 5 8 5C7.44772 5 7 4.55228 7 4C7 3.44772 7.44772 3 8 3C8.55228 3 9 3.44772 9 4ZM8 6C8.55228 6 9 6.44772 9 7V11H9.5C9.77614 11 10 11.2239 10 11.5C10 11.7761 9.77614 12 9.5 12H6.5C6.22386 12 6 11.7761 6 11.5C6 11.2239 6.22386 11 6.5 11H7V7H6.5C6.22386 7 6 6.77614 6 6.5C6 6.22386 6.22386 6 6.5 6H8Z" fill="currentColor"/>
2259
+ </symbol>
2260
+ <symbol id="lg-image" viewBox="0 0 16 16">
2261
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M2 3C2 2.44772 2.44772 2 3 2H13C13.5523 2 14 2.44772 14 3V13C14 13.5523 13.5523 14 13 14H3C2.44772 14 2 13.5523 2 13V3ZM4 4V9.586L5.793 7.793C6.183 7.403 6.817 7.403 7.207 7.793L8.5 9.086L10.293 7.293C10.683 6.903 11.317 6.903 11.707 7.293L12 7.586V4H4ZM12 10.414L10.5 8.914L8.207 11.207C7.817 11.597 7.183 11.597 6.793 11.207L5.5 9.914L4 11.414V12H12V10.414ZM10 5.5C10 6.328 10.672 7 11.5 7C11.776 7 12 6.776 12 6.5V5C12 4.724 11.776 4.5 11.5 4.5H10.5C10.224 4.5 10 4.724 10 5V5.5Z" fill="currentColor"/>
2262
+ </symbol>
2263
+ <symbol id="lg-config" viewBox="0 0 16 16">
2264
+ <path d="M12.7 2.56989C12.41 2.56989 12.17 2.80989 12.17 3.09989V4.34989L11.32 3.49989C9.21 1.39989 5.75 1.51989 3.78 3.75989C2.65 5.03989 2.23 6.82989 2.68 8.48989C3.24 10.5299 4.98 12.0099 7.08 12.2299L8.1 12.3399C8.42 13.2799 9.3 13.9499 10.34 13.9499C11.65 13.9499 12.71 12.8899 12.71 11.5799C12.71 10.2699 11.65 9.20989 10.34 9.20989C9.14 9.20989 8.16 10.0999 8 11.2599L7.19 11.1799C5.53 11.0099 4.14 9.82989 3.7 8.21989C3.34 6.90989 3.67 5.48989 4.58 4.46989C6.15 2.68989 8.9 2.59989 10.57 4.26989L11.42 5.11989H10.17C9.88 5.11989 9.64 5.35989 9.64 5.64989C9.64 5.93989 9.88 6.17989 10.17 6.17989H12.71C13 6.17989 13.24 5.93989 13.24 5.64989V3.09989C13.24 2.80989 13 2.56989 12.71 2.56989H12.7ZM10.34 10.3099C11.04 10.3099 11.6 10.8799 11.6 11.5699C11.6 12.2599 11.03 12.8299 10.34 12.8299C9.65 12.8299 9.08 12.2599 9.08 11.5699C9.08 10.8799 9.65 10.3099 10.34 10.3099Z" fill="currentColor"/>
2265
+ </symbol>
2266
+ </svg>
2267
+
2268
+ <div class="app-shell">
2269
+
2270
+ <!-- Sidebar -->
2271
+ <aside class="sidebar">
2272
+ <div class="sidebar-drag-region">
2273
+ <img class="sidebar-logo" id="sidebarLogo" src="/icons/dark/64.png" alt="Vai">
2274
+ <span class="sidebar-title">Vai</span>
2275
+ </div>
2276
+ <nav class="sidebar-nav">
2277
+ <div class="sidebar-nav-group">
2278
+ <div class="sidebar-nav-label">Tools</div>
2279
+ <button class="tab-btn active" data-tab="embed"><span class="tab-btn-icon"><svg><use href="#lg-lightning"/></svg></span><span>Embed</span></button>
2280
+ <button class="tab-btn" data-tab="compare"><span class="tab-btn-icon"><svg><use href="#lg-arrows"/></svg></span><span>Compare</span></button>
2281
+ <button class="tab-btn" data-tab="search"><span class="tab-btn-icon"><svg><use href="#lg-search"/></svg></span><span>Search</span></button>
2282
+ <button class="tab-btn" data-tab="multimodal"><span class="tab-btn-icon"><svg><use href="#lg-image"/></svg></span><span>Multimodal</span></button>
2283
+ </div>
2284
+ <div class="sidebar-nav-divider"></div>
2285
+ <div class="sidebar-nav-group">
2286
+ <div class="sidebar-nav-label">Resources</div>
2287
+ <button class="tab-btn" data-tab="benchmark"><span class="tab-btn-icon"><svg><use href="#lg-gauge"/></svg></span><span>Benchmark</span></button>
2288
+ <button class="tab-btn" data-tab="explore"><span class="tab-btn-icon"><svg><use href="#lg-bulb"/></svg></span><span>Explore</span></button>
2289
+ <button class="tab-btn" data-tab="about"><span class="tab-btn-icon"><svg><use href="#lg-info"/></svg></span><span>About</span></button>
2290
+ </div>
2291
+ <div class="sidebar-nav-divider"></div>
2292
+ <button class="tab-btn" data-tab="settings"><span class="tab-btn-icon"><svg><use href="#lg-config"/></svg></span><span>Settings</span></button>
2293
+ </nav>
2294
+ <div class="sidebar-footer">
2295
+ <div style="display:flex;align-items:center;gap:8px;justify-content:space-between;">
2296
+ <div style="display:flex;align-items:center;gap:5px;">
2297
+ <div class="status-dot" id="statusDot"></div>
2298
+ <span class="status-label" id="statusLabel">Checking...</span>
2299
+ </div>
2300
+ <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">๐ŸŒ™</button>
2301
+ </div>
2302
+ <div id="appVersionLabel" style="font-size:10px;color:var(--text-muted);text-align:center;"></div>
995
2303
  </div>
996
- <div style="display:flex;align-items:center;gap:6px;">
997
- <div class="status-dot" id="statusDot"></div>
998
- <span class="status-label" id="statusLabel">Checking...</span>
2304
+ </aside>
2305
+
2306
+ <!-- Content -->
2307
+ <div class="content-area">
2308
+ <div class="content-drag-region"></div>
2309
+ <div class="update-banner" id="updateBanner">
2310
+ <span class="update-banner-icon">๐Ÿš€</span>
2311
+ <span class="update-banner-text">
2312
+ <strong>Vai <span id="updateVersion"></span></strong> is available.
2313
+ You're on <span id="currentVersion"></span>.
2314
+ </span>
2315
+ <button class="update-banner-btn" id="updateDownloadBtn">Download</button>
2316
+ <button class="update-banner-dismiss" id="updateDismissBtn" title="Dismiss">ร—</button>
999
2317
  </div>
1000
- </nav>
1001
-
1002
- <!-- Tabs -->
1003
- <div class="tab-bar">
1004
- <button class="tab-btn active" data-tab="embed">โšก Embed</button>
1005
- <button class="tab-btn" data-tab="compare">โš–๏ธ Compare</button>
1006
- <button class="tab-btn" data-tab="search">๐Ÿ” Search</button>
1007
- <button class="tab-btn" data-tab="benchmark">โฑ Benchmark</button>
1008
- <button class="tab-btn" data-tab="explore">๐Ÿ“š Explore</button>
1009
- <button class="tab-btn" data-tab="about">โ„น๏ธ About</button>
1010
- </div>
1011
-
1012
- <div class="main">
2318
+ <div class="main">
1013
2319
 
1014
2320
  <!-- ========== EMBED TAB ========== -->
1015
2321
  <div class="tab-panel active" id="tab-embed">
2322
+ <div class="page-header">
2323
+ <h2 class="page-header-title">Embed</h2>
2324
+ <p class="page-header-subtitle">Generate vector embeddings for text</p>
2325
+ <p class="page-header-hint">Paste or type text below, choose a model, and hit Embed to see the raw vectors and token usage.</p>
2326
+ </div>
1016
2327
  <div class="card">
1017
2328
  <div class="card-title">Input Text</div>
1018
2329
  <textarea id="embedInput" rows="5" placeholder="Enter text to embed...">MongoDB Atlas provides powerful vector search capabilities for AI applications.</textarea>
@@ -1072,6 +2383,11 @@ select:focus { outline: none; border-color: var(--accent); }
1072
2383
 
1073
2384
  <!-- ========== COMPARE TAB ========== -->
1074
2385
  <div class="tab-panel" id="tab-compare">
2386
+ <div class="page-header">
2387
+ <h2 class="page-header-title">Compare</h2>
2388
+ <p class="page-header-subtitle">Visualize similarity between text pairs</p>
2389
+ <p class="page-header-hint">Enter two texts and compare their embeddings โ€” see cosine similarity, a heatmap of vector dimensions, and a visual diff.</p>
2390
+ </div>
1075
2391
  <div class="compare-grid">
1076
2392
  <div class="card">
1077
2393
  <div class="card-title">Text A</div>
@@ -1112,13 +2428,20 @@ select:focus { outline: none; border-color: var(--accent); }
1112
2428
  <div class="similarity-bar-inner" id="simBar" style="width:0%"></div>
1113
2429
  </div>
1114
2430
  </div>
1115
- <div id="compareStats" style="text-align:center;"></div>
2431
+ <div class="metrics-grid" id="metricsGrid"></div>
2432
+ <div class="metric-note" id="metricNote"></div>
2433
+ <div id="compareStats" style="text-align:center;margin-top:16px;"></div>
1116
2434
  </div>
1117
2435
  </div>
1118
2436
  </div>
1119
2437
 
1120
2438
  <!-- ========== SEARCH TAB ========== -->
1121
2439
  <div class="tab-panel" id="tab-search">
2440
+ <div class="page-header">
2441
+ <h2 class="page-header-title">Rerank</h2>
2442
+ <p class="page-header-subtitle">Re-order documents by relevance to a query</p>
2443
+ <p class="page-header-hint">Enter a search query and a set of documents โ€” the reranker scores and sorts them by semantic relevance.</p>
2444
+ </div>
1122
2445
  <div class="card">
1123
2446
  <div class="card-title">Query</div>
1124
2447
  <input type="text" id="searchQuery" placeholder="Enter your search query..." value="How do I build AI-powered search?">
@@ -1163,8 +2486,128 @@ Semantic search understands meaning beyond keyword matching</textarea>
1163
2486
  </div>
1164
2487
  </div>
1165
2488
 
2489
+ <!-- ========== MULTIMODAL TAB ========== -->
2490
+ <div class="tab-panel" id="tab-multimodal">
2491
+ <div class="page-header">
2492
+ <h2 class="page-header-title">Multimodal</h2>
2493
+ <p class="page-header-subtitle">Compare images and text in the same vector space</p>
2494
+ <p class="page-header-hint">Voyage AI's multimodal models embed images and text into a unified vector space โ€” so you can compare them directly with cosine similarity.</p>
2495
+ </div>
2496
+
2497
+ <!-- Section A: Image โ†” Text Similarity -->
2498
+ <div class="mm-grid">
2499
+ <div class="card">
2500
+ <div class="card-title">Image</div>
2501
+ <div class="mm-drop-zone" id="mmDropZone">
2502
+ <div class="mm-drop-icon">๐Ÿ–ผ๏ธ</div>
2503
+ <div class="mm-drop-text">Drop an image here or click to browse</div>
2504
+ <div class="mm-drop-hint">PNG, JPEG, WebP, GIF โ€” max 20 MB ยท Paste from clipboard (โŒ˜V)</div>
2505
+ </div>
2506
+ <input type="file" id="mmFileInput" accept="image/png,image/jpeg,image/webp,image/gif" style="display:none">
2507
+ <div class="mm-preview" id="mmPreview">
2508
+ <img id="mmPreviewImg" src="" alt="Preview">
2509
+ <div class="mm-file-info" id="mmFileInfo"></div>
2510
+ <button class="mm-clear-btn" onclick="clearMultimodalImage()">โœ• Clear</button>
2511
+ </div>
2512
+ </div>
2513
+ <div class="card">
2514
+ <div class="card-title">Text</div>
2515
+ <textarea id="mmText" rows="8" placeholder="Describe what you see, or enter any text to compare against the image..."></textarea>
2516
+ </div>
2517
+ </div>
2518
+
2519
+ <div class="options-row">
2520
+ <div class="option-group">
2521
+ <span class="option-label">Model</span>
2522
+ <select id="mmModel">
2523
+ <option value="voyage-multimodal-3.5">voyage-multimodal-3.5</option>
2524
+ <option value="voyage-multimodal-3">voyage-multimodal-3</option>
2525
+ </select>
2526
+ </div>
2527
+ <div class="option-group">
2528
+ <span class="option-label">Dimensions</span>
2529
+ <select id="mmDimensions">
2530
+ <option value="">Default</option>
2531
+ <option value="256">256</option>
2532
+ <option value="512">512</option>
2533
+ <option value="1024">1024</option>
2534
+ <option value="2048">2048</option>
2535
+ </select>
2536
+ </div>
2537
+ <button class="btn" id="mmCompareBtn" onclick="doMultimodalCompare()">๐Ÿ”ฎ Compare</button>
2538
+ </div>
2539
+
2540
+ <div class="error-msg" id="mmError"></div>
2541
+
2542
+ <div class="result-section" id="mmResult">
2543
+ <div class="card">
2544
+ <div class="similarity-display">
2545
+ <div class="similarity-score" id="mmSimScore">โ€”</div>
2546
+ <div class="similarity-label">Cosine Similarity</div>
2547
+ <div class="similarity-bar-outer">
2548
+ <div class="similarity-bar-inner" id="mmSimBar" style="width:0%"></div>
2549
+ </div>
2550
+ </div>
2551
+ <div id="mmStats" style="text-align:center;margin-top:16px;"></div>
2552
+ <div class="metric-note" id="mmNote" style="margin-top:16px;"></div>
2553
+ </div>
2554
+ </div>
2555
+
2556
+ <!-- Section B: Cross-Modal Gallery -->
2557
+ <div class="mm-gallery-section">
2558
+ <h3 style="color:var(--accent-text);margin-bottom:4px;">Cross-Modal Gallery</h3>
2559
+ <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">Build a mini corpus of images and texts, then search across both modalities at once.</p>
2560
+
2561
+ <div class="card">
2562
+ <div class="card-title">Image Corpus (up to 6)</div>
2563
+ <div class="mm-gallery-grid" id="mmGalleryGrid"></div>
2564
+ <input type="file" id="mmGalleryFileInput" accept="image/png,image/jpeg,image/webp,image/gif" style="display:none">
2565
+ </div>
2566
+
2567
+ <div class="card" style="margin-top:12px;">
2568
+ <div class="card-title">Text Corpus (one per line)</div>
2569
+ <textarea id="mmCorpusText" rows="5" placeholder="A sunset over the ocean&#10;A cat sitting on a windowsill&#10;Abstract geometric patterns&#10;A city skyline at night"></textarea>
2570
+ </div>
2571
+
2572
+ <div class="card" style="margin-top:12px;">
2573
+ <div class="card-title">Search Query</div>
2574
+ <div class="mm-search-mode" id="mmSearchMode">
2575
+ <button class="active" data-mode="text" onclick="setMmSearchMode('text')">๐Ÿ“ Text Query</button>
2576
+ <button data-mode="image" onclick="setMmSearchMode('image')">๐Ÿ–ผ๏ธ Image Query</button>
2577
+ </div>
2578
+ <div id="mmSearchTextWrap">
2579
+ <div class="mm-search-row">
2580
+ <input type="text" id="mmSearchQuery" placeholder="Enter a search query...">
2581
+ <button class="btn" id="mmSearchBtn" onclick="doMultimodalSearch()">๐Ÿ”ฎ Search Corpus</button>
2582
+ </div>
2583
+ </div>
2584
+ <div id="mmSearchImageWrap" style="display:none;">
2585
+ <p style="font-size:13px;color:var(--text-dim);margin-bottom:8px;">Click an image in the corpus above to use it as the search query, then:</p>
2586
+ <div style="display:flex;gap:8px;align-items:center;">
2587
+ <span id="mmSearchImageLabel" style="font-size:13px;color:var(--text-muted);">No image selected</span>
2588
+ <button class="btn" id="mmSearchImgBtn" onclick="doMultimodalSearch()">๐Ÿ”ฎ Search Corpus</button>
2589
+ </div>
2590
+ </div>
2591
+ </div>
2592
+
2593
+ <div class="error-msg" id="mmSearchError"></div>
2594
+
2595
+ <div class="result-section" id="mmSearchResult">
2596
+ <div class="card">
2597
+ <div class="card-title">Search Results</div>
2598
+ <div class="mm-result-list" id="mmSearchResultList"></div>
2599
+ </div>
2600
+ </div>
2601
+ </div>
2602
+ </div>
2603
+
1166
2604
  <!-- ========== BENCHMARK TAB ========== -->
1167
2605
  <div class="tab-panel" id="tab-benchmark">
2606
+ <div class="page-header">
2607
+ <h2 class="page-header-title">Benchmark</h2>
2608
+ <p class="page-header-subtitle">Compare model speed, cost, and quality</p>
2609
+ <p class="page-header-hint">Run latency tests, compare ranking accuracy, analyze quantization trade-offs, and estimate costs across models.</p>
2610
+ </div>
1168
2611
 
1169
2612
  <!-- Sub-panel switcher -->
1170
2613
  <div class="bench-panels">
@@ -1351,13 +2794,13 @@ Reranking models rescore initial search results to improve relevance ordering.</
1351
2794
  <!-- โ”€โ”€ Cost Panel โ”€โ”€ -->
1352
2795
  <div class="bench-view" id="bench-cost">
1353
2796
  <div class="card">
1354
- <div class="card-title">๐Ÿ’ฐ RAG Cost Calculator</div>
2797
+ <div class="card-title">๐Ÿ’ฐ RAG Cost Calculator <button class="cost-help-btn" id="costHelpBtn" title="How the math works">?</button></div>
1355
2798
 
1356
2799
  <!-- Mode toggle -->
1357
2800
  <div style="margin-bottom: 20px;">
1358
2801
  <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>
2802
+ <button class="cost-mode-btn active" data-mode="simple" id="costModeSimple">Simple</button>
2803
+ <button class="cost-mode-btn" data-mode="rag" id="costModeRag">RAG Planner</button>
1361
2804
  </div>
1362
2805
  </div>
1363
2806
 
@@ -1521,7 +2964,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
1521
2964
  <div class="about-section-title">What You Can Do Here</div>
1522
2965
  <div class="about-text">
1523
2966
  <strong>โšก Embed</strong> โ€” Generate vector embeddings for any text<br>
1524
- <strong>โš–๏ธ Compare</strong> โ€” Measure cosine similarity between texts<br>
2967
+ <strong>โš–๏ธ Compare</strong> โ€” Measure similarity with cosine, dot product &amp; euclidean distance<br>
1525
2968
  <strong>๐Ÿ” Search</strong> โ€” Semantic search with optional reranking<br>
1526
2969
  <strong>โฑ Benchmark</strong> โ€” Compare model latency, ranking quality, and costs<br>
1527
2970
  <strong>๐Ÿ“š Explore</strong> โ€” Learn about embeddings, vector search, RAG, and more
@@ -1548,18 +2991,321 @@ Reranking models rescore initial search results to improve relevance ordering.</
1548
2991
 
1549
2992
  <!-- ========== EXPLORE TAB ========== -->
1550
2993
  <div class="tab-panel" id="tab-explore">
2994
+ <div class="page-header">
2995
+ <h2 class="page-header-title">Explore</h2>
2996
+ <p class="page-header-subtitle">Learn embedding and vector search concepts</p>
2997
+ <p class="page-header-hint">Browse interactive explanations of key topics โ€” from cosine similarity to quantization to RAG pipelines.</p>
2998
+ </div>
1551
2999
  <div style="margin-bottom:16px;">
1552
3000
  <input type="text" id="exploreSearch" placeholder="๐Ÿ” Search concepts..." oninput="filterExplore()" style="max-width:400px;">
1553
3001
  </div>
1554
- <div class="explore-grid" id="exploreGrid"></div>
3002
+ <div class="explore-grid" id="exploreGrid"></div>
3003
+ </div>
3004
+
3005
+ <!-- Explore Concept Modal -->
3006
+ <div class="explore-modal-overlay" id="exploreModal">
3007
+ <div class="explore-modal">
3008
+ <button class="explore-modal-close" id="exploreModalClose">&times;</button>
3009
+ <div class="explore-modal-header">
3010
+ <div class="explore-modal-icon" id="exploreModalIcon"></div>
3011
+ <div>
3012
+ <div class="explore-modal-title" id="exploreModalTitle"></div>
3013
+ <div class="explore-modal-summary" id="exploreModalSummary"></div>
3014
+ </div>
3015
+ </div>
3016
+ <div class="explore-modal-body" id="exploreModalBody"></div>
3017
+ <div class="explore-modal-actions" id="exploreModalActions"></div>
3018
+ </div>
3019
+ </div>
3020
+
3021
+ <!-- Hidden global model select (used by JS for model sync) -->
3022
+ <select id="globalModel" style="display:none;"></select>
3023
+
3024
+ <!-- ========== SETTINGS TAB ========== -->
3025
+ <div class="tab-panel" id="tab-settings">
3026
+ <div class="settings-container">
3027
+ <div class="page-header">
3028
+ <h2 class="page-header-title">Settings</h2>
3029
+ <p class="page-header-subtitle">Configure appearance, models, and API access</p>
3030
+ </div>
3031
+
3032
+ <div class="settings-section">
3033
+ <div class="settings-section-title">Appearance</div>
3034
+ <div class="settings-row">
3035
+ <div class="settings-label">
3036
+ <span class="settings-label-text">Theme</span>
3037
+ <span class="settings-label-hint">Controls the color scheme of the interface</span>
3038
+ </div>
3039
+ <div class="settings-control">
3040
+ <select class="settings-select" id="settingsTheme">
3041
+ <option value="dark">Dark</option>
3042
+ <option value="light">Light</option>
3043
+ </select>
3044
+ </div>
3045
+ </div>
3046
+ <div class="settings-row">
3047
+ <div class="settings-label">
3048
+ <span class="settings-label-text">Vector Heatmap Colors</span>
3049
+ <span class="settings-label-hint">Color palette for embedding visualizations</span>
3050
+ </div>
3051
+ <div class="settings-control" style="display:flex;align-items:center;gap:8px;">
3052
+ <select class="settings-select" id="settingsHeatmap">
3053
+ <option value="viridis">Viridis</option>
3054
+ <option value="plasma">Plasma</option>
3055
+ <option value="inferno">Inferno</option>
3056
+ <option value="magma">Magma</option>
3057
+ <option value="cividis">Cividis</option>
3058
+ <option value="turbo">Turbo</option>
3059
+ </select>
3060
+ <span class="heatmap-preview" id="heatmapPreview"></span>
3061
+ </div>
3062
+ </div>
3063
+ </div>
3064
+
3065
+ <div class="settings-section">
3066
+ <div class="settings-section-title">Models</div>
3067
+ <div class="settings-row">
3068
+ <div class="settings-label">
3069
+ <span class="settings-label-text">Default Embedding Model</span>
3070
+ <span class="settings-label-hint">Pre-selected model for Embed and Compare tabs</span>
3071
+ </div>
3072
+ <div class="settings-control">
3073
+ <select class="settings-select" id="settingsDefaultModel"></select>
3074
+ </div>
3075
+ </div>
3076
+ <div class="settings-row">
3077
+ <div class="settings-label">
3078
+ <span class="settings-label-text">Default Input Type</span>
3079
+ <span class="settings-label-hint">Pre-selected input type for embedding requests</span>
3080
+ </div>
3081
+ <div class="settings-control">
3082
+ <select class="settings-select" id="settingsInputType">
3083
+ <option value="document">document</option>
3084
+ <option value="query">query</option>
3085
+ </select>
3086
+ </div>
3087
+ </div>
3088
+ </div>
3089
+
3090
+ <div class="settings-section">
3091
+ <div class="settings-section-title">API</div>
3092
+ <div class="settings-api-banner" id="settingsApiBanner">
3093
+ <span class="settings-api-banner-icon" id="settingsApiBannerIcon">โš ๏ธ</span>
3094
+ <span id="settingsApiKeyMsg">No API key configured โ€” add your key below to get started.</span>
3095
+ </div>
3096
+ <div class="settings-row">
3097
+ <div class="settings-label">
3098
+ <span class="settings-label-text">API Key</span>
3099
+ <span class="settings-label-hint">Encrypted via OS keychain ยท <a href="https://dash.voyageai.com" target="_blank" class="settings-key-link">๐Ÿ”‘ Get a key</a></span>
3100
+ </div>
3101
+ <div class="settings-control" style="min-width:260px;">
3102
+ <div class="settings-api-field">
3103
+ <input type="password" id="settingsApiKey" placeholder="pa-..." autocomplete="off" spellcheck="false">
3104
+ <button type="button" id="settingsApiKeyToggle" title="Show/hide key">๐Ÿ‘</button>
3105
+ <button type="button" id="settingsApiKeySave" class="save-btn" title="Save key">Save</button>
3106
+ </div>
3107
+ </div>
3108
+ </div>
3109
+ <div class="settings-row">
3110
+ <div class="settings-label">
3111
+ <span class="settings-label-text">API Base URL</span>
3112
+ <span class="settings-label-hint">Override the default endpoint (leave empty for default)</span>
3113
+ </div>
3114
+ <div class="settings-control">
3115
+ <input type="text" class="settings-input" id="settingsApiBase" placeholder="https://api.voyageai.com/v1">
3116
+ </div>
3117
+ </div>
3118
+ <div class="settings-row">
3119
+ <div class="settings-label">
3120
+ <span class="settings-label-text">Request Timeout</span>
3121
+ <span class="settings-label-hint">Max seconds to wait for API responses</span>
3122
+ </div>
3123
+ <div class="settings-control">
3124
+ <select class="settings-select" id="settingsTimeout">
3125
+ <option value="15">15 seconds</option>
3126
+ <option value="30" selected>30 seconds</option>
3127
+ <option value="60">60 seconds</option>
3128
+ <option value="120">120 seconds</option>
3129
+ </select>
3130
+ </div>
3131
+ </div>
3132
+ </div>
3133
+
3134
+ <div class="settings-section">
3135
+ <div class="settings-section-title">Benchmark Defaults</div>
3136
+ <div class="settings-row">
3137
+ <div class="settings-label">
3138
+ <span class="settings-label-text">Iterations per Model</span>
3139
+ <span class="settings-label-hint">Number of runs when benchmarking latency</span>
3140
+ </div>
3141
+ <div class="settings-control">
3142
+ <select class="settings-select" id="settingsBenchIter">
3143
+ <option value="3">3 runs</option>
3144
+ <option value="5" selected>5 runs</option>
3145
+ <option value="10">10 runs</option>
3146
+ <option value="20">20 runs</option>
3147
+ </select>
3148
+ </div>
3149
+ </div>
3150
+ <div class="settings-row">
3151
+ <div class="settings-label">
3152
+ <span class="settings-label-text">Show Detailed Timings</span>
3153
+ <span class="settings-label-hint">Display p50/p95/p99 in benchmark results</span>
3154
+ </div>
3155
+ <div class="settings-control">
3156
+ <button class="settings-toggle" id="settingsDetailedTimings" type="button"></button>
3157
+ </div>
3158
+ </div>
3159
+ </div>
3160
+
3161
+ <div class="settings-section">
3162
+ <div class="settings-section-title">Help</div>
3163
+ <div class="settings-row">
3164
+ <div class="settings-label">
3165
+ <span class="settings-label-text">Welcome Tour</span>
3166
+ <span class="settings-label-hint">Replay the onboarding walkthrough</span>
3167
+ </div>
3168
+ <div class="settings-control">
3169
+ <button class="settings-reset-btn" id="settingsShowTour" style="background:var(--accent);color:var(--bg);">Show Welcome Tour</button>
3170
+ </div>
3171
+ </div>
3172
+ </div>
3173
+
3174
+ <div class="settings-section">
3175
+ <div class="settings-section-title">Data &amp; Privacy</div>
3176
+ <div class="settings-row">
3177
+ <div class="settings-label">
3178
+ <span class="settings-label-text">Persist Embeddings Locally</span>
3179
+ <span class="settings-label-hint">Cache embedding results in browser storage to avoid re-fetching</span>
3180
+ </div>
3181
+ <div class="settings-control">
3182
+ <button class="settings-toggle active" id="settingsCacheEmbeddings" type="button"></button>
3183
+ </div>
3184
+ </div>
3185
+ <div class="settings-row">
3186
+ <div class="settings-label">
3187
+ <span class="settings-label-text">Clear Cached Data</span>
3188
+ <span class="settings-label-hint">Remove all locally stored embeddings and preferences</span>
3189
+ </div>
3190
+ <div class="settings-control">
3191
+ <button class="settings-reset-btn" id="settingsClearCache">Clear All Data</button>
3192
+ </div>
3193
+ </div>
3194
+ </div>
3195
+
3196
+ <!-- Version Info (visible in Electron only) -->
3197
+ <div class="settings-section" id="settingsVersionSection" style="display:none;">
3198
+ <div class="settings-section-title">Version</div>
3199
+ <div class="settings-row">
3200
+ <div class="settings-label">
3201
+ <span class="settings-label-text">Vai Desktop</span>
3202
+ <span class="settings-label-hint">Electron desktop application</span>
3203
+ </div>
3204
+ <div class="settings-control">
3205
+ <span class="version-badge" id="settingsAppVersion">โ€”</span>
3206
+ </div>
3207
+ </div>
3208
+ <div class="settings-row">
3209
+ <div class="settings-label">
3210
+ <span class="settings-label-text">Vai CLI Engine</span>
3211
+ <span class="settings-label-hint">Underlying CLI powering embeddings, search &amp; reranking</span>
3212
+ </div>
3213
+ <div class="settings-control">
3214
+ <span class="version-badge" id="settingsCliVersion">โ€”</span>
3215
+ </div>
3216
+ </div>
3217
+ </div>
3218
+
3219
+ <div style="text-align:center;padding:8px 0;">
3220
+ <span class="settings-saved" id="settingsSavedMsg">โœ“ Saved</span>
3221
+ </div>
3222
+
3223
+ </div>
3224
+ </div>
3225
+
3226
+ </div><!-- .main -->
3227
+ </div><!-- .content-area -->
3228
+ </div><!-- .app-shell -->
3229
+
3230
+ <!-- Onboarding Walkthrough Overlay -->
3231
+ <div class="onboarding-overlay" id="onboardingOverlay">
3232
+ <div class="onboarding-backdrop" id="onboardingBackdrop"></div>
3233
+ <div class="onboarding-spotlight" id="onboardingSpotlight"></div>
3234
+ <!-- Welcome card (step 0) -->
3235
+ <div class="onboarding-welcome-center" id="onboardingWelcomeWrap">
3236
+ <div class="onboarding-welcome-card" id="onboardingWelcomeCard">
3237
+ <img class="onboarding-welcome-logo" id="onboardingLogo" src="/icons/dark/64.png" alt="Vai">
3238
+ <div class="onboarding-welcome-title">Welcome to Vai</div>
3239
+ <div class="onboarding-welcome-sub">Explore embeddings, compare text similarity, and search with vector models โ€” all from one playground.</div>
3240
+ <div class="onboarding-footer" style="justify-content:center;">
3241
+ <button class="onboarding-skip" id="onboardingWelcomeSkip">Skip tour</button>
3242
+ <button class="onboarding-next" id="onboardingWelcomeNext">Take the tour โ†’</button>
3243
+ </div>
3244
+ </div>
3245
+ </div>
3246
+ <!-- Tooltip for steps 1-4 -->
3247
+ <div class="onboarding-tooltip" id="onboardingTooltip">
3248
+ <div class="onboarding-tooltip-arrow top" id="onboardingArrow"></div>
3249
+ <div class="onboarding-step-icon" id="onboardingIcon"></div>
3250
+ <div class="onboarding-step-title" id="onboardingTitle"></div>
3251
+ <div class="onboarding-step-body" id="onboardingBody"></div>
3252
+ <div class="onboarding-footer">
3253
+ <div class="onboarding-dots" id="onboardingDots"></div>
3254
+ <div class="onboarding-actions">
3255
+ <button class="onboarding-skip" id="onboardingSkip">Skip</button>
3256
+ <button class="onboarding-next" id="onboardingNext">Next</button>
3257
+ </div>
3258
+ </div>
3259
+ </div>
1555
3260
  </div>
1556
3261
 
1557
- </div><!-- .main -->
1558
-
3262
+ <script>
3263
+ // Apply saved theme immediately to prevent flash
3264
+ (function() {
3265
+ var t = localStorage.getItem('vai-theme') || 'dark';
3266
+ if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
3267
+ // Set correct logo immediately
3268
+ var logo = document.getElementById('sidebarLogo');
3269
+ if (logo) logo.src = '/icons/' + (t === 'light' ? 'light' : 'dark') + '/64.png';
3270
+ })();
3271
+ </script>
1559
3272
  <script>
1560
3273
  (function() {
1561
3274
  'use strict';
1562
3275
 
3276
+ // โ”€โ”€ Theme Toggle โ”€โ”€
3277
+ function initThemeToggle() {
3278
+ const toggle = document.getElementById('themeToggle');
3279
+ const saved = localStorage.getItem('vai-theme') || 'dark';
3280
+ let current = saved;
3281
+
3282
+ function applyTheme(theme) {
3283
+ current = theme;
3284
+ const logo = document.getElementById('sidebarLogo');
3285
+ if (theme === 'light') {
3286
+ document.documentElement.setAttribute('data-theme', 'light');
3287
+ toggle.textContent = 'โ˜€๏ธ';
3288
+ toggle.title = 'Switch to dark mode';
3289
+ if (logo) logo.src = '/icons/light/64.png';
3290
+ } else {
3291
+ document.documentElement.removeAttribute('data-theme');
3292
+ toggle.textContent = '๐ŸŒ™';
3293
+ toggle.title = 'Switch to light mode';
3294
+ if (logo) logo.src = '/icons/dark/64.png';
3295
+ }
3296
+ localStorage.setItem('vai-theme', theme);
3297
+ }
3298
+
3299
+ applyTheme(current);
3300
+ toggle.addEventListener('click', () => {
3301
+ const next = current === 'dark' ? 'light' : 'dark';
3302
+ applyTheme(next);
3303
+ // Sync settings panel dropdown
3304
+ const themeSel = document.getElementById('settingsTheme');
3305
+ if (themeSel) themeSel.value = next;
3306
+ });
3307
+ }
3308
+
1563
3309
  // โ”€โ”€ State โ”€โ”€
1564
3310
  let allModels = [];
1565
3311
  let embedModels = [];
@@ -1595,6 +3341,8 @@ function switchTab(tab) {
1595
3341
  p.classList.toggle('active', p.id === 'tab-' + tab);
1596
3342
  });
1597
3343
  }
3344
+ // Expose globally so Electron main process can call it
3345
+ window.switchTab = switchTab;
1598
3346
 
1599
3347
  // โ”€โ”€ Config โ”€โ”€
1600
3348
  async function loadConfig() {
@@ -1801,27 +3549,73 @@ window.doCompare = async function() {
1801
3549
  const dimensions = dims ? parseInt(dims, 10) : undefined;
1802
3550
 
1803
3551
  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
3552
 
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)';
3553
+ // Get raw embeddings for all metrics
3554
+ const vecA = data.embeddings[0].embedding;
3555
+ const vecB = data.embeddings[1].embedding;
3556
+
3557
+ const cosine = cosineSim(vecA, vecB);
3558
+ const dot = dotProduct(vecA, vecB);
3559
+ const euclid = euclideanDist(vecA, vecB);
3560
+
3561
+ // Hero display โ€” cosine similarity
3562
+ const cosinePct = Math.max(0, cosine * 100);
3563
+ let cosineColor;
3564
+ if (cosine > 0.7) cosineColor = 'var(--green)';
3565
+ else if (cosine > 0.4) cosineColor = 'var(--yellow)';
3566
+ else cosineColor = 'var(--red)';
1812
3567
 
1813
3568
  const scoreEl = document.getElementById('simScore');
1814
- scoreEl.textContent = sim.toFixed(4);
1815
- scoreEl.style.color = color;
3569
+ scoreEl.textContent = cosine.toFixed(4);
3570
+ scoreEl.style.color = cosineColor;
1816
3571
 
1817
3572
  const barEl = document.getElementById('simBar');
1818
- barEl.style.width = pct + '%';
1819
- barEl.style.background = color;
3573
+ barEl.style.width = cosinePct + '%';
3574
+ barEl.style.background = cosineColor;
3575
+
3576
+ // Metric cards โ€” all three
3577
+ const dotColor = dot > 0.7 ? 'var(--green)' : dot > 0.4 ? 'var(--yellow)' : 'var(--red)';
3578
+ // Euclidean: 0 = identical, ~2 = max for normalized vectors. Invert for color.
3579
+ const euclidColor = euclid < 0.6 ? 'var(--green)' : euclid < 1.0 ? 'var(--yellow)' : 'var(--red)';
3580
+ // For euclidean bar, invert: 0 dist = 100% bar, 2.0 dist = 0%
3581
+ const euclidPct = Math.max(0, Math.min(100, (1 - euclid / 2) * 100));
3582
+
3583
+ const metricsEl = document.getElementById('metricsGrid');
3584
+ metricsEl.innerHTML = `
3585
+ <div class="metric-card primary">
3586
+ <div class="metric-card-value" style="color:${cosineColor}">${cosine.toFixed(4)}</div>
3587
+ <div class="metric-card-name">Cosine Similarity</div>
3588
+ <div class="metric-card-desc">Angle between vectors (โˆ’1 to 1). Standard for semantic search.</div>
3589
+ <div class="metric-bar"><div class="metric-bar-fill" style="width:${cosinePct}%;background:${cosineColor}"></div></div>
3590
+ </div>
3591
+ <div class="metric-card">
3592
+ <div class="metric-card-value" style="color:${dotColor}">${dot.toFixed(4)}</div>
3593
+ <div class="metric-card-name">Dot Product</div>
3594
+ <div class="metric-card-desc">Sum of element-wise products. Equals cosine for normalized vectors.</div>
3595
+ <div class="metric-bar"><div class="metric-bar-fill" style="width:${Math.max(0, dot * 100)}%;background:${dotColor}"></div></div>
3596
+ </div>
3597
+ <div class="metric-card">
3598
+ <div class="metric-card-value" style="color:${euclidColor}">${euclid.toFixed(4)}</div>
3599
+ <div class="metric-card-name">Euclidean Distance</div>
3600
+ <div class="metric-card-desc">Straight-line distance (0 = identical). Lower is more similar.</div>
3601
+ <div class="metric-bar"><div class="metric-bar-fill" style="width:${euclidPct}%;background:${euclidColor}"></div></div>
3602
+ </div>
3603
+ `;
3604
+
3605
+ // Insight note
3606
+ const noteEl = document.getElementById('metricNote');
3607
+ const diff = Math.abs(cosine - dot);
3608
+ if (diff < 0.001) {
3609
+ 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.';
3610
+ } else {
3611
+ noteEl.innerHTML = '๐Ÿ’ก Cosine and dot product differ because these vectors are not perfectly L2-normalized. Atlas Vector Search uses cosine by default.';
3612
+ }
1820
3613
 
1821
3614
  // Stats
1822
3615
  const statsEl = document.getElementById('compareStats');
1823
3616
  statsEl.innerHTML = `
1824
3617
  <span class="stat"><span class="stat-label">Model</span><span class="stat-value">${data.model}</span></span>
3618
+ <span class="stat"><span class="stat-label">Dimensions</span><span class="stat-value">${vecA.length}</span></span>
1825
3619
  <span class="stat"><span class="stat-label">Tokens</span><span class="stat-value">${data.usage?.total_tokens || 'โ€”'}</span></span>
1826
3620
  `;
1827
3621
 
@@ -1898,6 +3692,18 @@ function cosineSim(a, b) {
1898
3692
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
1899
3693
  }
1900
3694
 
3695
+ function dotProduct(a, b) {
3696
+ let sum = 0;
3697
+ for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
3698
+ return sum;
3699
+ }
3700
+
3701
+ function euclideanDist(a, b) {
3702
+ let sum = 0;
3703
+ for (let i = 0; i < a.length; i++) sum += (a[i] - b[i]) ** 2;
3704
+ return Math.sqrt(sum);
3705
+ }
3706
+
1901
3707
  function renderSearchResults(embResults, rerankResults) {
1902
3708
  const grid = document.getElementById('searchResultGrid');
1903
3709
  grid.innerHTML = '';
@@ -1975,6 +3781,15 @@ const CONCEPT_META = {
1975
3781
  'batch-processing': { icon: '๐Ÿ“ฆ', tab: 'embed' },
1976
3782
  benchmarking: { icon: 'โฑ', tab: 'benchmark' },
1977
3783
  quantization: { icon: 'โš—๏ธ', tab: 'benchmark' },
3784
+ 'mixture-of-experts': { icon: '๐Ÿงฉ', tab: 'embed' },
3785
+ 'shared-embedding-space': { icon: '๐Ÿ”—', tab: 'compare' },
3786
+ 'rteb-benchmarks': { icon: '๐Ÿ“Š', tab: 'benchmark' },
3787
+ 'voyage-4-nano': { icon: '๐Ÿ”ฌ', tab: 'embed' },
3788
+ 'rerank-eval': { icon: '๐Ÿ“', tab: 'benchmark' },
3789
+ 'multimodal-embeddings': { icon: '๐Ÿ–ผ๏ธ', tab: 'multimodal' },
3790
+ 'cross-modal-search': { icon: '๐Ÿ”€', tab: 'multimodal' },
3791
+ 'modality-gap': { icon: '๐Ÿ•ณ๏ธ', tab: 'multimodal' },
3792
+ 'multimodal-rag': { icon: '๐Ÿ“„', tab: 'multimodal' },
1978
3793
  };
1979
3794
 
1980
3795
  let exploreConcepts = {};
@@ -2003,40 +3818,64 @@ function buildExploreCards() {
2003
3818
  card.className = 'explore-card';
2004
3819
  card.dataset.key = key;
2005
3820
 
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
3821
  card.innerHTML = `
2023
3822
  <div class="explore-card-icon">${meta.icon}</div>
2024
3823
  <div class="explore-card-title">${escapeHtml(concept.title)}</div>
2025
3824
  <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
3825
  `;
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
- });
3826
+ card.addEventListener('click', () => openExploreModal(key));
2038
3827
  grid.appendChild(card);
2039
3828
  }
3829
+
3830
+ // Modal close handlers
3831
+ const modal = document.getElementById('exploreModal');
3832
+ document.getElementById('exploreModalClose').addEventListener('click', closeExploreModal);
3833
+ modal.addEventListener('click', (e) => { if (e.target === modal) closeExploreModal(); });
3834
+ }
3835
+
3836
+ function openExploreModal(key) {
3837
+ const concept = exploreConcepts[key];
3838
+ if (!concept) return;
3839
+ const meta = CONCEPT_META[key] || { icon: '๐Ÿ“š', tab: 'embed' };
3840
+
3841
+ document.getElementById('exploreModalIcon').textContent = meta.icon;
3842
+ document.getElementById('exploreModalTitle').textContent = concept.title;
3843
+ document.getElementById('exploreModalSummary').textContent = concept.summary;
3844
+
3845
+ // Build body: content + links + tryIt
3846
+ let bodyHtml = escapeHtml(concept.content);
3847
+
3848
+ if (concept.links && concept.links.length > 0) {
3849
+ bodyHtml += '<div class="explore-modal-links">' +
3850
+ '<div class="explore-modal-links-title">Learn More</div>' +
3851
+ concept.links.map(url =>
3852
+ `<a href="${escapeHtml(url)}" target="_blank" rel="noopener">${escapeHtml(url)}</a>`
3853
+ ).join('') + '</div>';
3854
+ }
3855
+
3856
+ if (concept.tryIt && concept.tryIt.length > 0) {
3857
+ bodyHtml += '<div class="explore-modal-tryit">' +
3858
+ '<div class="explore-modal-tryit-title">Try It</div>' +
3859
+ concept.tryIt.map(cmd =>
3860
+ `<div class="explore-modal-tryit-cmd">$ ${escapeHtml(cmd)}</div>`
3861
+ ).join('') + '</div>';
3862
+ }
3863
+
3864
+ document.getElementById('exploreModalBody').innerHTML = bodyHtml;
3865
+
3866
+ // Actions
3867
+ const actionsEl = document.getElementById('exploreModalActions');
3868
+ actionsEl.innerHTML = `<button class="btn btn-small" id="exploreModalTry">Try it in playground โ†’</button>`;
3869
+ actionsEl.querySelector('#exploreModalTry').addEventListener('click', () => {
3870
+ closeExploreModal();
3871
+ if (meta.tab) switchTab(meta.tab);
3872
+ });
3873
+
3874
+ document.getElementById('exploreModal').classList.add('open');
3875
+ }
3876
+
3877
+ function closeExploreModal() {
3878
+ document.getElementById('exploreModal').classList.remove('open');
2040
3879
  }
2041
3880
 
2042
3881
  window.tryTopic = function(key) {
@@ -2044,10 +3883,6 @@ window.tryTopic = function(key) {
2044
3883
  if (meta) switchTab(meta.tab);
2045
3884
  };
2046
3885
 
2047
- window.collapseTopic = function(btn) {
2048
- btn.closest('.explore-card').classList.remove('expanded');
2049
- };
2050
-
2051
3886
  window.filterExplore = function() {
2052
3887
  const q = document.getElementById('exploreSearch').value.toLowerCase().trim();
2053
3888
  document.querySelectorAll('#exploreGrid .explore-card').forEach(card => {
@@ -2115,8 +3950,8 @@ const BENCH_SAMPLE_TEXTS = [
2115
3950
  ];
2116
3951
 
2117
3952
  const MODEL_COLORS = [
2118
- '#00d4aa', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffd93d',
2119
- '#ff6b6b', '#c792ea', '#f78c6c', '#82aaff', '#c3e88d',
3953
+ '#00ED64', '#71F6BA', '#0498EC', '#B45AF2', '#FFC010',
3954
+ '#FF6960', '#B45AF2', '#FFC010', '#016BF8', '#C0FAE6',
2120
3955
  ];
2121
3956
 
2122
3957
  window.doBenchLatency = async function() {
@@ -2457,7 +4292,7 @@ window.doBenchQuantization = async function() {
2457
4292
  const baseline = completed.find(r => r.dtype === 'float') || completed[0];
2458
4293
  const maxBytes = Math.max(...completed.map(r => r.bytesPerVec));
2459
4294
  const maxLatency = Math.max(...completed.map(r => r.latency));
2460
- const DTYPE_COLORS = { float: '#00d4aa', int8: '#4ecdc4', uint8: '#45b7d1', ubinary: '#ffd93d', binary: '#ff6b6b' };
4295
+ const DTYPE_COLORS = { float: '#00ED64', int8: '#71F6BA', uint8: '#0498EC', ubinary: '#FFC010', binary: '#FF6960' };
2461
4296
 
2462
4297
  // โ”€โ”€ Storage Bar Chart โ”€โ”€
2463
4298
  let storageHTML = '';
@@ -2468,7 +4303,7 @@ window.doBenchQuantization = async function() {
2468
4303
  const savings = r.bytesPerVec < baseline.bytesPerVec
2469
4304
  ? `${(baseline.bytesPerVec / r.bytesPerVec).toFixed(0)}ร— smaller`
2470
4305
  : 'baseline';
2471
- const color = DTYPE_COLORS[r.dtype] || '#82aaff';
4306
+ const color = DTYPE_COLORS[r.dtype] || '#0498EC';
2472
4307
  storageHTML += `<div class="quant-bar-group">
2473
4308
  <div class="quant-bar-label">
2474
4309
  <span class="dtype-name">${r.dtype}</span>
@@ -2486,7 +4321,7 @@ window.doBenchQuantization = async function() {
2486
4321
  const minLatency = Math.min(...completed.map(r => r.latency));
2487
4322
  for (const r of completed) {
2488
4323
  const pct = Math.max(8, (r.latency / maxLatency) * 100);
2489
- const color = DTYPE_COLORS[r.dtype] || '#82aaff';
4324
+ const color = DTYPE_COLORS[r.dtype] || '#0498EC';
2490
4325
  const badge = r.latency === minLatency ? ' โšก' : '';
2491
4326
  latencyHTML += `<div class="quant-bar-group">
2492
4327
  <div class="quant-bar-label">
@@ -2621,6 +4456,22 @@ function setCostMode(mode) {
2621
4456
  // โ”€โ”€ Simple Mode (query-only comparison) โ”€โ”€
2622
4457
 
2623
4458
  function initCostCalculator() {
4459
+ // Mode toggle buttons
4460
+ document.getElementById('costModeSimple').addEventListener('click', () => setCostMode('simple'));
4461
+ document.getElementById('costModeRag').addEventListener('click', () => setCostMode('rag'));
4462
+
4463
+ // Help modal
4464
+ const helpModal = document.getElementById('costHelpModal');
4465
+ document.getElementById('costHelpBtn').addEventListener('click', () => helpModal.classList.add('open'));
4466
+ document.getElementById('costHelpClose').addEventListener('click', () => helpModal.classList.remove('open'));
4467
+ helpModal.addEventListener('click', (e) => { if (e.target === helpModal) helpModal.classList.remove('open'); });
4468
+ document.addEventListener('keydown', (e) => {
4469
+ if (e.key === 'Escape') {
4470
+ helpModal.classList.remove('open');
4471
+ closeExploreModal();
4472
+ }
4473
+ });
4474
+
2624
4475
  // Simple mode sliders
2625
4476
  const tokSlider = document.getElementById('costTokens');
2626
4477
  const qSlider = document.getElementById('costQueries');
@@ -2829,7 +4680,7 @@ function updateRagCalculator() {
2829
4680
  </div>
2830
4681
  <div class="cost-summary-card">
2831
4682
  <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>
4683
+ <div class="cost-summary-value" style="color:var(--success)">${maxCost > 0 ? ((1 - strategies[0].totalCost / maxCost) * 100).toFixed(0) + '%' : '0%'}</div>
2833
4684
  <div class="cost-summary-detail">vs ${strategies[strategies.length - 1].name.split(':')[1]?.trim() || 'most expensive'}</div>
2834
4685
  </div>
2835
4686
  `;
@@ -2981,20 +4832,988 @@ window.clearHistory = function() {
2981
4832
  renderHistory();
2982
4833
  };
2983
4834
 
4835
+ // โ”€โ”€ Settings โ”€โ”€
4836
+ const SETTINGS_KEY = 'vai-settings';
4837
+
4838
+ function getSettings() {
4839
+ try {
4840
+ return JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}');
4841
+ } catch { return {}; }
4842
+ }
4843
+
4844
+ function saveSetting(key, value) {
4845
+ const s = getSettings();
4846
+ s[key] = value;
4847
+ localStorage.setItem(SETTINGS_KEY, JSON.stringify(s));
4848
+ flashSaved();
4849
+ }
4850
+
4851
+ function flashSaved() {
4852
+ const el = document.getElementById('settingsSavedMsg');
4853
+ if (!el) return;
4854
+ el.classList.add('show');
4855
+ clearTimeout(el._timer);
4856
+ el._timer = setTimeout(() => el.classList.remove('show'), 1500);
4857
+ }
4858
+
4859
+ function initSettings() {
4860
+ const s = getSettings();
4861
+
4862
+ // Theme sync
4863
+ const themeSelect = document.getElementById('settingsTheme');
4864
+ const currentTheme = localStorage.getItem('vai-theme') || 'dark';
4865
+ if (themeSelect) {
4866
+ themeSelect.value = currentTheme;
4867
+ themeSelect.addEventListener('change', () => {
4868
+ const t = themeSelect.value;
4869
+ saveSetting('theme', t);
4870
+ localStorage.setItem('vai-theme', t);
4871
+ // Reuse existing applyTheme by clicking the toggle if needed
4872
+ const toggle = document.getElementById('themeToggle');
4873
+ const cur = localStorage.getItem('vai-theme');
4874
+ if (cur === 'light') {
4875
+ document.documentElement.setAttribute('data-theme', 'light');
4876
+ if (toggle) { toggle.textContent = 'โ˜€๏ธ'; toggle.title = 'Switch to dark mode'; }
4877
+ } else {
4878
+ document.documentElement.removeAttribute('data-theme');
4879
+ if (toggle) { toggle.textContent = '๐ŸŒ™'; toggle.title = 'Switch to light mode'; }
4880
+ }
4881
+ const logo = document.getElementById('sidebarLogo');
4882
+ if (logo) logo.src = '/icons/' + (t === 'light' ? 'light' : 'dark') + '/64.png';
4883
+ });
4884
+ }
4885
+
4886
+ // Heatmap palette โ€” handled below with preview swatch
4887
+
4888
+ // Default model โ€” populate from global model select
4889
+ const settingsModel = document.getElementById('settingsDefaultModel');
4890
+ const globalModel = document.getElementById('globalModel');
4891
+ if (settingsModel && globalModel) {
4892
+ // Clone options from globalModel
4893
+ settingsModel.innerHTML = globalModel.innerHTML;
4894
+ const savedModel = s.defaultModel || globalModel.value;
4895
+ settingsModel.value = savedModel;
4896
+ settingsModel.addEventListener('change', () => {
4897
+ saveSetting('defaultModel', settingsModel.value);
4898
+ globalModel.value = settingsModel.value;
4899
+ // Update individual tab selects too
4900
+ ['embedModel', 'compareModel', 'searchEmbedModel'].forEach(id => {
4901
+ const sel = document.getElementById(id);
4902
+ if (sel) sel.value = settingsModel.value;
4903
+ });
4904
+ });
4905
+ }
4906
+
4907
+ // Default input type
4908
+ const inputTypeSel = document.getElementById('settingsInputType');
4909
+ if (inputTypeSel) {
4910
+ inputTypeSel.value = s.inputType || 'document';
4911
+ inputTypeSel.addEventListener('change', () => {
4912
+ saveSetting('inputType', inputTypeSel.value);
4913
+ const embedType = document.getElementById('embedInputType');
4914
+ if (embedType) embedType.value = inputTypeSel.value;
4915
+ });
4916
+ }
4917
+
4918
+ // API base URL
4919
+ const apiBaseInput = document.getElementById('settingsApiBase');
4920
+ if (apiBaseInput) {
4921
+ apiBaseInput.value = s.apiBase || '';
4922
+ apiBaseInput.addEventListener('change', () => saveSetting('apiBase', apiBaseInput.value));
4923
+ }
4924
+
4925
+ // Timeout
4926
+ const timeoutSel = document.getElementById('settingsTimeout');
4927
+ if (timeoutSel) {
4928
+ timeoutSel.value = s.timeout || '30';
4929
+ timeoutSel.addEventListener('change', () => saveSetting('timeout', timeoutSel.value));
4930
+ }
4931
+
4932
+ // Benchmark iterations
4933
+ const benchIterSel = document.getElementById('settingsBenchIter');
4934
+ if (benchIterSel) {
4935
+ benchIterSel.value = s.benchIter || '5';
4936
+ benchIterSel.addEventListener('change', () => saveSetting('benchIter', benchIterSel.value));
4937
+ }
4938
+
4939
+ // Toggle buttons
4940
+ function initToggle(id, settingKey, defaultVal) {
4941
+ const btn = document.getElementById(id);
4942
+ if (!btn) return;
4943
+ const val = s[settingKey] !== undefined ? s[settingKey] : defaultVal;
4944
+ btn.classList.toggle('active', val);
4945
+ btn.addEventListener('click', () => {
4946
+ const isActive = btn.classList.toggle('active');
4947
+ saveSetting(settingKey, isActive);
4948
+ });
4949
+ }
4950
+ initToggle('settingsDetailedTimings', 'detailedTimings', false);
4951
+ initToggle('settingsCacheEmbeddings', 'cacheEmbeddings', true);
4952
+
4953
+ // API Key (Electron only โ€” uses safeStorage via preload bridge)
4954
+ const apiKeyInput = document.getElementById('settingsApiKey');
4955
+ const apiKeyToggle = document.getElementById('settingsApiKeyToggle');
4956
+ const apiKeySave = document.getElementById('settingsApiKeySave');
4957
+ const apiKeyMsg = document.getElementById('settingsApiKeyMsg');
4958
+ const apiBanner = document.getElementById('settingsApiBanner');
4959
+ const apiBannerIcon = document.getElementById('settingsApiBannerIcon');
4960
+
4961
+ function updateApiBanner(hasKey, key) {
4962
+ if (hasKey) {
4963
+ apiBanner.className = 'settings-api-banner success';
4964
+ apiBannerIcon.textContent = '๐Ÿ”';
4965
+ apiKeyMsg.textContent = 'API key stored securely in OS keychain';
4966
+ apiKeyInput.placeholder = window.vai ? window.vai.apiKey.mask(key) : 'โ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข';
4967
+ } else {
4968
+ apiBanner.className = 'settings-api-banner';
4969
+ apiBannerIcon.textContent = 'โš ๏ธ';
4970
+ apiKeyMsg.textContent = 'No API key configured โ€” add your key below to get started.';
4971
+ apiKeyInput.placeholder = 'pa-...';
4972
+ }
4973
+ }
4974
+
4975
+ if (window.vai && window.vai.isElectron) {
4976
+ // Load existing key status
4977
+ window.vai.apiKey.get().then(key => updateApiBanner(!!key, key));
4978
+
4979
+ // Show/hide toggle
4980
+ let keyVisible = false;
4981
+ apiKeyToggle.addEventListener('click', async () => {
4982
+ if (!keyVisible && !apiKeyInput.value) {
4983
+ const key = await window.vai.apiKey.get();
4984
+ if (key) {
4985
+ apiKeyInput.value = key;
4986
+ apiKeyInput.type = 'text';
4987
+ apiKeyToggle.textContent = '๐Ÿ™ˆ';
4988
+ keyVisible = true;
4989
+ }
4990
+ } else if (keyVisible) {
4991
+ apiKeyInput.type = 'password';
4992
+ apiKeyToggle.textContent = '๐Ÿ‘';
4993
+ keyVisible = false;
4994
+ if (!apiKeyInput.value) {
4995
+ const key = await window.vai.apiKey.get();
4996
+ if (key) apiKeyInput.placeholder = window.vai.apiKey.mask(key);
4997
+ }
4998
+ } else {
4999
+ apiKeyInput.type = apiKeyInput.type === 'password' ? 'text' : 'password';
5000
+ apiKeyToggle.textContent = apiKeyInput.type === 'password' ? '๐Ÿ‘' : '๐Ÿ™ˆ';
5001
+ keyVisible = apiKeyInput.type === 'text';
5002
+ }
5003
+ });
5004
+
5005
+ // Save button
5006
+ apiKeySave.addEventListener('click', async () => {
5007
+ const key = apiKeyInput.value.trim();
5008
+ if (!key) {
5009
+ await window.vai.apiKey.delete();
5010
+ updateApiBanner(false);
5011
+ flashSaved();
5012
+ return;
5013
+ }
5014
+ try {
5015
+ await window.vai.apiKey.set(key);
5016
+ apiKeyInput.value = '';
5017
+ apiKeyInput.type = 'password';
5018
+ apiKeyToggle.textContent = '๐Ÿ‘';
5019
+ keyVisible = false;
5020
+ updateApiBanner(true, key);
5021
+ flashSaved();
5022
+ loadConfig();
5023
+ } catch (err) {
5024
+ apiBanner.className = 'settings-api-banner';
5025
+ apiBannerIcon.textContent = 'โŒ';
5026
+ apiKeyMsg.textContent = 'Failed to save: ' + err.message;
5027
+ }
5028
+ });
5029
+
5030
+ apiKeyInput.addEventListener('keydown', (e) => {
5031
+ if (e.key === 'Enter') apiKeySave.click();
5032
+ });
5033
+ } else {
5034
+ apiBanner.className = 'settings-api-banner';
5035
+ apiBannerIcon.textContent = '๐Ÿ’ก';
5036
+ apiKeyMsg.textContent = 'Set VOYAGE_API_KEY environment variable, or use "vai config set key YOUR_KEY"';
5037
+ apiKeySave.style.display = 'none';
5038
+ apiKeyToggle.style.display = 'none';
5039
+ apiKeyInput.disabled = true;
5040
+ apiKeyInput.placeholder = 'Secure storage available in desktop app';
5041
+ }
5042
+
5043
+ // Heatmap palette preview
5044
+ const HEATMAP_GRADIENTS = {
5045
+ viridis: 'linear-gradient(90deg, #440154, #31688e, #35b779, #fde725)',
5046
+ plasma: 'linear-gradient(90deg, #0d0887, #7e03a8, #cc4778, #f89540, #f0f921)',
5047
+ inferno: 'linear-gradient(90deg, #000004, #420a68, #932667, #dd513a, #fca50a, #fcffa4)',
5048
+ magma: 'linear-gradient(90deg, #000004, #3b0f70, #8c2981, #de4968, #fea16e, #fcfdbf)',
5049
+ cividis: 'linear-gradient(90deg, #00224e, #123570, #3b496c, #575d6d, #707173, #a5a88f, #fee838)',
5050
+ turbo: 'linear-gradient(90deg, #30123b, #4662d7, #36aaf9, #1ae4b6, #72fe5e, #c8ef34, #faba39, #f66b19, #d93806, #7a0403)',
5051
+ };
5052
+ const heatmapSel = document.getElementById('settingsHeatmap');
5053
+ const heatmapPreview = document.getElementById('heatmapPreview');
5054
+ function updateHeatmapPreview() {
5055
+ if (heatmapPreview && heatmapSel) {
5056
+ heatmapPreview.style.background = HEATMAP_GRADIENTS[heatmapSel.value] || HEATMAP_GRADIENTS.viridis;
5057
+ }
5058
+ }
5059
+ if (heatmapSel) {
5060
+ heatmapSel.value = s.heatmap || 'viridis';
5061
+ heatmapSel.addEventListener('change', () => {
5062
+ saveSetting('heatmap', heatmapSel.value);
5063
+ updateHeatmapPreview();
5064
+ });
5065
+ updateHeatmapPreview();
5066
+ }
5067
+
5068
+ // Clear cache
5069
+ const clearBtn = document.getElementById('settingsClearCache');
5070
+ if (clearBtn) {
5071
+ clearBtn.addEventListener('click', () => {
5072
+ if (confirm('Clear all cached data and reset settings to defaults?')) {
5073
+ localStorage.removeItem(SETTINGS_KEY);
5074
+ localStorage.removeItem('vai-theme');
5075
+ localStorage.removeItem('vai-embed-cache');
5076
+ location.reload();
5077
+ }
5078
+ });
5079
+ }
5080
+
5081
+ // Apply saved defaults on load
5082
+ if (s.defaultModel && globalModel) {
5083
+ globalModel.value = s.defaultModel;
5084
+ }
5085
+ if (s.inputType) {
5086
+ const embedType = document.getElementById('embedInputType');
5087
+ if (embedType) embedType.value = s.inputType;
5088
+ }
5089
+ }
5090
+
5091
+ // โ”€โ”€ Update Checker โ”€โ”€
5092
+ function checkForAppUpdate() {
5093
+ if (!window.vai || !window.vai.isElectron) return;
5094
+
5095
+ // Show version in sidebar + settings page
5096
+ window.vai.getVersion().then(v => {
5097
+ if (!v) return;
5098
+ // v can be string (old) or { app, cli } (new)
5099
+ const appVer = typeof v === 'object' ? v.app : v;
5100
+ const cliVer = typeof v === 'object' ? v.cli : null;
5101
+
5102
+ // Sidebar label
5103
+ const label = document.getElementById('appVersionLabel');
5104
+ if (label && appVer) label.textContent = 'v' + appVer;
5105
+
5106
+ // Settings version section
5107
+ const section = document.getElementById('settingsVersionSection');
5108
+ const appBadge = document.getElementById('settingsAppVersion');
5109
+ const cliBadge = document.getElementById('settingsCliVersion');
5110
+ if (section) section.style.display = '';
5111
+ if (appBadge && appVer) appBadge.textContent = 'v' + appVer;
5112
+ if (cliBadge && cliVer) cliBadge.textContent = 'v' + cliVer;
5113
+ });
5114
+
5115
+ // Don't check more than once per hour
5116
+ const DISMISS_KEY = 'vai-update-dismissed';
5117
+ const dismissed = localStorage.getItem(DISMISS_KEY);
5118
+ if (dismissed) {
5119
+ const dismissedAt = parseInt(dismissed, 10);
5120
+ if (Date.now() - dismissedAt < 3600000) return; // 1 hour cooldown
5121
+ }
5122
+
5123
+ window.vai.updates.check().then(result => {
5124
+ if (!result || !result.hasUpdate) return;
5125
+
5126
+ const banner = document.getElementById('updateBanner');
5127
+ const versionEl = document.getElementById('updateVersion');
5128
+ const currentEl = document.getElementById('currentVersion');
5129
+ const downloadBtn = document.getElementById('updateDownloadBtn');
5130
+ const dismissBtn = document.getElementById('updateDismissBtn');
5131
+
5132
+ if (!banner) return;
5133
+
5134
+ versionEl.textContent = 'v' + result.latestVersion;
5135
+ currentEl.textContent = 'v' + result.currentVersion;
5136
+ banner.classList.add('show');
5137
+
5138
+ downloadBtn.addEventListener('click', () => {
5139
+ window.vai.updates.openRelease(result.releaseUrl);
5140
+ });
5141
+
5142
+ dismissBtn.addEventListener('click', () => {
5143
+ banner.classList.remove('show');
5144
+ localStorage.setItem(DISMISS_KEY, String(Date.now()));
5145
+ });
5146
+ }).catch(() => {
5147
+ // Silent fail โ€” update check is non-critical
5148
+ });
5149
+ }
5150
+
5151
+ // โ”€โ”€ Onboarding Walkthrough โ”€โ”€
5152
+ function initOnboarding() {
5153
+ const ONBOARDING_KEY = 'vai-onboarding-complete';
5154
+ const overlay = document.getElementById('onboardingOverlay');
5155
+ const welcomeWrap = document.getElementById('onboardingWelcomeWrap');
5156
+ const welcomeCard = document.getElementById('onboardingWelcomeCard');
5157
+ const tooltip = document.getElementById('onboardingTooltip');
5158
+ const spotlight = document.getElementById('onboardingSpotlight');
5159
+ const arrow = document.getElementById('onboardingArrow');
5160
+ const dotsContainer = document.getElementById('onboardingDots');
5161
+ const titleEl = document.getElementById('onboardingTitle');
5162
+ const bodyEl = document.getElementById('onboardingBody');
5163
+ const iconEl = document.getElementById('onboardingIcon');
5164
+
5165
+ if (!overlay) return;
5166
+
5167
+ const isElectron = !!(window.vai && window.vai.isElectron);
5168
+
5169
+ const steps = [
5170
+ {
5171
+ target: null, // welcome card, no spotlight
5172
+ icon: '๐Ÿš€',
5173
+ title: 'Welcome to Vai',
5174
+ body: 'Explore embeddings, compare text similarity, and search with vector models โ€” all from one playground.',
5175
+ },
5176
+ {
5177
+ target: '[data-tab="settings"]',
5178
+ icon: '๐Ÿ”‘',
5179
+ title: 'API Key Setup',
5180
+ body: 'First, add your <strong>Voyage AI API key</strong> in Settings. You can get one free at <strong>dash.voyageai.com</strong>.' +
5181
+ (isElectron ? '<br><br>๐Ÿ’Ž On desktop, your key is encrypted via <strong>OS keychain</strong> for secure storage.' : ''),
5182
+ arrow: 'left',
5183
+ },
5184
+ {
5185
+ target: '[data-tab="embed"]',
5186
+ icon: 'โšก',
5187
+ title: 'Embed',
5188
+ body: 'Turn any text into a <strong>vector embedding</strong> โ€” a numerical fingerprint that captures meaning. Visualize the raw vectors and explore what models produce.',
5189
+ arrow: 'left',
5190
+ },
5191
+ {
5192
+ target: '[data-tab="compare"]',
5193
+ icon: '๐Ÿ”ฅ',
5194
+ title: 'Compare',
5195
+ body: 'Paste multiple texts and see a <strong>similarity heatmap</strong> โ€” instantly discover which phrases are semantically close and which are far apart.',
5196
+ arrow: 'left',
5197
+ },
5198
+ {
5199
+ target: '[data-tab="search"]',
5200
+ icon: '๐Ÿ”',
5201
+ title: 'Search & Rerank',
5202
+ body: 'Run <strong>vector search</strong> against a set of documents and optionally <strong>rerank</strong> results for higher precision. Great for building RAG pipelines.',
5203
+ arrow: 'left',
5204
+ },
5205
+ ];
5206
+
5207
+ let currentStep = 0;
5208
+ const totalSteps = steps.length;
5209
+
5210
+ function buildDots() {
5211
+ dotsContainer.innerHTML = '';
5212
+ for (let i = 1; i < totalSteps; i++) {
5213
+ const dot = document.createElement('div');
5214
+ dot.className = 'onboarding-dot';
5215
+ if (i < currentStep) dot.classList.add('completed');
5216
+ if (i === currentStep) dot.classList.add('active');
5217
+ dotsContainer.appendChild(dot);
5218
+ }
5219
+ }
5220
+
5221
+ function positionTooltipNear(targetEl) {
5222
+ const step = steps[currentStep];
5223
+ const rect = targetEl.getBoundingClientRect();
5224
+
5225
+ // Position spotlight over the target
5226
+ spotlight.style.display = 'block';
5227
+ const pad = 6;
5228
+ spotlight.style.left = (rect.left - pad) + 'px';
5229
+ spotlight.style.top = (rect.top - pad) + 'px';
5230
+ spotlight.style.width = (rect.width + pad * 2) + 'px';
5231
+ spotlight.style.height = (rect.height + pad * 2) + 'px';
5232
+
5233
+ // Reset arrow classes
5234
+ arrow.className = 'onboarding-tooltip-arrow';
5235
+
5236
+ // Position tooltip to the right of sidebar items
5237
+ if (step.arrow === 'left') {
5238
+ arrow.classList.add('left');
5239
+ tooltip.style.left = (rect.right + 16) + 'px';
5240
+ tooltip.style.top = Math.max(8, rect.top - 10) + 'px';
5241
+ tooltip.style.right = 'auto';
5242
+ tooltip.style.bottom = 'auto';
5243
+ } else {
5244
+ // Below the target
5245
+ arrow.classList.add('top');
5246
+ tooltip.style.left = Math.max(8, rect.left) + 'px';
5247
+ tooltip.style.top = (rect.bottom + 14) + 'px';
5248
+ tooltip.style.right = 'auto';
5249
+ tooltip.style.bottom = 'auto';
5250
+ }
5251
+
5252
+ // Ensure tooltip doesn't overflow viewport
5253
+ requestAnimationFrame(() => {
5254
+ const tr = tooltip.getBoundingClientRect();
5255
+ if (tr.bottom > window.innerHeight - 10) {
5256
+ tooltip.style.top = Math.max(8, window.innerHeight - tr.height - 10) + 'px';
5257
+ }
5258
+ if (tr.right > window.innerWidth - 10) {
5259
+ tooltip.style.left = Math.max(8, window.innerWidth - tr.width - 10) + 'px';
5260
+ }
5261
+ });
5262
+ }
5263
+
5264
+ function showStep(idx) {
5265
+ currentStep = idx;
5266
+ const step = steps[idx];
5267
+
5268
+ if (idx === 0) {
5269
+ // Welcome card โ€” centered, no spotlight
5270
+ welcomeWrap.style.display = 'flex';
5271
+ tooltip.classList.remove('visible');
5272
+ tooltip.style.display = 'none';
5273
+ spotlight.style.display = 'none';
5274
+ requestAnimationFrame(() => {
5275
+ welcomeCard.classList.add('visible');
5276
+ });
5277
+ return;
5278
+ }
5279
+
5280
+ // Hide welcome card
5281
+ welcomeWrap.style.display = 'none';
5282
+ welcomeCard.classList.remove('visible');
5283
+
5284
+ // Show tooltip
5285
+ tooltip.style.display = 'block';
5286
+ tooltip.classList.remove('visible');
5287
+ iconEl.textContent = step.icon;
5288
+ titleEl.textContent = step.title;
5289
+ bodyEl.innerHTML = step.body;
5290
+ buildDots();
5291
+
5292
+ // Update next button text
5293
+ const nextBtn = document.getElementById('onboardingNext');
5294
+ nextBtn.textContent = (idx === totalSteps - 1) ? 'Get Started' : 'Next';
5295
+
5296
+ // Find target element and position
5297
+ const targetEl = document.querySelector(step.target);
5298
+ if (targetEl) {
5299
+ positionTooltipNear(targetEl);
5300
+ }
5301
+
5302
+ requestAnimationFrame(() => {
5303
+ tooltip.classList.add('visible');
5304
+ });
5305
+ }
5306
+
5307
+ function finish() {
5308
+ tooltip.classList.remove('visible');
5309
+ welcomeCard.classList.remove('visible');
5310
+ spotlight.style.display = 'none';
5311
+ setTimeout(() => {
5312
+ overlay.classList.remove('active');
5313
+ }, 300);
5314
+ localStorage.setItem(ONBOARDING_KEY, 'true');
5315
+ }
5316
+
5317
+ function nextStep() {
5318
+ if (currentStep < totalSteps - 1) {
5319
+ showStep(currentStep + 1);
5320
+ } else {
5321
+ finish();
5322
+ }
5323
+ }
5324
+
5325
+ // Wire up buttons
5326
+ document.getElementById('onboardingWelcomeNext').addEventListener('click', nextStep);
5327
+ document.getElementById('onboardingWelcomeSkip').addEventListener('click', finish);
5328
+ document.getElementById('onboardingNext').addEventListener('click', nextStep);
5329
+ document.getElementById('onboardingSkip').addEventListener('click', finish);
5330
+ document.getElementById('onboardingBackdrop').addEventListener('click', finish);
5331
+
5332
+ // "Show Welcome Tour" button in settings
5333
+ const tourBtn = document.getElementById('settingsShowTour');
5334
+ if (tourBtn) {
5335
+ tourBtn.addEventListener('click', () => {
5336
+ startOnboarding();
5337
+ });
5338
+ }
5339
+
5340
+ // Auto-start on first visit
5341
+ function startOnboarding() {
5342
+ // Sync onboarding logo with current theme
5343
+ const onboardLogo = document.getElementById('onboardingLogo');
5344
+ if (onboardLogo) {
5345
+ const theme = localStorage.getItem('vai-theme') || 'dark';
5346
+ onboardLogo.src = '/icons/' + (theme === 'light' ? 'light' : 'dark') + '/64.png';
5347
+ }
5348
+ overlay.classList.add('active');
5349
+ showStep(0);
5350
+ }
5351
+
5352
+ if (!localStorage.getItem(ONBOARDING_KEY)) {
5353
+ // Delay slightly so the app is fully rendered
5354
+ setTimeout(startOnboarding, 600);
5355
+ }
5356
+
5357
+ // Expose for manual replay
5358
+ window.startOnboarding = startOnboarding;
5359
+ }
5360
+
5361
+ // โ”€โ”€ Multimodal Tab โ”€โ”€
5362
+ let mmImageData = null; // base64 data URL of the uploaded image
5363
+ let mmGalleryImages = []; // array of { dataUrl, name, size }
5364
+ let mmSearchMode = 'text';
5365
+ let mmSearchImageIndex = -1;
5366
+
5367
+ function initMultimodal() {
5368
+ const dropZone = document.getElementById('mmDropZone');
5369
+ const fileInput = document.getElementById('mmFileInput');
5370
+
5371
+ // Click to browse
5372
+ dropZone.addEventListener('click', () => fileInput.click());
5373
+ fileInput.addEventListener('change', (e) => {
5374
+ if (e.target.files && e.target.files[0]) handleMultimodalImage(e.target.files[0]);
5375
+ });
5376
+
5377
+ // Drag and drop
5378
+ ['dragenter', 'dragover'].forEach(evt => {
5379
+ dropZone.addEventListener(evt, (e) => {
5380
+ e.preventDefault();
5381
+ e.stopPropagation();
5382
+ dropZone.classList.add('drag-active');
5383
+ });
5384
+ });
5385
+ ['dragleave', 'drop'].forEach(evt => {
5386
+ dropZone.addEventListener(evt, (e) => {
5387
+ e.preventDefault();
5388
+ e.stopPropagation();
5389
+ dropZone.classList.remove('drag-active');
5390
+ });
5391
+ });
5392
+ dropZone.addEventListener('drop', (e) => {
5393
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
5394
+ handleMultimodalImage(e.dataTransfer.files[0]);
5395
+ }
5396
+ });
5397
+
5398
+ // Paste from clipboard
5399
+ document.addEventListener('paste', (e) => {
5400
+ // Only handle if multimodal tab is active
5401
+ const panel = document.getElementById('tab-multimodal');
5402
+ if (!panel || !panel.classList.contains('active')) return;
5403
+ const items = e.clipboardData?.items;
5404
+ if (!items) return;
5405
+ for (const item of items) {
5406
+ if (item.type.startsWith('image/')) {
5407
+ e.preventDefault();
5408
+ handleMultimodalImage(item.getAsFile());
5409
+ return;
5410
+ }
5411
+ }
5412
+ });
5413
+
5414
+ // Gallery
5415
+ renderGalleryGrid();
5416
+
5417
+ const galleryInput = document.getElementById('mmGalleryFileInput');
5418
+ galleryInput.addEventListener('change', (e) => {
5419
+ if (e.target.files && e.target.files[0]) {
5420
+ addGalleryImage(e.target.files[0]);
5421
+ }
5422
+ galleryInput.value = '';
5423
+ });
5424
+ }
5425
+
5426
+ function handleMultimodalImage(file) {
5427
+ const VALID_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
5428
+ if (!VALID_TYPES.includes(file.type)) {
5429
+ showError('mmError', 'Unsupported image type. Use PNG, JPEG, WebP, or GIF.');
5430
+ return;
5431
+ }
5432
+ if (file.size > 20 * 1024 * 1024) {
5433
+ showError('mmError', 'Image too large. Maximum size is 20 MB.');
5434
+ return;
5435
+ }
5436
+ hideError('mmError');
5437
+
5438
+ const reader = new FileReader();
5439
+ reader.onload = (e) => {
5440
+ mmImageData = e.target.result;
5441
+ const img = document.getElementById('mmPreviewImg');
5442
+ img.src = mmImageData;
5443
+
5444
+ img.onload = () => {
5445
+ const info = document.getElementById('mmFileInfo');
5446
+ const sizeStr = file.size > 1024 * 1024
5447
+ ? (file.size / (1024 * 1024)).toFixed(1) + ' MB'
5448
+ : (file.size / 1024).toFixed(0) + ' KB';
5449
+ info.textContent = `${file.name} ยท ${img.naturalWidth}ร—${img.naturalHeight} ยท ${sizeStr}`;
5450
+ };
5451
+
5452
+ document.getElementById('mmDropZone').style.display = 'none';
5453
+ document.getElementById('mmPreview').classList.add('visible');
5454
+ };
5455
+ reader.readAsDataURL(file);
5456
+ }
5457
+
5458
+ window.clearMultimodalImage = function() {
5459
+ mmImageData = null;
5460
+ document.getElementById('mmPreviewImg').src = '';
5461
+ document.getElementById('mmFileInfo').textContent = '';
5462
+ document.getElementById('mmPreview').classList.remove('visible');
5463
+ document.getElementById('mmDropZone').style.display = '';
5464
+ document.getElementById('mmFileInput').value = '';
5465
+ };
5466
+
5467
+ window.doMultimodalCompare = async function() {
5468
+ hideError('mmError');
5469
+ const text = document.getElementById('mmText').value.trim();
5470
+ if (!mmImageData) { showError('mmError', 'Upload an image first'); return; }
5471
+ if (!text) { showError('mmError', 'Enter text to compare against the image'); return; }
5472
+
5473
+ setLoading('mmCompareBtn', true);
5474
+ try {
5475
+ const model = document.getElementById('mmModel').value;
5476
+ const dimsVal = document.getElementById('mmDimensions').value;
5477
+ const body = {
5478
+ inputs: [
5479
+ { content: [{ type: 'image_base64', image_base64: mmImageData }] },
5480
+ { content: [{ type: 'text', text: text }] }
5481
+ ],
5482
+ model: model,
5483
+ input_type: 'document'
5484
+ };
5485
+ if (dimsVal) body.output_dimension = parseInt(dimsVal, 10);
5486
+
5487
+ const data = await apiPost('/api/multimodal-embed', body);
5488
+
5489
+ const vecA = data.data[0].embedding;
5490
+ const vecB = data.data[1].embedding;
5491
+ const cosine = cosineSim(vecA, vecB);
5492
+
5493
+ // Hero display
5494
+ const cosinePct = Math.max(0, cosine * 100);
5495
+ let cosineColor;
5496
+ if (cosine > 0.7) cosineColor = 'var(--green)';
5497
+ else if (cosine > 0.4) cosineColor = 'var(--yellow)';
5498
+ else cosineColor = 'var(--red)';
5499
+
5500
+ const scoreEl = document.getElementById('mmSimScore');
5501
+ scoreEl.textContent = cosine.toFixed(4);
5502
+ scoreEl.style.color = cosineColor;
5503
+
5504
+ const barEl = document.getElementById('mmSimBar');
5505
+ barEl.style.width = cosinePct + '%';
5506
+ barEl.style.background = cosineColor;
5507
+
5508
+ // Stats
5509
+ const usage = data.usage || {};
5510
+ const statsEl = document.getElementById('mmStats');
5511
+ statsEl.innerHTML = `
5512
+ <span class="stat"><span class="stat-label">Model</span><span class="stat-value">${data.model || model}</span></span>
5513
+ <span class="stat"><span class="stat-label">Dimensions</span><span class="stat-value">${vecA.length}</span></span>
5514
+ <span class="stat"><span class="stat-label">Text Tokens</span><span class="stat-value">${usage.text_tokens || 'โ€”'}</span></span>
5515
+ <span class="stat"><span class="stat-label">Image Pixels</span><span class="stat-value">${usage.image_pixels ? usage.image_pixels.toLocaleString() : 'โ€”'}</span></span>
5516
+ <span class="stat"><span class="stat-label">Total Tokens</span><span class="stat-value">${usage.total_tokens || 'โ€”'}</span></span>
5517
+ `;
5518
+
5519
+ // Insight note
5520
+ const noteEl = document.getElementById('mmNote');
5521
+ if (cosine > 0.7) {
5522
+ noteEl.innerHTML = '๐Ÿ’ก <strong>High similarity!</strong> The image and text are closely related in Voyage AI\'s multimodal embedding space. This means the text is a good semantic description of the image.';
5523
+ } else if (cosine > 0.4) {
5524
+ noteEl.innerHTML = '๐Ÿ’ก <strong>Moderate similarity.</strong> The image and text share some semantic overlap. They may be related but not a direct match.';
5525
+ } else {
5526
+ noteEl.innerHTML = '๐Ÿ’ก <strong>Low similarity.</strong> The image and text are semantically distant. Try a description that matches the image content more closely.';
5527
+ }
5528
+
5529
+ document.getElementById('mmResult').classList.add('visible');
5530
+ } catch (err) {
5531
+ showError('mmError', err.message);
5532
+ } finally {
5533
+ setLoading('mmCompareBtn', false);
5534
+ }
5535
+ };
5536
+
5537
+ // Gallery functions
5538
+ function renderGalleryGrid() {
5539
+ const grid = document.getElementById('mmGalleryGrid');
5540
+ grid.innerHTML = '';
5541
+
5542
+ mmGalleryImages.forEach((img, i) => {
5543
+ const slot = document.createElement('div');
5544
+ slot.className = 'mm-gallery-slot filled' + (mmSearchMode === 'image' && mmSearchImageIndex === i ? ' query-selected' : '');
5545
+ slot.innerHTML = `<img src="${img.dataUrl}" alt="${img.name}"><button class="mm-slot-remove" title="Remove">ร—</button>`;
5546
+ slot.querySelector('.mm-slot-remove').addEventListener('click', (e) => {
5547
+ e.stopPropagation();
5548
+ removeGalleryImage(i);
5549
+ });
5550
+ if (mmSearchMode === 'image') {
5551
+ slot.style.cursor = 'pointer';
5552
+ slot.addEventListener('click', () => selectGalleryImageAsQuery(i));
5553
+ }
5554
+ grid.appendChild(slot);
5555
+ });
5556
+
5557
+ // Add empty slot if under 6
5558
+ if (mmGalleryImages.length < 6) {
5559
+ const addSlot = document.createElement('div');
5560
+ addSlot.className = 'mm-gallery-slot';
5561
+ addSlot.innerHTML = '<span class="mm-slot-add">+</span>';
5562
+ addSlot.addEventListener('click', () => {
5563
+ document.getElementById('mmGalleryFileInput').click();
5564
+ });
5565
+ grid.appendChild(addSlot);
5566
+ }
5567
+ }
5568
+
5569
+ function addGalleryImage(file) {
5570
+ const VALID_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
5571
+ if (!VALID_TYPES.includes(file.type)) return;
5572
+ if (file.size > 20 * 1024 * 1024) return;
5573
+ if (mmGalleryImages.length >= 6) return;
5574
+
5575
+ const reader = new FileReader();
5576
+ reader.onload = (e) => {
5577
+ mmGalleryImages.push({ dataUrl: e.target.result, name: file.name, size: file.size });
5578
+ renderGalleryGrid();
5579
+ };
5580
+ reader.readAsDataURL(file);
5581
+ }
5582
+
5583
+ function removeGalleryImage(index) {
5584
+ mmGalleryImages.splice(index, 1);
5585
+ if (mmSearchImageIndex === index) mmSearchImageIndex = -1;
5586
+ else if (mmSearchImageIndex > index) mmSearchImageIndex--;
5587
+ renderGalleryGrid();
5588
+ updateSearchImageLabel();
5589
+ }
5590
+
5591
+ function selectGalleryImageAsQuery(index) {
5592
+ mmSearchImageIndex = (mmSearchImageIndex === index) ? -1 : index;
5593
+ renderGalleryGrid();
5594
+ updateSearchImageLabel();
5595
+ }
5596
+
5597
+ function updateSearchImageLabel() {
5598
+ const label = document.getElementById('mmSearchImageLabel');
5599
+ if (mmSearchImageIndex >= 0 && mmSearchImageIndex < mmGalleryImages.length) {
5600
+ label.textContent = 'โœ“ Image ' + (mmSearchImageIndex + 1) + ' selected as query';
5601
+ label.style.color = 'var(--accent)';
5602
+ } else {
5603
+ label.textContent = 'No image selected';
5604
+ label.style.color = 'var(--text-muted)';
5605
+ }
5606
+ }
5607
+
5608
+ window.setMmSearchMode = function(mode) {
5609
+ mmSearchMode = mode;
5610
+ document.querySelectorAll('#mmSearchMode button').forEach(b => {
5611
+ b.classList.toggle('active', b.dataset.mode === mode);
5612
+ });
5613
+ document.getElementById('mmSearchTextWrap').style.display = mode === 'text' ? '' : 'none';
5614
+ document.getElementById('mmSearchImageWrap').style.display = mode === 'image' ? '' : 'none';
5615
+ renderGalleryGrid();
5616
+ };
5617
+
5618
+ window.doMultimodalSearch = async function() {
5619
+ hideError('mmSearchError');
5620
+
5621
+ const corpusText = document.getElementById('mmCorpusText').value.trim();
5622
+ const textItems = corpusText ? corpusText.split('\n').map(t => t.trim()).filter(Boolean) : [];
5623
+
5624
+ if (mmGalleryImages.length === 0 && textItems.length === 0) {
5625
+ showError('mmSearchError', 'Add at least one image or text to the corpus');
5626
+ return;
5627
+ }
5628
+
5629
+ // Build query input
5630
+ let queryInput;
5631
+ if (mmSearchMode === 'text') {
5632
+ const q = document.getElementById('mmSearchQuery').value.trim();
5633
+ if (!q) { showError('mmSearchError', 'Enter a search query'); return; }
5634
+ queryInput = { content: [{ type: 'text', text: q }] };
5635
+ } else {
5636
+ if (mmSearchImageIndex < 0 || mmSearchImageIndex >= mmGalleryImages.length) {
5637
+ showError('mmSearchError', 'Select an image from the corpus to use as query');
5638
+ return;
5639
+ }
5640
+ queryInput = { content: [{ type: 'image_base64', image_base64: mmGalleryImages[mmSearchImageIndex].dataUrl }] };
5641
+ }
5642
+
5643
+ const btnId = mmSearchMode === 'text' ? 'mmSearchBtn' : 'mmSearchImgBtn';
5644
+ setLoading(btnId, true);
5645
+
5646
+ try {
5647
+ const model = document.getElementById('mmModel').value;
5648
+ const dimsVal = document.getElementById('mmDimensions').value;
5649
+
5650
+ // Build corpus inputs
5651
+ const corpusInputs = [];
5652
+ const corpusMeta = []; // track type/content for display
5653
+
5654
+ mmGalleryImages.forEach((img, i) => {
5655
+ corpusInputs.push({ content: [{ type: 'image_base64', image_base64: img.dataUrl }] });
5656
+ corpusMeta.push({ type: 'image', dataUrl: img.dataUrl, label: img.name || 'Image ' + (i + 1) });
5657
+ });
5658
+
5659
+ textItems.forEach(t => {
5660
+ corpusInputs.push({ content: [{ type: 'text', text: t }] });
5661
+ corpusMeta.push({ type: 'text', label: t });
5662
+ });
5663
+
5664
+ // Embed query + all corpus items in one call
5665
+ const allInputs = [queryInput, ...corpusInputs];
5666
+ const body = {
5667
+ inputs: allInputs,
5668
+ model: model,
5669
+ input_type: 'document'
5670
+ };
5671
+ if (dimsVal) body.output_dimension = parseInt(dimsVal, 10);
5672
+
5673
+ const data = await apiPost('/api/multimodal-embed', body);
5674
+
5675
+ const queryVec = data.data[0].embedding;
5676
+ const results = corpusMeta.map((meta, i) => {
5677
+ const vec = data.data[i + 1].embedding;
5678
+ const sim = cosineSim(queryVec, vec);
5679
+ return { ...meta, similarity: sim };
5680
+ }).sort((a, b) => b.similarity - a.similarity);
5681
+
5682
+ // Render results
5683
+ const listEl = document.getElementById('mmSearchResultList');
5684
+ listEl.innerHTML = '';
5685
+
5686
+ results.forEach((r, i) => {
5687
+ let simColor;
5688
+ if (r.similarity > 0.7) simColor = 'var(--green)';
5689
+ else if (r.similarity > 0.4) simColor = 'var(--yellow)';
5690
+ else simColor = 'var(--red)';
5691
+
5692
+ const item = document.createElement('div');
5693
+ item.className = 'mm-result-item';
5694
+
5695
+ const thumbHtml = r.type === 'image'
5696
+ ? `<img class="mm-result-thumb" src="${r.dataUrl}" alt="${r.label}">`
5697
+ : `<div class="mm-result-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-surface);font-size:18px;">๐Ÿ“</div>`;
5698
+
5699
+ item.innerHTML = `
5700
+ <div class="mm-result-rank">#${i + 1}</div>
5701
+ ${thumbHtml}
5702
+ <div class="mm-result-content">
5703
+ <div class="mm-result-text" title="${r.label.replace(/"/g, '&quot;')}">${r.label}</div>
5704
+ <div class="mm-result-type">${r.type}</div>
5705
+ </div>
5706
+ <div class="mm-result-score" style="color:${simColor}">${r.similarity.toFixed(4)}</div>
5707
+ `;
5708
+ listEl.appendChild(item);
5709
+ });
5710
+
5711
+ document.getElementById('mmSearchResult').classList.add('visible');
5712
+ } catch (err) {
5713
+ showError('mmSearchError', err.message);
5714
+ } finally {
5715
+ setLoading(btnId, false);
5716
+ }
5717
+ };
5718
+
2984
5719
  // โ”€โ”€ Patch init to include benchmark setup โ”€โ”€
2985
5720
  const _origInit = init;
2986
5721
  init = async function() {
2987
5722
  await _origInit();
5723
+ initThemeToggle();
2988
5724
  buildModelCheckboxes();
2989
5725
  populateBenchRankSelects();
2990
5726
  populateQuantModelSelect();
2991
5727
  initCostCalculator();
2992
5728
  renderHistory();
5729
+ initSettings();
5730
+ initMultimodal();
5731
+ checkForAppUpdate();
5732
+ initOnboarding();
2993
5733
  };
2994
5734
 
2995
5735
  // โ”€โ”€ Start โ”€โ”€
2996
5736
  init();
2997
5737
  })();
2998
5738
  </script>
5739
+ <!-- Cost Help Modal -->
5740
+ <div class="cost-modal-overlay" id="costHelpModal">
5741
+ <div class="cost-modal">
5742
+ <button class="cost-modal-close" id="costHelpClose">&times;</button>
5743
+
5744
+ <h2>๐Ÿ“ How the Cost Calculator Works</h2>
5745
+
5746
+ <p>Voyage AI charges per <strong>million tokens</strong> processed. A token is roughly ยพ of a word.
5747
+ The calculator estimates your total embedding cost based on how many documents you embed
5748
+ and how many queries you run over time.</p>
5749
+
5750
+ <h3>๐Ÿ’ก Simple Mode</h3>
5751
+ <p>Compares the per-model query cost for a given volume. Useful for quick "which model is cheapest?" checks.</p>
5752
+ <div class="formula">
5753
+ <span class="label">Daily cost =</span><br>
5754
+ <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>
5755
+ <span class="label">Monthly cost =</span> daily cost ร— 30
5756
+ </div>
5757
+
5758
+ <h3>๐Ÿ“Š RAG Planner Mode</h3>
5759
+ <p>Models the full cost of a Retrieval-Augmented Generation (RAG) pipeline, separating
5760
+ the <strong>one-time</strong> document ingestion cost from the <strong>recurring</strong> query cost.</p>
5761
+
5762
+ <div class="formula">
5763
+ <span class="label">Document embedding (one-time):</span><br>
5764
+ <span class="accent">doc_cost</span> = num_docs ร— tokens_per_doc รท 1,000,000 ร— <span class="accent">doc_model_price</span><br><br>
5765
+ <span class="label">Query embedding (monthly):</span><br>
5766
+ <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>
5767
+ <span class="label">Projected total:</span><br>
5768
+ <span class="accent">total</span> = doc_cost + (query_cost/mo ร— <span class="accent">months</span>)
5769
+ </div>
5770
+
5771
+ <h3>โš–๏ธ Three Strategies Compared</h3>
5772
+ <ul>
5773
+ <li><strong>Symmetric</strong> โ€” same model for documents and queries. Simple but expensive at scale,
5774
+ because query-heavy workloads pay the full model price on every request.</li>
5775
+ <li><strong>Asymmetric (โ˜… Recommended)</strong> โ€” use a high-quality model (e.g. <code>voyage-4-large</code>)
5776
+ for documents and a cheaper model (e.g. <code>voyage-4-lite</code>) for queries.
5777
+ This works because Voyage 4 models share the same embedding space โ€” vectors from different
5778
+ models are directly comparable.</li>
5779
+ <li><strong>Asymmetric + Local</strong> โ€” embed documents via the API, but run queries locally using
5780
+ <code>voyage-4-nano</code> on HuggingFace (free). Query cost drops to $0.</li>
5781
+ </ul>
5782
+
5783
+ <h3>๐Ÿ”— Shared Embedding Space</h3>
5784
+ <p>The Voyage 4 family (<code>voyage-4-large</code>, <code>voyage-4</code>, <code>voyage-4-lite</code>,
5785
+ <code>voyage-4-nano</code>) all produce vectors in the <em>same geometric space</em>.
5786
+ A document embedded with <code>voyage-4-large</code> can be searched with a query embedded by
5787
+ <code>voyage-4-lite</code> โ€” cosine similarity still works correctly. This is what makes
5788
+ asymmetric strategies possible.</p>
5789
+
5790
+ <div class="example">
5791
+ <strong>Example:</strong> 100K docs ร— 500 tok = 50M doc tokens<br>
5792
+ 1M queries/mo ร— 30 tok = 30M query tokens/mo<br><br>
5793
+ <strong>Symmetric</strong> (voyage-4-large @ $0.18/1M):<br>
5794
+ &nbsp;&nbsp;Docs: $9.00 + Queries: $5.40/mo ร— 12 = <strong>$73.80</strong><br><br>
5795
+ <strong>Asymmetric</strong> (large docs + lite queries @ $0.05/1M):<br>
5796
+ &nbsp;&nbsp;Docs: $9.00 + Queries: $1.50/mo ร— 12 = <strong>$27.00</strong><br><br>
5797
+ &nbsp;&nbsp;Savings: <strong>63%</strong> โ€” same document quality, cheaper queries.
5798
+ </div>
5799
+
5800
+ <h3>๐Ÿ“‹ Per-Model Table</h3>
5801
+ <p>The bottom table shows what it would cost to use each model symmetrically (same model for
5802
+ docs and queries). The relative bar shows cost compared to the most expensive option.
5803
+ Use this to understand the price spread across the full model lineup.</p>
5804
+
5805
+ <h3>๐ŸŽฏ Key Assumptions</h3>
5806
+ <ul>
5807
+ <li>Token counts are estimates โ€” actual counts depend on your text. Use <code>vai chunk --stats</code> to measure real token counts.</li>
5808
+ <li>Document embedding is a one-time cost (you embed once, search many times).</li>
5809
+ <li>Re-embedding (e.g. updated docs) is not modeled โ€” add a buffer if your corpus changes frequently.</li>
5810
+ <li>Reranking costs are separate and not included here. Reranking is priced per query pair, not per token.</li>
5811
+ </ul>
5812
+
5813
+ <p style="margin-top:20px;font-size:12px;color:var(--text-muted);">
5814
+ CLI equivalent: <code>vai estimate --docs 100K --queries 1M --doc-model voyage-4-large --query-model voyage-4-lite</code>
5815
+ </p>
5816
+ </div>
5817
+ </div>
2999
5818
  </body>
3000
5819
  </html>