@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.
Files changed (35) hide show
  1. package/.env.sample +3 -1
  2. package/README.md +160 -606
  3. package/dist/cli/index.js +23 -24
  4. package/dist/cli/setup.js +295 -36
  5. package/dist/mcp-server/index.js +67 -0
  6. package/dist/mcp-server/routes/generateStory.js +323 -56
  7. package/dist/story-generator/componentBlacklist.js +181 -0
  8. package/dist/story-generator/componentDiscovery.js +9 -2
  9. package/dist/story-generator/configLoader.js +109 -39
  10. package/dist/story-generator/considerationsLoader.js +204 -0
  11. package/dist/story-generator/documentation-sources.js +36 -0
  12. package/dist/story-generator/documentationLoader.js +214 -0
  13. package/dist/story-generator/dynamicPackageDiscovery.js +527 -0
  14. package/dist/story-generator/enhancedComponentDiscovery.js +369 -118
  15. package/dist/story-generator/generateStory.js +7 -3
  16. package/dist/story-generator/postProcessStory.js +71 -0
  17. package/dist/story-generator/promptGenerator.js +286 -37
  18. package/dist/story-generator/storyHistory.js +118 -0
  19. package/dist/story-generator/storyTracker.js +33 -18
  20. package/dist/story-generator/storyValidator.js +39 -0
  21. package/dist/story-generator/universalDesignSystemAdapter.js +209 -0
  22. package/dist/story-generator/validateStory.js +82 -7
  23. package/dist/story-ui.config.js +12 -5
  24. package/package.json +11 -6
  25. package/templates/StoryUI/StoryUIPanel.stories.tsx +29 -13
  26. package/templates/StoryUI/StoryUIPanel.tsx +489 -359
  27. package/templates/react-import-rule.json +36 -0
  28. package/templates/story-generation-rules.json +29 -0
  29. package/templates/story-ui-considerations.json +156 -0
  30. package/templates/story-ui-considerations.md +109 -0
  31. package/templates/story-ui-docs-README.md +55 -0
  32. package/dist/scripts/test-validation.js +0 -81
  33. package/dist/test-storybooks/chakra-test/src/components/index.js +0 -3
  34. package/dist/test-storybooks/custom-design-test/src/components/index.js +0 -3
  35. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -1,56 +1,224 @@
1
1
  import React, { useState, useRef, useEffect } from 'react';
2
2
 
3
- // Simple port configuration - defaults to 4001, configurable via window.STORY_UI_MCP_PORT
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; // fileName or hash
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
- // Professional, self-contained styles - no external dependencies
25
- const STYLES = {
26
- // Typography - Professional font stack like ChatGPT
27
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
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
- // Base container with CSS reset
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
- height: '600px',
40
- background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
41
- boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
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
- border: '1px solid rgba(255, 255, 255, 0.1)',
208
+ background: 'linear-gradient(135deg, #1e293b 0%, #334155 100%)',
209
+ color: '#e2e8f0',
44
210
  },
45
211
 
46
- // Sidebar styles
212
+ // Sidebar
47
213
  sidebar: {
48
214
  width: '280px',
49
- background: 'linear-gradient(180deg, #0f172a 0%, #1e293b 100%)',
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
- transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
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
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
229
+ width: '100%',
230
+ padding: '10px 16px',
62
231
  background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
63
- color: '#ffffff',
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
- boxShadow: '0 2px 8px rgba(59, 130, 246, 0.3)',
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: '12px 16px',
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: '#ffffff',
87
- fontWeight: '600',
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
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
96
- width: '100%',
97
- textAlign: 'left' as const,
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
- marginBottom: '4px',
269
+ position: 'relative' as const,
270
+ paddingRight: '40px',
111
271
  },
112
272
 
113
273
  chatItemActive: {
114
- border: '1px solid rgba(59, 130, 246, 0.5)',
115
- background: 'rgba(59, 130, 246, 0.1)',
116
- color: '#60a5fa',
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
- // Main chat area
121
- mainArea: {
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
- background: 'rgba(255, 255, 255, 0.02)',
313
+ overflow: 'hidden',
126
314
  },
127
315
 
128
- header: {
129
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
130
- color: '#f8fafc',
131
- margin: '0',
132
- padding: '24px 24px 16px 24px',
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
- background: 'rgba(0, 0, 0, 0.2)',
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
- color: '#ef4444',
267
- margin: '0 24px 16px 24px',
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
- border: '1px solid rgba(239, 68, 68, 0.3)',
272
- fontSize: '13px',
273
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
444
+ fontSize: '14px',
445
+ marginBottom: '16px',
446
+ border: '1px solid rgba(248, 113, 113, 0.3)',
274
447
  },
275
448
 
276
- deleteButton: {
277
- position: 'absolute' as const,
278
- right: '8px',
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
- function saveChats(chats: ChatSession[]) {
310
- localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(chats));
311
- }
312
-
313
- function removeDuplicateChats(chats: ChatSession[]): ChatSession[] {
314
- const seen = new Set<string>();
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
- console.log('Sync completed:', {
411
- totalStories: actualStories.length,
412
- existingChats: existingChats.length,
413
- validChats: validChats.length,
414
- newChats: newChats.length,
415
- finalChats: syncedChats.length
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
- return syncedChats;
419
- } catch (error) {
420
- console.warn('Failed to sync with server:', error);
421
- return loadChats();
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
- async function deleteStoryAndChat(storyId: string): Promise<boolean> {
426
- try {
427
- const response = await fetch(`${SYNC_API}/stories/${storyId}`, {
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
- const StoryUIPanel: React.FC = () => {
447
- const [conversation, setConversation] = useState<Message[]>([]);
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
- const syncedChats = await syncWithActualStories();
461
- const sortedChats = syncedChats.sort((a, b) => b.lastUpdated - a.lastUpdated).slice(0, MAX_RECENT_CHATS);
462
- setRecentChats(sortedChats);
463
-
464
- if (sortedChats.length > 0) {
465
- setConversation(sortedChats[0].conversation);
466
- setActiveChatId(sortedChats[0].id);
467
- setActiveTitle(sortedChats[0].title);
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: '0 16px 16px 16px' }}>
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
- position: 'relative',
762
- marginBottom: '4px',
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
- onClick={() => handleSelectChat(chat)}
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 story and chat"
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 chat area */}
807
- <div style={STYLES.mainArea}>
808
- <h2 style={STYLES.header}>Story UI: AI Story Generator</h2>
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
- {conversation.length === 0 && (
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 to generate a Storybook story</div>
960
+ <div style={STYLES.emptyStateTitle}>Start a new conversation</div>
814
961
  <div style={STYLES.emptyStateSubtitle}>
815
- (e.g. &quot;Build a login form with two fields and a button&quot;)
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={{ ...STYLES.messageContainer, justifyContent: 'flex-start' }}>
976
+ <div style={STYLES.messageContainer}>
833
977
  <div style={STYLES.loadingMessage}>
834
- <div style={{
835
- width: '6px',
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 your UI or give feedback..."
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() ? STYLES.sendButtonDisabled : {}),
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 = 'translateY(-1px)';
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
- if (!loading && input.trim()) {
881
- e.currentTarget.style.transform = 'translateY(0)';
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
- {loading ? '...' : 'Send'}
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
+ }