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