@tpitre/story-ui 3.10.7 → 4.1.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/index.js +0 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +111 -5
- package/dist/mcp-server/index.js +59 -4
- package/dist/mcp-server/routes/generateStory.d.ts.map +1 -1
- package/dist/mcp-server/routes/generateStory.js +106 -28
- package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -1
- package/dist/mcp-server/routes/generateStoryStream.js +152 -33
- package/dist/mcp-server/routes/storyHelpers.d.ts +9 -3
- package/dist/mcp-server/routes/storyHelpers.d.ts.map +1 -1
- package/dist/mcp-server/routes/storyHelpers.js +71 -30
- package/dist/story-generator/configLoader.d.ts.map +1 -1
- package/dist/story-generator/configLoader.js +54 -31
- package/dist/story-generator/framework-adapters/angular-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/angular-adapter.js +11 -7
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts +1 -1
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/svelte-adapter.js +351 -268
- package/dist/story-generator/llm-providers/story-llm-service.d.ts +4 -0
- package/dist/story-generator/llm-providers/story-llm-service.d.ts.map +1 -1
- package/dist/story-generator/llm-providers/story-llm-service.js +38 -2
- package/dist/story-generator/promptGenerator.js +1 -1
- package/dist/story-generator/selfHealingLoop.d.ts +1 -0
- package/dist/story-generator/selfHealingLoop.d.ts.map +1 -1
- package/dist/story-generator/selfHealingLoop.js +96 -8
- package/dist/story-generator/storyValidator.d.ts.map +1 -1
- package/dist/story-generator/storyValidator.js +16 -6
- package/dist/story-generator/validateStory.d.ts +2 -1
- package/dist/story-generator/validateStory.d.ts.map +1 -1
- package/dist/story-generator/validateStory.js +263 -17
- package/dist/templates/StoryUI/StoryUIPanel.css +1440 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +152 -26
- package/package.json +2 -3
- package/templates/StoryUI/StoryUIPanel.css +29 -0
- package/templates/StoryUI/StoryUIPanel.tsx +170 -27
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/cli/setup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../cli/setup.ts"],"names":[],"mappings":"AAmDA;;GAEG;AACH,wBAAgB,iCAAiC,SA8ChD;AAiWD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../cli/setup.ts"],"names":[],"mappings":"AAmDA;;GAEG;AACH,wBAAgB,iCAAiC,SA8ChD;AAiWD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,iBA4zB5D"}
|
package/dist/cli/setup.js
CHANGED
|
@@ -422,7 +422,9 @@ export async function setupCommand(options = {}) {
|
|
|
422
422
|
// Check for any Storybook framework
|
|
423
423
|
const storybookPackages = [
|
|
424
424
|
'@storybook/react', '@storybook/react-vite', '@storybook/react-webpack5', '@storybook/nextjs',
|
|
425
|
-
'@storybook/angular', '@storybook/vue3', '@storybook/
|
|
425
|
+
'@storybook/angular', '@storybook/vue3', '@storybook/vue3-vite',
|
|
426
|
+
'@storybook/svelte', '@storybook/svelte-vite',
|
|
427
|
+
'@storybook/web-components', '@storybook/web-components-vite'
|
|
426
428
|
];
|
|
427
429
|
const hasStorybook = storybookPackages.some(pkg => devDeps[pkg] || deps[pkg]) ||
|
|
428
430
|
fs.existsSync(path.join(process.cwd(), '.storybook'));
|
|
@@ -455,19 +457,34 @@ export async function setupCommand(options = {}) {
|
|
|
455
457
|
componentFramework = 'angular';
|
|
456
458
|
console.log(chalk.green('✅ Detected Angular Storybook'));
|
|
457
459
|
}
|
|
458
|
-
// Check for Vue Storybook
|
|
460
|
+
// Check for Vue Storybook (vite variant first)
|
|
461
|
+
else if (devDeps['@storybook/vue3-vite'] || deps['@storybook/vue3-vite']) {
|
|
462
|
+
storybookFramework = '@storybook/vue3-vite';
|
|
463
|
+
componentFramework = 'vue';
|
|
464
|
+
console.log(chalk.green('✅ Detected Vite-based Vue 3 Storybook'));
|
|
465
|
+
}
|
|
459
466
|
else if (devDeps['@storybook/vue3'] || deps['@storybook/vue3']) {
|
|
460
467
|
storybookFramework = '@storybook/vue3';
|
|
461
468
|
componentFramework = 'vue';
|
|
462
469
|
console.log(chalk.green('✅ Detected Vue 3 Storybook'));
|
|
463
470
|
}
|
|
464
|
-
// Check for Svelte Storybook
|
|
471
|
+
// Check for Svelte Storybook (vite variant first)
|
|
472
|
+
else if (devDeps['@storybook/svelte-vite'] || deps['@storybook/svelte-vite']) {
|
|
473
|
+
storybookFramework = '@storybook/svelte-vite';
|
|
474
|
+
componentFramework = 'svelte';
|
|
475
|
+
console.log(chalk.green('✅ Detected Vite-based Svelte Storybook'));
|
|
476
|
+
}
|
|
465
477
|
else if (devDeps['@storybook/svelte'] || deps['@storybook/svelte']) {
|
|
466
478
|
storybookFramework = '@storybook/svelte';
|
|
467
479
|
componentFramework = 'svelte';
|
|
468
480
|
console.log(chalk.green('✅ Detected Svelte Storybook'));
|
|
469
481
|
}
|
|
470
|
-
// Check for Web Components Storybook
|
|
482
|
+
// Check for Web Components Storybook (vite variant first)
|
|
483
|
+
else if (devDeps['@storybook/web-components-vite'] || deps['@storybook/web-components-vite']) {
|
|
484
|
+
storybookFramework = '@storybook/web-components-vite';
|
|
485
|
+
componentFramework = 'web-components';
|
|
486
|
+
console.log(chalk.green('✅ Detected Vite-based Web Components Storybook'));
|
|
487
|
+
}
|
|
471
488
|
else if (devDeps['@storybook/web-components'] || deps['@storybook/web-components']) {
|
|
472
489
|
storybookFramework = '@storybook/web-components';
|
|
473
490
|
componentFramework = 'web-components';
|
|
@@ -877,7 +894,7 @@ Material UI (MUI) is a React component library implementing Material Design.
|
|
|
877
894
|
}
|
|
878
895
|
// Copy component files
|
|
879
896
|
const templatesDir = path.resolve(__dirname, '../../templates/StoryUI');
|
|
880
|
-
const componentFiles = ['StoryUIPanel.tsx', 'StoryUIPanel.mdx'];
|
|
897
|
+
const componentFiles = ['StoryUIPanel.tsx', 'StoryUIPanel.mdx', 'StoryUIPanel.css'];
|
|
881
898
|
console.log(chalk.blue('\n📦 Installing Story UI component...'));
|
|
882
899
|
for (const file of componentFiles) {
|
|
883
900
|
const sourcePath = path.join(templatesDir, file);
|
|
@@ -895,6 +912,95 @@ Material UI (MUI) is a React component library implementing Material Design.
|
|
|
895
912
|
console.warn(chalk.yellow(`⚠️ Template file not found: ${file}`));
|
|
896
913
|
}
|
|
897
914
|
}
|
|
915
|
+
// Configure Storybook bundler for StoryUIPanel requirements
|
|
916
|
+
console.log(chalk.blue('\n🔧 Configuring Storybook for Story UI...'));
|
|
917
|
+
const mainConfigPath = path.join(process.cwd(), '.storybook', 'main.ts');
|
|
918
|
+
const mainConfigPathJs = path.join(process.cwd(), '.storybook', 'main.js');
|
|
919
|
+
const actualMainPath = fs.existsSync(mainConfigPath) ? mainConfigPath :
|
|
920
|
+
fs.existsSync(mainConfigPathJs) ? mainConfigPathJs : null;
|
|
921
|
+
if (actualMainPath) {
|
|
922
|
+
let mainContent = fs.readFileSync(actualMainPath, 'utf-8');
|
|
923
|
+
let configUpdated = false;
|
|
924
|
+
// Check if StoryUI config already exists
|
|
925
|
+
if (mainContent.includes('@tpitre/story-ui') || mainContent.includes('StoryUIPanel')) {
|
|
926
|
+
console.log(chalk.blue('ℹ️ Storybook already configured for Story UI'));
|
|
927
|
+
}
|
|
928
|
+
else if (componentFramework === 'angular') {
|
|
929
|
+
// Angular uses webpack - needs CSS loaders
|
|
930
|
+
if (!mainContent.includes('webpackFinal')) {
|
|
931
|
+
const webpackConfig = `webpackFinal: async (config) => {
|
|
932
|
+
// Story UI: Add CSS loader for StoryUIPanel CSS imports
|
|
933
|
+
config.module?.rules?.push({
|
|
934
|
+
test: /\\.css$/,
|
|
935
|
+
use: ['style-loader', 'css-loader'],
|
|
936
|
+
});
|
|
937
|
+
return config;
|
|
938
|
+
},`;
|
|
939
|
+
// Insert webpackFinal inside the config object, before the closing };
|
|
940
|
+
if (mainContent.match(/};\s*\n+\s*export\s+default/)) {
|
|
941
|
+
mainContent = mainContent.replace(/(\n)(};\s*\n+\s*export\s+default)/, `\n ${webpackConfig}\n$2`);
|
|
942
|
+
configUpdated = true;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
// Install required loaders for Angular
|
|
946
|
+
console.log(chalk.blue('📦 Installing CSS loaders for Angular...'));
|
|
947
|
+
try {
|
|
948
|
+
execSync('npm install --save-dev style-loader css-loader', { stdio: 'inherit' });
|
|
949
|
+
console.log(chalk.green('✅ Installed style-loader and css-loader'));
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
console.warn(chalk.yellow('⚠️ Could not install CSS loaders. You may need to run: npm install --save-dev style-loader css-loader'));
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
// Vite-based frameworks (React, Vue, Svelte, Web Components)
|
|
957
|
+
if (!mainContent.includes('viteFinal')) {
|
|
958
|
+
const viteConfig = `viteFinal: async (config) => {
|
|
959
|
+
// Story UI: Exclude from dependency optimization to handle CSS imports correctly
|
|
960
|
+
config.optimizeDeps = {
|
|
961
|
+
...config.optimizeDeps,
|
|
962
|
+
exclude: [
|
|
963
|
+
...(config.optimizeDeps?.exclude || []),
|
|
964
|
+
'@tpitre/story-ui'
|
|
965
|
+
]
|
|
966
|
+
};
|
|
967
|
+
return config;
|
|
968
|
+
},`;
|
|
969
|
+
// Insert viteFinal inside the config object, before the closing };
|
|
970
|
+
// Find the last property line and add viteFinal after it
|
|
971
|
+
// Pattern: match the closing }; that ends the config object (before export default)
|
|
972
|
+
if (mainContent.match(/};\s*\n+\s*export\s+default/)) {
|
|
973
|
+
mainContent = mainContent.replace(/(\n)(};\s*\n+\s*export\s+default)/, `\n ${viteConfig}\n$2`);
|
|
974
|
+
configUpdated = true;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
// For Web Components: Update tsconfig.json for TSX support
|
|
979
|
+
if (componentFramework === 'web-components') {
|
|
980
|
+
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
|
|
981
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
982
|
+
try {
|
|
983
|
+
let tsconfigContent = fs.readFileSync(tsconfigPath, 'utf-8');
|
|
984
|
+
if (!tsconfigContent.includes('"jsx"')) {
|
|
985
|
+
// Add jsx config for TSX compilation
|
|
986
|
+
tsconfigContent = tsconfigContent.replace(/"compilerOptions"\s*:\s*\{/, '"compilerOptions": {\n "jsx": "react-jsx",');
|
|
987
|
+
fs.writeFileSync(tsconfigPath, tsconfigContent);
|
|
988
|
+
console.log(chalk.green('✅ Added JSX support to tsconfig.json'));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
catch (error) {
|
|
992
|
+
console.warn(chalk.yellow('⚠️ Could not update tsconfig.json. You may need to add "jsx": "react-jsx" manually.'));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (configUpdated) {
|
|
997
|
+
fs.writeFileSync(actualMainPath, mainContent);
|
|
998
|
+
console.log(chalk.green('✅ Updated Storybook configuration for Story UI'));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
console.warn(chalk.yellow('⚠️ Could not find .storybook/main.ts or main.js'));
|
|
1003
|
+
}
|
|
898
1004
|
// Create considerations file
|
|
899
1005
|
const considerationsTemplatePath = path.resolve(__dirname, '../../templates/story-ui-considerations.md');
|
|
900
1006
|
const considerationsPath = path.join(process.cwd(), 'story-ui-considerations.md');
|
package/dist/mcp-server/index.js
CHANGED
|
@@ -163,7 +163,9 @@ app.get('/story-ui/stories', async (req, res) => {
|
|
|
163
163
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
164
164
|
// Extract title from story file
|
|
165
165
|
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
166
|
-
|
|
166
|
+
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : file.replace('.stories.tsx', '');
|
|
167
|
+
// Remove hash suffix like " (a1b2c3d4)" from display title - hash is for Storybook uniqueness only
|
|
168
|
+
title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
|
|
167
169
|
return {
|
|
168
170
|
id: file.replace('.stories.tsx', ''),
|
|
169
171
|
fileName: file,
|
|
@@ -210,17 +212,37 @@ app.post('/story-ui/stories', async (req, res) => {
|
|
|
210
212
|
}
|
|
211
213
|
});
|
|
212
214
|
// Delete story by ID (RESTful endpoint)
|
|
215
|
+
// Supports both fileName format (Button-a1b2c3d4.stories.tsx) and legacy storyId format (story-a1b2c3d4)
|
|
213
216
|
app.delete('/story-ui/stories/:id', async (req, res) => {
|
|
214
217
|
try {
|
|
215
218
|
const { id } = req.params;
|
|
216
219
|
const storiesPath = config.generatedStoriesPath;
|
|
217
|
-
|
|
220
|
+
// Try exact match first (fileName format)
|
|
221
|
+
// Handle both .tsx and .svelte extensions
|
|
222
|
+
let fileName = id;
|
|
223
|
+
if (!id.endsWith('.stories.tsx') && !id.endsWith('.stories.ts') && !id.endsWith('.stories.svelte')) {
|
|
224
|
+
fileName = `${id}.stories.tsx`;
|
|
225
|
+
}
|
|
218
226
|
const filePath = path.join(storiesPath, fileName);
|
|
219
227
|
if (fs.existsSync(filePath)) {
|
|
220
228
|
fs.unlinkSync(filePath);
|
|
221
229
|
console.log(`✅ Deleted story: ${filePath}`);
|
|
222
230
|
return res.json({ success: true, message: 'Story deleted successfully' });
|
|
223
231
|
}
|
|
232
|
+
// Fallback: Search for file by hash (for legacy storyId format like "story-a1b2c3d4")
|
|
233
|
+
// This handles backward compatibility with existing chats
|
|
234
|
+
const hashMatch = id.match(/-([a-f0-9]{8})(?:\.stories\.[a-z]+)?$/);
|
|
235
|
+
if (hashMatch) {
|
|
236
|
+
const hash = hashMatch[1];
|
|
237
|
+
const files = fs.readdirSync(storiesPath);
|
|
238
|
+
const matchingFile = files.find(f => f.includes(`-${hash}.stories.`));
|
|
239
|
+
if (matchingFile) {
|
|
240
|
+
const matchedFilePath = path.join(storiesPath, matchingFile);
|
|
241
|
+
fs.unlinkSync(matchedFilePath);
|
|
242
|
+
console.log(`✅ Deleted story by hash match: ${matchedFilePath}`);
|
|
243
|
+
return res.json({ success: true, message: 'Story deleted successfully' });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
224
246
|
return res.status(404).json({ success: false, error: 'Story not found' });
|
|
225
247
|
}
|
|
226
248
|
catch (error) {
|
|
@@ -312,16 +334,49 @@ app.post('/story-ui/stories/delete-bulk', async (req, res) => {
|
|
|
312
334
|
return res.status(500).json({ error: 'Failed to bulk delete stories' });
|
|
313
335
|
}
|
|
314
336
|
});
|
|
315
|
-
// Clear all generated stories
|
|
337
|
+
// Clear all generated stories OR delete single story by fileName query param
|
|
316
338
|
app.delete('/story-ui/stories', async (req, res) => {
|
|
317
339
|
try {
|
|
318
340
|
const storiesPath = config.generatedStoriesPath;
|
|
341
|
+
const { fileName } = req.query;
|
|
342
|
+
// If fileName query param provided, delete that specific file
|
|
343
|
+
if (fileName && typeof fileName === 'string') {
|
|
344
|
+
console.log(`🗑️ Deleting story by fileName: ${fileName}`);
|
|
345
|
+
if (!fs.existsSync(storiesPath)) {
|
|
346
|
+
return res.status(404).json({ success: false, error: 'Stories directory not found' });
|
|
347
|
+
}
|
|
348
|
+
// Try exact match first
|
|
349
|
+
let filePath = path.join(storiesPath, fileName);
|
|
350
|
+
if (fs.existsSync(filePath)) {
|
|
351
|
+
fs.unlinkSync(filePath);
|
|
352
|
+
console.log(`✅ Deleted story: ${filePath}`);
|
|
353
|
+
return res.json({ success: true, message: 'Story deleted successfully' });
|
|
354
|
+
}
|
|
355
|
+
// Try matching by hash pattern (e.g., "button-a1b2c3d4" without extension)
|
|
356
|
+
const hashMatch = fileName.match(/-([a-f0-9]{8})(?:\.stories\.[a-z]+)?$/);
|
|
357
|
+
if (hashMatch) {
|
|
358
|
+
const hash = hashMatch[1];
|
|
359
|
+
const files = fs.readdirSync(storiesPath);
|
|
360
|
+
const matchingFile = files.find(f => f.includes(`-${hash}.stories.`));
|
|
361
|
+
if (matchingFile) {
|
|
362
|
+
filePath = path.join(storiesPath, matchingFile);
|
|
363
|
+
fs.unlinkSync(filePath);
|
|
364
|
+
console.log(`✅ Deleted story by hash match: ${filePath}`);
|
|
365
|
+
return res.json({ success: true, message: 'Story deleted successfully' });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return res.status(404).json({ success: false, error: 'Story not found' });
|
|
369
|
+
}
|
|
370
|
+
// No fileName - clear ALL stories
|
|
319
371
|
console.log(`🗑️ Clearing all stories from: ${storiesPath}`);
|
|
320
372
|
if (!fs.existsSync(storiesPath)) {
|
|
321
373
|
return res.json({ success: true, deleted: 0, message: 'No stories directory found' });
|
|
322
374
|
}
|
|
323
375
|
const files = fs.readdirSync(storiesPath);
|
|
324
|
-
|
|
376
|
+
// Support all story file extensions: .tsx, .ts, .svelte
|
|
377
|
+
const storyFiles = files.filter(file => file.endsWith('.stories.tsx') ||
|
|
378
|
+
file.endsWith('.stories.ts') ||
|
|
379
|
+
file.endsWith('.stories.svelte'));
|
|
325
380
|
let deleted = 0;
|
|
326
381
|
for (const file of storyFiles) {
|
|
327
382
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateStory.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"generateStory.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAmd5C,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,2DA+kBxE"}
|
|
@@ -145,18 +145,58 @@ function slugify(str) {
|
|
|
145
145
|
.replace(/[^a-z0-9]+/g, '-')
|
|
146
146
|
.replace(/^-+|-+$/g, '');
|
|
147
147
|
}
|
|
148
|
-
function extractCodeBlock(text) {
|
|
149
|
-
//
|
|
150
|
-
const codeBlock = text.match(/```(?:tsx|jsx|typescript|ts|js|javascript)
|
|
151
|
-
|
|
148
|
+
function extractCodeBlock(text, framework) {
|
|
149
|
+
// Universal: Standard code blocks with language identifiers
|
|
150
|
+
const codeBlock = text.match(/```(?:tsx|jsx|typescript|ts|js|javascript|svelte|html|vue)?\s*([\s\S]*?)\s*```/i);
|
|
151
|
+
if (codeBlock) {
|
|
152
|
+
return codeBlock[1].trim();
|
|
153
|
+
}
|
|
154
|
+
// Framework-specific fallbacks
|
|
155
|
+
if (framework === 'svelte') {
|
|
156
|
+
// Svelte: look for <script module> (addon-svelte-csf v5+ format)
|
|
157
|
+
const scriptModuleIndex = text.indexOf('<script module>');
|
|
158
|
+
if (scriptModuleIndex !== -1) {
|
|
159
|
+
return text.slice(scriptModuleIndex).trim();
|
|
160
|
+
}
|
|
161
|
+
// Svelte: look for <script context="module"> (legacy format)
|
|
162
|
+
const scriptContextIndex = text.indexOf('<script context="module">');
|
|
163
|
+
if (scriptContextIndex !== -1) {
|
|
164
|
+
return text.slice(scriptContextIndex).trim();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (framework === 'vue') {
|
|
168
|
+
// Vue: look for <script setup> or <template>
|
|
169
|
+
const scriptSetupIndex = text.indexOf('<script setup');
|
|
170
|
+
if (scriptSetupIndex !== -1) {
|
|
171
|
+
return text.slice(scriptSetupIndex).trim();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Universal fallback: look for import statement
|
|
175
|
+
const importIndex = text.indexOf('import');
|
|
176
|
+
if (importIndex !== -1) {
|
|
177
|
+
return text.slice(importIndex).trim();
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
152
180
|
}
|
|
153
|
-
async function callLLM(messages, images) {
|
|
181
|
+
async function callLLM(messages, images, options) {
|
|
154
182
|
// Check if any provider is configured
|
|
155
183
|
if (!isProviderConfigured()) {
|
|
156
184
|
throw new Error('No LLM provider configured. Please set CLAUDE_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY.');
|
|
157
185
|
}
|
|
158
186
|
const providerInfo = getProviderInfo();
|
|
159
|
-
|
|
187
|
+
// Log which provider will be used
|
|
188
|
+
if (options?.provider) {
|
|
189
|
+
logger.log(`🎯 Explicit provider requested: ${options.provider} (model: ${options.model || 'default'})`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
logger.debug(`Using default ${providerInfo.currentProvider} (${providerInfo.currentModel}) for story generation`);
|
|
193
|
+
}
|
|
194
|
+
// Build options to pass to chat completion
|
|
195
|
+
const llmOptions = {
|
|
196
|
+
maxTokens: 8192,
|
|
197
|
+
provider: options?.provider,
|
|
198
|
+
model: options?.model,
|
|
199
|
+
};
|
|
160
200
|
// If images are provided, use vision-capable chat
|
|
161
201
|
if (images && images.length > 0) {
|
|
162
202
|
if (!providerInfo.supportsVision) {
|
|
@@ -173,9 +213,9 @@ async function callLLM(messages, images) {
|
|
|
173
213
|
}
|
|
174
214
|
return msg;
|
|
175
215
|
});
|
|
176
|
-
return await chatCompletionWithImages(messagesWithImages,
|
|
216
|
+
return await chatCompletionWithImages(messagesWithImages, llmOptions);
|
|
177
217
|
}
|
|
178
|
-
return await chatCompletion(messages,
|
|
218
|
+
return await chatCompletion(messages, llmOptions);
|
|
179
219
|
}
|
|
180
220
|
function cleanPromptForTitle(prompt) {
|
|
181
221
|
if (!prompt || typeof prompt !== 'string') {
|
|
@@ -342,7 +382,9 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
342
382
|
autoDetectFramework, // Auto-detect from project (default: false)
|
|
343
383
|
images, // Array of images for vision-based generation
|
|
344
384
|
visionMode, // Vision mode: 'screenshot_to_story', 'design_to_story', 'component_analysis', 'layout_analysis'
|
|
345
|
-
designSystem // Design system being used (chakra-ui, mantine, etc.)
|
|
385
|
+
designSystem, // Design system being used (chakra-ui, mantine, etc.)
|
|
386
|
+
provider, // LLM provider selected in UI (claude, openai, gemini)
|
|
387
|
+
model // Model selected in UI
|
|
346
388
|
} = req.body;
|
|
347
389
|
if (!prompt)
|
|
348
390
|
return res.status(400).json({ error: 'Missing prompt' });
|
|
@@ -485,7 +527,7 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
485
527
|
while (attempts < maxRetries) {
|
|
486
528
|
attempts++;
|
|
487
529
|
logger.log(`--- Story Generation Attempt ${attempts} ---`);
|
|
488
|
-
const claudeResponse = await callLLM(messages, processedImages.length > 0 ? processedImages : undefined);
|
|
530
|
+
const claudeResponse = await callLLM(messages, processedImages.length > 0 ? processedImages : undefined, { provider, model });
|
|
489
531
|
const extractedCode = extractCodeBlock(claudeResponse);
|
|
490
532
|
if (!extractedCode) {
|
|
491
533
|
aiText = claudeResponse; // Use raw response if no code block
|
|
@@ -507,7 +549,9 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
507
549
|
// 1. Pattern validation (storyValidator)
|
|
508
550
|
validationErrors = validateStory(aiText);
|
|
509
551
|
// 2. TypeScript AST validation (validateStory.ts)
|
|
510
|
-
|
|
552
|
+
// Use correct filename extension for framework-specific validation (e.g., .stories.svelte for Svelte)
|
|
553
|
+
const validationFileName = `story${frameworkAdapter?.defaultExtension || '.stories.tsx'}`;
|
|
554
|
+
const astValidation = validateStoryCode(aiText, validationFileName, config);
|
|
511
555
|
// 3. Import validation (check against discovered components)
|
|
512
556
|
const importValidation = await preValidateImports(aiText, config, discovery);
|
|
513
557
|
// Aggregate all errors
|
|
@@ -550,6 +594,7 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
550
594
|
// Determine the best code to use
|
|
551
595
|
let fileContents;
|
|
552
596
|
let hasValidationWarnings = false;
|
|
597
|
+
let isFallbackStory = false; // Track if we created a fallback error story
|
|
553
598
|
// Select the best attempt (fewest errors)
|
|
554
599
|
const bestAttempt = selectBestAttempt(allAttempts);
|
|
555
600
|
const finalErrors = bestAttempt ? bestAttempt.errors : createEmptyErrors();
|
|
@@ -560,15 +605,23 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
560
605
|
logger.log(` Remaining: ${formatErrorsForLog(finalErrors)}`);
|
|
561
606
|
// If there are still errors but we have an attempt, use the best one
|
|
562
607
|
// Only create fallback if we have no valid code at all
|
|
563
|
-
|
|
608
|
+
// Check for framework-specific code patterns (Svelte uses defineMeta, not export)
|
|
609
|
+
const hasUsableCode = bestAttempt.code && (bestAttempt.code.includes('export') ||
|
|
610
|
+
bestAttempt.code.includes('defineMeta') ||
|
|
611
|
+
bestAttempt.code.includes('<script module>') ||
|
|
612
|
+
bestAttempt.code.includes('<script context="module">'));
|
|
613
|
+
if (hasUsableCode) {
|
|
564
614
|
fileContents = bestAttempt.code;
|
|
565
615
|
hasValidationWarnings = true;
|
|
566
616
|
}
|
|
567
617
|
else {
|
|
568
618
|
// Create fallback story only if we have no usable code
|
|
619
|
+
// Pass both the raw prompt (for error display) and the aiTitle (for proper story title casing)
|
|
569
620
|
logger.log('Creating fallback story - no usable code generated');
|
|
570
|
-
|
|
621
|
+
// aiTitle may not be set yet at this point, so always use cleanPromptForTitle for fallbacks
|
|
622
|
+
fileContents = createFrameworkAwareFallbackStory(prompt, cleanPromptForTitle(prompt), config, detectedFramework);
|
|
571
623
|
hasValidationWarnings = true;
|
|
624
|
+
isFallbackStory = true; // Mark that we created a fallback error story
|
|
572
625
|
}
|
|
573
626
|
}
|
|
574
627
|
else if (bestAttempt) {
|
|
@@ -655,34 +708,55 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
655
708
|
storyId = `story-${hash}`;
|
|
656
709
|
logger.log('🆕 Creating new story:', { storyId, fileName: finalFileName });
|
|
657
710
|
}
|
|
658
|
-
// Create title for the story
|
|
711
|
+
// Create title for the story (clean, without hash - hash goes in id for uniqueness)
|
|
659
712
|
const prettyPrompt = escapeTitleForTS(aiTitle);
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
713
|
+
// Title is now clean without hash - uniqueness is ensured via Storybook's id parameter
|
|
714
|
+
const cleanTitle = prettyPrompt;
|
|
715
|
+
// Generate Storybook-compatible ID with hash for uniqueness (prevents duplicate story errors)
|
|
716
|
+
const storyIdSlug = `${prettyPrompt.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}-${hash}`;
|
|
717
|
+
// Fix title with storyPrefix - handle multiple story formats
|
|
664
718
|
// Note: (?::\s*\w+(?:<[^>]+>)?)? handles TypeScript type annotations including generics
|
|
665
719
|
// e.g., "const meta: Meta = {" or "const meta: Meta<typeof Button> = {"
|
|
720
|
+
// Pattern 1: CSF format - const meta = { title: "..." }
|
|
666
721
|
fixedFileContents = fixedFileContents.replace(/(const\s+meta\s*(?::\s*\w+(?:<[^>]+>)?)?\s*=\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
|
|
667
722
|
// Check if the title already has the prefix to avoid double prefixing
|
|
668
|
-
const titleToUse =
|
|
669
|
-
?
|
|
670
|
-
: config.storyPrefix +
|
|
723
|
+
const titleToUse = cleanTitle.startsWith(config.storyPrefix)
|
|
724
|
+
? cleanTitle
|
|
725
|
+
: config.storyPrefix + cleanTitle;
|
|
671
726
|
return p1 + titleToUse + p3;
|
|
672
727
|
});
|
|
673
|
-
//
|
|
728
|
+
// Pattern 2: export default { title: "..." } format
|
|
674
729
|
if (!fixedFileContents.includes(config.storyPrefix)) {
|
|
675
730
|
fixedFileContents = fixedFileContents.replace(/(export\s+default\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
|
|
676
731
|
// Check if the title already has the prefix to avoid double prefixing
|
|
677
|
-
const titleToUse =
|
|
678
|
-
?
|
|
679
|
-
: config.storyPrefix +
|
|
732
|
+
const titleToUse = cleanTitle.startsWith(config.storyPrefix)
|
|
733
|
+
? cleanTitle
|
|
734
|
+
: config.storyPrefix + cleanTitle;
|
|
735
|
+
return p1 + titleToUse + p3;
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
// Pattern 3: Svelte native format - defineMeta({ title: "..." })
|
|
739
|
+
// This is CRITICAL for Svelte - without this, all Svelte stories get generic titles
|
|
740
|
+
// causing duplicate story ID errors that break the entire Storybook
|
|
741
|
+
if (!fixedFileContents.includes(config.storyPrefix)) {
|
|
742
|
+
fixedFileContents = fixedFileContents.replace(/(defineMeta\s*\(\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
|
|
743
|
+
const titleToUse = cleanTitle.startsWith(config.storyPrefix)
|
|
744
|
+
? cleanTitle
|
|
745
|
+
: config.storyPrefix + cleanTitle;
|
|
680
746
|
return p1 + titleToUse + p3;
|
|
681
747
|
});
|
|
682
748
|
}
|
|
749
|
+
// Add Storybook id parameter for uniqueness (after title line)
|
|
750
|
+
// This ensures unique story IDs without polluting the visible title
|
|
751
|
+
// Pattern: After title line, add id: '...' if not already present
|
|
752
|
+
if (!fixedFileContents.includes("id:")) {
|
|
753
|
+
// Add id after title for CSF format
|
|
754
|
+
fixedFileContents = fixedFileContents.replace(/(title:\s*["'][^"']+["'])(,?\s*\n)/, `$1,\n id: '${storyIdSlug}'$2`);
|
|
755
|
+
}
|
|
683
756
|
// FIX #5: Final validation after ALL post-processing
|
|
684
757
|
// This catches any syntax errors introduced by post-processing (e.g., buggy regex replacements)
|
|
685
|
-
|
|
758
|
+
// Use correct filename for framework-specific validation (e.g., .stories.svelte for Svelte)
|
|
759
|
+
const finalValidation = validateStoryCode(fixedFileContents, finalFileName, config);
|
|
686
760
|
// ALWAYS apply fixedCode if it exists - handles both:
|
|
687
761
|
// 1. Syntax error fixes (where isValid = false)
|
|
688
762
|
// 2. React import removal for non-React frameworks (where isValid = true but fixedCode exists)
|
|
@@ -774,7 +848,10 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
774
848
|
}
|
|
775
849
|
}
|
|
776
850
|
res.json({
|
|
777
|
-
|
|
851
|
+
// IMPORTANT: success is FALSE when we had to create a fallback error story
|
|
852
|
+
// This allows the UI to properly inform the user that generation failed
|
|
853
|
+
success: !isFallbackStory,
|
|
854
|
+
isFallback: isFallbackStory, // Explicit flag for fallback detection
|
|
778
855
|
fileName: finalFileName,
|
|
779
856
|
storyId,
|
|
780
857
|
outPath,
|
|
@@ -787,7 +864,8 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
787
864
|
errors: [...finalErrors.syntaxErrors, ...finalErrors.patternErrors, ...finalErrors.importErrors],
|
|
788
865
|
warnings: [],
|
|
789
866
|
selfHealingUsed,
|
|
790
|
-
attempts
|
|
867
|
+
attempts,
|
|
868
|
+
isFallback: isFallbackStory // Also in validation for convenience
|
|
791
869
|
},
|
|
792
870
|
runtimeValidation: {
|
|
793
871
|
enabled: isRuntimeValidationEnabled(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateStoryStream.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStoryStream.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"generateStoryStream.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStoryStream.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AA0c5C,wBAAsB,6BAA6B,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,iBA4pB9E"}
|