voyageai-cli 1.19.2 → 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,7 +3,9 @@
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
 
@@ -13,7 +15,8 @@
13
15
  --bg-surface: #112733; /* Gray Dark 4 */
14
16
  --bg-card: #1C2D38; /* Gray Dark 3 */
15
17
  --bg-input: #112733; /* Gray Dark 4 */
16
- --accent: #00ED64; /* Green Base */
18
+ --accent: #00ED64; /* Green Base — interactive elements only */
19
+ --accent-text: #FFFFFF; /* Bright white — headings/labels in dark mode */
17
20
  --accent-dim: #00A35C; /* Green Dark 1 */
18
21
  --accent-glow: rgba(0, 237, 100, 0.12);
19
22
  --text: #E8EDEB; /* Gray Light 2 */
@@ -40,7 +43,8 @@
40
43
  --bg-surface: #F9FBFA; /* Gray Light 3 */
41
44
  --bg-card: #FFFFFF; /* White */
42
45
  --bg-input: #F9FBFA; /* Gray Light 3 */
43
- --accent: #00A35C; /* Green Dark 1 (better contrast on white) */
46
+ --accent: #00A35C; /* Green Dark 1 interactive elements */
47
+ --accent-text: #001E2B; /* MDB Black — headings/labels in light mode */
44
48
  --accent-dim: #00684A; /* Green Dark 2 */
45
49
  --accent-glow: rgba(0, 163, 92, 0.08);
46
50
  --text: #001E2B; /* MDB Black */
@@ -75,8 +79,8 @@
75
79
  [data-theme="light"] .explore-modal-overlay {
76
80
  background: rgba(0, 30, 43, 0.4);
77
81
  }
78
- [data-theme="light"] .nav {
79
- box-shadow: 0 1px 3px rgba(0, 30, 43, 0.06);
82
+ [data-theme="light"] .sidebar {
83
+ box-shadow: 1px 0 3px rgba(0, 30, 43, 0.06);
80
84
  }
81
85
  /* Light mode gradient overrides */
82
86
  [data-theme="light"] .quant-bar-fill.storage { background: linear-gradient(90deg, #00A35C, #00ED64); }
@@ -98,36 +102,401 @@ body {
98
102
  overflow-x: hidden;
99
103
  }
100
104
 
101
- /* Nav */
102
- .nav {
103
- background: var(--bg-surface);
104
- border-bottom: 1px solid var(--border);
105
- padding: 0 24px;
106
- 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 {
107
229
  display: flex;
108
230
  align-items: center;
109
- gap: 16px;
110
- position: sticky;
111
- 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;
112
347
  z-index: 100;
113
348
  }
114
349
 
115
- .nav-title {
116
- 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;
117
371
  font-weight: 700;
118
- color: var(--accent);
372
+ color: var(--accent-text);
373
+ white-space: nowrap;
374
+ letter-spacing: -0.2px;
375
+ }
376
+
377
+ .sidebar-nav {
378
+ flex: 1;
379
+ overflow-y: auto;
380
+ padding: 8px;
381
+ display: flex;
382
+ flex-direction: column;
383
+ gap: 1px;
384
+ }
385
+
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;
404
+ }
405
+
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;
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;
119
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;
461
+ }
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
+ }
475
+ .nav-model-select {
476
+ background: var(--bg-input);
477
+ border: 1px solid var(--border);
478
+ color: var(--text);
479
+ padding: 6px 10px;
480
+ border-radius: var(--radius);
481
+ font-size: 12px;
482
+ font-family: var(--mono);
483
+ cursor: pointer;
484
+ width: 100%;
120
485
  }
121
486
 
122
- .nav-spacer { flex: 1; }
487
+ .sidebar-controls {
488
+ display: flex;
489
+ align-items: center;
490
+ justify-content: space-between;
491
+ }
123
492
 
124
493
  .theme-toggle {
125
494
  background: none;
126
495
  border: 1px solid var(--border);
127
- border-radius: 20px;
128
- padding: 5px 10px;
496
+ border-radius: 16px;
497
+ padding: 4px 8px;
129
498
  cursor: pointer;
130
- font-size: 16px;
499
+ font-size: 14px;
131
500
  line-height: 1;
132
501
  transition: all 0.2s;
133
502
  display: flex;
@@ -149,47 +518,31 @@ body {
149
518
  .status-dot.error { background: var(--error); }
150
519
 
151
520
  .status-label {
152
- font-size: 12px;
521
+ font-size: 11px;
153
522
  color: var(--text-dim);
154
523
  }
155
524
 
156
- .nav-model-select {
157
- background: var(--bg-input);
158
- border: 1px solid var(--border);
159
- color: var(--text);
160
- padding: 6px 12px;
161
- border-radius: var(--radius);
162
- font-size: 13px;
163
- font-family: var(--mono);
164
- cursor: pointer;
165
- }
166
-
167
- /* Tabs */
168
- .tab-bar {
525
+ /* ── Content area ── */
526
+ .content-area {
527
+ flex: 1;
528
+ overflow-y: auto;
169
529
  display: flex;
170
- background: var(--bg-surface);
171
- border-bottom: 1px solid var(--border);
172
- padding: 0 24px;
173
- gap: 0;
530
+ flex-direction: column;
174
531
  }
175
532
 
176
- .tab-btn {
177
- background: none;
178
- border: none;
179
- color: var(--text-dim);
180
- padding: 12px 20px;
181
- font-size: 14px;
182
- font-family: var(--font);
183
- cursor: pointer;
184
- border-bottom: 2px solid transparent;
185
- transition: all 0.2s;
186
- white-space: nowrap;
533
+ .content-drag-region {
534
+ -webkit-app-region: drag;
535
+ height: 38px;
536
+ min-height: 38px;
537
+ flex-shrink: 0;
187
538
  }
188
- .tab-btn:hover { color: var(--text); background: rgba(255,255,255,0.03); }
189
- .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
190
539
 
191
540
  /* Main */
192
- .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; }
193
546
 
194
547
  .tab-panel { display: none; }
195
548
  .tab-panel.active { display: block; }
@@ -206,7 +559,7 @@ body {
206
559
  .card-title {
207
560
  font-size: 14px;
208
561
  font-weight: 600;
209
- color: var(--accent);
562
+ color: var(--accent-text);
210
563
  margin-bottom: 12px;
211
564
  text-transform: uppercase;
212
565
  letter-spacing: 0.5px;
@@ -333,7 +686,7 @@ select:focus { outline: none; border-color: var(--accent); }
333
686
  margin-bottom: 8px;
334
687
  }
335
688
  .stat-label { color: var(--text-dim); }
336
- .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); }
337
690
 
338
691
  .vector-preview {
339
692
  font-family: var(--mono);
@@ -481,7 +834,7 @@ select:focus { outline: none; border-color: var(--accent); }
481
834
  .result-rank {
482
835
  font-size: 20px;
483
836
  font-weight: 700;
484
- color: var(--accent);
837
+ color: var(--accent-text);
485
838
  font-family: var(--mono);
486
839
  min-width: 30px;
487
840
  }
@@ -646,7 +999,7 @@ select:focus { outline: none; border-color: var(--accent); }
646
999
  .explore-modal-links-title {
647
1000
  font-size: 11px;
648
1001
  font-weight: 600;
649
- color: var(--accent);
1002
+ color: var(--accent-text);
650
1003
  text-transform: uppercase;
651
1004
  letter-spacing: 0.5px;
652
1005
  margin-bottom: 6px;
@@ -668,7 +1021,7 @@ select:focus { outline: none; border-color: var(--accent); }
668
1021
  .explore-modal-tryit-title {
669
1022
  font-size: 11px;
670
1023
  font-weight: 600;
671
- color: var(--accent);
1024
+ color: var(--accent-text);
672
1025
  text-transform: uppercase;
673
1026
  letter-spacing: 0.5px;
674
1027
  margin-bottom: 8px;
@@ -781,7 +1134,7 @@ select:focus { outline: none; border-color: var(--accent); }
781
1134
  .rank-num {
782
1135
  font-size: 16px;
783
1136
  font-weight: 700;
784
- color: var(--accent);
1137
+ color: var(--accent-text);
785
1138
  font-family: var(--mono);
786
1139
  text-align: center;
787
1140
  }
@@ -810,7 +1163,7 @@ select:focus { outline: none; border-color: var(--accent); }
810
1163
  display: flex; justify-content: space-between; align-items: baseline;
811
1164
  margin-bottom: 4px; font-size: 13px;
812
1165
  }
813
- .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); }
814
1167
  .quant-bar-label .dtype-value { color: var(--text-dim); font-family: var(--mono); font-size: 12px; }
815
1168
  .quant-bar-track {
816
1169
  height: 32px; background: var(--bg-input); border-radius: 6px;
@@ -835,7 +1188,7 @@ select:focus { outline: none; border-color: var(--accent); }
835
1188
  display: flex; justify-content: space-between; align-items: center;
836
1189
  margin-bottom: 6px;
837
1190
  }
838
- .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; }
839
1192
  .quant-meter-header .verdict-badge {
840
1193
  font-size: 12px; padding: 2px 8px; border-radius: 10px; font-weight: 600;
841
1194
  }
@@ -858,7 +1211,7 @@ select:focus { outline: none; border-color: var(--accent); }
858
1211
  display: grid; gap: 12px;
859
1212
  }
860
1213
  .quant-rank-col-header {
861
- 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);
862
1215
  margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--border);
863
1216
  }
864
1217
  .quant-rank-item {
@@ -873,7 +1226,7 @@ select:focus { outline: none; border-color: var(--accent); }
873
1226
  .quant-rank-pos {
874
1227
  display: inline-block; width: 22px; height: 22px; line-height: 22px;
875
1228
  text-align: center; border-radius: 50%; background: var(--bg-surface);
876
- 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);
877
1230
  margin-right: 8px;
878
1231
  }
879
1232
  .quant-rank-score { color: var(--text-muted); font-size: 11px; font-family: var(--mono); margin-top: 3px; }
@@ -916,7 +1269,7 @@ select:focus { outline: none; border-color: var(--accent); }
916
1269
  .cost-slider-value {
917
1270
  font-family: var(--mono);
918
1271
  font-size: 13px;
919
- color: var(--accent);
1272
+ color: var(--accent-text);
920
1273
  min-width: 70px;
921
1274
  text-align: right;
922
1275
  font-weight: 600;
@@ -993,7 +1346,7 @@ select:focus { outline: none; border-color: var(--accent); }
993
1346
  font-family: var(--mono);
994
1347
  font-size: 20px;
995
1348
  font-weight: 700;
996
- color: var(--accent);
1349
+ color: var(--accent-text);
997
1350
  }
998
1351
  .cost-summary-detail {
999
1352
  font-size: 11px;
@@ -1056,7 +1409,7 @@ select:focus { outline: none; border-color: var(--accent); }
1056
1409
  align-items: center;
1057
1410
  }
1058
1411
  .cost-strategy-total-label { font-size: 13px; font-weight: 600; color: var(--text); }
1059
- .cost-strategy-total-value { font-family: var(--mono); font-size: 18px; font-weight: 700; color: var(--accent); }
1412
+ .cost-strategy-total-value { font-family: var(--mono); font-size: 18px; font-weight: 700; color: var(--accent-text); }
1060
1413
  .cost-savings {
1061
1414
  font-size: 11px;
1062
1415
  color: var(--success);
@@ -1087,7 +1440,7 @@ select:focus { outline: none; border-color: var(--accent); }
1087
1440
  }
1088
1441
  .cost-table tr:hover { background: rgba(0, 237, 100, 0.03); }
1089
1442
  .cost-highlight {
1090
- color: var(--accent);
1443
+ color: var(--accent-text);
1091
1444
  font-weight: 600;
1092
1445
  }
1093
1446
  .cost-bar-cell { position: relative; }
@@ -1195,7 +1548,7 @@ select:focus { outline: none; border-color: var(--accent); }
1195
1548
  }
1196
1549
  .cost-modal h3 {
1197
1550
  font-size: 14px;
1198
- color: var(--accent);
1551
+ color: var(--accent-text);
1199
1552
  margin: 22px 0 10px;
1200
1553
  text-transform: uppercase;
1201
1554
  letter-spacing: 0.5px;
@@ -1212,7 +1565,7 @@ select:focus { outline: none; border-color: var(--accent); }
1212
1565
  padding: 2px 7px;
1213
1566
  border-radius: 4px;
1214
1567
  font-size: 12px;
1215
- color: var(--accent);
1568
+ color: var(--accent-text);
1216
1569
  font-family: var(--mono);
1217
1570
  }
1218
1571
  .cost-modal .formula {
@@ -1295,99 +1648,682 @@ select:focus { outline: none; border-color: var(--accent); }
1295
1648
  justify-content: space-between;
1296
1649
  font-size: 10px;
1297
1650
  color: var(--text-muted);
1298
- margin-top: 4px;
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;
2132
+ }
2133
+ .mm-result-item {
2134
+ display: flex;
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;
2151
+ color: var(--text-muted);
2152
+ min-width: 28px;
2153
+ text-align: center;
1299
2154
  }
1300
-
1301
- /* About page */
1302
- .about-container { max-width: 680px; margin: 0 auto; }
1303
- .about-header { display: flex; gap: 24px; align-items: center; margin-bottom: 24px; }
1304
- .about-avatar {
1305
- width: 120px; height: 120px;
1306
- border-radius: 50%;
1307
- border: 3px solid var(--accent);
1308
- 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);
1309
2162
  flex-shrink: 0;
1310
2163
  }
1311
- .about-name { font-size: 24px; font-weight: 700; color: var(--text); }
1312
- .about-role { font-size: 14px; color: var(--accent); margin-top: 4px; }
1313
- .about-links { display: flex; gap: 12px; margin-top: 8px; }
1314
- .about-links a {
1315
- color: var(--text-dim);
1316
- font-size: 13px;
1317
- text-decoration: none;
1318
- transition: color 0.2s;
2164
+ .mm-result-item .mm-result-content {
2165
+ flex: 1;
2166
+ min-width: 0;
1319
2167
  }
1320
- .about-links a:hover { color: var(--accent); }
1321
- .about-section { margin-bottom: 24px; }
1322
- .about-section-title {
1323
- font-size: 13px;
1324
- font-weight: 600;
1325
- 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);
1326
2178
  text-transform: uppercase;
1327
2179
  letter-spacing: 0.5px;
1328
- margin-bottom: 8px;
1329
- }
1330
- .about-text { font-size: 14px; line-height: 1.8; color: var(--text); }
1331
- .about-text a { color: var(--blue); text-decoration: none; }
1332
- .about-text a:hover { text-decoration: underline; }
1333
- .about-disclaimer {
1334
- background: rgba(255, 215, 61, 0.08);
1335
- border: 1px solid rgba(255, 215, 61, 0.2);
1336
- border-radius: var(--radius);
1337
- padding: 16px 20px;
1338
- margin-top: 24px;
1339
2180
  }
1340
- .about-disclaimer-title {
1341
- font-size: 13px;
2181
+ .mm-result-item .mm-result-score {
2182
+ font-family: var(--mono);
1342
2183
  font-weight: 600;
1343
- color: var(--warning);
1344
- margin-bottom: 6px;
2184
+ font-size: 14px;
2185
+ flex-shrink: 0;
1345
2186
  }
1346
- .about-disclaimer-text {
1347
- font-size: 13px;
1348
- 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);
1349
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);
1350
2216
  }
1351
2217
 
1352
2218
  @media (max-width: 768px) {
2219
+ .mm-grid { grid-template-columns: 1fr; }
2220
+ .mm-gallery-grid { grid-template-columns: repeat(3, 1fr); }
1353
2221
  .compare-grid, .search-results { grid-template-columns: 1fr; }
1354
- .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; }
1355
2226
  .main { padding: 16px; }
1356
- .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%; }
1357
2229
  }
1358
2230
  </style>
1359
2231
  </head>
1360
2232
  <body>
1361
2233
 
1362
- <!-- Nav -->
1363
- <nav class="nav">
1364
- <div class="nav-title">🧭 Voyage AI Playground</div>
1365
- <div class="nav-spacer"></div>
1366
- <div class="option-group">
1367
- <span class="option-label">Default Model</span>
1368
- <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>
1369
2275
  </div>
1370
- <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">🌙</button>
1371
- <div style="display:flex;align-items:center;gap:6px;">
1372
- <div class="status-dot" id="statusDot"></div>
1373
- <span class="status-label" id="statusLabel">Checking...</span>
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>
1374
2303
  </div>
1375
- </nav>
1376
-
1377
- <!-- Tabs -->
1378
- <div class="tab-bar">
1379
- <button class="tab-btn active" data-tab="embed">⚡ Embed</button>
1380
- <button class="tab-btn" data-tab="compare">⚖️ Compare</button>
1381
- <button class="tab-btn" data-tab="search">🔍 Search</button>
1382
- <button class="tab-btn" data-tab="benchmark">⏱ Benchmark</button>
1383
- <button class="tab-btn" data-tab="explore">📚 Explore</button>
1384
- <button class="tab-btn" data-tab="about">ℹ️ About</button>
1385
- </div>
1386
-
1387
- <div class="main">
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>
2317
+ </div>
2318
+ <div class="main">
1388
2319
 
1389
2320
  <!-- ========== EMBED TAB ========== -->
1390
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>
1391
2327
  <div class="card">
1392
2328
  <div class="card-title">Input Text</div>
1393
2329
  <textarea id="embedInput" rows="5" placeholder="Enter text to embed...">MongoDB Atlas provides powerful vector search capabilities for AI applications.</textarea>
@@ -1447,6 +2383,11 @@ select:focus { outline: none; border-color: var(--accent); }
1447
2383
 
1448
2384
  <!-- ========== COMPARE TAB ========== -->
1449
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>
1450
2391
  <div class="compare-grid">
1451
2392
  <div class="card">
1452
2393
  <div class="card-title">Text A</div>
@@ -1496,6 +2437,11 @@ select:focus { outline: none; border-color: var(--accent); }
1496
2437
 
1497
2438
  <!-- ========== SEARCH TAB ========== -->
1498
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>
1499
2445
  <div class="card">
1500
2446
  <div class="card-title">Query</div>
1501
2447
  <input type="text" id="searchQuery" placeholder="Enter your search query..." value="How do I build AI-powered search?">
@@ -1540,8 +2486,128 @@ Semantic search understands meaning beyond keyword matching</textarea>
1540
2486
  </div>
1541
2487
  </div>
1542
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
+
1543
2604
  <!-- ========== BENCHMARK TAB ========== -->
1544
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>
1545
2611
 
1546
2612
  <!-- Sub-panel switcher -->
1547
2613
  <div class="bench-panels">
@@ -1894,66 +2960,313 @@ Reranking models rescore initial search results to improve relevance ordering.</
1894
2960
  </div>
1895
2961
  </div>
1896
2962
 
1897
- <div class="about-section">
1898
- <div class="about-section-title">What You Can Do Here</div>
1899
- <div class="about-text">
1900
- <strong>⚡ Embed</strong> — Generate vector embeddings for any text<br>
1901
- <strong>⚖️ Compare</strong> — Measure similarity with cosine, dot product &amp; euclidean distance<br>
1902
- <strong>🔍 Search</strong> — Semantic search with optional reranking<br>
1903
- <strong>⏱ Benchmark</strong> — Compare model latency, ranking quality, and costs<br>
1904
- <strong>📚 Explore</strong> — Learn about embeddings, vector search, RAG, and more
2963
+ <div class="about-section">
2964
+ <div class="about-section-title">What You Can Do Here</div>
2965
+ <div class="about-text">
2966
+ <strong>⚡ Embed</strong> — Generate vector embeddings for any text<br>
2967
+ <strong>⚖️ Compare</strong> — Measure similarity with cosine, dot product &amp; euclidean distance<br>
2968
+ <strong>🔍 Search</strong> — Semantic search with optional reranking<br>
2969
+ <strong>⏱ Benchmark</strong> — Compare model latency, ranking quality, and costs<br>
2970
+ <strong>📚 Explore</strong> — Learn about embeddings, vector search, RAG, and more
2971
+ </div>
2972
+ </div>
2973
+
2974
+ <div class="about-disclaimer">
2975
+ <div class="about-disclaimer-title">⚠️ Community Tool Disclaimer</div>
2976
+ <div class="about-disclaimer-text">
2977
+ This tool is <strong>not</strong> an official product of MongoDB, Inc. or Voyage AI.
2978
+ It is independently built and maintained by Michael Lynn as a community resource.
2979
+ It is not supported, endorsed, or guaranteed by either company. Use at your own discretion.
2980
+ For official documentation, visit
2981
+ <a href="https://www.mongodb.com/docs/voyageai/" target="_blank" style="color:var(--warning);">mongodb.com/docs/voyageai</a>.
2982
+ </div>
2983
+ </div>
2984
+ </div>
2985
+
2986
+ <div style="text-align:center;margin-top:16px;font-size:12px;color:var(--text-muted);">
2987
+ Made with ☕ and curiosity · <a href="https://github.com/mrlynn/voyageai-cli" target="_blank" style="color:var(--text-dim);">Source on GitHub</a>
2988
+ </div>
2989
+ </div>
2990
+ </div>
2991
+
2992
+ <!-- ========== EXPLORE TAB ========== -->
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>
2999
+ <div style="margin-bottom:16px;">
3000
+ <input type="text" id="exploreSearch" placeholder="🔍 Search concepts..." oninput="filterExplore()" style="max-width:400px;">
3001
+ </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>
1905
3206
  </div>
1906
3207
  </div>
1907
-
1908
- <div class="about-disclaimer">
1909
- <div class="about-disclaimer-title">⚠️ Community Tool Disclaimer</div>
1910
- <div class="about-disclaimer-text">
1911
- This tool is <strong>not</strong> an official product of MongoDB, Inc. or Voyage AI.
1912
- It is independently built and maintained by Michael Lynn as a community resource.
1913
- It is not supported, endorsed, or guaranteed by either company. Use at your own discretion.
1914
- For official documentation, visit
1915
- <a href="https://www.mongodb.com/docs/voyageai/" target="_blank" style="color:var(--warning);">mongodb.com/docs/voyageai</a>.
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>
1916
3215
  </div>
1917
3216
  </div>
1918
3217
  </div>
1919
3218
 
1920
- <div style="text-align:center;margin-top:16px;font-size:12px;color:var(--text-muted);">
1921
- Made with ☕ and curiosity · <a href="https://github.com/mrlynn/voyageai-cli" target="_blank" style="color:var(--text-dim);">Source on GitHub</a>
3219
+ <div style="text-align:center;padding:8px 0;">
3220
+ <span class="settings-saved" id="settingsSavedMsg">✓ Saved</span>
1922
3221
  </div>
1923
- </div>
1924
- </div>
1925
3222
 
1926
- <!-- ========== EXPLORE TAB ========== -->
1927
- <div class="tab-panel" id="tab-explore">
1928
- <div style="margin-bottom:16px;">
1929
- <input type="text" id="exploreSearch" placeholder="🔍 Search concepts..." oninput="filterExplore()" style="max-width:400px;">
1930
3223
  </div>
1931
- <div class="explore-grid" id="exploreGrid"></div>
1932
3224
  </div>
1933
3225
 
1934
- <!-- Explore Concept Modal -->
1935
- <div class="explore-modal-overlay" id="exploreModal">
1936
- <div class="explore-modal">
1937
- <button class="explore-modal-close" id="exploreModalClose">&times;</button>
1938
- <div class="explore-modal-header">
1939
- <div class="explore-modal-icon" id="exploreModalIcon"></div>
1940
- <div>
1941
- <div class="explore-modal-title" id="exploreModalTitle"></div>
1942
- <div class="explore-modal-summary" id="exploreModalSummary"></div>
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>
1943
3257
  </div>
1944
3258
  </div>
1945
- <div class="explore-modal-body" id="exploreModalBody"></div>
1946
- <div class="explore-modal-actions" id="exploreModalActions"></div>
1947
3259
  </div>
1948
3260
  </div>
1949
3261
 
1950
- </div><!-- .main -->
1951
-
1952
3262
  <script>
1953
3263
  // Apply saved theme immediately to prevent flash
1954
3264
  (function() {
1955
3265
  var t = localStorage.getItem('vai-theme') || 'dark';
1956
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';
1957
3270
  })();
1958
3271
  </script>
1959
3272
  <script>
@@ -1968,21 +3281,28 @@ function initThemeToggle() {
1968
3281
 
1969
3282
  function applyTheme(theme) {
1970
3283
  current = theme;
3284
+ const logo = document.getElementById('sidebarLogo');
1971
3285
  if (theme === 'light') {
1972
3286
  document.documentElement.setAttribute('data-theme', 'light');
1973
3287
  toggle.textContent = '☀️';
1974
3288
  toggle.title = 'Switch to dark mode';
3289
+ if (logo) logo.src = '/icons/light/64.png';
1975
3290
  } else {
1976
3291
  document.documentElement.removeAttribute('data-theme');
1977
3292
  toggle.textContent = '🌙';
1978
3293
  toggle.title = 'Switch to light mode';
3294
+ if (logo) logo.src = '/icons/dark/64.png';
1979
3295
  }
1980
3296
  localStorage.setItem('vai-theme', theme);
1981
3297
  }
1982
3298
 
1983
3299
  applyTheme(current);
1984
3300
  toggle.addEventListener('click', () => {
1985
- applyTheme(current === 'dark' ? 'light' : 'dark');
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;
1986
3306
  });
1987
3307
  }
1988
3308
 
@@ -2021,6 +3341,8 @@ function switchTab(tab) {
2021
3341
  p.classList.toggle('active', p.id === 'tab-' + tab);
2022
3342
  });
2023
3343
  }
3344
+ // Expose globally so Electron main process can call it
3345
+ window.switchTab = switchTab;
2024
3346
 
2025
3347
  // ── Config ──
2026
3348
  async function loadConfig() {
@@ -2459,6 +3781,15 @@ const CONCEPT_META = {
2459
3781
  'batch-processing': { icon: '📦', tab: 'embed' },
2460
3782
  benchmarking: { icon: '⏱', tab: 'benchmark' },
2461
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' },
2462
3793
  };
2463
3794
 
2464
3795
  let exploreConcepts = {};
@@ -3501,6 +4832,890 @@ window.clearHistory = function() {
3501
4832
  renderHistory();
3502
4833
  };
3503
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
+
3504
5719
  // ── Patch init to include benchmark setup ──
3505
5720
  const _origInit = init;
3506
5721
  init = async function() {
@@ -3511,6 +5726,10 @@ init = async function() {
3511
5726
  populateQuantModelSelect();
3512
5727
  initCostCalculator();
3513
5728
  renderHistory();
5729
+ initSettings();
5730
+ initMultimodal();
5731
+ checkForAppUpdate();
5732
+ initOnboarding();
3514
5733
  };
3515
5734
 
3516
5735
  // ── Start ──