@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.
Files changed (188) hide show
  1. package/.env.sample +82 -11
  2. package/README.md +89 -0
  3. package/dist/cli/deploy.d.ts +17 -0
  4. package/dist/cli/deploy.d.ts.map +1 -0
  5. package/dist/cli/deploy.js +696 -0
  6. package/dist/cli/index.d.ts +3 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +26 -2
  9. package/dist/cli/setup.d.ts +11 -0
  10. package/dist/cli/setup.d.ts.map +1 -0
  11. package/dist/cli/setup.js +437 -110
  12. package/dist/mcp-server/index.d.ts +2 -0
  13. package/dist/mcp-server/index.d.ts.map +1 -0
  14. package/dist/mcp-server/index.js +120 -2
  15. package/dist/mcp-server/mcp-stdio-server.d.ts +3 -0
  16. package/dist/mcp-server/mcp-stdio-server.d.ts.map +1 -0
  17. package/dist/mcp-server/mcp-stdio-server.js +8 -1
  18. package/dist/mcp-server/routes/claude.d.ts +3 -0
  19. package/dist/mcp-server/routes/claude.d.ts.map +1 -0
  20. package/dist/mcp-server/routes/claude.js +60 -23
  21. package/dist/mcp-server/routes/components.d.ts +4 -0
  22. package/dist/mcp-server/routes/components.d.ts.map +1 -0
  23. package/dist/mcp-server/routes/frameworks.d.ts +38 -0
  24. package/dist/mcp-server/routes/frameworks.d.ts.map +1 -0
  25. package/dist/mcp-server/routes/frameworks.js +183 -0
  26. package/dist/mcp-server/routes/generateStory.d.ts +3 -0
  27. package/dist/mcp-server/routes/generateStory.d.ts.map +1 -0
  28. package/dist/mcp-server/routes/generateStory.js +160 -76
  29. package/dist/mcp-server/routes/generateStoryStream.d.ts +12 -0
  30. package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -0
  31. package/dist/mcp-server/routes/generateStoryStream.js +947 -0
  32. package/dist/mcp-server/routes/hybridStories.d.ts +18 -0
  33. package/dist/mcp-server/routes/hybridStories.d.ts.map +1 -0
  34. package/dist/mcp-server/routes/mcpRemote.d.ts +14 -0
  35. package/dist/mcp-server/routes/mcpRemote.d.ts.map +1 -0
  36. package/dist/mcp-server/routes/mcpRemote.js +489 -0
  37. package/dist/mcp-server/routes/memoryStories.d.ts +26 -0
  38. package/dist/mcp-server/routes/memoryStories.d.ts.map +1 -0
  39. package/dist/mcp-server/routes/providers.d.ts +89 -0
  40. package/dist/mcp-server/routes/providers.d.ts.map +1 -0
  41. package/dist/mcp-server/routes/providers.js +369 -0
  42. package/dist/mcp-server/routes/storySync.d.ts +26 -0
  43. package/dist/mcp-server/routes/storySync.d.ts.map +1 -0
  44. package/dist/mcp-server/routes/streamTypes.d.ts +110 -0
  45. package/dist/mcp-server/routes/streamTypes.d.ts.map +1 -0
  46. package/dist/mcp-server/routes/streamTypes.js +18 -0
  47. package/dist/mcp-server/sessionManager.d.ts +50 -0
  48. package/dist/mcp-server/sessionManager.d.ts.map +1 -0
  49. package/dist/story-generator/componentBlacklist.d.ts +21 -0
  50. package/dist/story-generator/componentBlacklist.d.ts.map +1 -0
  51. package/dist/story-generator/componentDiscovery.d.ts +28 -0
  52. package/dist/story-generator/componentDiscovery.d.ts.map +1 -0
  53. package/dist/story-generator/componentRegistryGenerator.d.ts +49 -0
  54. package/dist/story-generator/componentRegistryGenerator.d.ts.map +1 -0
  55. package/dist/story-generator/componentRegistryGenerator.js +205 -0
  56. package/dist/story-generator/configLoader.d.ts +33 -0
  57. package/dist/story-generator/configLoader.d.ts.map +1 -0
  58. package/dist/story-generator/considerationsLoader.d.ts +32 -0
  59. package/dist/story-generator/considerationsLoader.d.ts.map +1 -0
  60. package/dist/story-generator/documentation-sources.d.ts +28 -0
  61. package/dist/story-generator/documentation-sources.d.ts.map +1 -0
  62. package/dist/story-generator/documentationLoader.d.ts +64 -0
  63. package/dist/story-generator/documentationLoader.d.ts.map +1 -0
  64. package/dist/story-generator/dynamicPackageDiscovery.d.ts +97 -0
  65. package/dist/story-generator/dynamicPackageDiscovery.d.ts.map +1 -0
  66. package/dist/story-generator/enhancedComponentDiscovery.d.ts +125 -0
  67. package/dist/story-generator/enhancedComponentDiscovery.d.ts.map +1 -0
  68. package/dist/story-generator/enhancedComponentDiscovery.js +111 -11
  69. package/dist/story-generator/framework-adapters/angular-adapter.d.ts +40 -0
  70. package/dist/story-generator/framework-adapters/angular-adapter.d.ts.map +1 -0
  71. package/dist/story-generator/framework-adapters/angular-adapter.js +427 -0
  72. package/dist/story-generator/framework-adapters/base-adapter.d.ts +75 -0
  73. package/dist/story-generator/framework-adapters/base-adapter.d.ts.map +1 -0
  74. package/dist/story-generator/framework-adapters/base-adapter.js +147 -0
  75. package/dist/story-generator/framework-adapters/framework-detector.d.ts +55 -0
  76. package/dist/story-generator/framework-adapters/framework-detector.d.ts.map +1 -0
  77. package/dist/story-generator/framework-adapters/framework-detector.js +323 -0
  78. package/dist/story-generator/framework-adapters/index.d.ts +97 -0
  79. package/dist/story-generator/framework-adapters/index.d.ts.map +1 -0
  80. package/dist/story-generator/framework-adapters/index.js +198 -0
  81. package/dist/story-generator/framework-adapters/react-adapter.d.ts +40 -0
  82. package/dist/story-generator/framework-adapters/react-adapter.d.ts.map +1 -0
  83. package/dist/story-generator/framework-adapters/react-adapter.js +316 -0
  84. package/dist/story-generator/framework-adapters/svelte-adapter.d.ts +40 -0
  85. package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -0
  86. package/dist/story-generator/framework-adapters/svelte-adapter.js +372 -0
  87. package/dist/story-generator/framework-adapters/types.d.ts +182 -0
  88. package/dist/story-generator/framework-adapters/types.d.ts.map +1 -0
  89. package/dist/story-generator/framework-adapters/types.js +8 -0
  90. package/dist/story-generator/framework-adapters/vue-adapter.d.ts +36 -0
  91. package/dist/story-generator/framework-adapters/vue-adapter.d.ts.map +1 -0
  92. package/dist/story-generator/framework-adapters/vue-adapter.js +336 -0
  93. package/dist/story-generator/framework-adapters/web-components-adapter.d.ts +54 -0
  94. package/dist/story-generator/framework-adapters/web-components-adapter.d.ts.map +1 -0
  95. package/dist/story-generator/framework-adapters/web-components-adapter.js +387 -0
  96. package/dist/story-generator/generateStory.d.ts +7 -0
  97. package/dist/story-generator/generateStory.d.ts.map +1 -0
  98. package/dist/story-generator/gitignoreManager.d.ts +50 -0
  99. package/dist/story-generator/gitignoreManager.d.ts.map +1 -0
  100. package/dist/story-generator/imageProcessor.d.ts +80 -0
  101. package/dist/story-generator/imageProcessor.d.ts.map +1 -0
  102. package/dist/story-generator/imageProcessor.js +391 -0
  103. package/dist/story-generator/inMemoryStoryService.d.ts +89 -0
  104. package/dist/story-generator/inMemoryStoryService.d.ts.map +1 -0
  105. package/dist/story-generator/llm-providers/base-provider.d.ts +36 -0
  106. package/dist/story-generator/llm-providers/base-provider.d.ts.map +1 -0
  107. package/dist/story-generator/llm-providers/base-provider.js +135 -0
  108. package/dist/story-generator/llm-providers/claude-provider.d.ts +23 -0
  109. package/dist/story-generator/llm-providers/claude-provider.d.ts.map +1 -0
  110. package/dist/story-generator/llm-providers/claude-provider.js +414 -0
  111. package/dist/story-generator/llm-providers/gemini-provider.d.ts +24 -0
  112. package/dist/story-generator/llm-providers/gemini-provider.d.ts.map +1 -0
  113. package/dist/story-generator/llm-providers/gemini-provider.js +406 -0
  114. package/dist/story-generator/llm-providers/index.d.ts +63 -0
  115. package/dist/story-generator/llm-providers/index.d.ts.map +1 -0
  116. package/dist/story-generator/llm-providers/index.js +169 -0
  117. package/dist/story-generator/llm-providers/openai-provider.d.ts +24 -0
  118. package/dist/story-generator/llm-providers/openai-provider.d.ts.map +1 -0
  119. package/dist/story-generator/llm-providers/openai-provider.js +458 -0
  120. package/dist/story-generator/llm-providers/settings-manager.d.ts +75 -0
  121. package/dist/story-generator/llm-providers/settings-manager.d.ts.map +1 -0
  122. package/dist/story-generator/llm-providers/settings-manager.js +173 -0
  123. package/dist/story-generator/llm-providers/story-llm-service.d.ts +79 -0
  124. package/dist/story-generator/llm-providers/story-llm-service.d.ts.map +1 -0
  125. package/dist/story-generator/llm-providers/story-llm-service.js +240 -0
  126. package/dist/story-generator/llm-providers/types.d.ts +153 -0
  127. package/dist/story-generator/llm-providers/types.d.ts.map +1 -0
  128. package/dist/story-generator/llm-providers/types.js +8 -0
  129. package/dist/story-generator/logger.d.ts +14 -0
  130. package/dist/story-generator/logger.d.ts.map +1 -0
  131. package/dist/story-generator/logger.js +96 -29
  132. package/dist/story-generator/postProcessStory.d.ts +6 -0
  133. package/dist/story-generator/postProcessStory.d.ts.map +1 -0
  134. package/dist/story-generator/productionGitignoreManager.d.ts +91 -0
  135. package/dist/story-generator/productionGitignoreManager.d.ts.map +1 -0
  136. package/dist/story-generator/promptGenerator.d.ts +48 -0
  137. package/dist/story-generator/promptGenerator.d.ts.map +1 -0
  138. package/dist/story-generator/promptGenerator.js +186 -1
  139. package/dist/story-generator/storyHistory.d.ts +44 -0
  140. package/dist/story-generator/storyHistory.d.ts.map +1 -0
  141. package/dist/story-generator/storySync.d.ts +68 -0
  142. package/dist/story-generator/storySync.d.ts.map +1 -0
  143. package/dist/story-generator/storyTracker.d.ts +48 -0
  144. package/dist/story-generator/storyTracker.d.ts.map +1 -0
  145. package/dist/story-generator/storyValidator.d.ts +6 -0
  146. package/dist/story-generator/storyValidator.d.ts.map +1 -0
  147. package/dist/story-generator/universalDesignSystemAdapter.d.ts +68 -0
  148. package/dist/story-generator/universalDesignSystemAdapter.d.ts.map +1 -0
  149. package/dist/story-generator/universalDesignSystemAdapter.js +138 -1
  150. package/dist/story-generator/urlRedirectService.d.ts +21 -0
  151. package/dist/story-generator/urlRedirectService.d.ts.map +1 -0
  152. package/dist/story-generator/validateStory.d.ts +19 -0
  153. package/dist/story-generator/validateStory.d.ts.map +1 -0
  154. package/dist/story-generator/validateStory.js +6 -2
  155. package/dist/story-generator/visionPrompts.d.ts +88 -0
  156. package/dist/story-generator/visionPrompts.d.ts.map +1 -0
  157. package/dist/story-generator/visionPrompts.js +462 -0
  158. package/dist/story-ui.config.d.ts +78 -0
  159. package/dist/story-ui.config.d.ts.map +1 -0
  160. package/dist/templates/StoryUI/StoryUIPanel.d.ts +4 -0
  161. package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -0
  162. package/dist/templates/StoryUI/StoryUIPanel.js +1874 -0
  163. package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts +18 -0
  164. package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts.map +1 -0
  165. package/dist/templates/StoryUI/StoryUIPanel.stories.js +37 -0
  166. package/dist/templates/StoryUI/index.d.ts +3 -0
  167. package/dist/templates/StoryUI/index.d.ts.map +1 -0
  168. package/dist/templates/StoryUI/index.js +2 -0
  169. package/package.json +17 -3
  170. package/templates/StoryUI/StoryUIPanel.tsx +1960 -384
  171. package/templates/StoryUI/index.tsx +1 -1
  172. package/templates/StoryUI/manager.tsx +264 -0
  173. package/templates/production-app/.env.example +11 -0
  174. package/templates/production-app/index.html +66 -0
  175. package/templates/production-app/package.json +30 -0
  176. package/templates/production-app/public/favicon.svg +5 -0
  177. package/templates/production-app/src/App.tsx +1560 -0
  178. package/templates/production-app/src/LivePreviewRenderer.tsx +420 -0
  179. package/templates/production-app/src/componentRegistry.ts +315 -0
  180. package/templates/production-app/src/considerations.ts +16 -0
  181. package/templates/production-app/src/index.css +284 -0
  182. package/templates/production-app/src/main.tsx +25 -0
  183. package/templates/production-app/tsconfig.json +32 -0
  184. package/templates/production-app/tsconfig.node.json +11 -0
  185. package/templates/production-app/vite.config.ts +83 -0
  186. package/templates/react-import-rule.json +2 -2
  187. package/dist/index.js +0 -12
  188. 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
+ }