@tpitre/story-ui 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +1157 -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,1157 @@
1
+ /**
2
+ * Story UI Production App
3
+ *
4
+ * A Lovable/Bolt-style interface for generating UI components
5
+ * using AI and a user's component library.
6
+ */
7
+
8
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
9
+ import { LivePreviewRenderer } from './LivePreviewRenderer';
10
+ import { availableComponents } from './componentRegistry';
11
+ import { aiConsiderations, hasConsiderations } from './considerations';
12
+
13
+ // ============================================================================
14
+ // TYPES
15
+ // ============================================================================
16
+
17
+ interface Message {
18
+ id: string;
19
+ role: 'user' | 'assistant';
20
+ content: string;
21
+ timestamp: number;
22
+ generatedCode?: string;
23
+ images?: ImageAttachment[];
24
+ }
25
+
26
+ interface ImageAttachment {
27
+ id: string;
28
+ data: string;
29
+ type: string;
30
+ name: string;
31
+ }
32
+
33
+ interface Conversation {
34
+ id: string;
35
+ title: string;
36
+ messages: Message[];
37
+ createdAt: number;
38
+ updatedAt: number;
39
+ }
40
+
41
+ // ============================================================================
42
+ // CONSTANTS & CONFIG
43
+ // ============================================================================
44
+
45
+ const getServerUrl = (): string => {
46
+ if (import.meta.env.VITE_STORY_UI_SERVER) {
47
+ return import.meta.env.VITE_STORY_UI_SERVER;
48
+ }
49
+ return window.location.origin;
50
+ };
51
+
52
+ const SERVER_URL = getServerUrl();
53
+
54
+ const THEME = {
55
+ bg: '#09090b',
56
+ bgSurface: '#18181b',
57
+ bgElevated: '#27272a',
58
+ bgHover: '#3f3f46',
59
+ border: '#27272a',
60
+ borderSubtle: '#3f3f46',
61
+ text: '#fafafa',
62
+ textMuted: '#a1a1aa',
63
+ textSubtle: '#71717a',
64
+ accent: '#3b82f6',
65
+ accentHover: '#2563eb',
66
+ accentMuted: 'rgba(59, 130, 246, 0.15)',
67
+ success: '#22c55e',
68
+ error: '#ef4444',
69
+ warning: '#f59e0b',
70
+ };
71
+
72
+ const generateId = () => Math.random().toString(36).substring(2, 15);
73
+
74
+ // ============================================================================
75
+ // HOOKS
76
+ // ============================================================================
77
+
78
+ const useLocalStorage = <T,>(key: string, initialValue: T): [T, React.Dispatch<React.SetStateAction<T>>] => {
79
+ const [storedValue, setStoredValue] = useState<T>(() => {
80
+ try {
81
+ const item = window.localStorage.getItem(key);
82
+ return item ? JSON.parse(item) : initialValue;
83
+ } catch {
84
+ return initialValue;
85
+ }
86
+ });
87
+
88
+ const setValue: React.Dispatch<React.SetStateAction<T>> = (value) => {
89
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
90
+ setStoredValue(valueToStore);
91
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
92
+ };
93
+
94
+ return [storedValue, setValue];
95
+ };
96
+
97
+ const useResizable = (initialWidth: number, minWidth: number, maxWidth: number) => {
98
+ const [width, setWidth] = useState(initialWidth);
99
+ const isDragging = useRef(false);
100
+ const startX = useRef(0);
101
+ const startWidth = useRef(0);
102
+
103
+ const startResize = useCallback((e: React.MouseEvent) => {
104
+ isDragging.current = true;
105
+ startX.current = e.clientX;
106
+ startWidth.current = width;
107
+ document.body.style.cursor = 'col-resize';
108
+ document.body.style.userSelect = 'none';
109
+ }, [width]);
110
+
111
+ useEffect(() => {
112
+ const handleMouseMove = (e: MouseEvent) => {
113
+ if (!isDragging.current) return;
114
+ const delta = e.clientX - startX.current;
115
+ const newWidth = Math.min(Math.max(startWidth.current + delta, minWidth), maxWidth);
116
+ setWidth(newWidth);
117
+ };
118
+
119
+ const handleMouseUp = () => {
120
+ isDragging.current = false;
121
+ document.body.style.cursor = '';
122
+ document.body.style.userSelect = '';
123
+ };
124
+
125
+ document.addEventListener('mousemove', handleMouseMove);
126
+ document.addEventListener('mouseup', handleMouseUp);
127
+ return () => {
128
+ document.removeEventListener('mousemove', handleMouseMove);
129
+ document.removeEventListener('mouseup', handleMouseUp);
130
+ };
131
+ }, [minWidth, maxWidth]);
132
+
133
+ return { width, startResize };
134
+ };
135
+
136
+ // ============================================================================
137
+ // ICON COMPONENTS
138
+ // ============================================================================
139
+
140
+ const Icons = {
141
+ Plus: () => (
142
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2">
143
+ <path d="M8 3v10M3 8h10" />
144
+ </svg>
145
+ ),
146
+ Trash: () => (
147
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
148
+ <path d="M3 4h10M6 4V3a1 1 0 011-1h2a1 1 0 011 1v1M5 4v9a1 1 0 001 1h4a1 1 0 001-1V4" />
149
+ </svg>
150
+ ),
151
+ Send: () => (
152
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
153
+ <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" />
154
+ </svg>
155
+ ),
156
+ Image: () => (
157
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
158
+ <rect x="3" y="3" width="18" height="18" rx="2" />
159
+ <circle cx="8.5" cy="8.5" r="1.5" />
160
+ <path d="M21 15l-5-5L5 21" />
161
+ </svg>
162
+ ),
163
+ Code: () => (
164
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
165
+ <path d="M16 18l6-6-6-6M8 6l-6 6 6 6" />
166
+ </svg>
167
+ ),
168
+ Eye: () => (
169
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
170
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
171
+ <circle cx="12" cy="12" r="3" />
172
+ </svg>
173
+ ),
174
+ Copy: () => (
175
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
176
+ <rect x="9" y="9" width="13" height="13" rx="2" />
177
+ <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
178
+ </svg>
179
+ ),
180
+ Sidebar: () => (
181
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
182
+ <rect x="3" y="3" width="18" height="18" rx="2" />
183
+ <path d="M9 3v18" />
184
+ </svg>
185
+ ),
186
+ Check: () => (
187
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
188
+ <path d="M20 6L9 17l-5-5" />
189
+ </svg>
190
+ ),
191
+ X: () => (
192
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
193
+ <path d="M18 6L6 18M6 6l12 12" />
194
+ </svg>
195
+ ),
196
+ };
197
+
198
+ // ============================================================================
199
+ // SUB-COMPONENTS
200
+ // ============================================================================
201
+
202
+ const LoadingDots: React.FC = () => (
203
+ <div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
204
+ {[0, 1, 2].map(i => (
205
+ <div
206
+ key={i}
207
+ style={{
208
+ width: '6px',
209
+ height: '6px',
210
+ borderRadius: '50%',
211
+ background: THEME.accent,
212
+ animation: `pulse 1.4s ease-in-out ${i * 0.2}s infinite`,
213
+ }}
214
+ />
215
+ ))}
216
+ </div>
217
+ );
218
+
219
+ const ImageUploadArea: React.FC<{
220
+ images: ImageAttachment[];
221
+ onImagesChange: (images: ImageAttachment[]) => void;
222
+ disabled?: boolean;
223
+ }> = ({ images, onImagesChange, disabled }) => {
224
+ const fileInputRef = useRef<HTMLInputElement>(null);
225
+
226
+ const handleFiles = useCallback((files: FileList) => {
227
+ Array.from(files).forEach(file => {
228
+ if (file.type.startsWith('image/')) {
229
+ const reader = new FileReader();
230
+ reader.onload = (e) => {
231
+ const newImage: ImageAttachment = {
232
+ id: generateId(),
233
+ data: e.target?.result as string,
234
+ type: file.type,
235
+ name: file.name,
236
+ };
237
+ onImagesChange([...images, newImage]);
238
+ };
239
+ reader.readAsDataURL(file);
240
+ }
241
+ });
242
+ }, [images, onImagesChange]);
243
+
244
+ const removeImage = (id: string) => {
245
+ onImagesChange(images.filter(img => img.id !== id));
246
+ };
247
+
248
+ return (
249
+ <div style={{ marginBottom: images.length > 0 ? '12px' : 0 }}>
250
+ <input
251
+ ref={fileInputRef}
252
+ type="file"
253
+ accept="image/*"
254
+ multiple
255
+ style={{ display: 'none' }}
256
+ onChange={(e) => e.target.files && handleFiles(e.target.files)}
257
+ disabled={disabled}
258
+ />
259
+
260
+ {images.length > 0 && (
261
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '8px' }}>
262
+ {images.map(img => (
263
+ <div
264
+ key={img.id}
265
+ style={{
266
+ position: 'relative',
267
+ width: '64px',
268
+ height: '64px',
269
+ borderRadius: '8px',
270
+ overflow: 'hidden',
271
+ border: `1px solid ${THEME.border}`,
272
+ }}
273
+ >
274
+ <img
275
+ src={img.data}
276
+ alt={img.name}
277
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
278
+ />
279
+ <button
280
+ onClick={() => removeImage(img.id)}
281
+ style={{
282
+ position: 'absolute',
283
+ top: '2px',
284
+ right: '2px',
285
+ width: '18px',
286
+ height: '18px',
287
+ borderRadius: '50%',
288
+ background: 'rgba(0,0,0,0.7)',
289
+ border: 'none',
290
+ color: '#fff',
291
+ cursor: 'pointer',
292
+ display: 'flex',
293
+ alignItems: 'center',
294
+ justifyContent: 'center',
295
+ fontSize: '10px',
296
+ }}
297
+ >
298
+ <Icons.X />
299
+ </button>
300
+ </div>
301
+ ))}
302
+ </div>
303
+ )}
304
+
305
+ <button
306
+ onClick={() => fileInputRef.current?.click()}
307
+ disabled={disabled}
308
+ style={{
309
+ display: 'flex',
310
+ alignItems: 'center',
311
+ gap: '6px',
312
+ padding: '6px 10px',
313
+ background: 'transparent',
314
+ border: `1px dashed ${THEME.border}`,
315
+ borderRadius: '6px',
316
+ color: THEME.textMuted,
317
+ fontSize: '12px',
318
+ cursor: disabled ? 'not-allowed' : 'pointer',
319
+ transition: 'all 0.2s',
320
+ opacity: disabled ? 0.5 : 1,
321
+ }}
322
+ >
323
+ <Icons.Image />
324
+ <span>Add image</span>
325
+ </button>
326
+ </div>
327
+ );
328
+ };
329
+
330
+ const CodeViewer: React.FC<{ code: string }> = ({ code }) => {
331
+ const [copied, setCopied] = useState(false);
332
+
333
+ const handleCopy = async () => {
334
+ await navigator.clipboard.writeText(code);
335
+ setCopied(true);
336
+ setTimeout(() => setCopied(false), 2000);
337
+ };
338
+
339
+ return (
340
+ <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
341
+ <div style={{
342
+ padding: '8px 12px',
343
+ borderBottom: `1px solid ${THEME.border}`,
344
+ display: 'flex',
345
+ justifyContent: 'space-between',
346
+ alignItems: 'center',
347
+ }}>
348
+ <span style={{ fontSize: '12px', color: THEME.textMuted }}>Generated Code</span>
349
+ <button
350
+ onClick={handleCopy}
351
+ style={{
352
+ display: 'flex',
353
+ alignItems: 'center',
354
+ gap: '4px',
355
+ padding: '4px 8px',
356
+ background: copied ? 'rgba(34, 197, 94, 0.2)' : 'transparent',
357
+ border: `1px solid ${copied ? THEME.success : THEME.border}`,
358
+ borderRadius: '4px',
359
+ color: copied ? THEME.success : THEME.textMuted,
360
+ fontSize: '11px',
361
+ cursor: 'pointer',
362
+ transition: 'all 0.2s',
363
+ }}
364
+ >
365
+ {copied ? <Icons.Check /> : <Icons.Copy />}
366
+ {copied ? 'Copied!' : 'Copy'}
367
+ </button>
368
+ </div>
369
+ <pre
370
+ style={{
371
+ flex: 1,
372
+ margin: 0,
373
+ padding: '16px',
374
+ overflow: 'auto',
375
+ fontSize: '13px',
376
+ lineHeight: 1.6,
377
+ fontFamily: '"Fira Code", "SF Mono", Monaco, monospace',
378
+ background: '#0d1117',
379
+ color: '#e6edf3',
380
+ }}
381
+ >
382
+ {code}
383
+ </pre>
384
+ </div>
385
+ );
386
+ };
387
+
388
+ // ============================================================================
389
+ // MAIN APP
390
+ // ============================================================================
391
+
392
+ const App: React.FC = () => {
393
+ const [conversations, setConversations] = useLocalStorage<Conversation[]>('storyui_conversations', []);
394
+ const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
395
+ const [inputValue, setInputValue] = useState('');
396
+ const [isGenerating, setIsGenerating] = useState(false);
397
+ const [previewCode, setPreviewCode] = useState<string | null>(null);
398
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
399
+ const [previewTab, setPreviewTab] = useState<'preview' | 'code'>('preview');
400
+ const [images, setImages] = useState<ImageAttachment[]>([]);
401
+
402
+ const messagesEndRef = useRef<HTMLDivElement>(null);
403
+ const inputRef = useRef<HTMLTextAreaElement>(null);
404
+ const { width: chatWidth, startResize } = useResizable(400, 320, 600);
405
+
406
+ const activeConversation = conversations.find(c => c.id === activeConversationId);
407
+
408
+ // Auto-scroll messages
409
+ useEffect(() => {
410
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
411
+ }, [activeConversation?.messages]);
412
+
413
+ // Initialize first conversation
414
+ useEffect(() => {
415
+ if (conversations.length > 0 && !activeConversationId) {
416
+ setActiveConversationId(conversations[0].id);
417
+ const lastMsgWithCode = [...conversations[0].messages].reverse().find(m => m.generatedCode);
418
+ setPreviewCode(lastMsgWithCode?.generatedCode || null);
419
+ } else if (conversations.length === 0) {
420
+ createNewConversation();
421
+ }
422
+ }, [conversations.length, activeConversationId]);
423
+
424
+ const createNewConversation = useCallback(() => {
425
+ const newConversation: Conversation = {
426
+ id: generateId(),
427
+ title: 'New Conversation',
428
+ messages: [],
429
+ createdAt: Date.now(),
430
+ updatedAt: Date.now(),
431
+ };
432
+ setConversations(prev => [newConversation, ...prev]);
433
+ setActiveConversationId(newConversation.id);
434
+ setPreviewCode(null);
435
+ setImages([]);
436
+ inputRef.current?.focus();
437
+ }, [setConversations]);
438
+
439
+ const deleteConversation = (id: string) => {
440
+ setConversations(prev => prev.filter(c => c.id !== id));
441
+ if (activeConversationId === id) {
442
+ const remaining = conversations.filter(c => c.id !== id);
443
+ if (remaining.length > 0) {
444
+ setActiveConversationId(remaining[0].id);
445
+ } else {
446
+ createNewConversation();
447
+ }
448
+ }
449
+ };
450
+
451
+ const generateComponent = async (prompt: string, imageAttachments: ImageAttachment[]): Promise<string> => {
452
+ // Get the last generated code from conversation for iteration context
453
+ const lastGeneratedCode = activeConversation?.messages
454
+ .filter(m => m.generatedCode)
455
+ .slice(-1)[0]?.generatedCode;
456
+
457
+ // Check if this is an iteration (modification of existing code)
458
+ const isIteration = !!lastGeneratedCode && (activeConversation?.messages.length || 0) > 0;
459
+
460
+ // Build conversation history, but for iterations include the code reference differently
461
+ const conversationHistory = isIteration
462
+ ? activeConversation?.messages.slice(0, -1).map(msg => ({
463
+ role: msg.role,
464
+ content: msg.role === 'assistant' && msg.generatedCode
465
+ ? `[Generated JSX component - see CURRENT_CODE below]`
466
+ : msg.content
467
+ })) || []
468
+ : [];
469
+
470
+ // Build a UNIVERSAL system prompt that works with ANY component library
471
+ // Design-system-specific rules come from aiConsiderations (generated from story-ui-considerations.md)
472
+ const basePrompt = `You are a JSX code generator. Your ONLY job is to output raw JSX code.
473
+
474
+ CRITICAL OUTPUT RULES:
475
+ 1. Start DIRECTLY with < (opening tag of a component)
476
+ 2. End with > (closing tag)
477
+ 3. NO markdown, NO headers, NO explanations, NO code fences
478
+ 4. NO internal tags like <budget>, <usage>, <thinking>, or any metadata
479
+ 5. NEVER output anything except JSX components
480
+
481
+ UNIVERSAL BEST PRACTICES (applies to ALL design systems):
482
+
483
+ THEME & COLORS:
484
+ - Components render on a LIGHT BACKGROUND by default
485
+ - Use DARK text colors for body text (ensure readability)
486
+ - Never use white/light text colors unless on a dark or colored background
487
+ - Ensure sufficient color contrast (4.5:1 for normal text, 3:1 for large text)
488
+
489
+ ACCESSIBILITY (WCAG):
490
+ - Use semantic HTML structure (headings, lists, landmarks)
491
+ - Include aria-labels on interactive elements without visible text
492
+ - Ensure focusable elements have visible focus states
493
+ - Use role attributes appropriately
494
+ - Form inputs should have associated labels
495
+ - Interactive elements should be keyboard accessible
496
+
497
+ RESPONSIVE DESIGN:
498
+ - Components should work at various viewport sizes
499
+ - Use relative units and flexible layouts
500
+ - Avoid fixed pixel widths that could cause overflow
501
+
502
+ AVAILABLE COMPONENTS (use ONLY these):
503
+ ${availableComponents.join(', ')}${hasConsiderations ? `
504
+
505
+ DESIGN SYSTEM GUIDELINES:
506
+ ${aiConsiderations}` : ''}`;
507
+
508
+ let systemPrompt: string;
509
+
510
+ if (isIteration && lastGeneratedCode) {
511
+ // Iteration-specific prompt with clear modification instructions
512
+ systemPrompt = `${basePrompt}
513
+
514
+ ITERATION MODE - You are MODIFYING existing code:
515
+
516
+ CURRENT_CODE (this is what you're modifying):
517
+ ${lastGeneratedCode}
518
+
519
+ MODIFICATION RULES:
520
+ 1. Keep the overall structure unless asked to change it
521
+ 2. Only modify what the user specifically requests
522
+ 3. Preserve existing styling, layout, and components not mentioned
523
+ 4. Output the COMPLETE modified JSX (not just the changed parts)
524
+
525
+ OUTPUT: Start immediately with < and output only the complete modified JSX.`;
526
+ } else {
527
+ // New generation prompt
528
+ systemPrompt = `${basePrompt}
529
+
530
+ GENERATION RULES:
531
+ 1. Output a SINGLE JSX expression starting with < and ending with >
532
+ 2. Use ONLY components from the list above
533
+ 3. NO imports, NO exports, NO function definitions
534
+ 4. NO explanations, NO comments outside JSX
535
+
536
+ OUTPUT: Start immediately with < and output only JSX.`;
537
+ }
538
+
539
+ // Use assistant prefill to force Claude to start with JSX
540
+ // This is a powerful technique that constrains the output format
541
+ const prefillAssistant = '<';
542
+
543
+ const response = await fetch(`${SERVER_URL}/story-ui/claude`, {
544
+ method: 'POST',
545
+ headers: { 'Content-Type': 'application/json' },
546
+ body: JSON.stringify({
547
+ prompt,
548
+ messages: conversationHistory,
549
+ systemPrompt,
550
+ prefillAssistant,
551
+ maxTokens: 4096,
552
+ images: imageAttachments.map(img => ({
553
+ type: img.type,
554
+ data: img.data.split(',')[1],
555
+ })),
556
+ }),
557
+ });
558
+
559
+ if (!response.ok) {
560
+ const errorData = await response.json().catch(() => ({}));
561
+ throw new Error(errorData.error || `Server Error: ${response.status}`);
562
+ }
563
+
564
+ const data = await response.json();
565
+ // Handle various response formats from different LLM providers
566
+ let content: string;
567
+ if (Array.isArray(data.content)) {
568
+ // Claude API format: { content: [{ type: 'text', text: '...' }] }
569
+ content = data.content[0]?.text || '';
570
+ } else if (typeof data.content === 'string') {
571
+ content = data.content;
572
+ } else if (typeof data.text === 'string') {
573
+ // Alternative format
574
+ content = data.text;
575
+ } else {
576
+ content = String(data.content || data.text || '');
577
+ }
578
+
579
+ // Clean up the response
580
+ let cleanCode = content.trim();
581
+ if (cleanCode.startsWith('```')) {
582
+ cleanCode = cleanCode
583
+ .replace(/^```(?:jsx|tsx|javascript|js)?\n?/, '')
584
+ .replace(/\n?```$/, '');
585
+ }
586
+
587
+ return cleanCode;
588
+ };
589
+
590
+ const sendMessage = async () => {
591
+ if (!inputValue.trim() || !activeConversationId || isGenerating) return;
592
+
593
+ const userMessage: Message = {
594
+ id: generateId(),
595
+ role: 'user',
596
+ content: inputValue.trim(),
597
+ timestamp: Date.now(),
598
+ images: images.length > 0 ? [...images] : undefined,
599
+ };
600
+
601
+ setConversations(prev => prev.map(conv => {
602
+ if (conv.id === activeConversationId) {
603
+ return {
604
+ ...conv,
605
+ messages: [...conv.messages, userMessage],
606
+ updatedAt: Date.now(),
607
+ title: conv.messages.length === 0 ? inputValue.trim().substring(0, 40) : conv.title,
608
+ };
609
+ }
610
+ return conv;
611
+ }));
612
+
613
+ const currentImages = [...images];
614
+ setInputValue('');
615
+ setImages([]);
616
+ setIsGenerating(true);
617
+
618
+ try {
619
+ const generatedCode = await generateComponent(inputValue.trim(), currentImages);
620
+
621
+ const assistantMessage: Message = {
622
+ id: generateId(),
623
+ role: 'assistant',
624
+ content: 'Component generated successfully.',
625
+ timestamp: Date.now(),
626
+ generatedCode,
627
+ };
628
+
629
+ setConversations(prev => prev.map(conv => {
630
+ if (conv.id === activeConversationId) {
631
+ return {
632
+ ...conv,
633
+ messages: [...conv.messages, assistantMessage],
634
+ updatedAt: Date.now(),
635
+ };
636
+ }
637
+ return conv;
638
+ }));
639
+
640
+ if (generatedCode) {
641
+ setPreviewCode(generatedCode);
642
+ setPreviewTab('preview');
643
+ }
644
+ } catch (error) {
645
+ const errorMessage: Message = {
646
+ id: generateId(),
647
+ role: 'assistant',
648
+ content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
649
+ timestamp: Date.now(),
650
+ };
651
+
652
+ setConversations(prev => prev.map(conv => {
653
+ if (conv.id === activeConversationId) {
654
+ return {
655
+ ...conv,
656
+ messages: [...conv.messages, errorMessage],
657
+ updatedAt: Date.now(),
658
+ };
659
+ }
660
+ return conv;
661
+ }));
662
+ } finally {
663
+ setIsGenerating(false);
664
+ }
665
+ };
666
+
667
+ const handleKeyPress = (e: React.KeyboardEvent) => {
668
+ if (e.key === 'Enter' && !e.shiftKey) {
669
+ e.preventDefault();
670
+ sendMessage();
671
+ }
672
+ };
673
+
674
+ return (
675
+ <div style={{
676
+ display: 'flex',
677
+ height: '100vh',
678
+ overflow: 'hidden',
679
+ background: THEME.bg,
680
+ color: THEME.text,
681
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
682
+ }}>
683
+ {/* Global Styles */}
684
+ <style>{`
685
+ @keyframes pulse {
686
+ 0%, 100% { opacity: 0.4; transform: scale(0.8); }
687
+ 50% { opacity: 1; transform: scale(1); }
688
+ }
689
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
690
+ ::-webkit-scrollbar-track { background: transparent; }
691
+ ::-webkit-scrollbar-thumb { background: ${THEME.bgElevated}; border-radius: 3px; }
692
+ ::-webkit-scrollbar-thumb:hover { background: ${THEME.bgHover}; }
693
+ `}</style>
694
+
695
+ {/* Sidebar */}
696
+ <aside style={{
697
+ width: sidebarCollapsed ? '60px' : '240px',
698
+ background: THEME.bgSurface,
699
+ borderRight: `1px solid ${THEME.border}`,
700
+ display: 'flex',
701
+ flexDirection: 'column',
702
+ transition: 'width 0.2s ease',
703
+ overflow: 'hidden',
704
+ }}>
705
+ {/* Sidebar Header */}
706
+ <div style={{
707
+ padding: '16px',
708
+ borderBottom: `1px solid ${THEME.border}`,
709
+ display: 'flex',
710
+ alignItems: 'center',
711
+ justifyContent: sidebarCollapsed ? 'center' : 'space-between',
712
+ }}>
713
+ {!sidebarCollapsed && (
714
+ <h1 style={{
715
+ fontSize: '16px',
716
+ fontWeight: 600,
717
+ background: `linear-gradient(135deg, ${THEME.accent}, #8b5cf6)`,
718
+ WebkitBackgroundClip: 'text',
719
+ WebkitTextFillColor: 'transparent',
720
+ letterSpacing: '-0.5px',
721
+ }}>
722
+ Story UI
723
+ </h1>
724
+ )}
725
+ <button
726
+ onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
727
+ style={{
728
+ padding: '8px',
729
+ background: 'transparent',
730
+ border: 'none',
731
+ color: THEME.textMuted,
732
+ cursor: 'pointer',
733
+ borderRadius: '6px',
734
+ }}
735
+ >
736
+ <Icons.Sidebar />
737
+ </button>
738
+ </div>
739
+
740
+ {/* New Chat Button */}
741
+ <div style={{ padding: sidebarCollapsed ? '8px' : '12px' }}>
742
+ <button
743
+ onClick={createNewConversation}
744
+ style={{
745
+ width: '100%',
746
+ padding: sidebarCollapsed ? '10px' : '10px 14px',
747
+ background: THEME.accent,
748
+ border: 'none',
749
+ borderRadius: '8px',
750
+ color: '#fff',
751
+ fontSize: '13px',
752
+ fontWeight: 500,
753
+ cursor: 'pointer',
754
+ display: 'flex',
755
+ alignItems: 'center',
756
+ justifyContent: 'center',
757
+ gap: '8px',
758
+ }}
759
+ >
760
+ <Icons.Plus />
761
+ {!sidebarCollapsed && 'New Chat'}
762
+ </button>
763
+ </div>
764
+
765
+ {/* Conversation List */}
766
+ {!sidebarCollapsed && (
767
+ <div style={{ flex: 1, overflow: 'auto', padding: '8px 12px' }}>
768
+ {conversations.map(conv => (
769
+ <div
770
+ key={conv.id}
771
+ onClick={() => {
772
+ setActiveConversationId(conv.id);
773
+ const lastMsgWithCode = [...conv.messages].reverse().find(m => m.generatedCode);
774
+ setPreviewCode(lastMsgWithCode?.generatedCode || null);
775
+ }}
776
+ style={{
777
+ padding: '10px 12px',
778
+ marginBottom: '4px',
779
+ borderRadius: '8px',
780
+ cursor: 'pointer',
781
+ display: 'flex',
782
+ alignItems: 'center',
783
+ justifyContent: 'space-between',
784
+ background: conv.id === activeConversationId ? THEME.bgElevated : 'transparent',
785
+ }}
786
+ >
787
+ <span style={{
788
+ fontSize: '13px',
789
+ color: conv.id === activeConversationId ? THEME.text : THEME.textMuted,
790
+ overflow: 'hidden',
791
+ textOverflow: 'ellipsis',
792
+ whiteSpace: 'nowrap',
793
+ flex: 1,
794
+ }}>
795
+ {conv.title}
796
+ </span>
797
+ <button
798
+ onClick={(e) => {
799
+ e.stopPropagation();
800
+ deleteConversation(conv.id);
801
+ }}
802
+ style={{
803
+ padding: '4px',
804
+ background: 'transparent',
805
+ border: 'none',
806
+ color: THEME.textSubtle,
807
+ cursor: 'pointer',
808
+ opacity: 0.5,
809
+ }}
810
+ >
811
+ <Icons.Trash />
812
+ </button>
813
+ </div>
814
+ ))}
815
+ </div>
816
+ )}
817
+
818
+ {/* Components Badge */}
819
+ {!sidebarCollapsed && (
820
+ <div style={{
821
+ padding: '12px',
822
+ borderTop: `1px solid ${THEME.border}`,
823
+ fontSize: '11px',
824
+ color: THEME.textSubtle,
825
+ }}>
826
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
827
+ <div style={{
828
+ width: '6px',
829
+ height: '6px',
830
+ borderRadius: '50%',
831
+ background: THEME.success,
832
+ }} />
833
+ <span>{availableComponents.length} components available</span>
834
+ </div>
835
+ </div>
836
+ )}
837
+ </aside>
838
+
839
+ {/* Chat Panel */}
840
+ <div style={{
841
+ width: `${chatWidth}px`,
842
+ display: 'flex',
843
+ flexDirection: 'column',
844
+ background: THEME.bg,
845
+ borderRight: `1px solid ${THEME.border}`,
846
+ position: 'relative',
847
+ }}>
848
+ {/* Messages */}
849
+ <div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
850
+ {activeConversation?.messages.length === 0 && (
851
+ <div style={{
852
+ padding: '32px 16px',
853
+ textAlign: 'center',
854
+ }}>
855
+ <h2 style={{
856
+ fontSize: '24px',
857
+ fontWeight: 600,
858
+ marginBottom: '8px',
859
+ background: `linear-gradient(135deg, ${THEME.text}, ${THEME.textMuted})`,
860
+ WebkitBackgroundClip: 'text',
861
+ WebkitTextFillColor: 'transparent',
862
+ }}>
863
+ Build UI Components
864
+ </h2>
865
+ <p style={{
866
+ color: THEME.textMuted,
867
+ fontSize: '14px',
868
+ marginBottom: '24px',
869
+ lineHeight: 1.6,
870
+ }}>
871
+ Describe what you want to build using your component library.
872
+ </p>
873
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', justifyContent: 'center' }}>
874
+ {['Create a pricing card', 'Build a dashboard', 'Design a login form'].map(suggestion => (
875
+ <button
876
+ key={suggestion}
877
+ onClick={() => setInputValue(suggestion)}
878
+ style={{
879
+ padding: '8px 14px',
880
+ background: THEME.bgSurface,
881
+ border: `1px solid ${THEME.border}`,
882
+ borderRadius: '20px',
883
+ color: THEME.textMuted,
884
+ fontSize: '13px',
885
+ cursor: 'pointer',
886
+ }}
887
+ >
888
+ {suggestion}
889
+ </button>
890
+ ))}
891
+ </div>
892
+ </div>
893
+ )}
894
+
895
+ {activeConversation?.messages.map(message => (
896
+ <div
897
+ key={message.id}
898
+ style={{
899
+ marginBottom: '16px',
900
+ padding: '14px 16px',
901
+ borderRadius: '12px',
902
+ maxWidth: '90%',
903
+ marginLeft: message.role === 'user' ? 'auto' : '0',
904
+ marginRight: message.role === 'user' ? '0' : 'auto',
905
+ background: message.role === 'user' ? THEME.bgElevated : THEME.bgSurface,
906
+ border: `1px solid ${THEME.border}`,
907
+ }}
908
+ >
909
+ {message.images && message.images.length > 0 && (
910
+ <div style={{ display: 'flex', gap: '8px', marginBottom: '10px', flexWrap: 'wrap' }}>
911
+ {message.images.map(img => (
912
+ <img
913
+ key={img.id}
914
+ src={img.data}
915
+ alt={img.name}
916
+ style={{
917
+ width: '80px',
918
+ height: '80px',
919
+ objectFit: 'cover',
920
+ borderRadius: '8px',
921
+ border: `1px solid ${THEME.border}`,
922
+ }}
923
+ />
924
+ ))}
925
+ </div>
926
+ )}
927
+ <div style={{ fontSize: '14px', lineHeight: 1.6, color: THEME.text }}>
928
+ {message.content}
929
+ </div>
930
+ {message.generatedCode && (
931
+ <button
932
+ onClick={() => {
933
+ setPreviewCode(message.generatedCode!);
934
+ setPreviewTab('preview');
935
+ }}
936
+ style={{
937
+ marginTop: '10px',
938
+ padding: '6px 12px',
939
+ background: THEME.accentMuted,
940
+ border: `1px solid ${THEME.accent}`,
941
+ borderRadius: '6px',
942
+ color: THEME.accent,
943
+ fontSize: '12px',
944
+ cursor: 'pointer',
945
+ display: 'flex',
946
+ alignItems: 'center',
947
+ gap: '6px',
948
+ }}
949
+ >
950
+ <Icons.Eye />
951
+ View Component
952
+ </button>
953
+ )}
954
+ </div>
955
+ ))}
956
+
957
+ {isGenerating && (
958
+ <div style={{
959
+ marginBottom: '16px',
960
+ padding: '14px 16px',
961
+ borderRadius: '12px',
962
+ maxWidth: '90%',
963
+ background: THEME.bgSurface,
964
+ border: `1px solid ${THEME.border}`,
965
+ }}>
966
+ <LoadingDots />
967
+ </div>
968
+ )}
969
+
970
+ <div ref={messagesEndRef} />
971
+ </div>
972
+
973
+ {/* Input Area */}
974
+ <div style={{
975
+ padding: '16px',
976
+ borderTop: `1px solid ${THEME.border}`,
977
+ background: THEME.bgSurface,
978
+ }}>
979
+ <ImageUploadArea
980
+ images={images}
981
+ onImagesChange={setImages}
982
+ disabled={isGenerating}
983
+ />
984
+
985
+ <div style={{ display: 'flex', gap: '10px', alignItems: 'flex-end' }}>
986
+ <textarea
987
+ ref={inputRef}
988
+ value={inputValue}
989
+ onChange={e => setInputValue(e.target.value)}
990
+ onKeyDown={handleKeyPress}
991
+ placeholder="Describe the component you want to create..."
992
+ disabled={isGenerating}
993
+ style={{
994
+ flex: 1,
995
+ padding: '12px 14px',
996
+ background: THEME.bgElevated,
997
+ border: `1px solid ${THEME.border}`,
998
+ borderRadius: '10px',
999
+ color: THEME.text,
1000
+ fontSize: '14px',
1001
+ resize: 'none',
1002
+ outline: 'none',
1003
+ fontFamily: 'inherit',
1004
+ minHeight: '44px',
1005
+ maxHeight: '120px',
1006
+ }}
1007
+ rows={1}
1008
+ />
1009
+ <button
1010
+ onClick={sendMessage}
1011
+ disabled={isGenerating || !inputValue.trim()}
1012
+ style={{
1013
+ padding: '12px',
1014
+ background: (isGenerating || !inputValue.trim()) ? THEME.bgElevated : THEME.accent,
1015
+ border: 'none',
1016
+ borderRadius: '10px',
1017
+ color: (isGenerating || !inputValue.trim()) ? THEME.textSubtle : '#fff',
1018
+ cursor: (isGenerating || !inputValue.trim()) ? 'not-allowed' : 'pointer',
1019
+ minWidth: '44px',
1020
+ height: '44px',
1021
+ }}
1022
+ >
1023
+ <Icons.Send />
1024
+ </button>
1025
+ </div>
1026
+ </div>
1027
+
1028
+ {/* Resize Handle */}
1029
+ <div
1030
+ onMouseDown={startResize}
1031
+ style={{
1032
+ position: 'absolute',
1033
+ right: 0,
1034
+ top: 0,
1035
+ bottom: 0,
1036
+ width: '4px',
1037
+ cursor: 'col-resize',
1038
+ background: 'transparent',
1039
+ }}
1040
+ />
1041
+ </div>
1042
+
1043
+ {/* Preview Panel */}
1044
+ <div style={{
1045
+ flex: 1,
1046
+ display: 'flex',
1047
+ flexDirection: 'column',
1048
+ background: THEME.bg,
1049
+ minWidth: 0,
1050
+ }}>
1051
+ {/* Preview Header */}
1052
+ <div style={{
1053
+ padding: '12px 16px',
1054
+ borderBottom: `1px solid ${THEME.border}`,
1055
+ display: 'flex',
1056
+ alignItems: 'center',
1057
+ justifyContent: 'space-between',
1058
+ }}>
1059
+ <div style={{ display: 'flex', gap: '4px' }}>
1060
+ <button
1061
+ onClick={() => setPreviewTab('preview')}
1062
+ style={{
1063
+ padding: '8px 14px',
1064
+ background: previewTab === 'preview' ? THEME.bgElevated : 'transparent',
1065
+ border: 'none',
1066
+ borderRadius: '6px',
1067
+ color: previewTab === 'preview' ? THEME.text : THEME.textMuted,
1068
+ fontSize: '13px',
1069
+ cursor: 'pointer',
1070
+ display: 'flex',
1071
+ alignItems: 'center',
1072
+ gap: '6px',
1073
+ }}
1074
+ >
1075
+ <Icons.Eye />
1076
+ Preview
1077
+ </button>
1078
+ <button
1079
+ onClick={() => setPreviewTab('code')}
1080
+ style={{
1081
+ padding: '8px 14px',
1082
+ background: previewTab === 'code' ? THEME.bgElevated : 'transparent',
1083
+ border: 'none',
1084
+ borderRadius: '6px',
1085
+ color: previewTab === 'code' ? THEME.text : THEME.textMuted,
1086
+ fontSize: '13px',
1087
+ cursor: 'pointer',
1088
+ display: 'flex',
1089
+ alignItems: 'center',
1090
+ gap: '6px',
1091
+ }}
1092
+ >
1093
+ <Icons.Code />
1094
+ Code
1095
+ </button>
1096
+ </div>
1097
+
1098
+ {previewCode && previewTab === 'preview' && (
1099
+ <span style={{ fontSize: '12px', color: THEME.textMuted }}>
1100
+ Live Preview
1101
+ </span>
1102
+ )}
1103
+ </div>
1104
+
1105
+ {/* Preview Content */}
1106
+ <div style={{ flex: 1, overflow: 'hidden' }}>
1107
+ {previewCode ? (
1108
+ previewTab === 'preview' ? (
1109
+ <LivePreviewRenderer
1110
+ code={previewCode}
1111
+ containerStyle={{ height: '100%', background: THEME.bgSurface }}
1112
+ onError={(err) => console.error('Preview error:', err)}
1113
+ />
1114
+ ) : (
1115
+ <CodeViewer code={previewCode} />
1116
+ )
1117
+ ) : (
1118
+ <div style={{
1119
+ height: '100%',
1120
+ display: 'flex',
1121
+ flexDirection: 'column',
1122
+ alignItems: 'center',
1123
+ justifyContent: 'center',
1124
+ color: THEME.textSubtle,
1125
+ padding: '40px',
1126
+ }}>
1127
+ <div style={{
1128
+ width: '80px',
1129
+ height: '80px',
1130
+ borderRadius: '20px',
1131
+ background: THEME.bgSurface,
1132
+ display: 'flex',
1133
+ alignItems: 'center',
1134
+ justifyContent: 'center',
1135
+ marginBottom: '20px',
1136
+ border: `1px solid ${THEME.border}`,
1137
+ }}>
1138
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
1139
+ <rect x="3" y="3" width="18" height="18" rx="2" />
1140
+ <path d="M3 9h18M9 21V9" />
1141
+ </svg>
1142
+ </div>
1143
+ <p style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px', color: THEME.textMuted }}>
1144
+ Ready to Build
1145
+ </p>
1146
+ <p style={{ fontSize: '14px', maxWidth: '260px', textAlign: 'center', lineHeight: 1.5 }}>
1147
+ Describe a component and watch it come to life
1148
+ </p>
1149
+ </div>
1150
+ )}
1151
+ </div>
1152
+ </div>
1153
+ </div>
1154
+ );
1155
+ };
1156
+
1157
+ export default App;