voyageai-cli 1.24.0 → 1.26.0

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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +2 -0
  3. package/src/commands/about.js +1 -1
  4. package/src/commands/bug.js +1 -1
  5. package/src/commands/playground.js +31 -0
  6. package/src/commands/scaffold.js +23 -1
  7. package/src/commands/workflow.js +336 -0
  8. package/src/lib/explanations.js +53 -0
  9. package/src/lib/scaffold-structure.js +8 -9
  10. package/src/lib/telemetry.js +1 -1
  11. package/src/lib/template-engine.js +240 -0
  12. package/src/lib/templates/nextjs/README.md.tpl +78 -55
  13. package/src/lib/templates/nextjs/favicon.svg.tpl +11 -0
  14. package/src/lib/templates/nextjs/footer.jsx.tpl +49 -0
  15. package/src/lib/templates/nextjs/layout.jsx.tpl +16 -10
  16. package/src/lib/templates/nextjs/lib-mongo.js.tpl +5 -5
  17. package/src/lib/templates/nextjs/lib-voyage.js.tpl +13 -8
  18. package/src/lib/templates/nextjs/navbar.jsx.tpl +98 -0
  19. package/src/lib/templates/nextjs/page-home.jsx.tpl +201 -0
  20. package/src/lib/templates/nextjs/page-search.jsx.tpl +184 -82
  21. package/src/lib/templates/nextjs/theme-registry.jsx.tpl +51 -0
  22. package/src/lib/templates/nextjs/theme.js.tpl +138 -65
  23. package/src/lib/templates/nextjs/vai-logo-256.png +0 -0
  24. package/src/lib/workflow-utils.js +65 -0
  25. package/src/lib/workflow.js +1259 -0
  26. package/src/mcp/tools/management.js +1 -60
  27. package/src/playground/icons/dark/128.png +0 -0
  28. package/src/playground/icons/dark/16.png +0 -0
  29. package/src/playground/icons/dark/256.png +0 -0
  30. package/src/playground/icons/dark/32.png +0 -0
  31. package/src/playground/icons/dark/64.png +0 -0
  32. package/src/playground/icons/light/128.png +0 -0
  33. package/src/playground/icons/light/16.png +0 -0
  34. package/src/playground/icons/light/256.png +0 -0
  35. package/src/playground/icons/light/32.png +0 -0
  36. package/src/playground/icons/light/64.png +0 -0
  37. package/src/playground/icons/watermark.png +0 -0
  38. package/src/playground/index.html +125 -73
  39. package/src/workflows/consistency-check.json +64 -0
  40. package/src/workflows/cost-analysis.json +69 -0
  41. package/src/workflows/multi-collection-search.json +80 -0
  42. package/src/workflows/research-and-summarize.json +46 -0
  43. 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
- A semantic search application powered by Voyage AI embeddings, MongoDB Atlas Vector Search, and Next.js with Material UI.
3
+ > Semantic search application powered by **Voyage AI** embeddings, **MongoDB Atlas Vector Search**, and **Next.js** with Material UI.
4
4
 
5
- ## Configuration
5
+ [![Generated by vai](https://img.shields.io/badge/scaffolded%20with-vai-00ED64?style=flat-square)](https://github.com/mrlynn/voyageai-cli)
6
+ [![Next.js](https://img.shields.io/badge/Next.js-14-black?style=flat-square&logo=next.js)](https://nextjs.org)
7
+ [![MUI](https://img.shields.io/badge/MUI-5-007FFF?style=flat-square&logo=mui)](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
- ## Setup
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
- ```bash
31
- cp .env.example .env.local
46
+ ```env
47
+ VOYAGE_API_KEY= # From dash.voyageai.com
48
+ MONGODB_URI= # Atlas connection string
32
49
  ```
33
50
 
34
- Required variables:
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
- ### 3. Create vector index
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
- "type": "vector",
47
- "path": "{{field}}",
48
- "numDimensions": {{dimensions}},
49
- "similarity": "cosine"
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 the index `{{index}}`.
56
-
57
- ### 4. Start the development server
58
-
59
- ```bash
60
- npm run dev
61
- ```
66
+ Name it **`{{index}}`**.
62
67
 
63
- Open [http://localhost:3000](http://localhost:3000) to see the search interface.
68
+ ---
64
69
 
65
- ## Project Structure
70
+ ## 📁 Project Structure
66
71
 
67
72
  ```
68
73
  {{projectName}}/
69
74
  ├── app/
70
- │ ├── layout.jsx # Root layout with MUI theme
71
- │ ├── page.jsx # Home page
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 # Search endpoint
76
- │ └── ingest/route.js # Ingest endpoint
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 # Voyage AI client
79
- │ ├── mongodb.js # MongoDB connection
80
- │ └── theme.js # MUI theme
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
- ├── package.json
83
- └── README.md
91
+ └── package.json
84
92
  ```
85
93
 
86
- ## API Endpoints
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 MUI Theme
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 { ThemeProvider } from '@mui/material/styles';
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
- <ThemeProvider theme={theme}>
22
- <CssBaseline />
28
+ <ThemeRegistry>
23
29
  {children}
24
- </ThemeProvider>
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
- const MONGODB_URI = process.env.MONGODB_URI;
11
-
12
- if (!MONGODB_URI) {
13
- throw new Error('MONGODB_URI environment variable is required');
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(MONGODB_URI);
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
- const VOYAGE_API_URL = process.env.VOYAGE_API_URL || 'https://api.voyageai.com/v1';
10
- const VOYAGE_API_KEY = process.env.VOYAGE_API_KEY;
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
- if (!VOYAGE_API_KEY) {
17
- throw new Error('VOYAGE_API_KEY environment variable is required');
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
- if (!VOYAGE_API_KEY) {
70
- throw new Error('VOYAGE_API_KEY environment variable is required');
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
+ }