voyageai-cli 1.33.3 → 1.33.5

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
@@ -1,7 +1,7 @@
1
1
  # voyageai-cli
2
2
 
3
3
  <p align="center">
4
- <img src="https://raw.githubusercontent.com/mrlynn/voyageai-cli/main/demo-readme.gif" alt="voyageai-cli demo" width="800" />
4
+ <img src="https://raw.githubusercontent.com/mrlynn/voyageai-cli/main/cli-quickstart.gif" alt="voyageai-cli demo" width="800" />
5
5
  </p>
6
6
 
7
7
  [![CI](https://github.com/mrlynn/voyageai-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/mrlynn/voyageai-cli/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/voyageai-cli.svg)](https://www.npmjs.com/package/voyageai-cli) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Node.js](https://img.shields.io/node/v/voyageai-cli.svg)](https://nodejs.org) [![GitHub release](https://img.shields.io/github/v/release/mrlynn/voyageai-cli?label=Desktop%20App)](https://github.com/mrlynn/voyageai-cli/releases)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voyageai-cli",
3
- "version": "1.33.3",
3
+ "version": "1.33.5",
4
4
  "description": "CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search",
5
5
  "_comment": "This package contains the CLI + web playground. The electron/ directory is excluded via .npmignore and distributed via GitHub Releases.",
6
6
  "bin": {
@@ -38,6 +38,9 @@
38
38
  "test": "node --test test/**/*.test.js",
39
39
  "prepare": "git config core.hooksPath scripts/hooks 2>/dev/null || true",
40
40
  "reset": "node scripts/reset.js",
41
+ "demos:list": "node scripts/run-demos.js list",
42
+ "demos:run": "node scripts/run-demos.js",
43
+ "demos:run:all": "node scripts/run-demos.js all",
41
44
  "version": "node scripts/sync-nano-version.js && git add src/nano/nano-bridge.py",
42
45
  "release": "./scripts/release.sh",
43
46
  "app:install": "cd electron && npm install",
@@ -275,7 +275,7 @@ function registerIngest(program) {
275
275
  batches: totalBatches,
276
276
  batchSize: opts.batchSize,
277
277
  estimatedTokens: estimated,
278
- model: opts.model,
278
+ model: ingestModel,
279
279
  textField: textKey,
280
280
  }, null, 2));
281
281
  } else {
@@ -285,7 +285,7 @@ function registerIngest(program) {
285
285
  console.log(ui.label('Documents', String(documents.length)));
286
286
  console.log(ui.label('Batches', `${totalBatches} (batch size: ${opts.batchSize})`));
287
287
  console.log(ui.label('Est. tokens', `~${estimated.toLocaleString()}`));
288
- console.log(ui.label('Model', opts.model));
288
+ console.log(ui.label('Model', ingestModel));
289
289
  console.log(ui.label('Text field', textKey));
290
290
  console.log(ui.label('Target', `${opts.db}.${opts.collection}`));
291
291
  console.log(ui.label('Embed field', opts.field));
@@ -404,7 +404,7 @@ function registerIngest(program) {
404
404
  collection: opts.collection,
405
405
  batches: totalBatches,
406
406
  tokens: totalTokens,
407
- model: opts.model,
407
+ model: ingestModel,
408
408
  durationSeconds: parseFloat(duration),
409
409
  docsPerSecond: parseFloat(rate),
410
410
  };
@@ -420,7 +420,7 @@ function registerIngest(program) {
420
420
  }
421
421
  console.log(ui.label('Batches', String(totalBatches)));
422
422
  console.log(ui.label('Tokens', totalTokens.toLocaleString()));
423
- console.log(ui.label('Model', opts.model));
423
+ console.log(ui.label('Model', ingestModel));
424
424
  console.log(ui.label('Duration', `${duration}s`));
425
425
  console.log(ui.label('Rate', `${rate} docs/sec`));
426
426
  if (errors.length > 0) {
@@ -128,6 +128,24 @@ function registerInit(program) {
128
128
  const filePath = saveProject(config);
129
129
  const relPath = path.relative(process.cwd(), filePath);
130
130
 
131
+ // Warn if nano selected but bridge is not set up
132
+ if (config.model === 'voyage-4-nano') {
133
+ try {
134
+ const { checkVenv, checkModel } = require('../nano/nano-health');
135
+ const venv = checkVenv();
136
+ const model = checkModel();
137
+ if (!venv.ok || !model.ok) {
138
+ console.log('');
139
+ console.log(pc.yellow('⚠ voyage-4-nano requires local setup before use.'));
140
+ if (!venv.ok) console.log(pc.dim(' • Python venv: not found'));
141
+ if (!model.ok) console.log(pc.dim(' • Model weights: not downloaded'));
142
+ console.log(pc.yellow(' Run: vai nano setup'));
143
+ }
144
+ } catch (_) {
145
+ // nano module not available — skip check
146
+ }
147
+ }
148
+
131
149
  if (!opts.quiet) {
132
150
  console.log('');
133
151
  console.log(ui.dim(' Next steps:'));
@@ -316,7 +316,10 @@ function createPlaygroundServer() {
316
316
 
317
317
  // Handle RAG API requests
318
318
  if (req.url.startsWith('/api/rag/')) {
319
- const handled = await handleRAGRequest(req, res, { generateEmbeddings });
319
+ const handled = await handleRAGRequest(req, res, {
320
+ generateEmbeddings,
321
+ generateLocalEmbeddings: require('../nano/nano-local.js').generateLocalEmbeddings,
322
+ });
320
323
  if (handled) return;
321
324
  }
322
325
 
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  let p;
6
6
  function clack() { if (!p) p = require('@clack/prompts'); return p; }
7
7
  const { loadProject } = require('../lib/project');
8
- const { connect, close } = require('../lib/mongo');
8
+ const { getMongoCollection } = require('../lib/mongo');
9
9
  const ui = require('../lib/ui');
10
10
 
11
11
  /**
@@ -125,8 +125,9 @@ async function purge(options = {}) {
125
125
  if (!quiet) {
126
126
  p.log.step(`Connecting to database: ${db}`);
127
127
  }
128
- client = await connect(db);
129
- const collection = client.db(db).collection(collectionName);
128
+ const mongo = await getMongoCollection(db, collectionName);
129
+ client = mongo.client;
130
+ const collection = mongo.collection;
130
131
 
131
132
  let filter = {};
132
133
  let staleIds = [];
@@ -223,8 +224,8 @@ async function purge(options = {}) {
223
224
  p.log.step('Deleting documents...');
224
225
  }
225
226
 
226
- const result = await collection.deleteMany(filter);
227
- const deleted = result.deletedCount;
227
+ const deleteResult = await collection.deleteMany(filter);
228
+ const deleted = deleteResult.deletedCount;
228
229
 
229
230
  if (options.json) {
230
231
  console.log(JSON.stringify({ success: true, deleted }));
@@ -244,7 +245,7 @@ async function purge(options = {}) {
244
245
  return { success: false, error: err.message };
245
246
  } finally {
246
247
  if (client) {
247
- await close();
248
+ await client.close();
248
249
  }
249
250
  }
250
251
  }
@@ -3,7 +3,7 @@
3
3
  let p;
4
4
  function clack() { if (!p) p = require('@clack/prompts'); return p; }
5
5
  const { loadProject, saveProject } = require('../lib/project');
6
- const { connect, close } = require('../lib/mongo');
6
+ const { getMongoCollection } = require('../lib/mongo');
7
7
  const { generateEmbeddings } = require('../lib/api');
8
8
  const { chunkText } = require('../lib/chunker');
9
9
  const ui = require('../lib/ui');
@@ -76,8 +76,9 @@ async function refresh(options = {}) {
76
76
  if (!quiet) {
77
77
  p.log.step(`Connecting to database: ${db}`);
78
78
  }
79
- client = await connect(db);
80
- const collection = client.db(db).collection(collectionName);
79
+ const result = await getMongoCollection(db, collectionName);
80
+ client = result.client;
81
+ const collection = result.collection;
81
82
 
82
83
  // Build filter
83
84
  let filter = {};
@@ -291,7 +292,7 @@ async function refresh(options = {}) {
291
292
  return { success: false, error: err.message };
292
293
  } finally {
293
294
  if (client) {
294
- await close();
295
+ await client.close();
295
296
  }
296
297
  }
297
298
  }
@@ -36,7 +36,7 @@ const MODEL_CATALOG = [
36
36
  { name: 'voyage-multimodal-3.5', type: 'embedding-multimodal', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/M + $0.60/B px', bestFor: 'Text + images + video', shortFor: 'Multimodal', multimodal: true },
37
37
  { name: 'rerank-2.5', type: 'reranking', context: '32K', dimensions: '—', price: '$0.05/1M tokens', pricePerMToken: 0.05, bestFor: 'Best quality reranking', shortFor: 'Best reranker' },
38
38
  { name: 'rerank-2.5-lite', type: 'reranking', context: '32K', dimensions: '—', price: '$0.02/1M tokens', pricePerMToken: 0.02, bestFor: 'Fast reranking', shortFor: 'Fast reranker' },
39
- { name: 'voyage-4-nano', type: 'embedding', context: '32K', dimensions: '512 (default), 128, 256', price: 'Open-weight (free)', pricePerMToken: 0, bestFor: 'Open-weight / edge / local', shortFor: 'Open / edge', local: true, unreleased: true, family: 'voyage-4', architecture: 'dense', sharedSpace: 'voyage-4', huggingface: 'https://huggingface.co/voyageai/voyage-4-nano', rtebScore: null },
39
+ { name: 'voyage-4-nano', type: 'embedding', context: '32K', dimensions: '512 (default), 128, 256, 1024, 2048', price: 'Open-weight (free)', pricePerMToken: 0, bestFor: 'Open-weight / edge / local', shortFor: 'Local / free', local: true, unreleased: true, family: 'voyage-4', architecture: 'dense', sharedSpace: 'voyage-4', huggingface: 'https://huggingface.co/voyageai/voyage-4-nano', rtebScore: null },
40
40
  // Legacy models
41
41
  { name: 'voyage-3-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', pricePerMToken: 0.18, bestFor: 'Previous gen quality', shortFor: 'Previous gen quality', legacy: true, rtebScore: null },
42
42
  { name: 'voyage-3', type: 'embedding', context: '32K', dimensions: '1024', price: '$0.06/1M tokens', pricePerMToken: 0.06, bestFor: 'Previous gen balanced', shortFor: 'Previous gen balanced', legacy: true, rtebScore: null },
@@ -25,6 +25,34 @@ async function extractTextFromPDF(buffer) {
25
25
  const RAG_DB = 'vai_rag';
26
26
  const KBS_COLLECTION = 'knowledge_bases';
27
27
 
28
+ async function computeKBStatsFromCollection(docsCollection) {
29
+ const stats = await docsCollection.aggregate([
30
+ { $group: {
31
+ _id: null,
32
+ totalSize: { $sum: { $strLenBytes: { $ifNull: ['$content', ''] } } },
33
+ chunkCount: { $sum: 1 },
34
+ files: { $addToSet: '$fileName' }
35
+ } }
36
+ ]).toArray();
37
+
38
+ const liveStats = stats[0] || { totalSize: 0, chunkCount: 0, files: [] };
39
+ return {
40
+ size: liveStats.totalSize,
41
+ chunkCount: liveStats.chunkCount,
42
+ docCount: liveStats.files.filter(Boolean).length
43
+ };
44
+ }
45
+
46
+ async function computeKBStats(db, kbName) {
47
+ return computeKBStatsFromCollection(db.collection(`kb_${kbName}_docs`));
48
+ }
49
+
50
+ function normalizeChunks(content) {
51
+ return chunkText(content)
52
+ .map(chunk => typeof chunk === 'string' ? chunk.trim() : '')
53
+ .filter(Boolean);
54
+ }
55
+
28
56
  // ── Friendly KB name generator ──
29
57
  const KB_ADJECTIVES = [
30
58
  'swift', 'bright', 'calm', 'bold', 'keen',
@@ -76,26 +104,86 @@ function generateKBName() {
76
104
  return `${adj}-${noun}-${suffix}`;
77
105
  }
78
106
 
107
+ /**
108
+ * Resolve the correct embedding function based on the selected model.
109
+ * When embeddingModel is 'voyage-4-nano', uses local nano embeddings.
110
+ * Otherwise, uses the remote Voyage API.
111
+ *
112
+ * @param {string} embeddingModel - Selected embedding model name
113
+ * @param {Function} remoteEmbed - Remote generateEmbeddings function
114
+ * @param {Function} localEmbed - Local generateLocalEmbeddings function
115
+ * @returns {{ embedFn: Function, model: string, isLocal: boolean }}
116
+ */
117
+ function resolveEmbedFn(embeddingModel, remoteEmbed, localEmbed) {
118
+ if (embeddingModel === 'voyage-4-nano' && localEmbed) {
119
+ return {
120
+ embedFn: (texts, opts) => localEmbed(texts, {
121
+ inputType: opts.inputType || 'document',
122
+ dimensions: 1024,
123
+ }),
124
+ model: 'voyage-4-nano',
125
+ isLocal: true,
126
+ };
127
+ }
128
+ return {
129
+ embedFn: (texts, opts) => remoteEmbed(texts, {
130
+ model: embeddingModel || 'voyage-4-large',
131
+ inputType: opts.inputType || 'document',
132
+ }),
133
+ model: embeddingModel || 'voyage-4-large',
134
+ isLocal: false,
135
+ };
136
+ }
137
+
79
138
  /**
80
139
  * Handle RAG API requests
81
140
  * Returns true if handled, false otherwise
82
141
  * @param {http.IncomingMessage} req
83
142
  * @param {http.ServerResponse} res
84
- * @param {Object} context - API context (generateEmbeddings, etc.)
143
+ * @param {Object} context - API context (generateEmbeddings, generateLocalEmbeddings)
85
144
  */
86
145
  async function handleRAGRequest(req, res, context) {
87
- const { generateEmbeddings } = context;
146
+ const { generateEmbeddings, generateLocalEmbeddings } = context;
88
147
 
89
148
  // GET /api/rag/kbs - List all knowledge bases
90
149
  if (req.method === 'GET' && req.url === '/api/rag/kbs') {
91
150
  try {
92
151
  const { client, collection: kbsCollection } = await getMongoCollection(RAG_DB, KBS_COLLECTION);
152
+ const db = client.db(RAG_DB);
93
153
  const kbs = await kbsCollection.find({}).toArray();
154
+ const metadataFixes = [];
155
+ const hydratedKbs = await Promise.all(kbs.map(async (kb) => {
156
+ const liveStats = await computeKBStats(db, kb.name);
157
+ if (
158
+ (kb.docCount || 0) !== liveStats.docCount ||
159
+ (kb.chunkCount || 0) !== liveStats.chunkCount ||
160
+ (kb.size || 0) !== liveStats.size
161
+ ) {
162
+ metadataFixes.push({
163
+ updateOne: {
164
+ filter: { _id: kb._id },
165
+ update: {
166
+ $set: {
167
+ docCount: liveStats.docCount,
168
+ chunkCount: liveStats.chunkCount,
169
+ size: liveStats.size
170
+ }
171
+ }
172
+ }
173
+ });
174
+ }
175
+ return { ...kb, ...liveStats };
176
+ }));
177
+
178
+ if (metadataFixes.length > 0) {
179
+ await kbsCollection.bulkWrite(metadataFixes, { ordered: false });
180
+ }
181
+
94
182
  client.close();
95
183
 
96
184
  res.writeHead(200, { 'Content-Type': 'application/json' });
97
185
  res.end(JSON.stringify({
98
- kbs: kbs.map(kb => ({
186
+ kbs: hydratedKbs.map(kb => ({
99
187
  name: kb.name,
100
188
  displayName: kb.displayName || kb.name,
101
189
  docCount: kb.docCount || 0,
@@ -223,6 +311,7 @@ async function handleRAGRequest(req, res, context) {
223
311
  const headerSep = Buffer.from('\r\n\r\n');
224
312
  const crlf = Buffer.from('\r\n');
225
313
  let kbName = null;
314
+ let embeddingModel = null;
226
315
 
227
316
  // Find all boundary positions in the raw Buffer
228
317
  let searchStart = 0;
@@ -267,6 +356,8 @@ async function handleRAGRequest(req, res, context) {
267
356
  files.push({ name: filename, path: filepath });
268
357
  } else if (nameMatch && nameMatch[1] === 'kbName') {
269
358
  kbName = body.slice(contentStart, contentEnd).toString('utf8').trim();
359
+ } else if (nameMatch && nameMatch[1] === 'embeddingModel') {
360
+ embeddingModel = body.slice(contentStart, contentEnd).toString('utf8').trim();
270
361
  }
271
362
  }
272
363
 
@@ -323,6 +414,9 @@ async function handleRAGRequest(req, res, context) {
323
414
  }
324
415
  }
325
416
 
417
+ // Resolve embedding function (local nano vs remote API)
418
+ const { embedFn } = resolveEmbedFn(embeddingModel, generateEmbeddings, generateLocalEmbeddings);
419
+
326
420
  // Ingest files
327
421
  res.writeHead(200, {
328
422
  'Content-Type': 'application/x-ndjson',
@@ -355,10 +449,10 @@ async function handleRAGRequest(req, res, context) {
355
449
  } else {
356
450
  content = fs.readFileSync(file.path, 'utf8');
357
451
  }
358
- totalSize += Buffer.byteLength(content, 'utf8');
452
+ const contentSize = Buffer.byteLength(content, 'utf8');
359
453
 
360
454
  // Stage: chunking
361
- const chunks = chunkText(content);
455
+ const chunks = normalizeChunks(content);
362
456
  res.write(JSON.stringify({
363
457
  type: 'progress',
364
458
  stage: 'chunking',
@@ -368,7 +462,18 @@ async function handleRAGRequest(req, res, context) {
368
462
  fileCount: files.length
369
463
  }) + '\n');
370
464
 
465
+ if (chunks.length === 0) {
466
+ res.write(JSON.stringify({
467
+ type: 'warning',
468
+ file: file.name,
469
+ warning: `No text content could be extracted from ${file.name}.`
470
+ }) + '\n');
471
+ continue;
472
+ }
473
+
371
474
  // Stage: embedding (per-chunk progress)
475
+ let persistedChunks = 0;
476
+ let lastEmbedError = null;
372
477
  for (let c = 0; c < chunks.length; c++) {
373
478
  try {
374
479
  res.write(JSON.stringify({
@@ -381,7 +486,7 @@ async function handleRAGRequest(req, res, context) {
381
486
  fileCount: files.length
382
487
  }) + '\n');
383
488
 
384
- const embedding = await generateEmbeddings(chunks[c], 'voyage-4-large');
489
+ const embedding = await embedFn([chunks[c]], { inputType: 'document' });
385
490
  const doc = {
386
491
  _id: crypto.randomUUID(),
387
492
  kbName,
@@ -391,8 +496,10 @@ async function handleRAGRequest(req, res, context) {
391
496
  createdAt: new Date()
392
497
  };
393
498
  await docsCollection.insertOne(doc);
499
+ persistedChunks++;
394
500
  totalChunks++;
395
501
  } catch (embedErr) {
502
+ lastEmbedError = embedErr;
396
503
  console.warn(`Failed to embed chunk from ${file.name}:`, embedErr.message);
397
504
  }
398
505
  }
@@ -406,7 +513,26 @@ async function handleRAGRequest(req, res, context) {
406
513
  fileCount: files.length
407
514
  }) + '\n');
408
515
 
409
- totalDocs++;
516
+ if (persistedChunks > 0) {
517
+ totalDocs++;
518
+ totalSize += contentSize;
519
+ } else {
520
+ const detail = lastEmbedError?.message ? ` ${lastEmbedError.message}` : '';
521
+ res.write(JSON.stringify({
522
+ type: 'warning',
523
+ file: file.name,
524
+ warning: `No chunks were stored for ${file.name}.${detail}`.trim()
525
+ }) + '\n');
526
+ }
527
+
528
+ if (persistedChunks > 0 && persistedChunks < chunks.length) {
529
+ res.write(JSON.stringify({
530
+ type: 'warning',
531
+ file: file.name,
532
+ warning: `Only ${persistedChunks}/${chunks.length} chunks were stored for ${file.name}.`
533
+ }) + '\n');
534
+ }
535
+
410
536
  try {
411
537
  fs.unlinkSync(file.path);
412
538
  } catch (e) {
@@ -414,18 +540,13 @@ async function handleRAGRequest(req, res, context) {
414
540
  }
415
541
  }
416
542
 
417
- // Update KB metadata (use $inc so size accumulates across uploads)
543
+ // Recompute live stats so counters stay accurate even when some files
544
+ // produce zero persisted chunks or partial embeddings succeed.
545
+ const liveStats = await computeKBStatsFromCollection(docsCollection);
418
546
  await kbsCollection.updateOne(
419
547
  { name: kbName },
420
548
  {
421
- $inc: {
422
- docCount: totalDocs,
423
- chunkCount: totalChunks,
424
- size: totalSize
425
- },
426
- $set: {
427
- updatedAt: new Date()
428
- }
549
+ $set: { ...liveStats, updatedAt: new Date() }
429
550
  }
430
551
  );
431
552
 
@@ -510,20 +631,8 @@ async function handleRAGRequest(req, res, context) {
510
631
  }
511
632
 
512
633
  // Compute live stats from docs collection (more accurate than stored metadata)
513
- const { collection: docsCollection } = await getMongoCollection(RAG_DB, `kb_${kbName}_docs`);
514
- const stats = await docsCollection.aggregate([
515
- { $group: {
516
- _id: null,
517
- totalSize: { $sum: { $strLenBytes: { $ifNull: ['$content', ''] } } },
518
- chunkCount: { $sum: 1 },
519
- files: { $addToSet: '$fileName' }
520
- }}
521
- ]).toArray();
522
-
523
- const liveStats = stats[0] || { totalSize: 0, chunkCount: 0, files: [] };
524
- kb.size = liveStats.totalSize;
525
- kb.chunkCount = liveStats.chunkCount;
526
- kb.docCount = liveStats.files.length;
634
+ const db = client.db(RAG_DB);
635
+ Object.assign(kb, await computeKBStats(db, kbName));
527
636
 
528
637
  client.close();
529
638
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -645,11 +754,10 @@ async function handleRAGRequest(req, res, context) {
645
754
 
646
755
  await docsCollection.deleteOne({ _id: docId });
647
756
 
648
- // Update KB doc count
649
- const docCount = await docsCollection.countDocuments();
757
+ const liveStats = await computeKBStatsFromCollection(docsCollection);
650
758
  await kbsCollection.updateOne(
651
759
  { name: kbName },
652
- { $set: { chunkCount: docCount, updatedAt: new Date() } }
760
+ { $set: { ...liveStats, updatedAt: new Date() } }
653
761
  );
654
762
 
655
763
  kbClient.close();
@@ -670,7 +778,7 @@ async function handleRAGRequest(req, res, context) {
670
778
  req.on('data', chunk => { body += chunk; });
671
779
  req.on('end', async () => {
672
780
  try {
673
- const { text, kbName, title } = JSON.parse(body);
781
+ const { text, kbName, title, embeddingModel } = JSON.parse(body);
674
782
 
675
783
  if (!text || typeof text !== 'string' || !text.trim()) {
676
784
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -700,17 +808,28 @@ async function handleRAGRequest(req, res, context) {
700
808
  'Cache-Control': 'no-cache'
701
809
  });
702
810
 
703
- const chunks = chunkText(text.trim());
811
+ const chunks = normalizeChunks(text.trim());
704
812
  const fileName = title && title.trim() ? title.trim().slice(0, 80) : `pasted-text-${Date.now()}`;
705
813
  const totalSize = Buffer.byteLength(text, 'utf8');
706
814
 
815
+ // Resolve embedding function (local nano vs remote API)
816
+ const { embedFn } = resolveEmbedFn(embeddingModel, generateEmbeddings, generateLocalEmbeddings);
817
+
707
818
  res.write(JSON.stringify({ type: 'progress', stage: 'chunking', current: chunks.length, total: chunks.length }) + '\n');
708
819
 
820
+ if (chunks.length === 0) {
821
+ res.write(JSON.stringify({ type: 'error', error: 'No text content could be chunked from the pasted text.' }) + '\n');
822
+ res.end();
823
+ kbClient.close();
824
+ return;
825
+ }
826
+
709
827
  let totalChunks = 0;
828
+ let lastEmbedError = null;
710
829
  for (let i = 0; i < chunks.length; i++) {
711
830
  res.write(JSON.stringify({ type: 'progress', stage: 'embedding', current: i + 1, total: chunks.length }) + '\n');
712
831
  try {
713
- const embedding = await generateEmbeddings(chunks[i], 'voyage-4-large');
832
+ const embedding = await embedFn([chunks[i]], { inputType: 'document' });
714
833
  const doc = {
715
834
  _id: crypto.randomUUID(),
716
835
  kbName,
@@ -722,15 +841,24 @@ async function handleRAGRequest(req, res, context) {
722
841
  await docsCollection.insertOne(doc);
723
842
  totalChunks++;
724
843
  } catch (embedErr) {
844
+ lastEmbedError = embedErr;
725
845
  console.warn(`Failed to embed chunk from pasted text:`, embedErr.message);
726
846
  }
727
847
  }
728
848
 
849
+ if (totalChunks === 0) {
850
+ const detail = lastEmbedError?.message ? ` ${lastEmbedError.message}` : '';
851
+ res.write(JSON.stringify({ type: 'error', error: `No chunks were stored for the pasted text.${detail}`.trim() }) + '\n');
852
+ res.end();
853
+ kbClient.close();
854
+ return;
855
+ }
856
+
857
+ const liveStats = await computeKBStatsFromCollection(docsCollection);
729
858
  await kbsCollection.updateOne(
730
859
  { name: kbName },
731
860
  {
732
- $inc: { docCount: 1, chunkCount: totalChunks, size: totalSize },
733
- $set: { updatedAt: new Date() }
861
+ $set: { ...liveStats, updatedAt: new Date() }
734
862
  }
735
863
  );
736
864
 
@@ -752,7 +880,7 @@ async function handleRAGRequest(req, res, context) {
752
880
  req.on('data', chunk => { body += chunk; });
753
881
  req.on('end', async () => {
754
882
  try {
755
- const { url, kbName } = JSON.parse(body);
883
+ const { url, kbName, embeddingModel } = JSON.parse(body);
756
884
 
757
885
  if (!url || typeof url !== 'string' || !/^https?:\/\//i.test(url)) {
758
886
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -826,20 +954,31 @@ async function handleRAGRequest(req, res, context) {
826
954
  return;
827
955
  }
828
956
 
829
- const chunks = chunkText(content);
957
+ const chunks = normalizeChunks(content);
830
958
  // Build fileName from URL hostname + path, truncated to 80 chars
831
959
  let parsedUrl;
832
960
  try { parsedUrl = new URL(url); } catch { parsedUrl = { hostname: 'unknown', pathname: '' }; }
833
961
  const fileName = (parsedUrl.hostname + parsedUrl.pathname).slice(0, 80);
834
962
  const totalSize = Buffer.byteLength(content, 'utf8');
835
963
 
964
+ // Resolve embedding function (local nano vs remote API)
965
+ const { embedFn } = resolveEmbedFn(embeddingModel, generateEmbeddings, generateLocalEmbeddings);
966
+
836
967
  res.write(JSON.stringify({ type: 'progress', stage: 'chunking', current: chunks.length, total: chunks.length }) + '\n');
837
968
 
969
+ if (chunks.length === 0) {
970
+ res.write(JSON.stringify({ type: 'error', error: 'No text content could be chunked from the fetched URL.' }) + '\n');
971
+ res.end();
972
+ kbClient.close();
973
+ return;
974
+ }
975
+
838
976
  let totalChunks = 0;
977
+ let lastEmbedError = null;
839
978
  for (let i = 0; i < chunks.length; i++) {
840
979
  res.write(JSON.stringify({ type: 'progress', stage: 'embedding', current: i + 1, total: chunks.length }) + '\n');
841
980
  try {
842
- const embedding = await generateEmbeddings(chunks[i], 'voyage-4-large');
981
+ const embedding = await embedFn([chunks[i]], { inputType: 'document' });
843
982
  const doc = {
844
983
  _id: crypto.randomUUID(),
845
984
  kbName,
@@ -851,15 +990,24 @@ async function handleRAGRequest(req, res, context) {
851
990
  await docsCollection.insertOne(doc);
852
991
  totalChunks++;
853
992
  } catch (embedErr) {
993
+ lastEmbedError = embedErr;
854
994
  console.warn(`Failed to embed chunk from URL ${url}:`, embedErr.message);
855
995
  }
856
996
  }
857
997
 
998
+ if (totalChunks === 0) {
999
+ const detail = lastEmbedError?.message ? ` ${lastEmbedError.message}` : '';
1000
+ res.write(JSON.stringify({ type: 'error', error: `No chunks were stored for the fetched URL.${detail}`.trim() }) + '\n');
1001
+ res.end();
1002
+ kbClient.close();
1003
+ return;
1004
+ }
1005
+
1006
+ const liveStats = await computeKBStatsFromCollection(docsCollection);
858
1007
  await kbsCollection.updateOne(
859
1008
  { name: kbName },
860
1009
  {
861
- $inc: { docCount: 1, chunkCount: totalChunks, size: totalSize },
862
- $set: { updatedAt: new Date() }
1010
+ $set: { ...liveStats, updatedAt: new Date() }
863
1011
  }
864
1012
  );
865
1013
 
@@ -14,7 +14,7 @@ Really wrestled with this... if you have better ideas, please let me know.
14
14
  import json
15
15
  import sys
16
16
 
17
- BRIDGE_VERSION = "1.33.3"
17
+ BRIDGE_VERSION = "1.33.5"
18
18
  MODEL_NAME = "voyageai/voyage-4-nano"
19
19
 
20
20
  # Lazy-loaded on first embed request
@@ -159,192 +159,6 @@ body {
159
159
  .update-progress-fill { height: 100%; background: var(--accent); border-radius: 3px; transition: width 0.3s ease; width: 0%; }
160
160
  .update-progress-label { font-size: 12px; font-family: var(--mono); color: var(--accent); min-width: 40px; text-align: right; }
161
161
 
162
- /* ── Onboarding Walkthrough ── */
163
- .onboarding-overlay {
164
- display: none;
165
- position: fixed;
166
- inset: 0;
167
- z-index: 10000;
168
- }
169
- .onboarding-overlay.active { display: block; }
170
- .onboarding-backdrop {
171
- position: absolute;
172
- inset: 0;
173
- background: rgba(0,0,0,0.6);
174
- transition: opacity 0.3s;
175
- }
176
- .onboarding-spotlight {
177
- position: absolute;
178
- border-radius: var(--radius);
179
- box-shadow: 0 0 0 9999px rgba(0,0,0,0.55);
180
- transition: all 0.4s cubic-bezier(0.4,0,0.2,1);
181
- z-index: 10001;
182
- pointer-events: none;
183
- }
184
- .onboarding-tooltip {
185
- position: absolute;
186
- z-index: 10002;
187
- background: var(--bg-card);
188
- border: 1px solid var(--accent);
189
- border-radius: 12px;
190
- padding: 24px 28px 20px;
191
- width: 380px;
192
- max-width: 90vw;
193
- box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(0,212,170,0.15);
194
- transition: all 0.4s cubic-bezier(0.4,0,0.2,1);
195
- opacity: 0;
196
- transform: translateY(10px);
197
- }
198
- .onboarding-tooltip.visible {
199
- opacity: 1;
200
- transform: translateY(0);
201
- }
202
- .onboarding-tooltip-arrow {
203
- position: absolute;
204
- width: 12px;
205
- height: 12px;
206
- background: var(--bg-card);
207
- border: 1px solid var(--accent);
208
- transform: rotate(45deg);
209
- }
210
- .onboarding-tooltip-arrow.top {
211
- top: -7px;
212
- left: 30px;
213
- border-right: none;
214
- border-bottom: none;
215
- }
216
- .onboarding-tooltip-arrow.bottom {
217
- bottom: -7px;
218
- left: 30px;
219
- border-left: none;
220
- border-top: none;
221
- }
222
- .onboarding-tooltip-arrow.left {
223
- left: -7px;
224
- top: 24px;
225
- border-right: none;
226
- border-top: none;
227
- }
228
- .onboarding-step-icon {
229
- font-size: 28px;
230
- margin-bottom: 8px;
231
- }
232
- .onboarding-step-title {
233
- font-size: 16px;
234
- font-weight: 700;
235
- color: var(--accent-text);
236
- margin-bottom: 6px;
237
- }
238
- .onboarding-step-body {
239
- font-size: 13px;
240
- color: var(--text-dim);
241
- line-height: 1.6;
242
- margin-bottom: 18px;
243
- }
244
- .onboarding-step-body strong { color: var(--accent); }
245
- .onboarding-footer {
246
- display: flex;
247
- align-items: center;
248
- justify-content: space-between;
249
- gap: 12px;
250
- }
251
- .onboarding-dots {
252
- display: flex;
253
- gap: 6px;
254
- }
255
- .onboarding-dot {
256
- width: 8px;
257
- height: 8px;
258
- border-radius: 50%;
259
- background: var(--border);
260
- transition: background 0.2s;
261
- }
262
- .onboarding-dot.active { background: var(--accent); }
263
- .onboarding-dot.completed { background: var(--accent-dim); }
264
- .onboarding-actions {
265
- display: flex;
266
- gap: 8px;
267
- align-items: center;
268
- }
269
- .onboarding-skip {
270
- background: none;
271
- border: none;
272
- color: var(--text-muted);
273
- font-size: 12px;
274
- cursor: pointer;
275
- font-family: var(--font);
276
- padding: 6px 10px;
277
- border-radius: 6px;
278
- transition: color 0.15s;
279
- }
280
- .onboarding-skip:hover { color: var(--text); }
281
- .onboarding-next {
282
- background: var(--accent);
283
- color: var(--bg);
284
- border: none;
285
- border-radius: 6px;
286
- padding: 8px 20px;
287
- font-size: 13px;
288
- font-weight: 600;
289
- cursor: pointer;
290
- font-family: var(--font);
291
- transition: opacity 0.15s;
292
- }
293
- .onboarding-next:hover { opacity: 0.85; }
294
- .onboarding-welcome-center {
295
- position: fixed;
296
- inset: 0;
297
- z-index: 10002;
298
- display: flex;
299
- align-items: center;
300
- justify-content: center;
301
- pointer-events: none;
302
- }
303
- .onboarding-welcome-card {
304
- background: var(--bg-card);
305
- border: 1px solid var(--accent);
306
- border-radius: 16px;
307
- padding: 40px 44px 32px;
308
- width: 460px;
309
- max-width: 90vw;
310
- text-align: center;
311
- box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 80px rgba(0,212,170,0.08);
312
- pointer-events: auto;
313
- opacity: 0;
314
- transform: scale(0.95);
315
- transition: all 0.4s cubic-bezier(0.4,0,0.2,1);
316
- }
317
- .onboarding-welcome-card.visible {
318
- opacity: 1;
319
- transform: scale(1);
320
- }
321
- .onboarding-welcome-logo {
322
- width: 64px;
323
- height: 64px;
324
- margin-bottom: 16px;
325
- }
326
- .onboarding-welcome-title {
327
- font-size: 24px;
328
- font-weight: 700;
329
- color: var(--accent-text);
330
- margin-bottom: 8px;
331
- }
332
- .onboarding-welcome-sub {
333
- font-size: 14px;
334
- color: var(--text-dim);
335
- line-height: 1.6;
336
- margin-bottom: 28px;
337
- }
338
- [data-theme="light"] .onboarding-tooltip {
339
- box-shadow: 0 12px 40px rgba(0,30,43,0.15), 0 0 0 1px rgba(0,158,128,0.2);
340
- }
341
- [data-theme="light"] .onboarding-welcome-card {
342
- box-shadow: 0 20px 60px rgba(0,30,43,0.18), 0 0 80px rgba(0,158,128,0.06);
343
- }
344
- [data-theme="light"] .onboarding-backdrop {
345
- background: rgba(0,30,43,0.45);
346
- }
347
-
348
162
  /* ── App Shell: sidebar + content layout ── */
349
163
  .app-shell {
350
164
  display: flex;
@@ -11065,18 +10879,6 @@ Retrieval augmented generation combines search with LLMs</textarea>
11065
10879
  </div>
11066
10880
  </div>
11067
10881
  </div>
11068
- <div class="settings-section">
11069
- <div class="settings-section-title">Help</div>
11070
- <div class="settings-row">
11071
- <div class="settings-label">
11072
- <span class="settings-label-text">Welcome Tour</span>
11073
- <span class="settings-label-hint">Replay the onboarding walkthrough</span>
11074
- </div>
11075
- <div class="settings-control">
11076
- <button class="settings-reset-btn" id="settingsShowTour" style="background:var(--accent);color:var(--bg);">Show Welcome Tour</button>
11077
- </div>
11078
- </div>
11079
- </div>
11080
10882
  <!-- Version Info (visible in Electron only) -->
11081
10883
  <div class="settings-section" id="settingsVersionSection" style="display:none;">
11082
10884
  <div class="settings-section-title">Version</div>
@@ -11153,38 +10955,6 @@ Retrieval augmented generation combines search with LLMs</textarea>
11153
10955
  </div><!-- .content-area -->
11154
10956
  </div><!-- .app-shell -->
11155
10957
 
11156
- <!-- Onboarding Walkthrough Overlay -->
11157
- <div class="onboarding-overlay" id="onboardingOverlay">
11158
- <div class="onboarding-backdrop" id="onboardingBackdrop"></div>
11159
- <div class="onboarding-spotlight" id="onboardingSpotlight"></div>
11160
- <!-- Welcome card (step 0) -->
11161
- <div class="onboarding-welcome-center" id="onboardingWelcomeWrap">
11162
- <div class="onboarding-welcome-card" id="onboardingWelcomeCard">
11163
- <img class="onboarding-welcome-logo" id="onboardingLogo" src="/icons/dark/64.png" alt="Vai">
11164
- <div class="onboarding-welcome-title">Welcome to Vai</div>
11165
- <div class="onboarding-welcome-sub">Explore embeddings, compare text similarity, and search with vector models — all from one playground.</div>
11166
- <div class="onboarding-footer" style="justify-content:center;">
11167
- <button class="onboarding-skip" id="onboardingWelcomeSkip">Skip tour</button>
11168
- <button class="onboarding-next" id="onboardingWelcomeNext">Take the tour →</button>
11169
- </div>
11170
- </div>
11171
- </div>
11172
- <!-- Tooltip for steps 1-4 -->
11173
- <div class="onboarding-tooltip" id="onboardingTooltip">
11174
- <div class="onboarding-tooltip-arrow top" id="onboardingArrow"></div>
11175
- <div class="onboarding-step-icon" id="onboardingIcon"></div>
11176
- <div class="onboarding-step-title" id="onboardingTitle"></div>
11177
- <div class="onboarding-step-body" id="onboardingBody"></div>
11178
- <div class="onboarding-footer">
11179
- <div class="onboarding-dots" id="onboardingDots"></div>
11180
- <div class="onboarding-actions">
11181
- <button class="onboarding-skip" id="onboardingSkip">Skip</button>
11182
- <button class="onboarding-next" id="onboardingNext">Next</button>
11183
- </div>
11184
- </div>
11185
- </div>
11186
- </div>
11187
-
11188
10958
  <script>
11189
10959
  // Apply saved theme immediately to prevent flash
11190
10960
  (function() {
@@ -15929,254 +15699,10 @@ function checkForAppUpdate() {
15929
15699
  // ── Onboarding Walkthrough ──
15930
15700
  function initOnboarding() {
15931
15701
  const ONBOARDING_KEY = 'vai-onboarding-complete';
15932
- const overlay = document.getElementById('onboardingOverlay');
15933
- const welcomeWrap = document.getElementById('onboardingWelcomeWrap');
15934
- const welcomeCard = document.getElementById('onboardingWelcomeCard');
15935
- const tooltip = document.getElementById('onboardingTooltip');
15936
- const spotlight = document.getElementById('onboardingSpotlight');
15937
- const arrow = document.getElementById('onboardingArrow');
15938
- const dotsContainer = document.getElementById('onboardingDots');
15939
- const titleEl = document.getElementById('onboardingTitle');
15940
- const bodyEl = document.getElementById('onboardingBody');
15941
- const iconEl = document.getElementById('onboardingIcon');
15942
-
15943
- if (!overlay) return;
15944
-
15945
- const isElectron = !!(window.vai && window.vai.isElectron);
15946
-
15947
- const steps = [
15948
- {
15949
- target: null, // welcome card, no spotlight
15950
- icon: '🚀',
15951
- title: 'Welcome to Vai',
15952
- body: 'Explore embeddings, build RAG pipelines, generate production code — all from one playground.',
15953
- },
15954
- {
15955
- target: '[data-tab="settings"]',
15956
- icon: '🔑',
15957
- title: 'API Key Setup',
15958
- body: 'First, add your <strong>Voyage AI API key</strong> in Settings. You can get one free at <strong>dash.voyageai.com</strong>.' +
15959
- (isElectron ? '<br><br>💎 On desktop, your key is encrypted via <strong>OS keychain</strong> for secure storage.' : ''),
15960
- arrow: 'left',
15961
- },
15962
- {
15963
- target: '[data-tab="embed"]',
15964
- icon: '⚡',
15965
- title: 'Embed',
15966
- 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.',
15967
- arrow: 'left',
15968
- },
15969
- {
15970
- target: '[data-tab="compare"]',
15971
- icon: '🔥',
15972
- title: 'Compare',
15973
- body: 'Paste multiple texts and see a <strong>similarity heatmap</strong> — instantly discover which phrases are semantically close and which are far apart.',
15974
- arrow: 'left',
15975
- },
15976
- {
15977
- target: '[data-tab="search"]',
15978
- icon: '🔍',
15979
- title: 'Search & Rerank',
15980
- 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.',
15981
- arrow: 'left',
15982
- },
15983
- {
15984
- target: '[data-tab="multimodal"]',
15985
- icon: '🖼️',
15986
- title: 'Multimodal',
15987
- body: 'Embed <strong>images and text</strong> in the same vector space. Search images with text queries, or find similar images across your collection.',
15988
- arrow: 'left',
15989
- },
15990
- {
15991
- target: '[data-tab="generate"]',
15992
- icon: '💻',
15993
- title: 'Generate & Scaffold',
15994
- body: '<strong>Generate code snippets</strong> for retrieval, ingestion, and API routes — or <strong>scaffold entire projects</strong> with Next.js, Express, or Flask.' +
15995
- (isElectron ? ' Create projects directly on disk.' : ' Download as ZIP.'),
15996
- arrow: 'left',
15997
- },
15998
- {
15999
- target: '[data-tab="chat"]',
16000
- icon: '💬',
16001
- title: 'RAG Chat',
16002
- body: '<strong>Chat with your knowledge base</strong> using retrieval-augmented generation. Voyage AI finds relevant documents, then your chosen LLM generates grounded answers with source citations.',
16003
- arrow: 'left',
16004
- },
16005
- {
16006
- target: '[data-tab="workflows"]',
16007
- icon: '\u2699\uFE0F',
16008
- title: 'Workflow Visualizer',
16009
- body: '<strong>Visualize and execute workflows</strong> as interactive DAGs. Browse built-in templates, inspect step configuration, and watch execution animate in real time.',
16010
- arrow: 'left',
16011
- },
16012
- {
16013
- target: '[data-tab="benchmark"]',
16014
- icon: '⏱️',
16015
- title: 'Benchmark',
16016
- body: 'Compare model <strong>latency, throughput, and cost</strong>. Test embedding speeds, reranking quality, and quantization trade-offs.',
16017
- arrow: 'left',
16018
- },
16019
- {
16020
- target: '[data-tab="explore"]',
16021
- icon: '💡',
16022
- title: 'Explore & Learn',
16023
- body: 'Dive into <strong>25 interactive explainers</strong> covering embeddings, vector search, RAG, reranking, and more. Each topic links to live demos.',
16024
- arrow: 'left',
16025
- },
16026
- ];
16027
-
16028
- let currentStep = 0;
16029
- const totalSteps = steps.length;
16030
-
16031
- function buildDots() {
16032
- dotsContainer.innerHTML = '';
16033
- for (let i = 1; i < totalSteps; i++) {
16034
- const dot = document.createElement('div');
16035
- dot.className = 'onboarding-dot';
16036
- if (i < currentStep) dot.classList.add('completed');
16037
- if (i === currentStep) dot.classList.add('active');
16038
- dotsContainer.appendChild(dot);
16039
- }
16040
- }
16041
-
16042
- function positionTooltipNear(targetEl) {
16043
- const step = steps[currentStep];
16044
- const rect = targetEl.getBoundingClientRect();
16045
-
16046
- // Position spotlight over the target
16047
- spotlight.style.display = 'block';
16048
- const pad = 6;
16049
- spotlight.style.left = (rect.left - pad) + 'px';
16050
- spotlight.style.top = (rect.top - pad) + 'px';
16051
- spotlight.style.width = (rect.width + pad * 2) + 'px';
16052
- spotlight.style.height = (rect.height + pad * 2) + 'px';
16053
-
16054
- // Reset arrow classes
16055
- arrow.className = 'onboarding-tooltip-arrow';
16056
-
16057
- // Position tooltip to the right of sidebar items
16058
- if (step.arrow === 'left') {
16059
- arrow.classList.add('left');
16060
- tooltip.style.left = (rect.right + 16) + 'px';
16061
- tooltip.style.top = Math.max(8, rect.top - 10) + 'px';
16062
- tooltip.style.right = 'auto';
16063
- tooltip.style.bottom = 'auto';
16064
- } else {
16065
- // Below the target
16066
- arrow.classList.add('top');
16067
- tooltip.style.left = Math.max(8, rect.left) + 'px';
16068
- tooltip.style.top = (rect.bottom + 14) + 'px';
16069
- tooltip.style.right = 'auto';
16070
- tooltip.style.bottom = 'auto';
16071
- }
16072
-
16073
- // Ensure tooltip doesn't overflow viewport
16074
- requestAnimationFrame(() => {
16075
- const tr = tooltip.getBoundingClientRect();
16076
- if (tr.bottom > window.innerHeight - 10) {
16077
- tooltip.style.top = Math.max(8, window.innerHeight - tr.height - 10) + 'px';
16078
- }
16079
- if (tr.right > window.innerWidth - 10) {
16080
- tooltip.style.left = Math.max(8, window.innerWidth - tr.width - 10) + 'px';
16081
- }
16082
- });
16083
- }
16084
-
16085
- function showStep(idx) {
16086
- currentStep = idx;
16087
- const step = steps[idx];
16088
-
16089
- if (idx === 0) {
16090
- // Welcome card — centered, no spotlight
16091
- welcomeWrap.style.display = 'flex';
16092
- tooltip.classList.remove('visible');
16093
- tooltip.style.display = 'none';
16094
- spotlight.style.display = 'none';
16095
- requestAnimationFrame(() => {
16096
- welcomeCard.classList.add('visible');
16097
- });
16098
- return;
16099
- }
16100
-
16101
- // Hide welcome card
16102
- welcomeWrap.style.display = 'none';
16103
- welcomeCard.classList.remove('visible');
16104
-
16105
- // Show tooltip
16106
- tooltip.style.display = 'block';
16107
- tooltip.classList.remove('visible');
16108
- iconEl.textContent = step.icon;
16109
- titleEl.textContent = step.title;
16110
- bodyEl.innerHTML = step.body;
16111
- buildDots();
16112
-
16113
- // Update next button text
16114
- const nextBtn = document.getElementById('onboardingNext');
16115
- nextBtn.textContent = (idx === totalSteps - 1) ? 'Get Started' : 'Next';
16116
-
16117
- // Find target element and position
16118
- const targetEl = document.querySelector(step.target);
16119
- if (targetEl) {
16120
- positionTooltipNear(targetEl);
16121
- }
16122
-
16123
- requestAnimationFrame(() => {
16124
- tooltip.classList.add('visible');
16125
- });
16126
- }
16127
-
16128
- function finish() {
16129
- tooltip.classList.remove('visible');
16130
- welcomeCard.classList.remove('visible');
16131
- spotlight.style.display = 'none';
16132
- setTimeout(() => {
16133
- overlay.classList.remove('active');
16134
- }, 300);
16135
- localStorage.setItem(ONBOARDING_KEY, 'true');
16136
- }
16137
-
16138
- function nextStep() {
16139
- if (currentStep < totalSteps - 1) {
16140
- showStep(currentStep + 1);
16141
- } else {
16142
- finish();
16143
- }
16144
- }
16145
-
16146
- // Wire up buttons
16147
- document.getElementById('onboardingWelcomeNext').addEventListener('click', nextStep);
16148
- document.getElementById('onboardingWelcomeSkip').addEventListener('click', finish);
16149
- document.getElementById('onboardingNext').addEventListener('click', nextStep);
16150
- document.getElementById('onboardingSkip').addEventListener('click', finish);
16151
- document.getElementById('onboardingBackdrop').addEventListener('click', finish);
16152
-
16153
- // "Show Welcome Tour" button in settings
16154
- const tourBtn = document.getElementById('settingsShowTour');
16155
- if (tourBtn) {
16156
- tourBtn.addEventListener('click', () => {
16157
- startOnboarding();
16158
- });
16159
- }
16160
-
16161
- // Auto-start on first visit
16162
- function startOnboarding() {
16163
- // Sync onboarding logo with current theme
16164
- const onboardLogo = document.getElementById('onboardingLogo');
16165
- if (onboardLogo) {
16166
- const theme = localStorage.getItem('vai-theme') || 'dark';
16167
- onboardLogo.src = '/icons/' + (theme === 'light' ? 'light' : 'dark') + '/64.png';
16168
- }
16169
- overlay.classList.add('active');
16170
- showStep(0);
16171
- }
16172
-
16173
- if (!localStorage.getItem(ONBOARDING_KEY)) {
16174
- // Delay slightly so the app is fully rendered
16175
- setTimeout(startOnboarding, 600);
16176
- }
16177
-
16178
- // Expose for manual replay
16179
- window.startOnboarding = startOnboarding;
15702
+ // The legacy welcome tour no longer matches the redesigned interface.
15703
+ // Mark it complete so startup skips the broken onboarding flow.
15704
+ localStorage.setItem(ONBOARDING_KEY, 'true');
15705
+ window.startOnboarding = function() {};
16180
15706
  }
16181
15707
 
16182
15708
  // ── Multimodal Tab ──
@@ -16871,7 +16397,6 @@ let _vsiGameTrigger = 'unknown';
16871
16397
  'exploreModal',
16872
16398
  'costHelpModal',
16873
16399
  'agentInfoModal',
16874
- 'onboardingOverlay',
16875
16400
  'wfStoreOverlay',
16876
16401
  'wfOutputModalBackdrop',
16877
16402
  'wfHelpModalBackdrop',
@@ -9,11 +9,20 @@ class KBManager {
9
9
  this.kbs = [];
10
10
  this.isIngesting = false;
11
11
  this.ingestionProgress = { current: 0, total: 0, currentFile: '' };
12
-
12
+
13
13
  // Local state
14
14
  this.loadLastKB();
15
15
  }
16
16
 
17
+ /**
18
+ * Get the currently selected embedding model from the chat config UI.
19
+ * Falls back to 'voyage-4-large' if the element is missing.
20
+ */
21
+ getEmbeddingModel() {
22
+ const sel = document.getElementById('chatEmbeddingModel');
23
+ return sel ? sel.value : 'voyage-4-large';
24
+ }
25
+
17
26
  /**
18
27
  * Load last used KB from localStorage
19
28
  */
@@ -97,6 +106,7 @@ class KBManager {
97
106
  if (kbName) {
98
107
  formData.append('kbName', kbName);
99
108
  }
109
+ formData.append('embeddingModel', this.getEmbeddingModel());
100
110
 
101
111
  try {
102
112
  this.isIngesting = true;
@@ -175,7 +185,7 @@ class KBManager {
175
185
  const res = await fetch('/api/rag/ingest-text', {
176
186
  method: 'POST',
177
187
  headers: { 'Content-Type': 'application/json' },
178
- body: JSON.stringify({ text, kbName, title })
188
+ body: JSON.stringify({ text, kbName, title, embeddingModel: this.getEmbeddingModel() })
179
189
  });
180
190
 
181
191
  if (!res.ok) {
@@ -239,7 +249,7 @@ class KBManager {
239
249
  const res = await fetch('/api/rag/ingest-url', {
240
250
  method: 'POST',
241
251
  headers: { 'Content-Type': 'application/json' },
242
- body: JSON.stringify({ url, kbName })
252
+ body: JSON.stringify({ url, kbName, embeddingModel: this.getEmbeddingModel() })
243
253
  });
244
254
 
245
255
  if (!res.ok) {
@@ -499,6 +499,7 @@ class KBUIManager {
499
499
  const progressBar = document.getElementById('kbPanelProgressBar');
500
500
  const progressLabel = document.getElementById('kbPanelProgressLabel');
501
501
  if (!progressContainer) return;
502
+ const warnings = [];
502
503
 
503
504
  try {
504
505
  progressContainer.style.display = 'block';
@@ -531,12 +532,27 @@ class KBUIManager {
531
532
 
532
533
  if (progressBar) progressBar.style.width = `${percent}%`;
533
534
  if (progressLabel) progressLabel.textContent = label;
535
+ } else if (event.type === 'warning') {
536
+ if (event.warning) warnings.push(event.warning);
537
+ if (progressLabel) progressLabel.textContent = event.warning || 'Upload completed with warnings';
534
538
  } else if (event.type === 'complete') {
535
539
  if (progressBar) progressBar.style.width = '100%';
536
- if (progressLabel) progressLabel.textContent = `✓ Complete: ${event.docCount} docs, ${event.chunkCount} chunks`;
540
+ const hasWarnings = warnings.length > 0 || event.docCount === 0;
541
+ if (progressLabel) {
542
+ progressLabel.textContent = hasWarnings
543
+ ? `Complete with warnings: ${event.docCount} docs, ${event.chunkCount} chunks`
544
+ : `✓ Complete: ${event.docCount} docs, ${event.chunkCount} chunks`;
545
+ }
537
546
  setTimeout(() => {
538
547
  this.updatePanelUI();
539
548
  progressContainer.style.display = 'none';
549
+ const messages = [...warnings];
550
+ if (event.docCount === 0) {
551
+ messages.unshift('No documents were stored from that upload.');
552
+ }
553
+ if (messages.length > 0) {
554
+ alert(messages.join('\n'));
555
+ }
540
556
  }, 1000);
541
557
  } else if (event.type === 'error') {
542
558
  console.error('Ingestion error:', event.error);