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 +1 -1
- package/package.json +4 -1
- package/src/commands/ingest.js +4 -4
- package/src/commands/init.js +18 -0
- package/src/commands/playground.js +4 -1
- package/src/commands/purge.js +7 -6
- package/src/commands/refresh.js +5 -4
- package/src/lib/catalog.js +1 -1
- package/src/lib/playground-rag-api.js +191 -43
- package/src/nano/nano-bridge.py +1 -1
- package/src/playground/index.html +4 -479
- package/src/playground/js/kb-manager.js +13 -3
- package/src/playground/js/kb-ui.js +17 -1
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/
|
|
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
|
[](https://github.com/mrlynn/voyageai-cli/actions/workflows/ci.yml) [](https://www.npmjs.com/package/voyageai-cli) [](https://opensource.org/licenses/MIT) [](https://nodejs.org) [](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
|
+
"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",
|
package/src/commands/ingest.js
CHANGED
|
@@ -275,7 +275,7 @@ function registerIngest(program) {
|
|
|
275
275
|
batches: totalBatches,
|
|
276
276
|
batchSize: opts.batchSize,
|
|
277
277
|
estimatedTokens: estimated,
|
|
278
|
-
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',
|
|
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:
|
|
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',
|
|
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) {
|
package/src/commands/init.js
CHANGED
|
@@ -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, {
|
|
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
|
|
package/src/commands/purge.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
227
|
-
const deleted =
|
|
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
|
}
|
package/src/commands/refresh.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
80
|
-
|
|
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
|
}
|
package/src/lib/catalog.js
CHANGED
|
@@ -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: '
|
|
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,
|
|
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:
|
|
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
|
-
|
|
452
|
+
const contentSize = Buffer.byteLength(content, 'utf8');
|
|
359
453
|
|
|
360
454
|
// Stage: chunking
|
|
361
|
-
const chunks =
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
$
|
|
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
|
|
514
|
-
|
|
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
|
-
|
|
649
|
-
const docCount = await docsCollection.countDocuments();
|
|
757
|
+
const liveStats = await computeKBStatsFromCollection(docsCollection);
|
|
650
758
|
await kbsCollection.updateOne(
|
|
651
759
|
{ name: kbName },
|
|
652
|
-
{ $set: {
|
|
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 =
|
|
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
|
|
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
|
-
$
|
|
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 =
|
|
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
|
|
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
|
-
$
|
|
862
|
-
$set: { updatedAt: new Date() }
|
|
1010
|
+
$set: { ...liveStats, updatedAt: new Date() }
|
|
863
1011
|
}
|
|
864
1012
|
);
|
|
865
1013
|
|
package/src/nano/nano-bridge.py
CHANGED
|
@@ -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
|
-
|
|
15933
|
-
|
|
15934
|
-
|
|
15935
|
-
|
|
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
|
-
|
|
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);
|