@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.
- package/.env.sample +3 -1
- package/README.md +160 -606
- package/dist/cli/index.js +23 -24
- package/dist/cli/setup.js +295 -36
- package/dist/mcp-server/index.js +67 -0
- package/dist/mcp-server/routes/generateStory.js +323 -56
- package/dist/story-generator/componentBlacklist.js +181 -0
- package/dist/story-generator/componentDiscovery.js +9 -2
- package/dist/story-generator/configLoader.js +109 -39
- package/dist/story-generator/considerationsLoader.js +204 -0
- package/dist/story-generator/documentation-sources.js +36 -0
- package/dist/story-generator/documentationLoader.js +214 -0
- package/dist/story-generator/dynamicPackageDiscovery.js +527 -0
- package/dist/story-generator/enhancedComponentDiscovery.js +369 -118
- package/dist/story-generator/generateStory.js +7 -3
- package/dist/story-generator/postProcessStory.js +71 -0
- package/dist/story-generator/promptGenerator.js +286 -37
- package/dist/story-generator/storyHistory.js +118 -0
- package/dist/story-generator/storyTracker.js +33 -18
- package/dist/story-generator/storyValidator.js +39 -0
- package/dist/story-generator/universalDesignSystemAdapter.js +209 -0
- package/dist/story-generator/validateStory.js +82 -7
- package/dist/story-ui.config.js +12 -5
- package/package.json +11 -6
- package/templates/StoryUI/StoryUIPanel.stories.tsx +29 -13
- package/templates/StoryUI/StoryUIPanel.tsx +489 -359
- package/templates/react-import-rule.json +36 -0
- package/templates/story-generation-rules.json +29 -0
- package/templates/story-ui-considerations.json +156 -0
- package/templates/story-ui-considerations.md +109 -0
- package/templates/story-ui-docs-README.md +55 -0
- package/dist/scripts/test-validation.js +0 -81
- package/dist/test-storybooks/chakra-test/src/components/index.js +0 -3
- package/dist/test-storybooks/custom-design-test/src/components/index.js +0 -3
- 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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
39
|
-
|
|
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 =
|
|
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(
|
|
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:
|
|
77
|
-
messages
|
|
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
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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 (
|
|
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
|
-
|
|
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({
|
|
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
|
*/
|