@tpitre/story-ui 2.2.0 → 2.3.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 +82 -11
- package/README.md +89 -0
- package/dist/cli/deploy.d.ts +17 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +696 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +26 -2
- package/dist/cli/setup.d.ts +11 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +437 -110
- package/dist/mcp-server/index.d.ts +2 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +120 -2
- package/dist/mcp-server/mcp-stdio-server.d.ts +3 -0
- package/dist/mcp-server/mcp-stdio-server.d.ts.map +1 -0
- package/dist/mcp-server/mcp-stdio-server.js +8 -1
- package/dist/mcp-server/routes/claude.d.ts +3 -0
- package/dist/mcp-server/routes/claude.d.ts.map +1 -0
- package/dist/mcp-server/routes/claude.js +60 -23
- package/dist/mcp-server/routes/components.d.ts +4 -0
- package/dist/mcp-server/routes/components.d.ts.map +1 -0
- package/dist/mcp-server/routes/frameworks.d.ts +38 -0
- package/dist/mcp-server/routes/frameworks.d.ts.map +1 -0
- package/dist/mcp-server/routes/frameworks.js +183 -0
- package/dist/mcp-server/routes/generateStory.d.ts +3 -0
- package/dist/mcp-server/routes/generateStory.d.ts.map +1 -0
- package/dist/mcp-server/routes/generateStory.js +160 -76
- package/dist/mcp-server/routes/generateStoryStream.d.ts +12 -0
- package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -0
- package/dist/mcp-server/routes/generateStoryStream.js +947 -0
- package/dist/mcp-server/routes/hybridStories.d.ts +18 -0
- package/dist/mcp-server/routes/hybridStories.d.ts.map +1 -0
- package/dist/mcp-server/routes/mcpRemote.d.ts +14 -0
- package/dist/mcp-server/routes/mcpRemote.d.ts.map +1 -0
- package/dist/mcp-server/routes/mcpRemote.js +489 -0
- package/dist/mcp-server/routes/memoryStories.d.ts +26 -0
- package/dist/mcp-server/routes/memoryStories.d.ts.map +1 -0
- package/dist/mcp-server/routes/providers.d.ts +89 -0
- package/dist/mcp-server/routes/providers.d.ts.map +1 -0
- package/dist/mcp-server/routes/providers.js +369 -0
- package/dist/mcp-server/routes/storySync.d.ts +26 -0
- package/dist/mcp-server/routes/storySync.d.ts.map +1 -0
- package/dist/mcp-server/routes/streamTypes.d.ts +110 -0
- package/dist/mcp-server/routes/streamTypes.d.ts.map +1 -0
- package/dist/mcp-server/routes/streamTypes.js +18 -0
- package/dist/mcp-server/sessionManager.d.ts +50 -0
- package/dist/mcp-server/sessionManager.d.ts.map +1 -0
- package/dist/story-generator/componentBlacklist.d.ts +21 -0
- package/dist/story-generator/componentBlacklist.d.ts.map +1 -0
- package/dist/story-generator/componentDiscovery.d.ts +28 -0
- package/dist/story-generator/componentDiscovery.d.ts.map +1 -0
- package/dist/story-generator/componentRegistryGenerator.d.ts +49 -0
- package/dist/story-generator/componentRegistryGenerator.d.ts.map +1 -0
- package/dist/story-generator/componentRegistryGenerator.js +205 -0
- package/dist/story-generator/configLoader.d.ts +33 -0
- package/dist/story-generator/configLoader.d.ts.map +1 -0
- package/dist/story-generator/considerationsLoader.d.ts +32 -0
- package/dist/story-generator/considerationsLoader.d.ts.map +1 -0
- package/dist/story-generator/documentation-sources.d.ts +28 -0
- package/dist/story-generator/documentation-sources.d.ts.map +1 -0
- package/dist/story-generator/documentationLoader.d.ts +64 -0
- package/dist/story-generator/documentationLoader.d.ts.map +1 -0
- package/dist/story-generator/dynamicPackageDiscovery.d.ts +97 -0
- package/dist/story-generator/dynamicPackageDiscovery.d.ts.map +1 -0
- package/dist/story-generator/enhancedComponentDiscovery.d.ts +125 -0
- package/dist/story-generator/enhancedComponentDiscovery.d.ts.map +1 -0
- package/dist/story-generator/enhancedComponentDiscovery.js +111 -11
- package/dist/story-generator/framework-adapters/angular-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/angular-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/angular-adapter.js +427 -0
- package/dist/story-generator/framework-adapters/base-adapter.d.ts +75 -0
- package/dist/story-generator/framework-adapters/base-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/base-adapter.js +147 -0
- package/dist/story-generator/framework-adapters/framework-detector.d.ts +55 -0
- package/dist/story-generator/framework-adapters/framework-detector.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/framework-detector.js +323 -0
- package/dist/story-generator/framework-adapters/index.d.ts +97 -0
- package/dist/story-generator/framework-adapters/index.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/index.js +198 -0
- package/dist/story-generator/framework-adapters/react-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/react-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/react-adapter.js +316 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.js +372 -0
- package/dist/story-generator/framework-adapters/types.d.ts +182 -0
- package/dist/story-generator/framework-adapters/types.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/types.js +8 -0
- package/dist/story-generator/framework-adapters/vue-adapter.d.ts +36 -0
- package/dist/story-generator/framework-adapters/vue-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/vue-adapter.js +336 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.d.ts +54 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.js +387 -0
- package/dist/story-generator/generateStory.d.ts +7 -0
- package/dist/story-generator/generateStory.d.ts.map +1 -0
- package/dist/story-generator/gitignoreManager.d.ts +50 -0
- package/dist/story-generator/gitignoreManager.d.ts.map +1 -0
- package/dist/story-generator/imageProcessor.d.ts +80 -0
- package/dist/story-generator/imageProcessor.d.ts.map +1 -0
- package/dist/story-generator/imageProcessor.js +391 -0
- package/dist/story-generator/inMemoryStoryService.d.ts +89 -0
- package/dist/story-generator/inMemoryStoryService.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/base-provider.d.ts +36 -0
- package/dist/story-generator/llm-providers/base-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/base-provider.js +135 -0
- package/dist/story-generator/llm-providers/claude-provider.d.ts +23 -0
- package/dist/story-generator/llm-providers/claude-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/claude-provider.js +414 -0
- package/dist/story-generator/llm-providers/gemini-provider.d.ts +24 -0
- package/dist/story-generator/llm-providers/gemini-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/gemini-provider.js +406 -0
- package/dist/story-generator/llm-providers/index.d.ts +63 -0
- package/dist/story-generator/llm-providers/index.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/index.js +169 -0
- package/dist/story-generator/llm-providers/openai-provider.d.ts +24 -0
- package/dist/story-generator/llm-providers/openai-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/openai-provider.js +458 -0
- package/dist/story-generator/llm-providers/settings-manager.d.ts +75 -0
- package/dist/story-generator/llm-providers/settings-manager.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/settings-manager.js +173 -0
- package/dist/story-generator/llm-providers/story-llm-service.d.ts +79 -0
- package/dist/story-generator/llm-providers/story-llm-service.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/story-llm-service.js +240 -0
- package/dist/story-generator/llm-providers/types.d.ts +153 -0
- package/dist/story-generator/llm-providers/types.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/types.js +8 -0
- package/dist/story-generator/logger.d.ts +14 -0
- package/dist/story-generator/logger.d.ts.map +1 -0
- package/dist/story-generator/logger.js +96 -29
- package/dist/story-generator/postProcessStory.d.ts +6 -0
- package/dist/story-generator/postProcessStory.d.ts.map +1 -0
- package/dist/story-generator/productionGitignoreManager.d.ts +91 -0
- package/dist/story-generator/productionGitignoreManager.d.ts.map +1 -0
- package/dist/story-generator/promptGenerator.d.ts +48 -0
- package/dist/story-generator/promptGenerator.d.ts.map +1 -0
- package/dist/story-generator/promptGenerator.js +186 -1
- package/dist/story-generator/storyHistory.d.ts +44 -0
- package/dist/story-generator/storyHistory.d.ts.map +1 -0
- package/dist/story-generator/storySync.d.ts +68 -0
- package/dist/story-generator/storySync.d.ts.map +1 -0
- package/dist/story-generator/storyTracker.d.ts +48 -0
- package/dist/story-generator/storyTracker.d.ts.map +1 -0
- package/dist/story-generator/storyValidator.d.ts +6 -0
- package/dist/story-generator/storyValidator.d.ts.map +1 -0
- package/dist/story-generator/universalDesignSystemAdapter.d.ts +68 -0
- package/dist/story-generator/universalDesignSystemAdapter.d.ts.map +1 -0
- package/dist/story-generator/universalDesignSystemAdapter.js +138 -1
- package/dist/story-generator/urlRedirectService.d.ts +21 -0
- package/dist/story-generator/urlRedirectService.d.ts.map +1 -0
- package/dist/story-generator/validateStory.d.ts +19 -0
- package/dist/story-generator/validateStory.d.ts.map +1 -0
- package/dist/story-generator/validateStory.js +6 -2
- package/dist/story-generator/visionPrompts.d.ts +88 -0
- package/dist/story-generator/visionPrompts.d.ts.map +1 -0
- package/dist/story-generator/visionPrompts.js +462 -0
- package/dist/story-ui.config.d.ts +78 -0
- package/dist/story-ui.config.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts +4 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.js +1874 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts +18 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.js +37 -0
- package/dist/templates/StoryUI/index.d.ts +3 -0
- package/dist/templates/StoryUI/index.d.ts.map +1 -0
- package/dist/templates/StoryUI/index.js +2 -0
- package/package.json +17 -3
- package/templates/StoryUI/StoryUIPanel.tsx +1960 -384
- package/templates/StoryUI/index.tsx +1 -1
- package/templates/StoryUI/manager.tsx +264 -0
- package/templates/production-app/.env.example +11 -0
- package/templates/production-app/index.html +66 -0
- package/templates/production-app/package.json +30 -0
- package/templates/production-app/public/favicon.svg +5 -0
- package/templates/production-app/src/App.tsx +1560 -0
- package/templates/production-app/src/LivePreviewRenderer.tsx +420 -0
- package/templates/production-app/src/componentRegistry.ts +315 -0
- package/templates/production-app/src/considerations.ts +16 -0
- package/templates/production-app/src/index.css +284 -0
- package/templates/production-app/src/main.tsx +25 -0
- package/templates/production-app/tsconfig.json +32 -0
- package/templates/production-app/tsconfig.node.json +11 -0
- package/templates/production-app/vite.config.ts +83 -0
- package/templates/react-import-rule.json +2 -2
- package/dist/index.js +0 -12
- package/dist/story-ui.config.loader.js +0 -205
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming Story Generation with Two-Way Communication
|
|
3
|
+
*
|
|
4
|
+
* This endpoint provides real-time feedback during story generation via SSE.
|
|
5
|
+
* It enables the chat to show:
|
|
6
|
+
* 1. Intent preview - what the AI plans to do
|
|
7
|
+
* 2. Progress updates - step-by-step execution
|
|
8
|
+
* 3. Execution feedback - detailed completion with reasoning
|
|
9
|
+
*/
|
|
10
|
+
import * as crypto from 'crypto';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { generateStory } from '../../story-generator/generateStory.js';
|
|
13
|
+
import { EnhancedComponentDiscovery } from '../../story-generator/enhancedComponentDiscovery.js';
|
|
14
|
+
import { buildClaudePrompt as buildFlexiblePrompt, buildFrameworkAwarePrompt, detectProjectFramework, } from '../../story-generator/promptGenerator.js';
|
|
15
|
+
import { loadUserConfig, validateConfig } from '../../story-generator/configLoader.js';
|
|
16
|
+
import { setupProductionGitignore } from '../../story-generator/productionGitignoreManager.js';
|
|
17
|
+
import { getInMemoryStoryService } from '../../story-generator/inMemoryStoryService.js';
|
|
18
|
+
import { extractAndValidateCodeBlock, createFallbackStory } from '../../story-generator/validateStory.js';
|
|
19
|
+
import { isBlacklistedComponent, isBlacklistedIcon, getBlacklistErrorMessage, ICON_CORRECTIONS } from '../../story-generator/componentBlacklist.js';
|
|
20
|
+
import { StoryTracker } from '../../story-generator/storyTracker.js';
|
|
21
|
+
import { getDocumentation } from '../../story-generator/documentation-sources.js';
|
|
22
|
+
import { postProcessStory } from '../../story-generator/postProcessStory.js';
|
|
23
|
+
import { validateStory } from '../../story-generator/storyValidator.js';
|
|
24
|
+
import { StoryHistoryManager } from '../../story-generator/storyHistory.js';
|
|
25
|
+
import { UrlRedirectService } from '../../story-generator/urlRedirectService.js';
|
|
26
|
+
import { chatCompletion, generateTitle as llmGenerateTitle, isProviderConfigured, getProviderInfo, chatCompletionWithImages, buildMessageWithImages } from '../../story-generator/llm-providers/story-llm-service.js';
|
|
27
|
+
import { processImageInputs } from '../../story-generator/imageProcessor.js';
|
|
28
|
+
import { buildVisionAwarePrompt } from '../../story-generator/visionPrompts.js';
|
|
29
|
+
import { formatSSE, createStreamEvent, } from './streamTypes.js';
|
|
30
|
+
// Helper class to manage SSE stream
|
|
31
|
+
class StreamWriter {
|
|
32
|
+
constructor(res) {
|
|
33
|
+
this.llmCalls = 0;
|
|
34
|
+
this.res = res;
|
|
35
|
+
this.startTime = Date.now();
|
|
36
|
+
}
|
|
37
|
+
// Send an event to the client
|
|
38
|
+
send(event) {
|
|
39
|
+
this.res.write(formatSSE(event));
|
|
40
|
+
}
|
|
41
|
+
// Send intent preview
|
|
42
|
+
sendIntent(intent) {
|
|
43
|
+
this.send(createStreamEvent('intent', intent));
|
|
44
|
+
}
|
|
45
|
+
// Send progress update
|
|
46
|
+
sendProgress(step, totalSteps, phase, message, details) {
|
|
47
|
+
this.send(createStreamEvent('progress', {
|
|
48
|
+
step,
|
|
49
|
+
totalSteps,
|
|
50
|
+
phase,
|
|
51
|
+
message,
|
|
52
|
+
details
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
// Send validation feedback
|
|
56
|
+
sendValidation(validation) {
|
|
57
|
+
this.send(createStreamEvent('validation', validation));
|
|
58
|
+
}
|
|
59
|
+
// Send retry info
|
|
60
|
+
sendRetry(attempt, maxAttempts, reason, errors) {
|
|
61
|
+
this.llmCalls++;
|
|
62
|
+
this.send(createStreamEvent('retry', {
|
|
63
|
+
attempt,
|
|
64
|
+
maxAttempts,
|
|
65
|
+
reason,
|
|
66
|
+
errors
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
// Send completion
|
|
70
|
+
sendCompletion(completion) {
|
|
71
|
+
this.llmCalls++;
|
|
72
|
+
const fullCompletion = {
|
|
73
|
+
...completion,
|
|
74
|
+
metrics: {
|
|
75
|
+
totalTimeMs: Date.now() - this.startTime,
|
|
76
|
+
llmCallsCount: this.llmCalls
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
this.send(createStreamEvent('completion', fullCompletion));
|
|
80
|
+
}
|
|
81
|
+
// Send error
|
|
82
|
+
sendError(error) {
|
|
83
|
+
this.send(createStreamEvent('error', error));
|
|
84
|
+
}
|
|
85
|
+
// Track LLM call
|
|
86
|
+
trackLLMCall() {
|
|
87
|
+
this.llmCalls++;
|
|
88
|
+
}
|
|
89
|
+
// Get elapsed time
|
|
90
|
+
getElapsedMs() {
|
|
91
|
+
return Date.now() - this.startTime;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Analyze prompt to determine intent
|
|
95
|
+
async function analyzeIntent(prompt, config, conversation, previousCode, options) {
|
|
96
|
+
// Determine framework
|
|
97
|
+
let framework = 'react';
|
|
98
|
+
if (options.framework) {
|
|
99
|
+
framework = options.framework;
|
|
100
|
+
}
|
|
101
|
+
else if (options.autoDetectFramework) {
|
|
102
|
+
try {
|
|
103
|
+
framework = await detectProjectFramework(process.cwd());
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
framework = 'react';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Analyze prompt for likely components
|
|
110
|
+
const componentKeywords = {
|
|
111
|
+
button: ['button', 'click', 'submit', 'action', 'cta'],
|
|
112
|
+
card: ['card', 'panel', 'tile', 'box'],
|
|
113
|
+
form: ['form', 'input', 'field', 'submit', 'login', 'signup', 'register'],
|
|
114
|
+
table: ['table', 'list', 'data', 'grid', 'rows'],
|
|
115
|
+
modal: ['modal', 'dialog', 'popup', 'overlay'],
|
|
116
|
+
navigation: ['nav', 'menu', 'header', 'sidebar', 'footer'],
|
|
117
|
+
layout: ['layout', 'page', 'section', 'container', 'grid', 'stack'],
|
|
118
|
+
pricing: ['pricing', 'price', 'plan', 'subscription', 'tier'],
|
|
119
|
+
dashboard: ['dashboard', 'analytics', 'stats', 'metrics', 'chart'],
|
|
120
|
+
profile: ['profile', 'user', 'avatar', 'account'],
|
|
121
|
+
};
|
|
122
|
+
const promptLower = prompt.toLowerCase();
|
|
123
|
+
const estimatedComponents = [];
|
|
124
|
+
for (const [component, keywords] of Object.entries(componentKeywords)) {
|
|
125
|
+
if (keywords.some(kw => promptLower.includes(kw))) {
|
|
126
|
+
estimatedComponents.push(component);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Determine strategy
|
|
130
|
+
let strategy = 'Creating new component story';
|
|
131
|
+
if (previousCode) {
|
|
132
|
+
strategy = 'Modifying existing story - preserving structure';
|
|
133
|
+
}
|
|
134
|
+
else if (options.hasImages) {
|
|
135
|
+
strategy = 'Analyzing visual reference to generate matching component';
|
|
136
|
+
}
|
|
137
|
+
else if (estimatedComponents.includes('dashboard')) {
|
|
138
|
+
strategy = 'Creating multi-section dashboard layout';
|
|
139
|
+
}
|
|
140
|
+
else if (estimatedComponents.includes('form')) {
|
|
141
|
+
strategy = 'Building form with validation-ready structure';
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
requestType: previousCode ? 'modification' : 'new',
|
|
145
|
+
framework,
|
|
146
|
+
detectedDesignSystem: options.designSystem || config.importPath?.includes('mantine') ? 'mantine' :
|
|
147
|
+
config.importPath?.includes('chakra') ? 'chakra-ui' :
|
|
148
|
+
config.importPath?.includes('mui') ? 'material-ui' : null,
|
|
149
|
+
strategy,
|
|
150
|
+
estimatedComponents,
|
|
151
|
+
promptAnalysis: {
|
|
152
|
+
hasVisionInput: !!options.hasImages,
|
|
153
|
+
hasConversationContext: !!(conversation && conversation.length > 1),
|
|
154
|
+
hasPreviousCode: !!previousCode
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// Component insights - contextual reasons based on component role
|
|
159
|
+
const COMPONENT_INSIGHTS = {
|
|
160
|
+
// Layout
|
|
161
|
+
Box: 'base container for custom layouts',
|
|
162
|
+
Container: 'centered content with max-width',
|
|
163
|
+
Stack: 'vertical flow with consistent spacing',
|
|
164
|
+
HStack: 'horizontal alignment',
|
|
165
|
+
VStack: 'vertical alignment',
|
|
166
|
+
Flex: 'flexible positioning',
|
|
167
|
+
Grid: 'multi-column responsive layout',
|
|
168
|
+
SimpleGrid: 'auto-sizing grid columns',
|
|
169
|
+
Group: 'inline element grouping',
|
|
170
|
+
Center: 'centered content',
|
|
171
|
+
Space: 'controlled whitespace',
|
|
172
|
+
Divider: 'visual section separation',
|
|
173
|
+
// Typography
|
|
174
|
+
Text: 'text with theme styling',
|
|
175
|
+
Title: 'semantic heading',
|
|
176
|
+
Heading: 'hierarchical heading',
|
|
177
|
+
Typography: 'styled text content',
|
|
178
|
+
// Feedback
|
|
179
|
+
Alert: 'contextual user notifications',
|
|
180
|
+
AlertTitle: 'alert heading',
|
|
181
|
+
Badge: 'status indicators',
|
|
182
|
+
Chip: 'compact info tags',
|
|
183
|
+
Progress: 'task completion feedback',
|
|
184
|
+
CircularProgress: 'loading state indicator',
|
|
185
|
+
LinearProgress: 'progress visualization',
|
|
186
|
+
Skeleton: 'loading placeholder',
|
|
187
|
+
Spinner: 'loading animation',
|
|
188
|
+
Loader: 'async state feedback',
|
|
189
|
+
// Actions
|
|
190
|
+
Button: 'primary user actions',
|
|
191
|
+
IconButton: 'icon-only actions',
|
|
192
|
+
ActionIcon: 'compact icon actions',
|
|
193
|
+
Menu: 'contextual options',
|
|
194
|
+
Tooltip: 'hover information',
|
|
195
|
+
// Forms
|
|
196
|
+
Input: 'text input field',
|
|
197
|
+
TextInput: 'text entry',
|
|
198
|
+
Textarea: 'multi-line text',
|
|
199
|
+
Select: 'dropdown selection',
|
|
200
|
+
Checkbox: 'binary toggle',
|
|
201
|
+
Switch: 'on/off toggle',
|
|
202
|
+
Radio: 'single selection',
|
|
203
|
+
Slider: 'range selection',
|
|
204
|
+
NumberInput: 'numeric entry',
|
|
205
|
+
// Data Display
|
|
206
|
+
Card: 'content container with elevation',
|
|
207
|
+
Paper: 'surface elevation',
|
|
208
|
+
Table: 'tabular data display',
|
|
209
|
+
List: 'sequential items',
|
|
210
|
+
Avatar: 'user representation',
|
|
211
|
+
Image: 'visual content',
|
|
212
|
+
// Navigation
|
|
213
|
+
Tabs: 'content organization',
|
|
214
|
+
Breadcrumb: 'navigation hierarchy',
|
|
215
|
+
Pagination: 'paged navigation',
|
|
216
|
+
Stepper: 'multi-step progress',
|
|
217
|
+
NavLink: 'navigation item',
|
|
218
|
+
// Overlay
|
|
219
|
+
Modal: 'focused interaction',
|
|
220
|
+
Dialog: 'user confirmation',
|
|
221
|
+
Drawer: 'side panel content',
|
|
222
|
+
Popover: 'contextual overlay',
|
|
223
|
+
Sheet: 'bottom panel (mobile-friendly)',
|
|
224
|
+
};
|
|
225
|
+
// Analyze generated code to extract components and decisions
|
|
226
|
+
function analyzeGeneratedCode(code, prompt, config) {
|
|
227
|
+
const componentsUsed = [];
|
|
228
|
+
const layoutChoices = [];
|
|
229
|
+
const styleChoices = [];
|
|
230
|
+
const promptLower = prompt.toLowerCase();
|
|
231
|
+
// Extract imported components with contextual reasons
|
|
232
|
+
const importMatch = code.match(/import\s*{([^}]+)}\s*from\s*['"][^'"]+['"]/g);
|
|
233
|
+
if (importMatch) {
|
|
234
|
+
for (const imp of importMatch) {
|
|
235
|
+
const components = imp.match(/{([^}]+)}/);
|
|
236
|
+
if (components) {
|
|
237
|
+
const names = components[1].split(',').map(n => n.trim());
|
|
238
|
+
for (const name of names) {
|
|
239
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
240
|
+
// Get contextual insight if available
|
|
241
|
+
const insight = COMPONENT_INSIGHTS[name];
|
|
242
|
+
componentsUsed.push({
|
|
243
|
+
name,
|
|
244
|
+
reason: insight || undefined
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Detect layout patterns with better context
|
|
252
|
+
const hasGrid = code.includes('Grid') || code.includes('SimpleGrid');
|
|
253
|
+
const hasStack = code.includes('Stack') || code.includes('VStack') || code.includes('HStack');
|
|
254
|
+
const hasFlex = code.includes('Flex') || /display:\s*['"]?flex/i.test(code);
|
|
255
|
+
const hasContainer = code.includes('Container');
|
|
256
|
+
if (hasGrid) {
|
|
257
|
+
const colMatch = code.match(/columns?[=:]\s*[{]?\s*(\d+|[{][^}]+[}])/i);
|
|
258
|
+
const cols = colMatch ? 'responsive columns' : 'auto columns';
|
|
259
|
+
layoutChoices.push({
|
|
260
|
+
pattern: 'Grid',
|
|
261
|
+
reason: `${cols} for organized content arrangement`
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (hasStack && !hasGrid) {
|
|
265
|
+
const isHorizontal = code.includes('HStack') || code.includes('direction="row"') || code.includes("direction='row'");
|
|
266
|
+
layoutChoices.push({
|
|
267
|
+
pattern: isHorizontal ? 'Horizontal Stack' : 'Vertical Stack',
|
|
268
|
+
reason: isHorizontal
|
|
269
|
+
? 'inline element alignment with automatic spacing'
|
|
270
|
+
: 'stacked sections with consistent gaps'
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (hasFlex && !hasStack && !hasGrid) {
|
|
274
|
+
const hasJustify = /justify/i.test(code);
|
|
275
|
+
const hasAlign = /align/i.test(code);
|
|
276
|
+
layoutChoices.push({
|
|
277
|
+
pattern: 'Flexbox',
|
|
278
|
+
reason: hasJustify && hasAlign
|
|
279
|
+
? 'precise control over element distribution and alignment'
|
|
280
|
+
: 'flexible element positioning'
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (hasContainer) {
|
|
284
|
+
layoutChoices.push({
|
|
285
|
+
pattern: 'Container',
|
|
286
|
+
reason: 'centered content with readable max-width'
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
// Detect meaningful style choices
|
|
290
|
+
const variantMatch = code.match(/variant[=:]\s*["']([^"']+)["']/gi);
|
|
291
|
+
if (variantMatch) {
|
|
292
|
+
const variants = new Set(variantMatch.map(m => m.split(/[=:]/)[1]?.trim().replace(/["']/g, '')).filter(Boolean));
|
|
293
|
+
for (const variant of Array.from(variants).slice(0, 2)) {
|
|
294
|
+
const variantReasons = {
|
|
295
|
+
'filled': 'high visual emphasis',
|
|
296
|
+
'outlined': 'secondary emphasis',
|
|
297
|
+
'subtle': 'minimal visual weight',
|
|
298
|
+
'light': 'soft background emphasis',
|
|
299
|
+
'gradient': 'eye-catching visual treatment',
|
|
300
|
+
'contained': 'solid button style',
|
|
301
|
+
'text': 'inline text action',
|
|
302
|
+
};
|
|
303
|
+
if (variantReasons[variant]) {
|
|
304
|
+
styleChoices.push({
|
|
305
|
+
property: 'variant',
|
|
306
|
+
value: variant,
|
|
307
|
+
reason: variantReasons[variant]
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Detect color usage with semantic context
|
|
313
|
+
const colorMatch = code.match(/color[=:]\s*["']([^"']+)["']/gi);
|
|
314
|
+
if (colorMatch) {
|
|
315
|
+
const colors = new Set(colorMatch.map(m => m.split(/[=:]/)[1]?.trim().replace(/["']/g, '')).filter(Boolean));
|
|
316
|
+
const semanticColors = {
|
|
317
|
+
'primary': 'brand identity emphasis',
|
|
318
|
+
'secondary': 'supporting visual accent',
|
|
319
|
+
'success': 'positive outcome indication',
|
|
320
|
+
'error': 'error state signaling',
|
|
321
|
+
'warning': 'caution indication',
|
|
322
|
+
'info': 'informational context',
|
|
323
|
+
'green': 'success/positive state',
|
|
324
|
+
'red': 'error/danger state',
|
|
325
|
+
'blue': 'informational emphasis',
|
|
326
|
+
'yellow': 'warning indication',
|
|
327
|
+
'orange': 'attention drawing',
|
|
328
|
+
};
|
|
329
|
+
for (const color of Array.from(colors).slice(0, 2)) {
|
|
330
|
+
const colorLower = color.toLowerCase();
|
|
331
|
+
for (const [key, reason] of Object.entries(semanticColors)) {
|
|
332
|
+
if (colorLower.includes(key)) {
|
|
333
|
+
styleChoices.push({
|
|
334
|
+
property: 'color',
|
|
335
|
+
value: color,
|
|
336
|
+
reason
|
|
337
|
+
});
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { componentsUsed, layoutChoices, styleChoices };
|
|
344
|
+
}
|
|
345
|
+
// Main streaming handler
|
|
346
|
+
export async function generateStoryFromPromptStream(req, res) {
|
|
347
|
+
// Set up SSE headers
|
|
348
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
349
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
350
|
+
res.setHeader('Connection', 'keep-alive');
|
|
351
|
+
res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
|
352
|
+
res.flushHeaders();
|
|
353
|
+
const stream = new StreamWriter(res);
|
|
354
|
+
const totalSteps = 8;
|
|
355
|
+
let currentStep = 0;
|
|
356
|
+
const { prompt, fileName, conversation, isUpdate, originalTitle, storyId: providedStoryId, framework, autoDetectFramework, images, visionMode, designSystem, considerations } = req.body;
|
|
357
|
+
if (!prompt) {
|
|
358
|
+
stream.sendError({
|
|
359
|
+
code: 'MISSING_PROMPT',
|
|
360
|
+
message: 'No prompt provided',
|
|
361
|
+
recoverable: false,
|
|
362
|
+
suggestion: 'Please provide a description of what you want to generate'
|
|
363
|
+
});
|
|
364
|
+
res.end();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
// Step 1: Load configuration
|
|
369
|
+
currentStep++;
|
|
370
|
+
stream.sendProgress(currentStep, totalSteps, 'config_loaded', 'Loading configuration...');
|
|
371
|
+
const config = loadUserConfig();
|
|
372
|
+
const validation = validateConfig(config);
|
|
373
|
+
if (!validation.isValid) {
|
|
374
|
+
stream.sendError({
|
|
375
|
+
code: 'CONFIG_ERROR',
|
|
376
|
+
message: 'Configuration validation failed',
|
|
377
|
+
details: validation.errors.join('; '),
|
|
378
|
+
recoverable: false,
|
|
379
|
+
suggestion: 'Check your story-ui.config.js file'
|
|
380
|
+
});
|
|
381
|
+
res.end();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Process images if provided
|
|
385
|
+
let processedImages = [];
|
|
386
|
+
if (images && Array.isArray(images) && images.length > 0) {
|
|
387
|
+
try {
|
|
388
|
+
processedImages = await processImageInputs(images);
|
|
389
|
+
}
|
|
390
|
+
catch (imageError) {
|
|
391
|
+
stream.sendError({
|
|
392
|
+
code: 'IMAGE_PROCESSING_ERROR',
|
|
393
|
+
message: 'Failed to process images',
|
|
394
|
+
details: imageError instanceof Error ? imageError.message : String(imageError),
|
|
395
|
+
recoverable: true,
|
|
396
|
+
suggestion: 'Try again without images or use a different format'
|
|
397
|
+
});
|
|
398
|
+
res.end();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Step 2: Discover components
|
|
403
|
+
currentStep++;
|
|
404
|
+
stream.sendProgress(currentStep, totalSteps, 'components_discovered', 'Discovering available components...');
|
|
405
|
+
const discovery = new EnhancedComponentDiscovery(config);
|
|
406
|
+
const components = await discovery.discoverAll();
|
|
407
|
+
stream.sendProgress(currentStep, totalSteps, 'components_discovered', `Found ${components.length} components from ${config.importPath}`, { componentCount: components.length });
|
|
408
|
+
// Set up environment
|
|
409
|
+
const gitignoreManager = setupProductionGitignore(config);
|
|
410
|
+
const storyService = getInMemoryStoryService(config);
|
|
411
|
+
const isProduction = gitignoreManager.isProductionMode();
|
|
412
|
+
const storyTracker = new StoryTracker(config);
|
|
413
|
+
const historyManager = new StoryHistoryManager(process.cwd());
|
|
414
|
+
const redirectDir = isProduction ? process.cwd() : path.dirname(config.generatedStoriesPath);
|
|
415
|
+
const redirectService = new UrlRedirectService(redirectDir);
|
|
416
|
+
// Check for previous code if update
|
|
417
|
+
const isActualUpdate = isUpdate || (fileName && conversation && conversation.length > 2);
|
|
418
|
+
let previousCode;
|
|
419
|
+
let parentVersionId;
|
|
420
|
+
let oldTitle;
|
|
421
|
+
let oldStoryUrl;
|
|
422
|
+
if (isActualUpdate && fileName) {
|
|
423
|
+
const currentVersion = historyManager.getCurrentVersion(fileName);
|
|
424
|
+
if (currentVersion) {
|
|
425
|
+
previousCode = currentVersion.code;
|
|
426
|
+
parentVersionId = currentVersion.id;
|
|
427
|
+
const titleMatch = previousCode.match(/title:\s*["']([^"']+)['"]/);
|
|
428
|
+
if (titleMatch) {
|
|
429
|
+
oldTitle = titleMatch[1];
|
|
430
|
+
const cleanOldTitle = oldTitle.replace(config.storyPrefix || 'Generated/', '');
|
|
431
|
+
oldStoryUrl = `/story/${cleanOldTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-')}--primary`;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// INTENT PREVIEW: Analyze and show what we're going to do
|
|
436
|
+
const intent = await analyzeIntent(prompt, config, conversation, previousCode, {
|
|
437
|
+
framework: framework,
|
|
438
|
+
autoDetectFramework: autoDetectFramework === true,
|
|
439
|
+
visionMode: visionMode,
|
|
440
|
+
designSystem,
|
|
441
|
+
hasImages: processedImages.length > 0
|
|
442
|
+
});
|
|
443
|
+
stream.sendIntent(intent);
|
|
444
|
+
// Step 3: Build prompt
|
|
445
|
+
currentStep++;
|
|
446
|
+
stream.sendProgress(currentStep, totalSteps, 'prompt_built', 'Building generation prompt...', {
|
|
447
|
+
framework: intent.framework,
|
|
448
|
+
hasContext: intent.promptAnalysis.hasConversationContext
|
|
449
|
+
});
|
|
450
|
+
const frameworkOptions = {
|
|
451
|
+
framework: framework,
|
|
452
|
+
autoDetectFramework: autoDetectFramework === true,
|
|
453
|
+
visionMode: visionMode,
|
|
454
|
+
designSystem: designSystem,
|
|
455
|
+
considerations: considerations,
|
|
456
|
+
};
|
|
457
|
+
const initialPrompt = await buildClaudePromptWithContext(prompt, config, conversation, previousCode, components, frameworkOptions);
|
|
458
|
+
const messages = [
|
|
459
|
+
{ role: 'user', content: initialPrompt }
|
|
460
|
+
];
|
|
461
|
+
// Step 4: Call LLM
|
|
462
|
+
currentStep++;
|
|
463
|
+
stream.sendProgress(currentStep, totalSteps, 'llm_thinking', 'AI is generating your story...');
|
|
464
|
+
// Validation and retry loop
|
|
465
|
+
let aiText = '';
|
|
466
|
+
let validationErrors = [];
|
|
467
|
+
const maxRetries = 3;
|
|
468
|
+
let attempts = 0;
|
|
469
|
+
while (attempts < maxRetries) {
|
|
470
|
+
attempts++;
|
|
471
|
+
stream.trackLLMCall();
|
|
472
|
+
if (attempts > 1) {
|
|
473
|
+
stream.sendRetry(attempts, maxRetries, 'Fixing validation errors', validationErrors.map(e => e.message));
|
|
474
|
+
}
|
|
475
|
+
const claudeResponse = await callLLM(messages, processedImages.length > 0 ? processedImages : undefined);
|
|
476
|
+
const extractedCode = extractCodeBlock(claudeResponse);
|
|
477
|
+
if (!extractedCode) {
|
|
478
|
+
aiText = claudeResponse;
|
|
479
|
+
if (attempts < maxRetries) {
|
|
480
|
+
messages.push({ role: 'assistant', content: aiText });
|
|
481
|
+
messages.push({ role: 'user', content: 'You did not provide a code block. Please provide the complete story in a single `tsx` code block.' });
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
aiText = extractedCode;
|
|
490
|
+
}
|
|
491
|
+
// Step 5: Validate
|
|
492
|
+
currentStep = 5;
|
|
493
|
+
stream.sendProgress(currentStep, totalSteps, 'validating', 'Validating generated code...');
|
|
494
|
+
validationErrors = validateStory(aiText);
|
|
495
|
+
if (validationErrors.length === 0) {
|
|
496
|
+
stream.sendValidation({
|
|
497
|
+
isValid: true,
|
|
498
|
+
errors: [],
|
|
499
|
+
warnings: [],
|
|
500
|
+
autoFixApplied: false
|
|
501
|
+
});
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
stream.sendValidation({
|
|
505
|
+
isValid: false,
|
|
506
|
+
errors: validationErrors.map(e => e.message),
|
|
507
|
+
warnings: [],
|
|
508
|
+
autoFixApplied: false
|
|
509
|
+
});
|
|
510
|
+
if (attempts < maxRetries) {
|
|
511
|
+
const errorFeedback = validationErrors
|
|
512
|
+
.map(err => `- Line ${err.line}: ${err.message}`)
|
|
513
|
+
.join('\n');
|
|
514
|
+
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.`;
|
|
515
|
+
messages.push({ role: 'assistant', content: claudeResponse });
|
|
516
|
+
messages.push({ role: 'user', content: retryPrompt });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Step 6: Code extraction and validation
|
|
520
|
+
currentStep = 6;
|
|
521
|
+
stream.sendProgress(currentStep, totalSteps, 'code_extracted', 'Processing generated code...');
|
|
522
|
+
// Pre-validate imports
|
|
523
|
+
const preValidation = await preValidateImports(aiText, config, discovery);
|
|
524
|
+
if (!preValidation.isValid) {
|
|
525
|
+
stream.sendError({
|
|
526
|
+
code: 'INVALID_IMPORTS',
|
|
527
|
+
message: 'Generated code contains invalid imports',
|
|
528
|
+
details: preValidation.errors.join('; '),
|
|
529
|
+
recoverable: true,
|
|
530
|
+
suggestion: 'Try using basic components like Box, Stack, Button'
|
|
531
|
+
});
|
|
532
|
+
res.end();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// Full validation
|
|
536
|
+
const validationResult = extractAndValidateCodeBlock(aiText, config);
|
|
537
|
+
let fileContents;
|
|
538
|
+
let hasValidationWarnings = false;
|
|
539
|
+
if (!validationResult.isValid && !validationResult.fixedCode) {
|
|
540
|
+
fileContents = createFallbackStory(prompt, config);
|
|
541
|
+
hasValidationWarnings = true;
|
|
542
|
+
stream.sendValidation({
|
|
543
|
+
isValid: false,
|
|
544
|
+
errors: validationResult.errors || [],
|
|
545
|
+
warnings: ['Using fallback template due to validation failures'],
|
|
546
|
+
autoFixApplied: false
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
if (validationResult.fixedCode) {
|
|
551
|
+
fileContents = validationResult.fixedCode;
|
|
552
|
+
hasValidationWarnings = true;
|
|
553
|
+
stream.sendValidation({
|
|
554
|
+
isValid: true,
|
|
555
|
+
errors: [],
|
|
556
|
+
warnings: validationResult.warnings || [],
|
|
557
|
+
autoFixApplied: true,
|
|
558
|
+
fixDetails: ['Applied automatic corrections to fix validation errors']
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
const codeMatch = aiText.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?\s*([\s\S]*?)\s*```/i);
|
|
563
|
+
fileContents = codeMatch ? codeMatch[1].trim() : aiText.trim();
|
|
564
|
+
if (validationResult.warnings?.length) {
|
|
565
|
+
hasValidationWarnings = true;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Step 7: Post-processing
|
|
570
|
+
currentStep++;
|
|
571
|
+
stream.sendProgress(currentStep, totalSteps, 'post_processing', 'Applying finishing touches...');
|
|
572
|
+
// Ensure React import
|
|
573
|
+
if (!fileContents.includes("import React from 'react';")) {
|
|
574
|
+
fileContents = "import React from 'react';\n" + fileContents;
|
|
575
|
+
}
|
|
576
|
+
let fixedFileContents = postProcessStory(fileContents, config.importPath);
|
|
577
|
+
// Generate title
|
|
578
|
+
let aiTitle;
|
|
579
|
+
if (isActualUpdate && originalTitle) {
|
|
580
|
+
aiTitle = originalTitle;
|
|
581
|
+
}
|
|
582
|
+
else if (isActualUpdate && conversation) {
|
|
583
|
+
const originalPrompt = conversation.find((msg) => msg.role === 'user')?.content || prompt;
|
|
584
|
+
aiTitle = await getLLMTitle(originalPrompt);
|
|
585
|
+
stream.trackLLMCall();
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
aiTitle = await getLLMTitle(prompt);
|
|
589
|
+
stream.trackLLMCall();
|
|
590
|
+
}
|
|
591
|
+
if (!aiTitle || aiTitle.length < 2) {
|
|
592
|
+
aiTitle = cleanPromptForTitle(prompt);
|
|
593
|
+
}
|
|
594
|
+
const prettyPrompt = escapeTitleForTS(aiTitle);
|
|
595
|
+
// Fix title with storyPrefix
|
|
596
|
+
fixedFileContents = fixedFileContents.replace(/(const\s+meta\s*=\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
|
|
597
|
+
const titleToUse = prettyPrompt.startsWith(config.storyPrefix)
|
|
598
|
+
? prettyPrompt
|
|
599
|
+
: config.storyPrefix + prettyPrompt;
|
|
600
|
+
return p1 + titleToUse + p3;
|
|
601
|
+
});
|
|
602
|
+
if (!fixedFileContents.includes(config.storyPrefix)) {
|
|
603
|
+
fixedFileContents = fixedFileContents.replace(/(export\s+default\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
|
|
604
|
+
const titleToUse = prettyPrompt.startsWith(config.storyPrefix)
|
|
605
|
+
? prettyPrompt
|
|
606
|
+
: config.storyPrefix + prettyPrompt;
|
|
607
|
+
return p1 + titleToUse + p3;
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
// Generate IDs
|
|
611
|
+
let hash;
|
|
612
|
+
let finalFileName;
|
|
613
|
+
let storyId;
|
|
614
|
+
if (isActualUpdate && (fileName || providedStoryId)) {
|
|
615
|
+
if (providedStoryId) {
|
|
616
|
+
storyId = providedStoryId;
|
|
617
|
+
const hashMatch = providedStoryId.match(/^story-([a-f0-9]{8})$/);
|
|
618
|
+
hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
const hashMatch = fileName?.match(/-([a-f0-9]{8})(?:\.stories\.tsx)?$/);
|
|
622
|
+
hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
|
|
623
|
+
storyId = `story-${hash}`;
|
|
624
|
+
}
|
|
625
|
+
// Ensure finalFileName is always set
|
|
626
|
+
finalFileName = fileName || fileNameFromTitle(aiTitle, hash);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
const timestamp = Date.now();
|
|
630
|
+
hash = crypto.createHash('sha1').update(prompt + timestamp).digest('hex').slice(0, 8);
|
|
631
|
+
finalFileName = fileName || fileNameFromTitle(aiTitle, hash);
|
|
632
|
+
storyId = `story-${hash}`;
|
|
633
|
+
}
|
|
634
|
+
// Step 8: Save story
|
|
635
|
+
currentStep++;
|
|
636
|
+
stream.sendProgress(currentStep, totalSteps, 'saving', 'Saving your story...');
|
|
637
|
+
// Analyze what was generated
|
|
638
|
+
const analysis = analyzeGeneratedCode(fixedFileContents, prompt, config);
|
|
639
|
+
if (isProduction) {
|
|
640
|
+
const generatedStory = {
|
|
641
|
+
id: storyId,
|
|
642
|
+
title: aiTitle,
|
|
643
|
+
description: isActualUpdate ? `Updated: ${prompt}` : prompt,
|
|
644
|
+
content: fixedFileContents,
|
|
645
|
+
createdAt: isActualUpdate ? new Date() : new Date(),
|
|
646
|
+
lastAccessed: new Date(),
|
|
647
|
+
prompt: isActualUpdate && conversation
|
|
648
|
+
? conversation.map((msg) => `${msg.role}: ${msg.content}`).join('\n\n')
|
|
649
|
+
: prompt,
|
|
650
|
+
components: extractComponentsFromContent(fixedFileContents)
|
|
651
|
+
};
|
|
652
|
+
storyService.storeStory(generatedStory);
|
|
653
|
+
const mapping = {
|
|
654
|
+
title: aiTitle,
|
|
655
|
+
fileName: finalFileName,
|
|
656
|
+
storyId,
|
|
657
|
+
hash,
|
|
658
|
+
createdAt: new Date().toISOString(),
|
|
659
|
+
updatedAt: new Date().toISOString(),
|
|
660
|
+
prompt
|
|
661
|
+
};
|
|
662
|
+
storyTracker.registerStory(mapping);
|
|
663
|
+
historyManager.addVersion(finalFileName, prompt, fixedFileContents, parentVersionId);
|
|
664
|
+
// Track URL redirect if needed
|
|
665
|
+
if (isActualUpdate && oldTitle && oldStoryUrl) {
|
|
666
|
+
const newTitleMatch = fixedFileContents.match(/title:\s*["']([^"']+)['"]/);
|
|
667
|
+
if (newTitleMatch) {
|
|
668
|
+
const newTitle = newTitleMatch[1];
|
|
669
|
+
const cleanNewTitle = newTitle.replace(config.storyPrefix, '');
|
|
670
|
+
const cleanOldTitle = oldTitle.replace(config.storyPrefix, '');
|
|
671
|
+
const newStoryUrl = `/story/${cleanNewTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-')}--primary`;
|
|
672
|
+
if (oldStoryUrl !== newStoryUrl) {
|
|
673
|
+
redirectService.addRedirect(oldStoryUrl, newStoryUrl, cleanOldTitle, cleanNewTitle, storyId);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
const outPath = generateStory({
|
|
680
|
+
fileContents: fixedFileContents,
|
|
681
|
+
fileName: finalFileName,
|
|
682
|
+
config: config
|
|
683
|
+
});
|
|
684
|
+
const mapping = {
|
|
685
|
+
title: aiTitle,
|
|
686
|
+
fileName: finalFileName,
|
|
687
|
+
storyId,
|
|
688
|
+
hash,
|
|
689
|
+
createdAt: new Date().toISOString(),
|
|
690
|
+
updatedAt: new Date().toISOString(),
|
|
691
|
+
prompt
|
|
692
|
+
};
|
|
693
|
+
storyTracker.registerStory(mapping);
|
|
694
|
+
historyManager.addVersion(finalFileName, prompt, fixedFileContents, parentVersionId);
|
|
695
|
+
if (isActualUpdate && oldTitle && oldStoryUrl) {
|
|
696
|
+
const newTitleMatch = fixedFileContents.match(/title:\s*["']([^"']+)['"]/);
|
|
697
|
+
if (newTitleMatch) {
|
|
698
|
+
const newTitle = newTitleMatch[1];
|
|
699
|
+
const cleanNewTitle = newTitle.replace(config.storyPrefix, '');
|
|
700
|
+
const cleanOldTitle = oldTitle.replace(config.storyPrefix, '');
|
|
701
|
+
const newStoryUrl = `/story/${cleanNewTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-')}--primary`;
|
|
702
|
+
if (oldStoryUrl !== newStoryUrl) {
|
|
703
|
+
redirectService.addRedirect(oldStoryUrl, newStoryUrl, cleanOldTitle, cleanNewTitle, storyId);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// COMPLETION: Send detailed feedback
|
|
709
|
+
stream.sendCompletion({
|
|
710
|
+
success: true,
|
|
711
|
+
title: aiTitle,
|
|
712
|
+
fileName: finalFileName,
|
|
713
|
+
storyId,
|
|
714
|
+
summary: {
|
|
715
|
+
action: isActualUpdate ? 'updated' : 'created',
|
|
716
|
+
description: isActualUpdate
|
|
717
|
+
? `Updated story based on your request: "${prompt.slice(0, 100)}${prompt.length > 100 ? '...' : ''}"`
|
|
718
|
+
: `Created new story for: "${prompt.slice(0, 100)}${prompt.length > 100 ? '...' : ''}"`
|
|
719
|
+
},
|
|
720
|
+
componentsUsed: analysis.componentsUsed,
|
|
721
|
+
layoutChoices: analysis.layoutChoices,
|
|
722
|
+
styleChoices: analysis.styleChoices,
|
|
723
|
+
suggestions: hasValidationWarnings
|
|
724
|
+
? ['Some automatic fixes were applied. Review the generated code.']
|
|
725
|
+
: undefined,
|
|
726
|
+
validation: {
|
|
727
|
+
isValid: !hasValidationWarnings,
|
|
728
|
+
errors: validationResult?.errors || [],
|
|
729
|
+
warnings: validationResult?.warnings || [],
|
|
730
|
+
autoFixApplied: !!validationResult?.fixedCode
|
|
731
|
+
},
|
|
732
|
+
code: fixedFileContents
|
|
733
|
+
});
|
|
734
|
+
res.end();
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
stream.sendError({
|
|
738
|
+
code: 'GENERATION_ERROR',
|
|
739
|
+
message: err.message || 'Story generation failed',
|
|
740
|
+
recoverable: false,
|
|
741
|
+
suggestion: 'Please try again with a different prompt'
|
|
742
|
+
});
|
|
743
|
+
res.end();
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// Helper functions (copied from generateStory.ts for consistency)
|
|
747
|
+
async function buildClaudePromptWithContext(userPrompt, config, conversation, previousCode, components, options) {
|
|
748
|
+
const discovery = new EnhancedComponentDiscovery(config);
|
|
749
|
+
const discoveredComponents = components || await discovery.discoverAll();
|
|
750
|
+
let useFrameworkAware = false;
|
|
751
|
+
let frameworkOptions;
|
|
752
|
+
if (options?.framework) {
|
|
753
|
+
useFrameworkAware = true;
|
|
754
|
+
frameworkOptions = { framework: options.framework };
|
|
755
|
+
}
|
|
756
|
+
else if (options?.autoDetectFramework) {
|
|
757
|
+
try {
|
|
758
|
+
const detectedFramework = await detectProjectFramework(process.cwd());
|
|
759
|
+
useFrameworkAware = true;
|
|
760
|
+
frameworkOptions = { framework: detectedFramework };
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
// Default to React
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
let prompt;
|
|
767
|
+
if (useFrameworkAware && frameworkOptions) {
|
|
768
|
+
prompt = await buildFrameworkAwarePrompt(userPrompt, config, discoveredComponents, frameworkOptions);
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
prompt = await buildFlexiblePrompt(userPrompt, config, discoveredComponents);
|
|
772
|
+
}
|
|
773
|
+
if (options?.visionMode) {
|
|
774
|
+
const visionPrompts = buildVisionAwarePrompt({
|
|
775
|
+
promptType: options.visionMode,
|
|
776
|
+
userDescription: userPrompt,
|
|
777
|
+
availableComponents: discoveredComponents.map((c) => c.name),
|
|
778
|
+
framework: frameworkOptions?.framework || 'react',
|
|
779
|
+
designSystem: options.designSystem,
|
|
780
|
+
});
|
|
781
|
+
prompt = `${visionPrompts.systemPrompt}\n\n---\n\n${prompt}\n\n---\n\n${visionPrompts.userPrompt}`;
|
|
782
|
+
}
|
|
783
|
+
// Inject passed considerations (from frontend) for environment parity
|
|
784
|
+
// This takes precedence over file system loading for production deployments
|
|
785
|
+
if (options?.considerations) {
|
|
786
|
+
const considerationsEnhancement = `\n\n📋 DESIGN SYSTEM CONSIDERATIONS:\n${options.considerations}`;
|
|
787
|
+
prompt = prompt.replace('User request:', `${considerationsEnhancement}\n\nUser request:`);
|
|
788
|
+
}
|
|
789
|
+
const documentation = getDocumentation(config.importPath);
|
|
790
|
+
if (documentation) {
|
|
791
|
+
const bundledEnhancement = `\n\n📚 BUNDLED DOCUMENTATION:\n${Object.entries(documentation.components || {}).map(([name, info]) => {
|
|
792
|
+
if (discoveredComponents.some((c) => c.name === name)) {
|
|
793
|
+
return `- ${name}: ${info.description || 'Component available'}`;
|
|
794
|
+
}
|
|
795
|
+
return null;
|
|
796
|
+
}).filter(Boolean).join('\n')}`;
|
|
797
|
+
prompt = prompt.replace('User request:', `${bundledEnhancement}\n\nUser request:`);
|
|
798
|
+
}
|
|
799
|
+
if (!conversation || conversation.length <= 1) {
|
|
800
|
+
return prompt;
|
|
801
|
+
}
|
|
802
|
+
const conversationContext = conversation
|
|
803
|
+
.slice(0, -1)
|
|
804
|
+
.map((msg) => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`)
|
|
805
|
+
.join('\n\n');
|
|
806
|
+
let contextSection = `CONVERSATION CONTEXT (for modifications/updates):\n${conversationContext}`;
|
|
807
|
+
if (previousCode) {
|
|
808
|
+
contextSection += `\n\nPREVIOUS GENERATED CODE (this is what you're modifying):\n\`\`\`tsx\n${previousCode}\n\`\`\`\n\nCRITICAL INSTRUCTIONS FOR MODIFICATIONS:\n1. DO NOT regenerate the entire story from scratch\n2. PRESERVE all existing styling, components, and structure\n3. ONLY change what the user specifically requests`;
|
|
809
|
+
}
|
|
810
|
+
const contextualPrompt = prompt.replace('User request:', `${contextSection}\n\nIMPORTANT: The user is asking to modify/update the story based on the above conversation.\n\nCurrent modification request:`);
|
|
811
|
+
return contextualPrompt;
|
|
812
|
+
}
|
|
813
|
+
function extractCodeBlock(text) {
|
|
814
|
+
const codeBlock = text.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?([\s\S]*?)```/i);
|
|
815
|
+
return codeBlock ? codeBlock[1].trim() : null;
|
|
816
|
+
}
|
|
817
|
+
async function callLLM(messages, images) {
|
|
818
|
+
if (!isProviderConfigured()) {
|
|
819
|
+
throw new Error('No LLM provider configured');
|
|
820
|
+
}
|
|
821
|
+
if (images && images.length > 0) {
|
|
822
|
+
const providerInfo = getProviderInfo();
|
|
823
|
+
if (!providerInfo.supportsVision) {
|
|
824
|
+
throw new Error(`${providerInfo.currentProvider} does not support vision`);
|
|
825
|
+
}
|
|
826
|
+
const messagesWithImages = messages.map((msg, index) => {
|
|
827
|
+
if (msg.role === 'user' && index === 0) {
|
|
828
|
+
return {
|
|
829
|
+
role: msg.role,
|
|
830
|
+
content: buildMessageWithImages(msg.content, images)
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
return msg;
|
|
834
|
+
});
|
|
835
|
+
return await chatCompletionWithImages(messagesWithImages, { maxTokens: 8192 });
|
|
836
|
+
}
|
|
837
|
+
return await chatCompletion(messages, { maxTokens: 8192 });
|
|
838
|
+
}
|
|
839
|
+
function cleanPromptForTitle(prompt) {
|
|
840
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
841
|
+
return 'Untitled Story';
|
|
842
|
+
}
|
|
843
|
+
const leadingPhrases = [
|
|
844
|
+
/^generate (a|an|the)? /i,
|
|
845
|
+
/^build (a|an|the)? /i,
|
|
846
|
+
/^create (a|an|the)? /i,
|
|
847
|
+
/^make (a|an|the)? /i,
|
|
848
|
+
/^design (a|an|the)? /i,
|
|
849
|
+
/^show (me )?(a|an|the)? /i,
|
|
850
|
+
];
|
|
851
|
+
let cleaned = prompt.trim();
|
|
852
|
+
for (const regex of leadingPhrases) {
|
|
853
|
+
cleaned = cleaned.replace(regex, '');
|
|
854
|
+
}
|
|
855
|
+
return cleaned
|
|
856
|
+
.replace(/[^\w\s'"?!-]/g, ' ')
|
|
857
|
+
.replace(/\s+/g, ' ')
|
|
858
|
+
.trim()
|
|
859
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
860
|
+
}
|
|
861
|
+
async function getLLMTitle(userPrompt) {
|
|
862
|
+
try {
|
|
863
|
+
return await llmGenerateTitle(userPrompt);
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
return '';
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function escapeTitleForTS(title) {
|
|
870
|
+
return title
|
|
871
|
+
.replace(/\\/g, '\\\\')
|
|
872
|
+
.replace(/"/g, '\\"')
|
|
873
|
+
.replace(/'/g, "\\'")
|
|
874
|
+
.replace(/`/g, '\\`')
|
|
875
|
+
.replace(/\n/g, '\\n')
|
|
876
|
+
.replace(/\r/g, '\\r')
|
|
877
|
+
.replace(/\t/g, '\\t');
|
|
878
|
+
}
|
|
879
|
+
function fileNameFromTitle(title, hash) {
|
|
880
|
+
if (!title || typeof title !== 'string') {
|
|
881
|
+
title = 'untitled';
|
|
882
|
+
}
|
|
883
|
+
if (!hash || typeof hash !== 'string') {
|
|
884
|
+
hash = 'default';
|
|
885
|
+
}
|
|
886
|
+
let base = title
|
|
887
|
+
.toLowerCase()
|
|
888
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
889
|
+
.replace(/^-+|-+$/g, '')
|
|
890
|
+
.replace(/"|'/g, '')
|
|
891
|
+
.slice(0, 60);
|
|
892
|
+
return `${base}-${hash}.stories.tsx`;
|
|
893
|
+
}
|
|
894
|
+
function extractImportsFromCode(code, importPath) {
|
|
895
|
+
const imports = [];
|
|
896
|
+
const importRegex = new RegExp(`import\\s*{([^}]+)}\\s*from\\s*['"]${importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
|
|
897
|
+
let match;
|
|
898
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
899
|
+
const importList = match[1];
|
|
900
|
+
const components = importList.split(',').map(comp => comp.trim());
|
|
901
|
+
imports.push(...components);
|
|
902
|
+
}
|
|
903
|
+
return imports;
|
|
904
|
+
}
|
|
905
|
+
async function preValidateImports(code, config, discovery) {
|
|
906
|
+
const errors = [];
|
|
907
|
+
const componentImports = extractImportsFromCode(code, config.importPath);
|
|
908
|
+
const validation = await discovery.validateComponentNames(componentImports);
|
|
909
|
+
const allowedComponents = new Set(discovery.getAvailableComponentNames());
|
|
910
|
+
for (const importName of componentImports) {
|
|
911
|
+
if (isBlacklistedComponent(importName, allowedComponents, config.importPath)) {
|
|
912
|
+
const errorMsg = getBlacklistErrorMessage(importName, config.importPath);
|
|
913
|
+
errors.push(`Blacklisted component detected: ${errorMsg}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
for (const invalidComponent of validation.invalid) {
|
|
917
|
+
const suggestion = validation.suggestions.get(invalidComponent);
|
|
918
|
+
if (suggestion) {
|
|
919
|
+
errors.push(`Invalid component: "${invalidComponent}" does not exist. Did you mean "${suggestion}"?`);
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
errors.push(`Invalid component: "${invalidComponent}" does not exist.`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (config.iconImports?.package) {
|
|
926
|
+
const allowedIcons = new Set(config.iconImports?.commonIcons || []);
|
|
927
|
+
const iconImports = extractImportsFromCode(code, config.iconImports.package);
|
|
928
|
+
for (const iconName of iconImports) {
|
|
929
|
+
if (isBlacklistedIcon(iconName, allowedIcons)) {
|
|
930
|
+
const correction = ICON_CORRECTIONS[iconName];
|
|
931
|
+
if (correction) {
|
|
932
|
+
errors.push(`Invalid icon: "${iconName}" does not exist. Did you mean "${correction}"?`);
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
errors.push(`Invalid icon: "${iconName}" is not available.`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return { isValid: errors.length === 0, errors };
|
|
941
|
+
}
|
|
942
|
+
function extractComponentsFromContent(content) {
|
|
943
|
+
const componentMatches = content.match(/<[A-Z][A-Za-z0-9]*\s/g);
|
|
944
|
+
if (!componentMatches)
|
|
945
|
+
return [];
|
|
946
|
+
return Array.from(new Set(componentMatches.map(match => match.replace(/[<\s]/g, ''))));
|
|
947
|
+
}
|