@tpitre/story-ui 1.7.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.env.sample +3 -1
  2. package/README.md +160 -606
  3. package/dist/cli/index.js +23 -24
  4. package/dist/cli/setup.js +295 -36
  5. package/dist/mcp-server/index.js +67 -0
  6. package/dist/mcp-server/routes/generateStory.js +323 -56
  7. package/dist/story-generator/componentBlacklist.js +181 -0
  8. package/dist/story-generator/componentDiscovery.js +9 -2
  9. package/dist/story-generator/configLoader.js +109 -39
  10. package/dist/story-generator/considerationsLoader.js +204 -0
  11. package/dist/story-generator/documentation-sources.js +36 -0
  12. package/dist/story-generator/documentationLoader.js +214 -0
  13. package/dist/story-generator/dynamicPackageDiscovery.js +527 -0
  14. package/dist/story-generator/enhancedComponentDiscovery.js +369 -118
  15. package/dist/story-generator/generateStory.js +7 -3
  16. package/dist/story-generator/postProcessStory.js +71 -0
  17. package/dist/story-generator/promptGenerator.js +286 -37
  18. package/dist/story-generator/storyHistory.js +118 -0
  19. package/dist/story-generator/storyTracker.js +33 -18
  20. package/dist/story-generator/storyValidator.js +39 -0
  21. package/dist/story-generator/universalDesignSystemAdapter.js +209 -0
  22. package/dist/story-generator/validateStory.js +82 -7
  23. package/dist/story-ui.config.js +12 -5
  24. package/package.json +11 -6
  25. package/templates/StoryUI/StoryUIPanel.stories.tsx +29 -13
  26. package/templates/StoryUI/StoryUIPanel.tsx +489 -359
  27. package/templates/react-import-rule.json +36 -0
  28. package/templates/story-generation-rules.json +29 -0
  29. package/templates/story-ui-considerations.json +156 -0
  30. package/templates/story-ui-considerations.md +109 -0
  31. package/templates/story-ui-docs-README.md +55 -0
  32. package/dist/scripts/test-validation.js +0 -81
  33. package/dist/test-storybooks/chakra-test/src/components/index.js +0 -3
  34. package/dist/test-storybooks/custom-design-test/src/components/index.js +0 -3
  35. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -1,13 +1,18 @@
1
1
  import fetch from 'node-fetch';
2
2
  import { generateStory } from '../../story-generator/generateStory.js';
3
- import crypto from 'crypto';
3
+ import * as crypto from 'crypto';
4
4
  import { buildClaudePrompt as buildFlexiblePrompt } from '../../story-generator/promptGenerator.js';
5
5
  import { loadUserConfig, validateConfig } from '../../story-generator/configLoader.js';
6
6
  import { setupProductionGitignore } from '../../story-generator/productionGitignoreManager.js';
7
7
  import { getInMemoryStoryService } from '../../story-generator/inMemoryStoryService.js';
8
8
  import { extractAndValidateCodeBlock, createFallbackStory } from '../../story-generator/validateStory.js';
9
+ import { isBlacklistedComponent, isBlacklistedIcon, getBlacklistErrorMessage, ICON_CORRECTIONS } from '../../story-generator/componentBlacklist.js';
9
10
  import { StoryTracker } from '../../story-generator/storyTracker.js';
10
11
  import { EnhancedComponentDiscovery } from '../../story-generator/enhancedComponentDiscovery.js';
12
+ import { getDocumentation } from '../../story-generator/documentation-sources.js';
13
+ import { postProcessStory } from '../../story-generator/postProcessStory.js';
14
+ import { validateStory } from '../../story-generator/storyValidator.js';
15
+ import { StoryHistoryManager } from '../../story-generator/storyHistory.js';
11
16
  const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages';
12
17
  const CLAUDE_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-4-20250514';
13
18
  // Legacy constants - now using dynamic discovery
@@ -20,26 +25,70 @@ async function buildClaudePrompt(userPrompt) {
20
25
  const config = loadUserConfig();
21
26
  const discovery = new EnhancedComponentDiscovery(config);
22
27
  const components = await discovery.discoverAll();
23
- return buildFlexiblePrompt(userPrompt, config, components);
28
+ return await buildFlexiblePrompt(userPrompt, config, components);
24
29
  }
25
- // Enhanced function that includes conversation context
26
- async function buildClaudePromptWithContext(userPrompt, config, conversation) {
30
+ // Enhanced function that includes conversation context and previous code
31
+ async function buildClaudePromptWithContext(userPrompt, config, conversation, previousCode) {
27
32
  const discovery = new EnhancedComponentDiscovery(config);
28
33
  const components = await discovery.discoverAll();
29
- // If no conversation context, use the standard prompt
34
+ // Always start with component discovery as the authoritative source
35
+ console.log(`📦 Discovered ${components.length} components from ${config.importPath}`);
36
+ const availableComponents = components.map(c => c.name).join(', ');
37
+ console.log(`✅ Available components: ${availableComponents}`);
38
+ // Build base prompt with discovered components (always required)
39
+ let prompt = await buildFlexiblePrompt(userPrompt, config, components);
40
+ // Try to enhance with bundled documentation for usage patterns and design tokens
41
+ console.log('📋 Using bundled documentation for enhancement');
42
+ const documentation = getDocumentation(config.importPath);
43
+ if (documentation) {
44
+ const bundledEnhancement = `
45
+
46
+ 📚 BUNDLED DOCUMENTATION:
47
+ ${Object.entries(documentation.components || {}).map(([name, info]) => {
48
+ // Only include docs for components that actually exist in the discovered list
49
+ if (components.some(c => c.name === name)) {
50
+ return `- ${name}: ${info.description || 'Component available'}
51
+ ${info.variants ? `Variants: ${info.variants.join(', ')}` : ''}
52
+ ${info.commonProps ? `Props: ${info.commonProps.join(', ')}` : ''}
53
+ ${info.examples ? `\n Examples:\n${info.examples.map((ex) => ` // ${ex.label}
54
+ ${ex.code}`).join('\n')}` : ''}`;
55
+ }
56
+ return null;
57
+ }).filter(Boolean).join('\n\n')}`;
58
+ prompt = prompt.replace('User request:', `${bundledEnhancement}
59
+
60
+ User request:`);
61
+ }
62
+ // If no conversation context, return the prompt as-is
30
63
  if (!conversation || conversation.length <= 1) {
31
- return buildFlexiblePrompt(userPrompt, config, components);
64
+ return prompt;
32
65
  }
33
66
  // Extract conversation context for modifications
34
67
  const conversationContext = conversation
35
68
  .slice(0, -1) // Remove the current message (last one)
36
69
  .map((msg) => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`)
37
70
  .join('\n\n');
38
- // Get the base prompt
39
- const basePrompt = buildFlexiblePrompt(userPrompt, config, components);
71
+ // Build contextual prompt with previous code if available
72
+ let contextSection = `CONVERSATION CONTEXT (for modifications/updates):
73
+ ${conversationContext}`;
74
+ if (previousCode) {
75
+ contextSection += `
76
+
77
+ PREVIOUS GENERATED CODE (this is what you're modifying):
78
+ \`\`\`tsx
79
+ ${previousCode}
80
+ \`\`\`
81
+
82
+ CRITICAL INSTRUCTIONS FOR MODIFICATIONS:
83
+ 1. DO NOT regenerate the entire story from scratch
84
+ 2. PRESERVE all existing styling, components, and structure
85
+ 3. ONLY change what the user specifically requests
86
+ 4. Keep the exact same layout (Grid structure, columns, etc.) unless explicitly asked to change it
87
+ 5. Maintain all visual styling (colors, shadows, spacing) unless asked to modify them
88
+ 6. Think of this as EDITING the code above, not creating new code`;
89
+ }
40
90
  // Add conversation context to the prompt
41
- const contextualPrompt = basePrompt.replace('User request:', `CONVERSATION CONTEXT (for modifications/updates):
42
- ${conversationContext}
91
+ const contextualPrompt = prompt.replace('User request:', `${contextSection}
43
92
 
44
93
  IMPORTANT: The user is asking to modify/update the story based on the above conversation.
45
94
  - Keep the SAME layout structure (number of columns, grid setup) unless explicitly asked to change it
@@ -50,6 +99,9 @@ Current modification request:`);
50
99
  return contextualPrompt;
51
100
  }
52
101
  function slugify(str) {
102
+ if (!str || typeof str !== 'string') {
103
+ return 'untitled';
104
+ }
53
105
  return str
54
106
  .toLowerCase()
55
107
  .replace(/[^a-z0-9]+/g, '-')
@@ -60,7 +112,7 @@ function extractCodeBlock(text) {
60
112
  const codeBlock = text.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?([\s\S]*?)```/i);
61
113
  return codeBlock ? codeBlock[1].trim() : null;
62
114
  }
63
- async function callClaude(prompt) {
115
+ async function callClaude(messages) {
64
116
  const apiKey = process.env.CLAUDE_API_KEY;
65
117
  if (!apiKey)
66
118
  throw new Error('Claude API key not set');
@@ -73,8 +125,8 @@ async function callClaude(prompt) {
73
125
  },
74
126
  body: JSON.stringify({
75
127
  model: CLAUDE_MODEL,
76
- max_tokens: 4096,
77
- messages: [{ role: 'user', content: prompt }],
128
+ max_tokens: 8192,
129
+ messages,
78
130
  }),
79
131
  });
80
132
  const data = await response.json();
@@ -82,6 +134,9 @@ async function callClaude(prompt) {
82
134
  return data?.content?.[0]?.text || data?.completion || '';
83
135
  }
84
136
  function cleanPromptForTitle(prompt) {
137
+ if (!prompt || typeof prompt !== 'string') {
138
+ return 'Untitled Story';
139
+ }
85
140
  // Remove common leading phrases (case-insensitive)
86
141
  const leadingPhrases = [
87
142
  /^generate (a|an|the)? /i,
@@ -127,7 +182,7 @@ async function getClaudeTitle(userPrompt) {
127
182
  '',
128
183
  'Title:'
129
184
  ].join('\n');
130
- const aiText = await callClaude(titlePrompt);
185
+ const aiText = await callClaude([{ role: 'user', content: titlePrompt }]);
131
186
  // Take the first non-empty line, trim, and remove quotes if present
132
187
  const lines = aiText.split('\n').map(l => l.trim()).filter(Boolean);
133
188
  if (lines.length > 0) {
@@ -153,7 +208,101 @@ function escapeTitleForTS(title) {
153
208
  .replace(/\r/g, '\\r') // Escape carriage returns
154
209
  .replace(/\t/g, '\\t'); // Escape tabs
155
210
  }
211
+ function extractImportsFromCode(code, importPath) {
212
+ const imports = [];
213
+ // Match import statements from the specific import path
214
+ const importRegex = new RegExp(`import\\s*{([^}]+)}\\s*from\\s*['"]${importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
215
+ let match;
216
+ while ((match = importRegex.exec(code)) !== null) {
217
+ const importList = match[1];
218
+ // Split by comma and clean up each import
219
+ const components = importList.split(',').map(comp => comp.trim());
220
+ imports.push(...components);
221
+ }
222
+ return imports;
223
+ }
224
+ async function preValidateImports(code, config, discovery) {
225
+ const errors = [];
226
+ // Extract imports from the main import path
227
+ const componentImports = extractImportsFromCode(code, config.importPath);
228
+ // Use the enhanced discovery to validate components
229
+ const validation = await discovery.validateComponentNames(componentImports);
230
+ // Check for blacklisted components first
231
+ const allowedComponents = new Set(discovery.getAvailableComponentNames());
232
+ for (const importName of componentImports) {
233
+ if (isBlacklistedComponent(importName, allowedComponents, config.importPath)) {
234
+ const errorMsg = getBlacklistErrorMessage(importName, config.importPath);
235
+ errors.push(`Blacklisted component detected: ${errorMsg}`);
236
+ }
237
+ }
238
+ // Add invalid component errors with suggestions
239
+ for (const invalidComponent of validation.invalid) {
240
+ const suggestion = validation.suggestions.get(invalidComponent);
241
+ if (suggestion) {
242
+ errors.push(`Invalid component: "${invalidComponent}" does not exist in ${config.importPath}. Did you mean "${suggestion}"?`);
243
+ }
244
+ else {
245
+ errors.push(`Invalid component: "${invalidComponent}" does not exist in ${config.importPath}. Available components: ${validation.valid.slice(0, 5).join(', ')}${validation.valid.length > 5 ? '...' : ''}`);
246
+ }
247
+ }
248
+ // Extract icon imports (keep existing icon validation)
249
+ if (config.iconImports?.package) {
250
+ const allowedIcons = new Set(config.iconImports?.commonIcons || []);
251
+ const iconImports = extractImportsFromCode(code, config.iconImports.package);
252
+ for (const iconName of iconImports) {
253
+ if (isBlacklistedIcon(iconName, allowedIcons)) {
254
+ const correction = ICON_CORRECTIONS[iconName];
255
+ if (correction) {
256
+ errors.push(`Invalid icon: "${iconName}" does not exist. Did you mean "${correction}"?`);
257
+ }
258
+ else {
259
+ errors.push(`Invalid icon: "${iconName}" is not in the list of available icons.`);
260
+ }
261
+ }
262
+ else if (!allowedIcons.has(iconName)) {
263
+ // Try to find a similar icon
264
+ const similarIcon = findSimilarIcon(iconName, allowedIcons);
265
+ if (similarIcon) {
266
+ errors.push(`Invalid icon: "${iconName}" does not exist. Did you mean "${similarIcon}"?`);
267
+ }
268
+ else {
269
+ errors.push(`Invalid icon: "${iconName}" is not in the list of available icons.`);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ return {
275
+ isValid: errors.length === 0,
276
+ errors
277
+ };
278
+ }
279
+ function findSimilarIcon(iconName, allowedIcons) {
280
+ if (!iconName || typeof iconName !== 'string') {
281
+ return null;
282
+ }
283
+ // Simple similarity check - find icons that contain similar words
284
+ const iconLower = iconName.toLowerCase();
285
+ for (const allowed of allowedIcons) {
286
+ const allowedLower = allowed.toLowerCase();
287
+ // Check if the core word matches
288
+ if (iconLower.includes('commit') && allowedLower.includes('commit'))
289
+ return allowed;
290
+ if (iconLower.includes('branch') && allowedLower.includes('branch'))
291
+ return allowed;
292
+ if (iconLower.includes('merge') && allowedLower.includes('merge'))
293
+ return allowed;
294
+ if (iconLower.includes('pull') && allowedLower.includes('pull'))
295
+ return allowed;
296
+ }
297
+ return null;
298
+ }
156
299
  function fileNameFromTitle(title, hash) {
300
+ if (!title || typeof title !== 'string') {
301
+ title = 'untitled';
302
+ }
303
+ if (!hash || typeof hash !== 'string') {
304
+ hash = 'default';
305
+ }
157
306
  // Lowercase, replace spaces/special chars with dashes, remove quotes, truncate
158
307
  let base = title
159
308
  .toLowerCase()
@@ -183,42 +332,126 @@ export async function generateStoryFromPrompt(req, res) {
183
332
  const isProduction = gitignoreManager.isProductionMode();
184
333
  // Initialize story tracker for managing updates vs new creations
185
334
  const storyTracker = new StoryTracker(config);
335
+ // Initialize history manager - use the current working directory
336
+ const historyManager = new StoryHistoryManager(process.cwd());
186
337
  // Check if this is an update to an existing story
187
338
  const isUpdate = fileName && conversation && conversation.length > 2;
188
- // Build prompt with conversation context if available
189
- const fullPrompt = await buildClaudePromptWithContext(prompt, config, conversation);
190
- console.log('Layout configuration:', JSON.stringify(config.layoutRules, null, 2));
191
- console.log('Claude prompt:', fullPrompt);
192
- const aiText = await callClaude(fullPrompt);
193
- console.log('Claude raw response:', aiText);
339
+ // Get previous code if this is an update
340
+ let previousCode;
341
+ let parentVersionId;
342
+ if (isUpdate && fileName) {
343
+ const currentVersion = historyManager.getCurrentVersion(fileName);
344
+ if (currentVersion) {
345
+ previousCode = currentVersion.code;
346
+ parentVersionId = currentVersion.id;
347
+ console.log('🔄 Found previous version for iteration');
348
+ }
349
+ }
350
+ // --- Start of Validation and Retry Loop ---
351
+ let aiText = '';
352
+ let validationErrors = [];
353
+ const maxRetries = 3;
354
+ let attempts = 0;
355
+ const initialPrompt = await buildClaudePromptWithContext(prompt, config, conversation, previousCode);
356
+ const messages = [{ role: 'user', content: initialPrompt }];
357
+ while (attempts < maxRetries) {
358
+ attempts++;
359
+ console.log(`--- Story Generation Attempt ${attempts} ---`);
360
+ const claudeResponse = await callClaude(messages);
361
+ const extractedCode = extractCodeBlock(claudeResponse);
362
+ if (!extractedCode) {
363
+ aiText = claudeResponse; // Use raw response if no code block
364
+ if (attempts < maxRetries) {
365
+ console.log('No code block found, retrying...');
366
+ messages.push({ role: 'assistant', content: aiText });
367
+ messages.push({ role: 'user', content: 'You did not provide a code block. Please provide the complete story in a single `tsx` code block.' });
368
+ continue;
369
+ }
370
+ else {
371
+ // On last attempt, accept the response as is
372
+ break;
373
+ }
374
+ }
375
+ else {
376
+ aiText = extractedCode;
377
+ }
378
+ validationErrors = validateStory(aiText);
379
+ if (validationErrors.length === 0) {
380
+ console.log('✅ Validation successful!');
381
+ break; // Exit loop on success
382
+ }
383
+ console.log(`❌ Validation failed with ${validationErrors.length} errors:`);
384
+ validationErrors.forEach(err => console.log(` - Line ${err.line}: ${err.message}`));
385
+ if (attempts < maxRetries) {
386
+ const errorFeedback = validationErrors
387
+ .map(err => `- Line ${err.line}: ${err.message}`)
388
+ .join('\n');
389
+ const retryPrompt = `Your previous attempt failed validation with the following errors:\n${errorFeedback}\n\nPlease correct these issues and provide the full, valid story code. Do not use the forbidden patterns.`;
390
+ messages.push({ role: 'assistant', content: claudeResponse });
391
+ messages.push({ role: 'user', content: retryPrompt });
392
+ }
393
+ }
394
+ if (validationErrors.length > 0) {
395
+ console.error(`Story generation failed after ${maxRetries} attempts.`);
396
+ // Optional: decide if you want to return an error or proceed with the last attempt
397
+ // For now, we'll proceed with the last attempt and let the user see the result
398
+ }
399
+ // --- End of Validation and Retry Loop ---
400
+ console.log('Claude final response:', aiText);
401
+ // Create enhanced component discovery for validation
402
+ const discovery = new EnhancedComponentDiscovery(config);
403
+ await discovery.discoverAll();
404
+ // Pre-validate imports in the raw AI text to catch blacklisted components early
405
+ const preValidation = await preValidateImports(aiText, config, discovery);
406
+ if (!preValidation.isValid) {
407
+ console.error('Pre-validation failed - blacklisted components detected:', preValidation.errors);
408
+ // Return error immediately without creating file
409
+ return res.status(400).json({
410
+ error: 'Generated code contains invalid imports',
411
+ details: preValidation.errors,
412
+ suggestion: 'The AI tried to use components that do not exist. Please try rephrasing your request using basic components like Box, Stack, Header, Button, etc.'
413
+ });
414
+ }
194
415
  // Use the new robust validation system
195
- const validationResult = extractAndValidateCodeBlock(aiText);
416
+ const validationResult = extractAndValidateCodeBlock(aiText, config);
196
417
  let fileContents;
197
418
  let hasValidationWarnings = false;
198
- if (!validationResult.isValid) {
419
+ console.log('Validation result:', {
420
+ isValid: validationResult.isValid,
421
+ errors: validationResult.errors,
422
+ warnings: validationResult.warnings,
423
+ hasFixedCode: !!validationResult.fixedCode
424
+ });
425
+ if (!validationResult.isValid && !validationResult.fixedCode) {
199
426
  console.error('Generated code validation failed:', validationResult.errors);
200
- // If we have fixedCode, use it
427
+ // Create fallback story only if we can't fix the code
428
+ console.log('Creating fallback story due to validation failure');
429
+ fileContents = createFallbackStory(prompt, config);
430
+ hasValidationWarnings = true;
431
+ }
432
+ else {
433
+ // Use fixed code if available, otherwise use the extracted code
201
434
  if (validationResult.fixedCode) {
202
435
  fileContents = validationResult.fixedCode;
203
436
  hasValidationWarnings = true;
204
- console.log('Using auto-fixed code with warnings:', validationResult.warnings);
205
- }
206
- else {
207
- // Create fallback story
208
- console.log('Creating fallback story due to validation failure');
209
- fileContents = createFallbackStory(prompt, config);
210
- hasValidationWarnings = true;
211
- }
212
- }
213
- else {
214
- // Extract the validated code
215
- const codeMatch = aiText.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?\s*([\s\S]*?)\s*```/i);
216
- if (codeMatch) {
217
- fileContents = codeMatch[1].trim();
437
+ console.log('Using auto-fixed code');
218
438
  }
219
439
  else {
220
- const importIdx = aiText.indexOf('import');
221
- fileContents = importIdx !== -1 ? aiText.slice(importIdx).trim() : aiText.trim();
440
+ // Extract the validated code
441
+ const codeMatch = aiText.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?\s*([\s\S]*?)\s*```/i);
442
+ if (codeMatch) {
443
+ fileContents = codeMatch[1].trim();
444
+ }
445
+ else {
446
+ // Fallback: extract from import to end of valid TypeScript
447
+ const importIdx = aiText.indexOf('import');
448
+ if (importIdx !== -1) {
449
+ fileContents = aiText;
450
+ }
451
+ else {
452
+ fileContents = aiText.trim();
453
+ }
454
+ }
222
455
  }
223
456
  if (validationResult.warnings && validationResult.warnings.length > 0) {
224
457
  hasValidationWarnings = true;
@@ -229,6 +462,12 @@ export async function generateStoryFromPrompt(req, res) {
229
462
  console.error('No valid code could be extracted or generated.');
230
463
  return res.status(500).json({ error: 'Failed to generate valid TypeScript code.' });
231
464
  }
465
+ // CRITICAL: Ensure React import exists but avoid duplicates
466
+ if (!fileContents.includes("import React from 'react';")) {
467
+ fileContents = "import React from 'react';\n" + fileContents;
468
+ }
469
+ // Post-processing is now consolidated to run once on the final code
470
+ let fixedFileContents = postProcessStory(fileContents, config.importPath);
232
471
  // Generate title based on conversation context
233
472
  let aiTitle;
234
473
  if (isUpdate) {
@@ -245,26 +484,34 @@ export async function generateStoryFromPrompt(req, res) {
245
484
  }
246
485
  // Escape the title for TypeScript
247
486
  const prettyPrompt = escapeTitleForTS(aiTitle);
248
- const fixedFileContents = fileContents.replace(/(export default \{\s*\n\s*title:\s*["'])([^"']+)(["'])/, (match, p1, _p2, p3) => {
487
+ // Fix title with storyPrefix - handle both single-line and multi-line formats
488
+ fixedFileContents = fixedFileContents.replace(/(const\s+meta\s*=\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
249
489
  const title = config.storyPrefix + prettyPrompt;
250
490
  return p1 + title + p3;
251
491
  });
252
- // Check if there's an existing story with this title or prompt
253
- const existingByTitle = storyTracker.findByTitle(aiTitle);
254
- const existingByPrompt = storyTracker.findByPrompt(prompt);
255
- const existingStory = existingByTitle || existingByPrompt;
492
+ // Fallback: export default { title: "..." } format
493
+ if (!fixedFileContents.includes(config.storyPrefix)) {
494
+ fixedFileContents = fixedFileContents.replace(/(export\s+default\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
495
+ const title = config.storyPrefix + prettyPrompt;
496
+ return p1 + title + p3;
497
+ });
498
+ }
499
+ // Check if this is an update to an existing story
500
+ // ONLY consider it an update if we're in the same conversation context
501
+ let existingStory = null;
502
+ if (isUpdate && fileName) {
503
+ // When updating within a conversation, look for the story by fileName
504
+ existingStory = storyTracker.findByTitle(aiTitle);
505
+ if (existingStory && existingStory.fileName !== fileName) {
506
+ // If found story has different fileName, it's not the same story
507
+ existingStory = null;
508
+ }
509
+ }
510
+ // Remove the automatic "find by prompt" logic that was preventing duplicates
256
511
  // Generate unique ID and filename
257
512
  let hash, finalFileName, storyId;
258
513
  let isActuallyUpdate = false;
259
- if (existingStory) {
260
- // Use existing story's details to update instead of creating duplicate
261
- console.log(`Found existing story "${existingStory.title}" - updating instead of creating new`);
262
- hash = existingStory.hash;
263
- finalFileName = existingStory.fileName;
264
- storyId = existingStory.storyId;
265
- isActuallyUpdate = true;
266
- }
267
- else if (isUpdate && fileName) {
514
+ if (isUpdate && fileName) {
268
515
  // For conversation-based updates, use existing fileName and ID
269
516
  finalFileName = fileName;
270
517
  // Extract hash from existing fileName if possible
@@ -274,10 +521,12 @@ export async function generateStoryFromPrompt(req, res) {
274
521
  isActuallyUpdate = true;
275
522
  }
276
523
  else {
277
- // For new stories, generate new IDs
278
- hash = crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
524
+ // For new stories, ALWAYS generate new IDs with timestamp to ensure uniqueness
525
+ const timestamp = Date.now();
526
+ hash = crypto.createHash('sha1').update(prompt + timestamp).digest('hex').slice(0, 8);
279
527
  finalFileName = fileName || fileNameFromTitle(aiTitle, hash);
280
528
  storyId = `story-${hash}`;
529
+ isActuallyUpdate = false;
281
530
  }
282
531
  if (isProduction) {
283
532
  // Production: Store in memory
@@ -303,6 +552,8 @@ export async function generateStoryFromPrompt(req, res) {
303
552
  prompt
304
553
  };
305
554
  storyTracker.registerStory(mapping);
555
+ // Save to history
556
+ historyManager.addVersion(finalFileName, prompt, fixedFileContents, parentVersionId);
306
557
  console.log(`Story ${isActuallyUpdate ? 'updated' : 'stored'} in memory: ${storyId}`);
307
558
  res.json({
308
559
  success: true,
@@ -322,7 +573,11 @@ export async function generateStoryFromPrompt(req, res) {
322
573
  }
323
574
  else {
324
575
  // Development: Write to file system
325
- const outPath = generateStory({ fileContents: fixedFileContents, fileName: finalFileName });
576
+ const outPath = generateStory({
577
+ fileContents: fixedFileContents,
578
+ fileName: finalFileName,
579
+ config: config
580
+ });
326
581
  // Register with story tracker
327
582
  const mapping = {
328
583
  title: aiTitle,
@@ -334,6 +589,8 @@ export async function generateStoryFromPrompt(req, res) {
334
589
  prompt
335
590
  };
336
591
  storyTracker.registerStory(mapping);
592
+ // Save to history
593
+ historyManager.addVersion(finalFileName, prompt, fixedFileContents, parentVersionId);
337
594
  console.log(`Story ${isActuallyUpdate ? 'updated' : 'written'} to:`, outPath);
338
595
  res.json({
339
596
  success: true,
@@ -356,6 +613,16 @@ export async function generateStoryFromPrompt(req, res) {
356
613
  res.status(500).json({ error: err.message || 'Story generation failed' });
357
614
  }
358
615
  }
616
+ /**
617
+ * Fixes inline styles in the generated story content
618
+ * Converts React camelCase style properties to kebab-case CSS properties
619
+ */
620
+ function fixInlineStyles(content) {
621
+ // This function is now superseded by the validator and postProcessStory
622
+ // but can be kept for other potential style cleanups if needed.
623
+ // For now, the main logic is in the validator.
624
+ return content;
625
+ }
359
626
  /**
360
627
  * Extracts component names from story content
361
628
  */