@tpitre/story-ui 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/.env.sample +82 -11
  2. package/README.md +89 -0
  3. package/dist/cli/deploy.d.ts +17 -0
  4. package/dist/cli/deploy.d.ts.map +1 -0
  5. package/dist/cli/deploy.js +696 -0
  6. package/dist/cli/index.d.ts +3 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +26 -2
  9. package/dist/cli/setup.d.ts +11 -0
  10. package/dist/cli/setup.d.ts.map +1 -0
  11. package/dist/cli/setup.js +437 -110
  12. package/dist/mcp-server/index.d.ts +2 -0
  13. package/dist/mcp-server/index.d.ts.map +1 -0
  14. package/dist/mcp-server/index.js +120 -2
  15. package/dist/mcp-server/mcp-stdio-server.d.ts +3 -0
  16. package/dist/mcp-server/mcp-stdio-server.d.ts.map +1 -0
  17. package/dist/mcp-server/mcp-stdio-server.js +8 -1
  18. package/dist/mcp-server/routes/claude.d.ts +3 -0
  19. package/dist/mcp-server/routes/claude.d.ts.map +1 -0
  20. package/dist/mcp-server/routes/claude.js +60 -23
  21. package/dist/mcp-server/routes/components.d.ts +4 -0
  22. package/dist/mcp-server/routes/components.d.ts.map +1 -0
  23. package/dist/mcp-server/routes/frameworks.d.ts +38 -0
  24. package/dist/mcp-server/routes/frameworks.d.ts.map +1 -0
  25. package/dist/mcp-server/routes/frameworks.js +183 -0
  26. package/dist/mcp-server/routes/generateStory.d.ts +3 -0
  27. package/dist/mcp-server/routes/generateStory.d.ts.map +1 -0
  28. package/dist/mcp-server/routes/generateStory.js +160 -76
  29. package/dist/mcp-server/routes/generateStoryStream.d.ts +12 -0
  30. package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -0
  31. package/dist/mcp-server/routes/generateStoryStream.js +947 -0
  32. package/dist/mcp-server/routes/hybridStories.d.ts +18 -0
  33. package/dist/mcp-server/routes/hybridStories.d.ts.map +1 -0
  34. package/dist/mcp-server/routes/mcpRemote.d.ts +14 -0
  35. package/dist/mcp-server/routes/mcpRemote.d.ts.map +1 -0
  36. package/dist/mcp-server/routes/mcpRemote.js +489 -0
  37. package/dist/mcp-server/routes/memoryStories.d.ts +26 -0
  38. package/dist/mcp-server/routes/memoryStories.d.ts.map +1 -0
  39. package/dist/mcp-server/routes/providers.d.ts +89 -0
  40. package/dist/mcp-server/routes/providers.d.ts.map +1 -0
  41. package/dist/mcp-server/routes/providers.js +369 -0
  42. package/dist/mcp-server/routes/storySync.d.ts +26 -0
  43. package/dist/mcp-server/routes/storySync.d.ts.map +1 -0
  44. package/dist/mcp-server/routes/streamTypes.d.ts +110 -0
  45. package/dist/mcp-server/routes/streamTypes.d.ts.map +1 -0
  46. package/dist/mcp-server/routes/streamTypes.js +18 -0
  47. package/dist/mcp-server/sessionManager.d.ts +50 -0
  48. package/dist/mcp-server/sessionManager.d.ts.map +1 -0
  49. package/dist/story-generator/componentBlacklist.d.ts +21 -0
  50. package/dist/story-generator/componentBlacklist.d.ts.map +1 -0
  51. package/dist/story-generator/componentDiscovery.d.ts +28 -0
  52. package/dist/story-generator/componentDiscovery.d.ts.map +1 -0
  53. package/dist/story-generator/componentRegistryGenerator.d.ts +49 -0
  54. package/dist/story-generator/componentRegistryGenerator.d.ts.map +1 -0
  55. package/dist/story-generator/componentRegistryGenerator.js +205 -0
  56. package/dist/story-generator/configLoader.d.ts +33 -0
  57. package/dist/story-generator/configLoader.d.ts.map +1 -0
  58. package/dist/story-generator/considerationsLoader.d.ts +32 -0
  59. package/dist/story-generator/considerationsLoader.d.ts.map +1 -0
  60. package/dist/story-generator/documentation-sources.d.ts +28 -0
  61. package/dist/story-generator/documentation-sources.d.ts.map +1 -0
  62. package/dist/story-generator/documentationLoader.d.ts +64 -0
  63. package/dist/story-generator/documentationLoader.d.ts.map +1 -0
  64. package/dist/story-generator/dynamicPackageDiscovery.d.ts +97 -0
  65. package/dist/story-generator/dynamicPackageDiscovery.d.ts.map +1 -0
  66. package/dist/story-generator/enhancedComponentDiscovery.d.ts +125 -0
  67. package/dist/story-generator/enhancedComponentDiscovery.d.ts.map +1 -0
  68. package/dist/story-generator/enhancedComponentDiscovery.js +111 -11
  69. package/dist/story-generator/framework-adapters/angular-adapter.d.ts +40 -0
  70. package/dist/story-generator/framework-adapters/angular-adapter.d.ts.map +1 -0
  71. package/dist/story-generator/framework-adapters/angular-adapter.js +427 -0
  72. package/dist/story-generator/framework-adapters/base-adapter.d.ts +75 -0
  73. package/dist/story-generator/framework-adapters/base-adapter.d.ts.map +1 -0
  74. package/dist/story-generator/framework-adapters/base-adapter.js +147 -0
  75. package/dist/story-generator/framework-adapters/framework-detector.d.ts +55 -0
  76. package/dist/story-generator/framework-adapters/framework-detector.d.ts.map +1 -0
  77. package/dist/story-generator/framework-adapters/framework-detector.js +323 -0
  78. package/dist/story-generator/framework-adapters/index.d.ts +97 -0
  79. package/dist/story-generator/framework-adapters/index.d.ts.map +1 -0
  80. package/dist/story-generator/framework-adapters/index.js +198 -0
  81. package/dist/story-generator/framework-adapters/react-adapter.d.ts +40 -0
  82. package/dist/story-generator/framework-adapters/react-adapter.d.ts.map +1 -0
  83. package/dist/story-generator/framework-adapters/react-adapter.js +316 -0
  84. package/dist/story-generator/framework-adapters/svelte-adapter.d.ts +40 -0
  85. package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -0
  86. package/dist/story-generator/framework-adapters/svelte-adapter.js +372 -0
  87. package/dist/story-generator/framework-adapters/types.d.ts +182 -0
  88. package/dist/story-generator/framework-adapters/types.d.ts.map +1 -0
  89. package/dist/story-generator/framework-adapters/types.js +8 -0
  90. package/dist/story-generator/framework-adapters/vue-adapter.d.ts +36 -0
  91. package/dist/story-generator/framework-adapters/vue-adapter.d.ts.map +1 -0
  92. package/dist/story-generator/framework-adapters/vue-adapter.js +336 -0
  93. package/dist/story-generator/framework-adapters/web-components-adapter.d.ts +54 -0
  94. package/dist/story-generator/framework-adapters/web-components-adapter.d.ts.map +1 -0
  95. package/dist/story-generator/framework-adapters/web-components-adapter.js +387 -0
  96. package/dist/story-generator/generateStory.d.ts +7 -0
  97. package/dist/story-generator/generateStory.d.ts.map +1 -0
  98. package/dist/story-generator/gitignoreManager.d.ts +50 -0
  99. package/dist/story-generator/gitignoreManager.d.ts.map +1 -0
  100. package/dist/story-generator/imageProcessor.d.ts +80 -0
  101. package/dist/story-generator/imageProcessor.d.ts.map +1 -0
  102. package/dist/story-generator/imageProcessor.js +391 -0
  103. package/dist/story-generator/inMemoryStoryService.d.ts +89 -0
  104. package/dist/story-generator/inMemoryStoryService.d.ts.map +1 -0
  105. package/dist/story-generator/llm-providers/base-provider.d.ts +36 -0
  106. package/dist/story-generator/llm-providers/base-provider.d.ts.map +1 -0
  107. package/dist/story-generator/llm-providers/base-provider.js +135 -0
  108. package/dist/story-generator/llm-providers/claude-provider.d.ts +23 -0
  109. package/dist/story-generator/llm-providers/claude-provider.d.ts.map +1 -0
  110. package/dist/story-generator/llm-providers/claude-provider.js +414 -0
  111. package/dist/story-generator/llm-providers/gemini-provider.d.ts +24 -0
  112. package/dist/story-generator/llm-providers/gemini-provider.d.ts.map +1 -0
  113. package/dist/story-generator/llm-providers/gemini-provider.js +406 -0
  114. package/dist/story-generator/llm-providers/index.d.ts +63 -0
  115. package/dist/story-generator/llm-providers/index.d.ts.map +1 -0
  116. package/dist/story-generator/llm-providers/index.js +169 -0
  117. package/dist/story-generator/llm-providers/openai-provider.d.ts +24 -0
  118. package/dist/story-generator/llm-providers/openai-provider.d.ts.map +1 -0
  119. package/dist/story-generator/llm-providers/openai-provider.js +458 -0
  120. package/dist/story-generator/llm-providers/settings-manager.d.ts +75 -0
  121. package/dist/story-generator/llm-providers/settings-manager.d.ts.map +1 -0
  122. package/dist/story-generator/llm-providers/settings-manager.js +173 -0
  123. package/dist/story-generator/llm-providers/story-llm-service.d.ts +79 -0
  124. package/dist/story-generator/llm-providers/story-llm-service.d.ts.map +1 -0
  125. package/dist/story-generator/llm-providers/story-llm-service.js +240 -0
  126. package/dist/story-generator/llm-providers/types.d.ts +153 -0
  127. package/dist/story-generator/llm-providers/types.d.ts.map +1 -0
  128. package/dist/story-generator/llm-providers/types.js +8 -0
  129. package/dist/story-generator/logger.d.ts +14 -0
  130. package/dist/story-generator/logger.d.ts.map +1 -0
  131. package/dist/story-generator/logger.js +96 -29
  132. package/dist/story-generator/postProcessStory.d.ts +6 -0
  133. package/dist/story-generator/postProcessStory.d.ts.map +1 -0
  134. package/dist/story-generator/productionGitignoreManager.d.ts +91 -0
  135. package/dist/story-generator/productionGitignoreManager.d.ts.map +1 -0
  136. package/dist/story-generator/promptGenerator.d.ts +48 -0
  137. package/dist/story-generator/promptGenerator.d.ts.map +1 -0
  138. package/dist/story-generator/promptGenerator.js +186 -1
  139. package/dist/story-generator/storyHistory.d.ts +44 -0
  140. package/dist/story-generator/storyHistory.d.ts.map +1 -0
  141. package/dist/story-generator/storySync.d.ts +68 -0
  142. package/dist/story-generator/storySync.d.ts.map +1 -0
  143. package/dist/story-generator/storyTracker.d.ts +48 -0
  144. package/dist/story-generator/storyTracker.d.ts.map +1 -0
  145. package/dist/story-generator/storyValidator.d.ts +6 -0
  146. package/dist/story-generator/storyValidator.d.ts.map +1 -0
  147. package/dist/story-generator/universalDesignSystemAdapter.d.ts +68 -0
  148. package/dist/story-generator/universalDesignSystemAdapter.d.ts.map +1 -0
  149. package/dist/story-generator/universalDesignSystemAdapter.js +138 -1
  150. package/dist/story-generator/urlRedirectService.d.ts +21 -0
  151. package/dist/story-generator/urlRedirectService.d.ts.map +1 -0
  152. package/dist/story-generator/validateStory.d.ts +19 -0
  153. package/dist/story-generator/validateStory.d.ts.map +1 -0
  154. package/dist/story-generator/validateStory.js +6 -2
  155. package/dist/story-generator/visionPrompts.d.ts +88 -0
  156. package/dist/story-generator/visionPrompts.d.ts.map +1 -0
  157. package/dist/story-generator/visionPrompts.js +462 -0
  158. package/dist/story-ui.config.d.ts +78 -0
  159. package/dist/story-ui.config.d.ts.map +1 -0
  160. package/dist/templates/StoryUI/StoryUIPanel.d.ts +4 -0
  161. package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -0
  162. package/dist/templates/StoryUI/StoryUIPanel.js +1874 -0
  163. package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts +18 -0
  164. package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts.map +1 -0
  165. package/dist/templates/StoryUI/StoryUIPanel.stories.js +37 -0
  166. package/dist/templates/StoryUI/index.d.ts +3 -0
  167. package/dist/templates/StoryUI/index.d.ts.map +1 -0
  168. package/dist/templates/StoryUI/index.js +2 -0
  169. package/package.json +17 -3
  170. package/templates/StoryUI/StoryUIPanel.tsx +1960 -384
  171. package/templates/StoryUI/index.tsx +1 -1
  172. package/templates/StoryUI/manager.tsx +264 -0
  173. package/templates/production-app/.env.example +11 -0
  174. package/templates/production-app/index.html +66 -0
  175. package/templates/production-app/package.json +30 -0
  176. package/templates/production-app/public/favicon.svg +5 -0
  177. package/templates/production-app/src/App.tsx +1560 -0
  178. package/templates/production-app/src/LivePreviewRenderer.tsx +420 -0
  179. package/templates/production-app/src/componentRegistry.ts +315 -0
  180. package/templates/production-app/src/considerations.ts +16 -0
  181. package/templates/production-app/src/index.css +284 -0
  182. package/templates/production-app/src/main.tsx +25 -0
  183. package/templates/production-app/tsconfig.json +32 -0
  184. package/templates/production-app/tsconfig.node.json +11 -0
  185. package/templates/production-app/vite.config.ts +83 -0
  186. package/templates/react-import-rule.json +2 -2
  187. package/dist/index.js +0 -12
  188. package/dist/story-ui.config.loader.js +0 -205
@@ -0,0 +1,1874 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useRef, useEffect, useCallback } from 'react';
3
+ // Simple markdown renderer for AI messages with icon marker support
4
+ const renderMarkdown = (text) => {
5
+ // Split by double newlines to get paragraphs
6
+ const paragraphs = text.split(/\n\n+/);
7
+ // Parse inline formatting within text
8
+ const parseInline = (str, paragraphIndex) => {
9
+ const parts = [];
10
+ let remaining = str;
11
+ let keyIndex = 0;
12
+ while (remaining.length > 0) {
13
+ // Icon markers: [SUCCESS], [ERROR], [TIP], [WRENCH]
14
+ const iconMatch = remaining.match(/^\[(SUCCESS|ERROR|TIP|WRENCH)\]/);
15
+ if (iconMatch) {
16
+ const iconType = iconMatch[1].toLowerCase();
17
+ parts.push(_jsx("span", { children: StatusIcons[iconType] }, `icon-${paragraphIndex}-${keyIndex++}`));
18
+ remaining = remaining.slice(iconMatch[0].length);
19
+ continue;
20
+ }
21
+ // Bold: **text**
22
+ const boldMatch = remaining.match(/^\*\*(.+?)\*\*/);
23
+ if (boldMatch) {
24
+ parts.push(_jsx("strong", { children: boldMatch[1] }, `b-${paragraphIndex}-${keyIndex++}`));
25
+ remaining = remaining.slice(boldMatch[0].length);
26
+ continue;
27
+ }
28
+ // Italic: _text_
29
+ const italicMatch = remaining.match(/^_(.+?)_/);
30
+ if (italicMatch) {
31
+ parts.push(_jsx("em", { style: { opacity: 0.7, fontSize: '0.9em' }, children: italicMatch[1] }, `i-${paragraphIndex}-${keyIndex++}`));
32
+ remaining = remaining.slice(italicMatch[0].length);
33
+ continue;
34
+ }
35
+ // Code: `text`
36
+ const codeMatch = remaining.match(/^`([^`]+)`/);
37
+ if (codeMatch) {
38
+ parts.push(_jsx("code", { style: {
39
+ background: 'rgba(0,0,0,0.08)',
40
+ padding: '1px 5px',
41
+ borderRadius: '3px',
42
+ fontFamily: 'ui-monospace, monospace',
43
+ fontSize: '0.88em'
44
+ }, children: codeMatch[1] }, `c-${paragraphIndex}-${keyIndex++}`));
45
+ remaining = remaining.slice(codeMatch[0].length);
46
+ continue;
47
+ }
48
+ // Single newline within paragraph - convert to space or line break
49
+ if (remaining.startsWith('\n')) {
50
+ parts.push(' ');
51
+ remaining = remaining.slice(1);
52
+ continue;
53
+ }
54
+ // Regular text - consume until next special character or bracket
55
+ const nextSpecial = remaining.search(/[*_`\[\n]/);
56
+ if (nextSpecial === -1) {
57
+ parts.push(remaining);
58
+ remaining = '';
59
+ }
60
+ else if (nextSpecial === 0) {
61
+ // Special char that didn't match a pattern, treat as regular text
62
+ parts.push(remaining[0]);
63
+ remaining = remaining.slice(1);
64
+ }
65
+ else {
66
+ parts.push(remaining.slice(0, nextSpecial));
67
+ remaining = remaining.slice(nextSpecial);
68
+ }
69
+ }
70
+ return parts;
71
+ };
72
+ return (_jsx(_Fragment, { children: paragraphs.map((paragraph, index) => (_jsx("div", { style: { marginBottom: index < paragraphs.length - 1 ? '12px' : 0 }, children: parseInline(paragraph.trim(), index) }, `p-${index}`))) }));
73
+ };
74
+ // Inline SVG icons for status indicators (avoiding emojis)
75
+ const StatusIcons = {
76
+ success: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { color: '#22c55e', verticalAlign: 'middle', marginRight: '6px' }, children: [_jsx("path", { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }), _jsx("polyline", { points: "22,4 12,14.01 9,11.01" })] })),
77
+ error: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { color: '#ef4444', verticalAlign: 'middle', marginRight: '6px' }, children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("line", { x1: "15", y1: "9", x2: "9", y2: "15" }), _jsx("line", { x1: "9", y1: "9", x2: "15", y2: "15" })] })),
78
+ tip: (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { color: '#f59e0b', verticalAlign: 'middle', marginRight: '4px' }, children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("line", { x1: "12", y1: "16", x2: "12", y2: "12" }), _jsx("line", { x1: "12", y1: "8", x2: "12.01", y2: "8" })] })),
79
+ wrench: (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { color: '#6366f1', verticalAlign: 'middle', marginRight: '4px' }, children: _jsx("path", { d: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" }) }))
80
+ };
81
+ // Determine the MCP API base URL.
82
+ // Priority order:
83
+ // 1. VITE_STORY_UI_EDGE_URL - Edge Worker URL for cloud deployments
84
+ // 2. window.__STORY_UI_EDGE_URL__ - Runtime override for edge URL
85
+ // 3. VITE_STORY_UI_PORT - Custom port for localhost
86
+ // 4. window.__STORY_UI_PORT__ - Legacy port override
87
+ // 5. window.STORY_UI_MCP_PORT - MCP port override
88
+ // 6. Default to localhost:4001
89
+ const getApiBaseUrl = () => {
90
+ // Check for Edge Worker URL (cloud deployment)
91
+ const edgeUrl = import.meta.env?.VITE_STORY_UI_EDGE_URL;
92
+ if (edgeUrl)
93
+ return edgeUrl.replace(/\/$/, ''); // Remove trailing slash
94
+ // Check for window override for edge URL (support both naming conventions)
95
+ const windowEdgeUrl = window.__STORY_UI_EDGE_URL__ || window.STORY_UI_EDGE_URL;
96
+ if (windowEdgeUrl)
97
+ return windowEdgeUrl.replace(/\/$/, '');
98
+ // Check for Vite port environment variable
99
+ const vitePort = import.meta.env?.VITE_STORY_UI_PORT;
100
+ if (vitePort)
101
+ return `http://localhost:${vitePort}`;
102
+ // Check for window override (legacy support)
103
+ const windowOverride = window.__STORY_UI_PORT__;
104
+ if (windowOverride)
105
+ return `http://localhost:${windowOverride}`;
106
+ // Check for MCP port override set by stories file
107
+ const mcpOverride = window.STORY_UI_MCP_PORT;
108
+ if (mcpOverride)
109
+ return `http://localhost:${mcpOverride}`;
110
+ return 'http://localhost:4001';
111
+ };
112
+ // Helper to check if we're using Edge mode (cloud deployment)
113
+ const isEdgeMode = () => {
114
+ const baseUrl = getApiBaseUrl();
115
+ return baseUrl.includes('workers.dev') || baseUrl.includes('pages.dev') ||
116
+ baseUrl.startsWith('https://') && !baseUrl.includes('localhost');
117
+ };
118
+ // Legacy helper for backwards compatibility
119
+ const getApiPort = () => {
120
+ const baseUrl = getApiBaseUrl();
121
+ const match = baseUrl.match(/:(\d+)$/);
122
+ return match ? match[1] : '4001';
123
+ };
124
+ // Get connection display text
125
+ const getConnectionDisplayText = () => {
126
+ const baseUrl = getApiBaseUrl();
127
+ if (isEdgeMode()) {
128
+ // Extract domain for Edge URL
129
+ try {
130
+ const url = new URL(baseUrl);
131
+ return `Edge Worker (${url.hostname})`;
132
+ }
133
+ catch {
134
+ return 'Edge Worker';
135
+ }
136
+ }
137
+ return `MCP server (port ${getApiPort()})`;
138
+ };
139
+ const API_BASE = getApiBaseUrl();
140
+ const MCP_API = `${API_BASE}/story-ui/generate`;
141
+ const MCP_STREAM_API = `${API_BASE}/story-ui/generate-stream`;
142
+ const STORIES_API = `${API_BASE}/story-ui/stories`;
143
+ const DELETE_API_BASE = `${API_BASE}/story-ui/stories`;
144
+ const PROVIDERS_API = `${API_BASE}/story-ui/providers`;
145
+ // Considerations API URL - includes storybookOrigin param for Edge mode
146
+ const getConsiderationsApiUrl = () => {
147
+ const baseUrl = `${API_BASE}/story-ui/considerations`;
148
+ if (isEdgeMode()) {
149
+ // In Edge mode, tell the Edge Worker where to fetch considerations from
150
+ // The Storybook origin is where the panel is running (window.location.origin)
151
+ const storybookOrigin = window.location.origin;
152
+ return `${baseUrl}?storybookOrigin=${encodeURIComponent(storybookOrigin)}`;
153
+ }
154
+ return baseUrl;
155
+ };
156
+ const CONSIDERATIONS_API = getConsiderationsApiUrl();
157
+ const STORAGE_KEY = `story-ui-chats-${window.location.port}`;
158
+ const MAX_RECENT_CHATS = 20;
159
+ // Feature flag: Enable streaming mode (can be toggled for testing)
160
+ const USE_STREAMING = true;
161
+ // Load from localStorage
162
+ const loadChats = () => {
163
+ try {
164
+ const stored = localStorage.getItem(STORAGE_KEY);
165
+ if (!stored)
166
+ return [];
167
+ const chats = JSON.parse(stored);
168
+ // Sort by lastUpdated and limit
169
+ return chats
170
+ .sort((a, b) => b.lastUpdated - a.lastUpdated)
171
+ .slice(0, MAX_RECENT_CHATS);
172
+ }
173
+ catch (e) {
174
+ console.error('Failed to load chats:', e);
175
+ return [];
176
+ }
177
+ };
178
+ // Save to localStorage
179
+ const saveChats = (chats) => {
180
+ try {
181
+ // Keep only the most recent chats
182
+ const toSave = chats
183
+ .sort((a, b) => b.lastUpdated - a.lastUpdated)
184
+ .slice(0, MAX_RECENT_CHATS);
185
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
186
+ }
187
+ catch (e) {
188
+ console.error('Failed to save chats:', e);
189
+ }
190
+ };
191
+ // Sync with memory stories from backend
192
+ const syncWithActualStories = async () => {
193
+ try {
194
+ const response = await fetch(STORIES_API);
195
+ if (!response.ok) {
196
+ console.error('Failed to fetch stories from backend');
197
+ return loadChats();
198
+ }
199
+ // Check if response is JSON
200
+ const contentType = response.headers.get('content-type');
201
+ if (!contentType || !contentType.includes('application/json')) {
202
+ console.error('Server returned non-JSON response, likely server not running or wrong port');
203
+ return loadChats();
204
+ }
205
+ const data = await response.json();
206
+ const memoryStories = data.stories || [];
207
+ // Load existing chats
208
+ const existingChats = loadChats();
209
+ // Create a map for quick lookup - using chat.id as the primary key
210
+ const chatMap = new Map();
211
+ existingChats.forEach(chat => {
212
+ chatMap.set(chat.id, chat);
213
+ });
214
+ // Update or add memory stories
215
+ memoryStories.forEach((story) => {
216
+ const storyId = story.storyId || story.fileName;
217
+ // Look for existing chat by ID or by matching fileName
218
+ let existingChat = chatMap.get(storyId);
219
+ // If not found by ID, search by fileName
220
+ if (!existingChat && story.fileName) {
221
+ for (const [id, chat] of chatMap.entries()) {
222
+ if (chat.fileName === story.fileName) {
223
+ existingChat = chat;
224
+ break;
225
+ }
226
+ }
227
+ }
228
+ if (existingChat) {
229
+ // Update existing chat with latest info
230
+ existingChat.title = story.title || existingChat.title;
231
+ existingChat.fileName = story.fileName || existingChat.fileName;
232
+ existingChat.lastUpdated = new Date(story.updatedAt || story.createdAt).getTime();
233
+ }
234
+ else {
235
+ // Create new chat from memory story
236
+ const newChat = {
237
+ id: storyId,
238
+ title: story.title || story.fileName,
239
+ fileName: story.fileName,
240
+ conversation: [{
241
+ role: 'user',
242
+ content: story.prompt || `Generate ${story.title}`
243
+ }, {
244
+ role: 'ai',
245
+ content: `✅ Created story: "${story.title}"\n\nThis story was recovered from memory. You can continue updating it or view it in Storybook.`
246
+ }],
247
+ lastUpdated: new Date(story.updatedAt || story.createdAt).getTime()
248
+ };
249
+ chatMap.set(storyId, newChat);
250
+ }
251
+ });
252
+ // Convert back to array and save
253
+ const syncedChats = Array.from(chatMap.values());
254
+ saveChats(syncedChats);
255
+ return syncedChats;
256
+ }
257
+ catch (error) {
258
+ console.error('Error syncing with backend:', error);
259
+ return loadChats();
260
+ }
261
+ };
262
+ // Delete story and chat
263
+ const deleteStoryAndChat = async (chatId) => {
264
+ try {
265
+ // Remove .stories.tsx extension if present to get the actual story ID
266
+ const storyId = chatId.replace(/\.stories\.tsx$/, '');
267
+ console.log(`Attempting to delete story: chatId="${chatId}", storyId="${storyId}"`);
268
+ let serverDeleteSucceeded = false;
269
+ // First try to delete from backend
270
+ try {
271
+ const response = await fetch(`${DELETE_API_BASE}/${storyId}`, {
272
+ method: 'DELETE',
273
+ headers: { 'Content-Type': 'application/json' }
274
+ });
275
+ // 404 means story doesn't exist on server - that's OK, we can still clean up localStorage
276
+ if (response.ok || response.status === 404) {
277
+ serverDeleteSucceeded = true;
278
+ if (response.status === 404) {
279
+ console.log('Story not found on server (may have been a failed generation), cleaning up localStorage');
280
+ }
281
+ }
282
+ else {
283
+ console.warn(`Backend delete returned ${response.status}, trying legacy endpoint`);
284
+ }
285
+ }
286
+ catch (fetchError) {
287
+ console.warn('Backend delete request failed, trying legacy endpoint:', fetchError);
288
+ }
289
+ // Try legacy endpoint as fallback only if primary didn't succeed
290
+ if (!serverDeleteSucceeded) {
291
+ try {
292
+ const legacyResponse = await fetch(`${API_BASE}/story-ui/delete`, {
293
+ method: 'POST',
294
+ headers: { 'Content-Type': 'application/json' },
295
+ body: JSON.stringify({
296
+ chatId: storyId,
297
+ storyId: storyId
298
+ })
299
+ });
300
+ // 404 is also OK for legacy endpoint
301
+ if (legacyResponse.ok || legacyResponse.status === 404) {
302
+ serverDeleteSucceeded = true;
303
+ }
304
+ else {
305
+ console.warn('Legacy delete endpoint also returned non-success status');
306
+ }
307
+ }
308
+ catch (legacyError) {
309
+ console.warn('Legacy delete request failed:', legacyError);
310
+ }
311
+ }
312
+ // Always clean up localStorage - the chat/story data is primarily client-side
313
+ // Even if server delete failed, we should allow users to clean up their chat history
314
+ const chats = loadChats().filter(chat => chat.id !== chatId);
315
+ saveChats(chats);
316
+ console.log('Cleaned up localStorage chat entry');
317
+ return true;
318
+ }
319
+ catch (error) {
320
+ console.error('Error deleting story:', error);
321
+ // Still try to clean up localStorage even on error
322
+ try {
323
+ const chats = loadChats().filter(chat => chat.id !== chatId);
324
+ saveChats(chats);
325
+ console.log('Cleaned up localStorage despite error');
326
+ return true;
327
+ }
328
+ catch (localError) {
329
+ console.error('Failed to clean up localStorage:', localError);
330
+ return false;
331
+ }
332
+ }
333
+ };
334
+ // Test connection to MCP server
335
+ const testMCPConnection = async () => {
336
+ try {
337
+ const response = await fetch(STORIES_API, {
338
+ method: 'GET',
339
+ headers: { 'Content-Type': 'application/json' },
340
+ });
341
+ if (!response.ok) {
342
+ return { connected: false, error: `HTTP ${response.status}: ${response.statusText}` };
343
+ }
344
+ const contentType = response.headers.get('content-type');
345
+ if (!contentType || !contentType.includes('application/json')) {
346
+ return { connected: false, error: 'Server returned non-JSON response (likely wrong port or server not running)' };
347
+ }
348
+ return { connected: true };
349
+ }
350
+ catch (error) {
351
+ return { connected: false, error: error instanceof Error ? error.message : 'Unknown error' };
352
+ }
353
+ };
354
+ // Component styles
355
+ const STYLES = {
356
+ container: {
357
+ display: 'flex',
358
+ flexDirection: 'row',
359
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
360
+ height: '100vh',
361
+ overflow: 'hidden',
362
+ background: 'linear-gradient(135deg, #1e293b 0%, #334155 100%)',
363
+ color: '#e2e8f0',
364
+ },
365
+ // Sidebar
366
+ sidebar: {
367
+ width: '240px',
368
+ background: 'rgba(255, 255, 255, 0.03)',
369
+ borderRight: '1px solid rgba(255, 255, 255, 0.08)',
370
+ display: 'flex',
371
+ flexDirection: 'column',
372
+ backdropFilter: 'blur(10px)',
373
+ transition: 'width 0.3s ease',
374
+ position: 'relative',
375
+ },
376
+ sidebarCollapsed: {
377
+ width: '56px',
378
+ },
379
+ sidebarToggle: {
380
+ width: '100%',
381
+ padding: '8px 12px',
382
+ background: '#3b82f6',
383
+ color: 'white',
384
+ border: 'none',
385
+ borderRadius: '6px',
386
+ fontSize: '13px',
387
+ fontWeight: '500',
388
+ cursor: 'pointer',
389
+ marginBottom: '6px',
390
+ transition: 'all 0.2s ease',
391
+ boxShadow: 'none',
392
+ display: 'flex',
393
+ alignItems: 'center',
394
+ justifyContent: 'center',
395
+ gap: '6px',
396
+ lineHeight: '1',
397
+ },
398
+ newChatButton: {
399
+ width: '100%',
400
+ padding: '8px 12px',
401
+ background: '#3b82f6',
402
+ color: 'white',
403
+ border: 'none',
404
+ borderRadius: '6px',
405
+ fontSize: '13px',
406
+ fontWeight: '500',
407
+ cursor: 'pointer',
408
+ marginBottom: '12px',
409
+ transition: 'all 0.2s ease',
410
+ boxShadow: 'none',
411
+ display: 'flex',
412
+ alignItems: 'center',
413
+ justifyContent: 'center',
414
+ gap: '6px',
415
+ lineHeight: '1',
416
+ },
417
+ chatItem: {
418
+ padding: '8px 10px',
419
+ marginBottom: '4px',
420
+ background: 'rgba(255, 255, 255, 0.05)',
421
+ borderRadius: '6px',
422
+ cursor: 'pointer',
423
+ transition: 'all 0.15s ease',
424
+ position: 'relative',
425
+ paddingRight: '32px',
426
+ },
427
+ chatItemActive: {
428
+ background: 'rgba(59, 130, 246, 0.15)',
429
+ borderLeft: '2px solid #3b82f6',
430
+ },
431
+ chatItemTitle: {
432
+ fontSize: '13px',
433
+ fontWeight: '500',
434
+ marginBottom: '2px',
435
+ whiteSpace: 'nowrap',
436
+ overflow: 'hidden',
437
+ textOverflow: 'ellipsis',
438
+ },
439
+ chatItemTime: {
440
+ fontSize: '11px',
441
+ color: '#94a3b8',
442
+ },
443
+ deleteButton: {
444
+ position: 'absolute',
445
+ right: '8px',
446
+ top: '50%',
447
+ transform: 'translateY(-50%)',
448
+ background: 'rgba(239, 68, 68, 0.8)',
449
+ color: 'white',
450
+ border: 'none',
451
+ borderRadius: '4px',
452
+ padding: '4px 8px',
453
+ fontSize: '12px',
454
+ cursor: 'pointer',
455
+ opacity: 0,
456
+ transition: 'opacity 0.2s ease',
457
+ },
458
+ // Main content
459
+ mainContent: {
460
+ flex: 1,
461
+ display: 'flex',
462
+ flexDirection: 'column',
463
+ overflow: 'hidden',
464
+ },
465
+ chatHeader: {
466
+ padding: '12px 16px',
467
+ borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
468
+ background: 'rgba(255, 255, 255, 0.03)',
469
+ backdropFilter: 'blur(10px)',
470
+ },
471
+ chatContainer: {
472
+ flex: 1,
473
+ padding: '16px',
474
+ overflowY: 'auto',
475
+ scrollBehavior: 'smooth',
476
+ },
477
+ emptyState: {
478
+ color: '#94a3b8',
479
+ textAlign: 'center',
480
+ marginTop: '60px',
481
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
482
+ },
483
+ emptyStateTitle: {
484
+ fontSize: '16px',
485
+ fontWeight: '500',
486
+ marginBottom: '8px',
487
+ color: '#cbd5e1',
488
+ },
489
+ emptyStateSubtitle: {
490
+ fontSize: '13px',
491
+ color: '#64748b',
492
+ },
493
+ // Message bubbles
494
+ messageContainer: {
495
+ display: 'flex',
496
+ marginBottom: '10px',
497
+ },
498
+ userMessage: {
499
+ background: '#3b82f6',
500
+ color: '#ffffff',
501
+ borderRadius: '16px 16px 4px 16px',
502
+ padding: '10px 14px',
503
+ maxWidth: '85%',
504
+ marginLeft: 'auto',
505
+ fontSize: '14px',
506
+ lineHeight: '1.5',
507
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
508
+ boxShadow: 'none',
509
+ wordWrap: 'break-word',
510
+ },
511
+ aiMessage: {
512
+ background: 'rgba(255, 255, 255, 0.95)',
513
+ color: '#1f2937',
514
+ borderRadius: '16px 16px 16px 4px',
515
+ padding: '10px 14px',
516
+ maxWidth: '85%',
517
+ fontSize: '14px',
518
+ lineHeight: '1.5',
519
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
520
+ boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
521
+ wordWrap: 'break-word',
522
+ whiteSpace: 'pre-wrap',
523
+ },
524
+ loadingMessage: {
525
+ background: 'rgba(255, 255, 255, 0.9)',
526
+ color: '#6b7280',
527
+ borderRadius: '16px 16px 16px 4px',
528
+ padding: '10px 14px',
529
+ fontSize: '14px',
530
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
531
+ display: 'flex',
532
+ alignItems: 'center',
533
+ gap: '6px',
534
+ },
535
+ // Input form
536
+ inputForm: {
537
+ display: 'flex',
538
+ alignItems: 'center',
539
+ gap: '10px',
540
+ margin: '0 16px 16px 16px',
541
+ padding: '10px',
542
+ background: 'rgba(255, 255, 255, 0.03)',
543
+ borderRadius: '10px',
544
+ border: '1px solid rgba(255, 255, 255, 0.08)',
545
+ backdropFilter: 'blur(10px)',
546
+ },
547
+ textInput: {
548
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
549
+ flex: 1,
550
+ padding: '10px 14px',
551
+ borderRadius: '6px',
552
+ border: '1px solid rgba(255, 255, 255, 0.15)',
553
+ fontSize: '13px',
554
+ color: '#1f2937',
555
+ background: '#ffffff',
556
+ outline: 'none',
557
+ transition: 'all 0.15s ease',
558
+ boxSizing: 'border-box',
559
+ },
560
+ sendButton: {
561
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
562
+ padding: '10px 16px',
563
+ borderRadius: '6px',
564
+ border: 'none',
565
+ background: '#10b981',
566
+ color: '#ffffff',
567
+ fontSize: '13px',
568
+ fontWeight: '500',
569
+ cursor: 'pointer',
570
+ display: 'flex',
571
+ alignItems: 'center',
572
+ gap: '5px',
573
+ transition: 'all 0.15s ease',
574
+ boxShadow: 'none',
575
+ },
576
+ errorMessage: {
577
+ background: 'rgba(248, 113, 113, 0.1)',
578
+ color: '#f87171',
579
+ padding: '8px 12px',
580
+ borderRadius: '6px',
581
+ fontSize: '13px',
582
+ marginBottom: '10px',
583
+ border: '1px solid rgba(248, 113, 113, 0.2)',
584
+ },
585
+ loadingDots: {
586
+ display: 'inline-block',
587
+ animation: 'loadingDots 1.4s infinite',
588
+ },
589
+ '@keyframes loadingDots': {
590
+ '0%': { content: '""' },
591
+ '25%': { content: '"."' },
592
+ '50%': { content: '".."' },
593
+ '75%': { content: '"..."' },
594
+ },
595
+ codeBlock: {
596
+ background: '#1e293b',
597
+ padding: '10px 12px',
598
+ borderRadius: '6px',
599
+ fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
600
+ fontSize: '12px',
601
+ lineHeight: '1.5',
602
+ overflowX: 'auto',
603
+ marginTop: '6px',
604
+ border: '1px solid rgba(255, 255, 255, 0.08)',
605
+ },
606
+ // Streaming progress styles
607
+ streamingContainer: {
608
+ background: 'rgba(255, 255, 255, 0.95)',
609
+ borderRadius: '16px 16px 16px 4px',
610
+ padding: '12px',
611
+ maxWidth: '85%',
612
+ boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
613
+ },
614
+ intentPreview: {
615
+ background: 'rgba(59, 130, 246, 0.08)',
616
+ borderRadius: '8px',
617
+ padding: '10px',
618
+ marginBottom: '10px',
619
+ border: '1px solid rgba(59, 130, 246, 0.15)',
620
+ },
621
+ intentTitle: {
622
+ fontSize: '13px',
623
+ fontWeight: '600',
624
+ color: '#1e40af',
625
+ marginBottom: '6px',
626
+ display: 'flex',
627
+ alignItems: 'center',
628
+ gap: '5px',
629
+ },
630
+ intentStrategy: {
631
+ fontSize: '12px',
632
+ color: '#4b5563',
633
+ marginBottom: '4px',
634
+ },
635
+ intentComponents: {
636
+ display: 'flex',
637
+ flexWrap: 'wrap',
638
+ gap: '4px',
639
+ marginTop: '6px',
640
+ },
641
+ componentTag: {
642
+ background: 'rgba(59, 130, 246, 0.12)',
643
+ color: '#1d4ed8',
644
+ fontSize: '10px',
645
+ padding: '2px 6px',
646
+ borderRadius: '10px',
647
+ fontWeight: '500',
648
+ },
649
+ progressBar: {
650
+ background: 'rgba(0, 0, 0, 0.08)',
651
+ borderRadius: '3px',
652
+ height: '4px',
653
+ marginTop: '10px',
654
+ marginBottom: '6px',
655
+ overflow: 'hidden',
656
+ },
657
+ progressFill: {
658
+ background: '#3b82f6',
659
+ height: '100%',
660
+ borderRadius: '3px',
661
+ transition: 'width 0.3s ease',
662
+ },
663
+ progressPhase: {
664
+ fontSize: '11px',
665
+ color: '#6b7280',
666
+ display: 'flex',
667
+ alignItems: 'center',
668
+ gap: '5px',
669
+ },
670
+ phaseIcon: {
671
+ fontSize: '12px',
672
+ },
673
+ validationBox: {
674
+ marginTop: '8px',
675
+ padding: '8px',
676
+ borderRadius: '6px',
677
+ fontSize: '11px',
678
+ },
679
+ validationSuccess: {
680
+ background: 'rgba(16, 185, 129, 0.08)',
681
+ border: '1px solid rgba(16, 185, 129, 0.15)',
682
+ color: '#047857',
683
+ },
684
+ validationWarning: {
685
+ background: 'rgba(245, 158, 11, 0.08)',
686
+ border: '1px solid rgba(245, 158, 11, 0.15)',
687
+ color: '#b45309',
688
+ },
689
+ validationError: {
690
+ background: 'rgba(239, 68, 68, 0.08)',
691
+ border: '1px solid rgba(239, 68, 68, 0.15)',
692
+ color: '#dc2626',
693
+ },
694
+ retryBadge: {
695
+ background: 'rgba(245, 158, 11, 0.12)',
696
+ color: '#b45309',
697
+ fontSize: '10px',
698
+ padding: '2px 8px',
699
+ borderRadius: '10px',
700
+ display: 'inline-flex',
701
+ alignItems: 'center',
702
+ gap: '3px',
703
+ marginTop: '6px',
704
+ },
705
+ completionSummary: {
706
+ marginTop: '10px',
707
+ paddingTop: '10px',
708
+ borderTop: '1px solid rgba(0, 0, 0, 0.08)',
709
+ },
710
+ summaryTitle: {
711
+ fontSize: '14px',
712
+ fontWeight: '600',
713
+ color: '#111827',
714
+ marginBottom: '6px',
715
+ display: 'flex',
716
+ alignItems: 'center',
717
+ gap: '6px',
718
+ },
719
+ summaryDescription: {
720
+ fontSize: '12px',
721
+ color: '#4b5563',
722
+ lineHeight: '1.5',
723
+ },
724
+ metricsRow: {
725
+ display: 'flex',
726
+ gap: '12px',
727
+ marginTop: '8px',
728
+ fontSize: '10px',
729
+ color: '#6b7280',
730
+ },
731
+ metric: {
732
+ display: 'flex',
733
+ alignItems: 'center',
734
+ gap: '3px',
735
+ },
736
+ // Code viewer styles for generated stories
737
+ codeViewerContainer: {
738
+ marginTop: '12px',
739
+ borderTop: '1px solid rgba(0, 0, 0, 0.08)',
740
+ paddingTop: '12px',
741
+ },
742
+ codeViewerToggle: {
743
+ display: 'flex',
744
+ alignItems: 'center',
745
+ justifyContent: 'space-between',
746
+ padding: '8px 12px',
747
+ background: 'rgba(59, 130, 246, 0.08)',
748
+ borderRadius: '6px',
749
+ cursor: 'pointer',
750
+ border: '1px solid rgba(59, 130, 246, 0.15)',
751
+ transition: 'all 0.2s ease',
752
+ fontSize: '13px',
753
+ fontWeight: '500',
754
+ color: '#1e40af',
755
+ },
756
+ codeViewerToggleHover: {
757
+ background: 'rgba(59, 130, 246, 0.15)',
758
+ },
759
+ codeViewerContent: {
760
+ marginTop: '10px',
761
+ background: '#1e293b',
762
+ borderRadius: '8px',
763
+ overflow: 'hidden',
764
+ border: '1px solid rgba(255, 255, 255, 0.08)',
765
+ },
766
+ codeViewerHeader: {
767
+ display: 'flex',
768
+ alignItems: 'center',
769
+ justifyContent: 'space-between',
770
+ padding: '8px 12px',
771
+ background: 'rgba(0, 0, 0, 0.2)',
772
+ borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
773
+ },
774
+ codeViewerFileName: {
775
+ fontSize: '12px',
776
+ color: '#94a3b8',
777
+ fontFamily: 'ui-monospace, monospace',
778
+ },
779
+ copyButton: {
780
+ padding: '4px 10px',
781
+ fontSize: '11px',
782
+ fontWeight: '500',
783
+ color: '#e2e8f0',
784
+ background: 'rgba(59, 130, 246, 0.3)',
785
+ border: '1px solid rgba(59, 130, 246, 0.5)',
786
+ borderRadius: '4px',
787
+ cursor: 'pointer',
788
+ transition: 'all 0.2s ease',
789
+ },
790
+ copyButtonSuccess: {
791
+ background: 'rgba(34, 197, 94, 0.3)',
792
+ borderColor: 'rgba(34, 197, 94, 0.5)',
793
+ color: '#86efac',
794
+ },
795
+ codeViewerPre: {
796
+ margin: 0,
797
+ padding: '12px',
798
+ fontSize: '11px',
799
+ lineHeight: '1.5',
800
+ fontFamily: 'ui-monospace, Consolas, Monaco, monospace',
801
+ color: '#e2e8f0',
802
+ overflowX: 'auto',
803
+ maxHeight: '400px',
804
+ overflowY: 'auto',
805
+ },
806
+ // Image upload styles
807
+ uploadButton: {
808
+ display: 'flex',
809
+ alignItems: 'center',
810
+ justifyContent: 'center',
811
+ width: '36px',
812
+ height: '36px',
813
+ borderRadius: '6px',
814
+ border: '1px solid rgba(255, 255, 255, 0.15)',
815
+ background: 'rgba(255, 255, 255, 0.08)',
816
+ color: '#e2e8f0',
817
+ cursor: 'pointer',
818
+ transition: 'all 0.2s ease',
819
+ flexShrink: 0,
820
+ },
821
+ uploadButtonHover: {
822
+ background: 'rgba(59, 130, 246, 0.2)',
823
+ borderColor: 'rgba(59, 130, 246, 0.5)',
824
+ },
825
+ imagePreviewContainer: {
826
+ display: 'flex',
827
+ flexWrap: 'wrap',
828
+ gap: '6px',
829
+ padding: '8px 12px',
830
+ background: 'rgba(255, 255, 255, 0.03)',
831
+ borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
832
+ margin: '0 16px',
833
+ borderRadius: '6px 6px 0 0',
834
+ },
835
+ imagePreviewItem: {
836
+ position: 'relative',
837
+ width: '56px',
838
+ height: '56px',
839
+ borderRadius: '6px',
840
+ overflow: 'hidden',
841
+ border: '1px solid rgba(255, 255, 255, 0.15)',
842
+ background: '#1e293b',
843
+ },
844
+ imagePreviewImg: {
845
+ width: '100%',
846
+ height: '100%',
847
+ objectFit: 'cover',
848
+ },
849
+ imageRemoveButton: {
850
+ position: 'absolute',
851
+ top: '2px',
852
+ right: '2px',
853
+ width: '18px',
854
+ height: '18px',
855
+ borderRadius: '50%',
856
+ background: 'rgba(239, 68, 68, 0.9)',
857
+ color: 'white',
858
+ border: 'none',
859
+ fontSize: '12px',
860
+ cursor: 'pointer',
861
+ display: 'flex',
862
+ alignItems: 'center',
863
+ justifyContent: 'center',
864
+ lineHeight: 1,
865
+ },
866
+ imagePreviewLabel: {
867
+ display: 'flex',
868
+ alignItems: 'center',
869
+ gap: '6px',
870
+ fontSize: '12px',
871
+ color: '#94a3b8',
872
+ marginRight: 'auto',
873
+ },
874
+ userMessageImages: {
875
+ display: 'flex',
876
+ gap: '6px',
877
+ marginTop: '6px',
878
+ flexWrap: 'wrap',
879
+ },
880
+ userMessageImage: {
881
+ width: '40px',
882
+ height: '40px',
883
+ borderRadius: '6px',
884
+ objectFit: 'cover',
885
+ border: '1px solid rgba(255, 255, 255, 0.25)',
886
+ },
887
+ // Drag and drop overlay
888
+ dropOverlay: {
889
+ position: 'absolute',
890
+ top: 0,
891
+ left: 0,
892
+ right: 0,
893
+ bottom: 0,
894
+ background: 'rgba(59, 130, 246, 0.12)',
895
+ border: '2px dashed rgba(59, 130, 246, 0.4)',
896
+ borderRadius: '10px',
897
+ display: 'flex',
898
+ alignItems: 'center',
899
+ justifyContent: 'center',
900
+ zIndex: 100,
901
+ backdropFilter: 'blur(3px)',
902
+ },
903
+ dropOverlayText: {
904
+ background: 'rgba(59, 130, 246, 0.85)',
905
+ color: 'white',
906
+ padding: '12px 24px',
907
+ borderRadius: '8px',
908
+ fontSize: '14px',
909
+ fontWeight: '500',
910
+ display: 'flex',
911
+ alignItems: 'center',
912
+ gap: '8px',
913
+ boxShadow: '0 2px 8px rgba(59, 130, 246, 0.3)',
914
+ },
915
+ };
916
+ // Add custom style for loading animation
917
+ const styleSheet = document.createElement('style');
918
+ styleSheet.textContent = `
919
+ @keyframes loadingDots {
920
+ 0%, 20% { content: "."; }
921
+ 40% { content: ".."; }
922
+ 60%, 100% { content: "..."; }
923
+ }
924
+
925
+ .loading-dots::after {
926
+ content: ".";
927
+ animation: loadingDots 1.4s infinite;
928
+ }
929
+ `;
930
+ document.head.appendChild(styleSheet);
931
+ // Helper function to format timestamp
932
+ const formatTime = (timestamp) => {
933
+ const date = new Date(timestamp);
934
+ const now = new Date();
935
+ const diffMs = now.getTime() - date.getTime();
936
+ const diffMins = Math.floor(diffMs / 60000);
937
+ const diffHours = Math.floor(diffMs / 3600000);
938
+ const diffDays = Math.floor(diffMs / 86400000);
939
+ if (diffMins < 1)
940
+ return 'just now';
941
+ if (diffMins < 60)
942
+ return `${diffMins}m ago`;
943
+ if (diffHours < 24)
944
+ return `${diffHours}h ago`;
945
+ if (diffDays < 7)
946
+ return `${diffDays}d ago`;
947
+ return date.toLocaleDateString();
948
+ };
949
+ // Helper to get phase icon and text
950
+ const getPhaseInfo = (phase) => {
951
+ const phases = {
952
+ config_loaded: { icon: '⚙️', text: 'Loading configuration' },
953
+ components_discovered: { icon: '🔍', text: 'Discovering components' },
954
+ prompt_built: { icon: '📝', text: 'Building prompt' },
955
+ llm_thinking: { icon: '🤔', text: 'AI is thinking' },
956
+ code_extracted: { icon: '📦', text: 'Extracting code' },
957
+ validating: { icon: '✅', text: 'Validating output' },
958
+ post_processing: { icon: '🔧', text: 'Processing' },
959
+ saving: { icon: '💾', text: 'Saving story' },
960
+ };
961
+ return phases[phase] || { icon: '⏳', text: 'Working' };
962
+ };
963
+ // Streaming Progress Message Component
964
+ const StreamingProgressMessage = ({ streamingData }) => {
965
+ const { intent, progress, validation, retry, completion, error } = streamingData;
966
+ const [showCode, setShowCode] = useState(true); // Show code by default for better UX
967
+ const [copyStatus, setCopyStatus] = useState('idle');
968
+ // Handle copy to clipboard
969
+ const handleCopyCode = async (code) => {
970
+ try {
971
+ await navigator.clipboard.writeText(code);
972
+ setCopyStatus('copied');
973
+ setTimeout(() => setCopyStatus('idle'), 2000);
974
+ }
975
+ catch (err) {
976
+ console.error('Failed to copy:', err);
977
+ }
978
+ };
979
+ // If completed, show completion summary
980
+ if (completion) {
981
+ return (_jsx("div", { style: STYLES.streamingContainer, children: _jsxs("div", { style: STYLES.completionSummary, children: [_jsxs("div", { style: STYLES.summaryTitle, children: [completion.success ? '✅' : '❌', " ", completion.title] }), _jsx("div", { style: STYLES.summaryDescription, children: completion.summary.description }), completion.componentsUsed.length > 0 && (_jsxs("div", { style: { marginTop: '10px' }, children: [_jsx("div", { style: { fontSize: '12px', color: '#6b7280', marginBottom: '6px' }, children: "Components used:" }), _jsx("div", { style: STYLES.intentComponents, children: completion.componentsUsed.map((comp, i) => (_jsx("span", { style: STYLES.componentTag, children: comp.name }, i))) })] })), completion.layoutChoices.length > 0 && (_jsxs("div", { style: { marginTop: '10px' }, children: [_jsx("div", { style: { fontSize: '12px', color: '#6b7280', marginBottom: '6px' }, children: "Layout:" }), _jsx("div", { style: { fontSize: '12px', color: '#4b5563' }, children: completion.layoutChoices.map(l => l.pattern).join(', ') })] })), completion.validation && !completion.validation.isValid && (_jsxs("div", { style: { ...STYLES.validationBox, ...STYLES.validationWarning }, children: ["\u26A0\uFE0F ", completion.validation.autoFixApplied ? 'Auto-fixed issues' : 'Minor issues detected'] })), completion.suggestions && completion.suggestions.length > 0 && (_jsxs("div", { style: { marginTop: '10px', fontSize: '12px', color: '#6b7280' }, children: ["\uD83D\uDCA1 ", completion.suggestions[0]] })), completion.metrics && (_jsxs("div", { style: STYLES.metricsRow, children: [_jsxs("span", { style: STYLES.metric, children: ["\u23F1\uFE0F ", (completion.metrics.totalTimeMs / 1000).toFixed(1), "s"] }), _jsxs("span", { style: STYLES.metric, children: ["\uD83D\uDD04 ", completion.metrics.llmCallsCount, " LLM calls"] })] })), completion.code && (_jsxs("div", { style: STYLES.codeViewerContainer, children: [_jsxs("div", { style: STYLES.codeViewerToggle, onClick: () => setShowCode(!showCode), role: "button", tabIndex: 0, onKeyDown: (e) => e.key === 'Enter' && setShowCode(!showCode), children: [_jsxs("span", { children: [showCode ? '▼' : '▶', " View Generated Code"] }), _jsx("span", { style: { fontSize: '11px', color: '#6366f1' }, children: completion.fileName })] }), showCode && (_jsxs("div", { style: STYLES.codeViewerContent, children: [_jsxs("div", { style: STYLES.codeViewerHeader, children: [_jsx("span", { style: STYLES.codeViewerFileName, children: completion.fileName }), _jsx("button", { style: {
982
+ ...STYLES.copyButton,
983
+ ...(copyStatus === 'copied' ? STYLES.copyButtonSuccess : {})
984
+ }, onClick: () => handleCopyCode(completion.code), children: copyStatus === 'copied' ? '✓ Copied!' : 'Copy Code' })] }), _jsx("pre", { style: STYLES.codeViewerPre, children: _jsx("code", { children: completion.code }) })] }))] }))] }) }));
985
+ }
986
+ // If error, show error
987
+ if (error) {
988
+ 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] })] }) }));
989
+ }
990
+ // Show progress
991
+ 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: {
992
+ ...STYLES.progressFill,
993
+ width: `${(progress.step / progress.totalSteps) * 100}%`
994
+ } }) }), _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" }) }))] }));
995
+ };
996
+ // Main component
997
+ function StoryUIPanel() {
998
+ const [input, setInput] = useState('');
999
+ const [conversation, setConversation] = useState([]);
1000
+ const [loading, setLoading] = useState(false);
1001
+ const [error, setError] = useState(null);
1002
+ const [recentChats, setRecentChats] = useState([]);
1003
+ const [activeChatId, setActiveChatId] = useState(null);
1004
+ const [activeTitle, setActiveTitle] = useState('');
1005
+ const [sidebarOpen, setSidebarOpen] = useState(true);
1006
+ const [connectionStatus, setConnectionStatus] = useState({ connected: false });
1007
+ const [availableProviders, setAvailableProviders] = useState([]);
1008
+ const [selectedProvider, setSelectedProvider] = useState('');
1009
+ const [selectedModel, setSelectedModel] = useState('');
1010
+ const [streamingState, setStreamingState] = useState(null);
1011
+ const [attachedImages, setAttachedImages] = useState([]);
1012
+ const [considerations, setConsiderations] = useState('');
1013
+ const chatEndRef = useRef(null);
1014
+ const inputRef = useRef(null);
1015
+ const fileInputRef = useRef(null);
1016
+ const abortControllerRef = useRef(null);
1017
+ // Maximum images allowed
1018
+ const MAX_IMAGES = 4;
1019
+ const MAX_IMAGE_SIZE_MB = 20;
1020
+ // Helper to convert file to base64
1021
+ const fileToBase64 = (file) => {
1022
+ return new Promise((resolve, reject) => {
1023
+ const reader = new FileReader();
1024
+ reader.readAsDataURL(file);
1025
+ reader.onload = () => {
1026
+ const result = reader.result;
1027
+ // Extract base64 data (remove data:image/...;base64, prefix)
1028
+ const base64 = result.split(',')[1];
1029
+ resolve(base64);
1030
+ };
1031
+ reader.onerror = error => reject(error);
1032
+ });
1033
+ };
1034
+ // Handle file selection
1035
+ const handleFileSelect = async (e) => {
1036
+ const files = e.target.files;
1037
+ if (!files)
1038
+ return;
1039
+ const newImages = [];
1040
+ const errors = [];
1041
+ for (let i = 0; i < files.length && (attachedImages.length + newImages.length) < MAX_IMAGES; i++) {
1042
+ const file = files[i];
1043
+ // Validate file type
1044
+ if (!file.type.startsWith('image/')) {
1045
+ errors.push(`${file.name}: Not an image file`);
1046
+ continue;
1047
+ }
1048
+ // Validate file size
1049
+ if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
1050
+ errors.push(`${file.name}: File too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
1051
+ continue;
1052
+ }
1053
+ try {
1054
+ const base64 = await fileToBase64(file);
1055
+ const preview = URL.createObjectURL(file);
1056
+ newImages.push({
1057
+ id: `${Date.now()}-${i}`,
1058
+ file,
1059
+ preview,
1060
+ base64,
1061
+ });
1062
+ }
1063
+ catch (err) {
1064
+ errors.push(`${file.name}: Failed to process`);
1065
+ }
1066
+ }
1067
+ if (errors.length > 0) {
1068
+ setError(errors.join('\n'));
1069
+ }
1070
+ setAttachedImages(prev => [...prev, ...newImages]);
1071
+ // Reset file input
1072
+ if (fileInputRef.current) {
1073
+ fileInputRef.current.value = '';
1074
+ }
1075
+ };
1076
+ // Remove attached image
1077
+ const removeAttachedImage = (id) => {
1078
+ setAttachedImages(prev => {
1079
+ const removed = prev.find(img => img.id === id);
1080
+ if (removed) {
1081
+ URL.revokeObjectURL(removed.preview);
1082
+ }
1083
+ return prev.filter(img => img.id !== id);
1084
+ });
1085
+ };
1086
+ // Clear all attached images
1087
+ const clearAttachedImages = () => {
1088
+ attachedImages.forEach(img => URL.revokeObjectURL(img.preview));
1089
+ setAttachedImages([]);
1090
+ };
1091
+ // Drag and drop state
1092
+ const [isDragging, setIsDragging] = useState(false);
1093
+ // Handle drag events
1094
+ const handleDragEnter = useCallback((e) => {
1095
+ e.preventDefault();
1096
+ e.stopPropagation();
1097
+ if (e.dataTransfer.types.includes('Files')) {
1098
+ setIsDragging(true);
1099
+ }
1100
+ }, []);
1101
+ const handleDragLeave = useCallback((e) => {
1102
+ e.preventDefault();
1103
+ e.stopPropagation();
1104
+ // Only set to false if we're leaving the main container
1105
+ if (!e.currentTarget.contains(e.relatedTarget)) {
1106
+ setIsDragging(false);
1107
+ }
1108
+ }, []);
1109
+ const handleDragOver = useCallback((e) => {
1110
+ e.preventDefault();
1111
+ e.stopPropagation();
1112
+ }, []);
1113
+ const handleDrop = useCallback(async (e) => {
1114
+ e.preventDefault();
1115
+ e.stopPropagation();
1116
+ setIsDragging(false);
1117
+ const files = Array.from(e.dataTransfer.files);
1118
+ const imageFiles = files.filter(f => f.type.startsWith('image/'));
1119
+ if (imageFiles.length === 0) {
1120
+ setError('Please drop image files only');
1121
+ return;
1122
+ }
1123
+ const newImages = [];
1124
+ const errors = [];
1125
+ for (let i = 0; i < imageFiles.length && (attachedImages.length + newImages.length) < MAX_IMAGES; i++) {
1126
+ const file = imageFiles[i];
1127
+ if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
1128
+ errors.push(`${file.name}: File too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
1129
+ continue;
1130
+ }
1131
+ try {
1132
+ const base64 = await fileToBase64(file);
1133
+ const preview = URL.createObjectURL(file);
1134
+ newImages.push({
1135
+ id: `${Date.now()}-${i}`,
1136
+ file,
1137
+ preview,
1138
+ base64,
1139
+ });
1140
+ }
1141
+ catch (err) {
1142
+ errors.push(`${file.name}: Failed to process`);
1143
+ }
1144
+ }
1145
+ if (errors.length > 0) {
1146
+ setError(errors.join('\n'));
1147
+ }
1148
+ setAttachedImages(prev => [...prev, ...newImages]);
1149
+ }, [attachedImages.length, fileToBase64]);
1150
+ // Handle clipboard paste for images
1151
+ const handlePaste = useCallback(async (e) => {
1152
+ const items = e.clipboardData?.items;
1153
+ if (!items)
1154
+ return;
1155
+ const imageItems = [];
1156
+ for (let i = 0; i < items.length; i++) {
1157
+ if (items[i].type.startsWith('image/')) {
1158
+ imageItems.push(items[i]);
1159
+ }
1160
+ }
1161
+ if (imageItems.length === 0)
1162
+ return;
1163
+ // Prevent default text paste behavior when pasting images
1164
+ e.preventDefault();
1165
+ if (attachedImages.length >= MAX_IMAGES) {
1166
+ setError(`Maximum ${MAX_IMAGES} images allowed`);
1167
+ return;
1168
+ }
1169
+ const newImages = [];
1170
+ const errors = [];
1171
+ for (let i = 0; i < imageItems.length && (attachedImages.length + newImages.length) < MAX_IMAGES; i++) {
1172
+ const item = imageItems[i];
1173
+ const file = item.getAsFile();
1174
+ if (!file) {
1175
+ errors.push('Failed to get image from clipboard');
1176
+ continue;
1177
+ }
1178
+ if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
1179
+ errors.push(`Pasted image too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
1180
+ continue;
1181
+ }
1182
+ try {
1183
+ const base64 = await fileToBase64(file);
1184
+ const preview = URL.createObjectURL(file);
1185
+ // Create a meaningful name for pasted images
1186
+ const timestamp = new Date().toISOString().slice(11, 19).replace(/:/g, '-');
1187
+ const renamedFile = new File([file], `pasted-image-${timestamp}.${file.type.split('/')[1] || 'png'}`, { type: file.type });
1188
+ newImages.push({
1189
+ id: `paste-${Date.now()}-${i}`,
1190
+ file: renamedFile,
1191
+ preview,
1192
+ base64,
1193
+ });
1194
+ }
1195
+ catch (err) {
1196
+ errors.push('Failed to process pasted image');
1197
+ }
1198
+ }
1199
+ if (errors.length > 0) {
1200
+ setError(errors.join('\n'));
1201
+ }
1202
+ if (newImages.length > 0) {
1203
+ setAttachedImages(prev => [...prev, ...newImages]);
1204
+ // Clear any existing error on successful paste
1205
+ if (errors.length === 0) {
1206
+ setError(null);
1207
+ }
1208
+ }
1209
+ }, [attachedImages.length, fileToBase64]);
1210
+ // Load and sync chats on mount
1211
+ useEffect(() => {
1212
+ const initializeChats = async () => {
1213
+ // Test connection first
1214
+ const connectionTest = await testMCPConnection();
1215
+ setConnectionStatus(connectionTest);
1216
+ if (connectionTest.connected) {
1217
+ // Fetch available providers
1218
+ try {
1219
+ const providersRes = await fetch(PROVIDERS_API);
1220
+ if (providersRes.ok) {
1221
+ const providersData = await providersRes.json();
1222
+ setAvailableProviders(providersData.providers.filter(p => p.configured));
1223
+ // Set initial selection from server defaults
1224
+ if (providersData.current) {
1225
+ setSelectedProvider(providersData.current.provider.toLowerCase());
1226
+ setSelectedModel(providersData.current.model);
1227
+ }
1228
+ }
1229
+ }
1230
+ catch (e) {
1231
+ console.error('Failed to fetch providers:', e);
1232
+ }
1233
+ // Fetch design system considerations for environment parity
1234
+ // This ensures production gets the same considerations as local development
1235
+ try {
1236
+ const considerationsRes = await fetch(CONSIDERATIONS_API);
1237
+ if (considerationsRes.ok) {
1238
+ const considerationsData = await considerationsRes.json();
1239
+ if (considerationsData.hasConsiderations && considerationsData.considerations) {
1240
+ setConsiderations(considerationsData.considerations);
1241
+ console.log(`Loaded considerations from ${considerationsData.source}`);
1242
+ }
1243
+ }
1244
+ }
1245
+ catch (e) {
1246
+ console.error('Failed to fetch considerations:', e);
1247
+ }
1248
+ const syncedChats = await syncWithActualStories();
1249
+ const sortedChats = syncedChats.sort((a, b) => b.lastUpdated - a.lastUpdated).slice(0, MAX_RECENT_CHATS);
1250
+ setRecentChats(sortedChats);
1251
+ if (sortedChats.length > 0) {
1252
+ setConversation(sortedChats[0].conversation);
1253
+ setActiveChatId(sortedChats[0].id);
1254
+ setActiveTitle(sortedChats[0].title);
1255
+ }
1256
+ }
1257
+ else {
1258
+ // Load from local storage if server is not available
1259
+ const localChats = loadChats();
1260
+ setRecentChats(localChats);
1261
+ }
1262
+ };
1263
+ initializeChats();
1264
+ }, []);
1265
+ // Scroll to bottom on new message
1266
+ useEffect(() => {
1267
+ if (chatEndRef.current) {
1268
+ chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
1269
+ }
1270
+ }, [conversation, loading]);
1271
+ // Helper function for non-streaming fallback
1272
+ const handleSendNonStreaming = async (userInput, newConversation) => {
1273
+ const res = await fetch(MCP_API, {
1274
+ method: 'POST',
1275
+ headers: { 'Content-Type': 'application/json' },
1276
+ body: JSON.stringify({
1277
+ prompt: userInput,
1278
+ conversation: newConversation,
1279
+ fileName: activeChatId || undefined,
1280
+ provider: selectedProvider || undefined,
1281
+ model: selectedModel || undefined,
1282
+ considerations: considerations || undefined,
1283
+ }),
1284
+ });
1285
+ const contentType = res.headers.get('content-type');
1286
+ if (!contentType || !contentType.includes('application/json')) {
1287
+ const text = await res.text();
1288
+ throw new Error(`Server returned non-JSON response. Response: ${text.substring(0, 200)}...`);
1289
+ }
1290
+ const data = await res.json();
1291
+ if (!res.ok || !data.success)
1292
+ throw new Error(data.error || 'Story generation failed');
1293
+ return data;
1294
+ };
1295
+ // Helper function to build a conversational response from completion data
1296
+ // Uses special markers [SUCCESS], [ERROR], [TIP], [WRENCH] that renderMarkdown converts to icons
1297
+ // Track whether we've shown the refresh hint in this session
1298
+ const hasShownRefreshHint = useRef(false);
1299
+ const buildConversationalResponse = (completion, isUpdate) => {
1300
+ const parts = [];
1301
+ const statusMarker = completion.success ? '[SUCCESS]' : '[ERROR]';
1302
+ // Lead with the result - more conversational
1303
+ if (isUpdate) {
1304
+ parts.push(`${statusMarker} **Updated: "${completion.title}"**`);
1305
+ }
1306
+ else {
1307
+ parts.push(`${statusMarker} **Created: "${completion.title}"**`);
1308
+ }
1309
+ // Build component insights with reasons when available
1310
+ const componentCount = completion.componentsUsed?.length || 0;
1311
+ if (componentCount > 0) {
1312
+ const componentList = completion.componentsUsed.slice(0, 5);
1313
+ // Check if we have meaningful reasons (not just "Used in composition")
1314
+ const componentsWithReasons = componentList.filter(c => c.reason && c.reason !== 'Used in composition');
1315
+ if (componentsWithReasons.length > 0) {
1316
+ // Show components with their reasons
1317
+ const insights = componentsWithReasons
1318
+ .slice(0, 3)
1319
+ .map(c => `\`${c.name}\` - ${c.reason?.toLowerCase()}`)
1320
+ .join(', ');
1321
+ parts.push(`\nUsed ${insights}${componentCount > 3 ? ` and ${componentCount - 3} more` : ''}.`);
1322
+ }
1323
+ else {
1324
+ // Fallback to simple list
1325
+ const names = componentList.map(c => `\`${c.name}\``).join(', ');
1326
+ parts.push(`\nBuilt with ${names}${componentCount > 5 ? '...' : ''}.`);
1327
+ }
1328
+ }
1329
+ // Add layout decisions with educational context
1330
+ if (completion.layoutChoices && completion.layoutChoices.length > 0) {
1331
+ const primaryLayout = completion.layoutChoices[0];
1332
+ parts.push(`\n\n**Layout:** ${primaryLayout.pattern} - ${primaryLayout.reason.charAt(0).toLowerCase()}${primaryLayout.reason.slice(1)}.`);
1333
+ }
1334
+ // Add style choices only if they add value
1335
+ if (completion.styleChoices && completion.styleChoices.length > 0) {
1336
+ const notableStyles = completion.styleChoices.filter(s => s.reason && s.reason !== 'Semantic color from design system');
1337
+ if (notableStyles.length > 0) {
1338
+ const styleInfo = notableStyles[0];
1339
+ parts.push(` Applied \`${styleInfo.value}\` for ${styleInfo.reason?.toLowerCase() || 'visual consistency'}.`);
1340
+ }
1341
+ }
1342
+ // Add validation fixes notice
1343
+ if (completion.validation?.autoFixApplied) {
1344
+ parts.push(`\n\n[WRENCH] **Auto-fixed:** Minor syntax issues were automatically corrected.`);
1345
+ }
1346
+ // Add suggestions only if meaningful
1347
+ if (completion.suggestions && completion.suggestions.length > 0) {
1348
+ const suggestion = completion.suggestions[0];
1349
+ // Only show if it's not the generic "review the generated code" message
1350
+ if (!suggestion.toLowerCase().includes('review the generated code')) {
1351
+ parts.push(`\n\n[TIP] **Tip:** ${suggestion}`);
1352
+ }
1353
+ }
1354
+ // Show refresh hint only once per session for new stories (local mode only)
1355
+ // In Edge mode, stories are stored in Durable Objects, not on filesystem
1356
+ if (!isUpdate && !hasShownRefreshHint.current) {
1357
+ if (isEdgeMode()) {
1358
+ parts.push(`\n\n_Story saved to cloud. View code in chat history above._`);
1359
+ }
1360
+ else {
1361
+ parts.push(`\n\n_Refresh Storybook (Cmd/Ctrl + R) to see new stories in the sidebar._`);
1362
+ }
1363
+ hasShownRefreshHint.current = true;
1364
+ }
1365
+ // Add metrics in a subtle way (if available)
1366
+ if (completion.metrics?.totalTimeMs) {
1367
+ const seconds = (completion.metrics.totalTimeMs / 1000).toFixed(1);
1368
+ parts.push(`\n\n_${seconds}s_`);
1369
+ }
1370
+ return parts.join('');
1371
+ };
1372
+ // Helper function to finalize conversation after streaming completes
1373
+ const finalizeStreamingConversation = useCallback((newConversation, completion, userInput) => {
1374
+ // Build conversational response using rich completion data
1375
+ const isUpdate = completion.summary.action === 'updated';
1376
+ const responseMessage = buildConversationalResponse(completion, isUpdate);
1377
+ const aiMsg = { role: 'ai', content: responseMessage };
1378
+ const updatedConversation = [...newConversation, aiMsg];
1379
+ setConversation(updatedConversation);
1380
+ // Update chat session
1381
+ const isExistingSession = activeChatId && conversation.length > 0;
1382
+ if (isExistingSession && activeChatId) {
1383
+ const updatedSession = {
1384
+ id: activeChatId,
1385
+ title: activeTitle,
1386
+ fileName: completion.fileName || activeChatId,
1387
+ conversation: updatedConversation,
1388
+ lastUpdated: Date.now(),
1389
+ };
1390
+ const chats = loadChats();
1391
+ const chatIndex = chats.findIndex(c => c.id === activeChatId);
1392
+ if (chatIndex !== -1) {
1393
+ chats[chatIndex] = updatedSession;
1394
+ }
1395
+ saveChats(chats);
1396
+ setRecentChats(chats);
1397
+ }
1398
+ else {
1399
+ const chatId = completion.storyId || completion.fileName || Date.now().toString();
1400
+ const chatTitle = completion.title || userInput;
1401
+ setActiveChatId(chatId);
1402
+ setActiveTitle(chatTitle);
1403
+ const newSession = {
1404
+ id: chatId,
1405
+ title: chatTitle,
1406
+ fileName: completion.fileName || '',
1407
+ conversation: updatedConversation,
1408
+ lastUpdated: Date.now(),
1409
+ };
1410
+ const chats = loadChats().filter(c => c.id !== chatId);
1411
+ chats.unshift(newSession);
1412
+ if (chats.length > MAX_RECENT_CHATS) {
1413
+ chats.splice(MAX_RECENT_CHATS);
1414
+ }
1415
+ saveChats(chats);
1416
+ setRecentChats(chats);
1417
+ }
1418
+ }, [activeChatId, activeTitle, conversation.length]);
1419
+ const handleSend = async (e) => {
1420
+ if (e)
1421
+ e.preventDefault();
1422
+ // Allow sending with either text or images
1423
+ if (!input.trim() && attachedImages.length === 0)
1424
+ return;
1425
+ // Use input text or default vision prompt if only images
1426
+ const userInput = input.trim() || (attachedImages.length > 0 ? 'Create a component that matches this design' : '');
1427
+ setError(null);
1428
+ setLoading(true);
1429
+ setStreamingState(null);
1430
+ // Test connection before sending
1431
+ const connectionTest = await testMCPConnection();
1432
+ setConnectionStatus(connectionTest);
1433
+ if (!connectionTest.connected) {
1434
+ setError(`Cannot connect to MCP server: ${connectionTest.error || 'Server not running'}`);
1435
+ setLoading(false);
1436
+ return;
1437
+ }
1438
+ // Capture images before clearing
1439
+ const imagesToSend = [...attachedImages];
1440
+ const hasImages = imagesToSend.length > 0;
1441
+ // Create user message with images
1442
+ const userMessage = {
1443
+ role: 'user',
1444
+ content: userInput,
1445
+ attachedImages: hasImages ? imagesToSend : undefined
1446
+ };
1447
+ const newConversation = [...conversation, userMessage];
1448
+ setConversation(newConversation);
1449
+ setInput('');
1450
+ clearAttachedImages();
1451
+ // Use streaming if enabled
1452
+ if (USE_STREAMING) {
1453
+ try {
1454
+ // Cancel any existing request
1455
+ if (abortControllerRef.current) {
1456
+ abortControllerRef.current.abort();
1457
+ }
1458
+ abortControllerRef.current = new AbortController();
1459
+ // Initialize streaming state
1460
+ setStreamingState({});
1461
+ // Prepare images for API request
1462
+ const imagePayload = hasImages
1463
+ ? imagesToSend.map(img => ({
1464
+ type: 'base64',
1465
+ data: img.base64,
1466
+ mediaType: img.file.type,
1467
+ }))
1468
+ : undefined;
1469
+ const response = await fetch(MCP_STREAM_API, {
1470
+ method: 'POST',
1471
+ headers: { 'Content-Type': 'application/json' },
1472
+ body: JSON.stringify({
1473
+ prompt: userInput,
1474
+ conversation: newConversation,
1475
+ fileName: activeChatId || undefined,
1476
+ isUpdate: activeChatId && conversation.length > 0,
1477
+ originalTitle: activeTitle || undefined,
1478
+ storyId: activeChatId || undefined,
1479
+ images: imagePayload,
1480
+ visionMode: hasImages ? 'screenshot_to_story' : undefined,
1481
+ provider: selectedProvider || undefined,
1482
+ model: selectedModel || undefined,
1483
+ considerations: considerations || undefined,
1484
+ }),
1485
+ signal: abortControllerRef.current.signal,
1486
+ });
1487
+ if (!response.ok) {
1488
+ throw new Error(`Streaming request failed: ${response.status}`);
1489
+ }
1490
+ const reader = response.body?.getReader();
1491
+ if (!reader) {
1492
+ throw new Error('No response body');
1493
+ }
1494
+ const decoder = new TextDecoder();
1495
+ let buffer = '';
1496
+ let completionData = null;
1497
+ let errorData = null;
1498
+ while (true) {
1499
+ const { done, value } = await reader.read();
1500
+ if (done)
1501
+ break;
1502
+ buffer += decoder.decode(value, { stream: true });
1503
+ // Parse SSE events from buffer
1504
+ const lines = buffer.split('\n');
1505
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
1506
+ for (const line of lines) {
1507
+ if (line.startsWith('data: ')) {
1508
+ try {
1509
+ const event = JSON.parse(line.slice(6));
1510
+ // Update streaming state based on event type
1511
+ switch (event.type) {
1512
+ case 'intent':
1513
+ setStreamingState(prev => ({ ...prev, intent: event.data }));
1514
+ break;
1515
+ case 'progress':
1516
+ setStreamingState(prev => ({ ...prev, progress: event.data }));
1517
+ break;
1518
+ case 'validation':
1519
+ setStreamingState(prev => ({ ...prev, validation: event.data }));
1520
+ break;
1521
+ case 'retry':
1522
+ setStreamingState(prev => ({ ...prev, retry: event.data }));
1523
+ break;
1524
+ case 'completion':
1525
+ completionData = event.data;
1526
+ setStreamingState(prev => ({ ...prev, completion: event.data }));
1527
+ break;
1528
+ case 'error':
1529
+ errorData = event.data;
1530
+ setStreamingState(prev => ({ ...prev, error: event.data }));
1531
+ break;
1532
+ }
1533
+ }
1534
+ catch (parseError) {
1535
+ console.warn('Failed to parse SSE event:', line, parseError);
1536
+ }
1537
+ }
1538
+ }
1539
+ }
1540
+ // Handle completion or error
1541
+ if (completionData) {
1542
+ finalizeStreamingConversation(newConversation, completionData, userInput);
1543
+ }
1544
+ else if (errorData) {
1545
+ setError(errorData.message);
1546
+ const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorData.message}\n\n${errorData.suggestion || ''}` }];
1547
+ setConversation(errorConversation);
1548
+ }
1549
+ }
1550
+ catch (err) {
1551
+ if (err.name === 'AbortError') {
1552
+ console.log('Request aborted');
1553
+ return;
1554
+ }
1555
+ // Fall back to non-streaming on error
1556
+ console.warn('Streaming failed, falling back to non-streaming:', err);
1557
+ setStreamingState(null);
1558
+ try {
1559
+ const data = await handleSendNonStreaming(userInput, newConversation);
1560
+ // Process non-streaming response (same as before)
1561
+ let responseMessage;
1562
+ const statusIcon = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '🔧' : '⚠️') : '✅';
1563
+ // Build conversational response for fallback
1564
+ if (data.isUpdate) {
1565
+ responseMessage = `${statusIcon} **Updated: "${data.title}"**\n\nI've made the requested changes to your component. You can view the updated version in Storybook.\n\n_Check the Docs tab to see both the rendered component and its code._`;
1566
+ }
1567
+ else {
1568
+ responseMessage = `${statusIcon} **Created: "${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.\n\n💡 **Note**: If you don't see the story immediately, you may need to refresh your Storybook page (Cmd/Ctrl + R).`;
1569
+ }
1570
+ const aiMsg = { role: 'ai', content: responseMessage };
1571
+ const updatedConversation = [...newConversation, aiMsg];
1572
+ setConversation(updatedConversation);
1573
+ // Update chat session
1574
+ const isUpdate = activeChatId && conversation.length > 0;
1575
+ if (isUpdate && activeChatId) {
1576
+ const updatedSession = {
1577
+ id: activeChatId,
1578
+ title: activeTitle,
1579
+ fileName: data.fileName || activeChatId,
1580
+ conversation: updatedConversation,
1581
+ lastUpdated: Date.now(),
1582
+ };
1583
+ const chats = loadChats();
1584
+ const chatIndex = chats.findIndex(c => c.id === activeChatId);
1585
+ if (chatIndex !== -1)
1586
+ chats[chatIndex] = updatedSession;
1587
+ saveChats(chats);
1588
+ setRecentChats(chats);
1589
+ }
1590
+ else {
1591
+ const chatId = data.storyId || data.fileName || Date.now().toString();
1592
+ setActiveChatId(chatId);
1593
+ setActiveTitle(data.title || userInput);
1594
+ const newSession = {
1595
+ id: chatId,
1596
+ title: data.title || userInput,
1597
+ fileName: data.fileName || '',
1598
+ conversation: updatedConversation,
1599
+ lastUpdated: Date.now(),
1600
+ };
1601
+ const chats = loadChats().filter(c => c.id !== chatId);
1602
+ chats.unshift(newSession);
1603
+ if (chats.length > MAX_RECENT_CHATS)
1604
+ chats.splice(MAX_RECENT_CHATS);
1605
+ saveChats(chats);
1606
+ setRecentChats(chats);
1607
+ }
1608
+ }
1609
+ catch (fallbackErr) {
1610
+ const errorMessage = fallbackErr instanceof Error ? fallbackErr.message : 'Unknown error';
1611
+ setError(errorMessage);
1612
+ const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorMessage}` }];
1613
+ setConversation(errorConversation);
1614
+ }
1615
+ }
1616
+ finally {
1617
+ setLoading(false);
1618
+ setStreamingState(null);
1619
+ abortControllerRef.current = null;
1620
+ }
1621
+ }
1622
+ else {
1623
+ // Non-streaming mode (original implementation)
1624
+ try {
1625
+ const data = await handleSendNonStreaming(userInput, newConversation);
1626
+ let responseMessage;
1627
+ const statusIcon = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '🔧' : '⚠️') : '✅';
1628
+ // Build conversational response for non-streaming mode
1629
+ if (data.isUpdate) {
1630
+ responseMessage = `${statusIcon} **Updated: "${data.title}"**\n\nI've made the requested changes to your component. You can view the updated version in Storybook.\n\n_Check the Docs tab to see both the rendered component and its code._`;
1631
+ }
1632
+ else {
1633
+ responseMessage = `${statusIcon} **Created: "${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.\n\n💡 **Note**: If you don't see the story immediately, you may need to refresh your Storybook page (Cmd/Ctrl + R).`;
1634
+ }
1635
+ const aiMsg = { role: 'ai', content: responseMessage };
1636
+ const updatedConversation = [...newConversation, aiMsg];
1637
+ setConversation(updatedConversation);
1638
+ const isUpdate = activeChatId && conversation.length > 0;
1639
+ if (isUpdate && activeChatId) {
1640
+ const updatedSession = {
1641
+ id: activeChatId,
1642
+ title: activeTitle,
1643
+ fileName: data.fileName || activeChatId,
1644
+ conversation: updatedConversation,
1645
+ lastUpdated: Date.now(),
1646
+ };
1647
+ const chats = loadChats();
1648
+ const chatIndex = chats.findIndex(c => c.id === activeChatId);
1649
+ if (chatIndex !== -1)
1650
+ chats[chatIndex] = updatedSession;
1651
+ saveChats(chats);
1652
+ setRecentChats(chats);
1653
+ }
1654
+ else {
1655
+ const chatId = data.storyId || data.fileName || Date.now().toString();
1656
+ setActiveChatId(chatId);
1657
+ setActiveTitle(data.title || userInput);
1658
+ const newSession = {
1659
+ id: chatId,
1660
+ title: data.title || userInput,
1661
+ fileName: data.fileName || '',
1662
+ conversation: updatedConversation,
1663
+ lastUpdated: Date.now(),
1664
+ };
1665
+ const chats = loadChats().filter(c => c.id !== chatId);
1666
+ chats.unshift(newSession);
1667
+ if (chats.length > MAX_RECENT_CHATS)
1668
+ chats.splice(MAX_RECENT_CHATS);
1669
+ saveChats(chats);
1670
+ setRecentChats(chats);
1671
+ }
1672
+ }
1673
+ catch (err) {
1674
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
1675
+ setError(errorMessage);
1676
+ const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorMessage}` }];
1677
+ setConversation(errorConversation);
1678
+ }
1679
+ finally {
1680
+ setLoading(false);
1681
+ }
1682
+ }
1683
+ };
1684
+ const handleSelectChat = (chat) => {
1685
+ setConversation(chat.conversation);
1686
+ setActiveChatId(chat.id);
1687
+ setActiveTitle(chat.title);
1688
+ };
1689
+ const handleNewChat = () => {
1690
+ setConversation([]);
1691
+ setActiveChatId(null);
1692
+ setActiveTitle('');
1693
+ };
1694
+ const handleDeleteChat = async (chatId, e) => {
1695
+ e.stopPropagation(); // Prevent selecting the chat
1696
+ if (confirm('Delete this story and chat? This action cannot be undone.')) {
1697
+ const success = await deleteStoryAndChat(chatId);
1698
+ if (success) {
1699
+ // Update local state
1700
+ const updatedChats = recentChats.filter(chat => chat.id !== chatId);
1701
+ setRecentChats(updatedChats);
1702
+ // If we deleted the active chat, switch to another or clear
1703
+ if (activeChatId === chatId) {
1704
+ if (updatedChats.length > 0) {
1705
+ setConversation(updatedChats[0].conversation);
1706
+ setActiveChatId(updatedChats[0].id);
1707
+ setActiveTitle(updatedChats[0].title);
1708
+ }
1709
+ else {
1710
+ handleNewChat();
1711
+ }
1712
+ }
1713
+ }
1714
+ else {
1715
+ alert('Failed to delete story. Please try again.');
1716
+ }
1717
+ }
1718
+ };
1719
+ return (_jsxs("div", { style: STYLES.container, children: [_jsxs("div", { style: {
1720
+ ...STYLES.sidebar,
1721
+ ...(sidebarOpen ? {} : STYLES.sidebarCollapsed),
1722
+ }, children: [sidebarOpen && (_jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '16px' }, children: [_jsxs("button", { onClick: () => setSidebarOpen(false), style: STYLES.sidebarToggle, title: "Collapse sidebar", onMouseEnter: (e) => {
1723
+ e.currentTarget.style.transform = 'translateY(-1px)';
1724
+ e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.4)';
1725
+ }, onMouseLeave: (e) => {
1726
+ e.currentTarget.style.transform = 'translateY(0)';
1727
+ e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.3)';
1728
+ }, children: [_jsx("span", { style: { lineHeight: '0.5', display: 'inline-block', alignItems: 'center', width: '10px', height: '10px' }, children: "\u2630" }), _jsx("span", { children: "Chats" })] }), _jsxs("button", { onClick: handleNewChat, style: STYLES.newChatButton, onMouseEnter: (e) => {
1729
+ e.currentTarget.style.transform = 'translateY(-1px)';
1730
+ e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.4)';
1731
+ }, onMouseLeave: (e) => {
1732
+ e.currentTarget.style.transform = 'translateY(0)';
1733
+ e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.2)';
1734
+ }, children: [_jsx("span", { style: { lineHeight: '0.5', display: 'inline-block', alignItems: 'center', width: '10px', height: '10px' }, children: "+" }), _jsx("span", { children: "New Chat" })] }), recentChats.length > 0 && (_jsx("div", { style: {
1735
+ color: '#64748b',
1736
+ fontSize: '12px',
1737
+ marginBottom: '8px',
1738
+ fontWeight: '500',
1739
+ textTransform: 'uppercase',
1740
+ letterSpacing: '0.05em',
1741
+ }, children: "Recent Chats" })), recentChats.map(chat => (_jsxs("div", { onClick: () => handleSelectChat(chat), style: {
1742
+ ...STYLES.chatItem,
1743
+ ...(activeChatId === chat.id ? STYLES.chatItemActive : {}),
1744
+ }, onMouseEnter: (e) => {
1745
+ if (activeChatId !== chat.id) {
1746
+ e.currentTarget.style.background = 'rgba(255, 255, 255, 0.12)';
1747
+ }
1748
+ const deleteBtn = e.currentTarget.querySelector('.delete-btn');
1749
+ if (deleteBtn)
1750
+ deleteBtn.style.opacity = '1';
1751
+ }, onMouseLeave: (e) => {
1752
+ if (activeChatId !== chat.id) {
1753
+ e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)';
1754
+ }
1755
+ const deleteBtn = e.currentTarget.querySelector('.delete-btn');
1756
+ if (deleteBtn)
1757
+ deleteBtn.style.opacity = '0';
1758
+ }, children: [_jsx("div", { style: STYLES.chatItemTitle, children: chat.title }), _jsx("div", { style: STYLES.chatItemTime, children: formatTime(chat.lastUpdated) }), _jsx("button", { className: "delete-btn", onClick: (e) => handleDeleteChat(chat.id, e), style: STYLES.deleteButton, title: "Delete chat", children: "\u2715" })] }, chat.id)))] })), !sidebarOpen && (_jsx("div", { style: { padding: '8px', display: 'flex', justifyContent: 'center' }, children: _jsx("button", { onClick: () => setSidebarOpen(true), style: {
1759
+ ...STYLES.sidebarToggle,
1760
+ width: '38px',
1761
+ height: '38px',
1762
+ padding: '0',
1763
+ fontSize: '16px',
1764
+ borderRadius: '8px',
1765
+ }, title: "Expand sidebar", onMouseEnter: (e) => {
1766
+ e.currentTarget.style.transform = 'scale(1.05)';
1767
+ e.currentTarget.style.background = '#2563eb';
1768
+ }, onMouseLeave: (e) => {
1769
+ e.currentTarget.style.transform = 'scale(1)';
1770
+ e.currentTarget.style.background = '#3b82f6';
1771
+ }, children: _jsx("span", { style: { lineHeight: '0.4', display: 'inline-block', height: '10px' }, children: "\u2630" }) }) }))] }), _jsxs("div", { style: { ...STYLES.mainContent, position: 'relative' }, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [isDragging && (_jsx("div", { style: STYLES.dropOverlay, children: _jsxs("div", { style: STYLES.dropOverlayText, children: [_jsx("svg", { width: 24, height: 24, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }), "Drop images here"] }) })), _jsxs("div", { style: STYLES.chatHeader, children: [_jsx("h1", { style: {
1772
+ fontSize: '24px',
1773
+ margin: 0,
1774
+ fontWeight: '600',
1775
+ background: 'linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%)',
1776
+ WebkitBackgroundClip: 'text',
1777
+ WebkitTextFillColor: 'transparent',
1778
+ display: 'inline-block'
1779
+ }, children: "Story UI" }), _jsx("p", { style: { fontSize: '14px', margin: '4px 0 0 0', color: '#94a3b8' }, children: "Generate Storybook stories with AI" }), _jsxs("div", { style: {
1780
+ display: 'flex',
1781
+ alignItems: 'center',
1782
+ gap: '8px',
1783
+ marginTop: '8px',
1784
+ fontSize: '12px'
1785
+ }, children: [_jsx("div", { style: {
1786
+ width: '8px',
1787
+ height: '8px',
1788
+ borderRadius: '50%',
1789
+ backgroundColor: connectionStatus.connected ? '#10b981' : '#f87171'
1790
+ } }), _jsx("span", { style: { color: connectionStatus.connected ? '#10b981' : '#f87171' }, children: connectionStatus.connected
1791
+ ? `Connected to ${getConnectionDisplayText()}`
1792
+ : `Disconnected: ${connectionStatus.error || 'Server not running'}` })] }), connectionStatus.connected && availableProviders.length > 0 && (_jsxs("div", { style: {
1793
+ display: 'flex',
1794
+ gap: '12px',
1795
+ marginTop: '12px',
1796
+ flexWrap: 'wrap'
1797
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '6px' }, children: [_jsx("label", { style: { fontSize: '12px', color: '#94a3b8' }, children: "Provider:" }), _jsx("select", { value: selectedProvider, onChange: (e) => {
1798
+ const newProvider = e.target.value;
1799
+ setSelectedProvider(newProvider);
1800
+ // Reset model to first available for new provider
1801
+ const provider = availableProviders.find(p => p.type === newProvider);
1802
+ if (provider && provider.models.length > 0) {
1803
+ setSelectedModel(provider.models[0]);
1804
+ }
1805
+ }, style: {
1806
+ background: '#1e293b',
1807
+ border: '1px solid #334155',
1808
+ borderRadius: '6px',
1809
+ color: '#e2e8f0',
1810
+ padding: '4px 8px',
1811
+ fontSize: '12px',
1812
+ cursor: 'pointer'
1813
+ }, children: availableProviders.map(p => (_jsx("option", { value: p.type, children: p.name }, p.type))) })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '6px' }, children: [_jsx("label", { style: { fontSize: '12px', color: '#94a3b8' }, children: "Model:" }), _jsx("select", { value: selectedModel, onChange: (e) => setSelectedModel(e.target.value), style: {
1814
+ background: '#1e293b',
1815
+ border: '1px solid #334155',
1816
+ borderRadius: '6px',
1817
+ color: '#e2e8f0',
1818
+ padding: '4px 8px',
1819
+ fontSize: '12px',
1820
+ cursor: 'pointer',
1821
+ maxWidth: '200px'
1822
+ }, children: availableProviders
1823
+ .find(p => p.type === selectedProvider)
1824
+ ?.models.map(model => (_jsx("option", { value: model, children: model }, model))) })] })] }))] }), _jsxs("div", { style: STYLES.chatContainer, children: [error && (_jsx("div", { style: STYLES.errorMessage, children: error })), conversation.length === 0 && !loading && (_jsxs("div", { style: STYLES.emptyState, children: [_jsx("div", { style: STYLES.emptyStateTitle, children: "Start a new conversation" }), _jsx("div", { style: STYLES.emptyStateSubtitle, children: "Describe the UI component you'd like to create" })] })), conversation.map((msg, i) => (_jsx("div", { style: STYLES.messageContainer, children: _jsxs("div", { style: msg.role === 'user' ? STYLES.userMessage : STYLES.aiMessage, children: [msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content, msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (_jsx("div", { style: STYLES.userMessageImages, children: msg.attachedImages.map((img) => (_jsx("img", { src: img.preview, alt: "attached", style: STYLES.userMessageImage }, img.id))) }))] }) }, i))), loading && (_jsx("div", { style: STYLES.messageContainer, children: streamingState ? (_jsx(StreamingProgressMessage, { streamingData: streamingState })) : (_jsxs("div", { style: STYLES.loadingMessage, children: [_jsx("span", { children: "Generating story" }), _jsx("span", { className: "loading-dots" })] })) })), _jsx("div", { ref: chatEndRef })] }), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: 'none' }, onChange: handleFileSelect }), attachedImages.length > 0 && (_jsxs("div", { style: STYLES.imagePreviewContainer, children: [_jsxs("span", { style: STYLES.imagePreviewLabel, children: [_jsx("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }), attachedImages.length, " image", attachedImages.length > 1 ? 's' : '', " attached"] }), attachedImages.map((img) => (_jsxs("div", { style: STYLES.imagePreviewItem, children: [_jsx("img", { src: img.preview, alt: "preview", style: STYLES.imagePreviewImg }), _jsx("button", { type: "button", style: STYLES.imageRemoveButton, onClick: () => removeAttachedImage(img.id), title: "Remove image", children: "\u00D7" })] }, img.id)))] })), _jsxs("form", { onSubmit: handleSend, style: {
1825
+ ...STYLES.inputForm,
1826
+ ...(attachedImages.length > 0 ? {
1827
+ marginTop: 0,
1828
+ borderTopLeftRadius: 0,
1829
+ borderTopRightRadius: 0,
1830
+ } : {})
1831
+ }, children: [_jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), disabled: loading || attachedImages.length >= MAX_IMAGES, style: {
1832
+ ...STYLES.uploadButton,
1833
+ ...(attachedImages.length >= MAX_IMAGES ? {
1834
+ opacity: 0.5,
1835
+ cursor: 'not-allowed',
1836
+ } : {})
1837
+ }, title: attachedImages.length >= MAX_IMAGES
1838
+ ? `Maximum ${MAX_IMAGES} images`
1839
+ : 'Attach images (screenshots, designs)', onMouseEnter: (e) => {
1840
+ if (attachedImages.length < MAX_IMAGES && !loading) {
1841
+ e.currentTarget.style.background = 'rgba(59, 130, 246, 0.2)';
1842
+ e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
1843
+ }
1844
+ }, onMouseLeave: (e) => {
1845
+ e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
1846
+ e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
1847
+ }, children: _jsx("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }) }), _jsx("input", { ref: inputRef, type: "text", value: input, onChange: e => setInput(e.target.value), onPaste: handlePaste, placeholder: attachedImages.length > 0
1848
+ ? "Describe what to create from these images..."
1849
+ : "Describe a UI component...", style: STYLES.textInput, onFocus: (e) => {
1850
+ e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
1851
+ e.currentTarget.style.boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.1)';
1852
+ }, onBlur: (e) => {
1853
+ e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
1854
+ e.currentTarget.style.boxShadow = 'none';
1855
+ } }), _jsxs("button", { type: "submit", disabled: loading || (!input.trim() && attachedImages.length === 0), style: {
1856
+ ...STYLES.sendButton,
1857
+ ...(loading || (!input.trim() && attachedImages.length === 0) ? {
1858
+ opacity: 0.5,
1859
+ cursor: 'not-allowed',
1860
+ background: '#6b7280',
1861
+ boxShadow: 'none'
1862
+ } : {})
1863
+ }, onMouseEnter: (e) => {
1864
+ if (!loading && (input.trim() || attachedImages.length > 0)) {
1865
+ e.currentTarget.style.transform = 'scale(1.05)';
1866
+ e.currentTarget.style.boxShadow = '0 4px 16px rgba(16, 185, 129, 0.4)';
1867
+ }
1868
+ }, onMouseLeave: (e) => {
1869
+ e.currentTarget.style.transform = 'scale(1)';
1870
+ e.currentTarget.style.boxShadow = '0 2px 8px rgba(16, 185, 129, 0.3)';
1871
+ }, children: [_jsx("span", { children: "Send" }), _jsx("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })] })] })] })] }));
1872
+ }
1873
+ export default StoryUIPanel;
1874
+ export { StoryUIPanel };