@tpitre/story-ui 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.sample +82 -11
- package/README.md +89 -0
- package/dist/cli/deploy.d.ts +17 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +696 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +26 -2
- package/dist/cli/setup.d.ts +11 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +437 -110
- package/dist/mcp-server/index.d.ts +2 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +120 -2
- package/dist/mcp-server/mcp-stdio-server.d.ts +3 -0
- package/dist/mcp-server/mcp-stdio-server.d.ts.map +1 -0
- package/dist/mcp-server/mcp-stdio-server.js +8 -1
- package/dist/mcp-server/routes/claude.d.ts +3 -0
- package/dist/mcp-server/routes/claude.d.ts.map +1 -0
- package/dist/mcp-server/routes/claude.js +60 -23
- package/dist/mcp-server/routes/components.d.ts +4 -0
- package/dist/mcp-server/routes/components.d.ts.map +1 -0
- package/dist/mcp-server/routes/frameworks.d.ts +38 -0
- package/dist/mcp-server/routes/frameworks.d.ts.map +1 -0
- package/dist/mcp-server/routes/frameworks.js +183 -0
- package/dist/mcp-server/routes/generateStory.d.ts +3 -0
- package/dist/mcp-server/routes/generateStory.d.ts.map +1 -0
- package/dist/mcp-server/routes/generateStory.js +160 -76
- package/dist/mcp-server/routes/generateStoryStream.d.ts +12 -0
- package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -0
- package/dist/mcp-server/routes/generateStoryStream.js +947 -0
- package/dist/mcp-server/routes/hybridStories.d.ts +18 -0
- package/dist/mcp-server/routes/hybridStories.d.ts.map +1 -0
- package/dist/mcp-server/routes/mcpRemote.d.ts +14 -0
- package/dist/mcp-server/routes/mcpRemote.d.ts.map +1 -0
- package/dist/mcp-server/routes/mcpRemote.js +489 -0
- package/dist/mcp-server/routes/memoryStories.d.ts +26 -0
- package/dist/mcp-server/routes/memoryStories.d.ts.map +1 -0
- package/dist/mcp-server/routes/providers.d.ts +89 -0
- package/dist/mcp-server/routes/providers.d.ts.map +1 -0
- package/dist/mcp-server/routes/providers.js +369 -0
- package/dist/mcp-server/routes/storySync.d.ts +26 -0
- package/dist/mcp-server/routes/storySync.d.ts.map +1 -0
- package/dist/mcp-server/routes/streamTypes.d.ts +110 -0
- package/dist/mcp-server/routes/streamTypes.d.ts.map +1 -0
- package/dist/mcp-server/routes/streamTypes.js +18 -0
- package/dist/mcp-server/sessionManager.d.ts +50 -0
- package/dist/mcp-server/sessionManager.d.ts.map +1 -0
- package/dist/story-generator/componentBlacklist.d.ts +21 -0
- package/dist/story-generator/componentBlacklist.d.ts.map +1 -0
- package/dist/story-generator/componentDiscovery.d.ts +28 -0
- package/dist/story-generator/componentDiscovery.d.ts.map +1 -0
- package/dist/story-generator/componentRegistryGenerator.d.ts +49 -0
- package/dist/story-generator/componentRegistryGenerator.d.ts.map +1 -0
- package/dist/story-generator/componentRegistryGenerator.js +205 -0
- package/dist/story-generator/configLoader.d.ts +33 -0
- package/dist/story-generator/configLoader.d.ts.map +1 -0
- package/dist/story-generator/considerationsLoader.d.ts +32 -0
- package/dist/story-generator/considerationsLoader.d.ts.map +1 -0
- package/dist/story-generator/documentation-sources.d.ts +28 -0
- package/dist/story-generator/documentation-sources.d.ts.map +1 -0
- package/dist/story-generator/documentationLoader.d.ts +64 -0
- package/dist/story-generator/documentationLoader.d.ts.map +1 -0
- package/dist/story-generator/dynamicPackageDiscovery.d.ts +97 -0
- package/dist/story-generator/dynamicPackageDiscovery.d.ts.map +1 -0
- package/dist/story-generator/enhancedComponentDiscovery.d.ts +125 -0
- package/dist/story-generator/enhancedComponentDiscovery.d.ts.map +1 -0
- package/dist/story-generator/enhancedComponentDiscovery.js +111 -11
- package/dist/story-generator/framework-adapters/angular-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/angular-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/angular-adapter.js +427 -0
- package/dist/story-generator/framework-adapters/base-adapter.d.ts +75 -0
- package/dist/story-generator/framework-adapters/base-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/base-adapter.js +147 -0
- package/dist/story-generator/framework-adapters/framework-detector.d.ts +55 -0
- package/dist/story-generator/framework-adapters/framework-detector.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/framework-detector.js +323 -0
- package/dist/story-generator/framework-adapters/index.d.ts +97 -0
- package/dist/story-generator/framework-adapters/index.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/index.js +198 -0
- package/dist/story-generator/framework-adapters/react-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/react-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/react-adapter.js +316 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts +40 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.js +372 -0
- package/dist/story-generator/framework-adapters/types.d.ts +182 -0
- package/dist/story-generator/framework-adapters/types.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/types.js +8 -0
- package/dist/story-generator/framework-adapters/vue-adapter.d.ts +36 -0
- package/dist/story-generator/framework-adapters/vue-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/vue-adapter.js +336 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.d.ts +54 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.d.ts.map +1 -0
- package/dist/story-generator/framework-adapters/web-components-adapter.js +387 -0
- package/dist/story-generator/generateStory.d.ts +7 -0
- package/dist/story-generator/generateStory.d.ts.map +1 -0
- package/dist/story-generator/gitignoreManager.d.ts +50 -0
- package/dist/story-generator/gitignoreManager.d.ts.map +1 -0
- package/dist/story-generator/imageProcessor.d.ts +80 -0
- package/dist/story-generator/imageProcessor.d.ts.map +1 -0
- package/dist/story-generator/imageProcessor.js +391 -0
- package/dist/story-generator/inMemoryStoryService.d.ts +89 -0
- package/dist/story-generator/inMemoryStoryService.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/base-provider.d.ts +36 -0
- package/dist/story-generator/llm-providers/base-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/base-provider.js +135 -0
- package/dist/story-generator/llm-providers/claude-provider.d.ts +23 -0
- package/dist/story-generator/llm-providers/claude-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/claude-provider.js +414 -0
- package/dist/story-generator/llm-providers/gemini-provider.d.ts +24 -0
- package/dist/story-generator/llm-providers/gemini-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/gemini-provider.js +406 -0
- package/dist/story-generator/llm-providers/index.d.ts +63 -0
- package/dist/story-generator/llm-providers/index.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/index.js +169 -0
- package/dist/story-generator/llm-providers/openai-provider.d.ts +24 -0
- package/dist/story-generator/llm-providers/openai-provider.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/openai-provider.js +458 -0
- package/dist/story-generator/llm-providers/settings-manager.d.ts +75 -0
- package/dist/story-generator/llm-providers/settings-manager.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/settings-manager.js +173 -0
- package/dist/story-generator/llm-providers/story-llm-service.d.ts +79 -0
- package/dist/story-generator/llm-providers/story-llm-service.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/story-llm-service.js +240 -0
- package/dist/story-generator/llm-providers/types.d.ts +153 -0
- package/dist/story-generator/llm-providers/types.d.ts.map +1 -0
- package/dist/story-generator/llm-providers/types.js +8 -0
- package/dist/story-generator/logger.d.ts +14 -0
- package/dist/story-generator/logger.d.ts.map +1 -0
- package/dist/story-generator/logger.js +96 -29
- package/dist/story-generator/postProcessStory.d.ts +6 -0
- package/dist/story-generator/postProcessStory.d.ts.map +1 -0
- package/dist/story-generator/productionGitignoreManager.d.ts +91 -0
- package/dist/story-generator/productionGitignoreManager.d.ts.map +1 -0
- package/dist/story-generator/promptGenerator.d.ts +48 -0
- package/dist/story-generator/promptGenerator.d.ts.map +1 -0
- package/dist/story-generator/promptGenerator.js +186 -1
- package/dist/story-generator/storyHistory.d.ts +44 -0
- package/dist/story-generator/storyHistory.d.ts.map +1 -0
- package/dist/story-generator/storySync.d.ts +68 -0
- package/dist/story-generator/storySync.d.ts.map +1 -0
- package/dist/story-generator/storyTracker.d.ts +48 -0
- package/dist/story-generator/storyTracker.d.ts.map +1 -0
- package/dist/story-generator/storyValidator.d.ts +6 -0
- package/dist/story-generator/storyValidator.d.ts.map +1 -0
- package/dist/story-generator/universalDesignSystemAdapter.d.ts +68 -0
- package/dist/story-generator/universalDesignSystemAdapter.d.ts.map +1 -0
- package/dist/story-generator/universalDesignSystemAdapter.js +138 -1
- package/dist/story-generator/urlRedirectService.d.ts +21 -0
- package/dist/story-generator/urlRedirectService.d.ts.map +1 -0
- package/dist/story-generator/validateStory.d.ts +19 -0
- package/dist/story-generator/validateStory.d.ts.map +1 -0
- package/dist/story-generator/validateStory.js +6 -2
- package/dist/story-generator/visionPrompts.d.ts +88 -0
- package/dist/story-generator/visionPrompts.d.ts.map +1 -0
- package/dist/story-generator/visionPrompts.js +462 -0
- package/dist/story-ui.config.d.ts +78 -0
- package/dist/story-ui.config.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts +4 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.js +1874 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts +18 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts.map +1 -0
- package/dist/templates/StoryUI/StoryUIPanel.stories.js +37 -0
- package/dist/templates/StoryUI/index.d.ts +3 -0
- package/dist/templates/StoryUI/index.d.ts.map +1 -0
- package/dist/templates/StoryUI/index.js +2 -0
- package/package.json +17 -3
- package/templates/StoryUI/StoryUIPanel.tsx +1960 -384
- package/templates/StoryUI/index.tsx +1 -1
- package/templates/StoryUI/manager.tsx +264 -0
- package/templates/production-app/.env.example +11 -0
- package/templates/production-app/index.html +66 -0
- package/templates/production-app/package.json +30 -0
- package/templates/production-app/public/favicon.svg +5 -0
- package/templates/production-app/src/App.tsx +1560 -0
- package/templates/production-app/src/LivePreviewRenderer.tsx +420 -0
- package/templates/production-app/src/componentRegistry.ts +315 -0
- package/templates/production-app/src/considerations.ts +16 -0
- package/templates/production-app/src/index.css +284 -0
- package/templates/production-app/src/main.tsx +25 -0
- package/templates/production-app/tsconfig.json +32 -0
- package/templates/production-app/tsconfig.node.json +11 -0
- package/templates/production-app/vite.config.ts +83 -0
- package/templates/react-import-rule.json +2 -2
- package/dist/index.js +0 -12
- package/dist/story-ui.config.loader.js +0 -205
|
@@ -0,0 +1,1560 @@
|
|
|
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
|
+
interface ProviderOption {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
isDefault?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ModelOption {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
provider?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ServerConfig {
|
|
54
|
+
providers: ProviderOption[];
|
|
55
|
+
currentProvider: string;
|
|
56
|
+
models: ModelOption[];
|
|
57
|
+
currentModel: string;
|
|
58
|
+
isConfigured: boolean;
|
|
59
|
+
loading: boolean;
|
|
60
|
+
error: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// CONSTANTS & CONFIG
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
const getServerUrl = (): string => {
|
|
68
|
+
if (import.meta.env.VITE_STORY_UI_SERVER) {
|
|
69
|
+
return import.meta.env.VITE_STORY_UI_SERVER;
|
|
70
|
+
}
|
|
71
|
+
return window.location.origin;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const SERVER_URL = getServerUrl();
|
|
75
|
+
|
|
76
|
+
const THEME = {
|
|
77
|
+
bg: '#09090b',
|
|
78
|
+
bgSurface: '#18181b',
|
|
79
|
+
bgElevated: '#27272a',
|
|
80
|
+
bgHover: '#3f3f46',
|
|
81
|
+
border: '#27272a',
|
|
82
|
+
borderSubtle: '#3f3f46',
|
|
83
|
+
text: '#fafafa',
|
|
84
|
+
textMuted: '#a1a1aa',
|
|
85
|
+
textSubtle: '#71717a',
|
|
86
|
+
accent: '#3b82f6',
|
|
87
|
+
accentHover: '#2563eb',
|
|
88
|
+
accentMuted: 'rgba(59, 130, 246, 0.15)',
|
|
89
|
+
success: '#22c55e',
|
|
90
|
+
error: '#ef4444',
|
|
91
|
+
warning: '#f59e0b',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const generateId = () => Math.random().toString(36).substring(2, 15);
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// HOOKS
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
const useLocalStorage = <T,>(key: string, initialValue: T): [T, React.Dispatch<React.SetStateAction<T>>] => {
|
|
101
|
+
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
102
|
+
try {
|
|
103
|
+
const item = window.localStorage.getItem(key);
|
|
104
|
+
return item ? JSON.parse(item) : initialValue;
|
|
105
|
+
} catch {
|
|
106
|
+
return initialValue;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Use a ref to always have access to the latest value for functional updates
|
|
111
|
+
const storedValueRef = useRef(storedValue);
|
|
112
|
+
storedValueRef.current = storedValue;
|
|
113
|
+
|
|
114
|
+
const setValue: React.Dispatch<React.SetStateAction<T>> = useCallback((value) => {
|
|
115
|
+
const valueToStore = value instanceof Function ? value(storedValueRef.current) : value;
|
|
116
|
+
storedValueRef.current = valueToStore;
|
|
117
|
+
setStoredValue(valueToStore);
|
|
118
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
119
|
+
}, [key]);
|
|
120
|
+
|
|
121
|
+
return [storedValue, setValue];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const useResizable = (initialWidth: number, minWidth: number, maxWidth: number) => {
|
|
125
|
+
const [width, setWidth] = useState(initialWidth);
|
|
126
|
+
const isDragging = useRef(false);
|
|
127
|
+
const startX = useRef(0);
|
|
128
|
+
const startWidth = useRef(0);
|
|
129
|
+
|
|
130
|
+
const startResize = useCallback((e: React.MouseEvent) => {
|
|
131
|
+
isDragging.current = true;
|
|
132
|
+
startX.current = e.clientX;
|
|
133
|
+
startWidth.current = width;
|
|
134
|
+
document.body.style.cursor = 'col-resize';
|
|
135
|
+
document.body.style.userSelect = 'none';
|
|
136
|
+
}, [width]);
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
140
|
+
if (!isDragging.current) return;
|
|
141
|
+
const delta = e.clientX - startX.current;
|
|
142
|
+
const newWidth = Math.min(Math.max(startWidth.current + delta, minWidth), maxWidth);
|
|
143
|
+
setWidth(newWidth);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleMouseUp = () => {
|
|
147
|
+
isDragging.current = false;
|
|
148
|
+
document.body.style.cursor = '';
|
|
149
|
+
document.body.style.userSelect = '';
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
153
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
154
|
+
return () => {
|
|
155
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
156
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
157
|
+
};
|
|
158
|
+
}, [minWidth, maxWidth]);
|
|
159
|
+
|
|
160
|
+
return { width, startResize };
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Default models for fallback
|
|
164
|
+
const DEFAULT_MODELS: Record<string, ModelOption[]> = {
|
|
165
|
+
claude: [
|
|
166
|
+
{ id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5', provider: 'claude' },
|
|
167
|
+
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'claude' },
|
|
168
|
+
{ id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', provider: 'claude' },
|
|
169
|
+
],
|
|
170
|
+
openai: [
|
|
171
|
+
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' },
|
|
172
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', provider: 'openai' },
|
|
173
|
+
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'openai' },
|
|
174
|
+
],
|
|
175
|
+
gemini: [
|
|
176
|
+
{ id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', provider: 'gemini' },
|
|
177
|
+
{ id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', provider: 'gemini' },
|
|
178
|
+
{ id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', provider: 'gemini' },
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const useServerConfig = () => {
|
|
183
|
+
const [config, setConfig] = useState<ServerConfig>({
|
|
184
|
+
providers: [],
|
|
185
|
+
currentProvider: 'claude',
|
|
186
|
+
models: DEFAULT_MODELS.claude,
|
|
187
|
+
currentModel: DEFAULT_MODELS.claude[0].id,
|
|
188
|
+
isConfigured: false,
|
|
189
|
+
loading: true,
|
|
190
|
+
error: null,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const fetchConfig = useCallback(async () => {
|
|
194
|
+
setConfig(prev => ({ ...prev, loading: true, error: null }));
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch(`${SERVER_URL}/story-ui/providers`);
|
|
197
|
+
if (response.ok) {
|
|
198
|
+
const data = await response.json();
|
|
199
|
+
const providers = data.providers || [];
|
|
200
|
+
const defaultProvider = providers.find((p: ProviderOption) => p.isDefault)?.id || providers[0]?.id || 'claude';
|
|
201
|
+
const models = DEFAULT_MODELS[defaultProvider] || DEFAULT_MODELS.claude;
|
|
202
|
+
setConfig({
|
|
203
|
+
providers,
|
|
204
|
+
currentProvider: defaultProvider,
|
|
205
|
+
models,
|
|
206
|
+
currentModel: models[0]?.id || '',
|
|
207
|
+
isConfigured: true,
|
|
208
|
+
loading: false,
|
|
209
|
+
error: null,
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
throw new Error('Failed to fetch providers');
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
// Fallback to default Claude config
|
|
216
|
+
setConfig({
|
|
217
|
+
providers: [{ id: 'claude', name: 'Claude (Anthropic)', isDefault: true }],
|
|
218
|
+
currentProvider: 'claude',
|
|
219
|
+
models: DEFAULT_MODELS.claude,
|
|
220
|
+
currentModel: DEFAULT_MODELS.claude[0].id,
|
|
221
|
+
isConfigured: true,
|
|
222
|
+
loading: false,
|
|
223
|
+
error: null,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}, []);
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
fetchConfig();
|
|
230
|
+
}, [fetchConfig]);
|
|
231
|
+
|
|
232
|
+
const changeProvider = useCallback((providerId: string) => {
|
|
233
|
+
const models = DEFAULT_MODELS[providerId] || DEFAULT_MODELS.claude;
|
|
234
|
+
setConfig(prev => ({
|
|
235
|
+
...prev,
|
|
236
|
+
currentProvider: providerId,
|
|
237
|
+
models,
|
|
238
|
+
currentModel: models[0]?.id || '',
|
|
239
|
+
}));
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
const changeModel = useCallback((modelId: string) => {
|
|
243
|
+
setConfig(prev => ({
|
|
244
|
+
...prev,
|
|
245
|
+
currentModel: modelId,
|
|
246
|
+
}));
|
|
247
|
+
}, []);
|
|
248
|
+
|
|
249
|
+
return { ...config, refetch: fetchConfig, changeProvider, changeModel };
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// ICON COMPONENTS
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
const Icons = {
|
|
257
|
+
Plus: () => (
|
|
258
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2">
|
|
259
|
+
<path d="M8 3v10M3 8h10" />
|
|
260
|
+
</svg>
|
|
261
|
+
),
|
|
262
|
+
Trash: () => (
|
|
263
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
264
|
+
<path d="M3 4h10M6 4V3a1 1 0 011-1h2a1 1 0 011 1v1M5 4v9a1 1 0 001 1h4a1 1 0 001-1V4" />
|
|
265
|
+
</svg>
|
|
266
|
+
),
|
|
267
|
+
Send: () => (
|
|
268
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
269
|
+
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" />
|
|
270
|
+
</svg>
|
|
271
|
+
),
|
|
272
|
+
Image: () => (
|
|
273
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
274
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
275
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
276
|
+
<path d="M21 15l-5-5L5 21" />
|
|
277
|
+
</svg>
|
|
278
|
+
),
|
|
279
|
+
Code: () => (
|
|
280
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
281
|
+
<path d="M16 18l6-6-6-6M8 6l-6 6 6 6" />
|
|
282
|
+
</svg>
|
|
283
|
+
),
|
|
284
|
+
Eye: () => (
|
|
285
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
286
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
287
|
+
<circle cx="12" cy="12" r="3" />
|
|
288
|
+
</svg>
|
|
289
|
+
),
|
|
290
|
+
Copy: () => (
|
|
291
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
292
|
+
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
293
|
+
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
|
294
|
+
</svg>
|
|
295
|
+
),
|
|
296
|
+
Sidebar: () => (
|
|
297
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
298
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
299
|
+
<path d="M9 3v18" />
|
|
300
|
+
</svg>
|
|
301
|
+
),
|
|
302
|
+
Check: () => (
|
|
303
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
304
|
+
<path d="M20 6L9 17l-5-5" />
|
|
305
|
+
</svg>
|
|
306
|
+
),
|
|
307
|
+
X: () => (
|
|
308
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
309
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
310
|
+
</svg>
|
|
311
|
+
),
|
|
312
|
+
ChevronDown: () => (
|
|
313
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
314
|
+
<path d="M6 9l6 6 6-6" />
|
|
315
|
+
</svg>
|
|
316
|
+
),
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// SUB-COMPONENTS
|
|
321
|
+
// ============================================================================
|
|
322
|
+
|
|
323
|
+
// Dropdown component for provider/model selection
|
|
324
|
+
const Dropdown: React.FC<{
|
|
325
|
+
label: string;
|
|
326
|
+
value: string;
|
|
327
|
+
options: { id: string; name: string }[];
|
|
328
|
+
onChange: (value: string) => void;
|
|
329
|
+
disabled?: boolean;
|
|
330
|
+
}> = ({ label, value, options, onChange, disabled }) => {
|
|
331
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
332
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
333
|
+
const selectedOption = options.find(o => o.id === value);
|
|
334
|
+
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
337
|
+
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
338
|
+
setIsOpen(false);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
342
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
343
|
+
}, []);
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div ref={ref} style={{ position: 'relative', width: '100%' }}>
|
|
347
|
+
<button
|
|
348
|
+
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
349
|
+
disabled={disabled}
|
|
350
|
+
style={{
|
|
351
|
+
display: 'flex',
|
|
352
|
+
alignItems: 'center',
|
|
353
|
+
justifyContent: 'space-between',
|
|
354
|
+
width: '100%',
|
|
355
|
+
padding: '8px 10px',
|
|
356
|
+
background: THEME.bgElevated,
|
|
357
|
+
border: `1px solid ${THEME.border}`,
|
|
358
|
+
borderRadius: '6px',
|
|
359
|
+
color: disabled ? THEME.textSubtle : THEME.text,
|
|
360
|
+
fontSize: '12px',
|
|
361
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
362
|
+
transition: 'all 0.2s',
|
|
363
|
+
opacity: disabled ? 0.6 : 1,
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '2px' }}>
|
|
367
|
+
<span style={{ color: THEME.textSubtle, fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
|
368
|
+
{label}
|
|
369
|
+
</span>
|
|
370
|
+
<span style={{ fontWeight: 500 }}>{selectedOption?.name || 'Select'}</span>
|
|
371
|
+
</div>
|
|
372
|
+
<Icons.ChevronDown />
|
|
373
|
+
</button>
|
|
374
|
+
{isOpen && !disabled && (
|
|
375
|
+
<div
|
|
376
|
+
style={{
|
|
377
|
+
position: 'absolute',
|
|
378
|
+
bottom: '100%',
|
|
379
|
+
left: 0,
|
|
380
|
+
right: 0,
|
|
381
|
+
marginBottom: '4px',
|
|
382
|
+
maxHeight: '200px',
|
|
383
|
+
overflowY: 'auto',
|
|
384
|
+
background: THEME.bgElevated,
|
|
385
|
+
border: `1px solid ${THEME.border}`,
|
|
386
|
+
borderRadius: '6px',
|
|
387
|
+
boxShadow: '0 -4px 12px rgba(0,0,0,0.3)',
|
|
388
|
+
zIndex: 100,
|
|
389
|
+
}}
|
|
390
|
+
>
|
|
391
|
+
{options.map(option => (
|
|
392
|
+
<button
|
|
393
|
+
key={option.id}
|
|
394
|
+
onClick={() => {
|
|
395
|
+
onChange(option.id);
|
|
396
|
+
setIsOpen(false);
|
|
397
|
+
}}
|
|
398
|
+
style={{
|
|
399
|
+
display: 'block',
|
|
400
|
+
width: '100%',
|
|
401
|
+
padding: '8px 10px',
|
|
402
|
+
background: option.id === value ? THEME.accentMuted : 'transparent',
|
|
403
|
+
border: 'none',
|
|
404
|
+
textAlign: 'left',
|
|
405
|
+
color: THEME.text,
|
|
406
|
+
fontSize: '12px',
|
|
407
|
+
cursor: 'pointer',
|
|
408
|
+
transition: 'background 0.15s',
|
|
409
|
+
}}
|
|
410
|
+
onMouseEnter={e => {
|
|
411
|
+
if (option.id !== value) e.currentTarget.style.background = THEME.bgHover;
|
|
412
|
+
}}
|
|
413
|
+
onMouseLeave={e => {
|
|
414
|
+
e.currentTarget.style.background = option.id === value ? THEME.accentMuted : 'transparent';
|
|
415
|
+
}}
|
|
416
|
+
>
|
|
417
|
+
{option.name}
|
|
418
|
+
</button>
|
|
419
|
+
))}
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const LoadingDots: React.FC = () => (
|
|
427
|
+
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
|
428
|
+
{[0, 1, 2].map(i => (
|
|
429
|
+
<div
|
|
430
|
+
key={i}
|
|
431
|
+
style={{
|
|
432
|
+
width: '6px',
|
|
433
|
+
height: '6px',
|
|
434
|
+
borderRadius: '50%',
|
|
435
|
+
background: THEME.accent,
|
|
436
|
+
animation: `pulse 1.4s ease-in-out ${i * 0.2}s infinite`,
|
|
437
|
+
}}
|
|
438
|
+
/>
|
|
439
|
+
))}
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const ImageUploadArea: React.FC<{
|
|
444
|
+
images: ImageAttachment[];
|
|
445
|
+
onImagesChange: (images: ImageAttachment[]) => void;
|
|
446
|
+
disabled?: boolean;
|
|
447
|
+
}> = ({ images, onImagesChange, disabled }) => {
|
|
448
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
449
|
+
|
|
450
|
+
const handleFiles = useCallback((files: FileList) => {
|
|
451
|
+
Array.from(files).forEach(file => {
|
|
452
|
+
if (file.type.startsWith('image/')) {
|
|
453
|
+
const reader = new FileReader();
|
|
454
|
+
reader.onload = (e) => {
|
|
455
|
+
const newImage: ImageAttachment = {
|
|
456
|
+
id: generateId(),
|
|
457
|
+
data: e.target?.result as string,
|
|
458
|
+
type: file.type,
|
|
459
|
+
name: file.name,
|
|
460
|
+
};
|
|
461
|
+
onImagesChange([...images, newImage]);
|
|
462
|
+
};
|
|
463
|
+
reader.readAsDataURL(file);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}, [images, onImagesChange]);
|
|
467
|
+
|
|
468
|
+
const removeImage = (id: string) => {
|
|
469
|
+
onImagesChange(images.filter(img => img.id !== id));
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
return (
|
|
473
|
+
<div style={{ marginBottom: images.length > 0 ? '12px' : 0 }}>
|
|
474
|
+
<input
|
|
475
|
+
ref={fileInputRef}
|
|
476
|
+
type="file"
|
|
477
|
+
accept="image/*"
|
|
478
|
+
multiple
|
|
479
|
+
style={{ display: 'none' }}
|
|
480
|
+
onChange={(e) => e.target.files && handleFiles(e.target.files)}
|
|
481
|
+
disabled={disabled}
|
|
482
|
+
/>
|
|
483
|
+
|
|
484
|
+
{images.length > 0 && (
|
|
485
|
+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '8px' }}>
|
|
486
|
+
{images.map(img => (
|
|
487
|
+
<div
|
|
488
|
+
key={img.id}
|
|
489
|
+
style={{
|
|
490
|
+
position: 'relative',
|
|
491
|
+
width: '64px',
|
|
492
|
+
height: '64px',
|
|
493
|
+
borderRadius: '8px',
|
|
494
|
+
overflow: 'hidden',
|
|
495
|
+
border: `1px solid ${THEME.border}`,
|
|
496
|
+
}}
|
|
497
|
+
>
|
|
498
|
+
<img
|
|
499
|
+
src={img.data}
|
|
500
|
+
alt={img.name}
|
|
501
|
+
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
502
|
+
/>
|
|
503
|
+
<button
|
|
504
|
+
onClick={() => removeImage(img.id)}
|
|
505
|
+
style={{
|
|
506
|
+
position: 'absolute',
|
|
507
|
+
top: '2px',
|
|
508
|
+
right: '2px',
|
|
509
|
+
width: '18px',
|
|
510
|
+
height: '18px',
|
|
511
|
+
borderRadius: '50%',
|
|
512
|
+
background: 'rgba(0,0,0,0.7)',
|
|
513
|
+
border: 'none',
|
|
514
|
+
color: '#fff',
|
|
515
|
+
cursor: 'pointer',
|
|
516
|
+
display: 'flex',
|
|
517
|
+
alignItems: 'center',
|
|
518
|
+
justifyContent: 'center',
|
|
519
|
+
fontSize: '10px',
|
|
520
|
+
}}
|
|
521
|
+
>
|
|
522
|
+
<Icons.X />
|
|
523
|
+
</button>
|
|
524
|
+
</div>
|
|
525
|
+
))}
|
|
526
|
+
</div>
|
|
527
|
+
)}
|
|
528
|
+
|
|
529
|
+
<button
|
|
530
|
+
onClick={() => fileInputRef.current?.click()}
|
|
531
|
+
disabled={disabled}
|
|
532
|
+
style={{
|
|
533
|
+
display: 'flex',
|
|
534
|
+
alignItems: 'center',
|
|
535
|
+
gap: '6px',
|
|
536
|
+
padding: '6px 10px',
|
|
537
|
+
background: 'transparent',
|
|
538
|
+
border: `1px dashed ${THEME.border}`,
|
|
539
|
+
borderRadius: '6px',
|
|
540
|
+
color: THEME.textMuted,
|
|
541
|
+
fontSize: '12px',
|
|
542
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
543
|
+
transition: 'all 0.2s',
|
|
544
|
+
opacity: disabled ? 0.5 : 1,
|
|
545
|
+
}}
|
|
546
|
+
>
|
|
547
|
+
<Icons.Image />
|
|
548
|
+
<span>Add image</span>
|
|
549
|
+
</button>
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const CodeViewer: React.FC<{ code: string }> = ({ code }) => {
|
|
555
|
+
const [copied, setCopied] = useState(false);
|
|
556
|
+
|
|
557
|
+
const handleCopy = async () => {
|
|
558
|
+
await navigator.clipboard.writeText(code);
|
|
559
|
+
setCopied(true);
|
|
560
|
+
setTimeout(() => setCopied(false), 2000);
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
return (
|
|
564
|
+
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
565
|
+
<div style={{
|
|
566
|
+
padding: '8px 12px',
|
|
567
|
+
borderBottom: `1px solid ${THEME.border}`,
|
|
568
|
+
display: 'flex',
|
|
569
|
+
justifyContent: 'space-between',
|
|
570
|
+
alignItems: 'center',
|
|
571
|
+
}}>
|
|
572
|
+
<span style={{ fontSize: '12px', color: THEME.textMuted }}>Generated Code</span>
|
|
573
|
+
<button
|
|
574
|
+
onClick={handleCopy}
|
|
575
|
+
style={{
|
|
576
|
+
display: 'flex',
|
|
577
|
+
alignItems: 'center',
|
|
578
|
+
gap: '4px',
|
|
579
|
+
padding: '4px 8px',
|
|
580
|
+
background: copied ? 'rgba(34, 197, 94, 0.2)' : 'transparent',
|
|
581
|
+
border: `1px solid ${copied ? THEME.success : THEME.border}`,
|
|
582
|
+
borderRadius: '4px',
|
|
583
|
+
color: copied ? THEME.success : THEME.textMuted,
|
|
584
|
+
fontSize: '11px',
|
|
585
|
+
cursor: 'pointer',
|
|
586
|
+
transition: 'all 0.2s',
|
|
587
|
+
}}
|
|
588
|
+
>
|
|
589
|
+
{copied ? <Icons.Check /> : <Icons.Copy />}
|
|
590
|
+
{copied ? 'Copied!' : 'Copy'}
|
|
591
|
+
</button>
|
|
592
|
+
</div>
|
|
593
|
+
<pre
|
|
594
|
+
style={{
|
|
595
|
+
flex: 1,
|
|
596
|
+
margin: 0,
|
|
597
|
+
padding: '16px',
|
|
598
|
+
overflow: 'auto',
|
|
599
|
+
fontSize: '13px',
|
|
600
|
+
lineHeight: 1.6,
|
|
601
|
+
fontFamily: '"Fira Code", "SF Mono", Monaco, monospace',
|
|
602
|
+
background: '#0d1117',
|
|
603
|
+
color: '#e6edf3',
|
|
604
|
+
}}
|
|
605
|
+
>
|
|
606
|
+
{code}
|
|
607
|
+
</pre>
|
|
608
|
+
</div>
|
|
609
|
+
);
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// MAIN APP
|
|
614
|
+
// ============================================================================
|
|
615
|
+
|
|
616
|
+
const App: React.FC = () => {
|
|
617
|
+
// Server configuration (providers, models)
|
|
618
|
+
const serverConfig = useServerConfig();
|
|
619
|
+
|
|
620
|
+
const [conversations, setConversations] = useLocalStorage<Conversation[]>('storyui_conversations', []);
|
|
621
|
+
const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
|
|
622
|
+
const [inputValue, setInputValue] = useState('');
|
|
623
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
624
|
+
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
|
625
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
626
|
+
const [previewTab, setPreviewTab] = useState<'preview' | 'code'>('preview');
|
|
627
|
+
const [images, setImages] = useState<ImageAttachment[]>([]);
|
|
628
|
+
|
|
629
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
630
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
631
|
+
const { width: chatWidth, startResize } = useResizable(400, 320, 600);
|
|
632
|
+
|
|
633
|
+
const activeConversation = conversations.find(c => c.id === activeConversationId);
|
|
634
|
+
|
|
635
|
+
// Filter out empty conversations for display (deferred creation)
|
|
636
|
+
const displayConversations = conversations.filter(c => c.messages.length > 0);
|
|
637
|
+
|
|
638
|
+
// Auto-scroll messages
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
641
|
+
}, [activeConversation?.messages]);
|
|
642
|
+
|
|
643
|
+
// Initialize - select existing conversation or stay in "new chat" mode
|
|
644
|
+
useEffect(() => {
|
|
645
|
+
// Only select an existing conversation if we don't have one selected
|
|
646
|
+
// and there are conversations with messages
|
|
647
|
+
if (!activeConversationId && displayConversations.length > 0) {
|
|
648
|
+
setActiveConversationId(displayConversations[0].id);
|
|
649
|
+
const lastMsgWithCode = [...displayConversations[0].messages].reverse().find(m => m.generatedCode);
|
|
650
|
+
setPreviewCode(lastMsgWithCode?.generatedCode || null);
|
|
651
|
+
}
|
|
652
|
+
// Don't create an empty conversation - we'll create one when user sends first message
|
|
653
|
+
}, [displayConversations.length, activeConversationId]);
|
|
654
|
+
|
|
655
|
+
// Start a new chat - just clear state, don't create empty conversation
|
|
656
|
+
const startNewChat = useCallback(() => {
|
|
657
|
+
setActiveConversationId(null);
|
|
658
|
+
setPreviewCode(null);
|
|
659
|
+
setImages([]);
|
|
660
|
+
inputRef.current?.focus();
|
|
661
|
+
}, []);
|
|
662
|
+
|
|
663
|
+
// Actually create a conversation when first message is sent
|
|
664
|
+
const createConversationWithMessage = useCallback((message: Message): string => {
|
|
665
|
+
const newConversation: Conversation = {
|
|
666
|
+
id: generateId(),
|
|
667
|
+
title: message.content.substring(0, 40),
|
|
668
|
+
messages: [message],
|
|
669
|
+
createdAt: Date.now(),
|
|
670
|
+
updatedAt: Date.now(),
|
|
671
|
+
};
|
|
672
|
+
setConversations(prev => [newConversation, ...prev]);
|
|
673
|
+
return newConversation.id;
|
|
674
|
+
}, [setConversations]);
|
|
675
|
+
|
|
676
|
+
const deleteConversation = (id: string) => {
|
|
677
|
+
setConversations(prev => prev.filter(c => c.id !== id));
|
|
678
|
+
if (activeConversationId === id) {
|
|
679
|
+
const remaining = displayConversations.filter(c => c.id !== id);
|
|
680
|
+
if (remaining.length > 0) {
|
|
681
|
+
setActiveConversationId(remaining[0].id);
|
|
682
|
+
} else {
|
|
683
|
+
// Go to "new chat" mode instead of creating empty conversation
|
|
684
|
+
setActiveConversationId(null);
|
|
685
|
+
setPreviewCode(null);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const generateComponent = async (prompt: string, imageAttachments: ImageAttachment[]): Promise<string> => {
|
|
691
|
+
// Get the last generated code from conversation for iteration context
|
|
692
|
+
const lastGeneratedCode = activeConversation?.messages
|
|
693
|
+
.filter(m => m.generatedCode)
|
|
694
|
+
.slice(-1)[0]?.generatedCode;
|
|
695
|
+
|
|
696
|
+
// Check if this is an iteration (modification of existing code)
|
|
697
|
+
const isIteration = !!lastGeneratedCode && (activeConversation?.messages.length || 0) > 0;
|
|
698
|
+
|
|
699
|
+
// Build conversation history, but for iterations include the code reference differently
|
|
700
|
+
const conversationHistory = isIteration
|
|
701
|
+
? activeConversation?.messages.slice(0, -1).map(msg => ({
|
|
702
|
+
role: msg.role,
|
|
703
|
+
content: msg.role === 'assistant' && msg.generatedCode
|
|
704
|
+
? `[Generated JSX component - see CURRENT_CODE below]`
|
|
705
|
+
: msg.content
|
|
706
|
+
})) || []
|
|
707
|
+
: [];
|
|
708
|
+
|
|
709
|
+
// Build a UNIVERSAL system prompt that works with ANY component library
|
|
710
|
+
// Design-system-specific rules come from aiConsiderations (generated from story-ui-considerations.md)
|
|
711
|
+
const basePrompt = `You are a JSX code generator. Your ONLY job is to output raw JSX code.
|
|
712
|
+
|
|
713
|
+
CRITICAL OUTPUT RULES:
|
|
714
|
+
1. Start DIRECTLY with < (opening tag of a component)
|
|
715
|
+
2. End with > (closing tag)
|
|
716
|
+
3. NO markdown, NO headers, NO explanations, NO code fences
|
|
717
|
+
4. NO internal tags like <budget>, <usage>, <thinking>, or any metadata
|
|
718
|
+
5. NEVER output anything except JSX components
|
|
719
|
+
|
|
720
|
+
UNIVERSAL BEST PRACTICES (applies to ALL design systems):
|
|
721
|
+
|
|
722
|
+
THEME & COLORS:
|
|
723
|
+
- Components render on a LIGHT BACKGROUND by default
|
|
724
|
+
- Use DARK text colors for body text (ensure readability)
|
|
725
|
+
- Never use white/light text colors unless on a dark or colored background
|
|
726
|
+
- Ensure sufficient color contrast (4.5:1 for normal text, 3:1 for large text)
|
|
727
|
+
|
|
728
|
+
ACCESSIBILITY (WCAG):
|
|
729
|
+
- Use semantic HTML structure (headings, lists, landmarks)
|
|
730
|
+
- Include aria-labels on interactive elements without visible text
|
|
731
|
+
- Ensure focusable elements have visible focus states
|
|
732
|
+
- Use role attributes appropriately
|
|
733
|
+
- Form inputs should have associated labels
|
|
734
|
+
- Interactive elements should be keyboard accessible
|
|
735
|
+
|
|
736
|
+
RESPONSIVE DESIGN:
|
|
737
|
+
- Components should work at various viewport sizes
|
|
738
|
+
- Use relative units and flexible layouts
|
|
739
|
+
- Avoid fixed pixel widths that could cause overflow
|
|
740
|
+
|
|
741
|
+
AVAILABLE COMPONENTS (use ONLY these):
|
|
742
|
+
${availableComponents.join(', ')}${hasConsiderations ? `
|
|
743
|
+
|
|
744
|
+
DESIGN SYSTEM GUIDELINES:
|
|
745
|
+
${aiConsiderations}` : ''}`;
|
|
746
|
+
|
|
747
|
+
let systemPrompt: string;
|
|
748
|
+
|
|
749
|
+
if (isIteration && lastGeneratedCode) {
|
|
750
|
+
// Iteration-specific prompt with clear modification instructions
|
|
751
|
+
systemPrompt = `${basePrompt}
|
|
752
|
+
|
|
753
|
+
ITERATION MODE - You are MODIFYING existing code:
|
|
754
|
+
|
|
755
|
+
CURRENT_CODE (this is what you're modifying):
|
|
756
|
+
${lastGeneratedCode}
|
|
757
|
+
|
|
758
|
+
MODIFICATION RULES:
|
|
759
|
+
1. Keep the overall structure unless asked to change it
|
|
760
|
+
2. Only modify what the user specifically requests
|
|
761
|
+
3. Preserve existing styling, layout, and components not mentioned
|
|
762
|
+
4. Output the COMPLETE modified JSX (not just the changed parts)
|
|
763
|
+
|
|
764
|
+
OUTPUT: Start immediately with < and output only the complete modified JSX.`;
|
|
765
|
+
} else {
|
|
766
|
+
// New generation prompt
|
|
767
|
+
systemPrompt = `${basePrompt}
|
|
768
|
+
|
|
769
|
+
GENERATION RULES:
|
|
770
|
+
1. Output a SINGLE JSX expression starting with < and ending with >
|
|
771
|
+
2. Use ONLY components from the list above
|
|
772
|
+
3. NO imports, NO exports, NO function definitions
|
|
773
|
+
4. NO explanations, NO comments outside JSX
|
|
774
|
+
|
|
775
|
+
OUTPUT: Start immediately with < and output only JSX.`;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Use assistant prefill to force Claude to start with JSX
|
|
779
|
+
// This is a powerful technique that constrains the output format
|
|
780
|
+
const prefillAssistant = '<';
|
|
781
|
+
|
|
782
|
+
const response = await fetch(`${SERVER_URL}/story-ui/claude`, {
|
|
783
|
+
method: 'POST',
|
|
784
|
+
headers: { 'Content-Type': 'application/json' },
|
|
785
|
+
body: JSON.stringify({
|
|
786
|
+
prompt,
|
|
787
|
+
messages: conversationHistory,
|
|
788
|
+
systemPrompt,
|
|
789
|
+
prefillAssistant,
|
|
790
|
+
model: serverConfig.currentModel,
|
|
791
|
+
maxTokens: 4096,
|
|
792
|
+
images: imageAttachments.map(img => ({
|
|
793
|
+
type: img.type,
|
|
794
|
+
data: img.data.split(',')[1],
|
|
795
|
+
})),
|
|
796
|
+
}),
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
if (!response.ok) {
|
|
800
|
+
const errorData = await response.json().catch(() => ({}));
|
|
801
|
+
throw new Error(errorData.error || `Server Error: ${response.status}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const data = await response.json();
|
|
805
|
+
// Handle various response formats from different LLM providers
|
|
806
|
+
let content: string;
|
|
807
|
+
if (Array.isArray(data.content)) {
|
|
808
|
+
// Claude API format: { content: [{ type: 'text', text: '...' }] }
|
|
809
|
+
content = data.content[0]?.text || '';
|
|
810
|
+
} else if (typeof data.content === 'string') {
|
|
811
|
+
content = data.content;
|
|
812
|
+
} else if (typeof data.text === 'string') {
|
|
813
|
+
// Alternative format
|
|
814
|
+
content = data.text;
|
|
815
|
+
} else {
|
|
816
|
+
content = String(data.content || data.text || '');
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Clean up the response
|
|
820
|
+
let cleanCode = content.trim();
|
|
821
|
+
if (cleanCode.startsWith('```')) {
|
|
822
|
+
cleanCode = cleanCode
|
|
823
|
+
.replace(/^```(?:jsx|tsx|javascript|js)?\n?/, '')
|
|
824
|
+
.replace(/\n?```$/, '');
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return cleanCode;
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
const sendMessage = async () => {
|
|
831
|
+
if (!inputValue.trim() || isGenerating) return;
|
|
832
|
+
|
|
833
|
+
const userMessage: Message = {
|
|
834
|
+
id: generateId(),
|
|
835
|
+
role: 'user',
|
|
836
|
+
content: inputValue.trim(),
|
|
837
|
+
timestamp: Date.now(),
|
|
838
|
+
images: images.length > 0 ? [...images] : undefined,
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// Determine if we need to create a new conversation or add to existing
|
|
842
|
+
let conversationId = activeConversationId;
|
|
843
|
+
|
|
844
|
+
if (!conversationId) {
|
|
845
|
+
// Create new conversation with the first message
|
|
846
|
+
conversationId = createConversationWithMessage(userMessage);
|
|
847
|
+
setActiveConversationId(conversationId);
|
|
848
|
+
} else {
|
|
849
|
+
// Add message to existing conversation
|
|
850
|
+
setConversations(prev => prev.map(conv => {
|
|
851
|
+
if (conv.id === conversationId) {
|
|
852
|
+
return {
|
|
853
|
+
...conv,
|
|
854
|
+
messages: [...conv.messages, userMessage],
|
|
855
|
+
updatedAt: Date.now(),
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
return conv;
|
|
859
|
+
}));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const currentImages = [...images];
|
|
863
|
+
setInputValue('');
|
|
864
|
+
setImages([]);
|
|
865
|
+
setIsGenerating(true);
|
|
866
|
+
|
|
867
|
+
try {
|
|
868
|
+
const generatedCode = await generateComponent(inputValue.trim(), currentImages);
|
|
869
|
+
|
|
870
|
+
const assistantMessage: Message = {
|
|
871
|
+
id: generateId(),
|
|
872
|
+
role: 'assistant',
|
|
873
|
+
content: 'Component generated successfully.',
|
|
874
|
+
timestamp: Date.now(),
|
|
875
|
+
generatedCode,
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
setConversations(prev => prev.map(conv => {
|
|
879
|
+
if (conv.id === conversationId) {
|
|
880
|
+
return {
|
|
881
|
+
...conv,
|
|
882
|
+
messages: [...conv.messages, assistantMessage],
|
|
883
|
+
updatedAt: Date.now(),
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
return conv;
|
|
887
|
+
}));
|
|
888
|
+
|
|
889
|
+
if (generatedCode) {
|
|
890
|
+
setPreviewCode(generatedCode);
|
|
891
|
+
setPreviewTab('preview');
|
|
892
|
+
}
|
|
893
|
+
} catch (error) {
|
|
894
|
+
const errorMessage: Message = {
|
|
895
|
+
id: generateId(),
|
|
896
|
+
role: 'assistant',
|
|
897
|
+
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
898
|
+
timestamp: Date.now(),
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
setConversations(prev => prev.map(conv => {
|
|
902
|
+
if (conv.id === conversationId) {
|
|
903
|
+
return {
|
|
904
|
+
...conv,
|
|
905
|
+
messages: [...conv.messages, errorMessage],
|
|
906
|
+
updatedAt: Date.now(),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
return conv;
|
|
910
|
+
}));
|
|
911
|
+
} finally {
|
|
912
|
+
setIsGenerating(false);
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
917
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
918
|
+
e.preventDefault();
|
|
919
|
+
sendMessage();
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
return (
|
|
924
|
+
<div style={{
|
|
925
|
+
display: 'flex',
|
|
926
|
+
height: '100vh',
|
|
927
|
+
overflow: 'hidden',
|
|
928
|
+
background: THEME.bg,
|
|
929
|
+
color: THEME.text,
|
|
930
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
931
|
+
}}>
|
|
932
|
+
{/* Global Styles */}
|
|
933
|
+
<style>{`
|
|
934
|
+
@keyframes pulse {
|
|
935
|
+
0%, 100% { opacity: 0.4; transform: scale(0.8); }
|
|
936
|
+
50% { opacity: 1; transform: scale(1); }
|
|
937
|
+
}
|
|
938
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
939
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
940
|
+
::-webkit-scrollbar-thumb { background: ${THEME.bgElevated}; border-radius: 3px; }
|
|
941
|
+
::-webkit-scrollbar-thumb:hover { background: ${THEME.bgHover}; }
|
|
942
|
+
`}</style>
|
|
943
|
+
|
|
944
|
+
{/* Sidebar */}
|
|
945
|
+
<aside style={{
|
|
946
|
+
width: sidebarCollapsed ? '60px' : '240px',
|
|
947
|
+
background: THEME.bgSurface,
|
|
948
|
+
borderRight: `1px solid ${THEME.border}`,
|
|
949
|
+
display: 'flex',
|
|
950
|
+
flexDirection: 'column',
|
|
951
|
+
transition: 'width 0.2s ease',
|
|
952
|
+
overflow: 'hidden',
|
|
953
|
+
}}>
|
|
954
|
+
{/* Sidebar Header */}
|
|
955
|
+
<div style={{
|
|
956
|
+
padding: '16px',
|
|
957
|
+
borderBottom: `1px solid ${THEME.border}`,
|
|
958
|
+
display: 'flex',
|
|
959
|
+
alignItems: 'center',
|
|
960
|
+
justifyContent: sidebarCollapsed ? 'center' : 'space-between',
|
|
961
|
+
}}>
|
|
962
|
+
{!sidebarCollapsed && (
|
|
963
|
+
<h1 style={{
|
|
964
|
+
fontSize: '16px',
|
|
965
|
+
fontWeight: 600,
|
|
966
|
+
background: `linear-gradient(135deg, ${THEME.accent}, #8b5cf6)`,
|
|
967
|
+
WebkitBackgroundClip: 'text',
|
|
968
|
+
WebkitTextFillColor: 'transparent',
|
|
969
|
+
letterSpacing: '-0.5px',
|
|
970
|
+
}}>
|
|
971
|
+
Story UI
|
|
972
|
+
</h1>
|
|
973
|
+
)}
|
|
974
|
+
<button
|
|
975
|
+
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
976
|
+
style={{
|
|
977
|
+
padding: '8px',
|
|
978
|
+
background: 'transparent',
|
|
979
|
+
border: 'none',
|
|
980
|
+
color: THEME.textMuted,
|
|
981
|
+
cursor: 'pointer',
|
|
982
|
+
borderRadius: '6px',
|
|
983
|
+
}}
|
|
984
|
+
>
|
|
985
|
+
<Icons.Sidebar />
|
|
986
|
+
</button>
|
|
987
|
+
</div>
|
|
988
|
+
|
|
989
|
+
{/* New Chat Button */}
|
|
990
|
+
<div style={{ padding: sidebarCollapsed ? '8px' : '12px' }}>
|
|
991
|
+
<button
|
|
992
|
+
onClick={startNewChat}
|
|
993
|
+
style={{
|
|
994
|
+
width: '100%',
|
|
995
|
+
padding: sidebarCollapsed ? '10px' : '10px 14px',
|
|
996
|
+
background: THEME.accent,
|
|
997
|
+
border: 'none',
|
|
998
|
+
borderRadius: '8px',
|
|
999
|
+
color: '#fff',
|
|
1000
|
+
fontSize: '13px',
|
|
1001
|
+
fontWeight: 500,
|
|
1002
|
+
cursor: 'pointer',
|
|
1003
|
+
display: 'flex',
|
|
1004
|
+
alignItems: 'center',
|
|
1005
|
+
justifyContent: 'center',
|
|
1006
|
+
gap: '8px',
|
|
1007
|
+
}}
|
|
1008
|
+
>
|
|
1009
|
+
<Icons.Plus />
|
|
1010
|
+
{!sidebarCollapsed && 'New Chat'}
|
|
1011
|
+
</button>
|
|
1012
|
+
</div>
|
|
1013
|
+
|
|
1014
|
+
{/* Conversation List */}
|
|
1015
|
+
{!sidebarCollapsed && (
|
|
1016
|
+
<div style={{ flex: 1, overflow: 'auto', padding: '8px 12px' }}>
|
|
1017
|
+
{displayConversations.map(conv => (
|
|
1018
|
+
<div
|
|
1019
|
+
key={conv.id}
|
|
1020
|
+
onClick={() => {
|
|
1021
|
+
setActiveConversationId(conv.id);
|
|
1022
|
+
const lastMsgWithCode = [...conv.messages].reverse().find(m => m.generatedCode);
|
|
1023
|
+
setPreviewCode(lastMsgWithCode?.generatedCode || null);
|
|
1024
|
+
}}
|
|
1025
|
+
style={{
|
|
1026
|
+
padding: '10px 12px',
|
|
1027
|
+
marginBottom: '4px',
|
|
1028
|
+
borderRadius: '8px',
|
|
1029
|
+
cursor: 'pointer',
|
|
1030
|
+
display: 'flex',
|
|
1031
|
+
alignItems: 'center',
|
|
1032
|
+
justifyContent: 'space-between',
|
|
1033
|
+
background: conv.id === activeConversationId ? THEME.bgElevated : 'transparent',
|
|
1034
|
+
}}
|
|
1035
|
+
>
|
|
1036
|
+
<span style={{
|
|
1037
|
+
fontSize: '13px',
|
|
1038
|
+
color: conv.id === activeConversationId ? THEME.text : THEME.textMuted,
|
|
1039
|
+
overflow: 'hidden',
|
|
1040
|
+
textOverflow: 'ellipsis',
|
|
1041
|
+
whiteSpace: 'nowrap',
|
|
1042
|
+
flex: 1,
|
|
1043
|
+
}}>
|
|
1044
|
+
{conv.title}
|
|
1045
|
+
</span>
|
|
1046
|
+
<button
|
|
1047
|
+
onClick={(e) => {
|
|
1048
|
+
e.stopPropagation();
|
|
1049
|
+
deleteConversation(conv.id);
|
|
1050
|
+
}}
|
|
1051
|
+
style={{
|
|
1052
|
+
padding: '4px',
|
|
1053
|
+
background: 'transparent',
|
|
1054
|
+
border: 'none',
|
|
1055
|
+
color: THEME.textSubtle,
|
|
1056
|
+
cursor: 'pointer',
|
|
1057
|
+
opacity: 0.5,
|
|
1058
|
+
}}
|
|
1059
|
+
>
|
|
1060
|
+
<Icons.Trash />
|
|
1061
|
+
</button>
|
|
1062
|
+
</div>
|
|
1063
|
+
))}
|
|
1064
|
+
</div>
|
|
1065
|
+
)}
|
|
1066
|
+
|
|
1067
|
+
{/* Sidebar Footer - Provider/Model Info */}
|
|
1068
|
+
{!sidebarCollapsed && (
|
|
1069
|
+
<div style={{
|
|
1070
|
+
padding: '12px',
|
|
1071
|
+
borderTop: `1px solid ${THEME.border}`,
|
|
1072
|
+
display: 'flex',
|
|
1073
|
+
flexDirection: 'column',
|
|
1074
|
+
gap: '6px',
|
|
1075
|
+
}}>
|
|
1076
|
+
{/* Provider Label */}
|
|
1077
|
+
<div style={{ fontSize: '12px', color: THEME.textMuted }}>
|
|
1078
|
+
<span style={{ color: THEME.textSubtle, textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.5px' }}>Provider: </span>
|
|
1079
|
+
<span style={{ color: THEME.text, fontWeight: 500 }}>
|
|
1080
|
+
{serverConfig.providers.find(p => p.id === serverConfig.currentProvider)?.name || 'Claude (Anthropic)'}
|
|
1081
|
+
</span>
|
|
1082
|
+
</div>
|
|
1083
|
+
|
|
1084
|
+
{/* Model Dropdown */}
|
|
1085
|
+
<div style={{ fontSize: '12px', color: THEME.textMuted }}>
|
|
1086
|
+
<span style={{ color: THEME.textSubtle, textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.5px' }}>Model</span>
|
|
1087
|
+
</div>
|
|
1088
|
+
<select
|
|
1089
|
+
value={serverConfig.currentModel}
|
|
1090
|
+
onChange={(e) => serverConfig.changeModel(e.target.value)}
|
|
1091
|
+
disabled={serverConfig.loading}
|
|
1092
|
+
style={{
|
|
1093
|
+
width: '100%',
|
|
1094
|
+
padding: '8px 10px',
|
|
1095
|
+
background: THEME.bgElevated,
|
|
1096
|
+
border: `1px solid ${THEME.border}`,
|
|
1097
|
+
borderRadius: '6px',
|
|
1098
|
+
color: THEME.text,
|
|
1099
|
+
fontSize: '12px',
|
|
1100
|
+
cursor: 'pointer',
|
|
1101
|
+
outline: 'none',
|
|
1102
|
+
appearance: 'none',
|
|
1103
|
+
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E")`,
|
|
1104
|
+
backgroundRepeat: 'no-repeat',
|
|
1105
|
+
backgroundPosition: 'right 8px center',
|
|
1106
|
+
paddingRight: '28px',
|
|
1107
|
+
}}
|
|
1108
|
+
>
|
|
1109
|
+
{serverConfig.models.map(model => (
|
|
1110
|
+
<option key={model.id} value={model.id}>{model.id}</option>
|
|
1111
|
+
))}
|
|
1112
|
+
</select>
|
|
1113
|
+
|
|
1114
|
+
{/* Components Count */}
|
|
1115
|
+
<div style={{
|
|
1116
|
+
display: 'flex',
|
|
1117
|
+
alignItems: 'center',
|
|
1118
|
+
gap: '6px',
|
|
1119
|
+
fontSize: '11px',
|
|
1120
|
+
color: THEME.textSubtle,
|
|
1121
|
+
paddingTop: '4px',
|
|
1122
|
+
}}>
|
|
1123
|
+
<div style={{
|
|
1124
|
+
width: '6px',
|
|
1125
|
+
height: '6px',
|
|
1126
|
+
borderRadius: '50%',
|
|
1127
|
+
background: THEME.success,
|
|
1128
|
+
}} />
|
|
1129
|
+
<span>{availableComponents.length} components available</span>
|
|
1130
|
+
</div>
|
|
1131
|
+
</div>
|
|
1132
|
+
)}
|
|
1133
|
+
</aside>
|
|
1134
|
+
|
|
1135
|
+
{/* Chat Panel */}
|
|
1136
|
+
<div style={{
|
|
1137
|
+
width: `${chatWidth}px`,
|
|
1138
|
+
display: 'flex',
|
|
1139
|
+
flexDirection: 'column',
|
|
1140
|
+
background: THEME.bg,
|
|
1141
|
+
borderRight: `1px solid ${THEME.border}`,
|
|
1142
|
+
position: 'relative',
|
|
1143
|
+
}}>
|
|
1144
|
+
{/* Messages */}
|
|
1145
|
+
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
|
|
1146
|
+
{(!activeConversation || activeConversation.messages.length === 0) && (
|
|
1147
|
+
<div style={{
|
|
1148
|
+
padding: '32px 16px',
|
|
1149
|
+
textAlign: 'center',
|
|
1150
|
+
}}>
|
|
1151
|
+
<h2 style={{
|
|
1152
|
+
fontSize: '24px',
|
|
1153
|
+
fontWeight: 600,
|
|
1154
|
+
marginBottom: '8px',
|
|
1155
|
+
background: `linear-gradient(135deg, ${THEME.text}, ${THEME.textMuted})`,
|
|
1156
|
+
WebkitBackgroundClip: 'text',
|
|
1157
|
+
WebkitTextFillColor: 'transparent',
|
|
1158
|
+
}}>
|
|
1159
|
+
Build UI Components
|
|
1160
|
+
</h2>
|
|
1161
|
+
<p style={{
|
|
1162
|
+
color: THEME.textMuted,
|
|
1163
|
+
fontSize: '14px',
|
|
1164
|
+
marginBottom: '24px',
|
|
1165
|
+
lineHeight: 1.6,
|
|
1166
|
+
}}>
|
|
1167
|
+
Describe what you want to build using your component library.
|
|
1168
|
+
</p>
|
|
1169
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', justifyContent: 'center' }}>
|
|
1170
|
+
{['Create a pricing card', 'Build a dashboard', 'Design a login form'].map(suggestion => (
|
|
1171
|
+
<button
|
|
1172
|
+
key={suggestion}
|
|
1173
|
+
onClick={() => setInputValue(suggestion)}
|
|
1174
|
+
style={{
|
|
1175
|
+
padding: '8px 14px',
|
|
1176
|
+
background: THEME.bgSurface,
|
|
1177
|
+
border: `1px solid ${THEME.border}`,
|
|
1178
|
+
borderRadius: '20px',
|
|
1179
|
+
color: THEME.textMuted,
|
|
1180
|
+
fontSize: '13px',
|
|
1181
|
+
cursor: 'pointer',
|
|
1182
|
+
}}
|
|
1183
|
+
>
|
|
1184
|
+
{suggestion}
|
|
1185
|
+
</button>
|
|
1186
|
+
))}
|
|
1187
|
+
</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
)}
|
|
1190
|
+
|
|
1191
|
+
{activeConversation?.messages.map(message => (
|
|
1192
|
+
<div
|
|
1193
|
+
key={message.id}
|
|
1194
|
+
style={{
|
|
1195
|
+
marginBottom: '16px',
|
|
1196
|
+
padding: '14px 16px',
|
|
1197
|
+
borderRadius: '12px',
|
|
1198
|
+
maxWidth: '90%',
|
|
1199
|
+
marginLeft: message.role === 'user' ? 'auto' : '0',
|
|
1200
|
+
marginRight: message.role === 'user' ? '0' : 'auto',
|
|
1201
|
+
background: message.role === 'user' ? THEME.bgElevated : THEME.bgSurface,
|
|
1202
|
+
border: `1px solid ${THEME.border}`,
|
|
1203
|
+
}}
|
|
1204
|
+
>
|
|
1205
|
+
{message.images && message.images.length > 0 && (
|
|
1206
|
+
<div style={{ display: 'flex', gap: '8px', marginBottom: '10px', flexWrap: 'wrap' }}>
|
|
1207
|
+
{message.images.map(img => (
|
|
1208
|
+
<img
|
|
1209
|
+
key={img.id}
|
|
1210
|
+
src={img.data}
|
|
1211
|
+
alt={img.name}
|
|
1212
|
+
style={{
|
|
1213
|
+
width: '80px',
|
|
1214
|
+
height: '80px',
|
|
1215
|
+
objectFit: 'cover',
|
|
1216
|
+
borderRadius: '8px',
|
|
1217
|
+
border: `1px solid ${THEME.border}`,
|
|
1218
|
+
}}
|
|
1219
|
+
/>
|
|
1220
|
+
))}
|
|
1221
|
+
</div>
|
|
1222
|
+
)}
|
|
1223
|
+
<div style={{ fontSize: '14px', lineHeight: 1.6, color: THEME.text }}>
|
|
1224
|
+
{message.content}
|
|
1225
|
+
</div>
|
|
1226
|
+
{message.generatedCode && (
|
|
1227
|
+
<button
|
|
1228
|
+
onClick={() => {
|
|
1229
|
+
setPreviewCode(message.generatedCode!);
|
|
1230
|
+
setPreviewTab('preview');
|
|
1231
|
+
}}
|
|
1232
|
+
style={{
|
|
1233
|
+
marginTop: '10px',
|
|
1234
|
+
padding: '6px 12px',
|
|
1235
|
+
background: THEME.accentMuted,
|
|
1236
|
+
border: `1px solid ${THEME.accent}`,
|
|
1237
|
+
borderRadius: '6px',
|
|
1238
|
+
color: THEME.accent,
|
|
1239
|
+
fontSize: '12px',
|
|
1240
|
+
cursor: 'pointer',
|
|
1241
|
+
display: 'flex',
|
|
1242
|
+
alignItems: 'center',
|
|
1243
|
+
gap: '6px',
|
|
1244
|
+
}}
|
|
1245
|
+
>
|
|
1246
|
+
<Icons.Eye />
|
|
1247
|
+
View Component
|
|
1248
|
+
</button>
|
|
1249
|
+
)}
|
|
1250
|
+
</div>
|
|
1251
|
+
))}
|
|
1252
|
+
|
|
1253
|
+
{isGenerating && (
|
|
1254
|
+
<div style={{
|
|
1255
|
+
marginBottom: '16px',
|
|
1256
|
+
padding: '14px 16px',
|
|
1257
|
+
borderRadius: '12px',
|
|
1258
|
+
maxWidth: '90%',
|
|
1259
|
+
background: THEME.bgSurface,
|
|
1260
|
+
border: `1px solid ${THEME.border}`,
|
|
1261
|
+
}}>
|
|
1262
|
+
<LoadingDots />
|
|
1263
|
+
</div>
|
|
1264
|
+
)}
|
|
1265
|
+
|
|
1266
|
+
<div ref={messagesEndRef} />
|
|
1267
|
+
</div>
|
|
1268
|
+
|
|
1269
|
+
{/* Input Area - ChatGPT/Lovable Style */}
|
|
1270
|
+
<div style={{
|
|
1271
|
+
padding: '16px',
|
|
1272
|
+
borderTop: `1px solid ${THEME.border}`,
|
|
1273
|
+
background: THEME.bgSurface,
|
|
1274
|
+
}}>
|
|
1275
|
+
{/* Image thumbnails if any */}
|
|
1276
|
+
{images.length > 0 && (
|
|
1277
|
+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '12px' }}>
|
|
1278
|
+
{images.map(img => (
|
|
1279
|
+
<div
|
|
1280
|
+
key={img.id}
|
|
1281
|
+
style={{
|
|
1282
|
+
position: 'relative',
|
|
1283
|
+
width: '64px',
|
|
1284
|
+
height: '64px',
|
|
1285
|
+
borderRadius: '8px',
|
|
1286
|
+
overflow: 'hidden',
|
|
1287
|
+
border: `1px solid ${THEME.border}`,
|
|
1288
|
+
}}
|
|
1289
|
+
>
|
|
1290
|
+
<img
|
|
1291
|
+
src={img.data}
|
|
1292
|
+
alt={img.name}
|
|
1293
|
+
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
1294
|
+
/>
|
|
1295
|
+
<button
|
|
1296
|
+
onClick={() => setImages(images.filter(i => i.id !== img.id))}
|
|
1297
|
+
style={{
|
|
1298
|
+
position: 'absolute',
|
|
1299
|
+
top: '2px',
|
|
1300
|
+
right: '2px',
|
|
1301
|
+
width: '18px',
|
|
1302
|
+
height: '18px',
|
|
1303
|
+
borderRadius: '50%',
|
|
1304
|
+
background: 'rgba(0,0,0,0.7)',
|
|
1305
|
+
border: 'none',
|
|
1306
|
+
color: '#fff',
|
|
1307
|
+
cursor: 'pointer',
|
|
1308
|
+
display: 'flex',
|
|
1309
|
+
alignItems: 'center',
|
|
1310
|
+
justifyContent: 'center',
|
|
1311
|
+
fontSize: '10px',
|
|
1312
|
+
}}
|
|
1313
|
+
>
|
|
1314
|
+
<Icons.X />
|
|
1315
|
+
</button>
|
|
1316
|
+
</div>
|
|
1317
|
+
))}
|
|
1318
|
+
</div>
|
|
1319
|
+
)}
|
|
1320
|
+
|
|
1321
|
+
{/* Combined input container matching reference */}
|
|
1322
|
+
<div style={{
|
|
1323
|
+
display: 'flex',
|
|
1324
|
+
flexDirection: 'column',
|
|
1325
|
+
background: THEME.bgElevated,
|
|
1326
|
+
border: `1px solid ${THEME.border}`,
|
|
1327
|
+
borderRadius: '12px',
|
|
1328
|
+
overflow: 'hidden',
|
|
1329
|
+
}}>
|
|
1330
|
+
{/* Text input area */}
|
|
1331
|
+
<textarea
|
|
1332
|
+
ref={inputRef}
|
|
1333
|
+
value={inputValue}
|
|
1334
|
+
onChange={e => setInputValue(e.target.value)}
|
|
1335
|
+
onKeyDown={handleKeyPress}
|
|
1336
|
+
placeholder="Describe the component you want to create..."
|
|
1337
|
+
disabled={isGenerating}
|
|
1338
|
+
style={{
|
|
1339
|
+
width: '100%',
|
|
1340
|
+
padding: '14px 16px',
|
|
1341
|
+
background: 'transparent',
|
|
1342
|
+
border: 'none',
|
|
1343
|
+
color: THEME.text,
|
|
1344
|
+
fontSize: '14px',
|
|
1345
|
+
resize: 'none',
|
|
1346
|
+
outline: 'none',
|
|
1347
|
+
fontFamily: 'inherit',
|
|
1348
|
+
minHeight: '24px',
|
|
1349
|
+
maxHeight: '120px',
|
|
1350
|
+
}}
|
|
1351
|
+
rows={1}
|
|
1352
|
+
/>
|
|
1353
|
+
|
|
1354
|
+
{/* Bottom row with attach button and send */}
|
|
1355
|
+
<div style={{
|
|
1356
|
+
display: 'flex',
|
|
1357
|
+
alignItems: 'center',
|
|
1358
|
+
justifyContent: 'space-between',
|
|
1359
|
+
padding: '8px 12px',
|
|
1360
|
+
borderTop: `1px solid ${THEME.border}`,
|
|
1361
|
+
}}>
|
|
1362
|
+
{/* + Attach button */}
|
|
1363
|
+
<label
|
|
1364
|
+
style={{
|
|
1365
|
+
display: 'flex',
|
|
1366
|
+
alignItems: 'center',
|
|
1367
|
+
gap: '6px',
|
|
1368
|
+
padding: '6px 12px',
|
|
1369
|
+
background: THEME.bgHover,
|
|
1370
|
+
border: `1px solid ${THEME.borderSubtle}`,
|
|
1371
|
+
borderRadius: '6px',
|
|
1372
|
+
color: THEME.textMuted,
|
|
1373
|
+
fontSize: '13px',
|
|
1374
|
+
cursor: isGenerating ? 'not-allowed' : 'pointer',
|
|
1375
|
+
opacity: isGenerating ? 0.5 : 1,
|
|
1376
|
+
transition: 'all 0.2s',
|
|
1377
|
+
}}
|
|
1378
|
+
>
|
|
1379
|
+
<Icons.Plus />
|
|
1380
|
+
<span>Attach</span>
|
|
1381
|
+
<input
|
|
1382
|
+
type="file"
|
|
1383
|
+
accept="image/*"
|
|
1384
|
+
multiple
|
|
1385
|
+
style={{ display: 'none' }}
|
|
1386
|
+
disabled={isGenerating}
|
|
1387
|
+
onChange={(e) => {
|
|
1388
|
+
if (e.target.files) {
|
|
1389
|
+
Array.from(e.target.files).forEach(file => {
|
|
1390
|
+
if (file.type.startsWith('image/')) {
|
|
1391
|
+
const reader = new FileReader();
|
|
1392
|
+
reader.onload = (ev) => {
|
|
1393
|
+
const newImage: ImageAttachment = {
|
|
1394
|
+
id: generateId(),
|
|
1395
|
+
data: ev.target?.result as string,
|
|
1396
|
+
type: file.type,
|
|
1397
|
+
name: file.name,
|
|
1398
|
+
};
|
|
1399
|
+
setImages(prev => [...prev, newImage]);
|
|
1400
|
+
};
|
|
1401
|
+
reader.readAsDataURL(file);
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
}}
|
|
1406
|
+
/>
|
|
1407
|
+
</label>
|
|
1408
|
+
|
|
1409
|
+
{/* Send button */}
|
|
1410
|
+
<button
|
|
1411
|
+
onClick={sendMessage}
|
|
1412
|
+
disabled={isGenerating || !inputValue.trim()}
|
|
1413
|
+
style={{
|
|
1414
|
+
padding: '8px 12px',
|
|
1415
|
+
background: (isGenerating || !inputValue.trim()) ? THEME.bgHover : THEME.accent,
|
|
1416
|
+
border: 'none',
|
|
1417
|
+
borderRadius: '8px',
|
|
1418
|
+
color: (isGenerating || !inputValue.trim()) ? THEME.textSubtle : '#fff',
|
|
1419
|
+
cursor: (isGenerating || !inputValue.trim()) ? 'not-allowed' : 'pointer',
|
|
1420
|
+
display: 'flex',
|
|
1421
|
+
alignItems: 'center',
|
|
1422
|
+
justifyContent: 'center',
|
|
1423
|
+
}}
|
|
1424
|
+
>
|
|
1425
|
+
<Icons.Send />
|
|
1426
|
+
</button>
|
|
1427
|
+
</div>
|
|
1428
|
+
</div>
|
|
1429
|
+
</div>
|
|
1430
|
+
|
|
1431
|
+
{/* Resize Handle */}
|
|
1432
|
+
<div
|
|
1433
|
+
onMouseDown={startResize}
|
|
1434
|
+
style={{
|
|
1435
|
+
position: 'absolute',
|
|
1436
|
+
right: 0,
|
|
1437
|
+
top: 0,
|
|
1438
|
+
bottom: 0,
|
|
1439
|
+
width: '4px',
|
|
1440
|
+
cursor: 'col-resize',
|
|
1441
|
+
background: 'transparent',
|
|
1442
|
+
}}
|
|
1443
|
+
/>
|
|
1444
|
+
</div>
|
|
1445
|
+
|
|
1446
|
+
{/* Preview Panel */}
|
|
1447
|
+
<div style={{
|
|
1448
|
+
flex: 1,
|
|
1449
|
+
display: 'flex',
|
|
1450
|
+
flexDirection: 'column',
|
|
1451
|
+
background: THEME.bg,
|
|
1452
|
+
minWidth: 0,
|
|
1453
|
+
}}>
|
|
1454
|
+
{/* Preview Header */}
|
|
1455
|
+
<div style={{
|
|
1456
|
+
padding: '12px 16px',
|
|
1457
|
+
borderBottom: `1px solid ${THEME.border}`,
|
|
1458
|
+
display: 'flex',
|
|
1459
|
+
alignItems: 'center',
|
|
1460
|
+
justifyContent: 'space-between',
|
|
1461
|
+
}}>
|
|
1462
|
+
<div style={{ display: 'flex', gap: '4px' }}>
|
|
1463
|
+
<button
|
|
1464
|
+
onClick={() => setPreviewTab('preview')}
|
|
1465
|
+
style={{
|
|
1466
|
+
padding: '8px 14px',
|
|
1467
|
+
background: previewTab === 'preview' ? THEME.bgElevated : 'transparent',
|
|
1468
|
+
border: 'none',
|
|
1469
|
+
borderRadius: '6px',
|
|
1470
|
+
color: previewTab === 'preview' ? THEME.text : THEME.textMuted,
|
|
1471
|
+
fontSize: '13px',
|
|
1472
|
+
cursor: 'pointer',
|
|
1473
|
+
display: 'flex',
|
|
1474
|
+
alignItems: 'center',
|
|
1475
|
+
gap: '6px',
|
|
1476
|
+
}}
|
|
1477
|
+
>
|
|
1478
|
+
<Icons.Eye />
|
|
1479
|
+
Preview
|
|
1480
|
+
</button>
|
|
1481
|
+
<button
|
|
1482
|
+
onClick={() => setPreviewTab('code')}
|
|
1483
|
+
style={{
|
|
1484
|
+
padding: '8px 14px',
|
|
1485
|
+
background: previewTab === 'code' ? THEME.bgElevated : 'transparent',
|
|
1486
|
+
border: 'none',
|
|
1487
|
+
borderRadius: '6px',
|
|
1488
|
+
color: previewTab === 'code' ? THEME.text : THEME.textMuted,
|
|
1489
|
+
fontSize: '13px',
|
|
1490
|
+
cursor: 'pointer',
|
|
1491
|
+
display: 'flex',
|
|
1492
|
+
alignItems: 'center',
|
|
1493
|
+
gap: '6px',
|
|
1494
|
+
}}
|
|
1495
|
+
>
|
|
1496
|
+
<Icons.Code />
|
|
1497
|
+
Code
|
|
1498
|
+
</button>
|
|
1499
|
+
</div>
|
|
1500
|
+
|
|
1501
|
+
{previewCode && previewTab === 'preview' && (
|
|
1502
|
+
<span style={{ fontSize: '12px', color: THEME.textMuted }}>
|
|
1503
|
+
Live Preview
|
|
1504
|
+
</span>
|
|
1505
|
+
)}
|
|
1506
|
+
</div>
|
|
1507
|
+
|
|
1508
|
+
{/* Preview Content */}
|
|
1509
|
+
<div style={{ flex: 1, overflow: 'hidden' }}>
|
|
1510
|
+
{previewCode ? (
|
|
1511
|
+
previewTab === 'preview' ? (
|
|
1512
|
+
<LivePreviewRenderer
|
|
1513
|
+
code={previewCode}
|
|
1514
|
+
containerStyle={{ height: '100%', background: THEME.bgSurface }}
|
|
1515
|
+
onError={(err) => console.error('Preview error:', err)}
|
|
1516
|
+
/>
|
|
1517
|
+
) : (
|
|
1518
|
+
<CodeViewer code={previewCode} />
|
|
1519
|
+
)
|
|
1520
|
+
) : (
|
|
1521
|
+
<div style={{
|
|
1522
|
+
height: '100%',
|
|
1523
|
+
display: 'flex',
|
|
1524
|
+
flexDirection: 'column',
|
|
1525
|
+
alignItems: 'center',
|
|
1526
|
+
justifyContent: 'center',
|
|
1527
|
+
color: THEME.textSubtle,
|
|
1528
|
+
padding: '40px',
|
|
1529
|
+
}}>
|
|
1530
|
+
<div style={{
|
|
1531
|
+
width: '80px',
|
|
1532
|
+
height: '80px',
|
|
1533
|
+
borderRadius: '20px',
|
|
1534
|
+
background: THEME.bgSurface,
|
|
1535
|
+
display: 'flex',
|
|
1536
|
+
alignItems: 'center',
|
|
1537
|
+
justifyContent: 'center',
|
|
1538
|
+
marginBottom: '20px',
|
|
1539
|
+
border: `1px solid ${THEME.border}`,
|
|
1540
|
+
}}>
|
|
1541
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
1542
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
1543
|
+
<path d="M3 9h18M9 21V9" />
|
|
1544
|
+
</svg>
|
|
1545
|
+
</div>
|
|
1546
|
+
<p style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px', color: THEME.textMuted }}>
|
|
1547
|
+
Ready to Build
|
|
1548
|
+
</p>
|
|
1549
|
+
<p style={{ fontSize: '14px', maxWidth: '260px', textAlign: 'center', lineHeight: 1.5 }}>
|
|
1550
|
+
Describe a component and watch it come to life
|
|
1551
|
+
</p>
|
|
1552
|
+
</div>
|
|
1553
|
+
)}
|
|
1554
|
+
</div>
|
|
1555
|
+
</div>
|
|
1556
|
+
</div>
|
|
1557
|
+
);
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
export default App;
|