voyageai-cli 1.24.0 → 1.26.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 +1 -1
- package/src/cli.js +2 -0
- package/src/commands/about.js +1 -1
- package/src/commands/bug.js +1 -1
- package/src/commands/chat.js +281 -78
- package/src/commands/playground.js +73 -19
- package/src/commands/scaffold.js +23 -1
- package/src/commands/workflow.js +336 -0
- package/src/lib/chat.js +170 -4
- package/src/lib/explanations.js +53 -0
- package/src/lib/llm.js +304 -2
- package/src/lib/mongo.js +6 -6
- package/src/lib/prompt.js +60 -1
- package/src/lib/scaffold-structure.js +8 -9
- package/src/lib/telemetry.js +1 -1
- package/src/lib/template-engine.js +240 -0
- package/src/lib/templates/nextjs/README.md.tpl +78 -55
- package/src/lib/templates/nextjs/favicon.svg.tpl +11 -0
- package/src/lib/templates/nextjs/footer.jsx.tpl +49 -0
- package/src/lib/templates/nextjs/layout.jsx.tpl +16 -10
- package/src/lib/templates/nextjs/lib-mongo.js.tpl +5 -5
- package/src/lib/templates/nextjs/lib-voyage.js.tpl +13 -8
- package/src/lib/templates/nextjs/navbar.jsx.tpl +98 -0
- package/src/lib/templates/nextjs/page-home.jsx.tpl +201 -0
- package/src/lib/templates/nextjs/page-search.jsx.tpl +184 -82
- package/src/lib/templates/nextjs/theme-registry.jsx.tpl +51 -0
- package/src/lib/templates/nextjs/theme.js.tpl +138 -65
- package/src/lib/templates/nextjs/vai-logo-256.png +0 -0
- package/src/lib/tool-registry.js +194 -0
- package/src/lib/workflow-utils.js +65 -0
- package/src/lib/workflow.js +1259 -0
- package/src/mcp/tools/embedding.js +55 -43
- package/src/mcp/tools/ingest.js +74 -67
- package/src/mcp/tools/management.js +54 -101
- package/src/mcp/tools/retrieval.js +181 -163
- package/src/mcp/tools/utility.js +171 -153
- package/src/playground/icons/dark/128.png +0 -0
- package/src/playground/icons/dark/16.png +0 -0
- package/src/playground/icons/dark/256.png +0 -0
- package/src/playground/icons/dark/32.png +0 -0
- package/src/playground/icons/dark/64.png +0 -0
- package/src/playground/icons/light/128.png +0 -0
- package/src/playground/icons/light/16.png +0 -0
- package/src/playground/icons/light/256.png +0 -0
- package/src/playground/icons/light/32.png +0 -0
- package/src/playground/icons/light/64.png +0 -0
- package/src/playground/icons/watermark.png +0 -0
- package/src/playground/index.html +633 -83
- package/src/workflows/consistency-check.json +64 -0
- package/src/workflows/cost-analysis.json +69 -0
- package/src/workflows/multi-collection-search.json +80 -0
- package/src/workflows/research-and-summarize.json +46 -0
- package/src/workflows/smart-ingest.json +63 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Template expression engine for vai workflows.
|
|
5
|
+
*
|
|
6
|
+
* Resolves {{ expression }} strings against a context object.
|
|
7
|
+
* Supports dot-path access and array indexing only.
|
|
8
|
+
* No eval(), no function calls, no arithmetic.
|
|
9
|
+
*
|
|
10
|
+
* Grammar:
|
|
11
|
+
* expression = segment ("." segment)*
|
|
12
|
+
* segment = identifier ("[" index "]")?
|
|
13
|
+
* identifier = [a-zA-Z_][a-zA-Z0-9_]*
|
|
14
|
+
* index = [0-9]+
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Matches {{ path.to.value }} including {{ path[0].field }}
|
|
18
|
+
const TEMPLATE_RE = /\{\{\s*(.+?)\s*\}\}/g;
|
|
19
|
+
|
|
20
|
+
// A string that is exactly one template expression with no surrounding text
|
|
21
|
+
// Use [^}] to prevent matching across multiple {{ }} pairs
|
|
22
|
+
const SOLE_TEMPLATE_RE = /^\{\{\s*([^}]+?)\s*\}\}$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if a string contains template expressions.
|
|
26
|
+
* @param {string} str
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function isTemplateString(str) {
|
|
30
|
+
if (typeof str !== 'string') return false;
|
|
31
|
+
// Use a fresh regex to avoid global lastIndex state issues
|
|
32
|
+
return /\{\{\s*(.+?)\s*\}\}/.test(str);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a dot-path expression into segments.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} expr - e.g. "search_api.output.results[0].content"
|
|
39
|
+
* @returns {Array<{key: string, index?: number}>}
|
|
40
|
+
* @throws {Error} on invalid syntax
|
|
41
|
+
*/
|
|
42
|
+
function parseExpression(expr) {
|
|
43
|
+
const trimmed = expr.trim();
|
|
44
|
+
if (!trimmed) throw new Error('Empty expression');
|
|
45
|
+
|
|
46
|
+
const segments = [];
|
|
47
|
+
const parts = trimmed.split('.');
|
|
48
|
+
|
|
49
|
+
for (const part of parts) {
|
|
50
|
+
// Check for array indexing: identifier[index]
|
|
51
|
+
const bracketMatch = part.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[(\d+)\]$/);
|
|
52
|
+
if (bracketMatch) {
|
|
53
|
+
segments.push({ key: bracketMatch[1], index: parseInt(bracketMatch[2], 10) });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Plain identifier
|
|
58
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(part)) {
|
|
59
|
+
segments.push({ key: part });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error(`Invalid expression segment: "${part}" in "${expr}"`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return segments;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a parsed path against a context object.
|
|
71
|
+
* Returns undefined (no throw) if any segment is missing.
|
|
72
|
+
*
|
|
73
|
+
* @param {Array<{key: string, index?: number}>} segments
|
|
74
|
+
* @param {object} context
|
|
75
|
+
* @returns {*}
|
|
76
|
+
*/
|
|
77
|
+
function resolvePath(segments, context) {
|
|
78
|
+
let current = context;
|
|
79
|
+
|
|
80
|
+
for (const seg of segments) {
|
|
81
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
82
|
+
|
|
83
|
+
current = current[seg.key];
|
|
84
|
+
if (current == null) return current; // null or undefined
|
|
85
|
+
|
|
86
|
+
if (seg.index !== undefined) {
|
|
87
|
+
if (!Array.isArray(current)) return undefined;
|
|
88
|
+
current = current[seg.index];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return current;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve template expressions within a single string.
|
|
97
|
+
*
|
|
98
|
+
* If the entire string is a single template expression, return the resolved
|
|
99
|
+
* value directly (preserving type: array, number, object, etc.).
|
|
100
|
+
* If the string contains templates mixed with text, return a string with
|
|
101
|
+
* substitutions.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} str
|
|
104
|
+
* @param {object} context
|
|
105
|
+
* @returns {*}
|
|
106
|
+
*/
|
|
107
|
+
function resolveString(str, context) {
|
|
108
|
+
// Fast path: no templates
|
|
109
|
+
if (!str.includes('{{')) return str;
|
|
110
|
+
|
|
111
|
+
// Check if the entire string is a single template expression
|
|
112
|
+
const soleMatch = str.match(SOLE_TEMPLATE_RE);
|
|
113
|
+
if (soleMatch) {
|
|
114
|
+
try {
|
|
115
|
+
const segments = parseExpression(soleMatch[1]);
|
|
116
|
+
return resolvePath(segments, context);
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Mixed text + templates: substitute all, coerce to string
|
|
123
|
+
return str.replace(TEMPLATE_RE, (_match, expr) => {
|
|
124
|
+
try {
|
|
125
|
+
const segments = parseExpression(expr);
|
|
126
|
+
const value = resolvePath(segments, context);
|
|
127
|
+
if (value === undefined) return '';
|
|
128
|
+
if (value === null) return 'null';
|
|
129
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
130
|
+
return String(value);
|
|
131
|
+
} catch {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Recursively resolve all template expressions in a value.
|
|
139
|
+
* Handles strings, arrays, and plain objects.
|
|
140
|
+
*
|
|
141
|
+
* @param {*} value
|
|
142
|
+
* @param {object} context
|
|
143
|
+
* @returns {*}
|
|
144
|
+
*/
|
|
145
|
+
function resolveTemplate(value, context) {
|
|
146
|
+
if (typeof value === 'string') {
|
|
147
|
+
return resolveString(value, context);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(value)) {
|
|
151
|
+
return value.map(item => resolveTemplate(item, context));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (value !== null && typeof value === 'object') {
|
|
155
|
+
const resolved = {};
|
|
156
|
+
for (const [k, v] of Object.entries(value)) {
|
|
157
|
+
resolved[k] = resolveTemplate(v, context);
|
|
158
|
+
}
|
|
159
|
+
return resolved;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// numbers, booleans, null, undefined pass through
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Deep-resolve all template expressions in an object tree.
|
|
168
|
+
* Alias for resolveTemplate when called on an object.
|
|
169
|
+
*
|
|
170
|
+
* @param {object} obj
|
|
171
|
+
* @param {object} context
|
|
172
|
+
* @returns {object}
|
|
173
|
+
*/
|
|
174
|
+
function resolveAllTemplates(obj, context) {
|
|
175
|
+
return resolveTemplate(obj, context);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extract step IDs referenced by template expressions in an object tree.
|
|
180
|
+
* Ignores "inputs" and "defaults" prefixes (those are workflow-level, not step refs).
|
|
181
|
+
*
|
|
182
|
+
* @param {*} obj - Step inputs, condition, forEach value
|
|
183
|
+
* @returns {Set<string>} Set of step IDs referenced
|
|
184
|
+
*/
|
|
185
|
+
function extractDependencies(obj) {
|
|
186
|
+
const deps = new Set();
|
|
187
|
+
|
|
188
|
+
function scan(value) {
|
|
189
|
+
if (typeof value === 'string') {
|
|
190
|
+
// Reset regex state
|
|
191
|
+
const re = /\{\{\s*(.+?)\s*\}\}/g;
|
|
192
|
+
let match;
|
|
193
|
+
while ((match = re.exec(value)) !== null) {
|
|
194
|
+
const expr = match[1].trim();
|
|
195
|
+
// Extract the first segment (root identifier)
|
|
196
|
+
const dotIdx = expr.indexOf('.');
|
|
197
|
+
const bracketIdx = expr.indexOf('[');
|
|
198
|
+
let root;
|
|
199
|
+
if (dotIdx === -1 && bracketIdx === -1) {
|
|
200
|
+
root = expr;
|
|
201
|
+
} else if (dotIdx === -1) {
|
|
202
|
+
root = expr.slice(0, bracketIdx);
|
|
203
|
+
} else if (bracketIdx === -1) {
|
|
204
|
+
root = expr.slice(0, dotIdx);
|
|
205
|
+
} else {
|
|
206
|
+
root = expr.slice(0, Math.min(dotIdx, bracketIdx));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Skip workflow-level references
|
|
210
|
+
if (root !== 'inputs' && root !== 'defaults') {
|
|
211
|
+
deps.add(root);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (Array.isArray(value)) {
|
|
218
|
+
value.forEach(scan);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (value !== null && typeof value === 'object') {
|
|
223
|
+
Object.values(value).forEach(scan);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
scan(obj);
|
|
228
|
+
return deps;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
isTemplateString,
|
|
233
|
+
parseExpression,
|
|
234
|
+
resolvePath,
|
|
235
|
+
resolveString,
|
|
236
|
+
resolveTemplate,
|
|
237
|
+
resolveAllTemplates,
|
|
238
|
+
extractDependencies,
|
|
239
|
+
TEMPLATE_RE,
|
|
240
|
+
};
|
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
# {{projectName}}
|
|
1
|
+
# 🔍 {{projectName}}
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Semantic search application powered by **Voyage AI** embeddings, **MongoDB Atlas Vector Search**, and **Next.js** with Material UI.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://github.com/mrlynn/voyageai-cli)
|
|
6
|
+
[](https://nextjs.org)
|
|
7
|
+
[](https://mui.com)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ⚡ Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 1. Install
|
|
15
|
+
npm install
|
|
16
|
+
|
|
17
|
+
# 2. Configure
|
|
18
|
+
cp .env.example .env.local
|
|
19
|
+
# → Edit .env.local with your Voyage AI key + MongoDB URI
|
|
20
|
+
|
|
21
|
+
# 3. Create vector index (see below)
|
|
22
|
+
|
|
23
|
+
# 4. Run
|
|
24
|
+
npm run dev
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Open **[http://localhost:3000](http://localhost:3000)** — you'll see a branded landing page with a link to the search UI.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🔧 Configuration
|
|
6
32
|
|
|
7
33
|
| Setting | Value |
|
|
8
34
|
|---------|-------|
|
|
9
35
|
| Embedding Model | `{{model}}` |
|
|
10
|
-
| Dimensions | {{dimensions}} |
|
|
36
|
+
| Dimensions | `{{dimensions}}` |
|
|
11
37
|
| Database | `{{db}}` |
|
|
12
38
|
| Collection | `{{collection}}` |
|
|
13
39
|
| Vector Index | `{{index}}` |
|
|
@@ -15,77 +41,74 @@ A semantic search application powered by Voyage AI embeddings, MongoDB Atlas Vec
|
|
|
15
41
|
| Rerank Model | `{{rerankModel}}` |
|
|
16
42
|
{{/if}}
|
|
17
43
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
### 1. Install dependencies
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### 2. Configure environment
|
|
27
|
-
|
|
28
|
-
Copy `.env.example` to `.env.local` and fill in your credentials:
|
|
44
|
+
### Environment Variables
|
|
29
45
|
|
|
30
|
-
```
|
|
31
|
-
|
|
46
|
+
```env
|
|
47
|
+
VOYAGE_API_KEY= # From dash.voyageai.com
|
|
48
|
+
MONGODB_URI= # Atlas connection string
|
|
32
49
|
```
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
- `VOYAGE_API_KEY` - Your Voyage AI API key from [dash.voyageai.com](https://dash.voyageai.com)
|
|
36
|
-
- `MONGODB_URI` - Your MongoDB Atlas connection string
|
|
51
|
+
### Vector Search Index
|
|
37
52
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
In MongoDB Atlas, create a vector search index on your collection:
|
|
53
|
+
Create this index on your `{{collection}}` collection in Atlas:
|
|
41
54
|
|
|
42
55
|
```json
|
|
43
56
|
{
|
|
44
|
-
"fields": [
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
]
|
|
57
|
+
"fields": [{
|
|
58
|
+
"type": "vector",
|
|
59
|
+
"path": "{{field}}",
|
|
60
|
+
"numDimensions": {{dimensions}},
|
|
61
|
+
"similarity": "cosine"
|
|
62
|
+
}]
|
|
52
63
|
}
|
|
53
64
|
```
|
|
54
65
|
|
|
55
|
-
Name
|
|
56
|
-
|
|
57
|
-
### 4. Start the development server
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npm run dev
|
|
61
|
-
```
|
|
66
|
+
Name it **`{{index}}`**.
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
---
|
|
64
69
|
|
|
65
|
-
## Project Structure
|
|
70
|
+
## 📁 Project Structure
|
|
66
71
|
|
|
67
72
|
```
|
|
68
73
|
{{projectName}}/
|
|
69
74
|
├── app/
|
|
70
|
-
│ ├── layout.jsx
|
|
71
|
-
│ ├── page.jsx
|
|
72
|
-
│ ├── search/
|
|
73
|
-
│ │ └── page.jsx # Search page (MUI)
|
|
75
|
+
│ ├── layout.jsx # Root layout (Inter font, theme)
|
|
76
|
+
│ ├── page.jsx # Landing page (hero + features)
|
|
77
|
+
│ ├── search/page.jsx # Search UI (score bars, copy, fade-in)
|
|
74
78
|
│ └── api/
|
|
75
|
-
│ ├── search/route.js
|
|
76
|
-
│ └── ingest/route.js
|
|
79
|
+
│ ├── search/route.js # POST /api/search
|
|
80
|
+
│ └── ingest/route.js # POST /api/ingest
|
|
81
|
+
├── components/
|
|
82
|
+
│ ├── ThemeRegistry.jsx # Light/dark mode with persistence
|
|
83
|
+
│ ├── Navbar.jsx # Branded AppBar + dark toggle
|
|
84
|
+
│ └── Footer.jsx # Powered-by footer
|
|
77
85
|
├── lib/
|
|
78
|
-
│ ├── voyage.js
|
|
79
|
-
│ ├── mongodb.js
|
|
80
|
-
│ └── theme.js
|
|
86
|
+
│ ├── voyage.js # Voyage AI client
|
|
87
|
+
│ ├── mongodb.js # MongoDB connection + vector search
|
|
88
|
+
│ └── theme.js # MUI theme (light + dark palettes)
|
|
89
|
+
├── public/favicon.svg
|
|
81
90
|
├── .env.example
|
|
82
|
-
|
|
83
|
-
└── README.md
|
|
91
|
+
└── package.json
|
|
84
92
|
```
|
|
85
93
|
|
|
86
|
-
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 🌗 Features
|
|
97
|
+
|
|
98
|
+
- **Dark mode** — auto-detects system preference, persists choice
|
|
99
|
+
- **Score visualization** — color-coded progress bars for relevance scores
|
|
100
|
+
- **Copy to clipboard** — one-click copy on each result
|
|
101
|
+
- **Responsive** — works on mobile and desktop
|
|
102
|
+
- **Branded** — vai + Voyage AI + MongoDB branding throughout
|
|
103
|
+
{{#if rerank}}
|
|
104
|
+
- **Reranking** — results refined with {{rerankModel}} for better relevance
|
|
105
|
+
{{/if}}
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 🔌 API Endpoints
|
|
87
110
|
|
|
88
|
-
### POST /api/search
|
|
111
|
+
### `POST /api/search`
|
|
89
112
|
|
|
90
113
|
```bash
|
|
91
114
|
curl -X POST http://localhost:3000/api/search \
|
|
@@ -93,7 +116,7 @@ curl -X POST http://localhost:3000/api/search \
|
|
|
93
116
|
-d '{"query": "How does vector search work?", "limit": 5}'
|
|
94
117
|
```
|
|
95
118
|
|
|
96
|
-
### POST /api/ingest
|
|
119
|
+
### `POST /api/ingest`
|
|
97
120
|
|
|
98
121
|
```bash
|
|
99
122
|
curl -X POST http://localhost:3000/api/ingest \
|
|
@@ -103,4 +126,4 @@ curl -X POST http://localhost:3000/api/ingest \
|
|
|
103
126
|
|
|
104
127
|
---
|
|
105
128
|
|
|
106
|
-
Generated by [vai](https://github.com/mrlynn/voyageai-cli) v{{vaiVersion}}
|
|
129
|
+
<sub>Generated by [vai](https://github.com/mrlynn/voyageai-cli) v{{vaiVersion}} on {{generatedAt}}</sub>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="vg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#00ED64"/>
|
|
5
|
+
<stop offset="100%" stop-color="#00D4AA"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="32" height="32" rx="8" fill="#001E2B"/>
|
|
9
|
+
<path d="M8 8 L16 26 L24 8" fill="none" stroke="url(#vg)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
10
|
+
<text x="18" y="20" font-family="sans-serif" font-size="7" font-weight="bold" fill="url(#vg)">ai</text>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded footer
|
|
3
|
+
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import { Box, Typography, Link, Stack, Divider } from '@mui/material';
|
|
9
|
+
|
|
10
|
+
export default function Footer() {
|
|
11
|
+
return (
|
|
12
|
+
<Box component="footer" sx={{ mt: 'auto', pt: 6, pb: 4 }}>
|
|
13
|
+
<Divider sx={{ mb: 3 }} />
|
|
14
|
+
<Stack
|
|
15
|
+
direction={{ xs: 'column', sm: 'row' }}
|
|
16
|
+
justifyContent="space-between"
|
|
17
|
+
alignItems="center"
|
|
18
|
+
spacing={1}
|
|
19
|
+
sx={{ px: 3 }}
|
|
20
|
+
>
|
|
21
|
+
<Typography variant="caption" color="text.secondary">
|
|
22
|
+
Built with{' '}
|
|
23
|
+
<Link
|
|
24
|
+
href="https://github.com/mrlynn/voyageai-cli"
|
|
25
|
+
target="_blank"
|
|
26
|
+
rel="noopener"
|
|
27
|
+
underline="hover"
|
|
28
|
+
color="primary"
|
|
29
|
+
fontWeight={600}
|
|
30
|
+
>
|
|
31
|
+
vai
|
|
32
|
+
</Link>
|
|
33
|
+
{' '}· Powered by{' '}
|
|
34
|
+
<Link href="https://www.voyageai.com" target="_blank" rel="noopener" underline="hover" color="inherit">
|
|
35
|
+
Voyage AI
|
|
36
|
+
</Link>
|
|
37
|
+
{' '}+{' '}
|
|
38
|
+
<Link href="https://www.mongodb.com/atlas/search" target="_blank" rel="noopener" underline="hover" color="inherit">
|
|
39
|
+
MongoDB Atlas Vector Search
|
|
40
|
+
</Link>
|
|
41
|
+
</Typography>
|
|
42
|
+
|
|
43
|
+
<Typography variant="caption" color="text.secondary" sx={{ fontFamily: 'monospace', fontSize: '0.65rem' }}>
|
|
44
|
+
v{{vaiVersion}}
|
|
45
|
+
</Typography>
|
|
46
|
+
</Stack>
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Root Layout with
|
|
2
|
+
* Root Layout with vai branding
|
|
3
3
|
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
|
|
7
|
-
import {
|
|
8
|
-
import CssBaseline from '@mui/material/CssBaseline';
|
|
9
|
-
import { theme } from '@/lib/theme';
|
|
7
|
+
import { ThemeRegistry } from '@/components/ThemeRegistry';
|
|
10
8
|
|
|
11
9
|
export const metadata = {
|
|
12
|
-
title: '{{projectName}}',
|
|
13
|
-
description: 'Semantic search powered by Voyage AI',
|
|
10
|
+
title: '{{projectName}} — Semantic Search',
|
|
11
|
+
description: 'Semantic search powered by Voyage AI embeddings and MongoDB Atlas Vector Search.',
|
|
12
|
+
icons: { icon: '/favicon.svg' },
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
export default function RootLayout({ children }) {
|
|
17
16
|
return (
|
|
18
|
-
<html lang="en">
|
|
17
|
+
<html lang="en" suppressHydrationWarning>
|
|
18
|
+
<head>
|
|
19
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
20
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
21
|
+
<link
|
|
22
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
|
23
|
+
rel="stylesheet"
|
|
24
|
+
/>
|
|
25
|
+
</head>
|
|
19
26
|
<body>
|
|
20
27
|
<AppRouterCacheProvider>
|
|
21
|
-
<
|
|
22
|
-
<CssBaseline />
|
|
28
|
+
<ThemeRegistry>
|
|
23
29
|
{children}
|
|
24
|
-
</
|
|
30
|
+
</ThemeRegistry>
|
|
25
31
|
</AppRouterCacheProvider>
|
|
26
32
|
</body>
|
|
27
33
|
</html>
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { MongoClient } from 'mongodb';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (!MONGODB_URI)
|
|
13
|
-
|
|
10
|
+
function getUri() {
|
|
11
|
+
const uri = process.env.MONGODB_URI;
|
|
12
|
+
if (!uri) throw new Error('MONGODB_URI environment variable is required');
|
|
13
|
+
return uri;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Cache the client promise for connection reuse in serverless
|
|
@@ -30,7 +30,7 @@ export async function getMongoClient() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (!cached.promise) {
|
|
33
|
-
cached.promise = MongoClient.connect(
|
|
33
|
+
cached.promise = MongoClient.connect(getUri());
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
cached.conn = await cached.promise;
|
|
@@ -6,16 +6,22 @@
|
|
|
6
6
|
* Dimensions: {{dimensions}}
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
function getApiUrl() {
|
|
10
|
+
return process.env.VOYAGE_API_URL || 'https://api.voyageai.com/v1';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getApiKey() {
|
|
14
|
+
const key = process.env.VOYAGE_API_KEY;
|
|
15
|
+
if (!key) throw new Error('VOYAGE_API_KEY environment variable is required');
|
|
16
|
+
return key;
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* Generate embeddings for text(s) using Voyage AI.
|
|
14
21
|
*/
|
|
15
22
|
export async function embed(input, options = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
23
|
+
const VOYAGE_API_URL = getApiUrl();
|
|
24
|
+
const VOYAGE_API_KEY = getApiKey();
|
|
19
25
|
|
|
20
26
|
const texts = Array.isArray(input) ? input : [input];
|
|
21
27
|
|
|
@@ -66,9 +72,8 @@ export async function embedDocuments(documents, options = {}) {
|
|
|
66
72
|
* Rerank documents by relevance to a query.
|
|
67
73
|
*/
|
|
68
74
|
export async function rerank(query, documents, options = {}) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
75
|
+
const VOYAGE_API_URL = getApiUrl();
|
|
76
|
+
const VOYAGE_API_KEY = getApiKey();
|
|
72
77
|
|
|
73
78
|
const response = await fetch(`${VOYAGE_API_URL}/rerank`, {
|
|
74
79
|
method: 'POST',
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded AppBar with dark mode toggle
|
|
3
|
+
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AppBar,
|
|
10
|
+
Toolbar,
|
|
11
|
+
Typography,
|
|
12
|
+
IconButton,
|
|
13
|
+
Box,
|
|
14
|
+
Chip,
|
|
15
|
+
Tooltip,
|
|
16
|
+
useTheme,
|
|
17
|
+
} from '@mui/material';
|
|
18
|
+
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
|
19
|
+
import LightModeIcon from '@mui/icons-material/LightMode';
|
|
20
|
+
import GitHubIcon from '@mui/icons-material/GitHub';
|
|
21
|
+
import { useColorMode } from './ThemeRegistry';
|
|
22
|
+
|
|
23
|
+
export default function Navbar() {
|
|
24
|
+
const theme = useTheme();
|
|
25
|
+
const { mode, toggle } = useColorMode();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<AppBar position="sticky" elevation={0} color="transparent">
|
|
29
|
+
<Toolbar sx={{ gap: 1.5 }}>
|
|
30
|
+
{/* Logo / wordmark */}
|
|
31
|
+
<Box
|
|
32
|
+
component="img"
|
|
33
|
+
src="/vai-logo.png"
|
|
34
|
+
alt="vai logo"
|
|
35
|
+
sx={{ height: 28, width: 28 }}
|
|
36
|
+
/>
|
|
37
|
+
<Typography
|
|
38
|
+
variant="h6"
|
|
39
|
+
sx={{
|
|
40
|
+
fontWeight: 800,
|
|
41
|
+
letterSpacing: '-0.02em',
|
|
42
|
+
background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`,
|
|
43
|
+
WebkitBackgroundClip: 'text',
|
|
44
|
+
WebkitTextFillColor: 'transparent',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{{projectName}}
|
|
48
|
+
</Typography>
|
|
49
|
+
|
|
50
|
+
<Chip
|
|
51
|
+
label="vai"
|
|
52
|
+
size="small"
|
|
53
|
+
sx={{
|
|
54
|
+
fontWeight: 700,
|
|
55
|
+
fontSize: '0.65rem',
|
|
56
|
+
height: 20,
|
|
57
|
+
bgcolor: 'primary.main',
|
|
58
|
+
color: 'primary.contrastText',
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<Box sx={{ flex: 1 }} />
|
|
63
|
+
|
|
64
|
+
{/* Model badge */}
|
|
65
|
+
<Chip
|
|
66
|
+
label="{{model}}"
|
|
67
|
+
size="small"
|
|
68
|
+
variant="outlined"
|
|
69
|
+
sx={{
|
|
70
|
+
display: { xs: 'none', sm: 'flex' },
|
|
71
|
+
fontFamily: 'monospace',
|
|
72
|
+
fontSize: '0.7rem',
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
{/* GitHub link */}
|
|
77
|
+
<Tooltip title="View on GitHub">
|
|
78
|
+
<IconButton
|
|
79
|
+
size="small"
|
|
80
|
+
href="https://github.com/mrlynn/voyageai-cli"
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener"
|
|
83
|
+
sx={{ color: 'text.secondary' }}
|
|
84
|
+
>
|
|
85
|
+
<GitHubIcon fontSize="small" />
|
|
86
|
+
</IconButton>
|
|
87
|
+
</Tooltip>
|
|
88
|
+
|
|
89
|
+
{/* Dark mode toggle */}
|
|
90
|
+
<Tooltip title={mode === 'dark' ? 'Light mode' : 'Dark mode'}>
|
|
91
|
+
<IconButton size="small" onClick={toggle} sx={{ color: 'text.secondary' }}>
|
|
92
|
+
{mode === 'dark' ? <LightModeIcon fontSize="small" /> : <DarkModeIcon fontSize="small" />}
|
|
93
|
+
</IconButton>
|
|
94
|
+
</Tooltip>
|
|
95
|
+
</Toolbar>
|
|
96
|
+
</AppBar>
|
|
97
|
+
);
|
|
98
|
+
}
|