@tpitre/story-ui 2.1.5 → 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.
- package/.env.sample +82 -11
- package/README.md +130 -4
- package/dist/cli/deploy.d.ts +17 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +696 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +55 -2
- package/dist/cli/setup.d.ts +11 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +437 -110
- package/dist/mcp-server/index.d.ts +2 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +138 -6
- package/dist/mcp-server/mcp-stdio-server.d.ts +3 -0
- package/dist/mcp-server/mcp-stdio-server.d.ts.map +1 -0
- package/dist/mcp-server/mcp-stdio-server.js +638 -0
- package/dist/mcp-server/routes/claude.d.ts +3 -0
- package/dist/mcp-server/routes/claude.d.ts.map +1 -0
- package/dist/mcp-server/routes/claude.js +60 -23
- package/dist/mcp-server/routes/components.d.ts +4 -0
- package/dist/mcp-server/routes/components.d.ts.map +1 -0
- package/dist/mcp-server/routes/frameworks.d.ts +38 -0
- package/dist/mcp-server/routes/frameworks.d.ts.map +1 -0
- package/dist/mcp-server/routes/frameworks.js +183 -0
- package/dist/mcp-server/routes/generateStory.d.ts +3 -0
- package/dist/mcp-server/routes/generateStory.d.ts.map +1 -0
- package/dist/mcp-server/routes/generateStory.js +274 -115
- package/dist/mcp-server/routes/generateStoryStream.d.ts +12 -0
- package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -0
- package/dist/mcp-server/routes/generateStoryStream.js +947 -0
- package/dist/mcp-server/routes/hybridStories.d.ts +18 -0
- package/dist/mcp-server/routes/hybridStories.d.ts.map +1 -0
- package/dist/mcp-server/routes/hybridStories.js +214 -0
- package/dist/mcp-server/routes/mcpRemote.d.ts +14 -0
- package/dist/mcp-server/routes/mcpRemote.d.ts.map +1 -0
- package/dist/mcp-server/routes/mcpRemote.js +489 -0
- package/dist/mcp-server/routes/memoryStories.d.ts +26 -0
- package/dist/mcp-server/routes/memoryStories.d.ts.map +1 -0
- package/dist/mcp-server/routes/memoryStories.js +13 -7
- package/dist/mcp-server/routes/providers.d.ts +89 -0
- package/dist/mcp-server/routes/providers.d.ts.map +1 -0
- package/dist/mcp-server/routes/providers.js +369 -0
- package/dist/mcp-server/routes/storySync.d.ts +26 -0
- package/dist/mcp-server/routes/storySync.d.ts.map +1 -0
- package/dist/mcp-server/routes/streamTypes.d.ts +110 -0
- package/dist/mcp-server/routes/streamTypes.d.ts.map +1 -0
- package/dist/mcp-server/routes/streamTypes.js +18 -0
- package/dist/mcp-server/sessionManager.d.ts +50 -0
- package/dist/mcp-server/sessionManager.d.ts.map +1 -0
- package/dist/mcp-server/sessionManager.js +125 -0
- package/dist/story-generator/componentBlacklist.d.ts +21 -0
- package/dist/story-generator/componentBlacklist.d.ts.map +1 -0
- package/dist/story-generator/componentBlacklist.js +4 -0
- package/dist/story-generator/componentDiscovery.d.ts +28 -0
- package/dist/story-generator/componentDiscovery.d.ts.map +1 -0
- package/dist/story-generator/componentRegistryGenerator.d.ts +49 -0
- package/dist/story-generator/componentRegistryGenerator.d.ts.map +1 -0
- package/dist/story-generator/componentRegistryGenerator.js +205 -0
- package/dist/story-generator/configLoader.d.ts +33 -0
- package/dist/story-generator/configLoader.d.ts.map +1 -0
- package/dist/story-generator/configLoader.js +8 -1
- package/dist/story-generator/considerationsLoader.d.ts +32 -0
- package/dist/story-generator/considerationsLoader.d.ts.map +1 -0
- package/dist/story-generator/considerationsLoader.js +2 -1
- package/dist/story-generator/documentation-sources.d.ts +28 -0
- package/dist/story-generator/documentation-sources.d.ts.map +1 -0
- package/dist/story-generator/documentationLoader.d.ts +64 -0
- package/dist/story-generator/documentationLoader.d.ts.map +1 -0
- package/dist/story-generator/documentationLoader.js +4 -3
- package/dist/story-generator/dynamicPackageDiscovery.d.ts +97 -0
- package/dist/story-generator/dynamicPackageDiscovery.d.ts.map +1 -0
- package/dist/story-generator/dynamicPackageDiscovery.js +31 -22
- package/dist/story-generator/enhancedComponentDiscovery.d.ts +125 -0
- package/dist/story-generator/enhancedComponentDiscovery.d.ts.map +1 -0
- package/dist/story-generator/enhancedComponentDiscovery.js +162 -21
- package/dist/story-generator/framework-adapters/angular-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/angular-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/angular-adapter.js +427 -0
- package/dist/story-generator/framework-adapters/base-adapter.d.ts +75 -0
- package/dist/story-generator/framework-adapters/base-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/base-adapter.js +147 -0
- package/dist/story-generator/framework-adapters/framework-detector.d.ts +55 -0
- package/dist/story-generator/framework-adapters/framework-detector.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/framework-detector.js +323 -0
- package/dist/story-generator/framework-adapters/index.d.ts +97 -0
- package/dist/story-generator/framework-adapters/index.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/index.js +198 -0
- package/dist/story-generator/framework-adapters/react-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/react-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/react-adapter.js +316 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.js +372 -0
- package/dist/story-generator/framework-adapters/types.d.ts +182 -0
- package/dist/story-generator/framework-adapters/types.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/types.js +8 -0
- package/dist/story-generator/framework-adapters/vue-adapter.d.ts +36 -0
- package/dist/story-generator/framework-adapters/vue-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/vue-adapter.js +336 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.d.ts +54 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.js +387 -0
- package/dist/story-generator/generateStory.d.ts +7 -0
- package/dist/story-generator/generateStory.d.ts.map +1 -0
- package/dist/story-generator/gitignoreManager.d.ts +50 -0
- package/dist/story-generator/gitignoreManager.d.ts.map +1 -0
- package/dist/story-generator/gitignoreManager.js +7 -6
- package/dist/story-generator/imageProcessor.d.ts +80 -0
- package/dist/story-generator/imageProcessor.d.ts.map +1 -0
- package/dist/story-generator/imageProcessor.js +391 -0
- package/dist/story-generator/inMemoryStoryService.d.ts +89 -0
- package/dist/story-generator/inMemoryStoryService.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/base-provider.d.ts +36 -0
- package/dist/story-generator/llm-providers/base-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/base-provider.js +135 -0
- package/dist/story-generator/llm-providers/claude-provider.d.ts +23 -0
- package/dist/story-generator/llm-providers/claude-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/claude-provider.js +414 -0
- package/dist/story-generator/llm-providers/gemini-provider.d.ts +24 -0
- package/dist/story-generator/llm-providers/gemini-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/gemini-provider.js +406 -0
- package/dist/story-generator/llm-providers/index.d.ts +63 -0
- package/dist/story-generator/llm-providers/index.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/index.js +169 -0
- package/dist/story-generator/llm-providers/openai-provider.d.ts +24 -0
- package/dist/story-generator/llm-providers/openai-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/openai-provider.js +458 -0
- package/dist/story-generator/llm-providers/settings-manager.d.ts +75 -0
- package/dist/story-generator/llm-providers/settings-manager.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/settings-manager.js +173 -0
- package/dist/story-generator/llm-providers/story-llm-service.d.ts +79 -0
- package/dist/story-generator/llm-providers/story-llm-service.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/story-llm-service.js +240 -0
- package/dist/story-generator/llm-providers/types.d.ts +153 -0
- package/dist/story-generator/llm-providers/types.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/types.js +8 -0
- package/dist/story-generator/logger.d.ts +14 -0
- package/dist/story-generator/logger.d.ts.map +1 -0
- package/dist/story-generator/logger.js +119 -0
- package/dist/story-generator/postProcessStory.d.ts +6 -0
- package/dist/story-generator/postProcessStory.d.ts.map +1 -0
- package/dist/story-generator/postProcessStory.js +8 -7
- package/dist/story-generator/productionGitignoreManager.d.ts +91 -0
- package/dist/story-generator/productionGitignoreManager.d.ts.map +1 -0
- package/dist/story-generator/productionGitignoreManager.js +11 -10
- package/dist/story-generator/promptGenerator.d.ts +48 -0
- package/dist/story-generator/promptGenerator.d.ts.map +1 -0
- package/dist/story-generator/promptGenerator.js +186 -1
- package/dist/story-generator/storyHistory.d.ts +44 -0
- package/dist/story-generator/storyHistory.d.ts.map +1 -0
- package/dist/story-generator/storySync.d.ts +68 -0
- package/dist/story-generator/storySync.d.ts.map +1 -0
- package/dist/story-generator/storyTracker.d.ts +48 -0
- package/dist/story-generator/storyTracker.d.ts.map +1 -0
- package/dist/story-generator/storyTracker.js +2 -1
- package/dist/story-generator/storyValidator.d.ts +6 -0
- package/dist/story-generator/storyValidator.d.ts.map +1 -0
- package/dist/story-generator/universalDesignSystemAdapter.d.ts +68 -0
- package/dist/story-generator/universalDesignSystemAdapter.d.ts.map +1 -0
- package/dist/story-generator/universalDesignSystemAdapter.js +141 -3
- package/dist/story-generator/urlRedirectService.d.ts +21 -0
- package/dist/story-generator/urlRedirectService.d.ts.map +1 -0
- package/dist/story-generator/urlRedirectService.js +140 -0
- package/dist/story-generator/validateStory.d.ts +19 -0
- package/dist/story-generator/validateStory.d.ts.map +1 -0
- package/dist/story-generator/validateStory.js +6 -2
- package/dist/story-generator/visionPrompts.d.ts +88 -0
- package/dist/story-generator/visionPrompts.d.ts.map +1 -0
- package/dist/story-generator/visionPrompts.js +462 -0
- package/dist/story-ui.config.d.ts +78 -0
- package/dist/story-ui.config.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts +4 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.js +1874 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts +18 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.js +37 -0
- package/dist/templates/StoryUI/index.d.ts +3 -0
- package/dist/templates/StoryUI/index.d.ts.map +1 -0
- package/dist/templates/StoryUI/index.js +2 -0
- package/package.json +35 -4
- package/templates/StoryUI/StoryUIPanel.tsx +1973 -388
- package/templates/StoryUI/index.tsx +1 -1
- package/templates/StoryUI/manager.tsx +264 -0
- package/templates/mcp-config-claude.json +11 -0
- package/templates/mcp-example.md +76 -0
- package/templates/production-app/.env.example +11 -0
- package/templates/production-app/index.html +66 -0
- package/templates/production-app/package.json +30 -0
- package/templates/production-app/public/favicon.svg +5 -0
- package/templates/production-app/src/App.tsx +1157 -0
- package/templates/production-app/src/LivePreviewRenderer.tsx +420 -0
- package/templates/production-app/src/componentRegistry.ts +315 -0
- package/templates/production-app/src/considerations.ts +16 -0
- package/templates/production-app/src/index.css +284 -0
- package/templates/production-app/src/main.tsx +25 -0
- package/templates/production-app/tsconfig.json +32 -0
- package/templates/production-app/tsconfig.node.json +11 -0
- package/templates/production-app/vite.config.ts +83 -0
- package/templates/react-import-rule.json +2 -2
- package/dist/index.js +0 -12
- package/dist/story-ui.config.loader.js +0 -205
|
@@ -0,0 +1,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;
|