@tpitre/story-ui 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/templates/StoryUI/StoryUIPanel.d.ts +12 -1
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +751 -1997
- package/package.json +1 -1
- package/templates/StoryUI/StoryUIPanel.css +1440 -0
- package/templates/StoryUI/StoryUIPanel.tsx +1224 -2754
|
@@ -1,1284 +1,595 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* StoryUIPanel - AI-powered Storybook story generator
|
|
4
|
+
*
|
|
5
|
+
* ShadCN-inspired design with Gemini-style layout.
|
|
6
|
+
* Self-contained React component with no external UI dependencies.
|
|
7
|
+
* Supports light and dark modes based on Storybook theme.
|
|
8
|
+
*/
|
|
9
|
+
import React, { useState, useEffect, useRef, useCallback, useReducer } from 'react';
|
|
10
|
+
import './StoryUIPanel.css';
|
|
11
|
+
const initialState = {
|
|
12
|
+
sidebarOpen: true,
|
|
13
|
+
showCode: false,
|
|
14
|
+
isDragging: false,
|
|
15
|
+
loading: false,
|
|
16
|
+
isBulkDeleting: false,
|
|
17
|
+
conversation: [],
|
|
18
|
+
recentChats: [],
|
|
19
|
+
orphanStories: [],
|
|
20
|
+
activeChatId: null,
|
|
21
|
+
activeTitle: '',
|
|
22
|
+
input: '',
|
|
23
|
+
attachedImages: [],
|
|
24
|
+
selectedStoryIds: new Set(),
|
|
25
|
+
availableProviders: [],
|
|
26
|
+
selectedProvider: '',
|
|
27
|
+
selectedModel: '',
|
|
28
|
+
connectionStatus: { connected: false },
|
|
29
|
+
streamingState: null,
|
|
30
|
+
error: null,
|
|
31
|
+
considerations: '',
|
|
32
|
+
isDarkMode: false,
|
|
33
|
+
};
|
|
34
|
+
function panelReducer(state, action) {
|
|
35
|
+
switch (action.type) {
|
|
36
|
+
case 'TOGGLE_SIDEBAR':
|
|
37
|
+
return { ...state, sidebarOpen: !state.sidebarOpen };
|
|
38
|
+
case 'SET_SIDEBAR':
|
|
39
|
+
return { ...state, sidebarOpen: action.payload };
|
|
40
|
+
case 'TOGGLE_CODE':
|
|
41
|
+
return { ...state, showCode: !state.showCode };
|
|
42
|
+
case 'SET_DRAGGING':
|
|
43
|
+
return { ...state, isDragging: action.payload };
|
|
44
|
+
case 'SET_LOADING':
|
|
45
|
+
return { ...state, loading: action.payload };
|
|
46
|
+
case 'SET_BULK_DELETING':
|
|
47
|
+
return { ...state, isBulkDeleting: action.payload };
|
|
48
|
+
case 'SET_CONVERSATION':
|
|
49
|
+
return { ...state, conversation: action.payload };
|
|
50
|
+
case 'ADD_MESSAGE':
|
|
51
|
+
return { ...state, conversation: [...state.conversation, action.payload] };
|
|
52
|
+
case 'SET_RECENT_CHATS':
|
|
53
|
+
return { ...state, recentChats: action.payload };
|
|
54
|
+
case 'SET_ORPHAN_STORIES':
|
|
55
|
+
return { ...state, orphanStories: action.payload };
|
|
56
|
+
case 'SET_ACTIVE_CHAT':
|
|
57
|
+
return { ...state, activeChatId: action.payload.id, activeTitle: action.payload.title };
|
|
58
|
+
case 'SET_INPUT':
|
|
59
|
+
return { ...state, input: action.payload };
|
|
60
|
+
case 'SET_ATTACHED_IMAGES':
|
|
61
|
+
return { ...state, attachedImages: action.payload };
|
|
62
|
+
case 'ADD_ATTACHED_IMAGE':
|
|
63
|
+
return { ...state, attachedImages: [...state.attachedImages, action.payload] };
|
|
64
|
+
case 'REMOVE_ATTACHED_IMAGE':
|
|
65
|
+
return {
|
|
66
|
+
...state,
|
|
67
|
+
attachedImages: state.attachedImages.filter(img => img.id !== action.payload),
|
|
68
|
+
};
|
|
69
|
+
case 'CLEAR_ATTACHED_IMAGES':
|
|
70
|
+
return { ...state, attachedImages: [] };
|
|
71
|
+
case 'SET_SELECTED_STORY_IDS':
|
|
72
|
+
return { ...state, selectedStoryIds: action.payload };
|
|
73
|
+
case 'TOGGLE_STORY_SELECTION': {
|
|
74
|
+
const newSet = new Set(state.selectedStoryIds);
|
|
75
|
+
if (newSet.has(action.payload)) {
|
|
76
|
+
newSet.delete(action.payload);
|
|
64
77
|
}
|
|
65
78
|
else {
|
|
66
|
-
|
|
67
|
-
remaining = remaining.slice(nextSpecial);
|
|
79
|
+
newSet.add(action.payload);
|
|
68
80
|
}
|
|
81
|
+
return { ...state, selectedStoryIds: newSet };
|
|
69
82
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
// Get friendly display name for a model, falling back to the API name if not found
|
|
110
|
-
const getModelDisplayName = (modelId) => {
|
|
111
|
-
return MODEL_DISPLAY_NAMES[modelId] || modelId;
|
|
112
|
-
};
|
|
113
|
-
// Determine the MCP API base URL.
|
|
114
|
-
// Priority order:
|
|
115
|
-
// 1. VITE_STORY_UI_EDGE_URL - Edge Worker URL for cloud deployments
|
|
116
|
-
// 2. window.__STORY_UI_EDGE_URL__ - Runtime override for edge URL
|
|
117
|
-
// 3. Production domains (railway.app, render.com, pages.dev) - use same origin
|
|
118
|
-
// 4. VITE_STORY_UI_PORT - Custom port for localhost
|
|
119
|
-
// 5. window.__STORY_UI_PORT__ - Legacy port override
|
|
120
|
-
// 6. window.STORY_UI_MCP_PORT - MCP port override
|
|
121
|
-
// 7. Default to localhost:4001
|
|
122
|
-
const getApiBaseUrl = () => {
|
|
123
|
-
// Check for Edge Worker URL (cloud deployment)
|
|
124
|
-
const edgeUrl = import.meta.env?.VITE_STORY_UI_EDGE_URL;
|
|
125
|
-
if (edgeUrl)
|
|
126
|
-
return edgeUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
127
|
-
// Check for window override for edge URL (support both naming conventions)
|
|
128
|
-
const windowEdgeUrl = window.__STORY_UI_EDGE_URL__ || window.STORY_UI_EDGE_URL;
|
|
129
|
-
if (windowEdgeUrl)
|
|
130
|
-
return windowEdgeUrl.replace(/\/$/, '');
|
|
131
|
-
// Check if we're running on Railway production domain
|
|
132
|
-
// In this case, the MCP server is proxied through the same origin
|
|
83
|
+
case 'SET_PROVIDERS':
|
|
84
|
+
return { ...state, availableProviders: action.payload };
|
|
85
|
+
case 'SET_SELECTED_PROVIDER':
|
|
86
|
+
return { ...state, selectedProvider: action.payload };
|
|
87
|
+
case 'SET_SELECTED_MODEL':
|
|
88
|
+
return { ...state, selectedModel: action.payload };
|
|
89
|
+
case 'SET_CONNECTION_STATUS':
|
|
90
|
+
return { ...state, connectionStatus: action.payload };
|
|
91
|
+
case 'SET_STREAMING_STATE':
|
|
92
|
+
return { ...state, streamingState: action.payload };
|
|
93
|
+
case 'UPDATE_STREAMING_STATE':
|
|
94
|
+
return { ...state, streamingState: { ...state.streamingState, ...action.payload } };
|
|
95
|
+
case 'SET_ERROR':
|
|
96
|
+
return { ...state, error: action.payload };
|
|
97
|
+
case 'SET_CONSIDERATIONS':
|
|
98
|
+
return { ...state, considerations: action.payload };
|
|
99
|
+
case 'SET_DARK_MODE':
|
|
100
|
+
return { ...state, isDarkMode: action.payload };
|
|
101
|
+
case 'NEW_CHAT':
|
|
102
|
+
return { ...state, conversation: [], activeChatId: null, activeTitle: '' };
|
|
103
|
+
default:
|
|
104
|
+
return state;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// ============================================
|
|
108
|
+
// Constants
|
|
109
|
+
// ============================================
|
|
110
|
+
const USE_STREAMING = true;
|
|
111
|
+
const MAX_RECENT_CHATS = 20;
|
|
112
|
+
const CHAT_STORAGE_KEY = 'story-ui-chats';
|
|
113
|
+
const MAX_IMAGES = 4;
|
|
114
|
+
const MAX_IMAGE_SIZE_MB = 20;
|
|
115
|
+
// ============================================
|
|
116
|
+
// Helper Functions
|
|
117
|
+
// ============================================
|
|
118
|
+
function getApiBaseUrl() {
|
|
119
|
+
if (typeof import.meta !== 'undefined' && import.meta.env?.VITE_STORY_UI_EDGE_URL) {
|
|
120
|
+
return import.meta.env.VITE_STORY_UI_EDGE_URL;
|
|
121
|
+
}
|
|
133
122
|
if (typeof window !== 'undefined') {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
123
|
+
if (window.__STORY_UI_EDGE_URL__) {
|
|
124
|
+
return window.__STORY_UI_EDGE_URL__;
|
|
125
|
+
}
|
|
126
|
+
if (window.location.hostname.includes('railway.app')) {
|
|
127
|
+
return window.location.origin;
|
|
138
128
|
}
|
|
139
129
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return `http://localhost:${windowOverride}`;
|
|
148
|
-
// Check for MCP port override set by stories file
|
|
149
|
-
const mcpOverride = window.STORY_UI_MCP_PORT;
|
|
150
|
-
if (mcpOverride)
|
|
151
|
-
return `http://localhost:${mcpOverride}`;
|
|
152
|
-
return 'http://localhost:4001';
|
|
153
|
-
};
|
|
154
|
-
// Helper to check if we're using Edge mode (cloud deployment)
|
|
155
|
-
const isEdgeMode = () => {
|
|
156
|
-
const baseUrl = getApiBaseUrl();
|
|
157
|
-
return baseUrl.includes('workers.dev') || baseUrl.includes('pages.dev') ||
|
|
158
|
-
baseUrl.startsWith('https://') && !baseUrl.includes('localhost');
|
|
159
|
-
};
|
|
160
|
-
// Helper to convert story title to Storybook URL format
|
|
161
|
-
// e.g., "Simple Card With Image" -> "generated-simple-card-with-image--default"
|
|
162
|
-
const titleToStoryPath = (title) => {
|
|
163
|
-
const kebabTitle = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
164
|
-
return `generated-${kebabTitle}--default`;
|
|
165
|
-
};
|
|
166
|
-
// Helper to navigate to a newly created story after generation completes
|
|
167
|
-
// In dev mode with HMR, this prevents the "Couldn't find story after HMR" error
|
|
168
|
-
// In all modes, this provides a better UX by auto-navigating to the new story
|
|
169
|
-
const navigateToNewStory = (title, _code, delayMs = 1500) => {
|
|
170
|
-
const storyPath = titleToStoryPath(title);
|
|
171
|
-
console.log(`[Story UI] Will navigate to story "${storyPath}" in ${delayMs}ms...`);
|
|
172
|
-
setTimeout(() => {
|
|
173
|
-
// Navigate the TOP window (parent Storybook UI), not the iframe
|
|
174
|
-
// The Story UI panel runs inside an iframe, so we need window.top to escape it
|
|
175
|
-
const topWindow = window.top || window;
|
|
176
|
-
const newUrl = `${topWindow.location.origin}/?path=/story/${storyPath}`;
|
|
177
|
-
console.log(`[Story UI] Navigating parent window to: ${newUrl}`);
|
|
178
|
-
topWindow.location.href = newUrl;
|
|
179
|
-
}, delayMs);
|
|
180
|
-
};
|
|
181
|
-
// Legacy helper for backwards compatibility
|
|
182
|
-
const getApiPort = () => {
|
|
183
|
-
const baseUrl = getApiBaseUrl();
|
|
184
|
-
const match = baseUrl.match(/:(\d+)$/);
|
|
185
|
-
return match ? match[1] : '4001';
|
|
186
|
-
};
|
|
187
|
-
// Get connection display text
|
|
188
|
-
const getConnectionDisplayText = () => {
|
|
189
|
-
const baseUrl = getApiBaseUrl();
|
|
190
|
-
if (isEdgeMode()) {
|
|
191
|
-
// Extract domain for Edge URL
|
|
192
|
-
try {
|
|
193
|
-
const url = new URL(baseUrl);
|
|
194
|
-
return `Edge Worker (${url.hostname})`;
|
|
130
|
+
let port = '4001';
|
|
131
|
+
if (typeof import.meta !== 'undefined' && import.meta.env?.VITE_STORY_UI_PORT) {
|
|
132
|
+
port = import.meta.env.VITE_STORY_UI_PORT;
|
|
133
|
+
}
|
|
134
|
+
else if (typeof window !== 'undefined') {
|
|
135
|
+
if (window.__STORY_UI_PORT__) {
|
|
136
|
+
port = window.__STORY_UI_PORT__;
|
|
195
137
|
}
|
|
196
|
-
|
|
197
|
-
|
|
138
|
+
else if (window.STORY_UI_MCP_PORT) {
|
|
139
|
+
port = window.STORY_UI_MCP_PORT;
|
|
198
140
|
}
|
|
199
141
|
}
|
|
200
|
-
return `
|
|
201
|
-
}
|
|
142
|
+
return `http://localhost:${port}`;
|
|
143
|
+
}
|
|
202
144
|
const API_BASE = getApiBaseUrl();
|
|
203
|
-
const MCP_API = `${API_BASE}/
|
|
204
|
-
const MCP_STREAM_API = `${API_BASE}/
|
|
145
|
+
const MCP_API = `${API_BASE}/mcp/generate-story`;
|
|
146
|
+
const MCP_STREAM_API = `${API_BASE}/mcp/generate-story-stream`;
|
|
147
|
+
const PROVIDERS_API = `${API_BASE}/mcp/providers`;
|
|
205
148
|
const STORIES_API = `${API_BASE}/story-ui/stories`;
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const MAX_RECENT_CHATS = 20;
|
|
222
|
-
// Feature flag: Enable streaming mode (can be toggled for testing)
|
|
223
|
-
const USE_STREAMING = true;
|
|
224
|
-
// Load from localStorage
|
|
225
|
-
const loadChats = () => {
|
|
149
|
+
const CONSIDERATIONS_API = `${API_BASE}/mcp/considerations`;
|
|
150
|
+
function isEdgeMode() {
|
|
151
|
+
const baseUrl = getApiBaseUrl();
|
|
152
|
+
return baseUrl.includes('railway.app') || baseUrl.includes('workers.dev');
|
|
153
|
+
}
|
|
154
|
+
function getConnectionDisplayText() {
|
|
155
|
+
const baseUrl = getApiBaseUrl();
|
|
156
|
+
if (baseUrl.includes('railway.app'))
|
|
157
|
+
return 'Railway Cloud';
|
|
158
|
+
if (baseUrl.includes('workers.dev'))
|
|
159
|
+
return 'Cloudflare Edge';
|
|
160
|
+
const port = baseUrl.match(/:(\d+)/)?.[1] || '4001';
|
|
161
|
+
return `localhost:${port}`;
|
|
162
|
+
}
|
|
163
|
+
function loadChats() {
|
|
226
164
|
try {
|
|
227
|
-
const stored = localStorage.getItem(
|
|
228
|
-
if (
|
|
229
|
-
return
|
|
230
|
-
const chats = JSON.parse(stored);
|
|
231
|
-
// Sort by lastUpdated and limit
|
|
232
|
-
return chats
|
|
233
|
-
.sort((a, b) => b.lastUpdated - a.lastUpdated)
|
|
234
|
-
.slice(0, MAX_RECENT_CHATS);
|
|
165
|
+
const stored = localStorage.getItem(CHAT_STORAGE_KEY);
|
|
166
|
+
if (stored)
|
|
167
|
+
return JSON.parse(stored);
|
|
235
168
|
}
|
|
236
169
|
catch (e) {
|
|
237
170
|
console.error('Failed to load chats:', e);
|
|
238
|
-
return [];
|
|
239
171
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
function saveChats(chats) {
|
|
243
175
|
try {
|
|
244
|
-
|
|
245
|
-
const toSave = chats
|
|
246
|
-
.sort((a, b) => b.lastUpdated - a.lastUpdated)
|
|
247
|
-
.slice(0, MAX_RECENT_CHATS);
|
|
248
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
|
|
176
|
+
localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(chats));
|
|
249
177
|
}
|
|
250
178
|
catch (e) {
|
|
251
179
|
console.error('Failed to save chats:', e);
|
|
252
180
|
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const syncWithActualStories = async () => {
|
|
256
|
-
try {
|
|
257
|
-
const response = await fetch(STORIES_API);
|
|
258
|
-
if (!response.ok) {
|
|
259
|
-
console.error('Failed to fetch stories from backend');
|
|
260
|
-
return loadChats();
|
|
261
|
-
}
|
|
262
|
-
// Check if response is JSON
|
|
263
|
-
const contentType = response.headers.get('content-type');
|
|
264
|
-
if (!contentType || !contentType.includes('application/json')) {
|
|
265
|
-
console.error('Server returned non-JSON response, likely server not running or wrong port');
|
|
266
|
-
return loadChats();
|
|
267
|
-
}
|
|
268
|
-
const data = await response.json();
|
|
269
|
-
const memoryStories = data.stories || [];
|
|
270
|
-
// Load existing chats
|
|
271
|
-
const existingChats = loadChats();
|
|
272
|
-
// Create a map for quick lookup - using chat.id as the primary key
|
|
273
|
-
const chatMap = new Map();
|
|
274
|
-
existingChats.forEach(chat => {
|
|
275
|
-
chatMap.set(chat.id, chat);
|
|
276
|
-
});
|
|
277
|
-
// Update or add memory stories
|
|
278
|
-
memoryStories.forEach((story) => {
|
|
279
|
-
const storyId = story.storyId || story.fileName;
|
|
280
|
-
// Look for existing chat by ID or by matching fileName
|
|
281
|
-
let existingChat = chatMap.get(storyId);
|
|
282
|
-
// If not found by ID, search by fileName
|
|
283
|
-
if (!existingChat && story.fileName) {
|
|
284
|
-
for (const [id, chat] of chatMap.entries()) {
|
|
285
|
-
if (chat.fileName === story.fileName) {
|
|
286
|
-
existingChat = chat;
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (existingChat) {
|
|
292
|
-
// Update existing chat with latest info
|
|
293
|
-
existingChat.title = story.title || existingChat.title;
|
|
294
|
-
existingChat.fileName = story.fileName || existingChat.fileName;
|
|
295
|
-
existingChat.lastUpdated = new Date(story.updatedAt || story.createdAt).getTime();
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
// Create new chat from memory story
|
|
299
|
-
const newChat = {
|
|
300
|
-
id: storyId,
|
|
301
|
-
title: story.title || story.fileName,
|
|
302
|
-
fileName: story.fileName,
|
|
303
|
-
conversation: [{
|
|
304
|
-
role: 'user',
|
|
305
|
-
content: story.prompt || `Generate ${story.title}`
|
|
306
|
-
}, {
|
|
307
|
-
role: 'ai',
|
|
308
|
-
content: `[SUCCESS] Created story: "${story.title}"\n\nThis story was recovered from memory. You can continue updating it or view it in Storybook.`
|
|
309
|
-
}],
|
|
310
|
-
lastUpdated: new Date(story.updatedAt || story.createdAt).getTime()
|
|
311
|
-
};
|
|
312
|
-
chatMap.set(storyId, newChat);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
// Convert back to array and save
|
|
316
|
-
const syncedChats = Array.from(chatMap.values());
|
|
317
|
-
saveChats(syncedChats);
|
|
318
|
-
return syncedChats;
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
console.error('Error syncing with backend:', error);
|
|
322
|
-
return loadChats();
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
// Delete story and chat
|
|
326
|
-
const deleteStoryAndChat = async (chatId) => {
|
|
327
|
-
try {
|
|
328
|
-
// Remove .stories.tsx extension if present to get the actual story ID
|
|
329
|
-
const storyId = chatId.replace(/\.stories\.tsx$/, '');
|
|
330
|
-
console.log(`Attempting to delete story: chatId="${chatId}", storyId="${storyId}"`);
|
|
331
|
-
let serverDeleteSucceeded = false;
|
|
332
|
-
// First try to delete from backend
|
|
333
|
-
try {
|
|
334
|
-
const response = await fetch(`${DELETE_API_BASE}/${storyId}`, {
|
|
335
|
-
method: 'DELETE',
|
|
336
|
-
headers: { 'Content-Type': 'application/json' }
|
|
337
|
-
});
|
|
338
|
-
// 404 means story doesn't exist on server - that's OK, we can still clean up localStorage
|
|
339
|
-
if (response.ok || response.status === 404) {
|
|
340
|
-
serverDeleteSucceeded = true;
|
|
341
|
-
if (response.status === 404) {
|
|
342
|
-
console.log('Story not found on server (may have been a failed generation), cleaning up localStorage');
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
console.warn(`Backend delete returned ${response.status}, trying legacy endpoint`);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
catch (fetchError) {
|
|
350
|
-
console.warn('Backend delete request failed, trying legacy endpoint:', fetchError);
|
|
351
|
-
}
|
|
352
|
-
// Try legacy endpoint as fallback only if primary didn't succeed
|
|
353
|
-
if (!serverDeleteSucceeded) {
|
|
354
|
-
try {
|
|
355
|
-
const legacyResponse = await fetch(`${API_BASE}/story-ui/delete`, {
|
|
356
|
-
method: 'POST',
|
|
357
|
-
headers: { 'Content-Type': 'application/json' },
|
|
358
|
-
body: JSON.stringify({
|
|
359
|
-
chatId: storyId,
|
|
360
|
-
storyId: storyId
|
|
361
|
-
})
|
|
362
|
-
});
|
|
363
|
-
// 404 is also OK for legacy endpoint
|
|
364
|
-
if (legacyResponse.ok || legacyResponse.status === 404) {
|
|
365
|
-
serverDeleteSucceeded = true;
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
console.warn('Legacy delete endpoint also returned non-success status');
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
catch (legacyError) {
|
|
372
|
-
console.warn('Legacy delete request failed:', legacyError);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
// Always clean up localStorage - the chat/story data is primarily client-side
|
|
376
|
-
// Even if server delete failed, we should allow users to clean up their chat history
|
|
377
|
-
const chats = loadChats().filter(chat => chat.id !== chatId);
|
|
378
|
-
saveChats(chats);
|
|
379
|
-
console.log('Cleaned up localStorage chat entry');
|
|
380
|
-
return true;
|
|
381
|
-
}
|
|
382
|
-
catch (error) {
|
|
383
|
-
console.error('Error deleting story:', error);
|
|
384
|
-
// Still try to clean up localStorage even on error
|
|
385
|
-
try {
|
|
386
|
-
const chats = loadChats().filter(chat => chat.id !== chatId);
|
|
387
|
-
saveChats(chats);
|
|
388
|
-
console.log('Cleaned up localStorage despite error');
|
|
389
|
-
return true;
|
|
390
|
-
}
|
|
391
|
-
catch (localError) {
|
|
392
|
-
console.error('Failed to clean up localStorage:', localError);
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
};
|
|
397
|
-
// Test connection to MCP server
|
|
398
|
-
const testMCPConnection = async () => {
|
|
181
|
+
}
|
|
182
|
+
async function testMCPConnection() {
|
|
399
183
|
try {
|
|
400
|
-
const response = await fetch(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
if (!response.ok) {
|
|
405
|
-
return { connected: false, error: `HTTP ${response.status}: ${response.statusText}` };
|
|
406
|
-
}
|
|
407
|
-
const contentType = response.headers.get('content-type');
|
|
408
|
-
if (!contentType || !contentType.includes('application/json')) {
|
|
409
|
-
return { connected: false, error: 'Server returned non-JSON response (likely wrong port or server not running)' };
|
|
410
|
-
}
|
|
411
|
-
return { connected: true };
|
|
184
|
+
const response = await fetch(PROVIDERS_API, { method: 'GET' });
|
|
185
|
+
if (response.ok)
|
|
186
|
+
return { connected: true };
|
|
187
|
+
return { connected: false, error: `Server returned ${response.status}` };
|
|
412
188
|
}
|
|
413
|
-
catch (
|
|
414
|
-
return { connected: false, error:
|
|
189
|
+
catch (e) {
|
|
190
|
+
return { connected: false, error: 'Cannot connect to MCP server' };
|
|
415
191
|
}
|
|
416
|
-
}
|
|
417
|
-
//
|
|
418
|
-
|
|
192
|
+
}
|
|
193
|
+
// Simply load chats from localStorage - don't filter based on server state
|
|
194
|
+
// Chats should persist independently of whether story files exist
|
|
195
|
+
async function syncWithActualStories() {
|
|
196
|
+
return loadChats();
|
|
197
|
+
}
|
|
198
|
+
async function fetchOrphanStories() {
|
|
419
199
|
try {
|
|
420
200
|
const response = await fetch(STORIES_API);
|
|
421
|
-
if (!response.ok)
|
|
422
|
-
console.error('Failed to fetch stories from backend for orphan detection');
|
|
201
|
+
if (!response.ok)
|
|
423
202
|
return [];
|
|
424
|
-
}
|
|
425
|
-
const contentType = response.headers.get('content-type');
|
|
426
|
-
if (!contentType || !contentType.includes('application/json')) {
|
|
427
|
-
console.error('Server returned non-JSON response for orphan detection');
|
|
428
|
-
return [];
|
|
429
|
-
}
|
|
430
203
|
const data = await response.json();
|
|
431
204
|
const serverStories = data.stories || [];
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const orphans = [];
|
|
438
|
-
serverStories.forEach((story) => {
|
|
439
|
-
const storyId = story.id || story.storyId || story.fileName;
|
|
440
|
-
const fileName = story.fileName || '';
|
|
441
|
-
// Check if this story has a corresponding chat
|
|
442
|
-
const hasMatchingChat = chatIds.has(storyId) || chatFileNames.has(fileName);
|
|
443
|
-
if (!hasMatchingChat && fileName) {
|
|
444
|
-
orphans.push({
|
|
445
|
-
id: storyId,
|
|
446
|
-
fileName: fileName,
|
|
447
|
-
title: story.title || fileName.replace(/\.stories\.(tsx|ts|jsx|js)$/, ''),
|
|
448
|
-
createdAt: new Date(story.createdAt || Date.now()).getTime(),
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
// Sort by creation date, newest first
|
|
453
|
-
return orphans.sort((a, b) => b.createdAt - a.createdAt);
|
|
205
|
+
const localChats = loadChats();
|
|
206
|
+
const chatIds = new Set(localChats.map(c => c.id));
|
|
207
|
+
return serverStories
|
|
208
|
+
.filter((s) => !chatIds.has(s.id))
|
|
209
|
+
.map((s) => ({ id: s.id, title: s.title, fileName: s.fileName }));
|
|
454
210
|
}
|
|
455
|
-
catch (
|
|
456
|
-
console.error('Error fetching orphan stories:', error);
|
|
211
|
+
catch (e) {
|
|
457
212
|
return [];
|
|
458
213
|
}
|
|
459
|
-
};
|
|
460
|
-
// Component styles
|
|
461
|
-
const STYLES = {
|
|
462
|
-
container: {
|
|
463
|
-
display: 'flex',
|
|
464
|
-
flexDirection: 'row',
|
|
465
|
-
fontFamily: '"IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif',
|
|
466
|
-
height: '100vh',
|
|
467
|
-
overflow: 'hidden',
|
|
468
|
-
background: 'linear-gradient(135deg, #1e293b 0%, #334155 100%)',
|
|
469
|
-
color: '#e2e8f0',
|
|
470
|
-
fontSize: '14px',
|
|
471
|
-
lineHeight: '1.5',
|
|
472
|
-
},
|
|
473
|
-
// Sidebar
|
|
474
|
-
sidebar: {
|
|
475
|
-
width: '240px',
|
|
476
|
-
background: 'rgba(255, 255, 255, 0.03)',
|
|
477
|
-
borderRight: '1px solid rgba(255, 255, 255, 0.08)',
|
|
478
|
-
display: 'flex',
|
|
479
|
-
flexDirection: 'column',
|
|
480
|
-
backdropFilter: 'blur(10px)',
|
|
481
|
-
transition: 'width 0.3s ease',
|
|
482
|
-
position: 'relative',
|
|
483
|
-
},
|
|
484
|
-
sidebarCollapsed: {
|
|
485
|
-
width: '56px',
|
|
486
|
-
},
|
|
487
|
-
sidebarToggle: {
|
|
488
|
-
width: '100%',
|
|
489
|
-
padding: '10px 14px',
|
|
490
|
-
background: 'rgba(59, 130, 246, 0.15)',
|
|
491
|
-
color: '#e2e8f0',
|
|
492
|
-
border: '1px solid rgba(59, 130, 246, 0.3)',
|
|
493
|
-
borderRadius: '8px',
|
|
494
|
-
fontSize: '14px',
|
|
495
|
-
fontWeight: '600',
|
|
496
|
-
cursor: 'pointer',
|
|
497
|
-
marginBottom: '8px',
|
|
498
|
-
transition: 'all 0.2s ease',
|
|
499
|
-
boxShadow: 'none',
|
|
500
|
-
display: 'flex',
|
|
501
|
-
alignItems: 'center',
|
|
502
|
-
justifyContent: 'flex-start',
|
|
503
|
-
gap: '10px',
|
|
504
|
-
lineHeight: '1',
|
|
505
|
-
},
|
|
506
|
-
newChatButton: {
|
|
507
|
-
width: '100%',
|
|
508
|
-
padding: '10px 14px',
|
|
509
|
-
background: '#3b82f6',
|
|
510
|
-
color: 'white',
|
|
511
|
-
border: 'none',
|
|
512
|
-
borderRadius: '8px',
|
|
513
|
-
fontSize: '14px',
|
|
514
|
-
fontWeight: '600',
|
|
515
|
-
cursor: 'pointer',
|
|
516
|
-
marginBottom: '16px',
|
|
517
|
-
transition: 'all 0.2s ease',
|
|
518
|
-
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.25)',
|
|
519
|
-
display: 'flex',
|
|
520
|
-
alignItems: 'center',
|
|
521
|
-
justifyContent: 'flex-start',
|
|
522
|
-
gap: '10px',
|
|
523
|
-
lineHeight: '1',
|
|
524
|
-
},
|
|
525
|
-
chatItem: {
|
|
526
|
-
padding: '8px 12px',
|
|
527
|
-
marginBottom: '4px',
|
|
528
|
-
background: 'rgba(255, 255, 255, 0.05)',
|
|
529
|
-
borderRadius: '6px',
|
|
530
|
-
cursor: 'pointer',
|
|
531
|
-
transition: 'all 0.15s ease',
|
|
532
|
-
position: 'relative',
|
|
533
|
-
paddingRight: '32px',
|
|
534
|
-
},
|
|
535
|
-
chatItemActive: {
|
|
536
|
-
background: 'rgba(59, 130, 246, 0.15)',
|
|
537
|
-
borderLeft: '2px solid #3b82f6',
|
|
538
|
-
},
|
|
539
|
-
chatItemTitle: {
|
|
540
|
-
fontSize: '14px',
|
|
541
|
-
fontWeight: '500',
|
|
542
|
-
marginBottom: '2px',
|
|
543
|
-
whiteSpace: 'nowrap',
|
|
544
|
-
overflow: 'hidden',
|
|
545
|
-
textOverflow: 'ellipsis',
|
|
546
|
-
},
|
|
547
|
-
chatItemTime: {
|
|
548
|
-
fontSize: '12px',
|
|
549
|
-
color: '#94a3b8',
|
|
550
|
-
},
|
|
551
|
-
deleteButton: {
|
|
552
|
-
position: 'absolute',
|
|
553
|
-
right: '8px',
|
|
554
|
-
top: '50%',
|
|
555
|
-
transform: 'translateY(-50%)',
|
|
556
|
-
background: 'rgba(239, 68, 68, 0.8)',
|
|
557
|
-
color: 'white',
|
|
558
|
-
border: 'none',
|
|
559
|
-
borderRadius: '4px',
|
|
560
|
-
padding: '4px 8px',
|
|
561
|
-
fontSize: '12px',
|
|
562
|
-
cursor: 'pointer',
|
|
563
|
-
opacity: 0,
|
|
564
|
-
transition: 'opacity 0.2s ease',
|
|
565
|
-
},
|
|
566
|
-
// Main content
|
|
567
|
-
mainContent: {
|
|
568
|
-
flex: 1,
|
|
569
|
-
display: 'flex',
|
|
570
|
-
flexDirection: 'column',
|
|
571
|
-
overflow: 'hidden',
|
|
572
|
-
},
|
|
573
|
-
chatHeader: {
|
|
574
|
-
padding: '12px 16px',
|
|
575
|
-
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
|
576
|
-
background: 'rgba(255, 255, 255, 0.03)',
|
|
577
|
-
backdropFilter: 'blur(10px)',
|
|
578
|
-
},
|
|
579
|
-
chatContainer: {
|
|
580
|
-
flex: 1,
|
|
581
|
-
padding: '16px',
|
|
582
|
-
overflowY: 'auto',
|
|
583
|
-
scrollBehavior: 'smooth',
|
|
584
|
-
},
|
|
585
|
-
emptyState: {
|
|
586
|
-
color: '#94a3b8',
|
|
587
|
-
textAlign: 'center',
|
|
588
|
-
marginTop: '60px',
|
|
589
|
-
},
|
|
590
|
-
emptyStateTitle: {
|
|
591
|
-
fontSize: '15px',
|
|
592
|
-
fontWeight: '500',
|
|
593
|
-
marginBottom: '8px',
|
|
594
|
-
color: '#cbd5e1',
|
|
595
|
-
},
|
|
596
|
-
emptyStateSubtitle: {
|
|
597
|
-
fontSize: '13px',
|
|
598
|
-
color: '#64748b',
|
|
599
|
-
},
|
|
600
|
-
// Message bubbles
|
|
601
|
-
messageContainer: {
|
|
602
|
-
display: 'flex',
|
|
603
|
-
marginBottom: '8px',
|
|
604
|
-
},
|
|
605
|
-
userMessage: {
|
|
606
|
-
background: 'rgba(59, 130, 246, 0.12)',
|
|
607
|
-
color: '#e2e8f0',
|
|
608
|
-
borderRadius: '16px 16px 4px 16px',
|
|
609
|
-
padding: '10px 14px',
|
|
610
|
-
maxWidth: '85%',
|
|
611
|
-
marginLeft: 'auto',
|
|
612
|
-
fontSize: '14px',
|
|
613
|
-
lineHeight: '1.45',
|
|
614
|
-
boxShadow: 'none',
|
|
615
|
-
wordWrap: 'break-word',
|
|
616
|
-
border: '1px solid rgba(59, 130, 246, 0.2)',
|
|
617
|
-
},
|
|
618
|
-
aiMessage: {
|
|
619
|
-
background: 'rgba(255, 255, 255, 0.95)',
|
|
620
|
-
color: '#1f2937',
|
|
621
|
-
borderRadius: '16px 16px 16px 4px',
|
|
622
|
-
padding: '10px 14px',
|
|
623
|
-
maxWidth: '90%',
|
|
624
|
-
fontSize: '14px',
|
|
625
|
-
lineHeight: '1.45',
|
|
626
|
-
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)',
|
|
627
|
-
border: '1px solid rgba(0, 0, 0, 0.08)',
|
|
628
|
-
wordWrap: 'break-word',
|
|
629
|
-
whiteSpace: 'pre-wrap',
|
|
630
|
-
},
|
|
631
|
-
loadingMessage: {
|
|
632
|
-
background: 'rgba(255, 255, 255, 0.95)',
|
|
633
|
-
color: '#4b5563',
|
|
634
|
-
borderRadius: '16px 16px 16px 4px',
|
|
635
|
-
padding: '10px 14px',
|
|
636
|
-
fontSize: '14px',
|
|
637
|
-
lineHeight: '1.45',
|
|
638
|
-
display: 'flex',
|
|
639
|
-
alignItems: 'center',
|
|
640
|
-
gap: '8px',
|
|
641
|
-
border: '1px solid rgba(0, 0, 0, 0.08)',
|
|
642
|
-
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)',
|
|
643
|
-
},
|
|
644
|
-
// Input form
|
|
645
|
-
inputForm: {
|
|
646
|
-
display: 'flex',
|
|
647
|
-
alignItems: 'center',
|
|
648
|
-
gap: '12px',
|
|
649
|
-
margin: '0 16px 16px 16px',
|
|
650
|
-
padding: '12px',
|
|
651
|
-
background: 'rgba(255, 255, 255, 0.03)',
|
|
652
|
-
borderRadius: '12px',
|
|
653
|
-
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
654
|
-
backdropFilter: 'blur(10px)',
|
|
655
|
-
},
|
|
656
|
-
textInput: {
|
|
657
|
-
font: 'inherit',
|
|
658
|
-
flex: 1,
|
|
659
|
-
padding: '12px 16px',
|
|
660
|
-
borderRadius: '8px',
|
|
661
|
-
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
662
|
-
fontSize: '13px',
|
|
663
|
-
color: '#1f2937',
|
|
664
|
-
background: '#ffffff',
|
|
665
|
-
outline: 'none',
|
|
666
|
-
transition: 'all 0.15s ease',
|
|
667
|
-
boxSizing: 'border-box',
|
|
668
|
-
},
|
|
669
|
-
sendButton: {
|
|
670
|
-
font: 'inherit',
|
|
671
|
-
padding: '10px 16px',
|
|
672
|
-
borderRadius: '10px',
|
|
673
|
-
border: 'none',
|
|
674
|
-
background: '#3b82f6',
|
|
675
|
-
color: 'white',
|
|
676
|
-
fontSize: '14px',
|
|
677
|
-
fontWeight: '600',
|
|
678
|
-
cursor: 'pointer',
|
|
679
|
-
display: 'flex',
|
|
680
|
-
alignItems: 'center',
|
|
681
|
-
gap: '6px',
|
|
682
|
-
transition: 'all 0.2s ease',
|
|
683
|
-
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.35)',
|
|
684
|
-
flexShrink: 0,
|
|
685
|
-
},
|
|
686
|
-
errorMessage: {
|
|
687
|
-
background: 'rgba(248, 113, 113, 0.1)',
|
|
688
|
-
color: '#f87171',
|
|
689
|
-
padding: '8px 12px',
|
|
690
|
-
borderRadius: '6px',
|
|
691
|
-
fontSize: '13px',
|
|
692
|
-
marginBottom: '8px',
|
|
693
|
-
border: '1px solid rgba(248, 113, 113, 0.2)',
|
|
694
|
-
},
|
|
695
|
-
loadingDots: {
|
|
696
|
-
display: 'inline-block',
|
|
697
|
-
animation: 'loadingDots 1.4s infinite',
|
|
698
|
-
},
|
|
699
|
-
'@keyframes loadingDots': {
|
|
700
|
-
'0%': { content: '""' },
|
|
701
|
-
'25%': { content: '"."' },
|
|
702
|
-
'50%': { content: '".."' },
|
|
703
|
-
'75%': { content: '"..."' },
|
|
704
|
-
},
|
|
705
|
-
codeBlock: {
|
|
706
|
-
background: '#1e293b',
|
|
707
|
-
padding: '12px',
|
|
708
|
-
borderRadius: '8px',
|
|
709
|
-
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
|
|
710
|
-
fontSize: '12px',
|
|
711
|
-
lineHeight: '1.5',
|
|
712
|
-
overflowX: 'auto',
|
|
713
|
-
marginTop: '8px',
|
|
714
|
-
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
715
|
-
},
|
|
716
|
-
// Streaming progress styles
|
|
717
|
-
streamingContainer: {
|
|
718
|
-
background: 'rgba(255, 255, 255, 0.95)',
|
|
719
|
-
borderRadius: '16px 16px 16px 4px',
|
|
720
|
-
padding: '10px 14px',
|
|
721
|
-
maxWidth: '90%',
|
|
722
|
-
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)',
|
|
723
|
-
border: '1px solid rgba(0, 0, 0, 0.08)',
|
|
724
|
-
fontSize: '14px',
|
|
725
|
-
lineHeight: '1.45',
|
|
726
|
-
},
|
|
727
|
-
intentPreview: {
|
|
728
|
-
background: 'rgba(59, 130, 246, 0.06)',
|
|
729
|
-
borderRadius: '8px',
|
|
730
|
-
padding: '10px 12px',
|
|
731
|
-
marginBottom: '10px',
|
|
732
|
-
border: '1px solid rgba(59, 130, 246, 0.12)',
|
|
733
|
-
},
|
|
734
|
-
intentTitle: {
|
|
735
|
-
fontSize: '13px',
|
|
736
|
-
fontWeight: '600',
|
|
737
|
-
color: '#1e40af',
|
|
738
|
-
marginBottom: '8px',
|
|
739
|
-
display: 'flex',
|
|
740
|
-
alignItems: 'center',
|
|
741
|
-
gap: '4px',
|
|
742
|
-
},
|
|
743
|
-
intentStrategy: {
|
|
744
|
-
fontSize: '12px',
|
|
745
|
-
color: '#4b5563',
|
|
746
|
-
marginBottom: '4px',
|
|
747
|
-
},
|
|
748
|
-
intentComponents: {
|
|
749
|
-
display: 'flex',
|
|
750
|
-
flexWrap: 'wrap',
|
|
751
|
-
gap: '4px',
|
|
752
|
-
marginTop: '8px',
|
|
753
|
-
},
|
|
754
|
-
componentTag: {
|
|
755
|
-
background: 'rgba(59, 130, 246, 0.12)',
|
|
756
|
-
color: '#1d4ed8',
|
|
757
|
-
fontSize: '11px',
|
|
758
|
-
padding: '2px 8px',
|
|
759
|
-
borderRadius: '10px',
|
|
760
|
-
fontWeight: '500',
|
|
761
|
-
},
|
|
762
|
-
progressBar: {
|
|
763
|
-
background: 'rgba(0, 0, 0, 0.08)',
|
|
764
|
-
borderRadius: '4px',
|
|
765
|
-
height: '4px',
|
|
766
|
-
marginTop: '12px',
|
|
767
|
-
marginBottom: '8px',
|
|
768
|
-
overflow: 'hidden',
|
|
769
|
-
},
|
|
770
|
-
progressFill: {
|
|
771
|
-
background: '#3b82f6',
|
|
772
|
-
height: '100%',
|
|
773
|
-
borderRadius: '3px',
|
|
774
|
-
transition: 'width 0.3s ease',
|
|
775
|
-
},
|
|
776
|
-
progressPhase: {
|
|
777
|
-
fontSize: '14px',
|
|
778
|
-
color: '#4b5563',
|
|
779
|
-
display: 'flex',
|
|
780
|
-
alignItems: 'center',
|
|
781
|
-
gap: '6px',
|
|
782
|
-
fontWeight: '500',
|
|
783
|
-
lineHeight: '1.45',
|
|
784
|
-
},
|
|
785
|
-
phaseIcon: {
|
|
786
|
-
fontSize: '14px',
|
|
787
|
-
},
|
|
788
|
-
validationBox: {
|
|
789
|
-
marginTop: '8px',
|
|
790
|
-
padding: '8px',
|
|
791
|
-
borderRadius: '6px',
|
|
792
|
-
fontSize: '11px',
|
|
793
|
-
},
|
|
794
|
-
validationSuccess: {
|
|
795
|
-
background: 'rgba(16, 185, 129, 0.08)',
|
|
796
|
-
border: '1px solid rgba(16, 185, 129, 0.15)',
|
|
797
|
-
color: '#047857',
|
|
798
|
-
},
|
|
799
|
-
validationWarning: {
|
|
800
|
-
background: 'rgba(245, 158, 11, 0.08)',
|
|
801
|
-
border: '1px solid rgba(245, 158, 11, 0.15)',
|
|
802
|
-
color: '#b45309',
|
|
803
|
-
},
|
|
804
|
-
validationError: {
|
|
805
|
-
background: 'rgba(239, 68, 68, 0.08)',
|
|
806
|
-
border: '1px solid rgba(239, 68, 68, 0.15)',
|
|
807
|
-
color: '#dc2626',
|
|
808
|
-
},
|
|
809
|
-
retryBadge: {
|
|
810
|
-
background: 'rgba(245, 158, 11, 0.12)',
|
|
811
|
-
color: '#b45309',
|
|
812
|
-
fontSize: '11px',
|
|
813
|
-
padding: '2px 8px',
|
|
814
|
-
borderRadius: '10px',
|
|
815
|
-
display: 'inline-flex',
|
|
816
|
-
alignItems: 'center',
|
|
817
|
-
gap: '4px',
|
|
818
|
-
marginTop: '8px',
|
|
819
|
-
},
|
|
820
|
-
completionSummary: {
|
|
821
|
-
marginTop: '10px',
|
|
822
|
-
paddingTop: '10px',
|
|
823
|
-
borderTop: '1px solid rgba(0, 0, 0, 0.06)',
|
|
824
|
-
},
|
|
825
|
-
summaryTitle: {
|
|
826
|
-
fontSize: '14px',
|
|
827
|
-
fontWeight: '600',
|
|
828
|
-
color: '#111827',
|
|
829
|
-
marginBottom: '6px',
|
|
830
|
-
display: 'flex',
|
|
831
|
-
alignItems: 'center',
|
|
832
|
-
gap: '6px',
|
|
833
|
-
lineHeight: '1.45',
|
|
834
|
-
},
|
|
835
|
-
summaryDescription: {
|
|
836
|
-
fontSize: '14px',
|
|
837
|
-
color: '#4b5563',
|
|
838
|
-
lineHeight: '1.45',
|
|
839
|
-
},
|
|
840
|
-
metricsRow: {
|
|
841
|
-
display: 'flex',
|
|
842
|
-
gap: '10px',
|
|
843
|
-
marginTop: '6px',
|
|
844
|
-
fontSize: '13px',
|
|
845
|
-
color: '#6b7280',
|
|
846
|
-
},
|
|
847
|
-
metric: {
|
|
848
|
-
display: 'flex',
|
|
849
|
-
alignItems: 'center',
|
|
850
|
-
gap: '4px',
|
|
851
|
-
},
|
|
852
|
-
// Code viewer styles for generated stories
|
|
853
|
-
codeViewerContainer: {
|
|
854
|
-
marginTop: '12px',
|
|
855
|
-
borderTop: '1px solid rgba(0, 0, 0, 0.08)',
|
|
856
|
-
paddingTop: '12px',
|
|
857
|
-
},
|
|
858
|
-
codeViewerToggle: {
|
|
859
|
-
display: 'flex',
|
|
860
|
-
alignItems: 'center',
|
|
861
|
-
justifyContent: 'space-between',
|
|
862
|
-
padding: '8px 12px',
|
|
863
|
-
background: 'rgba(59, 130, 246, 0.08)',
|
|
864
|
-
borderRadius: '6px',
|
|
865
|
-
cursor: 'pointer',
|
|
866
|
-
border: '1px solid rgba(59, 130, 246, 0.15)',
|
|
867
|
-
transition: 'all 0.2s ease',
|
|
868
|
-
fontSize: '13px',
|
|
869
|
-
fontWeight: '500',
|
|
870
|
-
color: '#1e40af',
|
|
871
|
-
},
|
|
872
|
-
codeViewerToggleHover: {
|
|
873
|
-
background: 'rgba(59, 130, 246, 0.15)',
|
|
874
|
-
},
|
|
875
|
-
codeViewerContent: {
|
|
876
|
-
marginTop: '12px',
|
|
877
|
-
background: '#1e293b',
|
|
878
|
-
borderRadius: '8px',
|
|
879
|
-
overflow: 'hidden',
|
|
880
|
-
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
881
|
-
},
|
|
882
|
-
codeViewerHeader: {
|
|
883
|
-
display: 'flex',
|
|
884
|
-
alignItems: 'center',
|
|
885
|
-
justifyContent: 'space-between',
|
|
886
|
-
padding: '8px 12px',
|
|
887
|
-
background: 'rgba(0, 0, 0, 0.2)',
|
|
888
|
-
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
|
889
|
-
},
|
|
890
|
-
codeViewerFileName: {
|
|
891
|
-
fontSize: '12px',
|
|
892
|
-
color: '#94a3b8',
|
|
893
|
-
fontFamily: 'ui-monospace, monospace',
|
|
894
|
-
},
|
|
895
|
-
copyButton: {
|
|
896
|
-
padding: '4px 12px',
|
|
897
|
-
fontSize: '11px',
|
|
898
|
-
fontWeight: '500',
|
|
899
|
-
color: '#e2e8f0',
|
|
900
|
-
background: 'rgba(59, 130, 246, 0.3)',
|
|
901
|
-
border: '1px solid rgba(59, 130, 246, 0.5)',
|
|
902
|
-
borderRadius: '4px',
|
|
903
|
-
cursor: 'pointer',
|
|
904
|
-
transition: 'all 0.2s ease',
|
|
905
|
-
},
|
|
906
|
-
copyButtonSuccess: {
|
|
907
|
-
background: 'rgba(34, 197, 94, 0.3)',
|
|
908
|
-
borderColor: 'rgba(34, 197, 94, 0.5)',
|
|
909
|
-
color: '#86efac',
|
|
910
|
-
},
|
|
911
|
-
codeViewerPre: {
|
|
912
|
-
margin: 0,
|
|
913
|
-
padding: '12px',
|
|
914
|
-
fontSize: '11px',
|
|
915
|
-
lineHeight: '1.5',
|
|
916
|
-
fontFamily: 'ui-monospace, Consolas, Monaco, monospace',
|
|
917
|
-
color: '#e2e8f0',
|
|
918
|
-
overflowX: 'auto',
|
|
919
|
-
maxHeight: '400px',
|
|
920
|
-
overflowY: 'auto',
|
|
921
|
-
},
|
|
922
|
-
// Image upload styles
|
|
923
|
-
uploadButton: {
|
|
924
|
-
display: 'flex',
|
|
925
|
-
alignItems: 'center',
|
|
926
|
-
justifyContent: 'center',
|
|
927
|
-
width: '36px',
|
|
928
|
-
height: '36px',
|
|
929
|
-
borderRadius: '6px',
|
|
930
|
-
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
931
|
-
background: 'rgba(255, 255, 255, 0.08)',
|
|
932
|
-
color: '#e2e8f0',
|
|
933
|
-
cursor: 'pointer',
|
|
934
|
-
transition: 'all 0.2s ease',
|
|
935
|
-
flexShrink: 0,
|
|
936
|
-
},
|
|
937
|
-
uploadButtonHover: {
|
|
938
|
-
background: 'rgba(59, 130, 246, 0.2)',
|
|
939
|
-
borderColor: 'rgba(59, 130, 246, 0.5)',
|
|
940
|
-
},
|
|
941
|
-
imagePreviewContainer: {
|
|
942
|
-
display: 'flex',
|
|
943
|
-
flexWrap: 'wrap',
|
|
944
|
-
gap: '8px',
|
|
945
|
-
padding: '8px 12px',
|
|
946
|
-
background: 'rgba(255, 255, 255, 0.03)',
|
|
947
|
-
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
|
948
|
-
margin: '0 16px',
|
|
949
|
-
borderRadius: '8px 8px 0 0',
|
|
950
|
-
},
|
|
951
|
-
imagePreviewItem: {
|
|
952
|
-
position: 'relative',
|
|
953
|
-
width: '56px',
|
|
954
|
-
height: '56px',
|
|
955
|
-
borderRadius: '6px',
|
|
956
|
-
overflow: 'hidden',
|
|
957
|
-
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
958
|
-
background: '#1e293b',
|
|
959
|
-
},
|
|
960
|
-
imagePreviewImg: {
|
|
961
|
-
width: '100%',
|
|
962
|
-
height: '100%',
|
|
963
|
-
objectFit: 'cover',
|
|
964
|
-
},
|
|
965
|
-
imageRemoveButton: {
|
|
966
|
-
position: 'absolute',
|
|
967
|
-
top: '2px',
|
|
968
|
-
right: '2px',
|
|
969
|
-
width: '18px',
|
|
970
|
-
height: '18px',
|
|
971
|
-
borderRadius: '50%',
|
|
972
|
-
background: 'rgba(239, 68, 68, 0.9)',
|
|
973
|
-
color: 'white',
|
|
974
|
-
border: 'none',
|
|
975
|
-
fontSize: '12px',
|
|
976
|
-
cursor: 'pointer',
|
|
977
|
-
display: 'flex',
|
|
978
|
-
alignItems: 'center',
|
|
979
|
-
justifyContent: 'center',
|
|
980
|
-
lineHeight: 1,
|
|
981
|
-
},
|
|
982
|
-
imagePreviewLabel: {
|
|
983
|
-
display: 'flex',
|
|
984
|
-
alignItems: 'center',
|
|
985
|
-
gap: '8px',
|
|
986
|
-
fontSize: '12px',
|
|
987
|
-
color: '#94a3b8',
|
|
988
|
-
marginRight: 'auto',
|
|
989
|
-
},
|
|
990
|
-
userMessageImages: {
|
|
991
|
-
display: 'flex',
|
|
992
|
-
gap: '8px',
|
|
993
|
-
marginTop: '8px',
|
|
994
|
-
flexWrap: 'wrap',
|
|
995
|
-
},
|
|
996
|
-
userMessageImage: {
|
|
997
|
-
width: '40px',
|
|
998
|
-
height: '40px',
|
|
999
|
-
borderRadius: '6px',
|
|
1000
|
-
objectFit: 'cover',
|
|
1001
|
-
border: '1px solid rgba(255, 255, 255, 0.25)',
|
|
1002
|
-
},
|
|
1003
|
-
// Drag and drop overlay
|
|
1004
|
-
dropOverlay: {
|
|
1005
|
-
position: 'absolute',
|
|
1006
|
-
top: 0,
|
|
1007
|
-
left: 0,
|
|
1008
|
-
right: 0,
|
|
1009
|
-
bottom: 0,
|
|
1010
|
-
background: 'rgba(59, 130, 246, 0.12)',
|
|
1011
|
-
border: '2px dashed rgba(59, 130, 246, 0.4)',
|
|
1012
|
-
borderRadius: '10px',
|
|
1013
|
-
display: 'flex',
|
|
1014
|
-
alignItems: 'center',
|
|
1015
|
-
justifyContent: 'center',
|
|
1016
|
-
zIndex: 100,
|
|
1017
|
-
backdropFilter: 'blur(3px)',
|
|
1018
|
-
},
|
|
1019
|
-
dropOverlayText: {
|
|
1020
|
-
background: 'rgba(59, 130, 246, 0.85)',
|
|
1021
|
-
color: 'white',
|
|
1022
|
-
padding: '12px 24px',
|
|
1023
|
-
borderRadius: '8px',
|
|
1024
|
-
fontSize: '14px',
|
|
1025
|
-
fontWeight: '500',
|
|
1026
|
-
display: 'flex',
|
|
1027
|
-
alignItems: 'center',
|
|
1028
|
-
gap: '8px',
|
|
1029
|
-
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.3)',
|
|
1030
|
-
},
|
|
1031
|
-
};
|
|
1032
|
-
// Add custom style for loading animation and IBM Plex Sans font
|
|
1033
|
-
// Use a unique ID to prevent duplicate stylesheets during HMR
|
|
1034
|
-
const STYLESHEET_ID = 'story-ui-panel-styles';
|
|
1035
|
-
if (!document.getElementById(STYLESHEET_ID)) {
|
|
1036
|
-
// Load IBM Plex Sans font
|
|
1037
|
-
const fontLink = document.createElement('link');
|
|
1038
|
-
fontLink.rel = 'stylesheet';
|
|
1039
|
-
fontLink.href = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap';
|
|
1040
|
-
document.head.appendChild(fontLink);
|
|
1041
|
-
const styleSheet = document.createElement('style');
|
|
1042
|
-
styleSheet.id = STYLESHEET_ID;
|
|
1043
|
-
styleSheet.textContent = `
|
|
1044
|
-
@keyframes loadingDots {
|
|
1045
|
-
0%, 20% { content: "."; }
|
|
1046
|
-
40% { content: ".."; }
|
|
1047
|
-
60%, 100% { content: "..."; }
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
.loading-dots::after {
|
|
1051
|
-
content: ".";
|
|
1052
|
-
animation: loadingDots 1.4s infinite;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
/* Override Storybook's default styles with !important */
|
|
1056
|
-
.story-ui-panel,
|
|
1057
|
-
.story-ui-panel * {
|
|
1058
|
-
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
.story-ui-panel {
|
|
1062
|
-
font-size: 14px !important;
|
|
1063
|
-
line-height: 1.5 !important;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
/* Message bubbles - consistent styling */
|
|
1067
|
-
.story-ui-message {
|
|
1068
|
-
font-size: 16px !important;
|
|
1069
|
-
line-height: 1.45 !important;
|
|
1070
|
-
padding: 12px 16px !important;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
.story-ui-user-message {
|
|
1074
|
-
background: rgba(59, 130, 246, 0.12) !important;
|
|
1075
|
-
color: #e2e8f0 !important;
|
|
1076
|
-
border-radius: 18px 18px 4px 18px !important;
|
|
1077
|
-
border: 1px solid rgba(59, 130, 246, 0.2) !important;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
.story-ui-ai-message {
|
|
1081
|
-
background: rgba(255, 255, 255, 0.97) !important;
|
|
1082
|
-
color: #1f2937 !important;
|
|
1083
|
-
border-radius: 18px 18px 18px 4px !important;
|
|
1084
|
-
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
|
1085
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
/* Override nested elements in AI messages (from renderMarkdown)
|
|
1089
|
-
.story-ui-ai-message p,
|
|
1090
|
-
.story-ui-ai-message span,
|
|
1091
|
-
.story-ui-ai-message strong,
|
|
1092
|
-
.story-ui-ai-message em,
|
|
1093
|
-
.story-ui-ai-message li,
|
|
1094
|
-
.story-ui-ai-message ul,
|
|
1095
|
-
.story-ui-ai-message ol {
|
|
1096
|
-
font-size: 14px !important;
|
|
1097
|
-
line-height: 1.45 !important;
|
|
1098
|
-
margin: 0 !important;
|
|
1099
|
-
} */
|
|
1100
|
-
|
|
1101
|
-
.story-ui-ai-message p + p {
|
|
1102
|
-
margin-top: 8px !important;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
/* Status text */
|
|
1106
|
-
.story-ui-status {
|
|
1107
|
-
font-size: 13px !important;
|
|
1108
|
-
font-weight: 400 !important;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
.story-ui-status-connected {
|
|
1112
|
-
color: #10b981 !important;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
.story-ui-status-disconnected {
|
|
1116
|
-
color: #ef4444 !important;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
/* Sidebar buttons */
|
|
1120
|
-
.story-ui-sidebar button {
|
|
1121
|
-
font-size: 14px !important;
|
|
1122
|
-
font-weight: 600 !important;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
/* Header text */
|
|
1126
|
-
.story-ui-header h1 {
|
|
1127
|
-
font-size: 24px !important;
|
|
1128
|
-
font-weight: 700 !important;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
.story-ui-header p {
|
|
1132
|
-
font-size: 14px !important;
|
|
1133
|
-
color: #94a3b8 !important;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/* Input field */
|
|
1137
|
-
.story-ui-input {
|
|
1138
|
-
font-size: 14px !important;
|
|
1139
|
-
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
/* Code blocks in messages */
|
|
1143
|
-
.story-ui-ai-message code {
|
|
1144
|
-
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace !important;
|
|
1145
|
-
font-size: 13px !important;
|
|
1146
|
-
background: rgba(0, 0, 0, 0.06) !important;
|
|
1147
|
-
padding: 2px 6px !important;
|
|
1148
|
-
border-radius: 4px !important;
|
|
1149
|
-
}
|
|
1150
|
-
`;
|
|
1151
|
-
document.head.appendChild(styleSheet);
|
|
1152
214
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
215
|
+
async function deleteStoryAndChat(chatId) {
|
|
216
|
+
try {
|
|
217
|
+
const response = await fetch(`${STORIES_API}/${chatId}`, { method: 'DELETE' });
|
|
218
|
+
// Delete chat from localStorage if:
|
|
219
|
+
// - Story was successfully deleted (200/204)
|
|
220
|
+
// - Story doesn't exist (404) - orphan chat case
|
|
221
|
+
if (response.ok || response.status === 404) {
|
|
222
|
+
const chats = loadChats().filter(c => c.id !== chatId);
|
|
223
|
+
saveChats(chats);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
1158
227
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
228
|
+
catch (e) {
|
|
229
|
+
// On network error, still allow removing the chat from localStorage
|
|
230
|
+
// since the story file may not exist anyway
|
|
231
|
+
const chats = loadChats().filter(c => c.id !== chatId);
|
|
232
|
+
saveChats(chats);
|
|
233
|
+
return true;
|
|
1163
234
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
const
|
|
1167
|
-
const
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
if (
|
|
1172
|
-
return
|
|
1173
|
-
if (
|
|
1174
|
-
return `${
|
|
1175
|
-
if (
|
|
1176
|
-
return `${
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
235
|
+
}
|
|
236
|
+
function formatTime(timestamp) {
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
const diff = now - timestamp;
|
|
239
|
+
const minutes = Math.floor(diff / 60000);
|
|
240
|
+
const hours = Math.floor(diff / 3600000);
|
|
241
|
+
const days = Math.floor(diff / 86400000);
|
|
242
|
+
if (minutes < 1)
|
|
243
|
+
return 'Just now';
|
|
244
|
+
if (minutes < 60)
|
|
245
|
+
return `${minutes}m ago`;
|
|
246
|
+
if (hours < 24)
|
|
247
|
+
return `${hours}h ago`;
|
|
248
|
+
if (days < 7)
|
|
249
|
+
return `${days}d ago`;
|
|
250
|
+
return new Date(timestamp).toLocaleDateString();
|
|
251
|
+
}
|
|
252
|
+
function getModelDisplayName(model) {
|
|
253
|
+
const displayNames = {
|
|
254
|
+
'claude-opus-4-5-20251101': 'Claude Opus 4.5',
|
|
255
|
+
'claude-sonnet-4-5-20250929': 'Claude Sonnet 4.5',
|
|
256
|
+
'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
|
|
257
|
+
'gpt-4o': 'GPT-4o',
|
|
258
|
+
'gpt-4o-mini': 'GPT-4o Mini',
|
|
259
|
+
'o1': 'o1',
|
|
260
|
+
'gemini-2.0-flash': 'Gemini 2.0 Flash',
|
|
261
|
+
'gemini-1.5-pro': 'Gemini 1.5 Pro',
|
|
1190
262
|
};
|
|
1191
|
-
return
|
|
263
|
+
return displayNames[model] || model;
|
|
264
|
+
}
|
|
265
|
+
// ============================================
|
|
266
|
+
// Icons (Lucide-style SVG)
|
|
267
|
+
// ============================================
|
|
268
|
+
const Icons = {
|
|
269
|
+
plus: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M5 12h14" }), _jsx("path", { d: "M12 5v14" })] })),
|
|
270
|
+
messageSquare: (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) })),
|
|
271
|
+
panelLeft: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }), _jsx("path", { d: "M9 3v18" })] })),
|
|
272
|
+
x: (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M18 6 6 18" }), _jsx("path", { d: "m6 6 12 12" })] })),
|
|
273
|
+
image: (_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }), _jsx("circle", { cx: "9", cy: "9", r: "2" }), _jsx("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" })] })),
|
|
274
|
+
send: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), _jsx("path", { d: "M22 2 11 13" })] })),
|
|
275
|
+
chevronDown: (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m6 9 6 6 6-6" }) })),
|
|
276
|
+
trash: (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M3 6h18" }), _jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }), _jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })] })),
|
|
277
|
+
sparkles: (_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" }), _jsx("path", { d: "M5 3v4" }), _jsx("path", { d: "M19 17v4" }), _jsx("path", { d: "M3 5h4" }), _jsx("path", { d: "M17 19h4" })] })),
|
|
278
|
+
moreVertical: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("circle", { cx: "12", cy: "12", r: "1" }), _jsx("circle", { cx: "12", cy: "5", r: "1" }), _jsx("circle", { cx: "12", cy: "19", r: "1" })] })),
|
|
279
|
+
pencil: (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }), _jsx("path", { d: "m15 5 4 4" })] })),
|
|
280
|
+
check: (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) })),
|
|
281
|
+
checkCircle: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }), _jsx("path", { d: "M22 4 12 14.01l-3-3" })] })),
|
|
282
|
+
xCircle: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "m15 9-6 6" }), _jsx("path", { d: "m9 9 6 6" })] })),
|
|
283
|
+
lightbulb: (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5" }), _jsx("path", { d: "M9 18h6" }), _jsx("path", { d: "M10 22h4" })] })),
|
|
284
|
+
wrench: (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", 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" }) })),
|
|
1192
285
|
};
|
|
1193
|
-
//
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
286
|
+
// ============================================
|
|
287
|
+
// Markdown Renderer
|
|
288
|
+
// ============================================
|
|
289
|
+
function renderMarkdown(content) {
|
|
290
|
+
const elements = [];
|
|
291
|
+
let key = 0;
|
|
292
|
+
// Split content into blocks (paragraphs, lists, headings)
|
|
293
|
+
const blocks = content.split(/\n\n+/);
|
|
294
|
+
blocks.forEach(block => {
|
|
295
|
+
if (!block.trim())
|
|
296
|
+
return;
|
|
297
|
+
// Check for headings (# ## ###)
|
|
298
|
+
const headingMatch = block.match(/^(#{1,6})\s+(.+)$/);
|
|
299
|
+
if (headingMatch) {
|
|
300
|
+
const level = headingMatch[1].length;
|
|
301
|
+
const text = headingMatch[2];
|
|
302
|
+
const inlineContent = parseInline(text);
|
|
303
|
+
switch (level) {
|
|
304
|
+
case 1:
|
|
305
|
+
elements.push(_jsx("h1", { children: inlineContent }, key++));
|
|
306
|
+
break;
|
|
307
|
+
case 2:
|
|
308
|
+
elements.push(_jsx("h2", { children: inlineContent }, key++));
|
|
309
|
+
break;
|
|
310
|
+
case 3:
|
|
311
|
+
elements.push(_jsx("h3", { children: inlineContent }, key++));
|
|
312
|
+
break;
|
|
313
|
+
case 4:
|
|
314
|
+
elements.push(_jsx("h4", { children: inlineContent }, key++));
|
|
315
|
+
break;
|
|
316
|
+
case 5:
|
|
317
|
+
elements.push(_jsx("h5", { children: inlineContent }, key++));
|
|
318
|
+
break;
|
|
319
|
+
case 6:
|
|
320
|
+
elements.push(_jsx("h6", { children: inlineContent }, key++));
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
1204
324
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
325
|
+
// Check for ordered lists (1. 2. 3.)
|
|
326
|
+
const orderedListMatch = block.match(/^(\d+\.\s+.+)$/m);
|
|
327
|
+
if (orderedListMatch) {
|
|
328
|
+
const items = block.split('\n').filter(line => /^\d+\.\s+/.test(line));
|
|
329
|
+
const listItems = items.map((item, i) => {
|
|
330
|
+
const text = item.replace(/^\d+\.\s+/, '');
|
|
331
|
+
return _jsx("li", { children: parseInline(text) }, i);
|
|
332
|
+
});
|
|
333
|
+
elements.push(_jsx("ol", { children: listItems }, key++));
|
|
334
|
+
return;
|
|
1207
335
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
336
|
+
// Check for unordered lists (- or *)
|
|
337
|
+
const unorderedListMatch = block.match(/^[-*]\s+.+$/m);
|
|
338
|
+
if (unorderedListMatch) {
|
|
339
|
+
const items = block.split('\n').filter(line => /^[-*]\s+/.test(line));
|
|
340
|
+
const listItems = items.map((item, i) => {
|
|
341
|
+
const text = item.replace(/^[-*]\s+/, '');
|
|
342
|
+
return _jsx("li", { children: parseInline(text) }, i);
|
|
343
|
+
});
|
|
344
|
+
elements.push(_jsx("ul", { children: listItems }, key++));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
// Regular paragraph with line breaks preserved
|
|
348
|
+
const lines = block.split('\n');
|
|
349
|
+
const paragraphElements = lines.map((line, i) => (_jsxs(React.Fragment, { children: [parseInline(line), i < lines.length - 1 && _jsx("br", {})] }, i)));
|
|
350
|
+
elements.push(_jsx("p", { children: paragraphElements }, key++));
|
|
351
|
+
});
|
|
352
|
+
return _jsx("div", { className: "sui-markdown", children: elements });
|
|
353
|
+
}
|
|
354
|
+
// Parse inline markdown elements and status icons
|
|
355
|
+
function parseInline(text) {
|
|
356
|
+
const parts = [];
|
|
357
|
+
let remaining = text;
|
|
358
|
+
// Replace status markers with icon components
|
|
359
|
+
// Use {{ICON:n}} format to avoid conflict with markdown underscore patterns
|
|
360
|
+
const iconReplacements = [
|
|
361
|
+
{ pattern: /\[SUCCESS\]/g, index: 0, icon: _jsx("span", { className: "sui-icon-inline sui-icon-success", "aria-label": "Success", children: Icons.checkCircle }, "icon-0") },
|
|
362
|
+
{ pattern: /\[ERROR\]/g, index: 1, icon: _jsx("span", { className: "sui-icon-inline sui-icon-error", "aria-label": "Error", children: Icons.xCircle }, "icon-1") },
|
|
363
|
+
{ pattern: /\[TIP\]/g, index: 2, icon: _jsx("span", { className: "sui-icon-inline sui-icon-tip", "aria-label": "Tip", children: Icons.lightbulb }, "icon-2") },
|
|
364
|
+
{ pattern: /\[WRENCH\]/g, index: 3, icon: _jsx("span", { className: "sui-icon-inline sui-icon-wrench", "aria-label": "Auto-fixed", children: Icons.wrench }, "icon-3") },
|
|
365
|
+
];
|
|
366
|
+
iconReplacements.forEach(({ pattern, index, icon }) => {
|
|
367
|
+
remaining = remaining.replace(pattern, `{{ICON:${index}}}`);
|
|
368
|
+
parts[index] = icon;
|
|
369
|
+
});
|
|
370
|
+
// Parse bold, code, italic, and icon placeholders
|
|
371
|
+
// Icon placeholder {{ICON:n}} uses curly braces to avoid markdown conflicts
|
|
372
|
+
const regex = /(\*\*[^*]+\*\*|`[^`]+`|_[^_]+_|\{\{ICON:\d+\}\})/g;
|
|
373
|
+
const tokens = remaining.split(regex);
|
|
374
|
+
return tokens.map((token, i) => {
|
|
375
|
+
if (token.startsWith('**') && token.endsWith('**')) {
|
|
376
|
+
return _jsx("strong", { children: token.slice(2, -2) }, `inline-${i}`);
|
|
377
|
+
}
|
|
378
|
+
if (token.startsWith('`') && token.endsWith('`')) {
|
|
379
|
+
return _jsx("code", { children: token.slice(1, -1) }, `inline-${i}`);
|
|
380
|
+
}
|
|
381
|
+
if (token.startsWith('_') && token.endsWith('_') && !token.startsWith('{{')) {
|
|
382
|
+
return _jsx("em", { children: token.slice(1, -1) }, `inline-${i}`);
|
|
383
|
+
}
|
|
384
|
+
if (token.startsWith('{{ICON:')) {
|
|
385
|
+
const iconIndex = parseInt(token.match(/\{\{ICON:(\d+)\}\}/)?.[1] || '0');
|
|
386
|
+
return parts[iconIndex] || token;
|
|
387
|
+
}
|
|
388
|
+
return token;
|
|
389
|
+
}).filter(Boolean);
|
|
390
|
+
}
|
|
391
|
+
const Badge = ({ variant = 'default', children, className = '' }) => (_jsx("span", { className: `sui-badge sui-badge-${variant} ${className}`, children: children }));
|
|
392
|
+
const ProgressIndicator = ({ streamingState }) => {
|
|
393
|
+
const { progress, retry, completion, error } = streamingState;
|
|
1217
394
|
if (error) {
|
|
1218
|
-
return (
|
|
395
|
+
return (_jsxs("div", { className: "sui-error", role: "alert", children: [_jsx("strong", { children: error.message }), error.details && _jsx("div", { children: error.details }), error.suggestion && _jsx("div", { children: error.suggestion })] }));
|
|
396
|
+
}
|
|
397
|
+
if (completion) {
|
|
398
|
+
return (_jsxs("div", { className: "sui-completion", children: [_jsxs("div", { className: "sui-completion-header", children: [_jsx("span", { children: completion.success ? '\u2705' : '\u274C' }), _jsxs("span", { children: [completion.summary.action, ": ", completion.title] })] }), completion.componentsUsed.length > 0 && (_jsx("div", { className: "sui-completion-components", children: completion.componentsUsed.map((comp, i) => (_jsx("span", { className: "sui-completion-tag", children: comp.name }, i))) })), completion.metrics && (_jsxs("div", { className: "sui-completion-metrics", children: [_jsxs("span", { children: [(completion.metrics.totalTimeMs / 1000).toFixed(1), "s"] }), _jsxs("span", { children: [completion.metrics.llmCallsCount, " LLM calls"] })] }))] }));
|
|
1219
399
|
}
|
|
1220
|
-
|
|
1221
|
-
return (_jsxs("div", { style: STYLES.streamingContainer, children: [_jsxs("div", { style: STYLES.intentPreview, children: [_jsxs("div", { style: STYLES.progressPhase, children: [_jsx("span", { children: "Generating story..." }), progress && (_jsxs("span", { style: { marginLeft: 'auto', color: '#9ca3af' }, children: [progress.step, "/", progress.totalSteps] }))] }), progress && (_jsx("div", { style: { ...STYLES.progressBar, marginTop: '8px' }, children: _jsx("div", { style: {
|
|
1222
|
-
...STYLES.progressFill,
|
|
1223
|
-
width: `${(progress.step / progress.totalSteps) * 100}%`
|
|
1224
|
-
} }) }))] }), retry && (_jsxs("div", { style: STYLES.retryBadge, children: ["Retry ", retry.attempt, "/", retry.maxAttempts, ": ", retry.reason] })), !progress && !intent && (_jsx("div", { style: STYLES.progressPhase, children: _jsx("span", { className: "loading-dots", children: "Connecting" }) }))] }));
|
|
400
|
+
return (_jsxs("div", { className: "sui-progress", role: "progressbar", "aria-valuenow": progress?.step, "aria-valuemax": progress?.totalSteps, children: [_jsxs("div", { className: "sui-progress-header", children: [_jsx("span", { className: "sui-progress-label", children: progress?.message || 'Generating story...' }), progress && _jsxs("span", { className: "sui-progress-step", children: [progress.step, "/", progress.totalSteps] })] }), progress && (_jsx("div", { className: "sui-progress-bar", children: _jsx("div", { className: "sui-progress-fill", style: { width: `${(progress.step / progress.totalSteps) * 100}%` } }) })), retry && _jsxs("div", { className: "sui-progress-retry", children: ["Retry ", retry.attempt, "/", retry.maxAttempts, ": ", retry.reason] })] }));
|
|
1225
401
|
};
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
const [
|
|
1229
|
-
const [
|
|
1230
|
-
const [
|
|
1231
|
-
const [error, setError] = useState(null);
|
|
1232
|
-
const [recentChats, setRecentChats] = useState([]);
|
|
1233
|
-
const [activeChatId, setActiveChatId] = useState(null);
|
|
1234
|
-
const [activeTitle, setActiveTitle] = useState('');
|
|
1235
|
-
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
1236
|
-
const [connectionStatus, setConnectionStatus] = useState({ connected: false });
|
|
1237
|
-
const [availableProviders, setAvailableProviders] = useState([]);
|
|
1238
|
-
const [selectedProvider, setSelectedProvider] = useState('');
|
|
1239
|
-
const [selectedModel, setSelectedModel] = useState('');
|
|
1240
|
-
const [streamingState, setStreamingState] = useState(null);
|
|
1241
|
-
const [attachedImages, setAttachedImages] = useState([]);
|
|
1242
|
-
const [considerations, setConsiderations] = useState('');
|
|
1243
|
-
const [orphanStories, setOrphanStories] = useState([]);
|
|
1244
|
-
const [selectedStoryIds, setSelectedStoryIds] = useState(new Set());
|
|
1245
|
-
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
|
|
402
|
+
function StoryUIPanel({ mcpPort }) {
|
|
403
|
+
const [state, dispatch] = useReducer(panelReducer, initialState);
|
|
404
|
+
const [contextMenuId, setContextMenuId] = useState(null);
|
|
405
|
+
const [renamingChatId, setRenamingChatId] = useState(null);
|
|
406
|
+
const [renameValue, setRenameValue] = useState('');
|
|
1246
407
|
const chatEndRef = useRef(null);
|
|
1247
408
|
const inputRef = useRef(null);
|
|
1248
409
|
const fileInputRef = useRef(null);
|
|
1249
410
|
const abortControllerRef = useRef(null);
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
411
|
+
const hasShownRefreshHint = useRef(false);
|
|
412
|
+
// Set port override if provided
|
|
413
|
+
useEffect(() => {
|
|
414
|
+
if (mcpPort && typeof window !== 'undefined') {
|
|
415
|
+
window.STORY_UI_MCP_PORT = String(mcpPort);
|
|
416
|
+
}
|
|
417
|
+
}, [mcpPort]);
|
|
418
|
+
// Detect Storybook theme
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
const detectTheme = () => {
|
|
421
|
+
const body = document.body;
|
|
422
|
+
const html = document.documentElement;
|
|
423
|
+
// Check URL parameters for Storybook background setting
|
|
424
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
425
|
+
const globals = urlParams.get('globals') || '';
|
|
426
|
+
const hasStorybookLightBg = globals.includes('backgrounds.value:light');
|
|
427
|
+
const hasStorybookDarkBg = globals.includes('backgrounds.value:dark') ||
|
|
428
|
+
globals.includes('backgrounds.value:%23') || // Hex colors starting with #
|
|
429
|
+
globals.includes('backgrounds.value:!hex');
|
|
430
|
+
// Check parent frame URL if we're in an iframe (Storybook 8+)
|
|
431
|
+
let parentHasDarkBg = false;
|
|
432
|
+
let parentHasLightBg = false;
|
|
433
|
+
let parentHasDarkClass = false;
|
|
434
|
+
try {
|
|
435
|
+
if (window.parent !== window) {
|
|
436
|
+
const parentUrl = new URL(window.parent.location.href);
|
|
437
|
+
const parentGlobals = parentUrl.searchParams.get('globals') || '';
|
|
438
|
+
parentHasLightBg = parentGlobals.includes('backgrounds.value:light');
|
|
439
|
+
parentHasDarkBg = parentGlobals.includes('backgrounds.value:dark') ||
|
|
440
|
+
parentGlobals.includes('backgrounds.value:%23');
|
|
441
|
+
// Check parent document for Storybook dark theme classes
|
|
442
|
+
const parentBody = window.parent.document.body;
|
|
443
|
+
const parentHtml = window.parent.document.documentElement;
|
|
444
|
+
parentHasDarkClass = parentBody.classList.contains('sb-dark') ||
|
|
445
|
+
parentHtml.classList.contains('dark') ||
|
|
446
|
+
parentHtml.getAttribute('data-theme') === 'dark' ||
|
|
447
|
+
parentBody.getAttribute('data-theme') === 'dark';
|
|
448
|
+
// Check Storybook 8+ manager theme
|
|
449
|
+
const sbMainEl = window.parent.document.querySelector('.sb-main-padded, .sb-show-main');
|
|
450
|
+
if (sbMainEl) {
|
|
451
|
+
const sbBgColor = window.getComputedStyle(sbMainEl).backgroundColor;
|
|
452
|
+
const sbRgb = sbBgColor.match(/\d+/g);
|
|
453
|
+
if (sbRgb && sbRgb.length >= 3) {
|
|
454
|
+
const sbLuminance = (0.299 * parseInt(sbRgb[0]) + 0.587 * parseInt(sbRgb[1]) + 0.114 * parseInt(sbRgb[2])) / 255;
|
|
455
|
+
if (sbLuminance < 0.5)
|
|
456
|
+
parentHasDarkClass = true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// Cross-origin access not allowed, ignore
|
|
463
|
+
}
|
|
464
|
+
// Check the actual background color of the body
|
|
465
|
+
const bgColor = window.getComputedStyle(body).backgroundColor;
|
|
466
|
+
const rgb = bgColor.match(/\d+/g);
|
|
467
|
+
let isBackgroundDark = false;
|
|
468
|
+
if (rgb && rgb.length >= 3) {
|
|
469
|
+
const luminance = (0.299 * parseInt(rgb[0]) + 0.587 * parseInt(rgb[1]) + 0.114 * parseInt(rgb[2])) / 255;
|
|
470
|
+
isBackgroundDark = luminance < 0.5;
|
|
471
|
+
}
|
|
472
|
+
// Check system preference as fallback
|
|
473
|
+
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
474
|
+
// Explicit light mode takes precedence - if user selected "light" in Storybook, respect that
|
|
475
|
+
const hasExplicitLightMode = hasStorybookLightBg || parentHasLightBg;
|
|
476
|
+
// Explicit dark mode indicators
|
|
477
|
+
const hasExplicitDarkMode = body.classList.contains('sb-dark') ||
|
|
478
|
+
html.classList.contains('dark') ||
|
|
479
|
+
html.getAttribute('data-theme') === 'dark' ||
|
|
480
|
+
body.getAttribute('data-theme') === 'dark' ||
|
|
481
|
+
hasStorybookDarkBg ||
|
|
482
|
+
parentHasDarkBg;
|
|
483
|
+
// Determine dark mode: explicit light mode forces light, otherwise check dark indicators
|
|
484
|
+
const isDark = hasExplicitLightMode
|
|
485
|
+
? false
|
|
486
|
+
: (hasExplicitDarkMode || parentHasDarkClass || isBackgroundDark || systemPrefersDark);
|
|
487
|
+
dispatch({ type: 'SET_DARK_MODE', payload: isDark });
|
|
488
|
+
};
|
|
489
|
+
detectTheme();
|
|
490
|
+
// Listen for URL changes (Storybook uses popstate for navigation)
|
|
491
|
+
window.addEventListener('popstate', detectTheme);
|
|
492
|
+
// Poll for changes since Storybook might change background without popstate
|
|
493
|
+
const intervalId = setInterval(detectTheme, 500);
|
|
494
|
+
const observer = new MutationObserver(detectTheme);
|
|
495
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme', 'style'] });
|
|
496
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme', 'style'] });
|
|
497
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
498
|
+
mediaQuery.addEventListener('change', detectTheme);
|
|
499
|
+
return () => {
|
|
500
|
+
window.removeEventListener('popstate', detectTheme);
|
|
501
|
+
clearInterval(intervalId);
|
|
502
|
+
observer.disconnect();
|
|
503
|
+
mediaQuery.removeEventListener('change', detectTheme);
|
|
504
|
+
};
|
|
505
|
+
}, []);
|
|
506
|
+
// Close context menu when clicking outside
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
if (!contextMenuId)
|
|
509
|
+
return;
|
|
510
|
+
const handleClickOutside = (e) => {
|
|
511
|
+
const target = e.target;
|
|
512
|
+
if (!target.closest('.sui-context-menu') && !target.closest('.sui-chat-item-menu')) {
|
|
513
|
+
setContextMenuId(null);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
document.addEventListener('click', handleClickOutside);
|
|
517
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
518
|
+
}, [contextMenuId]);
|
|
519
|
+
// Initialize on mount
|
|
520
|
+
useEffect(() => {
|
|
521
|
+
const initialize = async () => {
|
|
522
|
+
const connectionTest = await testMCPConnection();
|
|
523
|
+
dispatch({ type: 'SET_CONNECTION_STATUS', payload: connectionTest });
|
|
524
|
+
if (connectionTest.connected) {
|
|
525
|
+
try {
|
|
526
|
+
const res = await fetch(PROVIDERS_API);
|
|
527
|
+
if (res.ok) {
|
|
528
|
+
const data = await res.json();
|
|
529
|
+
dispatch({ type: 'SET_PROVIDERS', payload: data.providers.filter(p => p.configured) });
|
|
530
|
+
if (data.current) {
|
|
531
|
+
dispatch({ type: 'SET_SELECTED_PROVIDER', payload: data.current.provider.toLowerCase() });
|
|
532
|
+
dispatch({ type: 'SET_SELECTED_MODEL', payload: data.current.model });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch (e) {
|
|
537
|
+
console.error('Failed to fetch providers:', e);
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const res = await fetch(CONSIDERATIONS_API);
|
|
541
|
+
if (res.ok) {
|
|
542
|
+
const data = await res.json();
|
|
543
|
+
if (data.hasConsiderations && data.considerations) {
|
|
544
|
+
dispatch({ type: 'SET_CONSIDERATIONS', payload: data.considerations });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
console.error('Failed to fetch considerations:', e);
|
|
550
|
+
}
|
|
551
|
+
const syncedChats = await syncWithActualStories();
|
|
552
|
+
const sortedChats = syncedChats.sort((a, b) => b.lastUpdated - a.lastUpdated).slice(0, MAX_RECENT_CHATS);
|
|
553
|
+
dispatch({ type: 'SET_RECENT_CHATS', payload: sortedChats });
|
|
554
|
+
if (sortedChats.length > 0) {
|
|
555
|
+
dispatch({ type: 'SET_CONVERSATION', payload: sortedChats[0].conversation });
|
|
556
|
+
dispatch({ type: 'SET_ACTIVE_CHAT', payload: { id: sortedChats[0].id, title: sortedChats[0].title } });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
const localChats = loadChats();
|
|
561
|
+
dispatch({ type: 'SET_RECENT_CHATS', payload: localChats });
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
initialize();
|
|
565
|
+
}, []);
|
|
566
|
+
// Scroll to bottom on new messages
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
569
|
+
}, [state.conversation, state.loading]);
|
|
570
|
+
// File handling
|
|
1254
571
|
const fileToBase64 = (file) => {
|
|
1255
572
|
return new Promise((resolve, reject) => {
|
|
1256
573
|
const reader = new FileReader();
|
|
1257
574
|
reader.readAsDataURL(file);
|
|
1258
575
|
reader.onload = () => {
|
|
1259
576
|
const result = reader.result;
|
|
1260
|
-
|
|
1261
|
-
const base64 = result.split(',')[1];
|
|
1262
|
-
resolve(base64);
|
|
577
|
+
resolve(result.split(',')[1]);
|
|
1263
578
|
};
|
|
1264
579
|
reader.onerror = error => reject(error);
|
|
1265
580
|
});
|
|
1266
581
|
};
|
|
1267
|
-
// Handle file selection
|
|
1268
582
|
const handleFileSelect = async (e) => {
|
|
1269
583
|
const files = e.target.files;
|
|
1270
584
|
if (!files)
|
|
1271
585
|
return;
|
|
1272
|
-
const newImages = [];
|
|
1273
586
|
const errors = [];
|
|
1274
|
-
for (let i = 0; i < files.length && (attachedImages.length +
|
|
587
|
+
for (let i = 0; i < files.length && (state.attachedImages.length + i) < MAX_IMAGES; i++) {
|
|
1275
588
|
const file = files[i];
|
|
1276
|
-
// Validate file type
|
|
1277
589
|
if (!file.type.startsWith('image/')) {
|
|
1278
590
|
errors.push(`${file.name}: Not an image file`);
|
|
1279
591
|
continue;
|
|
1280
592
|
}
|
|
1281
|
-
// Validate file size
|
|
1282
593
|
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1283
594
|
errors.push(`${file.name}: File too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
|
|
1284
595
|
continue;
|
|
@@ -1286,59 +597,42 @@ function StoryUIPanel() {
|
|
|
1286
597
|
try {
|
|
1287
598
|
const base64 = await fileToBase64(file);
|
|
1288
599
|
const preview = URL.createObjectURL(file);
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
file,
|
|
1292
|
-
preview,
|
|
1293
|
-
base64,
|
|
1294
|
-
mediaType: file.type || 'image/png',
|
|
600
|
+
dispatch({
|
|
601
|
+
type: 'ADD_ATTACHED_IMAGE',
|
|
602
|
+
payload: { id: `${Date.now()}-${i}`, file, preview, base64, mediaType: file.type || 'image/png' },
|
|
1295
603
|
});
|
|
1296
604
|
}
|
|
1297
|
-
catch
|
|
605
|
+
catch {
|
|
1298
606
|
errors.push(`${file.name}: Failed to process`);
|
|
1299
607
|
}
|
|
1300
608
|
}
|
|
1301
|
-
if (errors.length > 0)
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
setAttachedImages(prev => [...prev, ...newImages]);
|
|
1305
|
-
// Reset file input
|
|
1306
|
-
if (fileInputRef.current) {
|
|
609
|
+
if (errors.length > 0)
|
|
610
|
+
dispatch({ type: 'SET_ERROR', payload: errors.join('\n') });
|
|
611
|
+
if (fileInputRef.current)
|
|
1307
612
|
fileInputRef.current.value = '';
|
|
1308
|
-
}
|
|
1309
613
|
};
|
|
1310
|
-
// Remove attached image
|
|
1311
614
|
const removeAttachedImage = (id) => {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
}
|
|
1317
|
-
return prev.filter(img => img.id !== id);
|
|
1318
|
-
});
|
|
615
|
+
const img = state.attachedImages.find(i => i.id === id);
|
|
616
|
+
if (img)
|
|
617
|
+
URL.revokeObjectURL(img.preview);
|
|
618
|
+
dispatch({ type: 'REMOVE_ATTACHED_IMAGE', payload: id });
|
|
1319
619
|
};
|
|
1320
|
-
// Clear all attached images
|
|
1321
620
|
const clearAttachedImages = () => {
|
|
1322
|
-
attachedImages.forEach(img => URL.revokeObjectURL(img.preview));
|
|
1323
|
-
|
|
621
|
+
state.attachedImages.forEach(img => URL.revokeObjectURL(img.preview));
|
|
622
|
+
dispatch({ type: 'CLEAR_ATTACHED_IMAGES' });
|
|
1324
623
|
};
|
|
1325
|
-
// Drag and drop
|
|
1326
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
1327
|
-
// Handle drag events
|
|
624
|
+
// Drag and drop handlers
|
|
1328
625
|
const handleDragEnter = useCallback((e) => {
|
|
1329
626
|
e.preventDefault();
|
|
1330
627
|
e.stopPropagation();
|
|
1331
|
-
if (e.dataTransfer.types.includes('Files'))
|
|
1332
|
-
|
|
1333
|
-
}
|
|
628
|
+
if (e.dataTransfer.types.includes('Files'))
|
|
629
|
+
dispatch({ type: 'SET_DRAGGING', payload: true });
|
|
1334
630
|
}, []);
|
|
1335
631
|
const handleDragLeave = useCallback((e) => {
|
|
1336
632
|
e.preventDefault();
|
|
1337
633
|
e.stopPropagation();
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
setIsDragging(false);
|
|
1341
|
-
}
|
|
634
|
+
if (!e.currentTarget.contains(e.relatedTarget))
|
|
635
|
+
dispatch({ type: 'SET_DRAGGING', payload: false });
|
|
1342
636
|
}, []);
|
|
1343
637
|
const handleDragOver = useCallback((e) => {
|
|
1344
638
|
e.preventDefault();
|
|
@@ -1347,298 +641,132 @@ function StoryUIPanel() {
|
|
|
1347
641
|
const handleDrop = useCallback(async (e) => {
|
|
1348
642
|
e.preventDefault();
|
|
1349
643
|
e.stopPropagation();
|
|
1350
|
-
|
|
644
|
+
dispatch({ type: 'SET_DRAGGING', payload: false });
|
|
1351
645
|
const files = Array.from(e.dataTransfer.files);
|
|
1352
646
|
const imageFiles = files.filter(f => f.type.startsWith('image/'));
|
|
1353
647
|
if (imageFiles.length === 0) {
|
|
1354
|
-
|
|
648
|
+
dispatch({ type: 'SET_ERROR', payload: 'Please drop image files only' });
|
|
1355
649
|
return;
|
|
1356
650
|
}
|
|
1357
|
-
const newImages = [];
|
|
1358
651
|
const errors = [];
|
|
1359
|
-
for (let i = 0; i < imageFiles.length && (attachedImages.length +
|
|
652
|
+
for (let i = 0; i < imageFiles.length && (state.attachedImages.length + i) < MAX_IMAGES; i++) {
|
|
1360
653
|
const file = imageFiles[i];
|
|
1361
654
|
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1362
|
-
errors.push(`${file.name}: File too large
|
|
655
|
+
errors.push(`${file.name}: File too large`);
|
|
1363
656
|
continue;
|
|
1364
657
|
}
|
|
1365
658
|
try {
|
|
1366
659
|
const base64 = await fileToBase64(file);
|
|
1367
660
|
const preview = URL.createObjectURL(file);
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
file,
|
|
1371
|
-
preview,
|
|
1372
|
-
base64,
|
|
1373
|
-
mediaType: file.type || 'image/png',
|
|
661
|
+
dispatch({
|
|
662
|
+
type: 'ADD_ATTACHED_IMAGE',
|
|
663
|
+
payload: { id: `${Date.now()}-${i}`, file, preview, base64, mediaType: file.type || 'image/png' },
|
|
1374
664
|
});
|
|
1375
665
|
}
|
|
1376
|
-
catch
|
|
666
|
+
catch {
|
|
1377
667
|
errors.push(`${file.name}: Failed to process`);
|
|
1378
668
|
}
|
|
1379
669
|
}
|
|
1380
|
-
if (errors.length > 0)
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
}, [attachedImages.length, fileToBase64]);
|
|
1385
|
-
// Handle clipboard paste for images
|
|
670
|
+
if (errors.length > 0)
|
|
671
|
+
dispatch({ type: 'SET_ERROR', payload: errors.join('\n') });
|
|
672
|
+
}, [state.attachedImages.length]);
|
|
673
|
+
// Paste handler
|
|
1386
674
|
const handlePaste = useCallback(async (e) => {
|
|
1387
675
|
const items = e.clipboardData?.items;
|
|
1388
676
|
if (!items)
|
|
1389
677
|
return;
|
|
1390
678
|
const imageItems = [];
|
|
1391
679
|
for (let i = 0; i < items.length; i++) {
|
|
1392
|
-
if (items[i].type.startsWith('image/'))
|
|
680
|
+
if (items[i].type.startsWith('image/'))
|
|
1393
681
|
imageItems.push(items[i]);
|
|
1394
|
-
}
|
|
1395
682
|
}
|
|
1396
683
|
if (imageItems.length === 0)
|
|
1397
684
|
return;
|
|
1398
|
-
// Prevent default text paste behavior when pasting images
|
|
1399
685
|
e.preventDefault();
|
|
1400
|
-
if (attachedImages.length >= MAX_IMAGES) {
|
|
1401
|
-
|
|
686
|
+
if (state.attachedImages.length >= MAX_IMAGES) {
|
|
687
|
+
dispatch({ type: 'SET_ERROR', payload: `Maximum ${MAX_IMAGES} images allowed` });
|
|
1402
688
|
return;
|
|
1403
689
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
const item = imageItems[i];
|
|
1408
|
-
const file = item.getAsFile();
|
|
1409
|
-
if (!file) {
|
|
1410
|
-
errors.push('Failed to get image from clipboard');
|
|
690
|
+
for (let i = 0; i < imageItems.length && (state.attachedImages.length + i) < MAX_IMAGES; i++) {
|
|
691
|
+
const file = imageItems[i].getAsFile();
|
|
692
|
+
if (!file)
|
|
1411
693
|
continue;
|
|
1412
|
-
}
|
|
1413
|
-
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1414
|
-
errors.push(`Pasted image too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
|
|
1415
|
-
continue;
|
|
1416
|
-
}
|
|
1417
694
|
try {
|
|
1418
695
|
const base64 = await fileToBase64(file);
|
|
1419
696
|
const preview = URL.createObjectURL(file);
|
|
1420
|
-
// Create a meaningful name for pasted images
|
|
1421
697
|
const timestamp = new Date().toISOString().slice(11, 19).replace(/:/g, '-');
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
698
|
+
dispatch({
|
|
699
|
+
type: 'ADD_ATTACHED_IMAGE',
|
|
700
|
+
payload: {
|
|
701
|
+
id: `paste-${Date.now()}-${i}`,
|
|
702
|
+
file: new File([file], `pasted-image-${timestamp}.png`, { type: file.type }),
|
|
703
|
+
preview,
|
|
704
|
+
base64,
|
|
705
|
+
mediaType: file.type || 'image/png',
|
|
706
|
+
},
|
|
1429
707
|
});
|
|
1430
708
|
}
|
|
1431
|
-
catch
|
|
1432
|
-
|
|
709
|
+
catch {
|
|
710
|
+
dispatch({ type: 'SET_ERROR', payload: 'Failed to process pasted image' });
|
|
1433
711
|
}
|
|
1434
712
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
}
|
|
1438
|
-
if (newImages.length > 0) {
|
|
1439
|
-
setAttachedImages(prev => [...prev, ...newImages]);
|
|
1440
|
-
// Clear any existing error on successful paste
|
|
1441
|
-
if (errors.length === 0) {
|
|
1442
|
-
setError(null);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
}, [attachedImages.length, fileToBase64]);
|
|
1446
|
-
// Load and sync chats on mount
|
|
1447
|
-
useEffect(() => {
|
|
1448
|
-
const initializeChats = async () => {
|
|
1449
|
-
// Test connection first
|
|
1450
|
-
const connectionTest = await testMCPConnection();
|
|
1451
|
-
setConnectionStatus(connectionTest);
|
|
1452
|
-
if (connectionTest.connected) {
|
|
1453
|
-
// Fetch available providers
|
|
1454
|
-
try {
|
|
1455
|
-
const providersRes = await fetch(PROVIDERS_API);
|
|
1456
|
-
if (providersRes.ok) {
|
|
1457
|
-
const providersData = await providersRes.json();
|
|
1458
|
-
setAvailableProviders(providersData.providers.filter(p => p.configured));
|
|
1459
|
-
// Set initial selection from server defaults
|
|
1460
|
-
if (providersData.current) {
|
|
1461
|
-
setSelectedProvider(providersData.current.provider.toLowerCase());
|
|
1462
|
-
setSelectedModel(providersData.current.model);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
catch (e) {
|
|
1467
|
-
console.error('Failed to fetch providers:', e);
|
|
1468
|
-
}
|
|
1469
|
-
// Fetch design system considerations for environment parity
|
|
1470
|
-
// This ensures production gets the same considerations as local development
|
|
1471
|
-
try {
|
|
1472
|
-
const considerationsRes = await fetch(CONSIDERATIONS_API);
|
|
1473
|
-
if (considerationsRes.ok) {
|
|
1474
|
-
const considerationsData = await considerationsRes.json();
|
|
1475
|
-
if (considerationsData.hasConsiderations && considerationsData.considerations) {
|
|
1476
|
-
setConsiderations(considerationsData.considerations);
|
|
1477
|
-
console.log(`Loaded considerations from ${considerationsData.source}`);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
catch (e) {
|
|
1482
|
-
console.error('Failed to fetch considerations:', e);
|
|
1483
|
-
}
|
|
1484
|
-
const syncedChats = await syncWithActualStories();
|
|
1485
|
-
const sortedChats = syncedChats.sort((a, b) => b.lastUpdated - a.lastUpdated).slice(0, MAX_RECENT_CHATS);
|
|
1486
|
-
setRecentChats(sortedChats);
|
|
1487
|
-
if (sortedChats.length > 0) {
|
|
1488
|
-
setConversation(sortedChats[0].conversation);
|
|
1489
|
-
setActiveChatId(sortedChats[0].id);
|
|
1490
|
-
setActiveTitle(sortedChats[0].title);
|
|
1491
|
-
}
|
|
1492
|
-
// Fetch orphan stories (stories on disk without chat history)
|
|
1493
|
-
const orphans = await fetchOrphanStories();
|
|
1494
|
-
setOrphanStories(orphans);
|
|
1495
|
-
}
|
|
1496
|
-
else {
|
|
1497
|
-
// Load from local storage if server is not available
|
|
1498
|
-
const localChats = loadChats();
|
|
1499
|
-
setRecentChats(localChats);
|
|
1500
|
-
}
|
|
1501
|
-
};
|
|
1502
|
-
initializeChats();
|
|
1503
|
-
}, []);
|
|
1504
|
-
// Scroll to bottom on new message
|
|
1505
|
-
useEffect(() => {
|
|
1506
|
-
if (chatEndRef.current) {
|
|
1507
|
-
chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
1508
|
-
}
|
|
1509
|
-
}, [conversation, loading]);
|
|
1510
|
-
// Helper function for non-streaming fallback
|
|
1511
|
-
const handleSendNonStreaming = async (userInput, newConversation) => {
|
|
1512
|
-
const res = await fetch(MCP_API, {
|
|
1513
|
-
method: 'POST',
|
|
1514
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1515
|
-
body: JSON.stringify({
|
|
1516
|
-
prompt: userInput,
|
|
1517
|
-
conversation: newConversation,
|
|
1518
|
-
fileName: activeChatId || undefined,
|
|
1519
|
-
provider: selectedProvider || undefined,
|
|
1520
|
-
model: selectedModel || undefined,
|
|
1521
|
-
considerations: considerations || undefined,
|
|
1522
|
-
}),
|
|
1523
|
-
});
|
|
1524
|
-
const contentType = res.headers.get('content-type');
|
|
1525
|
-
if (!contentType || !contentType.includes('application/json')) {
|
|
1526
|
-
const text = await res.text();
|
|
1527
|
-
throw new Error(`Server returned non-JSON response. Response: ${text.substring(0, 200)}...`);
|
|
1528
|
-
}
|
|
1529
|
-
const data = await res.json();
|
|
1530
|
-
if (!res.ok || !data.success)
|
|
1531
|
-
throw new Error(data.error || 'Story generation failed');
|
|
1532
|
-
return data;
|
|
1533
|
-
};
|
|
1534
|
-
// Helper function to build a conversational response from completion data
|
|
1535
|
-
// Uses special markers [SUCCESS], [ERROR], [TIP], [WRENCH] that renderMarkdown converts to icons
|
|
1536
|
-
// Track whether we've shown the refresh hint in this session
|
|
1537
|
-
const hasShownRefreshHint = useRef(false);
|
|
713
|
+
}, [state.attachedImages.length]);
|
|
714
|
+
// Build response message
|
|
1538
715
|
const buildConversationalResponse = (completion, isUpdate) => {
|
|
1539
716
|
const parts = [];
|
|
1540
717
|
const statusMarker = completion.success ? '[SUCCESS]' : '[ERROR]';
|
|
1541
|
-
|
|
1542
|
-
if (isUpdate) {
|
|
1543
|
-
parts.push(`${statusMarker} **Updated: "${completion.title}"**`);
|
|
1544
|
-
}
|
|
1545
|
-
else {
|
|
1546
|
-
parts.push(`${statusMarker} **Created: "${completion.title}"**`);
|
|
1547
|
-
}
|
|
1548
|
-
// Build component insights with reasons when available
|
|
718
|
+
parts.push(isUpdate ? `${statusMarker} **Updated: "${completion.title}"**` : `${statusMarker} **Created: "${completion.title}"**`);
|
|
1549
719
|
const componentCount = completion.componentsUsed?.length || 0;
|
|
1550
720
|
if (componentCount > 0) {
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
const componentsWithReasons = componentList.filter(c => c.reason && c.reason !== 'Used in composition');
|
|
1554
|
-
if (componentsWithReasons.length > 0) {
|
|
1555
|
-
// Show components with their reasons
|
|
1556
|
-
const insights = componentsWithReasons
|
|
1557
|
-
.slice(0, 3)
|
|
1558
|
-
.map(c => `\`${c.name}\` - ${c.reason?.toLowerCase()}`)
|
|
1559
|
-
.join(', ');
|
|
1560
|
-
parts.push(`\nUsed ${insights}${componentCount > 3 ? ` and ${componentCount - 3} more` : ''}.`);
|
|
1561
|
-
}
|
|
1562
|
-
else {
|
|
1563
|
-
// Fallback to simple list
|
|
1564
|
-
const names = componentList.map(c => `\`${c.name}\``).join(', ');
|
|
1565
|
-
parts.push(`\nBuilt with ${names}${componentCount > 5 ? '...' : ''}.`);
|
|
1566
|
-
}
|
|
721
|
+
const names = completion.componentsUsed.slice(0, 5).map(c => `\`${c.name}\``).join(', ');
|
|
722
|
+
parts.push(`\nBuilt with ${names}${componentCount > 5 ? '...' : ''}.`);
|
|
1567
723
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
parts.push(`\n\n**Layout:** ${primaryLayout.pattern} - ${primaryLayout.reason.charAt(0).toLowerCase()}${primaryLayout.reason.slice(1)}.`);
|
|
724
|
+
if (completion.layoutChoices?.length > 0) {
|
|
725
|
+
const layout = completion.layoutChoices[0];
|
|
726
|
+
parts.push(`\n\n**Layout:** ${layout.pattern} - ${layout.reason}.`);
|
|
1572
727
|
}
|
|
1573
|
-
// Add style choices only if they add value
|
|
1574
|
-
if (completion.styleChoices && completion.styleChoices.length > 0) {
|
|
1575
|
-
const notableStyles = completion.styleChoices.filter(s => s.reason && s.reason !== 'Semantic color from design system');
|
|
1576
|
-
if (notableStyles.length > 0) {
|
|
1577
|
-
const styleInfo = notableStyles[0];
|
|
1578
|
-
parts.push(` Applied \`${styleInfo.value}\` for ${styleInfo.reason?.toLowerCase() || 'visual consistency'}.`);
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
// Add validation fixes notice
|
|
1582
728
|
if (completion.validation?.autoFixApplied) {
|
|
1583
729
|
parts.push(`\n\n[WRENCH] **Auto-fixed:** Minor syntax issues were automatically corrected.`);
|
|
1584
730
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
const suggestion = completion.suggestions[0];
|
|
1588
|
-
// Only show if it's not the generic "review the generated code" message
|
|
1589
|
-
if (!suggestion.toLowerCase().includes('review the generated code')) {
|
|
1590
|
-
parts.push(`\n\n[TIP] **Tip:** ${suggestion}`);
|
|
1591
|
-
}
|
|
731
|
+
if (completion.suggestions && completion.suggestions.length > 0 && !completion.suggestions[0].toLowerCase().includes('review the generated code')) {
|
|
732
|
+
parts.push(`\n\n[TIP] **Tip:** ${completion.suggestions[0]}`);
|
|
1592
733
|
}
|
|
1593
|
-
// Show refresh hint only once per session for new stories (local mode only)
|
|
1594
|
-
// In Edge mode, stories are stored in Durable Objects, not on filesystem
|
|
1595
734
|
if (!isUpdate && !hasShownRefreshHint.current) {
|
|
1596
|
-
|
|
1597
|
-
parts.push(`\n\n_Story saved to cloud. View code in chat history recent chats navigation._`);
|
|
1598
|
-
}
|
|
1599
|
-
else {
|
|
1600
|
-
parts.push(`\n\n_Might need toefresh Storybook (Cmd/Ctrl + R) to see new stories in the sidebar._`);
|
|
1601
|
-
}
|
|
735
|
+
parts.push(isEdgeMode() ? `\n\n_Story saved to cloud._` : `\n\n_Might need to refresh Storybook (Cmd/Ctrl + R) to see new stories._`);
|
|
1602
736
|
hasShownRefreshHint.current = true;
|
|
1603
737
|
}
|
|
1604
|
-
// Add metrics in a subtle way (if available)
|
|
1605
738
|
if (completion.metrics?.totalTimeMs) {
|
|
1606
|
-
|
|
1607
|
-
parts.push(`\n\n_${seconds}s_`);
|
|
739
|
+
parts.push(`\n\n_${(completion.metrics.totalTimeMs / 1000).toFixed(1)}s_`);
|
|
1608
740
|
}
|
|
1609
741
|
return parts.join('');
|
|
1610
742
|
};
|
|
1611
|
-
//
|
|
743
|
+
// Finalize streaming
|
|
1612
744
|
const finalizeStreamingConversation = useCallback((newConversation, completion, userInput) => {
|
|
1613
|
-
// Build conversational response using rich completion data
|
|
1614
745
|
const isUpdate = completion.summary.action === 'updated';
|
|
1615
746
|
const responseMessage = buildConversationalResponse(completion, isUpdate);
|
|
1616
747
|
const aiMsg = { role: 'ai', content: responseMessage };
|
|
1617
748
|
const updatedConversation = [...newConversation, aiMsg];
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
if (isExistingSession && activeChatId) {
|
|
749
|
+
dispatch({ type: 'SET_CONVERSATION', payload: updatedConversation });
|
|
750
|
+
const isExistingSession = state.activeChatId && state.conversation.length > 0;
|
|
751
|
+
if (isExistingSession && state.activeChatId) {
|
|
1622
752
|
const updatedSession = {
|
|
1623
|
-
id: activeChatId,
|
|
1624
|
-
title: activeTitle,
|
|
1625
|
-
fileName: completion.fileName || activeChatId,
|
|
753
|
+
id: state.activeChatId,
|
|
754
|
+
title: state.activeTitle,
|
|
755
|
+
fileName: completion.fileName || state.activeChatId,
|
|
1626
756
|
conversation: updatedConversation,
|
|
1627
757
|
lastUpdated: Date.now(),
|
|
1628
758
|
};
|
|
1629
759
|
const chats = loadChats();
|
|
1630
|
-
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
1631
|
-
if (chatIndex !== -1)
|
|
760
|
+
const chatIndex = chats.findIndex(c => c.id === state.activeChatId);
|
|
761
|
+
if (chatIndex !== -1)
|
|
1632
762
|
chats[chatIndex] = updatedSession;
|
|
1633
|
-
}
|
|
1634
763
|
saveChats(chats);
|
|
1635
|
-
|
|
764
|
+
dispatch({ type: 'SET_RECENT_CHATS', payload: chats });
|
|
1636
765
|
}
|
|
1637
766
|
else {
|
|
1638
767
|
const chatId = completion.storyId || completion.fileName || Date.now().toString();
|
|
1639
768
|
const chatTitle = completion.title || userInput;
|
|
1640
|
-
|
|
1641
|
-
setActiveTitle(chatTitle);
|
|
769
|
+
dispatch({ type: 'SET_ACTIVE_CHAT', payload: { id: chatId, title: chatTitle } });
|
|
1642
770
|
const newSession = {
|
|
1643
771
|
id: chatId,
|
|
1644
772
|
title: chatTitle,
|
|
@@ -1648,66 +776,48 @@ function StoryUIPanel() {
|
|
|
1648
776
|
};
|
|
1649
777
|
const chats = loadChats().filter(c => c.id !== chatId);
|
|
1650
778
|
chats.unshift(newSession);
|
|
1651
|
-
if (chats.length > MAX_RECENT_CHATS)
|
|
779
|
+
if (chats.length > MAX_RECENT_CHATS)
|
|
1652
780
|
chats.splice(MAX_RECENT_CHATS);
|
|
1653
|
-
}
|
|
1654
781
|
saveChats(chats);
|
|
1655
|
-
|
|
1656
|
-
// Auto-navigate to the newly created story after HMR processes the file
|
|
1657
|
-
// This prevents the "Couldn't find story after HMR" error by refreshing
|
|
1658
|
-
// after the file system has been updated and HMR has processed the change
|
|
1659
|
-
navigateToNewStory(chatTitle, completion.code);
|
|
782
|
+
dispatch({ type: 'SET_RECENT_CHATS', payload: chats });
|
|
1660
783
|
}
|
|
1661
|
-
}, [activeChatId, activeTitle, conversation.length]);
|
|
784
|
+
}, [state.activeChatId, state.activeTitle, state.conversation.length]);
|
|
785
|
+
// Handle send
|
|
1662
786
|
const handleSend = async (e) => {
|
|
1663
787
|
if (e)
|
|
1664
788
|
e.preventDefault();
|
|
1665
|
-
|
|
1666
|
-
if (!input.trim() && attachedImages.length === 0)
|
|
789
|
+
if (!state.input.trim() && state.attachedImages.length === 0)
|
|
1667
790
|
return;
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
setStreamingState(null);
|
|
1673
|
-
// Test connection before sending
|
|
791
|
+
const userInput = state.input.trim() || (state.attachedImages.length > 0 ? 'Create a component that matches this design' : '');
|
|
792
|
+
dispatch({ type: 'SET_ERROR', payload: null });
|
|
793
|
+
dispatch({ type: 'SET_LOADING', payload: true });
|
|
794
|
+
dispatch({ type: 'SET_STREAMING_STATE', payload: null });
|
|
1674
795
|
const connectionTest = await testMCPConnection();
|
|
1675
|
-
|
|
796
|
+
dispatch({ type: 'SET_CONNECTION_STATUS', payload: connectionTest });
|
|
1676
797
|
if (!connectionTest.connected) {
|
|
1677
|
-
|
|
1678
|
-
|
|
798
|
+
dispatch({ type: 'SET_ERROR', payload: `Cannot connect to MCP server: ${connectionTest.error || 'Server not running'}` });
|
|
799
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
1679
800
|
return;
|
|
1680
801
|
}
|
|
1681
|
-
|
|
1682
|
-
const imagesToSend = [...attachedImages];
|
|
802
|
+
const imagesToSend = [...state.attachedImages];
|
|
1683
803
|
const hasImages = imagesToSend.length > 0;
|
|
1684
|
-
// Create user message with images
|
|
1685
804
|
const userMessage = {
|
|
1686
805
|
role: 'user',
|
|
1687
806
|
content: userInput,
|
|
1688
|
-
attachedImages: hasImages ? imagesToSend : undefined
|
|
807
|
+
attachedImages: hasImages ? imagesToSend : undefined,
|
|
1689
808
|
};
|
|
1690
|
-
const newConversation = [...conversation, userMessage];
|
|
1691
|
-
|
|
1692
|
-
|
|
809
|
+
const newConversation = [...state.conversation, userMessage];
|
|
810
|
+
dispatch({ type: 'SET_CONVERSATION', payload: newConversation });
|
|
811
|
+
dispatch({ type: 'SET_INPUT', payload: '' });
|
|
1693
812
|
clearAttachedImages();
|
|
1694
|
-
// Use streaming if enabled
|
|
1695
813
|
if (USE_STREAMING) {
|
|
1696
814
|
try {
|
|
1697
|
-
|
|
1698
|
-
if (abortControllerRef.current) {
|
|
815
|
+
if (abortControllerRef.current)
|
|
1699
816
|
abortControllerRef.current.abort();
|
|
1700
|
-
}
|
|
1701
817
|
abortControllerRef.current = new AbortController();
|
|
1702
|
-
|
|
1703
|
-
setStreamingState({});
|
|
1704
|
-
// Prepare images for API request
|
|
818
|
+
dispatch({ type: 'SET_STREAMING_STATE', payload: {} });
|
|
1705
819
|
const imagePayload = hasImages
|
|
1706
|
-
? imagesToSend.map(img => ({
|
|
1707
|
-
type: 'base64',
|
|
1708
|
-
data: img.base64,
|
|
1709
|
-
mediaType: img.file.type,
|
|
1710
|
-
}))
|
|
820
|
+
? imagesToSend.map(img => ({ type: 'base64', data: img.base64, mediaType: img.file.type }))
|
|
1711
821
|
: undefined;
|
|
1712
822
|
const response = await fetch(MCP_STREAM_API, {
|
|
1713
823
|
method: 'POST',
|
|
@@ -1715,25 +825,23 @@ function StoryUIPanel() {
|
|
|
1715
825
|
body: JSON.stringify({
|
|
1716
826
|
prompt: userInput,
|
|
1717
827
|
conversation: newConversation,
|
|
1718
|
-
fileName: activeChatId || undefined,
|
|
1719
|
-
isUpdate: activeChatId && conversation.length > 0,
|
|
1720
|
-
originalTitle: activeTitle || undefined,
|
|
1721
|
-
storyId: activeChatId || undefined,
|
|
828
|
+
fileName: state.activeChatId || undefined,
|
|
829
|
+
isUpdate: state.activeChatId && state.conversation.length > 0,
|
|
830
|
+
originalTitle: state.activeTitle || undefined,
|
|
831
|
+
storyId: state.activeChatId || undefined,
|
|
1722
832
|
images: imagePayload,
|
|
1723
833
|
visionMode: hasImages ? 'screenshot_to_story' : undefined,
|
|
1724
|
-
provider: selectedProvider || undefined,
|
|
1725
|
-
model: selectedModel || undefined,
|
|
1726
|
-
considerations: considerations || undefined,
|
|
834
|
+
provider: state.selectedProvider || undefined,
|
|
835
|
+
model: state.selectedModel || undefined,
|
|
836
|
+
considerations: state.considerations || undefined,
|
|
1727
837
|
}),
|
|
1728
838
|
signal: abortControllerRef.current.signal,
|
|
1729
839
|
});
|
|
1730
|
-
if (!response.ok)
|
|
840
|
+
if (!response.ok)
|
|
1731
841
|
throw new Error(`Streaming request failed: ${response.status}`);
|
|
1732
|
-
}
|
|
1733
842
|
const reader = response.body?.getReader();
|
|
1734
|
-
if (!reader)
|
|
843
|
+
if (!reader)
|
|
1735
844
|
throw new Error('No response body');
|
|
1736
|
-
}
|
|
1737
845
|
const decoder = new TextDecoder();
|
|
1738
846
|
let buffer = '';
|
|
1739
847
|
let completionData = null;
|
|
@@ -1743,215 +851,109 @@ function StoryUIPanel() {
|
|
|
1743
851
|
if (done)
|
|
1744
852
|
break;
|
|
1745
853
|
buffer += decoder.decode(value, { stream: true });
|
|
1746
|
-
// Parse SSE events from buffer
|
|
1747
854
|
const lines = buffer.split('\n');
|
|
1748
|
-
buffer = lines.pop() || '';
|
|
855
|
+
buffer = lines.pop() || '';
|
|
1749
856
|
for (const line of lines) {
|
|
1750
857
|
if (line.startsWith('data: ')) {
|
|
1751
858
|
try {
|
|
1752
859
|
const event = JSON.parse(line.slice(6));
|
|
1753
|
-
// Update streaming state based on event type
|
|
1754
860
|
switch (event.type) {
|
|
1755
861
|
case 'intent':
|
|
1756
|
-
|
|
862
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { intent: event.data } });
|
|
1757
863
|
break;
|
|
1758
864
|
case 'progress':
|
|
1759
|
-
|
|
865
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { progress: event.data } });
|
|
1760
866
|
break;
|
|
1761
867
|
case 'validation':
|
|
1762
|
-
|
|
868
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { validation: event.data } });
|
|
1763
869
|
break;
|
|
1764
870
|
case 'retry':
|
|
1765
|
-
|
|
871
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { retry: event.data } });
|
|
1766
872
|
break;
|
|
1767
873
|
case 'completion':
|
|
1768
874
|
completionData = event.data;
|
|
1769
|
-
|
|
875
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { completion: completionData } });
|
|
1770
876
|
break;
|
|
1771
877
|
case 'error':
|
|
1772
878
|
errorData = event.data;
|
|
1773
|
-
|
|
879
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { error: errorData } });
|
|
1774
880
|
break;
|
|
1775
881
|
}
|
|
1776
882
|
}
|
|
1777
|
-
catch
|
|
1778
|
-
console.warn('Failed to parse SSE event:', line
|
|
883
|
+
catch {
|
|
884
|
+
console.warn('Failed to parse SSE event:', line);
|
|
1779
885
|
}
|
|
1780
886
|
}
|
|
1781
887
|
}
|
|
1782
888
|
}
|
|
1783
|
-
// Handle completion or error
|
|
1784
889
|
if (completionData) {
|
|
1785
890
|
finalizeStreamingConversation(newConversation, completionData, userInput);
|
|
1786
891
|
}
|
|
1787
892
|
else if (errorData) {
|
|
1788
|
-
|
|
893
|
+
dispatch({ type: 'SET_ERROR', payload: errorData.message });
|
|
1789
894
|
const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorData.message}\n\n${errorData.suggestion || ''}` }];
|
|
1790
|
-
|
|
895
|
+
dispatch({ type: 'SET_CONVERSATION', payload: errorConversation });
|
|
1791
896
|
}
|
|
1792
897
|
}
|
|
1793
898
|
catch (err) {
|
|
1794
|
-
if (err.name === 'AbortError')
|
|
1795
|
-
console.log('Request aborted');
|
|
899
|
+
if (err.name === 'AbortError')
|
|
1796
900
|
return;
|
|
1797
|
-
}
|
|
1798
|
-
// Fall back to non-streaming on error
|
|
1799
901
|
console.warn('Streaming failed, falling back to non-streaming:', err);
|
|
1800
|
-
|
|
902
|
+
dispatch({ type: 'SET_STREAMING_STATE', payload: null });
|
|
1801
903
|
try {
|
|
1802
|
-
const
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
904
|
+
const res = await fetch(MCP_API, {
|
|
905
|
+
method: 'POST',
|
|
906
|
+
headers: { 'Content-Type': 'application/json' },
|
|
907
|
+
body: JSON.stringify({
|
|
908
|
+
prompt: userInput,
|
|
909
|
+
conversation: newConversation,
|
|
910
|
+
fileName: state.activeChatId || undefined,
|
|
911
|
+
provider: state.selectedProvider || undefined,
|
|
912
|
+
model: state.selectedModel || undefined,
|
|
913
|
+
considerations: state.considerations || undefined,
|
|
914
|
+
}),
|
|
915
|
+
});
|
|
916
|
+
const data = await res.json();
|
|
917
|
+
if (!res.ok || !data.success)
|
|
918
|
+
throw new Error(data.error || 'Story generation failed');
|
|
919
|
+
const responseMessage = `[SUCCESS] **Created: "${data.title}"**\n\nStory generated successfully.`;
|
|
1813
920
|
const aiMsg = { role: 'ai', content: responseMessage };
|
|
1814
921
|
const updatedConversation = [...newConversation, aiMsg];
|
|
1815
|
-
|
|
1816
|
-
// Update chat session
|
|
1817
|
-
const isUpdate = activeChatId && conversation.length > 0;
|
|
1818
|
-
if (isUpdate && activeChatId) {
|
|
1819
|
-
const updatedSession = {
|
|
1820
|
-
id: activeChatId,
|
|
1821
|
-
title: activeTitle,
|
|
1822
|
-
fileName: data.fileName || activeChatId,
|
|
1823
|
-
conversation: updatedConversation,
|
|
1824
|
-
lastUpdated: Date.now(),
|
|
1825
|
-
};
|
|
1826
|
-
const chats = loadChats();
|
|
1827
|
-
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
1828
|
-
if (chatIndex !== -1)
|
|
1829
|
-
chats[chatIndex] = updatedSession;
|
|
1830
|
-
saveChats(chats);
|
|
1831
|
-
setRecentChats(chats);
|
|
1832
|
-
}
|
|
1833
|
-
else {
|
|
1834
|
-
const chatId = data.storyId || data.fileName || Date.now().toString();
|
|
1835
|
-
const chatTitle = data.title || userInput;
|
|
1836
|
-
setActiveChatId(chatId);
|
|
1837
|
-
setActiveTitle(chatTitle);
|
|
1838
|
-
const newSession = {
|
|
1839
|
-
id: chatId,
|
|
1840
|
-
title: chatTitle,
|
|
1841
|
-
fileName: data.fileName || '',
|
|
1842
|
-
conversation: updatedConversation,
|
|
1843
|
-
lastUpdated: Date.now(),
|
|
1844
|
-
};
|
|
1845
|
-
const chats = loadChats().filter(c => c.id !== chatId);
|
|
1846
|
-
chats.unshift(newSession);
|
|
1847
|
-
if (chats.length > MAX_RECENT_CHATS)
|
|
1848
|
-
chats.splice(MAX_RECENT_CHATS);
|
|
1849
|
-
saveChats(chats);
|
|
1850
|
-
setRecentChats(chats);
|
|
1851
|
-
// Auto-navigate to the newly created story
|
|
1852
|
-
navigateToNewStory(chatTitle, data.code);
|
|
1853
|
-
}
|
|
922
|
+
dispatch({ type: 'SET_CONVERSATION', payload: updatedConversation });
|
|
1854
923
|
}
|
|
1855
924
|
catch (fallbackErr) {
|
|
1856
925
|
const errorMessage = fallbackErr instanceof Error ? fallbackErr.message : 'Unknown error';
|
|
1857
|
-
|
|
926
|
+
dispatch({ type: 'SET_ERROR', payload: errorMessage });
|
|
1858
927
|
const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorMessage}` }];
|
|
1859
|
-
|
|
928
|
+
dispatch({ type: 'SET_CONVERSATION', payload: errorConversation });
|
|
1860
929
|
}
|
|
1861
930
|
}
|
|
1862
931
|
finally {
|
|
1863
|
-
|
|
1864
|
-
|
|
932
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
933
|
+
dispatch({ type: 'SET_STREAMING_STATE', payload: null });
|
|
1865
934
|
abortControllerRef.current = null;
|
|
1866
935
|
}
|
|
1867
936
|
}
|
|
1868
|
-
else {
|
|
1869
|
-
// Non-streaming mode (original implementation)
|
|
1870
|
-
try {
|
|
1871
|
-
const data = await handleSendNonStreaming(userInput, newConversation);
|
|
1872
|
-
let responseMessage;
|
|
1873
|
-
const statusMarker = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '[WRENCH]' : '[TIP]') : '[SUCCESS]';
|
|
1874
|
-
// Build conversational response for non-streaming mode
|
|
1875
|
-
if (data.isUpdate) {
|
|
1876
|
-
responseMessage = `${statusMarker} **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._`;
|
|
1877
|
-
}
|
|
1878
|
-
else {
|
|
1879
|
-
responseMessage = `${statusMarker} **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[TIP] **Note**: If you don't see the story immediately, you may need to refresh your Storybook page (Cmd/Ctrl + R).`;
|
|
1880
|
-
}
|
|
1881
|
-
const aiMsg = { role: 'ai', content: responseMessage };
|
|
1882
|
-
const updatedConversation = [...newConversation, aiMsg];
|
|
1883
|
-
setConversation(updatedConversation);
|
|
1884
|
-
const isUpdate = activeChatId && conversation.length > 0;
|
|
1885
|
-
if (isUpdate && activeChatId) {
|
|
1886
|
-
const updatedSession = {
|
|
1887
|
-
id: activeChatId,
|
|
1888
|
-
title: activeTitle,
|
|
1889
|
-
fileName: data.fileName || activeChatId,
|
|
1890
|
-
conversation: updatedConversation,
|
|
1891
|
-
lastUpdated: Date.now(),
|
|
1892
|
-
};
|
|
1893
|
-
const chats = loadChats();
|
|
1894
|
-
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
1895
|
-
if (chatIndex !== -1)
|
|
1896
|
-
chats[chatIndex] = updatedSession;
|
|
1897
|
-
saveChats(chats);
|
|
1898
|
-
setRecentChats(chats);
|
|
1899
|
-
}
|
|
1900
|
-
else {
|
|
1901
|
-
const chatId = data.storyId || data.fileName || Date.now().toString();
|
|
1902
|
-
const chatTitle = data.title || userInput;
|
|
1903
|
-
setActiveChatId(chatId);
|
|
1904
|
-
setActiveTitle(chatTitle);
|
|
1905
|
-
const newSession = {
|
|
1906
|
-
id: chatId,
|
|
1907
|
-
title: chatTitle,
|
|
1908
|
-
fileName: data.fileName || '',
|
|
1909
|
-
conversation: updatedConversation,
|
|
1910
|
-
lastUpdated: Date.now(),
|
|
1911
|
-
};
|
|
1912
|
-
const chats = loadChats().filter(c => c.id !== chatId);
|
|
1913
|
-
chats.unshift(newSession);
|
|
1914
|
-
if (chats.length > MAX_RECENT_CHATS)
|
|
1915
|
-
chats.splice(MAX_RECENT_CHATS);
|
|
1916
|
-
saveChats(chats);
|
|
1917
|
-
setRecentChats(chats);
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
catch (err) {
|
|
1921
|
-
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
1922
|
-
setError(errorMessage);
|
|
1923
|
-
const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorMessage}` }];
|
|
1924
|
-
setConversation(errorConversation);
|
|
1925
|
-
}
|
|
1926
|
-
finally {
|
|
1927
|
-
setLoading(false);
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
937
|
};
|
|
938
|
+
// Chat management
|
|
1931
939
|
const handleSelectChat = (chat) => {
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
setActiveTitle(chat.title);
|
|
1935
|
-
};
|
|
1936
|
-
const handleNewChat = () => {
|
|
1937
|
-
setConversation([]);
|
|
1938
|
-
setActiveChatId(null);
|
|
1939
|
-
setActiveTitle('');
|
|
940
|
+
dispatch({ type: 'SET_CONVERSATION', payload: chat.conversation });
|
|
941
|
+
dispatch({ type: 'SET_ACTIVE_CHAT', payload: { id: chat.id, title: chat.title } });
|
|
1940
942
|
};
|
|
943
|
+
const handleNewChat = () => dispatch({ type: 'NEW_CHAT' });
|
|
1941
944
|
const handleDeleteChat = async (chatId, e) => {
|
|
1942
|
-
e
|
|
945
|
+
if (e)
|
|
946
|
+
e.stopPropagation();
|
|
947
|
+
setContextMenuId(null);
|
|
1943
948
|
if (confirm('Delete this story and chat? This action cannot be undone.')) {
|
|
1944
949
|
const success = await deleteStoryAndChat(chatId);
|
|
1945
950
|
if (success) {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
// If we deleted the active chat, switch to another or clear
|
|
1950
|
-
if (activeChatId === chatId) {
|
|
951
|
+
const updatedChats = state.recentChats.filter(chat => chat.id !== chatId);
|
|
952
|
+
dispatch({ type: 'SET_RECENT_CHATS', payload: updatedChats });
|
|
953
|
+
if (state.activeChatId === chatId) {
|
|
1951
954
|
if (updatedChats.length > 0) {
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
setActiveTitle(updatedChats[0].title);
|
|
955
|
+
dispatch({ type: 'SET_CONVERSATION', payload: updatedChats[0].conversation });
|
|
956
|
+
dispatch({ type: 'SET_ACTIVE_CHAT', payload: { id: updatedChats[0].id, title: updatedChats[0].title } });
|
|
1955
957
|
}
|
|
1956
958
|
else {
|
|
1957
959
|
handleNewChat();
|
|
@@ -1963,372 +965,124 @@ function StoryUIPanel() {
|
|
|
1963
965
|
}
|
|
1964
966
|
}
|
|
1965
967
|
};
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
968
|
+
const handleStartRename = (chatId, currentTitle, e) => {
|
|
969
|
+
if (e)
|
|
970
|
+
e.stopPropagation();
|
|
971
|
+
setContextMenuId(null);
|
|
972
|
+
setRenamingChatId(chatId);
|
|
973
|
+
setRenameValue(currentTitle);
|
|
974
|
+
};
|
|
975
|
+
const handleConfirmRename = (chatId) => {
|
|
976
|
+
if (!renameValue.trim()) {
|
|
977
|
+
setRenamingChatId(null);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const chats = loadChats();
|
|
981
|
+
const chatIndex = chats.findIndex(c => c.id === chatId);
|
|
982
|
+
if (chatIndex !== -1) {
|
|
983
|
+
chats[chatIndex].title = renameValue.trim();
|
|
984
|
+
saveChats(chats);
|
|
985
|
+
dispatch({ type: 'SET_RECENT_CHATS', payload: chats });
|
|
986
|
+
if (state.activeChatId === chatId) {
|
|
987
|
+
dispatch({ type: 'SET_ACTIVE_CHAT', payload: { id: chatId, title: renameValue.trim() } });
|
|
1975
988
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
989
|
+
}
|
|
990
|
+
setRenamingChatId(null);
|
|
991
|
+
setRenameValue('');
|
|
992
|
+
};
|
|
993
|
+
const handleCancelRename = () => {
|
|
994
|
+
setRenamingChatId(null);
|
|
995
|
+
setRenameValue('');
|
|
1978
996
|
};
|
|
1979
|
-
//
|
|
997
|
+
// Orphan story handlers
|
|
1980
998
|
const toggleSelectAll = () => {
|
|
1981
|
-
if (selectedStoryIds.size === orphanStories.length) {
|
|
1982
|
-
|
|
999
|
+
if (state.selectedStoryIds.size === state.orphanStories.length) {
|
|
1000
|
+
dispatch({ type: 'SET_SELECTED_STORY_IDS', payload: new Set() });
|
|
1983
1001
|
}
|
|
1984
1002
|
else {
|
|
1985
|
-
|
|
1003
|
+
dispatch({ type: 'SET_SELECTED_STORY_IDS', payload: new Set(state.orphanStories.map(s => s.id)) });
|
|
1986
1004
|
}
|
|
1987
1005
|
};
|
|
1988
|
-
// Bulk delete selected stories
|
|
1989
1006
|
const handleBulkDelete = async () => {
|
|
1990
|
-
if (selectedStoryIds.size === 0)
|
|
1007
|
+
if (state.selectedStoryIds.size === 0)
|
|
1991
1008
|
return;
|
|
1992
|
-
const count = selectedStoryIds.size;
|
|
1993
|
-
if (!confirm(`Delete ${count} selected ${count === 1 ? 'story' : 'stories'}
|
|
1009
|
+
const count = state.selectedStoryIds.size;
|
|
1010
|
+
if (!confirm(`Delete ${count} selected ${count === 1 ? 'story' : 'stories'}?`))
|
|
1994
1011
|
return;
|
|
1995
|
-
}
|
|
1996
|
-
setIsBulkDeleting(true);
|
|
1012
|
+
dispatch({ type: 'SET_BULK_DELETING', payload: true });
|
|
1997
1013
|
try {
|
|
1998
1014
|
const response = await fetch(`${STORIES_API}/delete-bulk`, {
|
|
1999
1015
|
method: 'POST',
|
|
2000
1016
|
headers: { 'Content-Type': 'application/json' },
|
|
2001
|
-
body: JSON.stringify({ ids: Array.from(selectedStoryIds) }),
|
|
1017
|
+
body: JSON.stringify({ ids: Array.from(state.selectedStoryIds) }),
|
|
2002
1018
|
});
|
|
2003
1019
|
if (response.ok) {
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
setOrphanStories(prev => prev.filter(s => !selectedStoryIds.has(s.id)));
|
|
2007
|
-
setSelectedStoryIds(new Set());
|
|
2008
|
-
console.log(`Deleted ${result.deleted?.length || count} stories`);
|
|
1020
|
+
dispatch({ type: 'SET_ORPHAN_STORIES', payload: state.orphanStories.filter(s => !state.selectedStoryIds.has(s.id)) });
|
|
1021
|
+
dispatch({ type: 'SET_SELECTED_STORY_IDS', payload: new Set() });
|
|
2009
1022
|
}
|
|
2010
1023
|
else {
|
|
2011
|
-
alert('Failed to delete some stories.
|
|
1024
|
+
alert('Failed to delete some stories.');
|
|
2012
1025
|
}
|
|
2013
1026
|
}
|
|
2014
|
-
catch
|
|
2015
|
-
|
|
2016
|
-
alert('Failed to delete stories. Please try again.');
|
|
1027
|
+
catch {
|
|
1028
|
+
alert('Failed to delete stories.');
|
|
2017
1029
|
}
|
|
2018
1030
|
finally {
|
|
2019
|
-
|
|
1031
|
+
dispatch({ type: 'SET_BULK_DELETING', payload: false });
|
|
2020
1032
|
}
|
|
2021
1033
|
};
|
|
2022
|
-
// Clear all generated stories
|
|
2023
1034
|
const handleClearAll = async () => {
|
|
2024
|
-
if (orphanStories.length === 0)
|
|
1035
|
+
if (state.orphanStories.length === 0)
|
|
2025
1036
|
return;
|
|
2026
|
-
if (!confirm(`Delete ALL ${orphanStories.length} generated stories
|
|
1037
|
+
if (!confirm(`Delete ALL ${state.orphanStories.length} generated stories?`))
|
|
2027
1038
|
return;
|
|
2028
|
-
}
|
|
2029
|
-
setIsBulkDeleting(true);
|
|
1039
|
+
dispatch({ type: 'SET_BULK_DELETING', payload: true });
|
|
2030
1040
|
try {
|
|
2031
|
-
const response = await fetch(STORIES_API, {
|
|
2032
|
-
method: 'DELETE',
|
|
2033
|
-
});
|
|
1041
|
+
const response = await fetch(STORIES_API, { method: 'DELETE' });
|
|
2034
1042
|
if (response.ok) {
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
setSelectedStoryIds(new Set());
|
|
2038
|
-
console.log(`Cleared ${result.deleted || 'all'} stories`);
|
|
1043
|
+
dispatch({ type: 'SET_ORPHAN_STORIES', payload: [] });
|
|
1044
|
+
dispatch({ type: 'SET_SELECTED_STORY_IDS', payload: new Set() });
|
|
2039
1045
|
}
|
|
2040
1046
|
else {
|
|
2041
|
-
alert('Failed to clear stories.
|
|
1047
|
+
alert('Failed to clear stories.');
|
|
2042
1048
|
}
|
|
2043
1049
|
}
|
|
2044
|
-
catch
|
|
2045
|
-
|
|
2046
|
-
alert('Failed to clear stories. Please try again.');
|
|
1050
|
+
catch {
|
|
1051
|
+
alert('Failed to clear stories.');
|
|
2047
1052
|
}
|
|
2048
1053
|
finally {
|
|
2049
|
-
|
|
1054
|
+
dispatch({ type: 'SET_BULK_DELETING', payload: false });
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
const handleDeleteOrphan = async (storyId) => {
|
|
1058
|
+
try {
|
|
1059
|
+
const response = await fetch(`${STORIES_API}/${storyId}`, { method: 'DELETE' });
|
|
1060
|
+
if (response.ok) {
|
|
1061
|
+
dispatch({ type: 'SET_ORPHAN_STORIES', payload: state.orphanStories.filter(s => s.id !== storyId) });
|
|
1062
|
+
const newSet = new Set(state.selectedStoryIds);
|
|
1063
|
+
newSet.delete(storyId);
|
|
1064
|
+
dispatch({ type: 'SET_SELECTED_STORY_IDS', payload: newSet });
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
catch (err) {
|
|
1068
|
+
console.error('Error deleting orphan story:', err);
|
|
2050
1069
|
}
|
|
2051
1070
|
};
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
}, children: [_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }), _jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })] }), _jsx("span", { children: "New Chat" })] }), recentChats.length > 0 && (_jsx("div", { style: {
|
|
2068
|
-
color: '#64748b',
|
|
2069
|
-
fontSize: '12px',
|
|
2070
|
-
marginBottom: '8px',
|
|
2071
|
-
fontWeight: '500',
|
|
2072
|
-
textTransform: 'uppercase',
|
|
2073
|
-
letterSpacing: '0.05em',
|
|
2074
|
-
}, children: "Recent Chats" })), recentChats.map(chat => (_jsxs("div", { onClick: () => handleSelectChat(chat), style: {
|
|
2075
|
-
...STYLES.chatItem,
|
|
2076
|
-
...(activeChatId === chat.id ? STYLES.chatItemActive : {}),
|
|
2077
|
-
}, onMouseEnter: (e) => {
|
|
2078
|
-
if (activeChatId !== chat.id) {
|
|
2079
|
-
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.12)';
|
|
2080
|
-
}
|
|
2081
|
-
const deleteBtn = e.currentTarget.querySelector('.delete-btn');
|
|
2082
|
-
if (deleteBtn)
|
|
2083
|
-
deleteBtn.style.opacity = '1';
|
|
2084
|
-
}, onMouseLeave: (e) => {
|
|
2085
|
-
if (activeChatId !== chat.id) {
|
|
2086
|
-
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)';
|
|
2087
|
-
}
|
|
2088
|
-
const deleteBtn = e.currentTarget.querySelector('.delete-btn');
|
|
2089
|
-
if (deleteBtn)
|
|
2090
|
-
deleteBtn.style.opacity = '0';
|
|
2091
|
-
}, 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: _jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] }, chat.id))), orphanStories.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
|
|
2092
|
-
display: 'flex',
|
|
2093
|
-
alignItems: 'center',
|
|
2094
|
-
justifyContent: 'space-between',
|
|
2095
|
-
marginTop: '16px',
|
|
2096
|
-
marginBottom: '8px',
|
|
2097
|
-
}, children: _jsxs("div", { style: {
|
|
2098
|
-
display: 'flex',
|
|
2099
|
-
alignItems: 'center',
|
|
2100
|
-
gap: '8px',
|
|
2101
|
-
}, children: [_jsx("input", { type: "checkbox", checked: selectedStoryIds.size === orphanStories.length && orphanStories.length > 0, onChange: toggleSelectAll, style: {
|
|
2102
|
-
width: '14px',
|
|
2103
|
-
height: '14px',
|
|
2104
|
-
cursor: 'pointer',
|
|
2105
|
-
accentColor: '#3b82f6',
|
|
2106
|
-
}, title: selectedStoryIds.size === orphanStories.length ? 'Deselect all' : 'Select all' }), _jsxs("span", { style: {
|
|
2107
|
-
color: '#64748b',
|
|
2108
|
-
fontSize: '12px',
|
|
2109
|
-
fontWeight: '500',
|
|
2110
|
-
textTransform: 'uppercase',
|
|
2111
|
-
letterSpacing: '0.05em',
|
|
2112
|
-
}, children: ["Generated Files (", orphanStories.length, ")"] })] }) }), selectedStoryIds.size > 0 && (_jsx("div", { style: {
|
|
2113
|
-
display: 'flex',
|
|
2114
|
-
gap: '8px',
|
|
2115
|
-
marginBottom: '12px',
|
|
2116
|
-
}, children: _jsx("button", { onClick: handleBulkDelete, disabled: isBulkDeleting, style: {
|
|
2117
|
-
flex: 1,
|
|
2118
|
-
padding: '6px 10px',
|
|
2119
|
-
fontSize: '11px',
|
|
2120
|
-
fontWeight: '500',
|
|
2121
|
-
background: 'rgba(239, 68, 68, 0.15)',
|
|
2122
|
-
color: '#f87171',
|
|
2123
|
-
border: '1px solid rgba(239, 68, 68, 0.3)',
|
|
2124
|
-
borderRadius: '6px',
|
|
2125
|
-
cursor: isBulkDeleting ? 'not-allowed' : 'pointer',
|
|
2126
|
-
opacity: isBulkDeleting ? 0.6 : 1,
|
|
2127
|
-
transition: 'all 0.15s ease',
|
|
2128
|
-
}, onMouseEnter: (e) => {
|
|
2129
|
-
if (!isBulkDeleting) {
|
|
2130
|
-
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.25)';
|
|
2131
|
-
}
|
|
2132
|
-
}, onMouseLeave: (e) => {
|
|
2133
|
-
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.15)';
|
|
2134
|
-
}, children: isBulkDeleting ? 'Deleting...' : `Delete Selected (${selectedStoryIds.size})` }) })), _jsx("div", { style: {
|
|
2135
|
-
display: 'flex',
|
|
2136
|
-
gap: '8px',
|
|
2137
|
-
marginBottom: '12px',
|
|
2138
|
-
}, children: _jsx("button", { onClick: handleClearAll, disabled: isBulkDeleting || orphanStories.length === 0, style: {
|
|
2139
|
-
flex: 1,
|
|
2140
|
-
padding: '6px 10px',
|
|
2141
|
-
fontSize: '11px',
|
|
2142
|
-
fontWeight: '500',
|
|
2143
|
-
background: 'rgba(100, 116, 139, 0.15)',
|
|
2144
|
-
color: '#94a3b8',
|
|
2145
|
-
border: '1px solid rgba(100, 116, 139, 0.3)',
|
|
2146
|
-
borderRadius: '6px',
|
|
2147
|
-
cursor: (isBulkDeleting || orphanStories.length === 0) ? 'not-allowed' : 'pointer',
|
|
2148
|
-
opacity: (isBulkDeleting || orphanStories.length === 0) ? 0.6 : 1,
|
|
2149
|
-
transition: 'all 0.15s ease',
|
|
2150
|
-
}, onMouseEnter: (e) => {
|
|
2151
|
-
if (!isBulkDeleting && orphanStories.length > 0) {
|
|
2152
|
-
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.15)';
|
|
2153
|
-
e.currentTarget.style.color = '#f87171';
|
|
2154
|
-
e.currentTarget.style.borderColor = 'rgba(239, 68, 68, 0.3)';
|
|
2155
|
-
}
|
|
2156
|
-
}, onMouseLeave: (e) => {
|
|
2157
|
-
e.currentTarget.style.background = 'rgba(100, 116, 139, 0.15)';
|
|
2158
|
-
e.currentTarget.style.color = '#94a3b8';
|
|
2159
|
-
e.currentTarget.style.borderColor = 'rgba(100, 116, 139, 0.3)';
|
|
2160
|
-
}, children: "Clear All Stories" }) }), orphanStories.map(story => (_jsxs("div", { style: {
|
|
2161
|
-
...STYLES.chatItem,
|
|
2162
|
-
background: selectedStoryIds.has(story.id)
|
|
2163
|
-
? 'rgba(59, 130, 246, 0.15)'
|
|
2164
|
-
: 'rgba(251, 191, 36, 0.1)',
|
|
2165
|
-
borderLeft: selectedStoryIds.has(story.id)
|
|
2166
|
-
? '3px solid rgba(59, 130, 246, 0.5)'
|
|
2167
|
-
: '3px solid rgba(251, 191, 36, 0.5)',
|
|
2168
|
-
display: 'flex',
|
|
2169
|
-
alignItems: 'flex-start',
|
|
2170
|
-
gap: '8px',
|
|
2171
|
-
}, onMouseEnter: (e) => {
|
|
2172
|
-
if (!selectedStoryIds.has(story.id)) {
|
|
2173
|
-
e.currentTarget.style.background = 'rgba(251, 191, 36, 0.15)';
|
|
2174
|
-
}
|
|
2175
|
-
const deleteBtn = e.currentTarget.querySelector('.delete-orphan-btn');
|
|
2176
|
-
if (deleteBtn)
|
|
2177
|
-
deleteBtn.style.opacity = '1';
|
|
2178
|
-
}, onMouseLeave: (e) => {
|
|
2179
|
-
if (!selectedStoryIds.has(story.id)) {
|
|
2180
|
-
e.currentTarget.style.background = 'rgba(251, 191, 36, 0.1)';
|
|
2181
|
-
}
|
|
2182
|
-
const deleteBtn = e.currentTarget.querySelector('.delete-orphan-btn');
|
|
2183
|
-
if (deleteBtn)
|
|
2184
|
-
deleteBtn.style.opacity = '0';
|
|
2185
|
-
}, children: [_jsx("input", { type: "checkbox", checked: selectedStoryIds.has(story.id), onChange: () => toggleStorySelection(story.id), onClick: (e) => e.stopPropagation(), style: {
|
|
2186
|
-
width: '14px',
|
|
2187
|
-
height: '14px',
|
|
2188
|
-
cursor: 'pointer',
|
|
2189
|
-
accentColor: '#3b82f6',
|
|
2190
|
-
marginTop: '2px',
|
|
2191
|
-
flexShrink: 0,
|
|
2192
|
-
} }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: STYLES.chatItemTitle, children: story.title }), _jsx("div", { style: { ...STYLES.chatItemTime, fontSize: '11px' }, children: story.fileName })] }), _jsx("button", { className: "delete-orphan-btn", onClick: async (e) => {
|
|
2193
|
-
e.stopPropagation();
|
|
2194
|
-
try {
|
|
2195
|
-
const response = await fetch(`${STORIES_API}/${story.id}`, {
|
|
2196
|
-
method: 'DELETE',
|
|
2197
|
-
});
|
|
2198
|
-
if (response.ok) {
|
|
2199
|
-
setOrphanStories(prev => prev.filter(s => s.id !== story.id));
|
|
2200
|
-
setSelectedStoryIds(prev => {
|
|
2201
|
-
const newSet = new Set(prev);
|
|
2202
|
-
newSet.delete(story.id);
|
|
2203
|
-
return newSet;
|
|
2204
|
-
});
|
|
2205
|
-
}
|
|
2206
|
-
else {
|
|
2207
|
-
console.error('Failed to delete orphan story');
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
catch (err) {
|
|
2211
|
-
console.error('Error deleting orphan story:', err);
|
|
2212
|
-
}
|
|
2213
|
-
}, style: STYLES.deleteButton, title: "Delete generated file", children: _jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] }, story.id)))] }))] })), !sidebarOpen && (_jsx("div", { style: { padding: '8px', display: 'flex', justifyContent: 'center' }, children: _jsx("button", { onClick: () => setSidebarOpen(true), style: {
|
|
2214
|
-
...STYLES.sidebarToggle,
|
|
2215
|
-
width: '38px',
|
|
2216
|
-
height: '38px',
|
|
2217
|
-
padding: '0',
|
|
2218
|
-
fontSize: '16px',
|
|
2219
|
-
borderRadius: '8px',
|
|
2220
|
-
}, title: "Expand sidebar", onMouseEnter: (e) => {
|
|
2221
|
-
e.currentTarget.style.transform = 'scale(1.05)';
|
|
2222
|
-
e.currentTarget.style.background = '#2563eb';
|
|
2223
|
-
}, onMouseLeave: (e) => {
|
|
2224
|
-
e.currentTarget.style.transform = 'scale(1)';
|
|
2225
|
-
e.currentTarget.style.background = '#3b82f6';
|
|
2226
|
-
}, children: _jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "3", y1: "6", x2: "21", y2: "6" }), _jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" }), _jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18" })] }) }) }))] }), _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: {
|
|
2227
|
-
fontSize: '22px',
|
|
2228
|
-
margin: 0,
|
|
2229
|
-
fontWeight: '700',
|
|
2230
|
-
background: 'linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%)',
|
|
2231
|
-
WebkitBackgroundClip: 'text',
|
|
2232
|
-
WebkitTextFillColor: 'transparent',
|
|
2233
|
-
display: 'inline-block',
|
|
2234
|
-
letterSpacing: '-0.02em'
|
|
2235
|
-
}, children: "Story UI" }), _jsx("p", { style: { fontSize: '14px', margin: '6px 0 0 0', color: '#94a3b8', fontWeight: '500' }, children: "Generate Storybook stories with AI" }), _jsxs("div", { style: {
|
|
2236
|
-
display: 'flex',
|
|
2237
|
-
alignItems: 'center',
|
|
2238
|
-
gap: '6px',
|
|
2239
|
-
marginTop: '10px',
|
|
2240
|
-
fontSize: '11px'
|
|
2241
|
-
}, children: [_jsx("div", { style: {
|
|
2242
|
-
width: '6px',
|
|
2243
|
-
height: '6px',
|
|
2244
|
-
borderRadius: '50%',
|
|
2245
|
-
backgroundColor: connectionStatus.connected ? '#10b981' : '#f87171'
|
|
2246
|
-
} }), _jsx("span", { className: `story-ui-status ${connectionStatus.connected ? 'story-ui-status-connected' : 'story-ui-status-disconnected'}`, style: { color: connectionStatus.connected ? '#10b981' : '#ef4444', fontWeight: '400' }, children: connectionStatus.connected
|
|
2247
|
-
? `Connected to ${getConnectionDisplayText()}`
|
|
2248
|
-
: `Disconnected: ${connectionStatus.error || 'Server not running'}` })] }), connectionStatus.connected && availableProviders.length > 0 && (_jsxs("div", { style: {
|
|
2249
|
-
display: 'flex',
|
|
2250
|
-
gap: '12px',
|
|
2251
|
-
marginTop: '12px',
|
|
2252
|
-
flexWrap: 'wrap'
|
|
2253
|
-
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("label", { style: { fontSize: '12px', color: '#94a3b8', fontWeight: '500' }, children: "Provider:" }), _jsx("select", { value: selectedProvider, onChange: (e) => {
|
|
2254
|
-
const newProvider = e.target.value;
|
|
2255
|
-
setSelectedProvider(newProvider);
|
|
2256
|
-
// Reset model to first available for new provider
|
|
2257
|
-
const provider = availableProviders.find(p => p.type === newProvider);
|
|
2258
|
-
if (provider && provider.models.length > 0) {
|
|
2259
|
-
setSelectedModel(provider.models[0]);
|
|
2260
|
-
}
|
|
2261
|
-
}, style: {
|
|
2262
|
-
background: '#1e293b',
|
|
2263
|
-
border: '1px solid #334155',
|
|
2264
|
-
borderRadius: '6px',
|
|
2265
|
-
color: '#e2e8f0',
|
|
2266
|
-
padding: '4px 8px',
|
|
2267
|
-
fontSize: '12px',
|
|
2268
|
-
cursor: 'pointer'
|
|
2269
|
-
}, children: availableProviders.map(p => (_jsx("option", { value: p.type, children: p.name }, p.type))) })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("label", { style: { fontSize: '12px', color: '#94a3b8', fontWeight: '500' }, children: "Model:" }), _jsx("select", { value: selectedModel, onChange: (e) => setSelectedModel(e.target.value), style: {
|
|
2270
|
-
background: '#1e293b',
|
|
2271
|
-
border: '1px solid #334155',
|
|
2272
|
-
borderRadius: '6px',
|
|
2273
|
-
color: '#e2e8f0',
|
|
2274
|
-
padding: '4px 8px',
|
|
2275
|
-
fontSize: '12px',
|
|
2276
|
-
cursor: 'pointer',
|
|
2277
|
-
maxWidth: '200px'
|
|
2278
|
-
}, children: availableProviders
|
|
2279
|
-
.find(p => p.type === selectedProvider)
|
|
2280
|
-
?.models.map(model => (_jsx("option", { value: model, children: getModelDisplayName(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", { className: `story-ui-message ${msg.role === 'user' ? 'story-ui-user-message' : 'story-ui-ai-message'}`, 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.base64
|
|
2281
|
-
? `data:${img.mediaType || 'image/png'};base64,${img.base64}`
|
|
2282
|
-
: 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: _jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] }, img.id)))] })), _jsxs("form", { onSubmit: handleSend, style: {
|
|
2283
|
-
...STYLES.inputForm,
|
|
2284
|
-
...(attachedImages.length > 0 ? {
|
|
2285
|
-
marginTop: 0,
|
|
2286
|
-
borderTopLeftRadius: 0,
|
|
2287
|
-
borderTopRightRadius: 0,
|
|
2288
|
-
} : {})
|
|
2289
|
-
}, children: [_jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), disabled: loading || attachedImages.length >= MAX_IMAGES, style: {
|
|
2290
|
-
...STYLES.uploadButton,
|
|
2291
|
-
...(attachedImages.length >= MAX_IMAGES ? {
|
|
2292
|
-
opacity: 0.5,
|
|
2293
|
-
cursor: 'not-allowed',
|
|
2294
|
-
} : {})
|
|
2295
|
-
}, title: attachedImages.length >= MAX_IMAGES
|
|
2296
|
-
? `Maximum ${MAX_IMAGES} images`
|
|
2297
|
-
: 'Attach images (screenshots, designs)', onMouseEnter: (e) => {
|
|
2298
|
-
if (attachedImages.length < MAX_IMAGES && !loading) {
|
|
2299
|
-
e.currentTarget.style.background = 'rgba(59, 130, 246, 0.2)';
|
|
2300
|
-
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
|
|
2301
|
-
}
|
|
2302
|
-
}, onMouseLeave: (e) => {
|
|
2303
|
-
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
|
|
2304
|
-
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
|
|
2305
|
-
}, 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
|
|
2306
|
-
? "Describe what to create from these images..."
|
|
2307
|
-
: "Describe a UI component...", style: STYLES.textInput, onFocus: (e) => {
|
|
2308
|
-
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
|
|
2309
|
-
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.1)';
|
|
2310
|
-
}, onBlur: (e) => {
|
|
2311
|
-
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
|
|
2312
|
-
e.currentTarget.style.boxShadow = 'none';
|
|
2313
|
-
} }), _jsxs("button", { type: "submit", disabled: loading || (!input.trim() && attachedImages.length === 0), style: {
|
|
2314
|
-
...STYLES.sendButton,
|
|
2315
|
-
...(loading || (!input.trim() && attachedImages.length === 0) ? {
|
|
2316
|
-
opacity: 0.4,
|
|
2317
|
-
cursor: 'not-allowed',
|
|
2318
|
-
background: '#64748b',
|
|
2319
|
-
boxShadow: 'none'
|
|
2320
|
-
} : {})
|
|
2321
|
-
}, onMouseEnter: (e) => {
|
|
2322
|
-
if (!loading && (input.trim() || attachedImages.length > 0)) {
|
|
2323
|
-
e.currentTarget.style.background = '#2563eb';
|
|
2324
|
-
e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.5)';
|
|
2325
|
-
}
|
|
2326
|
-
}, onMouseLeave: (e) => {
|
|
2327
|
-
if (!loading && (input.trim() || attachedImages.length > 0)) {
|
|
2328
|
-
e.currentTarget.style.background = '#3b82f6';
|
|
2329
|
-
e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.35)';
|
|
2330
|
-
}
|
|
2331
|
-
}, children: [_jsx("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) }), _jsx("span", { children: "Send" })] })] })] })] }));
|
|
1071
|
+
// ============================================
|
|
1072
|
+
// Render
|
|
1073
|
+
// ============================================
|
|
1074
|
+
return (_jsxs("div", { className: `sui-root ${state.isDarkMode ? 'dark' : ''}`, children: [_jsxs("aside", { className: `sui-sidebar ${state.sidebarOpen ? '' : 'collapsed'}`, "aria-label": "Chat history", children: [state.sidebarOpen && (_jsxs("div", { className: "sui-sidebar-content", children: [_jsxs("button", { className: "sui-button sui-button-ghost", onClick: () => dispatch({ type: 'TOGGLE_SIDEBAR' }), style: { width: '100%', marginBottom: '12px', justifyContent: 'flex-start' }, children: [Icons.panelLeft, _jsx("span", { style: { marginLeft: '8px' }, children: "Hide sidebar" })] }), _jsxs("button", { className: "sui-button sui-button-default", onClick: handleNewChat, style: { width: '100%', marginBottom: '16px' }, children: [Icons.plus, _jsx("span", { children: "New Chat" })] }), _jsx("div", { className: "sui-sidebar-chats", children: state.recentChats.map(chat => (_jsx("div", { className: `sui-chat-item ${state.activeChatId === chat.id ? 'active' : ''} ${contextMenuId === chat.id ? 'menu-open' : ''}`, onClick: () => renamingChatId !== chat.id && handleSelectChat(chat), role: "button", tabIndex: 0, onKeyDown: e => e.key === 'Enter' && renamingChatId !== chat.id && handleSelectChat(chat), children: renamingChatId === chat.id ? (_jsxs("div", { className: "sui-chat-item-rename", children: [_jsx("input", { type: "text", className: "sui-rename-input", value: renameValue, onChange: e => setRenameValue(e.target.value), onKeyDown: e => {
|
|
1075
|
+
if (e.key === 'Enter')
|
|
1076
|
+
handleConfirmRename(chat.id);
|
|
1077
|
+
if (e.key === 'Escape')
|
|
1078
|
+
handleCancelRename();
|
|
1079
|
+
}, onClick: e => e.stopPropagation(), autoFocus: true }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleConfirmRename(chat.id); }, "aria-label": "Save", children: Icons.check }), _jsx("button", { className: "sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); handleCancelRename(); }, "aria-label": "Cancel", children: Icons.x })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "sui-chat-item-title", children: chat.title }), _jsxs("div", { className: "sui-chat-item-actions", children: [_jsx("button", { className: "sui-chat-item-menu sui-button sui-button-icon sui-button-sm", onClick: e => { e.stopPropagation(); setContextMenuId(contextMenuId === chat.id ? null : chat.id); }, "aria-label": "More options", children: Icons.moreVertical }), contextMenuId === chat.id && (_jsxs("div", { className: "sui-context-menu", children: [_jsxs("button", { className: "sui-context-menu-item", onClick: e => handleStartRename(chat.id, chat.title, e), children: [Icons.pencil, _jsx("span", { children: "Rename" })] }), _jsxs("button", { className: "sui-context-menu-item sui-context-menu-item-danger", onClick: e => handleDeleteChat(chat.id, e), children: [Icons.trash, _jsx("span", { children: "Delete" })] })] }))] })] })) }, chat.id))) })] })), !state.sidebarOpen && (_jsx("div", { style: { padding: '12px', display: 'flex', justifyContent: 'center' }, children: _jsx("button", { className: "sui-button sui-button-ghost sui-button-icon", onClick: () => dispatch({ type: 'SET_SIDEBAR', payload: true }), "aria-label": "Show sidebar", children: Icons.panelLeft }) }))] }), _jsxs("main", { className: "sui-main", onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [state.isDragging && (_jsx("div", { className: "sui-drop-overlay", children: _jsxs("div", { className: "sui-drop-overlay-text", children: [Icons.image, _jsx("span", { children: "Drop images here" })] }) })), _jsxs("header", { className: "sui-header", children: [_jsxs("div", { className: "sui-header-left", children: [_jsx("span", { className: "sui-header-title", children: "Story UI" }), _jsxs(Badge, { variant: state.connectionStatus.connected ? 'success' : 'destructive', children: [_jsx("span", { className: "sui-badge-dot" }), state.connectionStatus.connected ? getConnectionDisplayText() : 'Disconnected'] })] }), _jsx("div", { className: "sui-header-right", children: state.connectionStatus.connected && state.availableProviders.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: state.availableProviders.find(p => p.type === state.selectedProvider)?.name || 'Provider' }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedProvider, onChange: e => {
|
|
1080
|
+
const newProvider = e.target.value;
|
|
1081
|
+
dispatch({ type: 'SET_SELECTED_PROVIDER', payload: newProvider });
|
|
1082
|
+
const provider = state.availableProviders.find(p => p.type === newProvider);
|
|
1083
|
+
if (provider?.models.length)
|
|
1084
|
+
dispatch({ type: 'SET_SELECTED_MODEL', payload: provider.models[0] });
|
|
1085
|
+
}, "aria-label": "Select provider", children: state.availableProviders.map(p => _jsx("option", { value: p.type, children: p.name }, p.type)) })] }), _jsxs("div", { className: "sui-select", children: [_jsxs("div", { className: "sui-select-trigger", children: [_jsx("span", { children: getModelDisplayName(state.selectedModel) }), Icons.chevronDown] }), _jsx("select", { className: "sui-select-native", value: state.selectedModel, onChange: e => dispatch({ type: 'SET_SELECTED_MODEL', payload: e.target.value }), "aria-label": "Select model", children: state.availableProviders.find(p => p.type === state.selectedProvider)?.models.map(model => (_jsx("option", { value: model, children: getModelDisplayName(model) }, model))) })] })] })) })] }), _jsxs("section", { className: "sui-chat-area", role: "log", "aria-live": "polite", children: [state.error && _jsx("div", { className: "sui-error", role: "alert", style: { margin: '24px' }, children: state.error }), state.conversation.length === 0 && !state.loading ? (_jsxs("div", { className: "sui-welcome", children: [_jsx("h2", { className: "sui-welcome-greeting", children: "What would you like to create?" }), _jsx("p", { className: "sui-welcome-subtitle", children: "Describe any UI component and I'll generate a Storybook story" }), _jsxs("div", { className: "sui-welcome-chips", children: [_jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a responsive card with image, title, and description' }), children: "Card" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a navigation bar with logo and menu links' }), children: "Navbar" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a form with input fields and validation' }), children: "Form" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a hero section with headline and call-to-action' }), children: "Hero" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a button group with primary and secondary actions' }), children: "Buttons" }), _jsx("button", { className: "sui-chip", onClick: () => dispatch({ type: 'SET_INPUT', payload: 'Create a modal dialog with header, content, and footer' }), children: "Modal" })] })] })) : (_jsxs("div", { className: "sui-chat-messages", children: [state.conversation.map((msg, i) => (_jsx("article", { className: `sui-message ${msg.role === 'user' ? 'sui-message-user' : 'sui-message-ai'}`, children: _jsxs("div", { className: "sui-message-bubble", children: [msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content, msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (_jsx("div", { className: "sui-message-images", children: msg.attachedImages.map(img => (_jsx("img", { src: img.base64 ? `data:${img.mediaType};base64,${img.base64}` : img.preview, alt: "attached", className: "sui-message-image" }, img.id))) }))] }) }, i))), state.loading && (_jsx("div", { className: "sui-message sui-message-ai", children: state.streamingState ? _jsx(ProgressIndicator, { streamingState: state.streamingState }) : (_jsx("div", { className: "sui-progress", children: _jsxs("span", { className: "sui-progress-label", children: ["Generating story", _jsx("span", { className: "sui-loading" })] }) })) })), _jsx("div", { ref: chatEndRef })] }))] }), _jsx("div", { className: "sui-input-area", children: _jsxs("div", { className: "sui-input-container", children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: 'none' }, onChange: handleFileSelect }), state.attachedImages.length > 0 && (_jsxs("div", { className: "sui-image-previews", children: [_jsxs("span", { className: "sui-image-preview-label", children: [Icons.image, " ", state.attachedImages.length, " image", state.attachedImages.length > 1 ? 's' : ''] }), state.attachedImages.map(img => (_jsxs("div", { className: "sui-image-preview-item", children: [_jsx("img", { src: img.preview, alt: "preview", className: "sui-image-preview-thumb" }), _jsx("button", { className: "sui-image-preview-remove", onClick: () => removeAttachedImage(img.id), "aria-label": "Remove", children: Icons.x })] }, img.id)))] })), _jsxs("form", { onSubmit: handleSend, className: "sui-input-form", style: state.attachedImages.length > 0 ? { borderTopLeftRadius: 0, borderTopRightRadius: 0 } : undefined, children: [_jsx("button", { type: "button", className: "sui-input-form-upload", onClick: () => fileInputRef.current?.click(), disabled: state.loading || state.attachedImages.length >= MAX_IMAGES, "aria-label": "Attach images", children: Icons.image }), _jsx("input", { ref: inputRef, type: "text", className: "sui-input-form-field", value: state.input, onChange: e => dispatch({ type: 'SET_INPUT', payload: e.target.value }), onPaste: handlePaste, placeholder: state.attachedImages.length > 0 ? 'Describe what to create from these images...' : 'Describe a UI component...' }), _jsx("button", { type: "submit", className: "sui-input-form-send", disabled: state.loading || (!state.input.trim() && state.attachedImages.length === 0), "aria-label": "Send", children: Icons.send })] })] }) })] })] }));
|
|
2332
1086
|
}
|
|
2333
1087
|
export default StoryUIPanel;
|
|
2334
1088
|
export { StoryUIPanel };
|