@tpitre/story-ui 4.14.0 → 4.16.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.
- package/dist/cli/setup.js +3 -3
- package/dist/mcp-server/index.js +0 -6
- package/dist/mcp-server/routes/canvasGenerate.d.ts +23 -1
- package/dist/mcp-server/routes/canvasGenerate.d.ts.map +1 -1
- package/dist/mcp-server/routes/canvasGenerate.js +179 -33
- package/dist/mcp-server/routes/canvasSave.d.ts +7 -0
- package/dist/mcp-server/routes/canvasSave.d.ts.map +1 -1
- package/dist/mcp-server/routes/canvasSave.js +21 -7
- package/dist/story-generator/llm-providers/claude-provider.d.ts.map +1 -1
- package/dist/story-generator/llm-providers/claude-provider.js +0 -39
- package/dist/story-generator/llm-providers/gemini-provider.d.ts.map +1 -1
- package/dist/story-generator/llm-providers/gemini-provider.js +12 -36
- package/dist/story-generator/llm-providers/index.js +3 -3
- package/dist/story-generator/llm-providers/openai-provider.d.ts.map +1 -1
- package/dist/story-generator/llm-providers/openai-provider.js +10 -50
- package/dist/story-generator/llm-providers/settings-manager.d.ts.map +1 -1
- package/dist/story-generator/llm-providers/settings-manager.js +4 -7
- package/dist/templates/StoryUI/StoryUIPanel.css +30 -1
- package/dist/templates/StoryUI/voice/VoiceCanvas.d.ts +1 -1
- package/dist/templates/StoryUI/voice/VoiceCanvas.d.ts.map +1 -1
- package/dist/templates/StoryUI/voice/VoiceCanvas.js +58 -16
- package/dist/templates/StoryUI/voice/VoiceCanvas.tsx +102 -71
- package/dist/templates/StoryUI/voice/types.d.ts +1 -1
- package/dist/templates/StoryUI/voice/types.d.ts.map +1 -1
- package/dist/templates/StoryUI/voice/types.ts +2 -1
- package/dist/templates/StoryUI/voice/voiceCommands.d.ts +4 -1
- package/dist/templates/StoryUI/voice/voiceCommands.d.ts.map +1 -1
- package/dist/templates/StoryUI/voice/voiceCommands.js +40 -7
- package/dist/templates/StoryUI/voice/voiceCommands.ts +42 -6
- package/package.json +5 -3
- package/templates/StoryUI/StoryUIPanel.css +30 -1
- package/templates/StoryUI/voice/VoiceCanvas.tsx +102 -71
- package/templates/StoryUI/voice/types.ts +2 -1
- package/templates/StoryUI/voice/voiceCommands.ts +42 -6
- package/dist/mcp-server/routes/canvasIntent.d.ts +0 -15
- package/dist/mcp-server/routes/canvasIntent.d.ts.map +0 -1
- package/dist/mcp-server/routes/canvasIntent.js +0 -553
- package/dist/mcp-server/routes/canvasPreview.d.ts +0 -14
- package/dist/mcp-server/routes/canvasPreview.d.ts.map +0 -1
- package/dist/mcp-server/routes/canvasPreview.js +0 -25
- package/dist/templates/StoryUI/voice/canvas/ComponentRenderer.d.ts +0 -14
- package/dist/templates/StoryUI/voice/canvas/ComponentRenderer.d.ts.map +0 -1
- package/dist/templates/StoryUI/voice/canvas/ComponentRenderer.js +0 -90
- package/dist/templates/StoryUI/voice/canvas/ComponentRenderer.tsx +0 -168
- package/dist/templates/StoryUI/voice/canvas/componentRegistry.d.ts +0 -21
- package/dist/templates/StoryUI/voice/canvas/componentRegistry.d.ts.map +0 -1
- package/dist/templates/StoryUI/voice/canvas/componentRegistry.js +0 -24
- package/dist/templates/StoryUI/voice/canvas/componentRegistry.ts +0 -30
- package/dist/templates/StoryUI/voice/canvas/operations.d.ts +0 -8
- package/dist/templates/StoryUI/voice/canvas/operations.d.ts.map +0 -1
- package/dist/templates/StoryUI/voice/canvas/operations.js +0 -189
- package/dist/templates/StoryUI/voice/canvas/operations.ts +0 -233
- package/dist/templates/StoryUI/voice/canvas/types.d.ts +0 -89
- package/dist/templates/StoryUI/voice/canvas/types.d.ts.map +0 -1
- package/dist/templates/StoryUI/voice/canvas/types.js +0 -2
- package/dist/templates/StoryUI/voice/canvas/types.ts +0 -106
- package/templates/StoryUI/voice/canvas/ComponentRenderer.tsx +0 -168
- package/templates/StoryUI/voice/canvas/componentRegistry.ts +0 -30
- package/templates/StoryUI/voice/canvas/operations.ts +0 -233
- package/templates/StoryUI/voice/canvas/types.ts +0 -106
package/dist/cli/setup.js
CHANGED
|
@@ -206,21 +206,21 @@ const LLM_PROVIDERS = {
|
|
|
206
206
|
claude: {
|
|
207
207
|
name: 'Claude (Anthropic)',
|
|
208
208
|
envKey: 'ANTHROPIC_API_KEY',
|
|
209
|
-
models: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-
|
|
209
|
+
models: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001'],
|
|
210
210
|
docsUrl: 'https://console.anthropic.com/',
|
|
211
211
|
description: 'Recommended - Best for complex reasoning and code quality'
|
|
212
212
|
},
|
|
213
213
|
openai: {
|
|
214
214
|
name: 'OpenAI (GPT)',
|
|
215
215
|
envKey: 'OPENAI_API_KEY',
|
|
216
|
-
models: ['gpt-4
|
|
216
|
+
models: ['gpt-5.4', 'gpt-5.4-mini', 'o4-mini'],
|
|
217
217
|
docsUrl: 'https://platform.openai.com/api-keys',
|
|
218
218
|
description: 'Versatile and fast'
|
|
219
219
|
},
|
|
220
220
|
gemini: {
|
|
221
221
|
name: 'Google Gemini',
|
|
222
222
|
envKey: 'GEMINI_API_KEY',
|
|
223
|
-
models: ['gemini-3.1-pro-preview', 'gemini-
|
|
223
|
+
models: ['gemini-3.1-pro-preview', 'gemini-3-flash-preview', 'gemini-2.5-flash'],
|
|
224
224
|
docsUrl: 'https://aistudio.google.com/app/apikey',
|
|
225
225
|
description: 'Cost-effective with good performance'
|
|
226
226
|
}
|
package/dist/mcp-server/index.js
CHANGED
|
@@ -23,10 +23,8 @@ import { getProviders, getModels, configureProviderRoute, validateApiKey, setDef
|
|
|
23
23
|
import { listFrameworks, detectCurrentFramework, getFrameworkDetails, validateStoryForFramework, postProcessStoryForFramework, } from './routes/frameworks.js';
|
|
24
24
|
import mcpRemoteRouter from './routes/mcpRemote.js';
|
|
25
25
|
// Voice Canvas endpoints
|
|
26
|
-
import { canvasIntentHandler, warmCanvasComponentCache } from './routes/canvasIntent.js';
|
|
27
26
|
import { canvasSaveHandler } from './routes/canvasSave.js';
|
|
28
27
|
import { canvasGenerateHandler, ensureVoiceCanvasStory } from './routes/canvasGenerate.js';
|
|
29
|
-
import { canvasPreviewHandler } from './routes/canvasPreview.js';
|
|
30
28
|
import { getAdapterRegistry } from '../story-generator/framework-adapters/index.js';
|
|
31
29
|
// Manifest — story ↔ chat source of truth
|
|
32
30
|
import { manifestGetHandler, manifestPatchHandler, manifestDeleteHandler, manifestReconcileHandler, manifestPollHandler, } from './routes/manifest.js';
|
|
@@ -110,9 +108,7 @@ app.post('/mcp/generate-story', generateStoryFromPrompt);
|
|
|
110
108
|
app.post('/mcp/generate-story-stream', generateStoryFromPromptStream);
|
|
111
109
|
// Voice Canvas endpoints
|
|
112
110
|
app.post('/mcp/canvas-generate', canvasGenerateHandler); // generate + write voice-canvas.stories.tsx
|
|
113
|
-
app.post('/mcp/canvas-preview', canvasPreviewHandler); // undo/redo: rewrite voice-canvas.stories.tsx
|
|
114
111
|
app.post('/mcp/canvas-save', canvasSaveHandler); // save canvas to named .stories.tsx
|
|
115
|
-
app.post('/mcp/canvas-intent', canvasIntentHandler); // legacy (kept for compatibility)
|
|
116
112
|
// Manifest — story ↔ chat source of truth
|
|
117
113
|
// NOTE: /reconcile must be registered BEFORE /:fileName to avoid route conflict
|
|
118
114
|
app.get('/story-ui/manifest/poll', manifestPollHandler);
|
|
@@ -883,8 +879,6 @@ if (storybookProxyEnabled) {
|
|
|
883
879
|
app.listen(PORT, () => {
|
|
884
880
|
console.error(`MCP server running on port ${PORT}`);
|
|
885
881
|
console.error(`Stories will be generated to: ${config.generatedStoriesPath}`);
|
|
886
|
-
// Pre-warm canvas component cache in background so first voice request is fast
|
|
887
|
-
warmCanvasComponentCache().catch(() => { });
|
|
888
882
|
// Ensure voice-canvas scratchpad story file exists before client polling starts.
|
|
889
883
|
// If it's missing, the first canvas generate creates it, triggering a false-positive
|
|
890
884
|
// "externally generated story" detection which reloads the page and kills Voice Canvas.
|
|
@@ -15,11 +15,33 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Request, Response } from 'express';
|
|
17
17
|
export declare const VOICE_CANVAS_STORY_ID = "generated-voice-canvas--default";
|
|
18
|
-
export declare function ensureReactLive(): void
|
|
18
|
+
export declare function ensureReactLive(): Promise<void>;
|
|
19
19
|
/**
|
|
20
20
|
* Write the static voice-canvas story template if it doesn't exist yet.
|
|
21
21
|
* Subsequent calls are no-ops — the file never changes after initial creation.
|
|
22
22
|
*/
|
|
23
23
|
export declare function ensureVoiceCanvasStory(storiesDir: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* If the LLM forgot to add a render() call (required by react-live noInline mode),
|
|
26
|
+
* detect the last defined PascalCase component and append render(<ComponentName />).
|
|
27
|
+
* This prevents the "No-Inline evaluations must call render" error when voice input
|
|
28
|
+
* is ambiguous or short and the LLM skips the final line.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ensureRenderCall(code: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Extract the canvas component code from the LLM response.
|
|
33
|
+
* Handles markdown code fences and stray text.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractCanvasCode(response: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Scan LLM-generated canvas code for dangerous patterns and neutralize them.
|
|
38
|
+
*
|
|
39
|
+
* Instead of rejecting the entire response, each dangerous token is replaced
|
|
40
|
+
* with a safe alternative (typically a comment + `undefined` or `void(`) so
|
|
41
|
+
* the rest of the generated JSX remains functional.
|
|
42
|
+
*
|
|
43
|
+
* Returns the sanitized code string. Logs a warning for every pattern found.
|
|
44
|
+
*/
|
|
45
|
+
export declare function sanitizeCanvasCode(code: string): string;
|
|
24
46
|
export declare function canvasGenerateHandler(req: Request, res: Response): Promise<Response<any, Record<string, any>>>;
|
|
25
47
|
//# sourceMappingURL=canvasGenerate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvasGenerate.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/canvasGenerate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"canvasGenerate.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/canvasGenerate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAkB5C,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAuIvE,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAmCrD;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAU/D;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc1D;AAgED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsBvD;AAID,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,+CA0GtE"}
|
|
@@ -15,12 +15,17 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'fs';
|
|
17
17
|
import path from 'path';
|
|
18
|
-
import {
|
|
18
|
+
import { exec } from 'child_process';
|
|
19
|
+
import { promisify } from 'util';
|
|
20
|
+
const execAsync = promisify(exec);
|
|
19
21
|
import { loadUserConfig } from '../../story-generator/configLoader.js';
|
|
20
22
|
import { EnhancedComponentDiscovery } from '../../story-generator/enhancedComponentDiscovery.js';
|
|
21
23
|
import { buildClaudePrompt } from '../../story-generator/promptGenerator.js';
|
|
22
24
|
import { chatCompletion } from '../../story-generator/llm-providers/story-llm-service.js';
|
|
23
25
|
import { logger } from '../../story-generator/logger.js';
|
|
26
|
+
// ── Component discovery cache ─────────────────────────────────
|
|
27
|
+
let _componentCache = null;
|
|
28
|
+
const COMPONENT_CACHE_TTL = 300000; // 5 minutes
|
|
24
29
|
// ── Constants ─────────────────────────────────────────────────
|
|
25
30
|
export const VOICE_CANVAS_STORY_ID = 'generated-voice-canvas--default';
|
|
26
31
|
const VOICE_CANVAS_STORY_FILE = 'voice-canvas.stories.tsx';
|
|
@@ -122,6 +127,8 @@ export const Default: StoryObj = {
|
|
|
122
127
|
|
|
123
128
|
useEffect(() => {
|
|
124
129
|
const handler = (e: MessageEvent) => {
|
|
130
|
+
// Only accept messages from same origin to prevent cross-origin code injection
|
|
131
|
+
if (e.origin !== window.location.origin) return;
|
|
125
132
|
if (e.data?.type === 'VOICE_CANVAS_UPDATE' && typeof e.data.code === 'string') {
|
|
126
133
|
setCode(e.data.code);
|
|
127
134
|
try { localStorage.setItem('${LS_KEY}', e.data.code); } catch {}
|
|
@@ -149,31 +156,43 @@ export const Default: StoryObj = {
|
|
|
149
156
|
* Detects pnpm / yarn / npm automatically.
|
|
150
157
|
*/
|
|
151
158
|
let reactLiveChecked = false;
|
|
152
|
-
|
|
159
|
+
let reactLiveInstalling = null;
|
|
160
|
+
export async function ensureReactLive() {
|
|
153
161
|
if (reactLiveChecked)
|
|
154
162
|
return;
|
|
155
|
-
|
|
163
|
+
// If another request is already installing, wait for it
|
|
164
|
+
if (reactLiveInstalling)
|
|
165
|
+
return reactLiveInstalling;
|
|
156
166
|
const cwd = process.cwd();
|
|
157
167
|
const reactLiveDir = path.join(cwd, 'node_modules', 'react-live');
|
|
158
|
-
if (fs.existsSync(reactLiveDir))
|
|
168
|
+
if (fs.existsSync(reactLiveDir)) {
|
|
169
|
+
reactLiveChecked = true;
|
|
159
170
|
return;
|
|
171
|
+
}
|
|
160
172
|
logger.log('[canvas-generate] react-live not found — installing...');
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
reactLiveInstalling = (async () => {
|
|
174
|
+
try {
|
|
175
|
+
let cmd = 'npm install react-live --save';
|
|
176
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) {
|
|
177
|
+
cmd = 'pnpm add react-live';
|
|
178
|
+
}
|
|
179
|
+
else if (fs.existsSync(path.join(cwd, 'yarn.lock'))) {
|
|
180
|
+
cmd = 'yarn add react-live';
|
|
181
|
+
}
|
|
182
|
+
await execAsync(cmd, { cwd });
|
|
183
|
+
reactLiveChecked = true;
|
|
184
|
+
logger.log('[canvas-generate] react-live installed successfully');
|
|
165
185
|
}
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
catch (err) {
|
|
187
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
188
|
+
logger.error('[canvas-generate] Could not auto-install react-live', { error: msg });
|
|
189
|
+
logger.log('[canvas-generate] Run manually: npm install react-live');
|
|
168
190
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
logger.error('[canvas-generate] Could not auto-install react-live', { error: msg });
|
|
175
|
-
logger.log('[canvas-generate] Run manually: npm install react-live');
|
|
176
|
-
}
|
|
191
|
+
finally {
|
|
192
|
+
reactLiveInstalling = null;
|
|
193
|
+
}
|
|
194
|
+
})();
|
|
195
|
+
return reactLiveInstalling;
|
|
177
196
|
}
|
|
178
197
|
// ── Write story to disk (once) ────────────────────────────────
|
|
179
198
|
/**
|
|
@@ -192,29 +211,144 @@ export function ensureVoiceCanvasStory(storiesDir) {
|
|
|
192
211
|
}
|
|
193
212
|
}
|
|
194
213
|
// ── Code extraction ───────────────────────────────────────────
|
|
214
|
+
/**
|
|
215
|
+
* If the LLM forgot to add a render() call (required by react-live noInline mode),
|
|
216
|
+
* detect the last defined PascalCase component and append render(<ComponentName />).
|
|
217
|
+
* This prevents the "No-Inline evaluations must call render" error when voice input
|
|
218
|
+
* is ambiguous or short and the LLM skips the final line.
|
|
219
|
+
*/
|
|
220
|
+
export function ensureRenderCall(code) {
|
|
221
|
+
if (/\brender\s*\(/.test(code))
|
|
222
|
+
return code;
|
|
223
|
+
// Find the last PascalCase component/const defined in the code
|
|
224
|
+
const matches = [...code.matchAll(/(?:const|function)\s+([A-Z][A-Za-z0-9]*)/g)];
|
|
225
|
+
const componentName = matches.at(-1)?.[1] ?? 'Canvas';
|
|
226
|
+
return `${code}\nrender(<${componentName} />);`;
|
|
227
|
+
}
|
|
195
228
|
/**
|
|
196
229
|
* Extract the canvas component code from the LLM response.
|
|
197
230
|
* Handles markdown code fences and stray text.
|
|
198
231
|
*/
|
|
199
|
-
function extractCanvasCode(response) {
|
|
232
|
+
export function extractCanvasCode(response) {
|
|
233
|
+
let code;
|
|
200
234
|
// Prefer explicit code fence
|
|
201
235
|
const fenceMatch = response.match(/```(?:jsx|tsx|js|ts)?\n([\s\S]+?)\n```/);
|
|
202
|
-
if (fenceMatch)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
236
|
+
if (fenceMatch) {
|
|
237
|
+
code = fenceMatch[1].trim();
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// Fall back: find the Canvas component block
|
|
241
|
+
const canvasMatch = response.match(/(const Canvas\s*=[\s\S]+?render\s*\(<Canvas\s*\/>?\);?\s*$)/m);
|
|
242
|
+
code = canvasMatch ? canvasMatch[1].trim() : response.trim();
|
|
243
|
+
}
|
|
244
|
+
return ensureRenderCall(code);
|
|
245
|
+
}
|
|
246
|
+
// ── Security sanitization ─────────────────────────────────────
|
|
247
|
+
/**
|
|
248
|
+
* Dangerous patterns that must be neutralized in LLM-generated canvas code.
|
|
249
|
+
*
|
|
250
|
+
* Each entry defines a regex (applied with the global flag) and a replacement
|
|
251
|
+
* string. The replacement comments out the dangerous call so the surrounding
|
|
252
|
+
* code still parses — this avoids rejecting an entire response because the LLM
|
|
253
|
+
* happened to mention one of these tokens inside a string literal or comment.
|
|
254
|
+
*
|
|
255
|
+
* Categories covered:
|
|
256
|
+
* - Arbitrary code execution (eval, Function constructor)
|
|
257
|
+
* - Cookie / domain access
|
|
258
|
+
* - Storage APIs (localStorage, sessionStorage)
|
|
259
|
+
* - Network requests (fetch, XMLHttpRequest, WebSocket)
|
|
260
|
+
* - Location manipulation
|
|
261
|
+
* - Script injection
|
|
262
|
+
* - Unsafe React patterns (dangerouslySetInnerHTML)
|
|
263
|
+
* - Prototype pollution (__proto__, constructor.prototype)
|
|
264
|
+
* - Dynamic / CommonJS imports
|
|
265
|
+
*/
|
|
266
|
+
const DANGEROUS_PATTERNS = [
|
|
267
|
+
// Arbitrary code execution
|
|
268
|
+
{ pattern: /\beval\s*\(/g, label: 'eval()', replacement: '/* [sanitized: eval] */void(' },
|
|
269
|
+
{ pattern: /\bnew\s+Function\s*\(/g, label: 'new Function()', replacement: '/* [sanitized: new Function] */void(' },
|
|
270
|
+
{ pattern: /\bFunction\s*\(/g, label: 'Function()', replacement: '/* [sanitized: Function] */void(' },
|
|
271
|
+
// Cookie / domain access
|
|
272
|
+
{ pattern: /\bdocument\.cookie\b/g, label: 'document.cookie', replacement: '/* [sanitized: document.cookie] */undefined' },
|
|
273
|
+
{ pattern: /\bdocument\.domain\b/g, label: 'document.domain', replacement: '/* [sanitized: document.domain] */undefined' },
|
|
274
|
+
// Storage APIs
|
|
275
|
+
{ pattern: /\blocalStorage\b/g, label: 'localStorage', replacement: '/* [sanitized: localStorage] */undefined' },
|
|
276
|
+
{ pattern: /\bsessionStorage\b/g, label: 'sessionStorage', replacement: '/* [sanitized: sessionStorage] */undefined' },
|
|
277
|
+
// Network requests
|
|
278
|
+
{ pattern: /\bfetch\s*\(/g, label: 'fetch()', replacement: '/* [sanitized: fetch] */void(' },
|
|
279
|
+
{ pattern: /\bnew\s+XMLHttpRequest\b/g, label: 'XMLHttpRequest', replacement: '/* [sanitized: XMLHttpRequest] */undefined' },
|
|
280
|
+
{ pattern: /\bXMLHttpRequest\b/g, label: 'XMLHttpRequest', replacement: '/* [sanitized: XMLHttpRequest] */undefined' },
|
|
281
|
+
{ pattern: /\bnew\s+WebSocket\s*\(/g, label: 'WebSocket', replacement: '/* [sanitized: WebSocket] */void(' },
|
|
282
|
+
{ pattern: /\bWebSocket\s*\(/g, label: 'WebSocket', replacement: '/* [sanitized: WebSocket] */void(' },
|
|
283
|
+
// Location manipulation
|
|
284
|
+
{ pattern: /\bwindow\.location\b/g, label: 'window.location', replacement: '/* [sanitized: window.location] */undefined' },
|
|
285
|
+
// Script injection
|
|
286
|
+
{ pattern: /<script\b/gi, label: '<script>', replacement: '/* [sanitized: script tag] */undefined' },
|
|
287
|
+
// Unsafe React patterns
|
|
288
|
+
{ pattern: /\bdangerouslySetInnerHTML\b/g, label: 'dangerouslySetInnerHTML', replacement: '/* [sanitized: dangerouslySetInnerHTML] */undefined' },
|
|
289
|
+
// Prototype pollution
|
|
290
|
+
{ pattern: /__proto__/g, label: '__proto__', replacement: '/* [sanitized: __proto__] */undefined' },
|
|
291
|
+
{ pattern: /\bconstructor\.prototype\b/g, label: 'constructor.prototype', replacement: '/* [sanitized: constructor.prototype] */undefined' },
|
|
292
|
+
// Dynamic imports
|
|
293
|
+
{ pattern: /\bimport\s*\(/g, label: 'dynamic import()', replacement: '/* [sanitized: dynamic import] */void(' },
|
|
294
|
+
// CommonJS require
|
|
295
|
+
{ pattern: /\brequire\s*\(/g, label: 'require()', replacement: '/* [sanitized: require] */void(' },
|
|
296
|
+
];
|
|
297
|
+
/**
|
|
298
|
+
* Scan LLM-generated canvas code for dangerous patterns and neutralize them.
|
|
299
|
+
*
|
|
300
|
+
* Instead of rejecting the entire response, each dangerous token is replaced
|
|
301
|
+
* with a safe alternative (typically a comment + `undefined` or `void(`) so
|
|
302
|
+
* the rest of the generated JSX remains functional.
|
|
303
|
+
*
|
|
304
|
+
* Returns the sanitized code string. Logs a warning for every pattern found.
|
|
305
|
+
*/
|
|
306
|
+
export function sanitizeCanvasCode(code) {
|
|
307
|
+
let sanitized = code;
|
|
308
|
+
const found = [];
|
|
309
|
+
for (const { pattern, label, replacement } of DANGEROUS_PATTERNS) {
|
|
310
|
+
// Reset lastIndex in case the regex was used before (global flag)
|
|
311
|
+
pattern.lastIndex = 0;
|
|
312
|
+
if (pattern.test(sanitized)) {
|
|
313
|
+
found.push(label);
|
|
314
|
+
// Reset again before replace — .test() advances lastIndex
|
|
315
|
+
pattern.lastIndex = 0;
|
|
316
|
+
sanitized = sanitized.replace(pattern, replacement);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (found.length > 0) {
|
|
320
|
+
logger.warn(`[canvas-generate] Sanitized ${found.length} dangerous pattern(s) from LLM output: ${found.join(', ')}`);
|
|
321
|
+
}
|
|
322
|
+
return sanitized;
|
|
210
323
|
}
|
|
211
324
|
// ── Handler ───────────────────────────────────────────────────
|
|
212
325
|
export async function canvasGenerateHandler(req, res) {
|
|
213
326
|
try {
|
|
214
|
-
|
|
327
|
+
let { prompt, canvasCode, provider, model, conversationHistory = [], } = req.body;
|
|
215
328
|
if (!prompt || typeof prompt !== 'string') {
|
|
216
329
|
return res.status(400).json({ error: 'prompt is required' });
|
|
217
330
|
}
|
|
331
|
+
// ── Request body size limits (truncate, don't reject) ──────
|
|
332
|
+
const MAX_PROMPT = 5000;
|
|
333
|
+
const MAX_CANVAS_CODE = 50000;
|
|
334
|
+
const MAX_HISTORY_ENTRIES = 50;
|
|
335
|
+
const MAX_HISTORY_CONTENT = 10000;
|
|
336
|
+
if (prompt.length > MAX_PROMPT) {
|
|
337
|
+
prompt = prompt.slice(0, MAX_PROMPT);
|
|
338
|
+
}
|
|
339
|
+
if (canvasCode && typeof canvasCode === 'string' && canvasCode.length > MAX_CANVAS_CODE) {
|
|
340
|
+
canvasCode = canvasCode.slice(0, MAX_CANVAS_CODE);
|
|
341
|
+
}
|
|
342
|
+
if (Array.isArray(conversationHistory)) {
|
|
343
|
+
if (conversationHistory.length > MAX_HISTORY_ENTRIES) {
|
|
344
|
+
conversationHistory = conversationHistory.slice(-MAX_HISTORY_ENTRIES);
|
|
345
|
+
}
|
|
346
|
+
for (const entry of conversationHistory) {
|
|
347
|
+
if (entry && typeof entry.content === 'string' && entry.content.length > MAX_HISTORY_CONTENT) {
|
|
348
|
+
entry.content = entry.content.slice(0, MAX_HISTORY_CONTENT);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
218
352
|
// Load config + discover components — same quality context as standard generation
|
|
219
353
|
const config = loadUserConfig();
|
|
220
354
|
// Voice Canvas requires React — it uses react-live to render JSX in the browser.
|
|
@@ -223,8 +357,16 @@ export async function canvasGenerateHandler(req, res) {
|
|
|
223
357
|
error: `Voice Canvas is only available for React-based Storybook projects. Current framework: ${config.componentFramework}`,
|
|
224
358
|
});
|
|
225
359
|
}
|
|
226
|
-
|
|
227
|
-
const
|
|
360
|
+
let components;
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
if (_componentCache && now - _componentCache.timestamp < COMPONENT_CACHE_TTL) {
|
|
363
|
+
components = _componentCache.components;
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const discovery = new EnhancedComponentDiscovery(config);
|
|
367
|
+
components = await discovery.discoverAll();
|
|
368
|
+
_componentCache = { components, timestamp: now };
|
|
369
|
+
}
|
|
228
370
|
// Build the system prompt using the standard prompt pipeline
|
|
229
371
|
const baseSystemPrompt = await buildClaudePrompt(prompt, config, components);
|
|
230
372
|
const systemPrompt = baseSystemPrompt + '\n' + CANVAS_MODE_SUFFIX;
|
|
@@ -248,10 +390,14 @@ export async function canvasGenerateHandler(req, res) {
|
|
|
248
390
|
maxTokens: 4096,
|
|
249
391
|
temperature: 0.3,
|
|
250
392
|
});
|
|
251
|
-
// Extract the canvas code from the LLM response
|
|
252
|
-
|
|
393
|
+
// Extract the canvas code from the LLM response and sanitize it.
|
|
394
|
+
// sanitizeCanvasCode neutralizes dangerous patterns (eval, fetch, script
|
|
395
|
+
// injection, prototype pollution, etc.) before the code reaches the
|
|
396
|
+
// client where react-live would execute it as arbitrary JS.
|
|
397
|
+
const rawCode = extractCanvasCode(response);
|
|
398
|
+
const result = sanitizeCanvasCode(rawCode);
|
|
253
399
|
// Ensure react-live is installed and story template exists (no-ops after first run)
|
|
254
|
-
ensureReactLive();
|
|
400
|
+
await ensureReactLive();
|
|
255
401
|
const storiesDir = config.generatedStoriesPath || './src/stories/generated/';
|
|
256
402
|
ensureVoiceCanvasStory(storiesDir);
|
|
257
403
|
logger.log(`[canvas-generate] Generated ${result.split('\n').length} lines for: "${prompt.slice(0, 60)}"`);
|
|
@@ -9,5 +9,12 @@
|
|
|
9
9
|
* Returns: { fileName, filePath, code }
|
|
10
10
|
*/
|
|
11
11
|
import { Request, Response } from 'express';
|
|
12
|
+
/**
|
|
13
|
+
* Convert a react-live canvas component (JSX string) to a proper .stories.tsx file.
|
|
14
|
+
* The input is in the format: `const Canvas = () => { ... }; render(<Canvas />);`
|
|
15
|
+
*/
|
|
16
|
+
export declare function jsxCodeToStory(jsxCode: string, title: string, importPath: string): string;
|
|
17
|
+
/** Derive a readable title from the last voice/text prompt. */
|
|
18
|
+
export declare function titleFromPrompt(prompt: string): string;
|
|
12
19
|
export declare function canvasSaveHandler(req: Request, res: Response): Promise<Response<any, Record<string, any>>>;
|
|
13
20
|
//# sourceMappingURL=canvasSave.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvasSave.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/canvasSave.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"canvasSave.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/canvasSave.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AA0I5C;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAwCzF;AAID,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAatD;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,+CAiHlE"}
|
|
@@ -125,7 +125,7 @@ ${jsx}
|
|
|
125
125
|
* Convert a react-live canvas component (JSX string) to a proper .stories.tsx file.
|
|
126
126
|
* The input is in the format: `const Canvas = () => { ... }; render(<Canvas />);`
|
|
127
127
|
*/
|
|
128
|
-
function jsxCodeToStory(jsxCode, title, importPath) {
|
|
128
|
+
export function jsxCodeToStory(jsxCode, title, importPath) {
|
|
129
129
|
// Remove the render(<Canvas />) call at the end
|
|
130
130
|
const cleanCode = jsxCode.replace(/\nrender\s*\(<Canvas\s*\/>\);?\s*$/, '').trim();
|
|
131
131
|
// Extract component names used in JSX (uppercase identifiers after '<')
|
|
@@ -166,7 +166,7 @@ function jsxCodeToStory(jsxCode, title, importPath) {
|
|
|
166
166
|
}
|
|
167
167
|
// ── Express handler ─────────────────────────────────────────
|
|
168
168
|
/** Derive a readable title from the last voice/text prompt. */
|
|
169
|
-
function titleFromPrompt(prompt) {
|
|
169
|
+
export function titleFromPrompt(prompt) {
|
|
170
170
|
// Strip filler words, take first ~6 meaningful words, title-case
|
|
171
171
|
const stop = new Set(['a', 'an', 'the', 'with', 'and', 'for', 'of', 'to', 'in', 'on', 'at', 'by']);
|
|
172
172
|
const words = prompt
|
|
@@ -183,15 +183,29 @@ function titleFromPrompt(prompt) {
|
|
|
183
183
|
}
|
|
184
184
|
export async function canvasSaveHandler(req, res) {
|
|
185
185
|
try {
|
|
186
|
-
const { tree, jsxCode, title: rawTitle, lastPrompt } = req.body;
|
|
186
|
+
const { tree, jsxCode, title: rawTitle, lastPrompt: rawLastPrompt } = req.body;
|
|
187
|
+
// ── Request body size limits ───────────────────────────────
|
|
188
|
+
const MAX_JSX_CODE = 100000;
|
|
189
|
+
const MAX_TITLE = 200;
|
|
190
|
+
const MAX_LAST_PROMPT = 5000;
|
|
191
|
+
if (jsxCode && typeof jsxCode === 'string' && jsxCode.length > MAX_JSX_CODE) {
|
|
192
|
+
return res.status(400).json({ error: `jsxCode exceeds maximum length of ${MAX_JSX_CODE} characters` });
|
|
193
|
+
}
|
|
194
|
+
// Truncate title and lastPrompt if needed (safe to trim these)
|
|
195
|
+
const safeTitle = (rawTitle && typeof rawTitle === 'string')
|
|
196
|
+
? rawTitle.slice(0, MAX_TITLE)
|
|
197
|
+
: rawTitle;
|
|
198
|
+
const lastPrompt = (rawLastPrompt && typeof rawLastPrompt === 'string' && rawLastPrompt.length > MAX_LAST_PROMPT)
|
|
199
|
+
? rawLastPrompt.slice(0, MAX_LAST_PROMPT)
|
|
200
|
+
: rawLastPrompt;
|
|
187
201
|
// Auto-generate title from last prompt if not provided
|
|
188
|
-
const title = (
|
|
189
|
-
?
|
|
202
|
+
const title = (safeTitle && typeof safeTitle === 'string' && safeTitle.trim())
|
|
203
|
+
? safeTitle.trim()
|
|
190
204
|
: (lastPrompt && typeof lastPrompt === 'string' && lastPrompt.trim())
|
|
191
205
|
? titleFromPrompt(lastPrompt.trim())
|
|
192
|
-
: '
|
|
206
|
+
: `Canvas ${new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}`;
|
|
193
207
|
const config = loadUserConfig();
|
|
194
|
-
const importPath = config.importPath || '
|
|
208
|
+
const importPath = config.importPath || '';
|
|
195
209
|
const storiesDir = config.generatedStoriesPath || './src/stories/generated/';
|
|
196
210
|
let code;
|
|
197
211
|
if (jsxCode && typeof jsxCode === 'string' && jsxCode.trim()) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/claude-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"claude-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/claude-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAmFrD,qBAAa,cAAe,SAAQ,eAAe;IACjD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAY;IACvC,QAAQ,CAAC,eAAe,cAAiB;gBAE7B,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAUtC,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAkE1E,UAAU,CACf,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,CAAC,EAAE,WAAW,GACpB,aAAa,CAAC,WAAW,CAAC;IAuGvB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAgD/D,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IAgCtB,OAAO,CAAC,eAAe;IAoBvB,OAAO,CAAC,aAAa;IAiBrB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAMrC;AAGD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAErF"}
|
|
@@ -33,32 +33,6 @@ const CLAUDE_MODELS = [
|
|
|
33
33
|
inputPricePer1kTokens: 0.003,
|
|
34
34
|
outputPricePer1kTokens: 0.015,
|
|
35
35
|
},
|
|
36
|
-
{
|
|
37
|
-
id: 'claude-opus-4-20250514',
|
|
38
|
-
name: 'Claude Opus 4',
|
|
39
|
-
provider: 'claude',
|
|
40
|
-
contextWindow: 200000,
|
|
41
|
-
maxOutputTokens: 32000,
|
|
42
|
-
supportsVision: true,
|
|
43
|
-
supportsDocuments: true,
|
|
44
|
-
supportsFunctionCalling: true,
|
|
45
|
-
supportsStreaming: true,
|
|
46
|
-
inputPricePer1kTokens: 0.015,
|
|
47
|
-
outputPricePer1kTokens: 0.075,
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: 'claude-sonnet-4-5-20250929',
|
|
51
|
-
name: 'Claude Sonnet 4.5',
|
|
52
|
-
provider: 'claude',
|
|
53
|
-
contextWindow: 200000,
|
|
54
|
-
maxOutputTokens: 16000,
|
|
55
|
-
supportsVision: true,
|
|
56
|
-
supportsDocuments: true,
|
|
57
|
-
supportsFunctionCalling: true,
|
|
58
|
-
supportsStreaming: true,
|
|
59
|
-
inputPricePer1kTokens: 0.003,
|
|
60
|
-
outputPricePer1kTokens: 0.015,
|
|
61
|
-
},
|
|
62
36
|
{
|
|
63
37
|
id: 'claude-haiku-4-5-20251001',
|
|
64
38
|
name: 'Claude Haiku 4.5',
|
|
@@ -70,19 +44,6 @@ const CLAUDE_MODELS = [
|
|
|
70
44
|
supportsFunctionCalling: true,
|
|
71
45
|
supportsStreaming: true,
|
|
72
46
|
inputPricePer1kTokens: 0.0008,
|
|
73
|
-
outputPricePer1kTokens: 0.004,
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
id: 'claude-sonnet-4-20250514',
|
|
77
|
-
name: 'Claude Sonnet 4',
|
|
78
|
-
provider: 'claude',
|
|
79
|
-
contextWindow: 200000,
|
|
80
|
-
maxOutputTokens: 16000,
|
|
81
|
-
supportsVision: true,
|
|
82
|
-
supportsDocuments: true,
|
|
83
|
-
supportsFunctionCalling: true,
|
|
84
|
-
supportsStreaming: true,
|
|
85
|
-
inputPricePer1kTokens: 0.003,
|
|
86
47
|
outputPricePer1kTokens: 0.015,
|
|
87
48
|
},
|
|
88
49
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/gemini-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"gemini-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/gemini-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAuFrD,qBAAa,cAAe,SAAQ,eAAe;IACjD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAY;IACvC,QAAQ,CAAC,eAAe,cAAiB;gBAE7B,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAU5C,OAAO,CAAC,SAAS;IAKX,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IA8D1E,UAAU,CACf,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,CAAC,EAAE,WAAW,GACpB,aAAa,CAAC,WAAW,CAAC;IAgHvB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA+C/D,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IAqCtB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,eAAe;IAiBvB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAKrC;AAGD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAErF"}
|
|
@@ -23,8 +23,8 @@ const GEMINI_MODELS = [
|
|
|
23
23
|
outputPricePer1kTokens: 0.012,
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
|
-
id: 'gemini-
|
|
27
|
-
name: 'Gemini
|
|
26
|
+
id: 'gemini-3-flash-preview',
|
|
27
|
+
name: 'Gemini 3 Flash Preview',
|
|
28
28
|
provider: 'gemini',
|
|
29
29
|
contextWindow: 1048576,
|
|
30
30
|
maxOutputTokens: 65536,
|
|
@@ -32,9 +32,8 @@ const GEMINI_MODELS = [
|
|
|
32
32
|
supportsDocuments: true,
|
|
33
33
|
supportsFunctionCalling: true,
|
|
34
34
|
supportsStreaming: true,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
outputPricePer1kTokens: 0.01,
|
|
35
|
+
inputPricePer1kTokens: 0.00015,
|
|
36
|
+
outputPricePer1kTokens: 0.0006,
|
|
38
37
|
},
|
|
39
38
|
{
|
|
40
39
|
id: 'gemini-2.5-flash',
|
|
@@ -50,35 +49,9 @@ const GEMINI_MODELS = [
|
|
|
50
49
|
inputPricePer1kTokens: 0.00015,
|
|
51
50
|
outputPricePer1kTokens: 0.0006,
|
|
52
51
|
},
|
|
53
|
-
{
|
|
54
|
-
id: 'gemini-2.5-flash-lite',
|
|
55
|
-
name: 'Gemini 2.5 Flash Lite',
|
|
56
|
-
provider: 'gemini',
|
|
57
|
-
contextWindow: 1048576,
|
|
58
|
-
maxOutputTokens: 65536,
|
|
59
|
-
supportsVision: true,
|
|
60
|
-
supportsDocuments: true,
|
|
61
|
-
supportsFunctionCalling: true,
|
|
62
|
-
supportsStreaming: true,
|
|
63
|
-
inputPricePer1kTokens: 0.00008,
|
|
64
|
-
outputPricePer1kTokens: 0.0003,
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id: 'gemini-3-flash-preview',
|
|
68
|
-
name: 'Gemini 3 Flash Preview',
|
|
69
|
-
provider: 'gemini',
|
|
70
|
-
contextWindow: 1048576,
|
|
71
|
-
maxOutputTokens: 65536,
|
|
72
|
-
supportsVision: true,
|
|
73
|
-
supportsDocuments: true,
|
|
74
|
-
supportsFunctionCalling: true,
|
|
75
|
-
supportsStreaming: true,
|
|
76
|
-
inputPricePer1kTokens: 0.00015,
|
|
77
|
-
outputPricePer1kTokens: 0.0006,
|
|
78
|
-
},
|
|
79
52
|
];
|
|
80
|
-
// Default model - Gemini
|
|
81
|
-
const DEFAULT_MODEL = 'gemini-
|
|
53
|
+
// Default model - Gemini 3.1 Pro Preview (flagship, March 2026)
|
|
54
|
+
const DEFAULT_MODEL = 'gemini-3.1-pro-preview';
|
|
82
55
|
// API configuration
|
|
83
56
|
const GEMINI_API_BASE = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
84
57
|
export class GeminiProvider extends BaseLLMProvider {
|
|
@@ -124,12 +97,13 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
124
97
|
parts: [{ text: systemPrompt }],
|
|
125
98
|
};
|
|
126
99
|
}
|
|
127
|
-
const url =
|
|
100
|
+
const url = this.getApiUrl(model);
|
|
128
101
|
try {
|
|
129
102
|
const response = await fetch(url, {
|
|
130
103
|
method: 'POST',
|
|
131
104
|
headers: {
|
|
132
105
|
'Content-Type': 'application/json',
|
|
106
|
+
'x-goog-api-key': apiKey,
|
|
133
107
|
},
|
|
134
108
|
body: JSON.stringify(requestBody),
|
|
135
109
|
signal: AbortSignal.timeout(this.config.timeout || 120000),
|
|
@@ -174,12 +148,13 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
174
148
|
parts: [{ text: systemPrompt }],
|
|
175
149
|
};
|
|
176
150
|
}
|
|
177
|
-
const url = `${this.getApiUrl(model, true)}?
|
|
151
|
+
const url = `${this.getApiUrl(model, true)}?alt=sse`;
|
|
178
152
|
try {
|
|
179
153
|
const response = await fetch(url, {
|
|
180
154
|
method: 'POST',
|
|
181
155
|
headers: {
|
|
182
156
|
'Content-Type': 'application/json',
|
|
157
|
+
'x-goog-api-key': apiKey,
|
|
183
158
|
},
|
|
184
159
|
body: JSON.stringify(requestBody),
|
|
185
160
|
signal: AbortSignal.timeout(this.config.timeout || 120000),
|
|
@@ -251,11 +226,12 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
251
226
|
async validateApiKey(apiKey) {
|
|
252
227
|
try {
|
|
253
228
|
// Make a minimal API call to validate the key
|
|
254
|
-
const url =
|
|
229
|
+
const url = this.getApiUrl('gemini-2.5-flash');
|
|
255
230
|
const response = await fetch(url, {
|
|
256
231
|
method: 'POST',
|
|
257
232
|
headers: {
|
|
258
233
|
'Content-Type': 'application/json',
|
|
234
|
+
'x-goog-api-key': apiKey,
|
|
259
235
|
},
|
|
260
236
|
body: JSON.stringify({
|
|
261
237
|
contents: [{ parts: [{ text: 'Hi' }] }],
|