voyageai-cli 1.21.0 → 1.22.1
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/package.json +3 -2
- package/src/cli.js +2 -0
- package/src/commands/bug.js +249 -0
- package/src/lib/codegen.js +27 -1
- package/src/lib/explanations.js +186 -0
- package/src/playground/index.html +189 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voyageai-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.1",
|
|
4
4
|
"description": "CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vai": "./src/cli.js"
|
|
@@ -37,9 +37,10 @@
|
|
|
37
37
|
"test": "node --test test/**/*.test.js"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
40
|
+
"node": ">=20.0.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@clack/prompts": "^1.0.0",
|
|
43
44
|
"commander": "^12.0.0",
|
|
44
45
|
"dotenv": "^17.2.3",
|
|
45
46
|
"mongodb": "^6.0.0",
|
package/src/cli.js
CHANGED
|
@@ -34,6 +34,7 @@ const { registerApp } = require('./commands/app');
|
|
|
34
34
|
const { registerAbout } = require('./commands/about');
|
|
35
35
|
const { register: registerDoctor } = require('./commands/doctor');
|
|
36
36
|
const { register: registerQuickstart } = require('./commands/quickstart');
|
|
37
|
+
const { registerBug } = require('./commands/bug');
|
|
37
38
|
const { showBanner, showQuickStart, getVersion } = require('./lib/banner');
|
|
38
39
|
|
|
39
40
|
const version = getVersion();
|
|
@@ -72,6 +73,7 @@ registerApp(program);
|
|
|
72
73
|
registerAbout(program);
|
|
73
74
|
registerDoctor(program);
|
|
74
75
|
registerQuickstart(program);
|
|
76
|
+
registerBug(program);
|
|
75
77
|
|
|
76
78
|
// Append disclaimer to all help output
|
|
77
79
|
program.addHelpText('after', `
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const pc = require('picocolors');
|
|
5
|
+
const ui = require('../lib/ui');
|
|
6
|
+
const { send: sendTelemetry } = require('../lib/telemetry');
|
|
7
|
+
|
|
8
|
+
// Try to get package version safely
|
|
9
|
+
function getVersion() {
|
|
10
|
+
try {
|
|
11
|
+
return require('../../package.json').version;
|
|
12
|
+
} catch {
|
|
13
|
+
return 'unknown';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const GITHUB_ISSUES_URL = 'https://github.com/mrlynn/voyageai-cli/issues/new';
|
|
18
|
+
const BUG_API_URL = 'https://vai.mlynn.org/api/bugs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a GitHub issue URL with pre-filled template
|
|
22
|
+
*/
|
|
23
|
+
function generateGitHubUrl(title, description, context = {}) {
|
|
24
|
+
const issueTitle = encodeURIComponent(`[Bug] ${title || 'Bug Report'}`);
|
|
25
|
+
|
|
26
|
+
const body = `## Description
|
|
27
|
+
${description || 'Describe the bug here...'}
|
|
28
|
+
|
|
29
|
+
## Steps to Reproduce
|
|
30
|
+
1.
|
|
31
|
+
2.
|
|
32
|
+
3.
|
|
33
|
+
|
|
34
|
+
## Expected Behavior
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Actual Behavior
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Environment
|
|
41
|
+
- **CLI Version:** ${context.cliVersion || getVersion()}
|
|
42
|
+
- **Node Version:** ${process.version}
|
|
43
|
+
- **Platform:** ${os.platform()} ${os.release()}
|
|
44
|
+
- **Arch:** ${os.arch()}
|
|
45
|
+
${context.command ? `- **Command:** \`${context.command}\`` : ''}
|
|
46
|
+
|
|
47
|
+
## Additional Context
|
|
48
|
+
${context.errorMessage ? `### Error\n\`\`\`\n${context.errorMessage}\n\`\`\`` : 'Add any other context here.'}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
return `${GITHUB_ISSUES_URL}?title=${issueTitle}&body=${encodeURIComponent(body)}&labels=bug`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Submit bug report to API
|
|
56
|
+
*/
|
|
57
|
+
async function submitBugReport(data) {
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(BUG_API_URL, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
...data,
|
|
64
|
+
source: 'cli',
|
|
65
|
+
cliVersion: getVersion(),
|
|
66
|
+
platform: os.platform(),
|
|
67
|
+
arch: os.arch(),
|
|
68
|
+
nodeVersion: process.version,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const error = await response.json().catch(() => ({}));
|
|
74
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return await response.json();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(`Failed to submit bug report: ${error.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Open URL in default browser
|
|
85
|
+
*/
|
|
86
|
+
function openUrl(url) {
|
|
87
|
+
const { exec } = require('child_process');
|
|
88
|
+
const command = os.platform() === 'darwin' ? 'open' :
|
|
89
|
+
os.platform() === 'win32' ? 'start' : 'xdg-open';
|
|
90
|
+
exec(`${command} "${url}"`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Interactive bug report (when no arguments provided)
|
|
95
|
+
*/
|
|
96
|
+
async function interactiveBugReport() {
|
|
97
|
+
const readline = require('readline');
|
|
98
|
+
const rl = readline.createInterface({
|
|
99
|
+
input: process.stdin,
|
|
100
|
+
output: process.stdout,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const question = (prompt) => new Promise((resolve) => {
|
|
104
|
+
rl.question(prompt, resolve);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log(ui.info('🐛 Bug Reporter'));
|
|
108
|
+
console.log(ui.dim('Report issues with the Vai CLI\n'));
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const title = await question(ui.label('Title', 'Brief description of the bug') + '\n> ');
|
|
112
|
+
if (!title.trim()) {
|
|
113
|
+
console.log(ui.warn('Bug report cancelled.'));
|
|
114
|
+
rl.close();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const description = await question(ui.label('Description', 'What happened?') + '\n> ');
|
|
119
|
+
const steps = await question(ui.label('Steps to Reproduce', 'Optional, press Enter to skip') + '\n> ');
|
|
120
|
+
const email = await question(ui.label('Email', 'Optional, for follow-up') + '\n> ');
|
|
121
|
+
|
|
122
|
+
console.log('');
|
|
123
|
+
const method = await question('Submit to:\n [1] Bug tracker (anonymous)\n [2] GitHub Issues (public)\n [3] Both\n> ');
|
|
124
|
+
|
|
125
|
+
rl.close();
|
|
126
|
+
|
|
127
|
+
const bugData = {
|
|
128
|
+
title: title.trim(),
|
|
129
|
+
description: description.trim(),
|
|
130
|
+
stepsToReproduce: steps.trim() || null,
|
|
131
|
+
email: email.trim() || null,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (method === '1' || method === '3') {
|
|
135
|
+
console.log(ui.dim('\nSubmitting to bug tracker...'));
|
|
136
|
+
try {
|
|
137
|
+
const result = await submitBugReport(bugData);
|
|
138
|
+
console.log(ui.success(`Bug submitted! ID: ${result.bugId}`));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.log(ui.error(error.message));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (method === '2' || method === '3') {
|
|
145
|
+
const url = generateGitHubUrl(bugData.title, bugData.description, {
|
|
146
|
+
cliVersion: getVersion(),
|
|
147
|
+
});
|
|
148
|
+
console.log(ui.dim('\nOpening GitHub...'));
|
|
149
|
+
openUrl(url);
|
|
150
|
+
console.log(ui.success('GitHub issue page opened in browser'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!['1', '2', '3'].includes(method)) {
|
|
154
|
+
console.log(ui.warn('No submission method selected.'));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
} catch (error) {
|
|
158
|
+
rl.close();
|
|
159
|
+
console.error(ui.error(`Error: ${error.message}`));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Main bug command
|
|
165
|
+
*/
|
|
166
|
+
async function bugCommand(args, flags) {
|
|
167
|
+
// --github flag: open GitHub issues directly
|
|
168
|
+
if (flags.github || flags.g) {
|
|
169
|
+
const title = args.join(' ');
|
|
170
|
+
const url = generateGitHubUrl(title, '', { cliVersion: getVersion() });
|
|
171
|
+
console.log(ui.info('Opening GitHub Issues...'));
|
|
172
|
+
openUrl(url);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --quick flag: quick submit with just title
|
|
177
|
+
if (flags.quick || flags.q) {
|
|
178
|
+
const title = args.join(' ');
|
|
179
|
+
if (!title) {
|
|
180
|
+
console.error(ui.error('Please provide a bug title: vai bug --quick "Something broke"'));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(ui.dim('Submitting quick bug report...'));
|
|
185
|
+
try {
|
|
186
|
+
const result = await submitBugReport({
|
|
187
|
+
title,
|
|
188
|
+
description: title,
|
|
189
|
+
});
|
|
190
|
+
console.log(ui.success(`Bug submitted! ID: ${result.bugId}`));
|
|
191
|
+
console.log(ui.dim(`Create GitHub issue: ${result.githubIssueUrl}`));
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(ui.error(error.message));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// If title provided as argument, use quick mode
|
|
200
|
+
if (args.length > 0) {
|
|
201
|
+
const title = args.join(' ');
|
|
202
|
+
console.log(ui.dim('Submitting bug report...'));
|
|
203
|
+
try {
|
|
204
|
+
const result = await submitBugReport({
|
|
205
|
+
title,
|
|
206
|
+
description: title,
|
|
207
|
+
});
|
|
208
|
+
console.log(ui.success(`Bug submitted! ID: ${result.bugId}`));
|
|
209
|
+
console.log(ui.dim('To create a GitHub issue with more details:'));
|
|
210
|
+
console.log(ui.dim(` ${result.githubIssueUrl.slice(0, 80)}...`));
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error(ui.error(error.message));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// No arguments: interactive mode
|
|
219
|
+
await interactiveBugReport();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Register the bug command with Commander
|
|
224
|
+
*/
|
|
225
|
+
function registerBug(program) {
|
|
226
|
+
program
|
|
227
|
+
.command('bug [title...]')
|
|
228
|
+
.description('Report a bug or issue with the Vai CLI')
|
|
229
|
+
.option('-g, --github', 'Open GitHub Issues in browser')
|
|
230
|
+
.option('-q, --quick', 'Quick submit (title only, no interaction)')
|
|
231
|
+
.action(async (titleParts, options) => {
|
|
232
|
+
sendTelemetry('bug', {
|
|
233
|
+
method: options.github ? 'github' : options.quick ? 'quick' : 'interactive'
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const args = titleParts || [];
|
|
237
|
+
const flags = {
|
|
238
|
+
github: options.github,
|
|
239
|
+
g: options.github,
|
|
240
|
+
quick: options.quick,
|
|
241
|
+
q: options.quick,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
await bugCommand(args, flags);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = { registerBug };
|
|
249
|
+
module.exports.bugCommand = bugCommand;
|
package/src/lib/codegen.js
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Safely get the CLI version, handling both development and packaged Electron app.
|
|
8
|
+
* @returns {string} The version string or 'unknown'
|
|
9
|
+
*/
|
|
10
|
+
function getCliVersion() {
|
|
11
|
+
// Try multiple paths to find package.json
|
|
12
|
+
const possiblePaths = [
|
|
13
|
+
path.join(__dirname, '..', '..', 'package.json'), // Development: src/lib -> root
|
|
14
|
+
path.join(process.resourcesPath || '', 'cli-package.json'), // Packaged Electron app
|
|
15
|
+
path.join(__dirname, '..', 'package.json'), // Alternative structure
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (const pkgPath of possiblePaths) {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(pkgPath)) {
|
|
21
|
+
const pkg = require(pkgPath);
|
|
22
|
+
if (pkg.version) return pkg.version;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Try next path
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
/**
|
|
7
33
|
* Lightweight template engine for code generation.
|
|
8
34
|
*
|
|
@@ -295,7 +321,7 @@ function buildContext(project, options = {}) {
|
|
|
295
321
|
|
|
296
322
|
// Metadata
|
|
297
323
|
generatedAt: new Date().toISOString(),
|
|
298
|
-
vaiVersion:
|
|
324
|
+
vaiVersion: getCliVersion(),
|
|
299
325
|
};
|
|
300
326
|
|
|
301
327
|
return context;
|
package/src/lib/explanations.js
CHANGED
|
@@ -1151,6 +1151,175 @@ const concepts = {
|
|
|
1151
1151
|
],
|
|
1152
1152
|
},
|
|
1153
1153
|
|
|
1154
|
+
'auto-embedding': {
|
|
1155
|
+
title: 'MongoDB Auto-Embedding',
|
|
1156
|
+
summary: 'Automatic vector embedding generation in Atlas Vector Search',
|
|
1157
|
+
content: [
|
|
1158
|
+
`${pc.bold('What is Auto-Embedding?')}`,
|
|
1159
|
+
`${pc.cyan('Auto-Embedding')} is a MongoDB Atlas Vector Search feature (currently in Preview)`,
|
|
1160
|
+
`that automatically generates vector embeddings for text fields using Voyage AI`,
|
|
1161
|
+
`models — no embedding code required.`,
|
|
1162
|
+
``,
|
|
1163
|
+
`${pc.bold('How it works:')}`,
|
|
1164
|
+
` ${pc.dim('1.')} Configure your vector search index with ${pc.cyan('autoEmbed')} type`,
|
|
1165
|
+
` ${pc.dim('2.')} Specify which text field to embed and which Voyage AI model to use`,
|
|
1166
|
+
` ${pc.dim('3.')} MongoDB automatically generates embeddings when documents are inserted/updated`,
|
|
1167
|
+
` ${pc.dim('4.')} At query time, pass natural language text — MongoDB embeds it automatically`,
|
|
1168
|
+
``,
|
|
1169
|
+
`${pc.bold('Supported models:')}`,
|
|
1170
|
+
` ${pc.cyan('voyage-4-lite')} — High-volume, cost-sensitive applications`,
|
|
1171
|
+
` ${pc.cyan('voyage-4')} — Balanced performance (recommended)`,
|
|
1172
|
+
` ${pc.cyan('voyage-4-large')} — Maximum accuracy for complex relationships`,
|
|
1173
|
+
` ${pc.cyan('voyage-code-3')} — Code search and technical documentation`,
|
|
1174
|
+
``,
|
|
1175
|
+
`${pc.bold('Index definition example:')}`,
|
|
1176
|
+
` ${pc.dim('{')}`,
|
|
1177
|
+
` ${pc.dim('"mappings": {')}`,
|
|
1178
|
+
` ${pc.dim('"fields": {')}`,
|
|
1179
|
+
` ${pc.cyan('"summary"')}: ${pc.dim('{')}`,
|
|
1180
|
+
` ${pc.dim('"type": "')}${pc.cyan('autoEmbed')}${pc.dim('",')}`,
|
|
1181
|
+
` ${pc.dim('"model": "voyage-4"')}`,
|
|
1182
|
+
` ${pc.dim('}')}`,
|
|
1183
|
+
` ${pc.dim('}')}`,
|
|
1184
|
+
` ${pc.dim('}')}`,
|
|
1185
|
+
` ${pc.dim('}')}`,
|
|
1186
|
+
``,
|
|
1187
|
+
`${pc.bold('Query syntax:')} Use ${pc.cyan('query.text')} in $vectorSearch instead of ${pc.cyan('queryVector')}:`,
|
|
1188
|
+
` ${pc.dim('$vectorSearch: {')}`,
|
|
1189
|
+
` ${pc.dim('index: "myIndex",')}`,
|
|
1190
|
+
` ${pc.dim('path: "summary",')}`,
|
|
1191
|
+
` ${pc.cyan('query: { text: "properties near amusement parks" }')},`,
|
|
1192
|
+
` ${pc.dim('numCandidates: 100,')}`,
|
|
1193
|
+
` ${pc.dim('limit: 10')}`,
|
|
1194
|
+
` ${pc.dim('}')}`,
|
|
1195
|
+
``,
|
|
1196
|
+
`${pc.bold('API keys:')}`,
|
|
1197
|
+
`Auto-Embedding uses Voyage AI API keys configured during mongot deployment.`,
|
|
1198
|
+
`Best practice: use separate keys for indexing vs. querying to avoid rate limit`,
|
|
1199
|
+
`conflicts. Keys can be created from Atlas (AI Models section) or Voyage AI directly.`,
|
|
1200
|
+
``,
|
|
1201
|
+
`${pc.bold('Current limitations (Preview):')}`,
|
|
1202
|
+
` ${pc.dim('•')} ${pc.yellow('Not yet available')} on Atlas clusters (only self-managed Community Edition)`,
|
|
1203
|
+
` ${pc.dim('•')} Not available on local Atlas deployments via Atlas CLI`,
|
|
1204
|
+
` ${pc.dim('•')} Not available on MongoDB Enterprise Edition`,
|
|
1205
|
+
` ${pc.dim('•')} Available via Docker, tarball, package manager, or Kubernetes with 8.2+ CE`,
|
|
1206
|
+
``,
|
|
1207
|
+
`${pc.bold('When to use Auto-Embedding:')}`,
|
|
1208
|
+
` ${pc.dim('•')} Simple use cases where you want zero embedding code`,
|
|
1209
|
+
` ${pc.dim('•')} Single-field text embedding scenarios`,
|
|
1210
|
+
` ${pc.dim('•')} When your data changes frequently and you want automatic sync`,
|
|
1211
|
+
` ${pc.dim('•')} Self-managed MongoDB deployments`,
|
|
1212
|
+
``,
|
|
1213
|
+
`${pc.bold('When to use vai (manual embedding) instead:')}`,
|
|
1214
|
+
` ${pc.dim('•')} Atlas clusters (auto-embedding not yet available)`,
|
|
1215
|
+
` ${pc.dim('•')} Custom chunking strategies needed`,
|
|
1216
|
+
` ${pc.dim('•')} Multi-field or multi-collection embeddings`,
|
|
1217
|
+
` ${pc.dim('•')} Reranking pipelines (auto-embedding doesn't include reranking)`,
|
|
1218
|
+
` ${pc.dim('•')} Quantization (int8/binary) for storage optimization`,
|
|
1219
|
+
` ${pc.dim('•')} Multimodal embeddings (images + text)`,
|
|
1220
|
+
].join('\n'),
|
|
1221
|
+
links: [
|
|
1222
|
+
'https://www.mongodb.com/docs/atlas/atlas-vector-search/crud-embeddings/create-embeddings-automatic/',
|
|
1223
|
+
'https://www.mongodb.com/docs/voyageai/management/api-keys/',
|
|
1224
|
+
],
|
|
1225
|
+
tryIt: [
|
|
1226
|
+
'vai explain vai-vs-auto-embedding',
|
|
1227
|
+
'vai explain vector-search',
|
|
1228
|
+
'vai models --type embedding',
|
|
1229
|
+
],
|
|
1230
|
+
},
|
|
1231
|
+
|
|
1232
|
+
'vai-vs-auto-embedding': {
|
|
1233
|
+
title: 'VAI vs Auto-Embedding — When to Use Each',
|
|
1234
|
+
summary: 'Choosing between manual embedding pipelines and MongoDB auto-embedding',
|
|
1235
|
+
content: [
|
|
1236
|
+
`Both ${pc.cyan('vai')} (manual embedding) and ${pc.cyan('MongoDB Auto-Embedding')} use the same`,
|
|
1237
|
+
`Voyage AI models, but they serve different use cases and deployment scenarios.`,
|
|
1238
|
+
``,
|
|
1239
|
+
`${pc.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`,
|
|
1240
|
+
``,
|
|
1241
|
+
`${pc.bold(pc.cyan('VAI (Manual Embedding Pipeline)'))}`,
|
|
1242
|
+
``,
|
|
1243
|
+
`You embed text explicitly using ${pc.cyan('vai embed')}, ${pc.cyan('vai pipeline')}, or ${pc.cyan('vai store')},`,
|
|
1244
|
+
`then store the vectors in any database. Full control over every step.`,
|
|
1245
|
+
``,
|
|
1246
|
+
`${pc.bold('Use vai when:')}`,
|
|
1247
|
+
` ${pc.green('✓')} Using ${pc.cyan('MongoDB Atlas clusters')} (auto-embedding not available yet)`,
|
|
1248
|
+
` ${pc.green('✓')} Using ${pc.cyan('any vector database')} (Pinecone, Weaviate, Qdrant, etc.)`,
|
|
1249
|
+
` ${pc.green('✓')} You need ${pc.cyan('custom chunking')} (sentence, paragraph, semantic, sliding window)`,
|
|
1250
|
+
` ${pc.green('✓')} You need ${pc.cyan('reranking')} (vai supports two-stage retrieval pipelines)`,
|
|
1251
|
+
` ${pc.green('✓')} You want ${pc.cyan('quantization')} (int8, binary) for storage optimization`,
|
|
1252
|
+
` ${pc.green('✓')} You need ${pc.cyan('multimodal embeddings')} (images + text)`,
|
|
1253
|
+
` ${pc.green('✓')} You need ${pc.cyan('flexible dimensions')} (256, 512, 1024, 2048)`,
|
|
1254
|
+
` ${pc.green('✓')} You want to ${pc.cyan('mix models')} (embed docs with -large, query with -lite)`,
|
|
1255
|
+
` ${pc.green('✓')} You need ${pc.cyan('batch processing')} with custom concurrency/rate limiting`,
|
|
1256
|
+
` ${pc.green('✓')} You're building ${pc.cyan('RAG pipelines')} with custom retrieval logic`,
|
|
1257
|
+
``,
|
|
1258
|
+
`${pc.dim('Workflow:')}`,
|
|
1259
|
+
` ${pc.cyan('vai chunk')} → ${pc.cyan('vai embed')} → ${pc.cyan('vai store')} → ${pc.cyan('vai search')} → ${pc.cyan('vai rerank')}`,
|
|
1260
|
+
``,
|
|
1261
|
+
`${pc.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`,
|
|
1262
|
+
``,
|
|
1263
|
+
`${pc.bold(pc.cyan('MongoDB Auto-Embedding'))}`,
|
|
1264
|
+
``,
|
|
1265
|
+
`MongoDB automatically generates embeddings when you insert/update documents.`,
|
|
1266
|
+
`No embedding code needed — just configure your index and insert data.`,
|
|
1267
|
+
``,
|
|
1268
|
+
`${pc.bold('Use Auto-Embedding when:')}`,
|
|
1269
|
+
` ${pc.green('✓')} Using ${pc.cyan('self-managed MongoDB Community Edition')} (8.2+)`,
|
|
1270
|
+
` ${pc.green('✓')} You want ${pc.cyan('zero embedding code')} — simplest possible setup`,
|
|
1271
|
+
` ${pc.green('✓')} You're embedding a ${pc.cyan('single text field')} per collection`,
|
|
1272
|
+
` ${pc.green('✓')} Your data ${pc.cyan('changes frequently')} and you want automatic sync`,
|
|
1273
|
+
` ${pc.green('✓')} You don't need reranking, quantization, or multimodal`,
|
|
1274
|
+
` ${pc.green('✓')} Standard chunking is sufficient (or you pre-chunk your data)`,
|
|
1275
|
+
``,
|
|
1276
|
+
`${pc.dim('Workflow:')}`,
|
|
1277
|
+
` ${pc.cyan('db.collection.insertOne({text: "..."})')} → embeddings auto-generated`,
|
|
1278
|
+
` ${pc.cyan('$vectorSearch: {query: {text: "..."}}')} → query auto-embedded`,
|
|
1279
|
+
``,
|
|
1280
|
+
`${pc.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`,
|
|
1281
|
+
``,
|
|
1282
|
+
`${pc.bold('FEATURE COMPARISON')}`,
|
|
1283
|
+
``,
|
|
1284
|
+
`${pc.dim('Feature vai Auto-Embedding')}`,
|
|
1285
|
+
`${pc.dim('─────────────────────────────────────────────────────────────────')}`,
|
|
1286
|
+
`Atlas clusters ${pc.green('Yes')} ${pc.yellow('Not yet')}`,
|
|
1287
|
+
`Self-managed CE 8.2+ ${pc.green('Yes')} ${pc.green('Yes')}`,
|
|
1288
|
+
`Other vector DBs ${pc.green('Yes')} ${pc.dim('No')}`,
|
|
1289
|
+
`Custom chunking ${pc.green('Yes')} ${pc.dim('No')}`,
|
|
1290
|
+
`Reranking ${pc.green('Yes')} ${pc.dim('No')}`,
|
|
1291
|
+
`Quantization ${pc.green('Yes')} ${pc.dim('No')}`,
|
|
1292
|
+
`Multimodal ${pc.green('Yes')} ${pc.dim('No')}`,
|
|
1293
|
+
`Flexible dimensions ${pc.green('Yes')} ${pc.dim('No')}`,
|
|
1294
|
+
`Mix query/doc models ${pc.green('Yes')} ${pc.dim('No')}`,
|
|
1295
|
+
`Auto-sync on update ${pc.dim('Manual')} ${pc.green('Yes')}`,
|
|
1296
|
+
`Zero code setup ${pc.dim('No')} ${pc.green('Yes')}`,
|
|
1297
|
+
``,
|
|
1298
|
+
`${pc.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`,
|
|
1299
|
+
``,
|
|
1300
|
+
`${pc.bold('RECOMMENDATION')}`,
|
|
1301
|
+
``,
|
|
1302
|
+
`${pc.dim('•')} For ${pc.cyan('Atlas users')}: Use vai — auto-embedding isn't available yet`,
|
|
1303
|
+
`${pc.dim('•')} For ${pc.cyan('production RAG')}: Use vai — you'll want reranking and custom chunking`,
|
|
1304
|
+
`${pc.dim('•')} For ${pc.cyan('quick prototypes')} on self-managed CE: Auto-embedding is faster to set up`,
|
|
1305
|
+
`${pc.dim('•')} For ${pc.cyan('complex pipelines')}: vai gives you full control over every step`,
|
|
1306
|
+
``,
|
|
1307
|
+
`${pc.bold('Migration path:')} Start with auto-embedding for simplicity, then migrate to`,
|
|
1308
|
+
`vai when you need advanced features. The models are the same — your embeddings`,
|
|
1309
|
+
`will be compatible.`,
|
|
1310
|
+
].join('\n'),
|
|
1311
|
+
links: [
|
|
1312
|
+
'https://www.mongodb.com/docs/atlas/atlas-vector-search/crud-embeddings/create-embeddings-automatic/',
|
|
1313
|
+
'https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/',
|
|
1314
|
+
],
|
|
1315
|
+
tryIt: [
|
|
1316
|
+
'vai explain auto-embedding',
|
|
1317
|
+
'vai pipeline --help',
|
|
1318
|
+
'vai chunk --help',
|
|
1319
|
+
'vai rerank --help',
|
|
1320
|
+
],
|
|
1321
|
+
},
|
|
1322
|
+
|
|
1154
1323
|
'eval-comparison': {
|
|
1155
1324
|
title: 'Evaluation Comparison — vai eval compare',
|
|
1156
1325
|
summary: 'Compare configurations and track quality over time',
|
|
@@ -1320,6 +1489,23 @@ const aliases = {
|
|
|
1320
1489
|
'save-results': 'eval-comparison',
|
|
1321
1490
|
'a-b-test': 'eval-comparison',
|
|
1322
1491
|
regression: 'eval-comparison',
|
|
1492
|
+
// Auto-embedding aliases
|
|
1493
|
+
'auto-embedding': 'auto-embedding',
|
|
1494
|
+
'auto-embed': 'auto-embedding',
|
|
1495
|
+
autoembed: 'auto-embedding',
|
|
1496
|
+
'autoEmbed': 'auto-embedding',
|
|
1497
|
+
'automatic-embedding': 'auto-embedding',
|
|
1498
|
+
'automatic-embeddings': 'auto-embedding',
|
|
1499
|
+
'atlas-auto-embed': 'auto-embedding',
|
|
1500
|
+
'mongodb-auto-embedding': 'auto-embedding',
|
|
1501
|
+
'zero-code': 'auto-embedding',
|
|
1502
|
+
// VAI vs Auto-embedding aliases
|
|
1503
|
+
'vai-vs-auto-embedding': 'vai-vs-auto-embedding',
|
|
1504
|
+
'vai-vs-autoembedding': 'vai-vs-auto-embedding',
|
|
1505
|
+
'manual-vs-auto': 'vai-vs-auto-embedding',
|
|
1506
|
+
'auto-vs-manual': 'vai-vs-auto-embedding',
|
|
1507
|
+
'which-approach': 'vai-vs-auto-embedding',
|
|
1508
|
+
'embedding-approach': 'vai-vs-auto-embedding',
|
|
1323
1509
|
// Provider comparison aliases
|
|
1324
1510
|
'provider-comparison': 'provider-comparison',
|
|
1325
1511
|
providers: 'provider-comparison',
|
|
@@ -7123,5 +7123,194 @@ init();
|
|
|
7123
7123
|
<canvas id="vsiCanvas" width="600" height="500" style="border:1px solid #3D4F58;border-radius:8px;image-rendering:pixelated;"></canvas>
|
|
7124
7124
|
<div style="color:#889397;font-size:11px;font-family:monospace;text-align:center;">← → move | SPACE shoot | ESC exit</div>
|
|
7125
7125
|
</div>
|
|
7126
|
+
|
|
7127
|
+
<!-- 🐛 Bug Reporter -->
|
|
7128
|
+
<style>
|
|
7129
|
+
.bug-floating-button{position:fixed;bottom:20px;right:20px;width:48px;height:48px;border-radius:50%;background:linear-gradient(135deg,#ff6b6b,#ee5a5a);border:none;cursor:pointer;font-size:24px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(238,90,90,0.4);transition:all .2s;z-index:9998}
|
|
7130
|
+
.bug-floating-button:hover{transform:scale(1.1);box-shadow:0 6px 16px rgba(238,90,90,0.5)}
|
|
7131
|
+
.bug-reporter-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999}
|
|
7132
|
+
.bug-reporter-modal{background:var(--bg-surface);border-radius:12px;width:90%;max-width:500px;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,0.5)}
|
|
7133
|
+
.bug-reporter-header{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border)}
|
|
7134
|
+
.bug-reporter-header h2{flex:1;margin:0;font-size:18px;font-weight:600;color:var(--accent-text)}
|
|
7135
|
+
.bug-reporter-header .close-btn{background:none;border:none;color:var(--text-muted);font-size:24px;cursor:pointer;padding:4px 8px;border-radius:4px}
|
|
7136
|
+
.bug-reporter-header .close-btn:hover{background:rgba(255,255,255,0.1);color:var(--text)}
|
|
7137
|
+
.bug-reporter-form{padding:20px}
|
|
7138
|
+
.bug-reporter-form .form-group{margin-bottom:16px}
|
|
7139
|
+
.bug-reporter-form label{display:block;font-size:13px;font-weight:500;color:var(--text-muted);margin-bottom:6px}
|
|
7140
|
+
.bug-reporter-form input,.bug-reporter-form textarea{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-input);color:var(--text);font-size:14px;font-family:inherit}
|
|
7141
|
+
.bug-reporter-form input:focus,.bug-reporter-form textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
|
|
7142
|
+
.bug-reporter-form textarea{resize:vertical;min-height:80px}
|
|
7143
|
+
.bug-env-info{padding:12px;background:var(--accent-glow);border-radius:8px;font-size:12px;margin-bottom:16px;color:var(--accent)}
|
|
7144
|
+
.bug-env-info code{color:var(--text-muted);font-size:11px}
|
|
7145
|
+
.bug-error{padding:10px 12px;background:rgba(255,107,107,0.15);border:1px solid rgba(255,107,107,0.3);border-radius:8px;color:var(--error);font-size:13px;margin-bottom:16px}
|
|
7146
|
+
.bug-actions{display:flex;gap:12px}
|
|
7147
|
+
.bug-actions button{flex:1;padding:12px 16px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s;border:none}
|
|
7148
|
+
.bug-actions .primary{background:linear-gradient(135deg,var(--accent),#00c853);color:#000}
|
|
7149
|
+
.bug-actions .primary:hover{box-shadow:0 4px 12px rgba(0,237,100,0.4)}
|
|
7150
|
+
.bug-actions .primary:disabled{opacity:0.6;cursor:not-allowed}
|
|
7151
|
+
.bug-actions .secondary{background:rgba(255,255,255,0.1);color:var(--text);border:1px solid var(--border)}
|
|
7152
|
+
.bug-actions .secondary:hover{background:rgba(255,255,255,0.15)}
|
|
7153
|
+
.bug-success{padding:40px 20px;text-align:center}
|
|
7154
|
+
.bug-success .icon{width:60px;height:60px;border-radius:50%;background:linear-gradient(135deg,var(--accent),#00c853);color:#000;font-size:32px;display:flex;align-items:center;justify-content:center;margin:0 auto 20px}
|
|
7155
|
+
.bug-success h3{margin:0 0 12px;color:var(--accent-text);font-size:20px}
|
|
7156
|
+
.bug-success p{margin:8px 0;color:var(--text-muted)}
|
|
7157
|
+
.bug-success code{background:rgba(255,255,255,0.1);padding:4px 8px;border-radius:4px;font-size:12px;color:var(--accent)}
|
|
7158
|
+
</style>
|
|
7159
|
+
|
|
7160
|
+
<button class="bug-floating-button" id="bugButton" title="Report a Bug">🐛</button>
|
|
7161
|
+
|
|
7162
|
+
<div class="bug-reporter-overlay" id="bugOverlay" style="display:none">
|
|
7163
|
+
<div class="bug-reporter-modal">
|
|
7164
|
+
<div class="bug-reporter-header">
|
|
7165
|
+
<span style="font-size:28px">🐛</span>
|
|
7166
|
+
<h2>Report a Bug</h2>
|
|
7167
|
+
<button class="close-btn" id="bugClose">×</button>
|
|
7168
|
+
</div>
|
|
7169
|
+
<div class="bug-reporter-form" id="bugForm">
|
|
7170
|
+
<div class="form-group">
|
|
7171
|
+
<label>Title *</label>
|
|
7172
|
+
<input type="text" id="bugTitle" placeholder="Brief description of the bug" maxlength="200">
|
|
7173
|
+
</div>
|
|
7174
|
+
<div class="form-group">
|
|
7175
|
+
<label>Description *</label>
|
|
7176
|
+
<textarea id="bugDescription" placeholder="What happened? What did you expect?" rows="4" maxlength="5000"></textarea>
|
|
7177
|
+
</div>
|
|
7178
|
+
<div class="form-group">
|
|
7179
|
+
<label>Steps to Reproduce</label>
|
|
7180
|
+
<textarea id="bugSteps" placeholder="1. Go to... 2. Click on... 3. See error" rows="3" maxlength="2000"></textarea>
|
|
7181
|
+
</div>
|
|
7182
|
+
<div class="form-group">
|
|
7183
|
+
<label>Email (optional, for follow-up)</label>
|
|
7184
|
+
<input type="email" id="bugEmail" placeholder="your@email.com">
|
|
7185
|
+
</div>
|
|
7186
|
+
<div class="bug-env-info">
|
|
7187
|
+
<span>📋 Environment will be included:</span><br>
|
|
7188
|
+
<code id="bugEnvInfo">Loading...</code>
|
|
7189
|
+
</div>
|
|
7190
|
+
<div class="bug-error" id="bugError" style="display:none"></div>
|
|
7191
|
+
<div class="bug-actions">
|
|
7192
|
+
<button class="primary" id="bugSubmit">Submit Bug Report</button>
|
|
7193
|
+
<button class="secondary" id="bugGithub">Open GitHub Issue</button>
|
|
7194
|
+
</div>
|
|
7195
|
+
</div>
|
|
7196
|
+
<div class="bug-success" id="bugSuccess" style="display:none">
|
|
7197
|
+
<div class="icon">✓</div>
|
|
7198
|
+
<h3>Bug Reported!</h3>
|
|
7199
|
+
<p>Bug ID: <code id="bugResultId"></code></p>
|
|
7200
|
+
<p>Thank you for helping improve Vai!</p>
|
|
7201
|
+
<div class="bug-actions" style="justify-content:center;margin-top:24px">
|
|
7202
|
+
<button class="primary" id="bugSuccessClose">Close</button>
|
|
7203
|
+
</div>
|
|
7204
|
+
</div>
|
|
7205
|
+
</div>
|
|
7206
|
+
</div>
|
|
7207
|
+
|
|
7208
|
+
<script>
|
|
7209
|
+
(function() {
|
|
7210
|
+
const BUG_API = 'https://vai.mlynn.org/api/bugs';
|
|
7211
|
+
const GITHUB_URL = 'https://github.com/mrlynn/voyageai-cli/issues/new';
|
|
7212
|
+
|
|
7213
|
+
function getEnv() {
|
|
7214
|
+
const env = { platform: navigator.platform, source: 'desktop-app' };
|
|
7215
|
+
if (window.electronAPI) {
|
|
7216
|
+
env.appVersion = window.electronAPI.appVersion || 'unknown';
|
|
7217
|
+
env.cliVersion = window.electronAPI.cliVersion || 'unknown';
|
|
7218
|
+
env.electronVersion = window.electronAPI.electronVersion || 'unknown';
|
|
7219
|
+
}
|
|
7220
|
+
env.currentScreen = document.querySelector('.tab-btn.active')?.textContent || 'unknown';
|
|
7221
|
+
return env;
|
|
7222
|
+
}
|
|
7223
|
+
|
|
7224
|
+
function showBugReporter() {
|
|
7225
|
+
const env = getEnv();
|
|
7226
|
+
document.getElementById('bugEnvInfo').textContent =
|
|
7227
|
+
`${env.platform} • App v${env.appVersion || '?'} • ${env.currentScreen}`;
|
|
7228
|
+
document.getElementById('bugOverlay').style.display = 'flex';
|
|
7229
|
+
document.getElementById('bugForm').style.display = 'block';
|
|
7230
|
+
document.getElementById('bugSuccess').style.display = 'none';
|
|
7231
|
+
document.getElementById('bugError').style.display = 'none';
|
|
7232
|
+
document.getElementById('bugTitle').value = '';
|
|
7233
|
+
document.getElementById('bugDescription').value = '';
|
|
7234
|
+
document.getElementById('bugSteps').value = '';
|
|
7235
|
+
document.getElementById('bugEmail').value = '';
|
|
7236
|
+
}
|
|
7237
|
+
|
|
7238
|
+
function hideBugReporter() {
|
|
7239
|
+
document.getElementById('bugOverlay').style.display = 'none';
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7242
|
+
async function submitBug() {
|
|
7243
|
+
const title = document.getElementById('bugTitle').value.trim();
|
|
7244
|
+
const description = document.getElementById('bugDescription').value.trim();
|
|
7245
|
+
const steps = document.getElementById('bugSteps').value.trim();
|
|
7246
|
+
const email = document.getElementById('bugEmail').value.trim();
|
|
7247
|
+
|
|
7248
|
+
if (!title || title.length < 5) {
|
|
7249
|
+
document.getElementById('bugError').textContent = 'Title is required (min 5 characters)';
|
|
7250
|
+
document.getElementById('bugError').style.display = 'block';
|
|
7251
|
+
return;
|
|
7252
|
+
}
|
|
7253
|
+
if (!description || description.length < 10) {
|
|
7254
|
+
document.getElementById('bugError').textContent = 'Description is required (min 10 characters)';
|
|
7255
|
+
document.getElementById('bugError').style.display = 'block';
|
|
7256
|
+
return;
|
|
7257
|
+
}
|
|
7258
|
+
|
|
7259
|
+
document.getElementById('bugSubmit').disabled = true;
|
|
7260
|
+
document.getElementById('bugSubmit').textContent = 'Submitting...';
|
|
7261
|
+
document.getElementById('bugError').style.display = 'none';
|
|
7262
|
+
|
|
7263
|
+
try {
|
|
7264
|
+
const env = getEnv();
|
|
7265
|
+
const res = await fetch(BUG_API, {
|
|
7266
|
+
method: 'POST',
|
|
7267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7268
|
+
body: JSON.stringify({
|
|
7269
|
+
title, description, stepsToReproduce: steps || null, email: email || null,
|
|
7270
|
+
...env
|
|
7271
|
+
})
|
|
7272
|
+
});
|
|
7273
|
+
|
|
7274
|
+
if (!res.ok) {
|
|
7275
|
+
const err = await res.json().catch(() => ({}));
|
|
7276
|
+
throw new Error(err.error || `HTTP ${res.status}`);
|
|
7277
|
+
}
|
|
7278
|
+
|
|
7279
|
+
const data = await res.json();
|
|
7280
|
+
document.getElementById('bugResultId').textContent = data.bugId;
|
|
7281
|
+
document.getElementById('bugForm').style.display = 'none';
|
|
7282
|
+
document.getElementById('bugSuccess').style.display = 'block';
|
|
7283
|
+
} catch (err) {
|
|
7284
|
+
document.getElementById('bugError').textContent = err.message;
|
|
7285
|
+
document.getElementById('bugError').style.display = 'block';
|
|
7286
|
+
} finally {
|
|
7287
|
+
document.getElementById('bugSubmit').disabled = false;
|
|
7288
|
+
document.getElementById('bugSubmit').textContent = 'Submit Bug Report';
|
|
7289
|
+
}
|
|
7290
|
+
}
|
|
7291
|
+
|
|
7292
|
+
function openGithub() {
|
|
7293
|
+
const title = document.getElementById('bugTitle').value.trim() || 'Bug Report';
|
|
7294
|
+
const description = document.getElementById('bugDescription').value.trim();
|
|
7295
|
+
const steps = document.getElementById('bugSteps').value.trim();
|
|
7296
|
+
const env = getEnv();
|
|
7297
|
+
|
|
7298
|
+
const body = `## Description\n${description}\n\n## Steps to Reproduce\n${steps || '1. \\n2. \\n3. '}\n\n## Environment\n- **App Version:** ${env.appVersion || 'N/A'}\n- **Platform:** ${env.platform}\n- **Screen:** ${env.currentScreen}`;
|
|
7299
|
+
const url = `${GITHUB_URL}?title=${encodeURIComponent('[Bug] ' + title)}&body=${encodeURIComponent(body)}&labels=bug`;
|
|
7300
|
+
window.open(url, '_blank');
|
|
7301
|
+
}
|
|
7302
|
+
|
|
7303
|
+
// Event listeners
|
|
7304
|
+
document.getElementById('bugButton').addEventListener('click', showBugReporter);
|
|
7305
|
+
document.getElementById('bugClose').addEventListener('click', hideBugReporter);
|
|
7306
|
+
document.getElementById('bugOverlay').addEventListener('click', (e) => {
|
|
7307
|
+
if (e.target.id === 'bugOverlay') hideBugReporter();
|
|
7308
|
+
});
|
|
7309
|
+
document.getElementById('bugSubmit').addEventListener('click', submitBug);
|
|
7310
|
+
document.getElementById('bugGithub').addEventListener('click', openGithub);
|
|
7311
|
+
document.getElementById('bugSuccessClose').addEventListener('click', hideBugReporter);
|
|
7312
|
+
})();
|
|
7313
|
+
</script>
|
|
7314
|
+
|
|
7126
7315
|
</body>
|
|
7127
7316
|
</html>
|