@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.
Files changed (36) hide show
  1. package/dist/cli/index.js +0 -0
  2. package/dist/cli/setup.d.ts.map +1 -1
  3. package/dist/cli/setup.js +111 -5
  4. package/dist/mcp-server/index.js +59 -4
  5. package/dist/mcp-server/routes/generateStory.d.ts.map +1 -1
  6. package/dist/mcp-server/routes/generateStory.js +106 -28
  7. package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -1
  8. package/dist/mcp-server/routes/generateStoryStream.js +152 -33
  9. package/dist/mcp-server/routes/storyHelpers.d.ts +9 -3
  10. package/dist/mcp-server/routes/storyHelpers.d.ts.map +1 -1
  11. package/dist/mcp-server/routes/storyHelpers.js +71 -30
  12. package/dist/story-generator/configLoader.d.ts.map +1 -1
  13. package/dist/story-generator/configLoader.js +54 -31
  14. package/dist/story-generator/framework-adapters/angular-adapter.d.ts.map +1 -1
  15. package/dist/story-generator/framework-adapters/angular-adapter.js +11 -7
  16. package/dist/story-generator/framework-adapters/svelte-adapter.d.ts +1 -1
  17. package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -1
  18. package/dist/story-generator/framework-adapters/svelte-adapter.js +351 -268
  19. package/dist/story-generator/llm-providers/story-llm-service.d.ts +4 -0
  20. package/dist/story-generator/llm-providers/story-llm-service.d.ts.map +1 -1
  21. package/dist/story-generator/llm-providers/story-llm-service.js +38 -2
  22. package/dist/story-generator/promptGenerator.js +1 -1
  23. package/dist/story-generator/selfHealingLoop.d.ts +1 -0
  24. package/dist/story-generator/selfHealingLoop.d.ts.map +1 -1
  25. package/dist/story-generator/selfHealingLoop.js +96 -8
  26. package/dist/story-generator/storyValidator.d.ts.map +1 -1
  27. package/dist/story-generator/storyValidator.js +16 -6
  28. package/dist/story-generator/validateStory.d.ts +2 -1
  29. package/dist/story-generator/validateStory.d.ts.map +1 -1
  30. package/dist/story-generator/validateStory.js +263 -17
  31. package/dist/templates/StoryUI/StoryUIPanel.css +1440 -0
  32. package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
  33. package/dist/templates/StoryUI/StoryUIPanel.js +152 -26
  34. package/package.json +2 -3
  35. package/templates/StoryUI/StoryUIPanel.css +29 -0
  36. package/templates/StoryUI/StoryUIPanel.tsx +170 -27
package/dist/cli/index.js CHANGED
File without changes
@@ -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,iBA2sB5D"}
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/svelte', '@storybook/web-components'
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');
@@ -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
- const title = titleMatch ? titleMatch[1].replace('Generated/', '') : file.replace('.stories.tsx', '');
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
- const fileName = id.endsWith('.stories.tsx') ? id : `${id}.stories.tsx`;
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
- const storyFiles = files.filter(file => file.endsWith('.stories.tsx'));
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;AAua5C,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,2DA0hBxE"}
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
- // More flexible code block extraction - accept various language identifiers
150
- const codeBlock = text.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?([\s\S]*?)```/i);
151
- return codeBlock ? codeBlock[1].trim() : null;
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
- logger.debug(`Using ${providerInfo.currentProvider} (${providerInfo.currentModel}) for story generation`);
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, { maxTokens: 8192 });
216
+ return await chatCompletionWithImages(messagesWithImages, llmOptions);
177
217
  }
178
- return await chatCompletion(messages, { maxTokens: 8192 });
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
- const astValidation = validateStoryCode(aiText, 'story.tsx', config);
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
- if (bestAttempt.code && bestAttempt.code.includes('export')) {
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
- fileContents = createFrameworkAwareFallbackStory(prompt, config, detectedFramework);
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
- // Use the title without hash suffix for cleaner sidebar display
661
- // The filename already contains the hash for uniqueness
662
- const uniqueTitle = prettyPrompt;
663
- // Fix title with storyPrefix and hash - handle both single-line and multi-line formats
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 = uniqueTitle.startsWith(config.storyPrefix)
669
- ? uniqueTitle
670
- : config.storyPrefix + uniqueTitle;
723
+ const titleToUse = cleanTitle.startsWith(config.storyPrefix)
724
+ ? cleanTitle
725
+ : config.storyPrefix + cleanTitle;
671
726
  return p1 + titleToUse + p3;
672
727
  });
673
- // Fallback: export default { title: "..." } format
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 = uniqueTitle.startsWith(config.storyPrefix)
678
- ? uniqueTitle
679
- : config.storyPrefix + uniqueTitle;
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
- const finalValidation = validateStoryCode(fixedFileContents, 'story.tsx', config);
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
- success: true,
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;AAsc5C,wBAAsB,6BAA6B,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,iBAkkB9E"}
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"}