voyageai-cli 1.20.4 โ†’ 1.20.6

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 CHANGED
@@ -12,6 +12,28 @@ The fastest path from documents to semantic search. Chunk files, generate [Voyag
12
12
 
13
13
  ---
14
14
 
15
+ ## Why Voyage AI?
16
+
17
+ Voyage AI provides **state-of-the-art embedding models** with the best quality-to-cost ratio in the industry. Here's why developers choose Voyage AI:
18
+
19
+ | Advantage | What It Means |
20
+ |-----------|---------------|
21
+ | **๐ŸŽฏ #1 on MTEB** | Voyage-3 ranks first on retrieval benchmarks, outperforming OpenAI, Cohere, and other providers |
22
+ | **๐Ÿ’ฐ Up to 83% Cost Savings** | Asymmetric retrieval: embed docs with `voyage-3-lite`, query with `voyage-3-large` โ€” same quality, fraction of the cost |
23
+ | **๐Ÿ”— Shared Embedding Space** | All Voyage-3 models produce compatible embeddings โ€” mix and match for optimal cost-quality tradeoffs |
24
+ | **๐Ÿข Domain-Specific Models** | Specialized models for code, finance, law, and multilingual content that beat general-purpose alternatives |
25
+ | **โšก Two-Stage Retrieval** | Rerank-2 boosts search precision by re-scoring candidates with a powerful cross-encoder |
26
+
27
+ **Get started:**
28
+ ```bash
29
+ # Get a free API key at https://dash.voyageai.com
30
+ vai quickstart # Interactive tutorial โ€” zero to semantic search in 2 minutes
31
+ ```
32
+
33
+ **Learn more:** [Voyage AI Docs](https://docs.voyageai.com) ยท [Pricing](https://voyageai.com/pricing) ยท [Blog](https://blog.voyageai.com)
34
+
35
+ ---
36
+
15
37
  ## Three Ways to Use It
16
38
 
17
39
  <table>
@@ -44,6 +66,7 @@ MongoDB LeafyGreen design system<br/><br/>
44
66
 
45
67
  ## Table of Contents
46
68
 
69
+ - [Why Voyage AI?](#why-voyage-ai)
47
70
  - [Desktop App](#desktop-app)
48
71
  - [Web Playground](#web-playground)
49
72
  - [CLI โ€” Quick Start](#cli--quick-start)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voyageai-cli",
3
- "version": "1.20.4",
3
+ "version": "1.20.6",
4
4
  "description": "CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search",
5
5
  "bin": {
6
6
  "vai": "./src/cli.js"
package/src/cli.js CHANGED
@@ -28,6 +28,8 @@ const { registerPipeline } = require('./commands/pipeline');
28
28
  const { registerEval } = require('./commands/eval');
29
29
  const { registerApp } = require('./commands/app');
30
30
  const { registerAbout } = require('./commands/about');
31
+ const { register: registerDoctor } = require('./commands/doctor');
32
+ const { register: registerQuickstart } = require('./commands/quickstart');
31
33
  const { showBanner, showQuickStart, getVersion } = require('./lib/banner');
32
34
 
33
35
  const version = getVersion();
@@ -60,6 +62,8 @@ registerPipeline(program);
60
62
  registerEval(program);
61
63
  registerApp(program);
62
64
  registerAbout(program);
65
+ registerDoctor(program);
66
+ registerQuickstart(program);
63
67
 
64
68
  // Append disclaimer to all help output
65
69
  program.addHelpText('after', `
@@ -20,6 +20,7 @@ function registerAbout(program) {
20
20
  name: 'Michael Lynn',
21
21
  role: 'Principal Staff Developer Advocate, MongoDB',
22
22
  github: 'https://github.com/mrlynn',
23
+ vai_website: 'https://vai.mlynn.org',
23
24
  website: 'https://mlynn.org',
24
25
  },
25
26
  links: {
@@ -52,23 +53,58 @@ function registerAbout(program) {
52
53
  console.log(` into their applications โ€” right from the terminal.`);
53
54
  console.log('');
54
55
 
56
+ // Why Voyage AI?
57
+ console.log(` ${pc.bold(pc.green('Why Voyage AI?'))}`);
58
+ console.log(` Voyage AI provides state-of-the-art embedding models with`);
59
+ console.log(` the best quality-to-cost ratio in the industry.`);
60
+ console.log('');
61
+ console.log(` ${pc.cyan('๐ŸŽฏ SOTA Quality')}`);
62
+ console.log(` Voyage-3 ranks #1 on MTEB retrieval benchmarks, outperforming`);
63
+ console.log(` OpenAI, Cohere, and other providers on real-world tasks.`);
64
+ console.log('');
65
+ console.log(` ${pc.cyan('๐Ÿ’ฐ Best Value')}`);
66
+ console.log(` Up to 83% cost reduction with asymmetric retrieval: embed`);
67
+ console.log(` documents with voyage-3-lite, query with voyage-3-large.`);
68
+ console.log('');
69
+ console.log(` ${pc.cyan('๐Ÿ”— Shared Embedding Space')}`);
70
+ console.log(` All Voyage-3 models share the same embedding space โ€” mix`);
71
+ console.log(` and match models for optimal cost-quality tradeoffs.`);
72
+ console.log('');
73
+ console.log(` ${pc.cyan('๐Ÿข Domain-Specific Models')}`);
74
+ console.log(` Specialized models for code, finance, law, and multilingual`);
75
+ console.log(` content that outperform general-purpose alternatives.`);
76
+ console.log('');
77
+ console.log(` ${pc.cyan('โšก Reranking')}`);
78
+ console.log(` Two-stage retrieval with rerank-2 boosts precision by`);
79
+ console.log(` re-scoring candidates with a powerful cross-encoder.`);
80
+ console.log('');
81
+
55
82
  // Features
56
83
  console.log(` ${pc.bold('What You Can Do')}`);
84
+ console.log(` ${pc.cyan('vai quickstart')} Zero-to-search tutorial (start here!)`);
57
85
  console.log(` ${pc.cyan('vai embed')} Generate vector embeddings for text`);
58
86
  console.log(` ${pc.cyan('vai similarity')} Compare texts with cosine similarity`);
59
87
  console.log(` ${pc.cyan('vai rerank')} Rerank documents against a query`);
60
88
  console.log(` ${pc.cyan('vai search')} Vector search against Atlas collections`);
61
89
  console.log(` ${pc.cyan('vai store')} Embed and store documents in Atlas`);
62
90
  console.log(` ${pc.cyan('vai benchmark')} Compare model latency, ranking & costs`);
91
+ console.log(` ${pc.cyan('vai doctor')} Health-check your setup`);
63
92
  console.log(` ${pc.cyan('vai explain')} Learn about embeddings, vector search & more`);
64
93
  console.log(` ${pc.cyan('vai playground')} Launch interactive web playground`);
65
94
  console.log('');
66
95
 
67
- // Links
68
- console.log(` ${pc.bold('Links')}`);
96
+ // Voyage AI Links
97
+ console.log(` ${pc.bold('Voyage AI Resources')}`);
98
+ console.log(` ${pc.dim('Docs:')} https://docs.voyageai.com`);
99
+ console.log(` ${pc.dim('Dashboard:')} https://dash.voyageai.com`);
100
+ console.log(` ${pc.dim('Pricing:')} https://voyageai.com/pricing`);
101
+ console.log(` ${pc.dim('Blog:')} https://blog.voyageai.com`);
102
+ console.log('');
103
+
104
+ // Tool Links
105
+ console.log(` ${pc.bold('This Tool')}`);
69
106
  console.log(` ${pc.dim('npm:')} https://www.npmjs.com/package/voyageai-cli`);
70
107
  console.log(` ${pc.dim('GitHub:')} https://github.com/mrlynn/voyageai-cli`);
71
- console.log(` ${pc.dim('Docs:')} https://www.mongodb.com/docs/voyageai/`);
72
108
  console.log(` ${pc.dim('Author:')} https://mlynn.org`);
73
109
  console.log('');
74
110
 
@@ -0,0 +1,325 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const pc = require('picocolors');
7
+ const { getConfigValue } = require('../lib/config');
8
+ const { getApiBase } = require('../lib/api');
9
+
10
+ /**
11
+ * vai doctor โ€” Health check command
12
+ * Validates the entire setup chain: Node version, API key, MongoDB connectivity,
13
+ * peer dependencies, and configuration.
14
+ */
15
+
16
+ const CHECKS = {
17
+ node: { name: 'Node.js Version', required: true },
18
+ apiKey: { name: 'Voyage AI API Key', required: true },
19
+ apiConnection: { name: 'Voyage AI API Connection', required: true },
20
+ mongodb: { name: 'MongoDB Connection', required: false },
21
+ pdfParse: { name: 'PDF Support (pdf-parse)', required: false },
22
+ config: { name: 'Configuration Files', required: false },
23
+ };
24
+
25
+ function checkMark(ok) {
26
+ return ok ? pc.green('โœ“') : pc.red('โœ—');
27
+ }
28
+
29
+ function warnMark() {
30
+ return pc.yellow('โš ');
31
+ }
32
+
33
+ async function checkNodeVersion() {
34
+ const version = process.version;
35
+ const major = parseInt(version.slice(1).split('.')[0], 10);
36
+ const minVersion = 18;
37
+ const ok = major >= minVersion;
38
+
39
+ return {
40
+ ok,
41
+ message: ok
42
+ ? `${version} (meets minimum v${minVersion})`
43
+ : `${version} โ€” ${pc.red(`requires v${minVersion}+`)}`,
44
+ hint: ok ? null : 'Upgrade Node.js: https://nodejs.org/',
45
+ };
46
+ }
47
+
48
+ async function checkApiKey() {
49
+ const key = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
50
+ const masked = key ? `${key.slice(0, 8)}...${key.slice(-4)}` : null;
51
+
52
+ if (!key) {
53
+ return {
54
+ ok: false,
55
+ message: 'Not configured',
56
+ hint: 'Set via: vai config set api-key YOUR_KEY\n or: export VOYAGE_API_KEY=YOUR_KEY\n Get a key: https://dash.voyageai.com/api-keys',
57
+ };
58
+ }
59
+
60
+ // Check key format (Voyage keys typically start with pa- or similar)
61
+ const validFormat = key.length > 20;
62
+
63
+ return {
64
+ ok: true,
65
+ message: masked,
66
+ hint: validFormat ? null : 'Key format looks unusual โ€” verify at dash.voyageai.com',
67
+ };
68
+ }
69
+
70
+ async function checkApiConnection() {
71
+ const key = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
72
+ if (!key) {
73
+ return {
74
+ ok: false,
75
+ message: 'Skipped (no API key)',
76
+ hint: null,
77
+ };
78
+ }
79
+
80
+ const baseUrl = getApiBase();
81
+
82
+ try {
83
+ const https = require('https');
84
+ const url = require('url');
85
+
86
+ const parsed = new URL(`${baseUrl}/embeddings`);
87
+
88
+ const result = await new Promise((resolve, reject) => {
89
+ const req = https.request({
90
+ hostname: parsed.hostname,
91
+ port: parsed.port || 443,
92
+ path: parsed.pathname,
93
+ method: 'POST',
94
+ headers: {
95
+ 'Authorization': `Bearer ${key}`,
96
+ 'Content-Type': 'application/json',
97
+ },
98
+ timeout: 10000,
99
+ }, (res) => {
100
+ let data = '';
101
+ res.on('data', chunk => data += chunk);
102
+ res.on('end', () => {
103
+ // 200 = success, 400 = bad request (but server reachable), 401 = bad key
104
+ if (res.statusCode === 401) {
105
+ resolve({ ok: false, status: 401, message: 'Invalid API key' });
106
+ } else if (res.statusCode === 400) {
107
+ // Bad request means server is reachable, key is valid
108
+ resolve({ ok: true, status: 400, message: 'Connected' });
109
+ } else if (res.statusCode === 200) {
110
+ resolve({ ok: true, status: 200, message: 'Connected' });
111
+ } else {
112
+ resolve({ ok: false, status: res.statusCode, message: `HTTP ${res.statusCode}` });
113
+ }
114
+ });
115
+ });
116
+
117
+ req.on('error', (err) => reject(err));
118
+ req.on('timeout', () => {
119
+ req.destroy();
120
+ reject(new Error('Connection timeout'));
121
+ });
122
+
123
+ // Send minimal request body
124
+ req.write(JSON.stringify({ model: 'voyage-3-lite', input: ['test'] }));
125
+ req.end();
126
+ });
127
+
128
+ return {
129
+ ok: result.ok,
130
+ message: result.ok ? `${result.message} (${baseUrl})` : result.message,
131
+ hint: result.ok ? null :
132
+ result.status === 401 ? 'API key is invalid โ€” get a new one at dash.voyageai.com' :
133
+ 'Check your network connection and API base URL',
134
+ };
135
+ } catch (err) {
136
+ return {
137
+ ok: false,
138
+ message: `Connection failed: ${err.message}`,
139
+ hint: 'Check your network connection or firewall settings',
140
+ };
141
+ }
142
+ }
143
+
144
+ async function checkMongoDB() {
145
+ const uri = process.env.MONGODB_URI || getConfigValue('mongoUri');
146
+
147
+ if (!uri) {
148
+ return {
149
+ ok: null, // null = not configured (optional)
150
+ message: 'Not configured (optional)',
151
+ hint: 'Set via: vai config set mongo-uri YOUR_URI\n Required for vai store, vai search, vai query',
152
+ };
153
+ }
154
+
155
+ try {
156
+ const { MongoClient } = require('mongodb');
157
+ const client = new MongoClient(uri, {
158
+ serverSelectionTimeoutMS: 5000,
159
+ connectTimeoutMS: 5000,
160
+ });
161
+
162
+ await client.connect();
163
+ await client.db().command({ ping: 1 });
164
+ await client.close();
165
+
166
+ // Mask the URI
167
+ const masked = uri.replace(/:\/\/[^@]+@/, '://***@').replace(/\?.*$/, '');
168
+
169
+ return {
170
+ ok: true,
171
+ message: `Connected (${masked})`,
172
+ hint: null,
173
+ };
174
+ } catch (err) {
175
+ return {
176
+ ok: false,
177
+ message: `Connection failed: ${err.message}`,
178
+ hint: 'Verify your connection string and network access',
179
+ };
180
+ }
181
+ }
182
+
183
+ async function checkPdfParse() {
184
+ try {
185
+ require.resolve('pdf-parse');
186
+ return {
187
+ ok: true,
188
+ message: 'Installed',
189
+ hint: null,
190
+ };
191
+ } catch {
192
+ return {
193
+ ok: null, // null = not installed (optional)
194
+ message: 'Not installed (optional)',
195
+ hint: 'Install for PDF support: npm install pdf-parse',
196
+ };
197
+ }
198
+ }
199
+
200
+ async function checkConfig() {
201
+ const homeConfig = path.join(os.homedir(), '.vai', 'config.json');
202
+ const projectConfig = path.join(process.cwd(), '.vai.json');
203
+
204
+ const results = [];
205
+
206
+ // Check home config
207
+ if (fs.existsSync(homeConfig)) {
208
+ try {
209
+ const stat = fs.statSync(homeConfig);
210
+ const mode = (stat.mode & 0o777).toString(8);
211
+ const secure = mode === '600';
212
+ results.push(`~/.vai/config.json: ${secure ? 'OK' : pc.yellow(`permissions ${mode} (should be 600)`)}`);
213
+ } catch {
214
+ results.push('~/.vai/config.json: exists');
215
+ }
216
+ }
217
+
218
+ // Check project config
219
+ if (fs.existsSync(projectConfig)) {
220
+ try {
221
+ JSON.parse(fs.readFileSync(projectConfig, 'utf8'));
222
+ results.push('.vai.json: OK');
223
+ } catch {
224
+ results.push(`.vai.json: ${pc.red('invalid JSON')}`);
225
+ }
226
+ }
227
+
228
+ if (results.length === 0) {
229
+ return {
230
+ ok: null,
231
+ message: 'No config files found',
232
+ hint: 'Run: vai init (for project config) or vai config set (for user config)',
233
+ };
234
+ }
235
+
236
+ return {
237
+ ok: true,
238
+ message: results.join(', '),
239
+ hint: null,
240
+ };
241
+ }
242
+
243
+ async function runDoctor(options = {}) {
244
+ const { json, verbose } = options;
245
+
246
+ console.log(pc.bold('\n๐Ÿฉบ Voyage AI CLI Health Check\n'));
247
+
248
+ const results = {};
249
+ let hasError = false;
250
+ let hasWarning = false;
251
+
252
+ // Run all checks
253
+ const checks = [
254
+ { key: 'node', fn: checkNodeVersion },
255
+ { key: 'apiKey', fn: checkApiKey },
256
+ { key: 'apiConnection', fn: checkApiConnection },
257
+ { key: 'mongodb', fn: checkMongoDB },
258
+ { key: 'pdfParse', fn: checkPdfParse },
259
+ { key: 'config', fn: checkConfig },
260
+ ];
261
+
262
+ for (const { key, fn } of checks) {
263
+ const check = CHECKS[key];
264
+ const result = await fn();
265
+ results[key] = result;
266
+
267
+ // Determine status icon
268
+ let icon;
269
+ if (result.ok === true) {
270
+ icon = checkMark(true);
271
+ } else if (result.ok === false) {
272
+ icon = checkMark(false);
273
+ if (check.required) hasError = true;
274
+ else hasWarning = true;
275
+ } else {
276
+ icon = warnMark();
277
+ hasWarning = true;
278
+ }
279
+
280
+ // Print result
281
+ console.log(` ${icon} ${pc.bold(check.name)}: ${result.message}`);
282
+
283
+ if (result.hint && (verbose || result.ok === false)) {
284
+ console.log(` ${pc.dim(result.hint)}`);
285
+ }
286
+ }
287
+
288
+ // Summary
289
+ console.log('');
290
+ if (hasError) {
291
+ console.log(pc.red(' โœ— Some required checks failed. Fix the issues above to use vai.\n'));
292
+ } else if (hasWarning) {
293
+ console.log(pc.yellow(' โš  Some optional features are not configured.\n'));
294
+ } else {
295
+ console.log(pc.green(' โœ“ All checks passed. vai is ready to use!\n'));
296
+ }
297
+
298
+ // Suggest next steps
299
+ if (!hasError) {
300
+ console.log(pc.dim(' Next steps:'));
301
+ console.log(pc.dim(' vai demo โ€” Interactive walkthrough'));
302
+ console.log(pc.dim(' vai quickstart โ€” Zero-to-search tutorial'));
303
+ console.log(pc.dim(' vai explain โ€” Learn key concepts\n'));
304
+ }
305
+
306
+ if (json) {
307
+ console.log(JSON.stringify(results, null, 2));
308
+ }
309
+
310
+ return hasError ? 1 : 0;
311
+ }
312
+
313
+ function register(program) {
314
+ program
315
+ .command('doctor')
316
+ .description('Run health checks on your vai setup')
317
+ .option('--json', 'Output results as JSON')
318
+ .option('-v, --verbose', 'Show hints for all checks')
319
+ .action(async (options) => {
320
+ const exitCode = await runDoctor(options);
321
+ if (exitCode !== 0) process.exit(exitCode);
322
+ });
323
+ }
324
+
325
+ module.exports = { register, runDoctor };
@@ -0,0 +1,203 @@
1
+ 'use strict';
2
+
3
+ const pc = require('picocolors');
4
+ const readline = require('readline');
5
+ const { getConfigValue } = require('../lib/config');
6
+ const { embed } = require('../lib/api');
7
+
8
+ /**
9
+ * vai quickstart โ€” Zero-to-search interactive tutorial
10
+ * Gets developers from nothing to their first semantic search in minutes.
11
+ */
12
+
13
+ const SAMPLE_DOCS = [
14
+ "MongoDB Atlas Vector Search enables semantic search on your data using machine learning embeddings.",
15
+ "Voyage AI provides state-of-the-art embedding models with the best quality-to-cost ratio.",
16
+ "RAG (Retrieval-Augmented Generation) combines vector search with LLMs for accurate AI responses.",
17
+ "The shared embedding space in Voyage 4 models lets you embed queries and documents with different models.",
18
+ "Reranking improves search precision by re-scoring results with a cross-encoder model.",
19
+ ];
20
+
21
+ function sleep(ms) {
22
+ return new Promise(resolve => setTimeout(resolve, ms));
23
+ }
24
+
25
+ function createPrompt() {
26
+ const rl = readline.createInterface({
27
+ input: process.stdin,
28
+ output: process.stdout,
29
+ });
30
+
31
+ return {
32
+ ask: (question) => new Promise((resolve) => {
33
+ rl.question(question, (answer) => resolve(answer));
34
+ }),
35
+ close: () => rl.close(),
36
+ };
37
+ }
38
+
39
+ async function runQuickstart(options = {}) {
40
+ const { skip } = options;
41
+
42
+ console.log(pc.bold('\n๐Ÿš€ Voyage AI CLI Quickstart\n'));
43
+ console.log(pc.dim('This tutorial will get you from zero to semantic search in 2 minutes.\n'));
44
+
45
+ // Check for API key
46
+ const apiKey = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
47
+ if (!apiKey) {
48
+ console.log(pc.red('โœ— No API key found.\n'));
49
+ console.log(' First, get a free API key:');
50
+ console.log(pc.cyan(' โ†’ https://dash.voyageai.com/api-keys\n'));
51
+ console.log(' Then configure it:');
52
+ console.log(pc.cyan(' โ†’ vai config set api-key YOUR_KEY\n'));
53
+ console.log(' Or set the environment variable:');
54
+ console.log(pc.cyan(' โ†’ export VOYAGE_API_KEY=YOUR_KEY\n'));
55
+ return 1;
56
+ }
57
+
58
+ console.log(pc.green('โœ“') + ' API key configured\n');
59
+
60
+ // Step 1: Explain what we're doing
61
+ console.log(pc.bold('Step 1: Understanding Embeddings'));
62
+ console.log(pc.dim('โ”€'.repeat(40)));
63
+ console.log(`
64
+ Embeddings turn text into ${pc.cyan('vectors')} (arrays of numbers) that capture
65
+ meaning. Similar texts have similar vectors, enabling semantic search.
66
+
67
+ We'll embed these ${SAMPLE_DOCS.length} sample documents:
68
+ `);
69
+
70
+ SAMPLE_DOCS.forEach((doc, i) => {
71
+ const preview = doc.length > 70 ? doc.slice(0, 70) + '...' : doc;
72
+ console.log(` ${i + 1}. ${pc.dim(preview)}`);
73
+ });
74
+ console.log('');
75
+
76
+ if (!skip) {
77
+ const prompt = createPrompt();
78
+ await prompt.ask(pc.dim('Press Enter to continue...'));
79
+ prompt.close();
80
+ }
81
+
82
+ // Step 2: Embed the documents
83
+ console.log(pc.bold('\nStep 2: Embedding Documents'));
84
+ console.log(pc.dim('โ”€'.repeat(40)));
85
+ console.log(`
86
+ Running: ${pc.cyan('vai embed --model voyage-3-lite')}
87
+ `);
88
+
89
+ let embeddings;
90
+ try {
91
+ process.stdout.write(' Embedding documents... ');
92
+ const result = await embed({
93
+ texts: SAMPLE_DOCS,
94
+ model: 'voyage-3-lite',
95
+ inputType: 'document',
96
+ });
97
+ embeddings = result.embeddings;
98
+ console.log(pc.green('โœ“'));
99
+ console.log(`
100
+ ${pc.green('โœ“')} Created ${embeddings.length} embeddings
101
+ ${pc.dim(` Dimensions: ${embeddings[0].length}`)}
102
+ ${pc.dim(` Model: voyage-3-lite`)}
103
+ `);
104
+ } catch (err) {
105
+ console.log(pc.red('โœ—'));
106
+ console.log(pc.red(`\n Error: ${err.message}\n`));
107
+ console.log(' Check your API key with: vai doctor\n');
108
+ return 1;
109
+ }
110
+
111
+ // Step 3: Search
112
+ console.log(pc.bold('Step 3: Semantic Search'));
113
+ console.log(pc.dim('โ”€'.repeat(40)));
114
+
115
+ const query = 'How do I improve search accuracy?';
116
+ console.log(`
117
+ Now let's search! We'll embed a query and find the most similar documents.
118
+
119
+ Query: "${pc.cyan(query)}"
120
+ `);
121
+
122
+ try {
123
+ process.stdout.write(' Embedding query... ');
124
+ const queryResult = await embed({
125
+ texts: [query],
126
+ model: 'voyage-3-lite',
127
+ inputType: 'query',
128
+ });
129
+ const queryEmbedding = queryResult.embeddings[0];
130
+ console.log(pc.green('โœ“'));
131
+
132
+ // Calculate similarities
133
+ const similarities = embeddings.map((docEmb, i) => {
134
+ const dotProduct = docEmb.reduce((sum, val, j) => sum + val * queryEmbedding[j], 0);
135
+ const normA = Math.sqrt(docEmb.reduce((sum, val) => sum + val * val, 0));
136
+ const normB = Math.sqrt(queryEmbedding.reduce((sum, val) => sum + val * val, 0));
137
+ return {
138
+ index: i,
139
+ score: dotProduct / (normA * normB),
140
+ text: SAMPLE_DOCS[i],
141
+ };
142
+ });
143
+
144
+ // Sort by similarity
145
+ similarities.sort((a, b) => b.score - a.score);
146
+
147
+ console.log(`
148
+ ${pc.bold('Results (ranked by similarity):')}
149
+ `);
150
+
151
+ similarities.forEach((item, rank) => {
152
+ const scoreColor = item.score > 0.5 ? pc.green : item.score > 0.3 ? pc.yellow : pc.dim;
153
+ const preview = item.text.length > 60 ? item.text.slice(0, 60) + '...' : item.text;
154
+ console.log(` ${rank + 1}. ${scoreColor(item.score.toFixed(3))} ${preview}`);
155
+ });
156
+
157
+ } catch (err) {
158
+ console.log(pc.red('โœ—'));
159
+ console.log(pc.red(`\n Error: ${err.message}\n`));
160
+ return 1;
161
+ }
162
+
163
+ // Success!
164
+ console.log(pc.bold('\nโœจ Congratulations!'));
165
+ console.log(pc.dim('โ”€'.repeat(40)));
166
+ console.log(`
167
+ You just performed your first semantic search with Voyage AI!
168
+
169
+ The top result about ${pc.cyan('reranking')} is relevant because it discusses
170
+ improving search ${pc.cyan('precision')} โ€” even though it doesn't contain the
171
+ exact words "improve" or "accuracy". That's the power of embeddings!
172
+
173
+ ${pc.bold('Why Voyage AI?')}
174
+ โ€ข ${pc.cyan('Best quality-to-cost ratio')} โ€” SOTA quality at lower prices
175
+ โ€ข ${pc.cyan('Shared embedding space')} โ€” mix models for cost optimization
176
+ โ€ข ${pc.cyan('Domain-specific models')} โ€” code, finance, law, multilingual
177
+ โ€ข ${pc.cyan('Reranking')} โ€” boost precision with two-stage retrieval
178
+
179
+ ${pc.bold('Next Steps:')}
180
+ ${pc.cyan('vai explain embeddings')} โ€” Learn more about how embeddings work
181
+ ${pc.cyan('vai explain reranking')} โ€” Understand two-stage retrieval
182
+ ${pc.cyan('vai demo')} โ€” Full interactive walkthrough
183
+ ${pc.cyan('vai pipeline')} โ€” Build a complete RAG pipeline
184
+ ${pc.cyan('vai playground')} โ€” Visual exploration in your browser
185
+
186
+ ${pc.dim('Docs: https://docs.voyageai.com | Dashboard: https://dash.voyageai.com')}
187
+ `);
188
+
189
+ return 0;
190
+ }
191
+
192
+ function register(program) {
193
+ program
194
+ .command('quickstart')
195
+ .description('Zero-to-search tutorial โ€” learn semantic search in 2 minutes')
196
+ .option('--skip', 'Skip interactive prompts')
197
+ .action(async (options) => {
198
+ const exitCode = await runQuickstart(options);
199
+ process.exit(exitCode);
200
+ });
201
+ }
202
+
203
+ module.exports = { register, runQuickstart };
@@ -1,6 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const pc = require('picocolors');
3
+ // Gracefully handle missing picocolors (e.g., in packaged Electron app)
4
+ let pc;
5
+ try {
6
+ pc = require('picocolors');
7
+ } catch {
8
+ // Fallback: no-op functions that return input unchanged
9
+ pc = new Proxy({}, { get: () => (s) => s });
10
+ }
4
11
 
5
12
  /**
6
13
  * Map of concept key โ†’ explanation object.
@@ -871,6 +878,147 @@ const concepts = {
871
878
  ],
872
879
  },
873
880
 
881
+ 'provider-comparison': {
882
+ title: 'Voyage AI vs OpenAI vs Anthropic',
883
+ summary: 'Understanding the differences between AI providers for embeddings and search',
884
+ content: [
885
+ `Developers often ask: "Should I use Voyage AI, OpenAI, or Anthropic for my vector`,
886
+ `search application?" The answer depends on what you're building โ€” these companies`,
887
+ `serve ${pc.cyan('fundamentally different purposes')}.`,
888
+ ``,
889
+ `${pc.bold('TL;DR:')}`,
890
+ ` ${pc.cyan('Voyage AI')} โ†’ ${pc.bold('Embeddings & Reranking')} (specialized, best-in-class)`,
891
+ ` ${pc.cyan('OpenAI')} โ†’ ${pc.bold('Embeddings + Generation')} (general-purpose, convenient)`,
892
+ ` ${pc.cyan('Anthropic')} โ†’ ${pc.bold('Generation only')} (Claude LLM, no embedding API)`,
893
+ ``,
894
+ `${pc.bold('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”')}`,
895
+ ``,
896
+ `${pc.bold(pc.cyan('VOYAGE AI'))} โ€” The Embedding Specialist`,
897
+ ``,
898
+ `Voyage AI is ${pc.cyan('100% focused on embeddings and reranking')}. That's it. No chatbots,`,
899
+ `no image generation โ€” just the best vector representations for retrieval.`,
900
+ ``,
901
+ `${pc.dim('Models:')}`,
902
+ ` โ€ข ${pc.cyan('voyage-4-large')} 71.41 RTEB score (SOTA), MoE architecture`,
903
+ ` โ€ข ${pc.cyan('voyage-4')} 70.07 RTEB, balanced quality/cost`,
904
+ ` โ€ข ${pc.cyan('voyage-4-lite')} 68.10 RTEB, lowest cost`,
905
+ ` โ€ข ${pc.cyan('voyage-code-3')} Optimized for code retrieval`,
906
+ ` โ€ข ${pc.cyan('voyage-finance-2')} Financial documents`,
907
+ ` โ€ข ${pc.cyan('voyage-law-2')} Legal text`,
908
+ ` โ€ข ${pc.cyan('voyage-multimodal-3.5')} Images + text in same space`,
909
+ ` โ€ข ${pc.cyan('rerank-2.5')} Best-in-class reranking`,
910
+ ``,
911
+ `${pc.dim('Unique strengths:')}`,
912
+ ` โ€ข ${pc.cyan('Shared embedding space')} โ€” mix models freely (embed docs with -large, query with -lite)`,
913
+ ` โ€ข ${pc.cyan('Matryoshka dimensions')} โ€” truncate 1024โ†’256 dims without re-embedding`,
914
+ ` โ€ข ${pc.cyan('Domain-specific models')} โ€” code, finance, law tuned for their domains`,
915
+ ` โ€ข ${pc.cyan('No modality gap')} โ€” multimodal model uses unified backbone, not CLIP`,
916
+ ` โ€ข ${pc.cyan('Quantization support')} โ€” int8, binary output for 4-32ร— storage savings`,
917
+ ``,
918
+ `${pc.dim('Pricing:')} $0.02-$0.12 per million tokens (embedding), $2/M (reranking)`,
919
+ ``,
920
+ `${pc.bold('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”')}`,
921
+ ``,
922
+ `${pc.bold(pc.cyan('OPENAI'))} โ€” The General-Purpose Platform`,
923
+ ``,
924
+ `OpenAI offers embeddings as ${pc.cyan('one product among many')}. They're convenient if you're`,
925
+ `already using GPT, but embeddings aren't their core focus.`,
926
+ ``,
927
+ `${pc.dim('Embedding models:')}`,
928
+ ` โ€ข ${pc.cyan('text-embedding-3-large')} 62.57 RTEB, 3072 dims, $0.13/M tokens`,
929
+ ` โ€ข ${pc.cyan('text-embedding-3-small')} 62.26 RTEB, 1536 dims, $0.02/M tokens`,
930
+ ` โ€ข ${pc.cyan('text-embedding-ada-002')} Legacy, 1536 dims, $0.10/M tokens`,
931
+ ``,
932
+ `${pc.dim('Strengths:')}`,
933
+ ` โ€ข ${pc.cyan('One platform')} โ€” embeddings + GPT generation in same account`,
934
+ ` โ€ข ${pc.cyan('Familiar API')} โ€” if you use ChatGPT API, embeddings are similar`,
935
+ ` โ€ข ${pc.cyan('Broad ecosystem')} โ€” many tutorials and integrations assume OpenAI`,
936
+ ``,
937
+ `${pc.dim('Limitations:')}`,
938
+ ` โ€ข ${pc.cyan('Lower quality')} โ€” voyage-4-large beats text-embedding-3-large by 14% on RTEB`,
939
+ ` โ€ข ${pc.cyan('No reranking')} โ€” you need a separate provider for two-stage retrieval`,
940
+ ` โ€ข ${pc.cyan('No domain models')} โ€” same model for code, legal, finance (suboptimal)`,
941
+ ` โ€ข ${pc.cyan('No shared space')} โ€” can't mix models or truncate dimensions freely`,
942
+ ` โ€ข ${pc.cyan('No multimodal')} โ€” text-only (CLIP is separate and limited)`,
943
+ ``,
944
+ `${pc.bold('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”')}`,
945
+ ``,
946
+ `${pc.bold(pc.cyan('ANTHROPIC'))} โ€” The LLM Company (No Embeddings!)`,
947
+ ``,
948
+ `${pc.yellow('Important:')} Anthropic ${pc.bold('does not offer embedding models')}. They make Claude,`,
949
+ `an LLM for text generation, reasoning, and analysis โ€” not vectorization.`,
950
+ ``,
951
+ `${pc.dim('What Anthropic offers:')}`,
952
+ ` โ€ข ${pc.cyan('Claude 3.5 Sonnet/Haiku')} โ€” Text generation, coding, analysis`,
953
+ ` โ€ข ${pc.cyan('Claude 3 Opus')} โ€” Most capable reasoning model`,
954
+ ` โ€ข ${pc.cyan('Tool use & function calling')} โ€” Structured outputs`,
955
+ ``,
956
+ `${pc.dim('What Anthropic does NOT offer:')}`,
957
+ ` โ€ข ${pc.red('โŒ Embedding models')}`,
958
+ ` โ€ข ${pc.red('โŒ Reranking models')}`,
959
+ ` โ€ข ${pc.red('โŒ Vector search APIs')}`,
960
+ ``,
961
+ `${pc.dim('How to use Claude with embeddings:')}`,
962
+ `Claude is the ${pc.cyan('"G" in RAG')} โ€” the generation model. You still need an embedding`,
963
+ `provider (Voyage AI, OpenAI, Cohere) for the retrieval step:`,
964
+ ` 1. Embed your documents with Voyage AI โ†’ store in vector DB`,
965
+ ` 2. Embed user query โ†’ search vector DB โ†’ retrieve context`,
966
+ ` 3. Pass context + query to Claude โ†’ generate response`,
967
+ ``,
968
+ `${pc.bold('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”')}`,
969
+ ``,
970
+ `${pc.bold('HEAD-TO-HEAD: Voyage AI vs OpenAI Embeddings')}`,
971
+ ``,
972
+ `${pc.dim(' Voyage AI OpenAI')}`,
973
+ `${pc.dim('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€')}`,
974
+ `Best RTEB score ${pc.cyan('71.41')} (v4-large) ${pc.dim('62.57')} (v3-large)`,
975
+ `Reranking ${pc.cyan('Yes')} (rerank-2.5) ${pc.dim('No')}`,
976
+ `Domain models ${pc.cyan('Yes')} (code/law/fin) ${pc.dim('No')}`,
977
+ `Multimodal ${pc.cyan('Yes')} (unified) ${pc.dim('Limited')} (CLIP)`,
978
+ `Shared embedding space ${pc.cyan('Yes')} ${pc.dim('No')}`,
979
+ `Flexible dimensions ${pc.cyan('Yes')} (Matryoshka) ${pc.dim('No')}`,
980
+ `Quantized output ${pc.cyan('Yes')} (int8/binary) ${pc.dim('No')}`,
981
+ `MongoDB integration ${pc.cyan('Native')} (Atlas) ${pc.dim('Manual')}`,
982
+ ``,
983
+ `${pc.bold('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”')}`,
984
+ ``,
985
+ `${pc.bold('WHEN TO USE EACH:')}`,
986
+ ``,
987
+ `${pc.cyan('Choose Voyage AI when:')}`,
988
+ ` โ€ข Retrieval quality is critical (RAG, search, recommendations)`,
989
+ ` โ€ข You need domain-specific embeddings (code, legal, finance)`,
990
+ ` โ€ข You want two-stage retrieval (embed โ†’ search โ†’ rerank)`,
991
+ ` โ€ข You're using MongoDB Atlas Vector Search`,
992
+ ` โ€ข You need multimodal search (images + text)`,
993
+ ` โ€ข Storage cost matters (quantization, flexible dims)`,
994
+ ``,
995
+ `${pc.cyan('Choose OpenAI embeddings when:')}`,
996
+ ` โ€ข You're already deep in the OpenAI ecosystem`,
997
+ ` โ€ข Convenience matters more than retrieval quality`,
998
+ ` โ€ข You don't need reranking or domain models`,
999
+ ` โ€ข Your use case is simple (not mission-critical search)`,
1000
+ ``,
1001
+ `${pc.cyan('Choose Anthropic (Claude) when:')}`,
1002
+ ` โ€ข You need ${pc.bold('generation')}, not embeddings`,
1003
+ ` โ€ข You want the best reasoning/coding LLM`,
1004
+ ` โ€ข You're building the "G" in RAG (pair with Voyage for "R")`,
1005
+ ``,
1006
+ `${pc.bold('The winning combination:')} Use ${pc.cyan('Voyage AI for retrieval')} (embeddings +`,
1007
+ `reranking) and ${pc.cyan('Claude for generation')}. Best of both worlds.`,
1008
+ ].join('\n'),
1009
+ links: [
1010
+ 'https://www.mongodb.com/docs/voyageai/models/',
1011
+ 'https://platform.openai.com/docs/guides/embeddings',
1012
+ 'https://docs.anthropic.com/',
1013
+ 'https://blog.voyageai.com/2026/01/15/voyage-4-model-family/',
1014
+ ],
1015
+ tryIt: [
1016
+ 'vai models --wide',
1017
+ 'vai benchmark embed --models voyage-4-large,voyage-4,voyage-4-lite',
1018
+ 'vai explain rteb-benchmarks',
1019
+ ],
1020
+ },
1021
+
874
1022
  'rerank-eval': {
875
1023
  title: 'Reranking Evaluation โ€” nDCG, Recall, MRR for Rerankers',
876
1024
  summary: 'Measure how well a reranker surfaces relevant documents',
@@ -1017,6 +1165,24 @@ const aliases = {
1017
1165
  recall: 'rerank-eval',
1018
1166
  mrr: 'rerank-eval',
1019
1167
  'eval-rerank': 'rerank-eval',
1168
+ // Provider comparison aliases
1169
+ 'provider-comparison': 'provider-comparison',
1170
+ providers: 'provider-comparison',
1171
+ 'voyage-vs-openai': 'provider-comparison',
1172
+ 'openai-vs-voyage': 'provider-comparison',
1173
+ 'voyage-vs-anthropic': 'provider-comparison',
1174
+ 'anthropic-vs-voyage': 'provider-comparison',
1175
+ 'openai-vs-anthropic': 'provider-comparison',
1176
+ 'anthropic-vs-openai': 'provider-comparison',
1177
+ 'voyage-openai-anthropic': 'provider-comparison',
1178
+ differences: 'provider-comparison',
1179
+ 'which-provider': 'provider-comparison',
1180
+ 'compare-providers': 'provider-comparison',
1181
+ 'embedding-providers': 'provider-comparison',
1182
+ 'vs-openai': 'provider-comparison',
1183
+ 'vs-anthropic': 'provider-comparison',
1184
+ competitors: 'provider-comparison',
1185
+ alternatives: 'provider-comparison',
1020
1186
  };
1021
1187
 
1022
1188
  /**
@@ -2309,19 +2309,19 @@ select:focus { outline: none; border-color: var(--accent); }
2309
2309
  <button class="sidebar-settings-btn" data-tab="settings" title="Settings"><svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-config"/></svg></button>
2310
2310
  </div>
2311
2311
  <nav class="sidebar-nav">
2312
- <div class="sidebar-nav-group">
2313
- <div class="sidebar-nav-label">Tools</div>
2314
- <button class="tab-btn active" data-tab="embed"><span class="tab-btn-icon"><svg><use href="#lg-lightning"/></svg></span><span>Embed</span></button>
2315
- <button class="tab-btn" data-tab="compare"><span class="tab-btn-icon"><svg><use href="#lg-arrows"/></svg></span><span>Compare</span></button>
2316
- <button class="tab-btn" data-tab="search"><span class="tab-btn-icon"><svg><use href="#lg-search"/></svg></span><span>Search</span></button>
2317
- <button class="tab-btn" data-tab="multimodal"><span class="tab-btn-icon"><svg><use href="#lg-image"/></svg></span><span>Multimodal</span></button>
2312
+ <div class="sidebar-nav-group" role="tablist" aria-label="Tools">
2313
+ <div class="sidebar-nav-label" id="nav-tools-label">Tools</div>
2314
+ <button class="tab-btn active" data-tab="embed" role="tab" aria-selected="true" aria-controls="tab-embed" id="tab-btn-embed"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-lightning"/></svg></span><span>Embed</span></button>
2315
+ <button class="tab-btn" data-tab="compare" role="tab" aria-selected="false" aria-controls="tab-compare" id="tab-btn-compare"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-arrows"/></svg></span><span>Compare</span></button>
2316
+ <button class="tab-btn" data-tab="search" role="tab" aria-selected="false" aria-controls="tab-search" id="tab-btn-search"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-search"/></svg></span><span>Search</span></button>
2317
+ <button class="tab-btn" data-tab="multimodal" role="tab" aria-selected="false" aria-controls="tab-multimodal" id="tab-btn-multimodal"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-image"/></svg></span><span>Multimodal</span></button>
2318
2318
  </div>
2319
2319
  <div class="sidebar-nav-divider"></div>
2320
- <div class="sidebar-nav-group">
2321
- <div class="sidebar-nav-label">Resources</div>
2322
- <button class="tab-btn" data-tab="benchmark"><span class="tab-btn-icon"><svg><use href="#lg-gauge"/></svg></span><span>Benchmark</span></button>
2323
- <button class="tab-btn" data-tab="explore"><span class="tab-btn-icon"><svg><use href="#lg-bulb"/></svg></span><span>Explore</span></button>
2324
- <button class="tab-btn" data-tab="about"><span class="tab-btn-icon"><svg><use href="#lg-info"/></svg></span><span>About</span></button>
2320
+ <div class="sidebar-nav-group" role="tablist" aria-label="Resources">
2321
+ <div class="sidebar-nav-label" id="nav-resources-label">Resources</div>
2322
+ <button class="tab-btn" data-tab="benchmark" role="tab" aria-selected="false" aria-controls="tab-benchmark" id="tab-btn-benchmark"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-gauge"/></svg></span><span>Benchmark</span></button>
2323
+ <button class="tab-btn" data-tab="explore" role="tab" aria-selected="false" aria-controls="tab-explore" id="tab-btn-explore"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-bulb"/></svg></span><span>Explore</span></button>
2324
+ <button class="tab-btn" data-tab="about" role="tab" aria-selected="false" aria-controls="tab-about" id="tab-btn-about"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-info"/></svg></span><span>About</span></button>
2325
2325
  </div>
2326
2326
  </nav>
2327
2327
  <div class="sidebar-footer">
@@ -2356,7 +2356,7 @@ select:focus { outline: none; border-color: var(--accent); }
2356
2356
  <div class="main">
2357
2357
 
2358
2358
  <!-- ========== EMBED TAB ========== -->
2359
- <div class="tab-panel active" id="tab-embed">
2359
+ <div class="tab-panel active" id="tab-embed" role="tabpanel" aria-labelledby="tab-btn-embed" tabindex="0">
2360
2360
  <div class="page-header">
2361
2361
  <h2 class="page-header-title">Embed</h2>
2362
2362
  <p class="page-header-subtitle">Generate vector embeddings for text</p>
@@ -2364,7 +2364,7 @@ select:focus { outline: none; border-color: var(--accent); }
2364
2364
  </div>
2365
2365
  <div class="card">
2366
2366
  <div class="card-title">Input Text</div>
2367
- <textarea id="embedInput" rows="5" placeholder="Enter text to embed...">MongoDB Atlas provides powerful vector search capabilities for AI applications.</textarea>
2367
+ <textarea id="embedInput" rows="5" placeholder="Enter text to embed..." aria-label="Text to embed">MongoDB Atlas provides powerful vector search capabilities for AI applications.</textarea>
2368
2368
  </div>
2369
2369
 
2370
2370
  <div class="options-row">
@@ -2420,7 +2420,7 @@ select:focus { outline: none; border-color: var(--accent); }
2420
2420
  </div>
2421
2421
 
2422
2422
  <!-- ========== COMPARE TAB ========== -->
2423
- <div class="tab-panel" id="tab-compare">
2423
+ <div class="tab-panel" id="tab-compare" role="tabpanel" aria-labelledby="tab-btn-compare" tabindex="0">
2424
2424
  <div class="page-header">
2425
2425
  <h2 class="page-header-title">Compare</h2>
2426
2426
  <p class="page-header-subtitle">Visualize similarity between text pairs</p>
@@ -2474,7 +2474,7 @@ select:focus { outline: none; border-color: var(--accent); }
2474
2474
  </div>
2475
2475
 
2476
2476
  <!-- ========== SEARCH TAB ========== -->
2477
- <div class="tab-panel" id="tab-search">
2477
+ <div class="tab-panel" id="tab-search" role="tabpanel" aria-labelledby="tab-btn-search" tabindex="0">
2478
2478
  <div class="page-header">
2479
2479
  <h2 class="page-header-title">Rerank</h2>
2480
2480
  <p class="page-header-subtitle">Re-order documents by relevance to a query</p>
@@ -2482,7 +2482,7 @@ select:focus { outline: none; border-color: var(--accent); }
2482
2482
  </div>
2483
2483
  <div class="card">
2484
2484
  <div class="card-title">Query</div>
2485
- <input type="text" id="searchQuery" placeholder="Enter your search query..." value="How do I build AI-powered search?">
2485
+ <input type="text" id="searchQuery" placeholder="Enter your search query..." value="How do I build AI-powered search?" aria-label="Search query">
2486
2486
  </div>
2487
2487
 
2488
2488
  <div class="card">
@@ -2525,7 +2525,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
2525
2525
  </div>
2526
2526
 
2527
2527
  <!-- ========== MULTIMODAL TAB ========== -->
2528
- <div class="tab-panel" id="tab-multimodal">
2528
+ <div class="tab-panel" id="tab-multimodal" role="tabpanel" aria-labelledby="tab-btn-multimodal" tabindex="0">
2529
2529
  <div class="page-header">
2530
2530
  <h2 class="page-header-title">Multimodal</h2>
2531
2531
  <p class="page-header-subtitle">Compare images and text in the same vector space</p>
@@ -2640,7 +2640,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
2640
2640
  </div>
2641
2641
 
2642
2642
  <!-- ========== BENCHMARK TAB ========== -->
2643
- <div class="tab-panel" id="tab-benchmark">
2643
+ <div class="tab-panel" id="tab-benchmark" role="tabpanel" aria-labelledby="tab-btn-benchmark" tabindex="0">
2644
2644
  <div class="page-header">
2645
2645
  <h2 class="page-header-title">Benchmark</h2>
2646
2646
  <p class="page-header-subtitle">Compare model speed, cost, and quality</p>
@@ -2651,6 +2651,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
2651
2651
  <div class="bench-panels">
2652
2652
  <button class="bench-panel-btn active" data-bench="latency">โšก Latency</button>
2653
2653
  <button class="bench-panel-btn" data-bench="ranking">๐Ÿ† Ranking</button>
2654
+ <button class="bench-panel-btn" data-bench="competitors">โš”๏ธ vs Competitors</button>
2654
2655
  <button class="bench-panel-btn" data-bench="quantization">โš—๏ธ Quantization</button>
2655
2656
  <button class="bench-panel-btn" data-bench="cost">๐Ÿ’ฐ Cost</button>
2656
2657
  <button class="bench-panel-btn" data-bench="history">๐Ÿ“Š History</button>
@@ -2744,6 +2745,168 @@ Reranking models rescore initial search results to improve relevance ordering.</
2744
2745
  </div>
2745
2746
  </div>
2746
2747
 
2748
+ <!-- โ”€โ”€ Competitors Panel โ”€โ”€ -->
2749
+ <div class="bench-view" id="bench-competitors">
2750
+ <div class="card">
2751
+ <div class="card-title">Why Voyage AI? โ€” Competitive Comparison</div>
2752
+ <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
2753
+ See how Voyage AI compares to other embedding providers on quality, cost, and features.
2754
+ </p>
2755
+ </div>
2756
+
2757
+ <!-- MTEB Retrieval Benchmark -->
2758
+ <div class="card" style="margin-top:16px;">
2759
+ <div class="card-title">๐Ÿ† MTEB Retrieval Benchmark (nDCG@10)</div>
2760
+ <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
2761
+ Industry-standard benchmark for retrieval quality. Higher is better.
2762
+ </p>
2763
+ <div id="mtebChart" style="display:flex;flex-direction:column;gap:8px;">
2764
+ <!-- Voyage AI -->
2765
+ <div style="display:flex;align-items:center;gap:12px;">
2766
+ <span style="min-width:140px;font-weight:600;color:var(--accent);">Voyage-3</span>
2767
+ <div style="flex:1;height:24px;background:var(--surface);border-radius:4px;overflow:hidden;">
2768
+ <div style="width:67.4%;height:100%;background:var(--accent);border-radius:4px;"></div>
2769
+ </div>
2770
+ <span style="min-width:50px;text-align:right;font-family:var(--mono);color:var(--accent);font-weight:600;">67.4</span>
2771
+ </div>
2772
+ <div style="display:flex;align-items:center;gap:12px;">
2773
+ <span style="min-width:140px;font-weight:600;color:var(--accent);">Voyage-3-lite</span>
2774
+ <div style="flex:1;height:24px;background:var(--surface);border-radius:4px;overflow:hidden;">
2775
+ <div style="width:64.3%;height:100%;background:var(--accent);opacity:0.7;border-radius:4px;"></div>
2776
+ </div>
2777
+ <span style="min-width:50px;text-align:right;font-family:var(--mono);color:var(--accent);">64.3</span>
2778
+ </div>
2779
+ <!-- Competitors -->
2780
+ <div style="display:flex;align-items:center;gap:12px;">
2781
+ <span style="min-width:140px;color:var(--text-dim);">OpenAI text-3-large</span>
2782
+ <div style="flex:1;height:24px;background:var(--surface);border-radius:4px;overflow:hidden;">
2783
+ <div style="width:64.6%;height:100%;background:#666;border-radius:4px;"></div>
2784
+ </div>
2785
+ <span style="min-width:50px;text-align:right;font-family:var(--mono);color:var(--text-dim);">64.6</span>
2786
+ </div>
2787
+ <div style="display:flex;align-items:center;gap:12px;">
2788
+ <span style="min-width:140px;color:var(--text-dim);">OpenAI text-3-small</span>
2789
+ <div style="flex:1;height:24px;background:var(--surface);border-radius:4px;overflow:hidden;">
2790
+ <div style="width:62.3%;height:100%;background:#666;border-radius:4px;"></div>
2791
+ </div>
2792
+ <span style="min-width:50px;text-align:right;font-family:var(--mono);color:var(--text-dim);">62.3</span>
2793
+ </div>
2794
+ <div style="display:flex;align-items:center;gap:12px;">
2795
+ <span style="min-width:140px;color:var(--text-dim);">Cohere embed-v3</span>
2796
+ <div style="flex:1;height:24px;background:var(--surface);border-radius:4px;overflow:hidden;">
2797
+ <div style="width:64.1%;height:100%;background:#666;border-radius:4px;"></div>
2798
+ </div>
2799
+ <span style="min-width:50px;text-align:right;font-family:var(--mono);color:var(--text-dim);">64.1</span>
2800
+ </div>
2801
+ </div>
2802
+ <p style="color:var(--text-dim);font-size:11px;margin-top:12px;">
2803
+ Source: <a href="https://huggingface.co/spaces/mteb/leaderboard" target="_blank" style="color:var(--accent);">MTEB Leaderboard</a> (Retrieval Average, Jan 2025)
2804
+ </p>
2805
+ </div>
2806
+
2807
+ <!-- Cost Comparison -->
2808
+ <div class="card" style="margin-top:16px;">
2809
+ <div class="card-title">๐Ÿ’ฐ Cost per Million Tokens</div>
2810
+ <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
2811
+ Voyage AI offers significant cost savings, especially with asymmetric retrieval strategies.
2812
+ </p>
2813
+ <table style="width:100%;border-collapse:collapse;font-size:13px;">
2814
+ <thead>
2815
+ <tr style="border-bottom:1px solid var(--border);">
2816
+ <th style="text-align:left;padding:8px 12px;color:var(--text-dim);">Provider / Model</th>
2817
+ <th style="text-align:right;padding:8px 12px;color:var(--text-dim);">Price</th>
2818
+ <th style="text-align:right;padding:8px 12px;color:var(--text-dim);">vs Voyage-3</th>
2819
+ </tr>
2820
+ </thead>
2821
+ <tbody>
2822
+ <tr style="border-bottom:1px solid var(--border);">
2823
+ <td style="padding:8px 12px;font-weight:600;color:var(--accent);">Voyage-3-lite</td>
2824
+ <td style="text-align:right;padding:8px 12px;font-family:var(--mono);color:var(--accent);">$0.02</td>
2825
+ <td style="text-align:right;padding:8px 12px;color:var(--success);font-weight:600;">83% cheaper</td>
2826
+ </tr>
2827
+ <tr style="border-bottom:1px solid var(--border);">
2828
+ <td style="padding:8px 12px;font-weight:600;color:var(--accent);">Voyage-3</td>
2829
+ <td style="text-align:right;padding:8px 12px;font-family:var(--mono);color:var(--accent);">$0.06</td>
2830
+ <td style="text-align:right;padding:8px 12px;color:var(--text-dim);">baseline</td>
2831
+ </tr>
2832
+ <tr style="border-bottom:1px solid var(--border);">
2833
+ <td style="padding:8px 12px;color:var(--text-dim);">OpenAI text-3-large</td>
2834
+ <td style="text-align:right;padding:8px 12px;font-family:var(--mono);">$0.13</td>
2835
+ <td style="text-align:right;padding:8px 12px;color:var(--error);">2.2x more</td>
2836
+ </tr>
2837
+ <tr style="border-bottom:1px solid var(--border);">
2838
+ <td style="padding:8px 12px;color:var(--text-dim);">OpenAI text-3-small</td>
2839
+ <td style="text-align:right;padding:8px 12px;font-family:var(--mono);">$0.02</td>
2840
+ <td style="text-align:right;padding:8px 12px;color:var(--text-dim);">same</td>
2841
+ </tr>
2842
+ <tr>
2843
+ <td style="padding:8px 12px;color:var(--text-dim);">Cohere embed-v3</td>
2844
+ <td style="text-align:right;padding:8px 12px;font-family:var(--mono);">$0.10</td>
2845
+ <td style="text-align:right;padding:8px 12px;color:var(--error);">1.7x more</td>
2846
+ </tr>
2847
+ </tbody>
2848
+ </table>
2849
+ <div style="margin-top:16px;padding:12px;background:var(--accent-glow);border-radius:8px;border-left:3px solid var(--accent);">
2850
+ <strong style="color:var(--accent);">๐Ÿ’ก Pro Tip: Asymmetric Retrieval</strong>
2851
+ <p style="margin:8px 0 0 0;font-size:13px;color:var(--text);">
2852
+ Embed your document corpus with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-3-lite</code> ($0.02/M) and
2853
+ query with <code style="background:var(--surface);padding:2px 6px;border-radius:4px;">voyage-3</code> ($0.06/M).
2854
+ Because all Voyage-3 models share the same embedding space, you get top-tier retrieval quality at a fraction of the cost.
2855
+ </p>
2856
+ </div>
2857
+ </div>
2858
+
2859
+ <!-- Key Differentiators -->
2860
+ <div class="card" style="margin-top:16px;">
2861
+ <div class="card-title">๐Ÿš€ Voyage AI Advantages</div>
2862
+ <div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:16px;margin-top:12px;">
2863
+ <div style="padding:16px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
2864
+ <div style="font-size:20px;margin-bottom:8px;">๐Ÿ”—</div>
2865
+ <div style="font-weight:600;margin-bottom:4px;">Shared Embedding Space</div>
2866
+ <div style="font-size:13px;color:var(--text-dim);">
2867
+ All Voyage-3 models produce compatible embeddings. Mix <code>voyage-3-large</code> for queries
2868
+ with <code>voyage-3-lite</code> for documents โ€” they work together seamlessly.
2869
+ </div>
2870
+ </div>
2871
+ <div style="padding:16px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
2872
+ <div style="font-size:20px;margin-bottom:8px;">๐Ÿข</div>
2873
+ <div style="font-weight:600;margin-bottom:4px;">Domain-Specific Models</div>
2874
+ <div style="font-size:13px;color:var(--text-dim);">
2875
+ Specialized models for code (<code>voyage-code-3</code>), finance, law, and multilingual content
2876
+ that outperform general-purpose alternatives.
2877
+ </div>
2878
+ </div>
2879
+ <div style="padding:16px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
2880
+ <div style="font-size:20px;margin-bottom:8px;">โšก</div>
2881
+ <div style="font-weight:600;margin-bottom:4px;">Two-Stage Retrieval</div>
2882
+ <div style="font-size:13px;color:var(--text-dim);">
2883
+ Combine embedding search with <code>rerank-2</code> to dramatically boost precision.
2884
+ The cross-encoder re-scores candidates for better relevance.
2885
+ </div>
2886
+ </div>
2887
+ <div style="padding:16px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
2888
+ <div style="font-size:20px;margin-bottom:8px;">๐Ÿ–ผ๏ธ</div>
2889
+ <div style="font-weight:600;margin-bottom:4px;">Multimodal Embeddings</div>
2890
+ <div style="font-size:13px;color:var(--text-dim);">
2891
+ <code>voyage-multimodal-3</code> embeds both text and images in the same space,
2892
+ enabling cross-modal search (find images with text queries and vice versa).
2893
+ </div>
2894
+ </div>
2895
+ </div>
2896
+ </div>
2897
+
2898
+ <!-- CTA -->
2899
+ <div class="card" style="margin-top:16px;text-align:center;padding:24px;">
2900
+ <div style="font-size:18px;font-weight:600;margin-bottom:8px;">Ready to try Voyage AI?</div>
2901
+ <p style="color:var(--text-dim);margin-bottom:16px;">Get a free API key and start building in minutes.</p>
2902
+ <a href="https://dash.voyageai.com" target="_blank" class="btn" style="display:inline-block;text-decoration:none;">
2903
+ Get API Key โ†’
2904
+ </a>
2905
+ <span style="margin:0 12px;color:var(--text-dim);">or</span>
2906
+ <a href="https://docs.voyageai.com" target="_blank" style="color:var(--accent);">Read the Docs</a>
2907
+ </div>
2908
+ </div>
2909
+
2747
2910
  <!-- โ”€โ”€ Quantization Panel โ”€โ”€ -->
2748
2911
  <div class="bench-view" id="bench-quantization">
2749
2912
  <div class="card">
@@ -2961,7 +3124,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
2961
3124
  </div>
2962
3125
 
2963
3126
  <!-- ========== ABOUT TAB ========== -->
2964
- <div class="tab-panel" id="tab-about">
3127
+ <div class="tab-panel" id="tab-about" role="tabpanel" aria-labelledby="tab-btn-about" tabindex="0">
2965
3128
  <div class="about-container">
2966
3129
  <div class="card">
2967
3130
  <div class="about-header">
@@ -3052,24 +3215,24 @@ Reranking models rescore initial search results to improve relevance ordering.</
3052
3215
  </div>
3053
3216
 
3054
3217
  <!-- ========== EXPLORE TAB ========== -->
3055
- <div class="tab-panel" id="tab-explore">
3218
+ <div class="tab-panel" id="tab-explore" role="tabpanel" aria-labelledby="tab-btn-explore" tabindex="0">
3056
3219
  <div class="page-header">
3057
3220
  <h2 class="page-header-title">Explore</h2>
3058
3221
  <p class="page-header-subtitle">Learn embedding and vector search concepts</p>
3059
3222
  <p class="page-header-hint">Browse interactive explanations of key topics โ€” from cosine similarity to quantization to RAG pipelines.</p>
3060
3223
  </div>
3061
3224
  <div style="margin-bottom:16px;">
3062
- <input type="text" id="exploreSearch" placeholder="๐Ÿ” Search concepts..." oninput="filterExplore()" style="max-width:400px;">
3225
+ <input type="text" id="exploreSearch" placeholder="๐Ÿ” Search concepts..." oninput="filterExplore()" style="max-width:400px;" aria-label="Search concepts">
3063
3226
  </div>
3064
3227
  <div class="explore-grid" id="exploreGrid"></div>
3065
3228
  </div>
3066
3229
 
3067
3230
  <!-- Explore Concept Modal -->
3068
- <div class="explore-modal-overlay" id="exploreModal">
3231
+ <div class="explore-modal-overlay" id="exploreModal" role="dialog" aria-modal="true" aria-labelledby="exploreModalTitle" aria-describedby="exploreModalSummary">
3069
3232
  <div class="explore-modal">
3070
- <button class="explore-modal-close" id="exploreModalClose">&times;</button>
3233
+ <button class="explore-modal-close" id="exploreModalClose" aria-label="Close modal">&times;</button>
3071
3234
  <div class="explore-modal-header">
3072
- <div class="explore-modal-icon" id="exploreModalIcon"></div>
3235
+ <div class="explore-modal-icon" id="exploreModalIcon" aria-hidden="true"></div>
3073
3236
  <div>
3074
3237
  <div class="explore-modal-title" id="exploreModalTitle"></div>
3075
3238
  <div class="explore-modal-summary" id="exploreModalSummary"></div>
@@ -3450,10 +3613,38 @@ async function init() {
3450
3613
 
3451
3614
  // โ”€โ”€ Tabs โ”€โ”€
3452
3615
  function setupTabs() {
3453
- document.querySelectorAll('.tab-btn').forEach(btn => {
3616
+ const tabBtns = document.querySelectorAll('.tab-btn');
3617
+ const tabList = Array.from(tabBtns);
3618
+
3619
+ tabBtns.forEach(btn => {
3454
3620
  btn.addEventListener('click', () => {
3455
3621
  switchTab(btn.dataset.tab);
3456
3622
  });
3623
+
3624
+ // Keyboard navigation for accessibility
3625
+ btn.addEventListener('keydown', (e) => {
3626
+ const currentIndex = tabList.indexOf(btn);
3627
+ let nextIndex = -1;
3628
+
3629
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
3630
+ e.preventDefault();
3631
+ nextIndex = (currentIndex + 1) % tabList.length;
3632
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
3633
+ e.preventDefault();
3634
+ nextIndex = (currentIndex - 1 + tabList.length) % tabList.length;
3635
+ } else if (e.key === 'Home') {
3636
+ e.preventDefault();
3637
+ nextIndex = 0;
3638
+ } else if (e.key === 'End') {
3639
+ e.preventDefault();
3640
+ nextIndex = tabList.length - 1;
3641
+ }
3642
+
3643
+ if (nextIndex >= 0) {
3644
+ tabList[nextIndex].focus();
3645
+ switchTab(tabList[nextIndex].dataset.tab);
3646
+ }
3647
+ });
3457
3648
  });
3458
3649
 
3459
3650
  // Settings cog in header
@@ -3467,10 +3658,17 @@ function setupTabs() {
3467
3658
 
3468
3659
  function switchTab(tab) {
3469
3660
  document.querySelectorAll('.tab-btn').forEach(b => {
3470
- b.classList.toggle('active', b.dataset.tab === tab);
3661
+ const isActive = b.dataset.tab === tab;
3662
+ b.classList.toggle('active', isActive);
3663
+ // Update ARIA attributes for accessibility
3664
+ b.setAttribute('aria-selected', isActive ? 'true' : 'false');
3665
+ b.setAttribute('tabindex', isActive ? '0' : '-1');
3471
3666
  });
3472
3667
  document.querySelectorAll('.tab-panel').forEach(p => {
3473
- p.classList.toggle('active', p.id === 'tab-' + tab);
3668
+ const isActive = p.id === 'tab-' + tab;
3669
+ p.classList.toggle('active', isActive);
3670
+ // Hide inactive panels from screen readers
3671
+ p.setAttribute('aria-hidden', isActive ? 'false' : 'true');
3474
3672
  });
3475
3673
  // Sync settings cog highlight
3476
3674
  const settingsBtn = document.querySelector('.sidebar-settings-btn');
@@ -3940,6 +4138,7 @@ const CONCEPT_META = {
3940
4138
  'cross-modal-search': { icon: '๐Ÿ”€', tab: 'multimodal' },
3941
4139
  'modality-gap': { icon: '๐Ÿ•ณ๏ธ', tab: 'multimodal' },
3942
4140
  'multimodal-rag': { icon: '๐Ÿ“„', tab: 'multimodal' },
4141
+ 'provider-comparison': { icon: 'โš–๏ธ', tab: 'explore' },
3943
4142
  };
3944
4143
 
3945
4144
  let exploreConcepts = {};
@@ -3981,13 +4180,26 @@ function buildExploreCards() {
3981
4180
  const modal = document.getElementById('exploreModal');
3982
4181
  document.getElementById('exploreModalClose').addEventListener('click', closeExploreModal);
3983
4182
  modal.addEventListener('click', (e) => { if (e.target === modal) closeExploreModal(); });
4183
+
4184
+ // Close modal on Escape key for accessibility
4185
+ document.addEventListener('keydown', (e) => {
4186
+ if (e.key === 'Escape' && modal.classList.contains('open')) {
4187
+ closeExploreModal();
4188
+ }
4189
+ });
3984
4190
  }
3985
4191
 
4192
+ // Store previously focused element for modal focus management
4193
+ let exploreModalPreviousFocus = null;
4194
+
3986
4195
  function openExploreModal(key) {
3987
4196
  const concept = exploreConcepts[key];
3988
4197
  if (!concept) return;
3989
4198
  const meta = CONCEPT_META[key] || { icon: '๐Ÿ“š', tab: 'embed' };
3990
4199
 
4200
+ // Save the currently focused element to restore when modal closes
4201
+ exploreModalPreviousFocus = document.activeElement;
4202
+
3991
4203
  document.getElementById('exploreModalIcon').textContent = meta.icon;
3992
4204
  document.getElementById('exploreModalTitle').textContent = concept.title;
3993
4205
  document.getElementById('exploreModalSummary').textContent = concept.summary;
@@ -4021,11 +4233,23 @@ function openExploreModal(key) {
4021
4233
  if (meta.tab) switchTab(meta.tab);
4022
4234
  });
4023
4235
 
4024
- document.getElementById('exploreModal').classList.add('open');
4236
+ const modal = document.getElementById('exploreModal');
4237
+ modal.classList.add('open');
4238
+
4239
+ // Focus the close button for accessibility
4240
+ setTimeout(() => {
4241
+ document.getElementById('exploreModalClose').focus();
4242
+ }, 100);
4025
4243
  }
4026
4244
 
4027
4245
  function closeExploreModal() {
4028
4246
  document.getElementById('exploreModal').classList.remove('open');
4247
+
4248
+ // Restore focus to the previously focused element
4249
+ if (exploreModalPreviousFocus && typeof exploreModalPreviousFocus.focus === 'function') {
4250
+ exploreModalPreviousFocus.focus();
4251
+ exploreModalPreviousFocus = null;
4252
+ }
4029
4253
  }
4030
4254
 
4031
4255
  window.tryTopic = function(key) {
@@ -5935,13 +6159,12 @@ init = async function() {
5935
6159
  initMultimodal();
5936
6160
  checkForAppUpdate();
5937
6161
  initOnboarding();
5938
- };
6162
+ };
5939
6163
 
5940
6164
  // โ”€โ”€ Start โ”€โ”€
5941
6165
  init();
5942
-
5943
- // โ”€โ”€ ๐Ÿ•น๏ธ Vector Space Invaders (Easter Egg) โ”€โ”€
5944
- // Trigger: Konami code (โ†‘โ†‘โ†“โ†“โ†โ†’โ†โ†’BA) or 7 rapid clicks on sidebar logo
6166
+
6167
+ // โ”€โ”€ Vector Space Invaders (Easter Egg) โ”€โ”€
5945
6168
  (function initEasterEgg() {
5946
6169
  const KONAMI = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
5947
6170
  let konamiIdx = 0;