@tpitre/story-ui 1.2.0 → 1.4.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/mcp-server/routes/generateStory.js +94 -18
- package/dist/story-generator/configLoader.js +11 -5
- package/dist/story-generator/storyTracker.js +150 -0
- package/dist/story-generator/validateStory.js +262 -0
- package/package.json +5 -5
- package/templates/StoryUI/StoryUIPanel.tsx +37 -2
- 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
package/dist/cli/index.js
CHANGED
|
File without changes
|
|
@@ -6,6 +6,8 @@ import { buildClaudePrompt as buildFlexiblePrompt } from '../../story-generator/
|
|
|
6
6
|
import { loadUserConfig, validateConfig } from '../../story-generator/configLoader.js';
|
|
7
7
|
import { setupProductionGitignore } from '../../story-generator/productionGitignoreManager.js';
|
|
8
8
|
import { getInMemoryStoryService } from '../../story-generator/inMemoryStoryService.js';
|
|
9
|
+
import { extractAndValidateCodeBlock, createFallbackStory } from '../../story-generator/validateStory.js';
|
|
10
|
+
import { StoryTracker } from '../../story-generator/storyTracker.js';
|
|
9
11
|
const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages';
|
|
10
12
|
const CLAUDE_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-4-20250514';
|
|
11
13
|
// Legacy constants - now using dynamic discovery
|
|
@@ -177,6 +179,8 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
177
179
|
const gitignoreManager = setupProductionGitignore(config);
|
|
178
180
|
const storyService = getInMemoryStoryService(config);
|
|
179
181
|
const isProduction = gitignoreManager.isProductionMode();
|
|
182
|
+
// Initialize story tracker for managing updates vs new creations
|
|
183
|
+
const storyTracker = new StoryTracker(config);
|
|
180
184
|
// Check if this is an update to an existing story
|
|
181
185
|
const isUpdate = fileName && conversation && conversation.length > 2;
|
|
182
186
|
// Build prompt with conversation context if available
|
|
@@ -185,17 +189,43 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
185
189
|
console.log('Claude prompt:', fullPrompt);
|
|
186
190
|
const aiText = await callClaude(fullPrompt);
|
|
187
191
|
console.log('Claude raw response:', aiText);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
// Use the new robust validation system
|
|
193
|
+
const validationResult = extractAndValidateCodeBlock(aiText);
|
|
194
|
+
let fileContents;
|
|
195
|
+
let hasValidationWarnings = false;
|
|
196
|
+
if (!validationResult.isValid) {
|
|
197
|
+
console.error('Generated code validation failed:', validationResult.errors);
|
|
198
|
+
// If we have fixedCode, use it
|
|
199
|
+
if (validationResult.fixedCode) {
|
|
200
|
+
fileContents = validationResult.fixedCode;
|
|
201
|
+
hasValidationWarnings = true;
|
|
202
|
+
console.log('Using auto-fixed code with warnings:', validationResult.warnings);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// Create fallback story
|
|
206
|
+
console.log('Creating fallback story due to validation failure');
|
|
207
|
+
fileContents = createFallbackStory(prompt, config);
|
|
208
|
+
hasValidationWarnings = true;
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
211
|
+
else {
|
|
212
|
+
// Extract the validated code
|
|
213
|
+
const codeMatch = aiText.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?\s*([\s\S]*?)\s*```/i);
|
|
214
|
+
if (codeMatch) {
|
|
215
|
+
fileContents = codeMatch[1].trim();
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const importIdx = aiText.indexOf('import');
|
|
219
|
+
fileContents = importIdx !== -1 ? aiText.slice(importIdx).trim() : aiText.trim();
|
|
220
|
+
}
|
|
221
|
+
if (validationResult.warnings.length > 0) {
|
|
222
|
+
hasValidationWarnings = true;
|
|
223
|
+
console.log('Validation warnings:', validationResult.warnings);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!fileContents) {
|
|
227
|
+
console.error('No valid code could be extracted or generated.');
|
|
228
|
+
return res.status(500).json({ error: 'Failed to generate valid TypeScript code.' });
|
|
199
229
|
}
|
|
200
230
|
// Generate title based on conversation context
|
|
201
231
|
let aiTitle;
|
|
@@ -217,15 +247,29 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
217
247
|
const title = config.storyPrefix + prettyPrompt;
|
|
218
248
|
return p1 + title + p3;
|
|
219
249
|
});
|
|
250
|
+
// Check if there's an existing story with this title or prompt
|
|
251
|
+
const existingByTitle = storyTracker.findByTitle(aiTitle);
|
|
252
|
+
const existingByPrompt = storyTracker.findByPrompt(prompt);
|
|
253
|
+
const existingStory = existingByTitle || existingByPrompt;
|
|
220
254
|
// Generate unique ID and filename
|
|
221
255
|
let hash, finalFileName, storyId;
|
|
222
|
-
|
|
223
|
-
|
|
256
|
+
let isActuallyUpdate = false;
|
|
257
|
+
if (existingStory) {
|
|
258
|
+
// Use existing story's details to update instead of creating duplicate
|
|
259
|
+
console.log(`Found existing story "${existingStory.title}" - updating instead of creating new`);
|
|
260
|
+
hash = existingStory.hash;
|
|
261
|
+
finalFileName = existingStory.fileName;
|
|
262
|
+
storyId = existingStory.storyId;
|
|
263
|
+
isActuallyUpdate = true;
|
|
264
|
+
}
|
|
265
|
+
else if (isUpdate && fileName) {
|
|
266
|
+
// For conversation-based updates, use existing fileName and ID
|
|
224
267
|
finalFileName = fileName;
|
|
225
268
|
// Extract hash from existing fileName if possible
|
|
226
269
|
const hashMatch = fileName.match(/-([a-f0-9]{8})(?:\.stories\.tsx)?$/);
|
|
227
270
|
hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
|
|
228
271
|
storyId = `story-${hash}`;
|
|
272
|
+
isActuallyUpdate = true;
|
|
229
273
|
}
|
|
230
274
|
else {
|
|
231
275
|
// For new stories, generate new IDs
|
|
@@ -238,15 +282,26 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
238
282
|
const generatedStory = {
|
|
239
283
|
id: storyId,
|
|
240
284
|
title: aiTitle,
|
|
241
|
-
description:
|
|
285
|
+
description: isActuallyUpdate ? `Updated: ${prompt}` : prompt,
|
|
242
286
|
content: fixedFileContents,
|
|
243
|
-
createdAt:
|
|
287
|
+
createdAt: isActuallyUpdate ? (new Date()) : new Date(),
|
|
244
288
|
lastAccessed: new Date(),
|
|
245
|
-
prompt:
|
|
289
|
+
prompt: isActuallyUpdate ? conversation.map((msg) => `${msg.role}: ${msg.content}`).join('\n\n') : prompt,
|
|
246
290
|
components: extractComponentsFromContent(fixedFileContents)
|
|
247
291
|
};
|
|
248
292
|
storyService.storeStory(generatedStory);
|
|
249
|
-
|
|
293
|
+
// Register with story tracker
|
|
294
|
+
const mapping = {
|
|
295
|
+
title: aiTitle,
|
|
296
|
+
fileName: finalFileName,
|
|
297
|
+
storyId,
|
|
298
|
+
hash,
|
|
299
|
+
createdAt: new Date().toISOString(),
|
|
300
|
+
updatedAt: new Date().toISOString(),
|
|
301
|
+
prompt
|
|
302
|
+
};
|
|
303
|
+
storyTracker.registerStory(mapping);
|
|
304
|
+
console.log(`Story ${isActuallyUpdate ? 'updated' : 'stored'} in memory: ${storyId}`);
|
|
250
305
|
res.json({
|
|
251
306
|
success: true,
|
|
252
307
|
fileName: finalFileName,
|
|
@@ -255,13 +310,29 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
255
310
|
story: fileContents,
|
|
256
311
|
environment: 'production',
|
|
257
312
|
storage: 'in-memory',
|
|
258
|
-
isUpdate
|
|
313
|
+
isUpdate: isActuallyUpdate,
|
|
314
|
+
validation: {
|
|
315
|
+
hasWarnings: hasValidationWarnings,
|
|
316
|
+
errors: validationResult.errors || [],
|
|
317
|
+
warnings: validationResult.warnings || []
|
|
318
|
+
}
|
|
259
319
|
});
|
|
260
320
|
}
|
|
261
321
|
else {
|
|
262
322
|
// Development: Write to file system
|
|
263
323
|
const outPath = generateStory({ fileContents: fixedFileContents, fileName: finalFileName });
|
|
264
|
-
|
|
324
|
+
// Register with story tracker
|
|
325
|
+
const mapping = {
|
|
326
|
+
title: aiTitle,
|
|
327
|
+
fileName: finalFileName,
|
|
328
|
+
storyId,
|
|
329
|
+
hash,
|
|
330
|
+
createdAt: new Date().toISOString(),
|
|
331
|
+
updatedAt: new Date().toISOString(),
|
|
332
|
+
prompt
|
|
333
|
+
};
|
|
334
|
+
storyTracker.registerStory(mapping);
|
|
335
|
+
console.log(`Story ${isActuallyUpdate ? 'updated' : 'written'} to:`, outPath);
|
|
265
336
|
res.json({
|
|
266
337
|
success: true,
|
|
267
338
|
fileName: finalFileName,
|
|
@@ -270,7 +341,12 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
270
341
|
story: fileContents,
|
|
271
342
|
environment: 'development',
|
|
272
343
|
storage: 'file-system',
|
|
273
|
-
isUpdate
|
|
344
|
+
isUpdate: isActuallyUpdate,
|
|
345
|
+
validation: {
|
|
346
|
+
hasWarnings: hasValidationWarnings,
|
|
347
|
+
errors: validationResult.errors || [],
|
|
348
|
+
warnings: validationResult.warnings || []
|
|
349
|
+
}
|
|
274
350
|
});
|
|
275
351
|
}
|
|
276
352
|
}
|
|
@@ -28,13 +28,18 @@ export function loadUserConfig() {
|
|
|
28
28
|
console.log(`Loading Story UI config from: ${configPath}`);
|
|
29
29
|
// Read and evaluate the config file
|
|
30
30
|
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
31
|
-
//
|
|
32
|
-
if (configContent.includes('module.exports')) {
|
|
31
|
+
// Handle both CommonJS and ES modules
|
|
32
|
+
if (configContent.includes('module.exports') || configContent.includes('export default')) {
|
|
33
33
|
// Create a temporary module context
|
|
34
34
|
const module = { exports: {} };
|
|
35
35
|
const exports = module.exports;
|
|
36
|
+
// For ES modules, convert to CommonJS for evaluation
|
|
37
|
+
let evalContent = configContent;
|
|
38
|
+
if (configContent.includes('export default')) {
|
|
39
|
+
evalContent = configContent.replace(/export\s+default\s+/, 'module.exports = ');
|
|
40
|
+
}
|
|
36
41
|
// Evaluate the config file content
|
|
37
|
-
eval(
|
|
42
|
+
eval(evalContent);
|
|
38
43
|
const userConfig = module.exports;
|
|
39
44
|
const config = createStoryUIConfig(userConfig.default || userConfig);
|
|
40
45
|
// Cache the loaded config
|
|
@@ -82,13 +87,14 @@ export function validateConfig(config) {
|
|
|
82
87
|
if (!config.componentsPath && !config.componentsMetadataPath && (!config.components || config.components.length === 0)) {
|
|
83
88
|
errors.push('Either componentsPath, componentsMetadataPath, or a components array must be specified');
|
|
84
89
|
}
|
|
85
|
-
|
|
90
|
+
// Only validate componentsPath if it's provided (not null/undefined)
|
|
91
|
+
if (config.componentsPath && config.componentsPath !== null && !fs.existsSync(config.componentsPath)) {
|
|
86
92
|
errors.push(`Components path does not exist: ${config.componentsPath}`);
|
|
87
93
|
}
|
|
88
94
|
if (config.componentsMetadataPath && !fs.existsSync(config.componentsMetadataPath)) {
|
|
89
95
|
errors.push(`Components metadata path does not exist: ${config.componentsMetadataPath}`);
|
|
90
96
|
}
|
|
91
|
-
// Check import path
|
|
97
|
+
// Check import path - but allow actual library names like 'antd'
|
|
92
98
|
if (!config.importPath || config.importPath === 'your-component-library' || config.importPath.trim() === '') {
|
|
93
99
|
errors.push('importPath must be configured to point to your component library');
|
|
94
100
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export class StoryTracker {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.mappingFile = path.join(path.dirname(config.generatedStoriesPath), '.story-mappings.json');
|
|
6
|
+
this.mappings = new Map();
|
|
7
|
+
this.loadMappings();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Load existing mappings from disk
|
|
11
|
+
*/
|
|
12
|
+
loadMappings() {
|
|
13
|
+
try {
|
|
14
|
+
if (fs.existsSync(this.mappingFile)) {
|
|
15
|
+
const data = JSON.parse(fs.readFileSync(this.mappingFile, 'utf-8'));
|
|
16
|
+
for (const mapping of data) {
|
|
17
|
+
this.mappings.set(mapping.title.toLowerCase(), mapping);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.warn('Failed to load story mappings:', error);
|
|
23
|
+
this.mappings = new Map();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Save mappings to disk
|
|
28
|
+
*/
|
|
29
|
+
saveMappings() {
|
|
30
|
+
try {
|
|
31
|
+
const data = Array.from(this.mappings.values());
|
|
32
|
+
fs.writeFileSync(this.mappingFile, JSON.stringify(data, null, 2));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('Failed to save story mappings:', error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Find an existing story by title
|
|
40
|
+
*/
|
|
41
|
+
findByTitle(title) {
|
|
42
|
+
// Normalize the title for comparison
|
|
43
|
+
const normalizedTitle = title.toLowerCase().trim();
|
|
44
|
+
// Try exact match first
|
|
45
|
+
let mapping = this.mappings.get(normalizedTitle);
|
|
46
|
+
if (mapping)
|
|
47
|
+
return mapping;
|
|
48
|
+
// Try fuzzy matching for similar titles
|
|
49
|
+
for (const [key, value] of this.mappings) {
|
|
50
|
+
// Check if the key contains the normalized title or vice versa
|
|
51
|
+
if (key.includes(normalizedTitle) || normalizedTitle.includes(key)) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
// Check for very similar titles (e.g., "dashboard" vs "inventory dashboard")
|
|
55
|
+
const keywords = normalizedTitle.split(/\s+/);
|
|
56
|
+
const keyKeywords = key.split(/\s+/);
|
|
57
|
+
// If all keywords from the shorter title are in the longer one
|
|
58
|
+
const shortKeywords = keywords.length < keyKeywords.length ? keywords : keyKeywords;
|
|
59
|
+
const longKeywords = keywords.length < keyKeywords.length ? keyKeywords : keywords;
|
|
60
|
+
if (shortKeywords.every(word => longKeywords.includes(word))) {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Find an existing story by prompt similarity
|
|
68
|
+
*/
|
|
69
|
+
findByPrompt(prompt) {
|
|
70
|
+
const normalizedPrompt = prompt.toLowerCase().trim();
|
|
71
|
+
// Remove common prefixes like "generate a", "create a", etc.
|
|
72
|
+
const cleanPrompt = normalizedPrompt
|
|
73
|
+
.replace(/^(generate|create|build|make|design|show|write|produce|construct|draft|compose|implement|add|render|display)\s+(a|an|the)?\s*/i, '')
|
|
74
|
+
.trim();
|
|
75
|
+
// Try to find by similar prompts
|
|
76
|
+
for (const mapping of this.mappings.values()) {
|
|
77
|
+
const mappingPrompt = mapping.prompt.toLowerCase()
|
|
78
|
+
.replace(/^(generate|create|build|make|design|show|write|produce|construct|draft|compose|implement|add|render|display)\s+(a|an|the)?\s*/i, '')
|
|
79
|
+
.trim();
|
|
80
|
+
if (mappingPrompt === cleanPrompt) {
|
|
81
|
+
return mapping;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Register a new or updated story
|
|
88
|
+
*/
|
|
89
|
+
registerStory(mapping) {
|
|
90
|
+
const normalizedTitle = mapping.title.toLowerCase();
|
|
91
|
+
// Check if we're updating an existing story
|
|
92
|
+
const existing = this.findByTitle(mapping.title);
|
|
93
|
+
if (existing) {
|
|
94
|
+
// Update the existing mapping
|
|
95
|
+
mapping.createdAt = existing.createdAt;
|
|
96
|
+
mapping.updatedAt = new Date().toISOString();
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// New story
|
|
100
|
+
mapping.createdAt = new Date().toISOString();
|
|
101
|
+
mapping.updatedAt = mapping.createdAt;
|
|
102
|
+
}
|
|
103
|
+
this.mappings.set(normalizedTitle, mapping);
|
|
104
|
+
this.saveMappings();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Remove a story mapping
|
|
108
|
+
*/
|
|
109
|
+
removeStory(titleOrFileName) {
|
|
110
|
+
// Try to find by title first
|
|
111
|
+
const byTitle = this.findByTitle(titleOrFileName);
|
|
112
|
+
if (byTitle) {
|
|
113
|
+
this.mappings.delete(byTitle.title.toLowerCase());
|
|
114
|
+
this.saveMappings();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
// Try to find by filename
|
|
118
|
+
for (const [key, mapping] of this.mappings) {
|
|
119
|
+
if (mapping.fileName === titleOrFileName) {
|
|
120
|
+
this.mappings.delete(key);
|
|
121
|
+
this.saveMappings();
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get all story mappings
|
|
129
|
+
*/
|
|
130
|
+
getAllMappings() {
|
|
131
|
+
return Array.from(this.mappings.values());
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Clean up orphaned mappings (stories that no longer exist)
|
|
135
|
+
*/
|
|
136
|
+
cleanupOrphaned(generatedStoriesPath) {
|
|
137
|
+
let removed = 0;
|
|
138
|
+
for (const [key, mapping] of this.mappings) {
|
|
139
|
+
const filePath = path.join(generatedStoriesPath, mapping.fileName);
|
|
140
|
+
if (!fs.existsSync(filePath)) {
|
|
141
|
+
this.mappings.delete(key);
|
|
142
|
+
removed++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (removed > 0) {
|
|
146
|
+
this.saveMappings();
|
|
147
|
+
}
|
|
148
|
+
return removed;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
/**
|
|
3
|
+
* Validates TypeScript code syntax and attempts to fix common issues
|
|
4
|
+
*/
|
|
5
|
+
export function validateStoryCode(code, fileName = 'story.tsx') {
|
|
6
|
+
const result = {
|
|
7
|
+
isValid: true,
|
|
8
|
+
errors: [],
|
|
9
|
+
warnings: []
|
|
10
|
+
};
|
|
11
|
+
try {
|
|
12
|
+
// Create a TypeScript source file
|
|
13
|
+
const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
14
|
+
// Check for syntax errors using the program API
|
|
15
|
+
const compilerOptions = {
|
|
16
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
17
|
+
target: ts.ScriptTarget.Latest,
|
|
18
|
+
module: ts.ModuleKind.ESNext,
|
|
19
|
+
allowJs: true,
|
|
20
|
+
skipLibCheck: true
|
|
21
|
+
};
|
|
22
|
+
// Create a program to get diagnostics
|
|
23
|
+
const program = ts.createProgram([fileName], compilerOptions, {
|
|
24
|
+
getSourceFile: (name) => name === fileName ? sourceFile : undefined,
|
|
25
|
+
writeFile: () => { },
|
|
26
|
+
getCurrentDirectory: () => '',
|
|
27
|
+
getDirectories: () => [],
|
|
28
|
+
fileExists: (name) => name === fileName,
|
|
29
|
+
readFile: () => '',
|
|
30
|
+
getCanonicalFileName: (name) => name,
|
|
31
|
+
useCaseSensitiveFileNames: () => true,
|
|
32
|
+
getNewLine: () => '\n',
|
|
33
|
+
getDefaultLibFileName: () => 'lib.d.ts'
|
|
34
|
+
});
|
|
35
|
+
const syntaxErrors = program.getSyntacticDiagnostics(sourceFile);
|
|
36
|
+
if (syntaxErrors.length > 0) {
|
|
37
|
+
result.isValid = false;
|
|
38
|
+
for (const diagnostic of syntaxErrors) {
|
|
39
|
+
if (diagnostic.file && diagnostic.start !== undefined) {
|
|
40
|
+
const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
|
41
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
42
|
+
result.errors.push(`Line ${position.line + 1}, Column ${position.character + 1}: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
result.errors.push(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Additional semantic checks
|
|
50
|
+
const semanticErrors = performSemanticChecks(sourceFile);
|
|
51
|
+
result.errors.push(...semanticErrors);
|
|
52
|
+
if (result.errors.length > 0) {
|
|
53
|
+
result.isValid = false;
|
|
54
|
+
// Attempt to fix common issues
|
|
55
|
+
const fixedCode = attemptAutoFix(code, result.errors);
|
|
56
|
+
if (fixedCode && fixedCode !== code) {
|
|
57
|
+
result.fixedCode = fixedCode;
|
|
58
|
+
// Re-validate the fixed code
|
|
59
|
+
const fixedValidation = validateStoryCode(fixedCode, fileName);
|
|
60
|
+
if (fixedValidation.isValid) {
|
|
61
|
+
result.isValid = true;
|
|
62
|
+
result.warnings.push('Code was automatically fixed for syntax errors');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
result.isValid = false;
|
|
69
|
+
result.errors.push(`Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Performs additional semantic checks on the AST
|
|
75
|
+
*/
|
|
76
|
+
function performSemanticChecks(sourceFile) {
|
|
77
|
+
const errors = [];
|
|
78
|
+
function visit(node) {
|
|
79
|
+
// Check for unclosed JSX elements
|
|
80
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
81
|
+
// Additional JSX-specific checks could go here
|
|
82
|
+
}
|
|
83
|
+
// Check for missing imports
|
|
84
|
+
if (ts.isIdentifier(node) && node.text && /^[A-Z]/.test(node.text)) {
|
|
85
|
+
// This is a potential component reference - would need more context to validate
|
|
86
|
+
}
|
|
87
|
+
ts.forEachChild(node, visit);
|
|
88
|
+
}
|
|
89
|
+
visit(sourceFile);
|
|
90
|
+
return errors;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Attempts to automatically fix common syntax errors
|
|
94
|
+
*/
|
|
95
|
+
function attemptAutoFix(code, errors) {
|
|
96
|
+
let fixedCode = code;
|
|
97
|
+
// Fix common issues based on error patterns
|
|
98
|
+
for (const error of errors) {
|
|
99
|
+
if (error.includes('expected ","')) {
|
|
100
|
+
// Try to fix missing commas in object literals
|
|
101
|
+
fixedCode = fixMissingCommas(fixedCode);
|
|
102
|
+
}
|
|
103
|
+
if (error.includes('Unexpected token')) {
|
|
104
|
+
// Try to fix unexpected tokens
|
|
105
|
+
fixedCode = fixUnexpectedTokens(fixedCode);
|
|
106
|
+
}
|
|
107
|
+
if (error.includes('Unterminated string literal')) {
|
|
108
|
+
// Try to fix unterminated strings
|
|
109
|
+
fixedCode = fixUnterminatedStrings(fixedCode);
|
|
110
|
+
}
|
|
111
|
+
if (error.includes('JSX element') && error.includes('has no corresponding closing tag')) {
|
|
112
|
+
// Try to fix unclosed JSX elements
|
|
113
|
+
fixedCode = fixUnclosedJSX(fixedCode);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return fixedCode;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fixes missing commas in object literals and function parameters
|
|
120
|
+
*/
|
|
121
|
+
function fixMissingCommas(code) {
|
|
122
|
+
// Common patterns where commas might be missing
|
|
123
|
+
const fixes = [
|
|
124
|
+
// Fix object property definitions
|
|
125
|
+
{ pattern: /(\w+):\s*([^,}\n]+)\s*\n\s*(\w+):/g, replacement: '$1: $2,\n $3:' },
|
|
126
|
+
// Fix array elements
|
|
127
|
+
{ pattern: /(\w+)\s*\n\s*(\w+)/g, replacement: '$1,\n $2' },
|
|
128
|
+
// Fix function parameters
|
|
129
|
+
{ pattern: /(\w+:\s*\w+)\s*\n\s*(\w+:)/g, replacement: '$1,\n $2' }
|
|
130
|
+
];
|
|
131
|
+
let fixedCode = code;
|
|
132
|
+
for (const fix of fixes) {
|
|
133
|
+
fixedCode = fixedCode.replace(fix.pattern, fix.replacement);
|
|
134
|
+
}
|
|
135
|
+
return fixedCode;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Fixes unexpected token issues
|
|
139
|
+
*/
|
|
140
|
+
function fixUnexpectedTokens(code) {
|
|
141
|
+
let fixedCode = code;
|
|
142
|
+
// Fix common unexpected token issues
|
|
143
|
+
const fixes = [
|
|
144
|
+
// Fix missing semicolons
|
|
145
|
+
{ pattern: /^(\s*)(export\s+\w+.*[^;])\s*$/gm, replacement: '$1$2;' },
|
|
146
|
+
// Fix missing quotes around strings
|
|
147
|
+
{ pattern: /:\s*([^"'\s,}]+)\s*(,|\})/g, replacement: ': "$1"$2' },
|
|
148
|
+
// Fix trailing commas in objects
|
|
149
|
+
{ pattern: /,(\s*\})/g, replacement: '$1' }
|
|
150
|
+
];
|
|
151
|
+
for (const fix of fixes) {
|
|
152
|
+
fixedCode = fixedCode.replace(fix.pattern, fix.replacement);
|
|
153
|
+
}
|
|
154
|
+
return fixedCode;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Fixes unterminated string literals
|
|
158
|
+
*/
|
|
159
|
+
function fixUnterminatedStrings(code) {
|
|
160
|
+
let fixedCode = code;
|
|
161
|
+
// Find lines with unterminated strings and try to fix them
|
|
162
|
+
const lines = fixedCode.split('\n');
|
|
163
|
+
for (let i = 0; i < lines.length; i++) {
|
|
164
|
+
const line = lines[i];
|
|
165
|
+
// Check for unterminated quotes
|
|
166
|
+
const singleQuoteCount = (line.match(/'/g) || []).length;
|
|
167
|
+
const doubleQuoteCount = (line.match(/"/g) || []).length;
|
|
168
|
+
if (singleQuoteCount % 2 !== 0) {
|
|
169
|
+
lines[i] = line + "'";
|
|
170
|
+
}
|
|
171
|
+
else if (doubleQuoteCount % 2 !== 0) {
|
|
172
|
+
lines[i] = line + '"';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Fixes unclosed JSX elements
|
|
179
|
+
*/
|
|
180
|
+
function fixUnclosedJSX(code) {
|
|
181
|
+
let fixedCode = code;
|
|
182
|
+
// Simple heuristic to fix common JSX issues
|
|
183
|
+
const jsxOpenTags = fixedCode.match(/<[A-Z]\w*[^>]*>/g) || [];
|
|
184
|
+
const jsxCloseTags = fixedCode.match(/<\/[A-Z]\w*>/g) || [];
|
|
185
|
+
// If we have more opening tags than closing tags, try to balance them
|
|
186
|
+
if (jsxOpenTags.length > jsxCloseTags.length) {
|
|
187
|
+
// This is a very basic fix - in practice, you'd need more sophisticated parsing
|
|
188
|
+
// For now, we'll just add a warning
|
|
189
|
+
}
|
|
190
|
+
return fixedCode;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Extracts and validates code blocks from AI responses
|
|
194
|
+
*/
|
|
195
|
+
export function extractAndValidateCodeBlock(aiResponse) {
|
|
196
|
+
// Try multiple extraction methods
|
|
197
|
+
const extractionMethods = [
|
|
198
|
+
// Standard code blocks
|
|
199
|
+
(text) => {
|
|
200
|
+
const match = text.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?\s*([\s\S]*?)\s*```/i);
|
|
201
|
+
return match ? match[1].trim() : null;
|
|
202
|
+
},
|
|
203
|
+
// Code starting with import
|
|
204
|
+
(text) => {
|
|
205
|
+
const importIndex = text.indexOf('import');
|
|
206
|
+
return importIndex !== -1 ? text.slice(importIndex).trim() : null;
|
|
207
|
+
},
|
|
208
|
+
// Code starting with export
|
|
209
|
+
(text) => {
|
|
210
|
+
const exportIndex = text.indexOf('export');
|
|
211
|
+
return exportIndex !== -1 ? text.slice(exportIndex).trim() : null;
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
let extractedCode = null;
|
|
215
|
+
for (const method of extractionMethods) {
|
|
216
|
+
extractedCode = method(aiResponse);
|
|
217
|
+
if (extractedCode)
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (!extractedCode) {
|
|
221
|
+
return {
|
|
222
|
+
isValid: false,
|
|
223
|
+
errors: ['No valid TypeScript code found in AI response'],
|
|
224
|
+
warnings: []
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// Validate the extracted code
|
|
228
|
+
return validateStoryCode(extractedCode);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Creates a fallback story template when generation fails
|
|
232
|
+
*/
|
|
233
|
+
export function createFallbackStory(prompt, config) {
|
|
234
|
+
const title = prompt.length > 50 ? prompt.substring(0, 50) + '...' : prompt;
|
|
235
|
+
const escapedTitle = title.replace(/"/g, '\\"');
|
|
236
|
+
return `import React from 'react';
|
|
237
|
+
import type { StoryObj } from '@storybook/react';
|
|
238
|
+
|
|
239
|
+
// Fallback story generated due to AI generation error
|
|
240
|
+
export default {
|
|
241
|
+
title: '${config.storyPrefix || 'Generated/'}${escapedTitle}',
|
|
242
|
+
component: () => (
|
|
243
|
+
<div style={{ padding: '2rem', textAlign: 'center', border: '2px dashed #ccc', borderRadius: '8px' }}>
|
|
244
|
+
<h2>Story Generation Error</h2>
|
|
245
|
+
<p>The AI-generated story contained syntax errors and could not be created.</p>
|
|
246
|
+
<p><strong>Original prompt:</strong> ${escapedTitle}</p>
|
|
247
|
+
<p>Please try rephrasing your request or contact support.</p>
|
|
248
|
+
</div>
|
|
249
|
+
),
|
|
250
|
+
parameters: {
|
|
251
|
+
docs: {
|
|
252
|
+
description: {
|
|
253
|
+
story: 'This is a fallback story created when the AI generation failed due to syntax errors.'
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export const Default: StoryObj = {
|
|
260
|
+
args: {}
|
|
261
|
+
};`;
|
|
262
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpitre/story-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "AI-powered Storybook story generator for any React component library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"license": "MIT",
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|
|
44
|
-
"url": "https://github.com/southleft/story-ui.git"
|
|
44
|
+
"url": "git+https://github.com/southleft/story-ui.git"
|
|
45
45
|
},
|
|
46
46
|
"bugs": {
|
|
47
47
|
"url": "https://github.com/southleft/story-ui/issues"
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"dotenv": "^16.3.1",
|
|
63
63
|
"express": "^4.18.2",
|
|
64
64
|
"inquirer": "^9.2.0",
|
|
65
|
-
"node-fetch": "^2.6.7"
|
|
65
|
+
"node-fetch": "^2.6.7",
|
|
66
|
+
"typescript": "^5.8.3"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@commitlint/cli": "^19.6.1",
|
|
@@ -82,8 +83,7 @@
|
|
|
82
83
|
"cz-conventional-changelog": "^3.3.0",
|
|
83
84
|
"husky": "^9.1.7",
|
|
84
85
|
"semantic-release": "^24.2.0",
|
|
85
|
-
"ts-node": "^10.9.2"
|
|
86
|
-
"typescript": "^5.8.3"
|
|
86
|
+
"ts-node": "^10.9.2"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"@storybook/react": ">=6.0.0",
|
|
@@ -501,10 +501,45 @@ const StoryUIPanel: React.FC = () => {
|
|
|
501
501
|
|
|
502
502
|
// Create user-friendly response message instead of showing raw markup
|
|
503
503
|
let responseMessage: string;
|
|
504
|
+
let statusIcon = '✅';
|
|
505
|
+
|
|
506
|
+
// Check for validation issues
|
|
507
|
+
if (data.validation && data.validation.hasWarnings) {
|
|
508
|
+
statusIcon = '⚠️';
|
|
509
|
+
const warningCount = data.validation.warnings.length;
|
|
510
|
+
const errorCount = data.validation.errors.length;
|
|
511
|
+
|
|
512
|
+
if (errorCount > 0) {
|
|
513
|
+
statusIcon = '🔧';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
504
517
|
if (data.isUpdate) {
|
|
505
|
-
responseMessage =
|
|
518
|
+
responseMessage = `${statusIcon} Updated your story: "${data.title}"\n\nI've made the requested changes while keeping the same layout structure. You can view the updated component in Storybook.`;
|
|
506
519
|
} else {
|
|
507
|
-
responseMessage =
|
|
520
|
+
responseMessage = `${statusIcon} Created new story: "${data.title}"\n\nI've generated the component with the requested features. You can view it in Storybook where you'll see both the rendered component and its markup in the Docs tab.`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Add validation information if there are issues
|
|
524
|
+
if (data.validation && data.validation.hasWarnings) {
|
|
525
|
+
responseMessage += '\n\n';
|
|
526
|
+
|
|
527
|
+
if (data.validation.errors.length > 0) {
|
|
528
|
+
responseMessage += `🔧 **Auto-fixed ${data.validation.errors.length} syntax error(s):**\n`;
|
|
529
|
+
responseMessage += data.validation.errors.slice(0, 3).map(error => ` • ${error}`).join('\n');
|
|
530
|
+
if (data.validation.errors.length > 3) {
|
|
531
|
+
responseMessage += `\n • ... and ${data.validation.errors.length - 3} more`;
|
|
532
|
+
}
|
|
533
|
+
responseMessage += '\n';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (data.validation.warnings.length > 0) {
|
|
537
|
+
responseMessage += `⚠️ **Warnings:**\n`;
|
|
538
|
+
responseMessage += data.validation.warnings.slice(0, 2).map(warning => ` • ${warning}`).join('\n');
|
|
539
|
+
if (data.validation.warnings.length > 2) {
|
|
540
|
+
responseMessage += `\n • ... and ${data.validation.warnings.length - 2} more`;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
508
543
|
}
|
|
509
544
|
|
|
510
545
|
const aiMsg = { role: 'ai' as const, content: responseMessage };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["../index.ts","../story-ui.config.loader.ts","../story-ui.config.ts","../cli/index.ts","../cli/setup.ts","../mcp-server/index.ts","../mcp-server/routes/claude.ts","../mcp-server/routes/components.ts","../mcp-server/routes/generatestory.ts","../mcp-server/routes/memorystories.ts","../mcp-server/routes/storysync.ts","../story-generator/componentdiscovery.ts","../story-generator/configloader.ts","../story-generator/generatestory.ts","../story-generator/gitignoremanager.ts","../story-generator/inmemorystoryservice.ts","../story-generator/productiongitignoremanager.ts","../story-generator/promptgenerator.ts","../story-generator/storysync.ts"],"version":"5.8.3"}
|