@tpitre/story-ui 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"generateStoryStream.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStoryStream.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAma5C,wBAAsB,6BAA6B,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,iBAob9E"}
1
+ {"version":3,"file":"generateStoryStream.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStoryStream.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAwa5C,wBAAsB,6BAA6B,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,iBAob9E"}
@@ -74,6 +74,11 @@ class StreamWriter {
74
74
  llmCallsCount: this.llmCalls
75
75
  }
76
76
  };
77
+ console.log('[Story UI Server DEBUG] sendCompletion called:', {
78
+ hasCode: !!fullCompletion.code,
79
+ codeLength: fullCompletion.code?.length,
80
+ title: fullCompletion.title
81
+ });
77
82
  this.send(createStreamEvent('completion', fullCompletion));
78
83
  }
79
84
  // Send error
@@ -1 +1 @@
1
- {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAkTA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,2BAA2B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACtD;CACF;AA6oCD,iBAAS,YAAY,4CAmtCpB;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAkTA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,2BAA2B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACtD;CACF;AAooCD,iBAAS,YAAY,4CAkvCpB;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -132,15 +132,32 @@ const titleToStoryPath = (title) => {
132
132
  return `generated-${kebabTitle}--default`;
133
133
  };
134
134
  // Helper to store generated code for the Source Code panel to display
135
- const storeGeneratedCode = (title, code) => {
136
- const storyPath = titleToStoryPath(title);
135
+ const storeGeneratedCode = (storyId, code, title) => {
137
136
  const topWindow = window.top || window;
138
137
  // Store code in the top window so it's accessible from manager frame
139
138
  if (!topWindow.__STORY_UI_GENERATED_CODE__) {
140
139
  topWindow.__STORY_UI_GENERATED_CODE__ = {};
141
140
  }
142
- topWindow.__STORY_UI_GENERATED_CODE__[storyPath] = code;
143
- console.log(`[Story UI] Stored code for story "${storyPath}" in window cache`);
141
+ // Store with story ID
142
+ topWindow.__STORY_UI_GENERATED_CODE__[storyId] = code;
143
+ // Also store in localStorage for persistence across page reloads
144
+ try {
145
+ const stored = JSON.parse(localStorage.getItem('storyui_generated_code') || '{}');
146
+ stored[storyId] = code;
147
+ // Also store with the title as key for easier lookup
148
+ if (title) {
149
+ const storyPath = titleToStoryPath(title);
150
+ stored[storyPath] = code;
151
+ stored[title] = code;
152
+ stored[title.replace(/\s+/g, '')] = code;
153
+ topWindow.__STORY_UI_GENERATED_CODE__[storyPath] = code;
154
+ }
155
+ localStorage.setItem('storyui_generated_code', JSON.stringify(stored));
156
+ console.log(`[Story UI] Stored code for story "${storyId}" in window cache and localStorage`);
157
+ }
158
+ catch (e) {
159
+ console.warn('[Story UI] Failed to store code in localStorage:', e);
160
+ }
144
161
  };
145
162
  // Helper to navigate to a newly created story after generation completes
146
163
  // In dev mode with HMR, this prevents the "Couldn't find story after HMR" error
@@ -150,7 +167,7 @@ const navigateToNewStory = (title, code, delayMs = 1500) => {
150
167
  console.log(`[Story UI] Will navigate to story "${storyPath}" in ${delayMs}ms...`);
151
168
  // Store the code for the Source Code panel if provided
152
169
  if (code) {
153
- storeGeneratedCode(title, code);
170
+ storeGeneratedCode(title, code, title);
154
171
  }
155
172
  setTimeout(() => {
156
173
  // Navigate the TOP window (parent Storybook UI), not the iframe
@@ -1033,11 +1050,11 @@ const StreamingProgressMessage = ({ streamingData }) => {
1033
1050
  if (error) {
1034
1051
  return (_jsx("div", { style: STYLES.streamingContainer, children: _jsxs("div", { style: { ...STYLES.validationBox, ...STYLES.validationError }, children: [_jsxs("strong", { children: ["\u274C ", error.message] }), error.details && _jsx("div", { style: { marginTop: '4px' }, children: error.details }), error.suggestion && _jsxs("div", { style: { marginTop: '8px' }, children: ["\uD83D\uDCA1 ", error.suggestion] })] }) }));
1035
1052
  }
1036
- // Show progress
1037
- return (_jsxs("div", { style: STYLES.streamingContainer, children: [intent && (_jsxs("div", { style: STYLES.intentPreview, children: [_jsxs("div", { style: STYLES.intentTitle, children: [intent.requestType === 'modification' ? '✏️' : '✨', intent.requestType === 'modification' ? ' Modifying Story' : ' Creating New Story'] }), _jsx("div", { style: STYLES.intentStrategy, children: intent.strategy }), intent.detectedDesignSystem && (_jsxs("div", { style: { fontSize: '12px', color: '#6b7280' }, children: ["Design system: ", _jsx("strong", { children: intent.detectedDesignSystem })] })), intent.estimatedComponents.length > 0 && (_jsx("div", { style: STYLES.intentComponents, children: intent.estimatedComponents.map((comp, i) => (_jsx("span", { style: STYLES.componentTag, children: comp }, i))) }))] })), progress && (_jsxs(_Fragment, { children: [_jsx("div", { style: STYLES.progressBar, children: _jsx("div", { style: {
1053
+ // Show progress - simplified to just show status without verbose details
1054
+ return (_jsxs("div", { style: STYLES.streamingContainer, children: [_jsxs("div", { style: STYLES.intentPreview, children: [_jsxs("div", { style: STYLES.progressPhase, children: [_jsx("span", { style: STYLES.phaseIcon, children: "\uD83E\uDD16" }), _jsx("span", { children: "AI is generating your story..." }), progress && (_jsxs("span", { style: { marginLeft: 'auto', color: '#9ca3af' }, children: [progress.step, "/", progress.totalSteps] }))] }), progress && (_jsx("div", { style: { ...STYLES.progressBar, marginTop: '8px' }, children: _jsx("div", { style: {
1038
1055
  ...STYLES.progressFill,
1039
1056
  width: `${(progress.step / progress.totalSteps) * 100}%`
1040
- } }) }), _jsxs("div", { style: STYLES.progressPhase, children: [_jsx("span", { style: STYLES.phaseIcon, children: getPhaseInfo(progress.phase).icon }), _jsx("span", { children: progress.message || getPhaseInfo(progress.phase).text }), _jsxs("span", { style: { marginLeft: 'auto', color: '#9ca3af' }, children: [progress.step, "/", progress.totalSteps] })] })] })), retry && (_jsxs("div", { style: STYLES.retryBadge, children: ["\uD83D\uDD04 Retry ", retry.attempt, "/", retry.maxAttempts, ": ", retry.reason] })), validation && !validation.isValid && (_jsxs("div", { style: { ...STYLES.validationBox, ...STYLES.validationWarning }, children: [validation.autoFixApplied ? '🔧 Auto-fixing issues...' : '⚠️ Validation issues found', validation.errors.slice(0, 2).map((err, i) => (_jsxs("div", { style: { marginTop: '4px', fontSize: '11px' }, children: ["\u2022 ", err] }, i)))] })), !progress && !intent && (_jsx("div", { style: STYLES.progressPhase, children: _jsx("span", { className: "loading-dots", children: "Connecting" }) }))] }));
1057
+ } }) }))] }), retry && (_jsxs("div", { style: STYLES.retryBadge, children: ["\uD83D\uDD04 Retry ", retry.attempt, "/", retry.maxAttempts, ": ", retry.reason] })), !progress && !intent && (_jsx("div", { style: STYLES.progressPhase, children: _jsx("span", { className: "loading-dots", children: "Connecting" }) }))] }));
1041
1058
  };
1042
1059
  // Main component
1043
1060
  function StoryUIPanel() {
@@ -1443,6 +1460,10 @@ function StoryUIPanel() {
1443
1460
  }
1444
1461
  saveChats(chats);
1445
1462
  setRecentChats(chats);
1463
+ // Store code for Source Code panel
1464
+ if (completion.code) {
1465
+ storeGeneratedCode(activeChatId, completion.code, activeTitle || completion.title);
1466
+ }
1446
1467
  }
1447
1468
  else {
1448
1469
  const chatId = completion.storyId || completion.fileName || Date.now().toString();
@@ -1467,6 +1488,10 @@ function StoryUIPanel() {
1467
1488
  // This prevents the "Couldn't find story after HMR" error by refreshing
1468
1489
  // after the file system has been updated and HMR has processed the change
1469
1490
  navigateToNewStory(chatTitle, completion.code);
1491
+ // Store code for Source Code panel
1492
+ if (completion.code) {
1493
+ storeGeneratedCode(chatId, completion.code, chatTitle);
1494
+ }
1470
1495
  }
1471
1496
  }, [activeChatId, activeTitle, conversation.length]);
1472
1497
  const handleSend = async (e) => {
@@ -1639,6 +1664,10 @@ function StoryUIPanel() {
1639
1664
  chats[chatIndex] = updatedSession;
1640
1665
  saveChats(chats);
1641
1666
  setRecentChats(chats);
1667
+ // Store code for Source Code panel
1668
+ if (data.code) {
1669
+ storeGeneratedCode(activeChatId, data.code, activeTitle || data.title);
1670
+ }
1642
1671
  }
1643
1672
  else {
1644
1673
  const chatId = data.storyId || data.fileName || Date.now().toString();
@@ -1660,6 +1689,10 @@ function StoryUIPanel() {
1660
1689
  setRecentChats(chats);
1661
1690
  // Auto-navigate to the newly created story
1662
1691
  navigateToNewStory(chatTitle, data.code);
1692
+ // Store code for Source Code panel
1693
+ if (data.code) {
1694
+ storeGeneratedCode(chatId, data.code, chatTitle);
1695
+ }
1663
1696
  }
1664
1697
  }
1665
1698
  catch (fallbackErr) {
@@ -1706,14 +1739,19 @@ function StoryUIPanel() {
1706
1739
  chats[chatIndex] = updatedSession;
1707
1740
  saveChats(chats);
1708
1741
  setRecentChats(chats);
1742
+ // Store code for Source Code panel
1743
+ if (data.code) {
1744
+ storeGeneratedCode(activeChatId, data.code, activeTitle || data.title);
1745
+ }
1709
1746
  }
1710
1747
  else {
1711
1748
  const chatId = data.storyId || data.fileName || Date.now().toString();
1749
+ const chatTitle = data.title || userInput;
1712
1750
  setActiveChatId(chatId);
1713
- setActiveTitle(data.title || userInput);
1751
+ setActiveTitle(chatTitle);
1714
1752
  const newSession = {
1715
1753
  id: chatId,
1716
- title: data.title || userInput,
1754
+ title: chatTitle,
1717
1755
  fileName: data.fileName || '',
1718
1756
  conversation: updatedConversation,
1719
1757
  lastUpdated: Date.now(),
@@ -1724,6 +1762,10 @@ function StoryUIPanel() {
1724
1762
  chats.splice(MAX_RECENT_CHATS);
1725
1763
  saveChats(chats);
1726
1764
  setRecentChats(chats);
1765
+ // Store code for Source Code panel
1766
+ if (data.code) {
1767
+ storeGeneratedCode(chatId, data.code, chatTitle);
1768
+ }
1727
1769
  }
1728
1770
  }
1729
1771
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -311,16 +311,36 @@ declare global {
311
311
  }
312
312
 
313
313
  // Helper to store generated code for the Source Code panel to display
314
- const storeGeneratedCode = (title: string, code: string) => {
315
- const storyPath = titleToStoryPath(title);
314
+ const storeGeneratedCode = (storyId: string, code: string, title?: string) => {
316
315
  const topWindow = window.top || window;
317
316
 
318
317
  // Store code in the top window so it's accessible from manager frame
319
318
  if (!topWindow.__STORY_UI_GENERATED_CODE__) {
320
319
  topWindow.__STORY_UI_GENERATED_CODE__ = {};
321
320
  }
322
- topWindow.__STORY_UI_GENERATED_CODE__[storyPath] = code;
323
- console.log(`[Story UI] Stored code for story "${storyPath}" in window cache`);
321
+
322
+ // Store with story ID
323
+ topWindow.__STORY_UI_GENERATED_CODE__[storyId] = code;
324
+
325
+ // Also store in localStorage for persistence across page reloads
326
+ try {
327
+ const stored = JSON.parse(localStorage.getItem('storyui_generated_code') || '{}');
328
+ stored[storyId] = code;
329
+
330
+ // Also store with the title as key for easier lookup
331
+ if (title) {
332
+ const storyPath = titleToStoryPath(title);
333
+ stored[storyPath] = code;
334
+ stored[title] = code;
335
+ stored[title.replace(/\s+/g, '')] = code;
336
+ topWindow.__STORY_UI_GENERATED_CODE__[storyPath] = code;
337
+ }
338
+
339
+ localStorage.setItem('storyui_generated_code', JSON.stringify(stored));
340
+ console.log(`[Story UI] Stored code for story "${storyId}" in window cache and localStorage`);
341
+ } catch (e) {
342
+ console.warn('[Story UI] Failed to store code in localStorage:', e);
343
+ }
324
344
  };
325
345
 
326
346
  // Helper to navigate to a newly created story after generation completes
@@ -332,7 +352,7 @@ const navigateToNewStory = (title: string, code?: string, delayMs: number = 1500
332
352
 
333
353
  // Store the code for the Source Code panel if provided
334
354
  if (code) {
335
- storeGeneratedCode(title, code);
355
+ storeGeneratedCode(title, code, title);
336
356
  }
337
357
 
338
358
  setTimeout(() => {
@@ -1398,36 +1418,24 @@ const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({
1398
1418
  );
1399
1419
  }
1400
1420
 
1401
- // Show progress
1421
+ // Show progress - simplified to just show status without verbose details
1402
1422
  return (
1403
1423
  <div style={STYLES.streamingContainer}>
1404
- {/* Intent Preview */}
1405
- {intent && (
1406
- <div style={STYLES.intentPreview}>
1407
- <div style={STYLES.intentTitle}>
1408
- {intent.requestType === 'modification' ? '✏️' : '✨'}
1409
- {intent.requestType === 'modification' ? ' Modifying Story' : ' Creating New Story'}
1410
- </div>
1411
- <div style={STYLES.intentStrategy}>{intent.strategy}</div>
1412
- {intent.detectedDesignSystem && (
1413
- <div style={{ fontSize: '12px', color: '#6b7280' }}>
1414
- Design system: <strong>{intent.detectedDesignSystem}</strong>
1415
- </div>
1416
- )}
1417
- {intent.estimatedComponents.length > 0 && (
1418
- <div style={STYLES.intentComponents}>
1419
- {intent.estimatedComponents.map((comp, i) => (
1420
- <span key={i} style={STYLES.componentTag}>{comp}</span>
1421
- ))}
1422
- </div>
1424
+ {/* Simple progress indicator */}
1425
+ <div style={STYLES.intentPreview}>
1426
+ <div style={STYLES.progressPhase}>
1427
+ <span style={STYLES.phaseIcon}>🤖</span>
1428
+ <span>AI is generating your story...</span>
1429
+ {progress && (
1430
+ <span style={{ marginLeft: 'auto', color: '#9ca3af' }}>
1431
+ {progress.step}/{progress.totalSteps}
1432
+ </span>
1423
1433
  )}
1424
1434
  </div>
1425
- )}
1426
1435
 
1427
- {/* Progress Bar */}
1428
- {progress && (
1429
- <>
1430
- <div style={STYLES.progressBar}>
1436
+ {/* Progress Bar */}
1437
+ {progress && (
1438
+ <div style={{ ...STYLES.progressBar, marginTop: '8px' }}>
1431
1439
  <div
1432
1440
  style={{
1433
1441
  ...STYLES.progressFill,
@@ -1435,33 +1443,16 @@ const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({
1435
1443
  }}
1436
1444
  />
1437
1445
  </div>
1438
- <div style={STYLES.progressPhase}>
1439
- <span style={STYLES.phaseIcon}>{getPhaseInfo(progress.phase).icon}</span>
1440
- <span>{progress.message || getPhaseInfo(progress.phase).text}</span>
1441
- <span style={{ marginLeft: 'auto', color: '#9ca3af' }}>
1442
- {progress.step}/{progress.totalSteps}
1443
- </span>
1444
- </div>
1445
- </>
1446
- )}
1446
+ )}
1447
+ </div>
1447
1448
 
1448
- {/* Retry Badge */}
1449
+ {/* Retry Badge - only show if retrying */}
1449
1450
  {retry && (
1450
1451
  <div style={STYLES.retryBadge}>
1451
1452
  🔄 Retry {retry.attempt}/{retry.maxAttempts}: {retry.reason}
1452
1453
  </div>
1453
1454
  )}
1454
1455
 
1455
- {/* Validation Feedback */}
1456
- {validation && !validation.isValid && (
1457
- <div style={{ ...STYLES.validationBox, ...STYLES.validationWarning }}>
1458
- {validation.autoFixApplied ? '🔧 Auto-fixing issues...' : '⚠️ Validation issues found'}
1459
- {validation.errors.slice(0, 2).map((err, i) => (
1460
- <div key={i} style={{ marginTop: '4px', fontSize: '11px' }}>• {err}</div>
1461
- ))}
1462
- </div>
1463
- )}
1464
-
1465
1456
  {/* Loading indicator when no specific phase */}
1466
1457
  {!progress && !intent && (
1467
1458
  <div style={STYLES.progressPhase}>
@@ -1942,6 +1933,11 @@ function StoryUIPanel() {
1942
1933
  }
1943
1934
  saveChats(chats);
1944
1935
  setRecentChats(chats);
1936
+
1937
+ // Store code for Source Code panel
1938
+ if (completion.code) {
1939
+ storeGeneratedCode(activeChatId, completion.code, activeTitle || completion.title);
1940
+ }
1945
1941
  } else {
1946
1942
  const chatId = completion.storyId || completion.fileName || Date.now().toString();
1947
1943
  const chatTitle = completion.title || userInput;
@@ -1968,6 +1964,11 @@ function StoryUIPanel() {
1968
1964
  // This prevents the "Couldn't find story after HMR" error by refreshing
1969
1965
  // after the file system has been updated and HMR has processed the change
1970
1966
  navigateToNewStory(chatTitle, completion.code);
1967
+
1968
+ // Store code for Source Code panel
1969
+ if (completion.code) {
1970
+ storeGeneratedCode(chatId, completion.code, chatTitle);
1971
+ }
1971
1972
  }
1972
1973
  }, [activeChatId, activeTitle, conversation.length]);
1973
1974
 
@@ -2158,6 +2159,11 @@ function StoryUIPanel() {
2158
2159
  if (chatIndex !== -1) chats[chatIndex] = updatedSession;
2159
2160
  saveChats(chats);
2160
2161
  setRecentChats(chats);
2162
+
2163
+ // Store code for Source Code panel
2164
+ if (data.code) {
2165
+ storeGeneratedCode(activeChatId, data.code, activeTitle || data.title);
2166
+ }
2161
2167
  } else {
2162
2168
  const chatId = data.storyId || data.fileName || Date.now().toString();
2163
2169
  const chatTitle = data.title || userInput;
@@ -2178,6 +2184,11 @@ function StoryUIPanel() {
2178
2184
 
2179
2185
  // Auto-navigate to the newly created story
2180
2186
  navigateToNewStory(chatTitle, data.code);
2187
+
2188
+ // Store code for Source Code panel
2189
+ if (data.code) {
2190
+ storeGeneratedCode(chatId, data.code, chatTitle);
2191
+ }
2181
2192
  }
2182
2193
  } catch (fallbackErr: unknown) {
2183
2194
  const errorMessage = fallbackErr instanceof Error ? fallbackErr.message : 'Unknown error';
@@ -2223,13 +2234,19 @@ function StoryUIPanel() {
2223
2234
  if (chatIndex !== -1) chats[chatIndex] = updatedSession;
2224
2235
  saveChats(chats);
2225
2236
  setRecentChats(chats);
2237
+
2238
+ // Store code for Source Code panel
2239
+ if (data.code) {
2240
+ storeGeneratedCode(activeChatId, data.code, activeTitle || data.title);
2241
+ }
2226
2242
  } else {
2227
2243
  const chatId = data.storyId || data.fileName || Date.now().toString();
2244
+ const chatTitle = data.title || userInput;
2228
2245
  setActiveChatId(chatId);
2229
- setActiveTitle(data.title || userInput);
2246
+ setActiveTitle(chatTitle);
2230
2247
  const newSession: ChatSession = {
2231
2248
  id: chatId,
2232
- title: data.title || userInput,
2249
+ title: chatTitle,
2233
2250
  fileName: data.fileName || '',
2234
2251
  conversation: updatedConversation,
2235
2252
  lastUpdated: Date.now(),
@@ -2239,6 +2256,11 @@ function StoryUIPanel() {
2239
2256
  if (chats.length > MAX_RECENT_CHATS) chats.splice(MAX_RECENT_CHATS);
2240
2257
  saveChats(chats);
2241
2258
  setRecentChats(chats);
2259
+
2260
+ // Store code for Source Code panel
2261
+ if (data.code) {
2262
+ storeGeneratedCode(chatId, data.code, chatTitle);
2263
+ }
2242
2264
  }
2243
2265
  } catch (err: unknown) {
2244
2266
  const errorMessage = err instanceof Error ? err.message : 'Unknown error';
@@ -20,6 +20,29 @@ const EVENTS = {
20
20
  STORY_SELECTED: `${ADDON_ID}/story-selected`,
21
21
  };
22
22
 
23
+ /**
24
+ * Get the API base URL for story operations.
25
+ * Works in both local development and production (Railway).
26
+ */
27
+ const getApiBaseUrl = (): string => {
28
+ if (typeof window === 'undefined') return 'http://localhost:4001';
29
+
30
+ // Check for Railway production domain - use same-origin requests
31
+ const hostname = window.location.hostname;
32
+ if (hostname.includes('.railway.app')) {
33
+ return '';
34
+ }
35
+
36
+ // Check for window overrides (local development)
37
+ const windowOverride = (window as any).__STORY_UI_PORT__;
38
+ if (windowOverride) return `http://localhost:${windowOverride}`;
39
+
40
+ const mcpOverride = (window as any).STORY_UI_MCP_PORT;
41
+ if (mcpOverride) return `http://localhost:${mcpOverride}`;
42
+
43
+ return 'http://localhost:4001';
44
+ };
45
+
23
46
  // Extend Window to include generated stories cache
24
47
  declare global {
25
48
  interface Window {
@@ -39,6 +62,47 @@ declare global {
39
62
  * <Button>Click</Button>
40
63
  */
41
64
  const extractUsageCode = (fullStoryCode: string, variantName?: string): string => {
65
+ // Helper function to convert object-style props to JSX attribute syntax
66
+ // e.g., "color: 'blue', variant: 'filled'" -> 'color="blue" variant="filled"'
67
+ const convertToJsxAttributes = (propsStr: string): string => {
68
+ if (!propsStr.trim()) return '';
69
+
70
+ const attributes: string[] = [];
71
+ // Match key: value pairs, handling strings, booleans, numbers, and expressions
72
+ // Pattern: key: 'value' or key: "value" or key: true/false or key: 123 or key: expression
73
+ const propRegex = /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|(\btrue\b|\bfalse\b)|(\d+(?:\.\d+)?)|(\{[^}]+\})|([^,}\s]+))/g;
74
+
75
+ let match;
76
+ while ((match = propRegex.exec(propsStr)) !== null) {
77
+ const key = match[1];
78
+ const stringValueSingle = match[2]; // 'value'
79
+ const stringValueDouble = match[3]; // "value"
80
+ const boolValue = match[4]; // true/false
81
+ const numValue = match[5]; // 123 or 1.5
82
+ const objValue = match[6]; // {expression}
83
+ const otherValue = match[7]; // other expressions
84
+
85
+ if (stringValueSingle !== undefined) {
86
+ attributes.push(`${key}="${stringValueSingle}"`);
87
+ } else if (stringValueDouble !== undefined) {
88
+ attributes.push(`${key}="${stringValueDouble}"`);
89
+ } else if (boolValue !== undefined) {
90
+ if (boolValue === 'true') {
91
+ attributes.push(key); // Just the prop name for true (e.g., fullWidth)
92
+ }
93
+ // Skip false values - they're the default and don't need to be shown
94
+ } else if (numValue !== undefined) {
95
+ attributes.push(`${key}={${numValue}}`);
96
+ } else if (objValue !== undefined) {
97
+ attributes.push(`${key}=${objValue}`);
98
+ } else if (otherValue !== undefined) {
99
+ attributes.push(`${key}={${otherValue}}`);
100
+ }
101
+ }
102
+
103
+ return attributes.join(' ');
104
+ };
105
+
42
106
  // Helper function to generate JSX from args
43
107
  const generateJsxFromArgs = (argsStr: string, componentName: string): string | null => {
44
108
  try {
@@ -46,19 +110,22 @@ const extractUsageCode = (fullStoryCode: string, variantName?: string): string =
46
110
  const childrenMatch = argsStr.match(/children:\s*['"`]([^'"`]+)['"`]/);
47
111
  const children = childrenMatch ? childrenMatch[1] : '';
48
112
 
49
- // Extract other props (simplified)
113
+ // Extract other props (remove children first)
50
114
  const propsStr = argsStr
51
115
  .replace(/children:\s*['"`][^'"`]*['"`],?/, '') // Remove children
52
116
  .replace(/^\{|\}$/g, '') // Remove braces
53
117
  .trim();
54
118
 
119
+ // Convert to JSX attribute syntax
120
+ const jsxAttributes = convertToJsxAttributes(propsStr);
121
+
55
122
  if (children) {
56
- if (propsStr) {
57
- return `<${componentName} ${propsStr.replace(/,\s*$/, '')}>${children}</${componentName}>`;
123
+ if (jsxAttributes) {
124
+ return `<${componentName} ${jsxAttributes}>${children}</${componentName}>`;
58
125
  }
59
126
  return `<${componentName}>${children}</${componentName}>`;
60
- } else if (propsStr) {
61
- return `<${componentName} ${propsStr.replace(/,\s*$/, '')} />`;
127
+ } else if (jsxAttributes) {
128
+ return `<${componentName} ${jsxAttributes} />`;
62
129
  }
63
130
  return `<${componentName} />`;
64
131
  } catch {
@@ -350,6 +417,26 @@ const styles = {
350
417
  color: '#888',
351
418
  marginTop: '4px',
352
419
  },
420
+ deleteButton: {
421
+ background: 'transparent',
422
+ color: '#C75050',
423
+ border: '1px solid #C75050',
424
+ borderRadius: '4px',
425
+ padding: '4px 10px',
426
+ fontSize: '11px',
427
+ cursor: 'pointer',
428
+ fontWeight: 500,
429
+ },
430
+ deleteButtonHover: {
431
+ background: '#C75050',
432
+ color: 'white',
433
+ },
434
+ deleteButtonDeleting: {
435
+ background: '#555',
436
+ color: '#888',
437
+ border: '1px solid #555',
438
+ cursor: 'not-allowed',
439
+ },
353
440
  };
354
441
 
355
442
  /**
@@ -362,6 +449,8 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
362
449
  const [storyTitle, setStoryTitle] = useState<string>('');
363
450
  const [copied, setCopied] = useState(false);
364
451
  const [showFullCode, setShowFullCode] = useState(false);
452
+ const [isDeleting, setIsDeleting] = useState(false);
453
+ const [deleteHover, setDeleteHover] = useState(false);
365
454
 
366
455
  // Get the current story ID
367
456
  const currentStoryId = state?.storyId;
@@ -391,6 +480,88 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
391
480
  return sourceCode && usageCode && usageCode !== sourceCode;
392
481
  }, [sourceCode, usageCode]);
393
482
 
483
+ // Check if this is a generated story (for showing delete button)
484
+ const isGeneratedStory = useMemo(() => {
485
+ return currentStoryId?.includes('generated') || false;
486
+ }, [currentStoryId]);
487
+
488
+ // Extract story file ID from the Storybook story ID
489
+ // e.g., "generated-simple-test-button--primary" -> "SimpleTestButton"
490
+ const getStoryFileId = useCallback((storyId: string): string => {
491
+ // Remove "generated-" prefix and variant suffix
492
+ const match = storyId.match(/^generated[-\/]?(.+?)(?:--.*)?$/i);
493
+ if (match) {
494
+ // Convert kebab-case to PascalCase: "simple-test-button" -> "SimpleTestButton"
495
+ const words = match[1].split('-');
496
+ return words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
497
+ }
498
+ return storyId;
499
+ }, []);
500
+
501
+ // Handle delete story
502
+ const handleDelete = useCallback(async () => {
503
+ if (!currentStoryId || !isGeneratedStory || isDeleting) return;
504
+
505
+ const confirmed = window.confirm('Delete this generated story? This cannot be undone.');
506
+ if (!confirmed) return;
507
+
508
+ setIsDeleting(true);
509
+
510
+ try {
511
+ const apiBase = getApiBaseUrl();
512
+ const storyFileId = getStoryFileId(currentStoryId);
513
+
514
+ console.log('[Source Code Panel] Deleting story:', { currentStoryId, storyFileId, apiBase });
515
+
516
+ // Try the RESTful DELETE endpoint first
517
+ const response = await fetch(`${apiBase}/story-ui/stories/${storyFileId}`, {
518
+ method: 'DELETE',
519
+ headers: { 'Content-Type': 'application/json' },
520
+ });
521
+
522
+ if (response.ok) {
523
+ console.log('[Source Code Panel] Story deleted successfully');
524
+
525
+ // Clear local cache
526
+ const topWindow = window.top || window;
527
+ if (topWindow.__STORY_UI_GENERATED_CODE__) {
528
+ delete topWindow.__STORY_UI_GENERATED_CODE__[currentStoryId];
529
+ }
530
+ if (window.__STORY_UI_GENERATED_CODE__) {
531
+ delete window.__STORY_UI_GENERATED_CODE__[currentStoryId];
532
+ }
533
+
534
+ // Clear localStorage cache
535
+ try {
536
+ const stored = JSON.parse(localStorage.getItem('storyui_generated_code') || '{}');
537
+ delete stored[storyFileId];
538
+ delete stored[currentStoryId];
539
+ localStorage.setItem('storyui_generated_code', JSON.stringify(stored));
540
+ } catch (e) {
541
+ console.warn('[Source Code Panel] Failed to clear localStorage:', e);
542
+ }
543
+
544
+ // Clear the source code display
545
+ setSourceCode('');
546
+
547
+ // Navigate to a different story (Story UI Generator default)
548
+ api.selectStory('story-ui-story-generator--default');
549
+
550
+ // Trigger Storybook refresh to update sidebar
551
+ window.location.reload();
552
+ } else {
553
+ const errorData = await response.json().catch(() => ({}));
554
+ console.error('[Source Code Panel] Delete failed:', errorData);
555
+ alert(`Failed to delete story: ${errorData.error || 'Unknown error'}`);
556
+ }
557
+ } catch (error) {
558
+ console.error('[Source Code Panel] Delete error:', error);
559
+ alert('Failed to delete story. Check console for details.');
560
+ } finally {
561
+ setIsDeleting(false);
562
+ }
563
+ }, [currentStoryId, isGeneratedStory, isDeleting, api, getStoryFileId]);
564
+
394
565
  // Try to get source code from the story
395
566
  useEffect(() => {
396
567
  if (!currentStoryId || !active) return;
@@ -624,15 +795,33 @@ const SourceCodePanel: React.FC<{ active?: boolean }> = ({ active }) => {
624
795
  </button>
625
796
  )}
626
797
  </div>
627
- <button
628
- style={{
629
- ...styles.copyButton,
630
- background: copied ? '#16825D' : '#0E639C',
631
- }}
632
- onClick={handleCopy}
633
- >
634
- {copied ? 'Copied!' : 'Copy'}
635
- </button>
798
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
799
+ <button
800
+ style={{
801
+ ...styles.copyButton,
802
+ background: copied ? '#16825D' : '#0E639C',
803
+ }}
804
+ onClick={handleCopy}
805
+ >
806
+ {copied ? 'Copied!' : 'Copy'}
807
+ </button>
808
+ {isGeneratedStory && (
809
+ <button
810
+ style={{
811
+ ...styles.deleteButton,
812
+ ...(isDeleting ? styles.deleteButtonDeleting : {}),
813
+ ...(deleteHover && !isDeleting ? styles.deleteButtonHover : {}),
814
+ }}
815
+ onClick={handleDelete}
816
+ onMouseEnter={() => setDeleteHover(true)}
817
+ onMouseLeave={() => setDeleteHover(false)}
818
+ disabled={isDeleting}
819
+ title="Delete this generated story"
820
+ >
821
+ {isDeleting ? 'Deleting...' : 'Delete'}
822
+ </button>
823
+ )}
824
+ </div>
636
825
  </div>
637
826
  <div style={styles.codeContainer}>
638
827
  <SyntaxHighlighter code={displayCode} />