@tpitre/story-ui 1.7.1 → 2.0.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 +3 -1
- package/README.md +160 -606
- package/dist/cli/index.js +23 -24
- package/dist/cli/setup.js +295 -36
- package/dist/mcp-server/index.js +67 -0
- package/dist/mcp-server/routes/generateStory.js +323 -56
- package/dist/story-generator/componentBlacklist.js +181 -0
- package/dist/story-generator/componentDiscovery.js +9 -2
- package/dist/story-generator/configLoader.js +109 -39
- package/dist/story-generator/considerationsLoader.js +204 -0
- package/dist/story-generator/documentation-sources.js +36 -0
- package/dist/story-generator/documentationLoader.js +214 -0
- package/dist/story-generator/dynamicPackageDiscovery.js +527 -0
- package/dist/story-generator/enhancedComponentDiscovery.js +369 -118
- package/dist/story-generator/generateStory.js +7 -3
- package/dist/story-generator/postProcessStory.js +71 -0
- package/dist/story-generator/promptGenerator.js +286 -37
- package/dist/story-generator/storyHistory.js +118 -0
- package/dist/story-generator/storyTracker.js +33 -18
- package/dist/story-generator/storyValidator.js +39 -0
- package/dist/story-generator/universalDesignSystemAdapter.js +209 -0
- package/dist/story-generator/validateStory.js +82 -7
- package/dist/story-ui.config.js +12 -5
- package/package.json +11 -6
- package/templates/StoryUI/StoryUIPanel.stories.tsx +29 -13
- package/templates/StoryUI/StoryUIPanel.tsx +489 -359
- package/templates/react-import-rule.json +36 -0
- package/templates/story-generation-rules.json +29 -0
- package/templates/story-ui-considerations.json +156 -0
- package/templates/story-ui-considerations.md +109 -0
- package/templates/story-ui-docs-README.md +55 -0
- package/dist/scripts/test-validation.js +0 -81
- package/dist/test-storybooks/chakra-test/src/components/index.js +0 -3
- package/dist/test-storybooks/custom-design-test/src/components/index.js +0 -3
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -1,56 +1,224 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect } from 'react';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
const MCP_PORT = (window as any).STORY_UI_MCP_PORT || '4001';
|
|
5
|
-
const MCP_API = `http://localhost:${MCP_PORT}/mcp/generate-story`;
|
|
6
|
-
const SYNC_API = `http://localhost:${MCP_PORT}/mcp/sync`;
|
|
7
|
-
const LOCAL_STORAGE_KEY = 'story_ui_chat_history_v2'; // Updated version for sync
|
|
8
|
-
const MAX_RECENT_CHATS = 20;
|
|
9
|
-
|
|
3
|
+
// Message type
|
|
10
4
|
interface Message {
|
|
11
5
|
role: 'user' | 'ai';
|
|
12
6
|
content: string;
|
|
13
7
|
}
|
|
14
8
|
|
|
9
|
+
// Session type
|
|
15
10
|
interface ChatSession {
|
|
16
|
-
id: string;
|
|
11
|
+
id: string;
|
|
17
12
|
title: string;
|
|
18
13
|
fileName: string;
|
|
19
14
|
conversation: Message[];
|
|
20
15
|
lastUpdated: number;
|
|
21
|
-
isValid?: boolean; // Whether the story still exists
|
|
22
16
|
}
|
|
23
17
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
// Determine the MCP API port.
|
|
19
|
+
// 1. Check multiple possible environment variables/overrides in order of preference
|
|
20
|
+
// 2. Check VITE_STORY_UI_PORT from environment
|
|
21
|
+
// 3. Check window.__STORY_UI_PORT__ set by host application
|
|
22
|
+
// 4. Otherwise fall back to the default 4001.
|
|
23
|
+
const getApiPort = () => {
|
|
24
|
+
// Check for Vite environment variable
|
|
25
|
+
const vitePort = (import.meta as any).env?.VITE_STORY_UI_PORT;
|
|
26
|
+
if (vitePort) return String(vitePort);
|
|
27
|
+
|
|
28
|
+
// Check for window override (legacy support)
|
|
29
|
+
const windowOverride = (window as any).__STORY_UI_PORT__;
|
|
30
|
+
if (windowOverride) return String(windowOverride);
|
|
31
|
+
|
|
32
|
+
// Check for MCP port override set by stories file
|
|
33
|
+
const mcpOverride = (window as any).STORY_UI_MCP_PORT;
|
|
34
|
+
if (mcpOverride) return String(mcpOverride);
|
|
35
|
+
|
|
36
|
+
return '4001';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const MCP_API = `http://localhost:${getApiPort()}/story-ui/generate`;
|
|
40
|
+
const STORIES_API = `http://localhost:${getApiPort()}/story-ui/stories`;
|
|
41
|
+
const DELETE_API_BASE = `http://localhost:${getApiPort()}/story-ui/stories`;
|
|
42
|
+
const STORAGE_KEY = `story-ui-chats-${window.location.port}`;
|
|
43
|
+
const MAX_RECENT_CHATS = 20;
|
|
44
|
+
|
|
45
|
+
// Load from localStorage
|
|
46
|
+
const loadChats = (): ChatSession[] => {
|
|
47
|
+
try {
|
|
48
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
49
|
+
if (!stored) return [];
|
|
50
|
+
const chats = JSON.parse(stored) as ChatSession[];
|
|
51
|
+
// Sort by lastUpdated and limit
|
|
52
|
+
return chats
|
|
53
|
+
.sort((a, b) => b.lastUpdated - a.lastUpdated)
|
|
54
|
+
.slice(0, MAX_RECENT_CHATS);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error('Failed to load chats:', e);
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Save to localStorage
|
|
62
|
+
const saveChats = (chats: ChatSession[]) => {
|
|
63
|
+
try {
|
|
64
|
+
// Keep only the most recent chats
|
|
65
|
+
const toSave = chats
|
|
66
|
+
.sort((a, b) => b.lastUpdated - a.lastUpdated)
|
|
67
|
+
.slice(0, MAX_RECENT_CHATS);
|
|
68
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error('Failed to save chats:', e);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Sync with memory stories from backend
|
|
75
|
+
const syncWithActualStories = async (): Promise<ChatSession[]> => {
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(STORIES_API);
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
console.error('Failed to fetch stories from backend');
|
|
80
|
+
return loadChats();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if response is JSON
|
|
84
|
+
const contentType = response.headers.get('content-type');
|
|
85
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
86
|
+
console.error('Server returned non-JSON response, likely server not running or wrong port');
|
|
87
|
+
return loadChats();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
const memoryStories = data.stories || [];
|
|
92
|
+
|
|
93
|
+
// Load existing chats
|
|
94
|
+
const existingChats = loadChats();
|
|
95
|
+
|
|
96
|
+
// Create a map for quick lookup
|
|
97
|
+
const chatMap = new Map<string, ChatSession>();
|
|
98
|
+
existingChats.forEach(chat => {
|
|
99
|
+
chatMap.set(chat.id, chat);
|
|
100
|
+
if (chat.fileName) {
|
|
101
|
+
chatMap.set(chat.fileName, chat);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Update or add memory stories
|
|
106
|
+
memoryStories.forEach((story: any) => {
|
|
107
|
+
const storyId = story.storyId || story.fileName;
|
|
108
|
+
const existingChat = chatMap.get(storyId) || chatMap.get(story.fileName);
|
|
109
|
+
|
|
110
|
+
if (existingChat) {
|
|
111
|
+
// Update existing chat with latest info
|
|
112
|
+
existingChat.title = story.title || existingChat.title;
|
|
113
|
+
existingChat.fileName = story.fileName || existingChat.fileName;
|
|
114
|
+
existingChat.lastUpdated = new Date(story.updatedAt || story.createdAt).getTime();
|
|
115
|
+
} else {
|
|
116
|
+
// Create new chat from memory story
|
|
117
|
+
const newChat: ChatSession = {
|
|
118
|
+
id: storyId,
|
|
119
|
+
title: story.title || story.fileName,
|
|
120
|
+
fileName: story.fileName,
|
|
121
|
+
conversation: [{
|
|
122
|
+
role: 'user',
|
|
123
|
+
content: story.prompt || `Generate ${story.title}`
|
|
124
|
+
}, {
|
|
125
|
+
role: 'ai',
|
|
126
|
+
content: `✅ Created story: "${story.title}"\n\nThis story was recovered from memory. You can continue updating it or view it in Storybook.`
|
|
127
|
+
}],
|
|
128
|
+
lastUpdated: new Date(story.updatedAt || story.createdAt).getTime()
|
|
129
|
+
};
|
|
130
|
+
chatMap.set(storyId, newChat);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Convert back to array and save
|
|
135
|
+
const syncedChats = Array.from(chatMap.values());
|
|
136
|
+
saveChats(syncedChats);
|
|
137
|
+
|
|
138
|
+
return syncedChats;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('Error syncing with backend:', error);
|
|
141
|
+
return loadChats();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Delete story and chat
|
|
146
|
+
const deleteStoryAndChat = async (chatId: string): Promise<boolean> => {
|
|
147
|
+
try {
|
|
148
|
+
// First try to delete from backend
|
|
149
|
+
const response = await fetch(`${DELETE_API_BASE}/${chatId}`, {
|
|
150
|
+
method: 'DELETE',
|
|
151
|
+
headers: { 'Content-Type': 'application/json' }
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
console.error('Failed to delete story from backend');
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check if response is JSON
|
|
160
|
+
const contentType = response.headers.get('content-type');
|
|
161
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
162
|
+
console.error('Server returned non-JSON response, likely server not running or wrong port');
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Then remove from local storage
|
|
167
|
+
const chats = loadChats().filter(chat => chat.id !== chatId);
|
|
168
|
+
saveChats(chats);
|
|
28
169
|
|
|
29
|
-
|
|
170
|
+
return true;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('Error deleting story:', error);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Test connection to MCP server
|
|
178
|
+
const testMCPConnection = async (): Promise<{ connected: boolean; error?: string }> => {
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch(STORIES_API, {
|
|
181
|
+
method: 'GET',
|
|
182
|
+
headers: { 'Content-Type': 'application/json' },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
return { connected: false, error: `HTTP ${response.status}: ${response.statusText}` };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const contentType = response.headers.get('content-type');
|
|
190
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
191
|
+
return { connected: false, error: 'Server returned non-JSON response (likely wrong port or server not running)' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { connected: true };
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return { connected: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Component styles
|
|
201
|
+
const STYLES = {
|
|
30
202
|
container: {
|
|
31
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
32
|
-
fontSize: '14px',
|
|
33
|
-
lineHeight: '1.5',
|
|
34
|
-
color: '#ffffff',
|
|
35
|
-
margin: 0,
|
|
36
|
-
padding: 0,
|
|
37
|
-
boxSizing: 'border-box' as const,
|
|
38
203
|
display: 'flex',
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
204
|
+
flexDirection: 'row' as const,
|
|
205
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
206
|
+
height: '100vh',
|
|
42
207
|
overflow: 'hidden',
|
|
43
|
-
|
|
208
|
+
background: 'linear-gradient(135deg, #1e293b 0%, #334155 100%)',
|
|
209
|
+
color: '#e2e8f0',
|
|
44
210
|
},
|
|
45
211
|
|
|
46
|
-
// Sidebar
|
|
212
|
+
// Sidebar
|
|
47
213
|
sidebar: {
|
|
48
214
|
width: '280px',
|
|
49
|
-
background: '
|
|
215
|
+
background: 'rgba(255, 255, 255, 0.05)',
|
|
50
216
|
borderRight: '1px solid rgba(255, 255, 255, 0.1)',
|
|
51
217
|
display: 'flex',
|
|
52
218
|
flexDirection: 'column' as const,
|
|
53
|
-
|
|
219
|
+
backdropFilter: 'blur(10px)',
|
|
220
|
+
transition: 'width 0.3s ease',
|
|
221
|
+
position: 'relative' as const,
|
|
54
222
|
},
|
|
55
223
|
|
|
56
224
|
sidebarCollapsed: {
|
|
@@ -58,96 +226,105 @@ const STYLES = {
|
|
|
58
226
|
},
|
|
59
227
|
|
|
60
228
|
sidebarToggle: {
|
|
61
|
-
|
|
229
|
+
width: '100%',
|
|
230
|
+
padding: '10px 16px',
|
|
62
231
|
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
|
|
63
|
-
color: '
|
|
232
|
+
color: 'white',
|
|
64
233
|
border: 'none',
|
|
65
234
|
borderRadius: '8px',
|
|
66
|
-
margin: '16px',
|
|
67
|
-
padding: '12px 16px',
|
|
68
|
-
fontWeight: '600',
|
|
69
235
|
fontSize: '14px',
|
|
236
|
+
fontWeight: '500',
|
|
70
237
|
cursor: 'pointer',
|
|
238
|
+
marginBottom: '8px',
|
|
71
239
|
transition: 'all 0.2s ease',
|
|
240
|
+
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.3)',
|
|
72
241
|
display: 'flex',
|
|
73
242
|
alignItems: 'center',
|
|
74
243
|
justifyContent: 'center',
|
|
75
|
-
|
|
244
|
+
gap: '8px',
|
|
76
245
|
},
|
|
77
246
|
|
|
78
247
|
newChatButton: {
|
|
79
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
80
248
|
width: '100%',
|
|
81
|
-
padding: '
|
|
82
|
-
marginBottom: '12px',
|
|
83
|
-
borderRadius: '8px',
|
|
84
|
-
border: '1px solid rgba(59, 130, 246, 0.3)',
|
|
249
|
+
padding: '10px 16px',
|
|
85
250
|
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
|
|
86
|
-
color: '
|
|
87
|
-
|
|
251
|
+
color: 'white',
|
|
252
|
+
border: 'none',
|
|
253
|
+
borderRadius: '8px',
|
|
88
254
|
fontSize: '14px',
|
|
255
|
+
fontWeight: '500',
|
|
89
256
|
cursor: 'pointer',
|
|
257
|
+
marginBottom: '16px',
|
|
90
258
|
transition: 'all 0.2s ease',
|
|
91
259
|
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.2)',
|
|
92
260
|
},
|
|
93
261
|
|
|
94
262
|
chatItem: {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
padding: '12px 40px 12px 16px',
|
|
263
|
+
padding: '12px 16px',
|
|
264
|
+
marginBottom: '8px',
|
|
265
|
+
background: 'rgba(255, 255, 255, 0.08)',
|
|
99
266
|
borderRadius: '8px',
|
|
100
|
-
border: '1px solid transparent',
|
|
101
|
-
background: 'rgba(255, 255, 255, 0.05)',
|
|
102
|
-
color: '#e2e8f0',
|
|
103
|
-
fontWeight: '400',
|
|
104
|
-
fontSize: '13px',
|
|
105
267
|
cursor: 'pointer',
|
|
106
|
-
overflow: 'hidden',
|
|
107
|
-
textOverflow: 'ellipsis',
|
|
108
|
-
whiteSpace: 'nowrap' as const,
|
|
109
268
|
transition: 'all 0.2s ease',
|
|
110
|
-
|
|
269
|
+
position: 'relative' as const,
|
|
270
|
+
paddingRight: '40px',
|
|
111
271
|
},
|
|
112
272
|
|
|
113
273
|
chatItemActive: {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
274
|
+
background: 'rgba(59, 130, 246, 0.2)',
|
|
275
|
+
borderLeft: '3px solid #3b82f6',
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
chatItemTitle: {
|
|
279
|
+
fontSize: '14px',
|
|
117
280
|
fontWeight: '500',
|
|
281
|
+
marginBottom: '4px',
|
|
282
|
+
whiteSpace: 'nowrap' as const,
|
|
283
|
+
overflow: 'hidden',
|
|
284
|
+
textOverflow: 'ellipsis',
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
chatItemTime: {
|
|
288
|
+
fontSize: '12px',
|
|
289
|
+
color: '#94a3b8',
|
|
118
290
|
},
|
|
119
291
|
|
|
120
|
-
|
|
121
|
-
|
|
292
|
+
deleteButton: {
|
|
293
|
+
position: 'absolute' as const,
|
|
294
|
+
right: '8px',
|
|
295
|
+
top: '50%',
|
|
296
|
+
transform: 'translateY(-50%)',
|
|
297
|
+
background: 'rgba(239, 68, 68, 0.8)',
|
|
298
|
+
color: 'white',
|
|
299
|
+
border: 'none',
|
|
300
|
+
borderRadius: '4px',
|
|
301
|
+
padding: '4px 8px',
|
|
302
|
+
fontSize: '12px',
|
|
303
|
+
cursor: 'pointer',
|
|
304
|
+
opacity: 0,
|
|
305
|
+
transition: 'opacity 0.2s ease',
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// Main content
|
|
309
|
+
mainContent: {
|
|
122
310
|
flex: 1,
|
|
123
311
|
display: 'flex',
|
|
124
312
|
flexDirection: 'column' as const,
|
|
125
|
-
|
|
313
|
+
overflow: 'hidden',
|
|
126
314
|
},
|
|
127
315
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
fontSize: '20px',
|
|
134
|
-
fontWeight: '600',
|
|
135
|
-
letterSpacing: '-0.01em',
|
|
136
|
-
background: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)',
|
|
137
|
-
WebkitBackgroundClip: 'text',
|
|
138
|
-
WebkitTextFillColor: 'transparent',
|
|
139
|
-
backgroundClip: 'text',
|
|
316
|
+
chatHeader: {
|
|
317
|
+
padding: '20px 24px',
|
|
318
|
+
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
|
319
|
+
background: 'rgba(255, 255, 255, 0.05)',
|
|
320
|
+
backdropFilter: 'blur(10px)',
|
|
140
321
|
},
|
|
141
322
|
|
|
142
323
|
chatContainer: {
|
|
143
324
|
flex: 1,
|
|
325
|
+
padding: '24px',
|
|
144
326
|
overflowY: 'auto' as const,
|
|
145
|
-
|
|
146
|
-
borderRadius: '12px',
|
|
147
|
-
padding: '20px',
|
|
148
|
-
margin: '0 24px 16px 24px',
|
|
149
|
-
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
150
|
-
backdropFilter: 'blur(10px)',
|
|
327
|
+
scrollBehavior: 'smooth' as const,
|
|
151
328
|
},
|
|
152
329
|
|
|
153
330
|
emptyState: {
|
|
@@ -249,222 +426,118 @@ const STYLES = {
|
|
|
249
426
|
border: 'none',
|
|
250
427
|
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
|
251
428
|
color: '#ffffff',
|
|
252
|
-
fontWeight: '600',
|
|
253
429
|
fontSize: '14px',
|
|
430
|
+
fontWeight: '500',
|
|
254
431
|
cursor: 'pointer',
|
|
432
|
+
display: 'flex',
|
|
433
|
+
alignItems: 'center',
|
|
434
|
+
gap: '6px',
|
|
255
435
|
transition: 'all 0.2s ease',
|
|
256
436
|
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)',
|
|
257
437
|
},
|
|
258
438
|
|
|
259
|
-
sendButtonDisabled: {
|
|
260
|
-
background: 'rgba(107, 114, 128, 0.5)',
|
|
261
|
-
cursor: 'not-allowed',
|
|
262
|
-
boxShadow: 'none',
|
|
263
|
-
},
|
|
264
|
-
|
|
265
439
|
errorMessage: {
|
|
266
|
-
|
|
267
|
-
|
|
440
|
+
background: 'rgba(248, 113, 113, 0.1)',
|
|
441
|
+
color: '#f87171',
|
|
268
442
|
padding: '12px 16px',
|
|
269
|
-
background: 'rgba(239, 68, 68, 0.1)',
|
|
270
443
|
borderRadius: '8px',
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
444
|
+
fontSize: '14px',
|
|
445
|
+
marginBottom: '16px',
|
|
446
|
+
border: '1px solid rgba(248, 113, 113, 0.3)',
|
|
274
447
|
},
|
|
275
448
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
top: '50%',
|
|
280
|
-
transform: 'translateY(-50%)',
|
|
281
|
-
width: '20px',
|
|
282
|
-
height: '20px',
|
|
283
|
-
borderRadius: '4px',
|
|
284
|
-
border: 'none',
|
|
285
|
-
background: 'rgba(239, 68, 68, 0.8)',
|
|
286
|
-
color: '#ffffff',
|
|
287
|
-
fontSize: '12px',
|
|
288
|
-
fontWeight: 'bold',
|
|
289
|
-
cursor: 'pointer',
|
|
290
|
-
display: 'flex',
|
|
291
|
-
alignItems: 'center',
|
|
292
|
-
justifyContent: 'center',
|
|
293
|
-
transition: 'all 0.2s ease',
|
|
294
|
-
zIndex: 10,
|
|
295
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
449
|
+
loadingDots: {
|
|
450
|
+
display: 'inline-block',
|
|
451
|
+
animation: 'loadingDots 1.4s infinite',
|
|
296
452
|
},
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
function loadChats(): ChatSession[] {
|
|
300
|
-
try {
|
|
301
|
-
const raw = localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
302
|
-
if (!raw) return [];
|
|
303
|
-
return JSON.parse(raw) as ChatSession[];
|
|
304
|
-
} catch {
|
|
305
|
-
return [];
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
453
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const seenTitles = new Map<string, ChatSession>();
|
|
316
|
-
|
|
317
|
-
return chats.filter(chat => {
|
|
318
|
-
// Remove exact ID duplicates
|
|
319
|
-
if (seen.has(chat.id)) {
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
seen.add(chat.id);
|
|
323
|
-
|
|
324
|
-
// For title duplicates, keep the most recent one
|
|
325
|
-
const existingChat = seenTitles.get(chat.title);
|
|
326
|
-
if (existingChat) {
|
|
327
|
-
if (chat.lastUpdated > existingChat.lastUpdated) {
|
|
328
|
-
// Remove the older chat and keep this newer one
|
|
329
|
-
const oldIndex = chats.findIndex(c => c.id === existingChat.id);
|
|
330
|
-
if (oldIndex !== -1) {
|
|
331
|
-
seen.delete(existingChat.id);
|
|
332
|
-
}
|
|
333
|
-
seenTitles.set(chat.title, chat);
|
|
334
|
-
return true;
|
|
335
|
-
} else {
|
|
336
|
-
// Keep the existing newer chat, skip this one
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
} else {
|
|
340
|
-
seenTitles.set(chat.title, chat);
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async function syncWithActualStories(): Promise<ChatSession[]> {
|
|
347
|
-
try {
|
|
348
|
-
// Get actual stories from the server
|
|
349
|
-
const response = await fetch(`${SYNC_API}/stories`);
|
|
350
|
-
const data = await response.json();
|
|
351
|
-
|
|
352
|
-
if (!data.success) {
|
|
353
|
-
console.warn('Failed to sync with actual stories:', data.error);
|
|
354
|
-
return loadChats();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const actualStories = data.stories;
|
|
358
|
-
const existingChats = loadChats();
|
|
359
|
-
|
|
360
|
-
// More robust matching: check multiple ID formats to prevent duplicates
|
|
361
|
-
const validChats = existingChats.filter(chat => {
|
|
362
|
-
return actualStories.some((story: { id: string; fileName: string }) => {
|
|
363
|
-
// Direct ID match
|
|
364
|
-
if (story.id === chat.id) return true;
|
|
365
|
-
|
|
366
|
-
// Filename match
|
|
367
|
-
if (story.fileName === chat.fileName) return true;
|
|
368
|
-
|
|
369
|
-
// Check if chat ID matches story filename (old format)
|
|
370
|
-
if (story.fileName === chat.id) return true;
|
|
371
|
-
|
|
372
|
-
// Check if story ID matches chat filename (common mismatch)
|
|
373
|
-
if (story.id === chat.fileName) return true;
|
|
374
|
-
|
|
375
|
-
return false;
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// Only add stories that truly don't have chat sessions after robust checking
|
|
380
|
-
const newChats: ChatSession[] = actualStories
|
|
381
|
-
.filter((story: { id: string; fileName: string }) => {
|
|
382
|
-
return !existingChats.some(chat => {
|
|
383
|
-
// Check all possible matches to avoid creating duplicates
|
|
384
|
-
return chat.id === story.id ||
|
|
385
|
-
chat.fileName === story.fileName ||
|
|
386
|
-
chat.id === story.fileName ||
|
|
387
|
-
chat.fileName === story.id;
|
|
388
|
-
});
|
|
389
|
-
})
|
|
390
|
-
.map((story: { id: string; title: string; fileName: string; createdAt: string }) => ({
|
|
391
|
-
id: story.id,
|
|
392
|
-
title: story.title,
|
|
393
|
-
fileName: story.fileName,
|
|
394
|
-
conversation: [
|
|
395
|
-
{ role: 'ai' as const, content: `Story "${story.title}" was found.\nGenerated: ${new Date(story.createdAt).toLocaleString()}` }
|
|
396
|
-
],
|
|
397
|
-
lastUpdated: new Date(story.createdAt).getTime(),
|
|
398
|
-
isValid: true
|
|
399
|
-
}));
|
|
400
|
-
|
|
401
|
-
// Mark all chats as valid and combine
|
|
402
|
-
const combinedChats = [...validChats.map(chat => ({ ...chat, isValid: true })), ...newChats];
|
|
403
|
-
|
|
404
|
-
// Remove any duplicates that might have been created
|
|
405
|
-
const syncedChats = removeDuplicateChats(combinedChats);
|
|
406
|
-
|
|
407
|
-
// Save the synchronized chats
|
|
408
|
-
saveChats(syncedChats);
|
|
454
|
+
'@keyframes loadingDots': {
|
|
455
|
+
'0%': { content: '""' },
|
|
456
|
+
'25%': { content: '"."' },
|
|
457
|
+
'50%': { content: '".."' },
|
|
458
|
+
'75%': { content: '"..."' },
|
|
459
|
+
},
|
|
409
460
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
461
|
+
codeBlock: {
|
|
462
|
+
background: '#1e293b',
|
|
463
|
+
padding: '12px 16px',
|
|
464
|
+
borderRadius: '8px',
|
|
465
|
+
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
|
|
466
|
+
fontSize: '13px',
|
|
467
|
+
lineHeight: '1.6',
|
|
468
|
+
overflowX: 'auto' as const,
|
|
469
|
+
marginTop: '8px',
|
|
470
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
471
|
+
},
|
|
472
|
+
};
|
|
417
473
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
474
|
+
// Add custom style for loading animation
|
|
475
|
+
const styleSheet = document.createElement('style');
|
|
476
|
+
styleSheet.textContent = `
|
|
477
|
+
@keyframes loadingDots {
|
|
478
|
+
0%, 20% { content: "."; }
|
|
479
|
+
40% { content: ".."; }
|
|
480
|
+
60%, 100% { content: "..."; }
|
|
422
481
|
}
|
|
423
|
-
}
|
|
424
482
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
method: 'DELETE'
|
|
429
|
-
});
|
|
430
|
-
const data = await response.json();
|
|
431
|
-
|
|
432
|
-
if (data.success) {
|
|
433
|
-
// Remove from localStorage
|
|
434
|
-
const chats = loadChats().filter(chat => chat.id !== storyId);
|
|
435
|
-
saveChats(chats);
|
|
436
|
-
return true;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return false;
|
|
440
|
-
} catch (error) {
|
|
441
|
-
console.warn('Failed to delete story:', error);
|
|
442
|
-
return false;
|
|
483
|
+
.loading-dots::after {
|
|
484
|
+
content: ".";
|
|
485
|
+
animation: loadingDots 1.4s infinite;
|
|
443
486
|
}
|
|
444
|
-
|
|
487
|
+
`;
|
|
488
|
+
document.head.appendChild(styleSheet);
|
|
489
|
+
|
|
490
|
+
// Helper function to format timestamp
|
|
491
|
+
const formatTime = (timestamp: number): string => {
|
|
492
|
+
const date = new Date(timestamp);
|
|
493
|
+
const now = new Date();
|
|
494
|
+
const diffMs = now.getTime() - date.getTime();
|
|
495
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
496
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
497
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
498
|
+
|
|
499
|
+
if (diffMins < 1) return 'just now';
|
|
500
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
501
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
502
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
503
|
+
return date.toLocaleDateString();
|
|
504
|
+
};
|
|
445
505
|
|
|
446
|
-
|
|
447
|
-
|
|
506
|
+
// Main component
|
|
507
|
+
export function StoryUIPanel() {
|
|
448
508
|
const [input, setInput] = useState('');
|
|
509
|
+
const [conversation, setConversation] = useState<Message[]>([]);
|
|
449
510
|
const [loading, setLoading] = useState(false);
|
|
450
511
|
const [error, setError] = useState<string | null>(null);
|
|
451
512
|
const [recentChats, setRecentChats] = useState<ChatSession[]>([]);
|
|
452
513
|
const [activeChatId, setActiveChatId] = useState<string | null>(null);
|
|
453
514
|
const [activeTitle, setActiveTitle] = useState<string>('');
|
|
454
515
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
516
|
+
const [connectionStatus, setConnectionStatus] = useState<{ connected: boolean; error?: string }>({ connected: false });
|
|
455
517
|
const chatEndRef = useRef<HTMLDivElement | null>(null);
|
|
518
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
456
519
|
|
|
457
520
|
// Load and sync chats on mount
|
|
458
521
|
useEffect(() => {
|
|
459
522
|
const initializeChats = async () => {
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
523
|
+
// Test connection first
|
|
524
|
+
const connectionTest = await testMCPConnection();
|
|
525
|
+
setConnectionStatus(connectionTest);
|
|
526
|
+
|
|
527
|
+
if (connectionTest.connected) {
|
|
528
|
+
const syncedChats = await syncWithActualStories();
|
|
529
|
+
const sortedChats = syncedChats.sort((a, b) => b.lastUpdated - a.lastUpdated).slice(0, MAX_RECENT_CHATS);
|
|
530
|
+
setRecentChats(sortedChats);
|
|
531
|
+
|
|
532
|
+
if (sortedChats.length > 0) {
|
|
533
|
+
setConversation(sortedChats[0].conversation);
|
|
534
|
+
setActiveChatId(sortedChats[0].id);
|
|
535
|
+
setActiveTitle(sortedChats[0].title);
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
// Load from local storage if server is not available
|
|
539
|
+
const localChats = loadChats();
|
|
540
|
+
setRecentChats(localChats);
|
|
468
541
|
}
|
|
469
542
|
};
|
|
470
543
|
|
|
@@ -483,6 +556,17 @@ const StoryUIPanel: React.FC = () => {
|
|
|
483
556
|
if (!input.trim()) return;
|
|
484
557
|
setError(null);
|
|
485
558
|
setLoading(true);
|
|
559
|
+
|
|
560
|
+
// Test connection before sending
|
|
561
|
+
const connectionTest = await testMCPConnection();
|
|
562
|
+
setConnectionStatus(connectionTest);
|
|
563
|
+
|
|
564
|
+
if (!connectionTest.connected) {
|
|
565
|
+
setError(`Cannot connect to MCP server: ${connectionTest.error || 'Server not running'}`);
|
|
566
|
+
setLoading(false);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
486
570
|
const newConversation = [...conversation, { role: 'user' as const, content: input }];
|
|
487
571
|
setConversation(newConversation);
|
|
488
572
|
setInput('');
|
|
@@ -496,6 +580,14 @@ const StoryUIPanel: React.FC = () => {
|
|
|
496
580
|
fileName: activeChatId || undefined,
|
|
497
581
|
}),
|
|
498
582
|
});
|
|
583
|
+
|
|
584
|
+
// Check if response is JSON
|
|
585
|
+
const contentType = res.headers.get('content-type');
|
|
586
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
587
|
+
const text = await res.text();
|
|
588
|
+
throw new Error(`Server returned non-JSON response (likely server not running or wrong port). Response: ${text.substring(0, 200)}...`);
|
|
589
|
+
}
|
|
590
|
+
|
|
499
591
|
const data = await res.json();
|
|
500
592
|
if (!res.ok || !data.success) throw new Error(data.error || 'Story generation failed');
|
|
501
593
|
|
|
@@ -518,6 +610,9 @@ const StoryUIPanel: React.FC = () => {
|
|
|
518
610
|
responseMessage = `${statusIcon} Updated your story: "${data.title}"\n\nI've made the requested changes while keeping the same layout structure. You can view the updated component in Storybook.`;
|
|
519
611
|
} else {
|
|
520
612
|
responseMessage = `${statusIcon} Created new story: "${data.title}"\n\nI've generated the component with the requested features. You can view it in Storybook where you'll see both the rendered component and its markup in the Docs tab.`;
|
|
613
|
+
|
|
614
|
+
// IMPORTANT: Add a note about refreshing for new stories
|
|
615
|
+
responseMessage += '\n\n💡 **Note**: If you don\'t see the story immediately, you may need to refresh your Storybook page (Cmd/Ctrl + R) for new stories to appear in the sidebar.';
|
|
521
616
|
}
|
|
522
617
|
|
|
523
618
|
// Add validation information if there are issues
|
|
@@ -707,26 +802,23 @@ const StoryUIPanel: React.FC = () => {
|
|
|
707
802
|
...STYLES.sidebar,
|
|
708
803
|
...(sidebarOpen ? {} : STYLES.sidebarCollapsed),
|
|
709
804
|
}}>
|
|
710
|
-
<button
|
|
711
|
-
onClick={() => setSidebarOpen(o => !o)}
|
|
712
|
-
style={{
|
|
713
|
-
...STYLES.sidebarToggle,
|
|
714
|
-
...(sidebarOpen ? {} : { width: '40px', height: '40px', padding: '0' }),
|
|
715
|
-
}}
|
|
716
|
-
title={sidebarOpen ? 'Collapse sidebar' : 'Expand sidebar'}
|
|
717
|
-
onMouseEnter={(e) => {
|
|
718
|
-
e.currentTarget.style.transform = 'scale(1.05)';
|
|
719
|
-
e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.4)';
|
|
720
|
-
}}
|
|
721
|
-
onMouseLeave={(e) => {
|
|
722
|
-
e.currentTarget.style.transform = 'scale(1)';
|
|
723
|
-
e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.3)';
|
|
724
|
-
}}
|
|
725
|
-
>
|
|
726
|
-
{sidebarOpen ? '☰ Chats' : '☰'}
|
|
727
|
-
</button>
|
|
728
805
|
{sidebarOpen && (
|
|
729
|
-
<div style={{ flex: 1, overflowY: 'auto', padding: '
|
|
806
|
+
<div style={{ flex: 1, overflowY: 'auto', padding: '16px' }}>
|
|
807
|
+
<button
|
|
808
|
+
onClick={() => setSidebarOpen(false)}
|
|
809
|
+
style={STYLES.sidebarToggle}
|
|
810
|
+
title="Collapse sidebar"
|
|
811
|
+
onMouseEnter={(e) => {
|
|
812
|
+
e.currentTarget.style.transform = 'translateY(-1px)';
|
|
813
|
+
e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.4)';
|
|
814
|
+
}}
|
|
815
|
+
onMouseLeave={(e) => {
|
|
816
|
+
e.currentTarget.style.transform = 'translateY(0)';
|
|
817
|
+
e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.3)';
|
|
818
|
+
}}
|
|
819
|
+
>
|
|
820
|
+
☰ Chats
|
|
821
|
+
</button>
|
|
730
822
|
<button
|
|
731
823
|
onClick={handleNewChat}
|
|
732
824
|
style={STYLES.newChatButton}
|
|
@@ -749,79 +841,131 @@ const StoryUIPanel: React.FC = () => {
|
|
|
749
841
|
fontWeight: '500',
|
|
750
842
|
textTransform: 'uppercase',
|
|
751
843
|
letterSpacing: '0.05em',
|
|
752
|
-
fontFamily: STYLES.fontFamily,
|
|
753
844
|
}}>
|
|
754
|
-
Recent
|
|
845
|
+
Recent Chats
|
|
755
846
|
</div>
|
|
756
847
|
)}
|
|
757
848
|
{recentChats.map(chat => (
|
|
758
849
|
<div
|
|
759
850
|
key={chat.id}
|
|
851
|
+
onClick={() => handleSelectChat(chat)}
|
|
760
852
|
style={{
|
|
761
|
-
|
|
762
|
-
|
|
853
|
+
...STYLES.chatItem,
|
|
854
|
+
...(activeChatId === chat.id ? STYLES.chatItemActive : {}),
|
|
855
|
+
}}
|
|
856
|
+
onMouseEnter={(e) => {
|
|
857
|
+
if (activeChatId !== chat.id) {
|
|
858
|
+
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.12)';
|
|
859
|
+
}
|
|
860
|
+
const deleteBtn = e.currentTarget.querySelector('.delete-btn') as HTMLElement;
|
|
861
|
+
if (deleteBtn) deleteBtn.style.opacity = '1';
|
|
862
|
+
}}
|
|
863
|
+
onMouseLeave={(e) => {
|
|
864
|
+
if (activeChatId !== chat.id) {
|
|
865
|
+
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)';
|
|
866
|
+
}
|
|
867
|
+
const deleteBtn = e.currentTarget.querySelector('.delete-btn') as HTMLElement;
|
|
868
|
+
if (deleteBtn) deleteBtn.style.opacity = '0';
|
|
763
869
|
}}
|
|
764
870
|
>
|
|
871
|
+
<div style={STYLES.chatItemTitle}>{chat.title}</div>
|
|
872
|
+
<div style={STYLES.chatItemTime}>{formatTime(chat.lastUpdated)}</div>
|
|
765
873
|
<button
|
|
766
|
-
|
|
767
|
-
style={{
|
|
768
|
-
...STYLES.chatItem,
|
|
769
|
-
...(chat.id === activeChatId ? STYLES.chatItemActive : {}),
|
|
770
|
-
}}
|
|
771
|
-
title={chat.title}
|
|
772
|
-
onMouseEnter={(e) => {
|
|
773
|
-
if (chat.id !== activeChatId) {
|
|
774
|
-
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
|
|
775
|
-
}
|
|
776
|
-
}}
|
|
777
|
-
onMouseLeave={(e) => {
|
|
778
|
-
if (chat.id !== activeChatId) {
|
|
779
|
-
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)';
|
|
780
|
-
}
|
|
781
|
-
}}
|
|
782
|
-
>
|
|
783
|
-
{chat.title}
|
|
784
|
-
</button>
|
|
785
|
-
<button
|
|
874
|
+
className="delete-btn"
|
|
786
875
|
onClick={(e) => handleDeleteChat(chat.id, e)}
|
|
787
876
|
style={STYLES.deleteButton}
|
|
788
|
-
title="Delete
|
|
789
|
-
onMouseEnter={(e) => {
|
|
790
|
-
e.currentTarget.style.background = '#ef4444';
|
|
791
|
-
e.currentTarget.style.transform = 'translateY(-50%) scale(1.1)';
|
|
792
|
-
}}
|
|
793
|
-
onMouseLeave={(e) => {
|
|
794
|
-
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.8)';
|
|
795
|
-
e.currentTarget.style.transform = 'translateY(-50%) scale(1)';
|
|
796
|
-
}}
|
|
877
|
+
title="Delete chat"
|
|
797
878
|
>
|
|
798
|
-
|
|
879
|
+
✕
|
|
799
880
|
</button>
|
|
800
881
|
</div>
|
|
801
882
|
))}
|
|
802
883
|
</div>
|
|
803
884
|
)}
|
|
885
|
+
{!sidebarOpen && (
|
|
886
|
+
<div style={{ padding: '16px' }}>
|
|
887
|
+
<button
|
|
888
|
+
onClick={() => setSidebarOpen(true)}
|
|
889
|
+
style={{
|
|
890
|
+
...STYLES.sidebarToggle,
|
|
891
|
+
width: '40px',
|
|
892
|
+
height: '40px',
|
|
893
|
+
padding: '0',
|
|
894
|
+
fontSize: '16px',
|
|
895
|
+
}}
|
|
896
|
+
title="Expand sidebar"
|
|
897
|
+
onMouseEnter={(e) => {
|
|
898
|
+
e.currentTarget.style.transform = 'scale(1.05)';
|
|
899
|
+
e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.4)';
|
|
900
|
+
}}
|
|
901
|
+
onMouseLeave={(e) => {
|
|
902
|
+
e.currentTarget.style.transform = 'scale(1)';
|
|
903
|
+
e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.3)';
|
|
904
|
+
}}
|
|
905
|
+
>
|
|
906
|
+
☰
|
|
907
|
+
</button>
|
|
908
|
+
</div>
|
|
909
|
+
)}
|
|
804
910
|
</div>
|
|
805
911
|
|
|
806
|
-
{/* Main
|
|
807
|
-
<div style={STYLES.
|
|
808
|
-
<
|
|
912
|
+
{/* Main content */}
|
|
913
|
+
<div style={STYLES.mainContent}>
|
|
914
|
+
<div style={STYLES.chatHeader}>
|
|
915
|
+
<h1 style={{
|
|
916
|
+
fontSize: '24px',
|
|
917
|
+
margin: 0,
|
|
918
|
+
fontWeight: '600',
|
|
919
|
+
background: 'linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%)',
|
|
920
|
+
WebkitBackgroundClip: 'text',
|
|
921
|
+
WebkitTextFillColor: 'transparent',
|
|
922
|
+
display: 'inline-block'
|
|
923
|
+
}}>
|
|
924
|
+
Story UI
|
|
925
|
+
</h1>
|
|
926
|
+
<p style={{ fontSize: '14px', margin: '4px 0 0 0', color: '#94a3b8' }}>
|
|
927
|
+
Generate Storybook stories with AI
|
|
928
|
+
</p>
|
|
929
|
+
<div style={{
|
|
930
|
+
display: 'flex',
|
|
931
|
+
alignItems: 'center',
|
|
932
|
+
gap: '8px',
|
|
933
|
+
marginTop: '8px',
|
|
934
|
+
fontSize: '12px'
|
|
935
|
+
}}>
|
|
936
|
+
<div style={{
|
|
937
|
+
width: '8px',
|
|
938
|
+
height: '8px',
|
|
939
|
+
borderRadius: '50%',
|
|
940
|
+
backgroundColor: connectionStatus.connected ? '#10b981' : '#f87171'
|
|
941
|
+
}}></div>
|
|
942
|
+
<span style={{ color: connectionStatus.connected ? '#10b981' : '#f87171' }}>
|
|
943
|
+
{connectionStatus.connected
|
|
944
|
+
? `Connected to MCP server (port ${getApiPort()})`
|
|
945
|
+
: `Disconnected: ${connectionStatus.error || 'Server not running'}`
|
|
946
|
+
}
|
|
947
|
+
</span>
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
809
950
|
|
|
810
951
|
<div style={STYLES.chatContainer}>
|
|
811
|
-
{
|
|
952
|
+
{error && (
|
|
953
|
+
<div style={STYLES.errorMessage}>
|
|
954
|
+
{error}
|
|
955
|
+
</div>
|
|
956
|
+
)}
|
|
957
|
+
|
|
958
|
+
{conversation.length === 0 && !loading && (
|
|
812
959
|
<div style={STYLES.emptyState}>
|
|
813
|
-
<div style={STYLES.emptyStateTitle}>Start a conversation
|
|
960
|
+
<div style={STYLES.emptyStateTitle}>Start a new conversation</div>
|
|
814
961
|
<div style={STYLES.emptyStateSubtitle}>
|
|
815
|
-
|
|
962
|
+
Describe the UI component you'd like to create
|
|
816
963
|
</div>
|
|
817
964
|
</div>
|
|
818
965
|
)}
|
|
819
966
|
|
|
820
967
|
{conversation.map((msg, i) => (
|
|
821
|
-
<div key={i} style={
|
|
822
|
-
...STYLES.messageContainer,
|
|
823
|
-
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
|
824
|
-
}}>
|
|
968
|
+
<div key={i} style={STYLES.messageContainer}>
|
|
825
969
|
<div style={msg.role === 'user' ? STYLES.userMessage : STYLES.aiMessage}>
|
|
826
970
|
{msg.content}
|
|
827
971
|
</div>
|
|
@@ -829,16 +973,10 @@ const StoryUIPanel: React.FC = () => {
|
|
|
829
973
|
))}
|
|
830
974
|
|
|
831
975
|
{loading && (
|
|
832
|
-
<div style={
|
|
976
|
+
<div style={STYLES.messageContainer}>
|
|
833
977
|
<div style={STYLES.loadingMessage}>
|
|
834
|
-
<
|
|
835
|
-
|
|
836
|
-
height: '6px',
|
|
837
|
-
backgroundColor: '#6b7280',
|
|
838
|
-
borderRadius: '50%',
|
|
839
|
-
animation: 'pulse 1.5s ease-in-out infinite',
|
|
840
|
-
}}></div>
|
|
841
|
-
Generating...
|
|
978
|
+
<span>Generating story</span>
|
|
979
|
+
<span className="loading-dots"></span>
|
|
842
980
|
</div>
|
|
843
981
|
</div>
|
|
844
982
|
)}
|
|
@@ -848,12 +986,12 @@ const StoryUIPanel: React.FC = () => {
|
|
|
848
986
|
|
|
849
987
|
<form onSubmit={handleSend} style={STYLES.inputForm}>
|
|
850
988
|
<input
|
|
989
|
+
ref={inputRef}
|
|
851
990
|
type="text"
|
|
852
991
|
value={input}
|
|
853
992
|
onChange={e => setInput(e.target.value)}
|
|
854
|
-
placeholder="Describe
|
|
993
|
+
placeholder="Describe a UI component..."
|
|
855
994
|
style={STYLES.textInput}
|
|
856
|
-
disabled={loading}
|
|
857
995
|
onFocus={(e) => {
|
|
858
996
|
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
|
|
859
997
|
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.1)';
|
|
@@ -868,39 +1006,31 @@ const StoryUIPanel: React.FC = () => {
|
|
|
868
1006
|
disabled={loading || !input.trim()}
|
|
869
1007
|
style={{
|
|
870
1008
|
...STYLES.sendButton,
|
|
871
|
-
...(loading || !input.trim() ?
|
|
1009
|
+
...(loading || !input.trim() ? {
|
|
1010
|
+
opacity: 0.5,
|
|
1011
|
+
cursor: 'not-allowed',
|
|
1012
|
+
background: '#6b7280',
|
|
1013
|
+
boxShadow: 'none'
|
|
1014
|
+
} : {})
|
|
872
1015
|
}}
|
|
873
1016
|
onMouseEnter={(e) => {
|
|
874
1017
|
if (!loading && input.trim()) {
|
|
875
|
-
e.currentTarget.style.transform = '
|
|
1018
|
+
e.currentTarget.style.transform = 'scale(1.05)';
|
|
876
1019
|
e.currentTarget.style.boxShadow = '0 4px 16px rgba(16, 185, 129, 0.4)';
|
|
877
1020
|
}
|
|
878
1021
|
}}
|
|
879
1022
|
onMouseLeave={(e) => {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
e.currentTarget.style.boxShadow = '0 2px 8px rgba(16, 185, 129, 0.3)';
|
|
883
|
-
}
|
|
1023
|
+
e.currentTarget.style.transform = 'scale(1)';
|
|
1024
|
+
e.currentTarget.style.boxShadow = '0 2px 8px rgba(16, 185, 129, 0.3)';
|
|
884
1025
|
}}
|
|
885
1026
|
>
|
|
886
|
-
|
|
1027
|
+
<span>Send</span>
|
|
1028
|
+
<svg width={16} height={16} viewBox="0 0 24 24" fill="currentColor">
|
|
1029
|
+
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
1030
|
+
</svg>
|
|
887
1031
|
</button>
|
|
888
1032
|
</form>
|
|
889
|
-
|
|
890
|
-
{error && <div style={STYLES.errorMessage}>{error}</div>}
|
|
891
1033
|
</div>
|
|
892
|
-
|
|
893
|
-
{/* Add keyframes animation for loading pulse */}
|
|
894
|
-
<style>
|
|
895
|
-
{`
|
|
896
|
-
@keyframes pulse {
|
|
897
|
-
0%, 100% { opacity: 0.4; }
|
|
898
|
-
50% { opacity: 1; }
|
|
899
|
-
}
|
|
900
|
-
`}
|
|
901
|
-
</style>
|
|
902
1034
|
</div>
|
|
903
1035
|
);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
export default StoryUIPanel;
|
|
1036
|
+
}
|