@tpitre/story-ui 3.8.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/cli/index.js +22 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +28 -2
- package/dist/cli/update.d.ts +29 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/cli/update.js +398 -0
- package/dist/mcp-server/index.js +81 -0
- 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 +788 -1862
- package/package.json +1 -1
- package/templates/StoryUI/StoryUIPanel.css +1440 -0
- package/templates/StoryUI/StoryUIPanel.tsx +1259 -2563
|
@@ -1,1282 +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 })] }));
|
|
1219
396
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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" }) }))] }));
|
|
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"] })] }))] }));
|
|
399
|
+
}
|
|
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([]);
|
|
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('');
|
|
1244
407
|
const chatEndRef = useRef(null);
|
|
1245
408
|
const inputRef = useRef(null);
|
|
1246
409
|
const fileInputRef = useRef(null);
|
|
1247
410
|
const abortControllerRef = useRef(null);
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
|
1252
571
|
const fileToBase64 = (file) => {
|
|
1253
572
|
return new Promise((resolve, reject) => {
|
|
1254
573
|
const reader = new FileReader();
|
|
1255
574
|
reader.readAsDataURL(file);
|
|
1256
575
|
reader.onload = () => {
|
|
1257
576
|
const result = reader.result;
|
|
1258
|
-
|
|
1259
|
-
const base64 = result.split(',')[1];
|
|
1260
|
-
resolve(base64);
|
|
577
|
+
resolve(result.split(',')[1]);
|
|
1261
578
|
};
|
|
1262
579
|
reader.onerror = error => reject(error);
|
|
1263
580
|
});
|
|
1264
581
|
};
|
|
1265
|
-
// Handle file selection
|
|
1266
582
|
const handleFileSelect = async (e) => {
|
|
1267
583
|
const files = e.target.files;
|
|
1268
584
|
if (!files)
|
|
1269
585
|
return;
|
|
1270
|
-
const newImages = [];
|
|
1271
586
|
const errors = [];
|
|
1272
|
-
for (let i = 0; i < files.length && (attachedImages.length +
|
|
587
|
+
for (let i = 0; i < files.length && (state.attachedImages.length + i) < MAX_IMAGES; i++) {
|
|
1273
588
|
const file = files[i];
|
|
1274
|
-
// Validate file type
|
|
1275
589
|
if (!file.type.startsWith('image/')) {
|
|
1276
590
|
errors.push(`${file.name}: Not an image file`);
|
|
1277
591
|
continue;
|
|
1278
592
|
}
|
|
1279
|
-
// Validate file size
|
|
1280
593
|
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1281
594
|
errors.push(`${file.name}: File too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
|
|
1282
595
|
continue;
|
|
@@ -1284,59 +597,42 @@ function StoryUIPanel() {
|
|
|
1284
597
|
try {
|
|
1285
598
|
const base64 = await fileToBase64(file);
|
|
1286
599
|
const preview = URL.createObjectURL(file);
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
file,
|
|
1290
|
-
preview,
|
|
1291
|
-
base64,
|
|
1292
|
-
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' },
|
|
1293
603
|
});
|
|
1294
604
|
}
|
|
1295
|
-
catch
|
|
605
|
+
catch {
|
|
1296
606
|
errors.push(`${file.name}: Failed to process`);
|
|
1297
607
|
}
|
|
1298
608
|
}
|
|
1299
|
-
if (errors.length > 0)
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
setAttachedImages(prev => [...prev, ...newImages]);
|
|
1303
|
-
// Reset file input
|
|
1304
|
-
if (fileInputRef.current) {
|
|
609
|
+
if (errors.length > 0)
|
|
610
|
+
dispatch({ type: 'SET_ERROR', payload: errors.join('\n') });
|
|
611
|
+
if (fileInputRef.current)
|
|
1305
612
|
fileInputRef.current.value = '';
|
|
1306
|
-
}
|
|
1307
613
|
};
|
|
1308
|
-
// Remove attached image
|
|
1309
614
|
const removeAttachedImage = (id) => {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
}
|
|
1315
|
-
return prev.filter(img => img.id !== id);
|
|
1316
|
-
});
|
|
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 });
|
|
1317
619
|
};
|
|
1318
|
-
// Clear all attached images
|
|
1319
620
|
const clearAttachedImages = () => {
|
|
1320
|
-
attachedImages.forEach(img => URL.revokeObjectURL(img.preview));
|
|
1321
|
-
|
|
621
|
+
state.attachedImages.forEach(img => URL.revokeObjectURL(img.preview));
|
|
622
|
+
dispatch({ type: 'CLEAR_ATTACHED_IMAGES' });
|
|
1322
623
|
};
|
|
1323
|
-
// Drag and drop
|
|
1324
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
1325
|
-
// Handle drag events
|
|
624
|
+
// Drag and drop handlers
|
|
1326
625
|
const handleDragEnter = useCallback((e) => {
|
|
1327
626
|
e.preventDefault();
|
|
1328
627
|
e.stopPropagation();
|
|
1329
|
-
if (e.dataTransfer.types.includes('Files'))
|
|
1330
|
-
|
|
1331
|
-
}
|
|
628
|
+
if (e.dataTransfer.types.includes('Files'))
|
|
629
|
+
dispatch({ type: 'SET_DRAGGING', payload: true });
|
|
1332
630
|
}, []);
|
|
1333
631
|
const handleDragLeave = useCallback((e) => {
|
|
1334
632
|
e.preventDefault();
|
|
1335
633
|
e.stopPropagation();
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
setIsDragging(false);
|
|
1339
|
-
}
|
|
634
|
+
if (!e.currentTarget.contains(e.relatedTarget))
|
|
635
|
+
dispatch({ type: 'SET_DRAGGING', payload: false });
|
|
1340
636
|
}, []);
|
|
1341
637
|
const handleDragOver = useCallback((e) => {
|
|
1342
638
|
e.preventDefault();
|
|
@@ -1345,298 +641,132 @@ function StoryUIPanel() {
|
|
|
1345
641
|
const handleDrop = useCallback(async (e) => {
|
|
1346
642
|
e.preventDefault();
|
|
1347
643
|
e.stopPropagation();
|
|
1348
|
-
|
|
644
|
+
dispatch({ type: 'SET_DRAGGING', payload: false });
|
|
1349
645
|
const files = Array.from(e.dataTransfer.files);
|
|
1350
646
|
const imageFiles = files.filter(f => f.type.startsWith('image/'));
|
|
1351
647
|
if (imageFiles.length === 0) {
|
|
1352
|
-
|
|
648
|
+
dispatch({ type: 'SET_ERROR', payload: 'Please drop image files only' });
|
|
1353
649
|
return;
|
|
1354
650
|
}
|
|
1355
|
-
const newImages = [];
|
|
1356
651
|
const errors = [];
|
|
1357
|
-
for (let i = 0; i < imageFiles.length && (attachedImages.length +
|
|
652
|
+
for (let i = 0; i < imageFiles.length && (state.attachedImages.length + i) < MAX_IMAGES; i++) {
|
|
1358
653
|
const file = imageFiles[i];
|
|
1359
654
|
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1360
|
-
errors.push(`${file.name}: File too large
|
|
655
|
+
errors.push(`${file.name}: File too large`);
|
|
1361
656
|
continue;
|
|
1362
657
|
}
|
|
1363
658
|
try {
|
|
1364
659
|
const base64 = await fileToBase64(file);
|
|
1365
660
|
const preview = URL.createObjectURL(file);
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
file,
|
|
1369
|
-
preview,
|
|
1370
|
-
base64,
|
|
1371
|
-
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' },
|
|
1372
664
|
});
|
|
1373
665
|
}
|
|
1374
|
-
catch
|
|
666
|
+
catch {
|
|
1375
667
|
errors.push(`${file.name}: Failed to process`);
|
|
1376
668
|
}
|
|
1377
669
|
}
|
|
1378
|
-
if (errors.length > 0)
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}, [attachedImages.length, fileToBase64]);
|
|
1383
|
-
// 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
|
|
1384
674
|
const handlePaste = useCallback(async (e) => {
|
|
1385
675
|
const items = e.clipboardData?.items;
|
|
1386
676
|
if (!items)
|
|
1387
677
|
return;
|
|
1388
678
|
const imageItems = [];
|
|
1389
679
|
for (let i = 0; i < items.length; i++) {
|
|
1390
|
-
if (items[i].type.startsWith('image/'))
|
|
680
|
+
if (items[i].type.startsWith('image/'))
|
|
1391
681
|
imageItems.push(items[i]);
|
|
1392
|
-
}
|
|
1393
682
|
}
|
|
1394
683
|
if (imageItems.length === 0)
|
|
1395
684
|
return;
|
|
1396
|
-
// Prevent default text paste behavior when pasting images
|
|
1397
685
|
e.preventDefault();
|
|
1398
|
-
if (attachedImages.length >= MAX_IMAGES) {
|
|
1399
|
-
|
|
686
|
+
if (state.attachedImages.length >= MAX_IMAGES) {
|
|
687
|
+
dispatch({ type: 'SET_ERROR', payload: `Maximum ${MAX_IMAGES} images allowed` });
|
|
1400
688
|
return;
|
|
1401
689
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
const item = imageItems[i];
|
|
1406
|
-
const file = item.getAsFile();
|
|
1407
|
-
if (!file) {
|
|
1408
|
-
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)
|
|
1409
693
|
continue;
|
|
1410
|
-
}
|
|
1411
|
-
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
|
|
1412
|
-
errors.push(`Pasted image too large (max ${MAX_IMAGE_SIZE_MB}MB)`);
|
|
1413
|
-
continue;
|
|
1414
|
-
}
|
|
1415
694
|
try {
|
|
1416
695
|
const base64 = await fileToBase64(file);
|
|
1417
696
|
const preview = URL.createObjectURL(file);
|
|
1418
|
-
// Create a meaningful name for pasted images
|
|
1419
697
|
const timestamp = new Date().toISOString().slice(11, 19).replace(/:/g, '-');
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
+
},
|
|
1427
707
|
});
|
|
1428
708
|
}
|
|
1429
|
-
catch
|
|
1430
|
-
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
if (errors.length > 0) {
|
|
1434
|
-
setError(errors.join('\n'));
|
|
1435
|
-
}
|
|
1436
|
-
if (newImages.length > 0) {
|
|
1437
|
-
setAttachedImages(prev => [...prev, ...newImages]);
|
|
1438
|
-
// Clear any existing error on successful paste
|
|
1439
|
-
if (errors.length === 0) {
|
|
1440
|
-
setError(null);
|
|
709
|
+
catch {
|
|
710
|
+
dispatch({ type: 'SET_ERROR', payload: 'Failed to process pasted image' });
|
|
1441
711
|
}
|
|
1442
712
|
}
|
|
1443
|
-
}, [attachedImages.length
|
|
1444
|
-
//
|
|
1445
|
-
useEffect(() => {
|
|
1446
|
-
const initializeChats = async () => {
|
|
1447
|
-
// Test connection first
|
|
1448
|
-
const connectionTest = await testMCPConnection();
|
|
1449
|
-
setConnectionStatus(connectionTest);
|
|
1450
|
-
if (connectionTest.connected) {
|
|
1451
|
-
// Fetch available providers
|
|
1452
|
-
try {
|
|
1453
|
-
const providersRes = await fetch(PROVIDERS_API);
|
|
1454
|
-
if (providersRes.ok) {
|
|
1455
|
-
const providersData = await providersRes.json();
|
|
1456
|
-
setAvailableProviders(providersData.providers.filter(p => p.configured));
|
|
1457
|
-
// Set initial selection from server defaults
|
|
1458
|
-
if (providersData.current) {
|
|
1459
|
-
setSelectedProvider(providersData.current.provider.toLowerCase());
|
|
1460
|
-
setSelectedModel(providersData.current.model);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
catch (e) {
|
|
1465
|
-
console.error('Failed to fetch providers:', e);
|
|
1466
|
-
}
|
|
1467
|
-
// Fetch design system considerations for environment parity
|
|
1468
|
-
// This ensures production gets the same considerations as local development
|
|
1469
|
-
try {
|
|
1470
|
-
const considerationsRes = await fetch(CONSIDERATIONS_API);
|
|
1471
|
-
if (considerationsRes.ok) {
|
|
1472
|
-
const considerationsData = await considerationsRes.json();
|
|
1473
|
-
if (considerationsData.hasConsiderations && considerationsData.considerations) {
|
|
1474
|
-
setConsiderations(considerationsData.considerations);
|
|
1475
|
-
console.log(`Loaded considerations from ${considerationsData.source}`);
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
catch (e) {
|
|
1480
|
-
console.error('Failed to fetch considerations:', e);
|
|
1481
|
-
}
|
|
1482
|
-
const syncedChats = await syncWithActualStories();
|
|
1483
|
-
const sortedChats = syncedChats.sort((a, b) => b.lastUpdated - a.lastUpdated).slice(0, MAX_RECENT_CHATS);
|
|
1484
|
-
setRecentChats(sortedChats);
|
|
1485
|
-
if (sortedChats.length > 0) {
|
|
1486
|
-
setConversation(sortedChats[0].conversation);
|
|
1487
|
-
setActiveChatId(sortedChats[0].id);
|
|
1488
|
-
setActiveTitle(sortedChats[0].title);
|
|
1489
|
-
}
|
|
1490
|
-
// Fetch orphan stories (stories on disk without chat history)
|
|
1491
|
-
const orphans = await fetchOrphanStories();
|
|
1492
|
-
setOrphanStories(orphans);
|
|
1493
|
-
}
|
|
1494
|
-
else {
|
|
1495
|
-
// Load from local storage if server is not available
|
|
1496
|
-
const localChats = loadChats();
|
|
1497
|
-
setRecentChats(localChats);
|
|
1498
|
-
}
|
|
1499
|
-
};
|
|
1500
|
-
initializeChats();
|
|
1501
|
-
}, []);
|
|
1502
|
-
// Scroll to bottom on new message
|
|
1503
|
-
useEffect(() => {
|
|
1504
|
-
if (chatEndRef.current) {
|
|
1505
|
-
chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
1506
|
-
}
|
|
1507
|
-
}, [conversation, loading]);
|
|
1508
|
-
// Helper function for non-streaming fallback
|
|
1509
|
-
const handleSendNonStreaming = async (userInput, newConversation) => {
|
|
1510
|
-
const res = await fetch(MCP_API, {
|
|
1511
|
-
method: 'POST',
|
|
1512
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1513
|
-
body: JSON.stringify({
|
|
1514
|
-
prompt: userInput,
|
|
1515
|
-
conversation: newConversation,
|
|
1516
|
-
fileName: activeChatId || undefined,
|
|
1517
|
-
provider: selectedProvider || undefined,
|
|
1518
|
-
model: selectedModel || undefined,
|
|
1519
|
-
considerations: considerations || undefined,
|
|
1520
|
-
}),
|
|
1521
|
-
});
|
|
1522
|
-
const contentType = res.headers.get('content-type');
|
|
1523
|
-
if (!contentType || !contentType.includes('application/json')) {
|
|
1524
|
-
const text = await res.text();
|
|
1525
|
-
throw new Error(`Server returned non-JSON response. Response: ${text.substring(0, 200)}...`);
|
|
1526
|
-
}
|
|
1527
|
-
const data = await res.json();
|
|
1528
|
-
if (!res.ok || !data.success)
|
|
1529
|
-
throw new Error(data.error || 'Story generation failed');
|
|
1530
|
-
return data;
|
|
1531
|
-
};
|
|
1532
|
-
// Helper function to build a conversational response from completion data
|
|
1533
|
-
// Uses special markers [SUCCESS], [ERROR], [TIP], [WRENCH] that renderMarkdown converts to icons
|
|
1534
|
-
// Track whether we've shown the refresh hint in this session
|
|
1535
|
-
const hasShownRefreshHint = useRef(false);
|
|
713
|
+
}, [state.attachedImages.length]);
|
|
714
|
+
// Build response message
|
|
1536
715
|
const buildConversationalResponse = (completion, isUpdate) => {
|
|
1537
716
|
const parts = [];
|
|
1538
717
|
const statusMarker = completion.success ? '[SUCCESS]' : '[ERROR]';
|
|
1539
|
-
|
|
1540
|
-
if (isUpdate) {
|
|
1541
|
-
parts.push(`${statusMarker} **Updated: "${completion.title}"**`);
|
|
1542
|
-
}
|
|
1543
|
-
else {
|
|
1544
|
-
parts.push(`${statusMarker} **Created: "${completion.title}"**`);
|
|
1545
|
-
}
|
|
1546
|
-
// Build component insights with reasons when available
|
|
718
|
+
parts.push(isUpdate ? `${statusMarker} **Updated: "${completion.title}"**` : `${statusMarker} **Created: "${completion.title}"**`);
|
|
1547
719
|
const componentCount = completion.componentsUsed?.length || 0;
|
|
1548
720
|
if (componentCount > 0) {
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1551
|
-
const componentsWithReasons = componentList.filter(c => c.reason && c.reason !== 'Used in composition');
|
|
1552
|
-
if (componentsWithReasons.length > 0) {
|
|
1553
|
-
// Show components with their reasons
|
|
1554
|
-
const insights = componentsWithReasons
|
|
1555
|
-
.slice(0, 3)
|
|
1556
|
-
.map(c => `\`${c.name}\` - ${c.reason?.toLowerCase()}`)
|
|
1557
|
-
.join(', ');
|
|
1558
|
-
parts.push(`\nUsed ${insights}${componentCount > 3 ? ` and ${componentCount - 3} more` : ''}.`);
|
|
1559
|
-
}
|
|
1560
|
-
else {
|
|
1561
|
-
// Fallback to simple list
|
|
1562
|
-
const names = componentList.map(c => `\`${c.name}\``).join(', ');
|
|
1563
|
-
parts.push(`\nBuilt with ${names}${componentCount > 5 ? '...' : ''}.`);
|
|
1564
|
-
}
|
|
721
|
+
const names = completion.componentsUsed.slice(0, 5).map(c => `\`${c.name}\``).join(', ');
|
|
722
|
+
parts.push(`\nBuilt with ${names}${componentCount > 5 ? '...' : ''}.`);
|
|
1565
723
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
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}.`);
|
|
1570
727
|
}
|
|
1571
|
-
// Add style choices only if they add value
|
|
1572
|
-
if (completion.styleChoices && completion.styleChoices.length > 0) {
|
|
1573
|
-
const notableStyles = completion.styleChoices.filter(s => s.reason && s.reason !== 'Semantic color from design system');
|
|
1574
|
-
if (notableStyles.length > 0) {
|
|
1575
|
-
const styleInfo = notableStyles[0];
|
|
1576
|
-
parts.push(` Applied \`${styleInfo.value}\` for ${styleInfo.reason?.toLowerCase() || 'visual consistency'}.`);
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
// Add validation fixes notice
|
|
1580
728
|
if (completion.validation?.autoFixApplied) {
|
|
1581
729
|
parts.push(`\n\n[WRENCH] **Auto-fixed:** Minor syntax issues were automatically corrected.`);
|
|
1582
730
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
const suggestion = completion.suggestions[0];
|
|
1586
|
-
// Only show if it's not the generic "review the generated code" message
|
|
1587
|
-
if (!suggestion.toLowerCase().includes('review the generated code')) {
|
|
1588
|
-
parts.push(`\n\n[TIP] **Tip:** ${suggestion}`);
|
|
1589
|
-
}
|
|
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]}`);
|
|
1590
733
|
}
|
|
1591
|
-
// Show refresh hint only once per session for new stories (local mode only)
|
|
1592
|
-
// In Edge mode, stories are stored in Durable Objects, not on filesystem
|
|
1593
734
|
if (!isUpdate && !hasShownRefreshHint.current) {
|
|
1594
|
-
|
|
1595
|
-
parts.push(`\n\n_Story saved to cloud. View code in chat history recent chats navigation._`);
|
|
1596
|
-
}
|
|
1597
|
-
else {
|
|
1598
|
-
parts.push(`\n\n_Might need toefresh Storybook (Cmd/Ctrl + R) to see new stories in the sidebar._`);
|
|
1599
|
-
}
|
|
735
|
+
parts.push(isEdgeMode() ? `\n\n_Story saved to cloud._` : `\n\n_Might need to refresh Storybook (Cmd/Ctrl + R) to see new stories._`);
|
|
1600
736
|
hasShownRefreshHint.current = true;
|
|
1601
737
|
}
|
|
1602
|
-
// Add metrics in a subtle way (if available)
|
|
1603
738
|
if (completion.metrics?.totalTimeMs) {
|
|
1604
|
-
|
|
1605
|
-
parts.push(`\n\n_${seconds}s_`);
|
|
739
|
+
parts.push(`\n\n_${(completion.metrics.totalTimeMs / 1000).toFixed(1)}s_`);
|
|
1606
740
|
}
|
|
1607
741
|
return parts.join('');
|
|
1608
742
|
};
|
|
1609
|
-
//
|
|
743
|
+
// Finalize streaming
|
|
1610
744
|
const finalizeStreamingConversation = useCallback((newConversation, completion, userInput) => {
|
|
1611
|
-
// Build conversational response using rich completion data
|
|
1612
745
|
const isUpdate = completion.summary.action === 'updated';
|
|
1613
746
|
const responseMessage = buildConversationalResponse(completion, isUpdate);
|
|
1614
747
|
const aiMsg = { role: 'ai', content: responseMessage };
|
|
1615
748
|
const updatedConversation = [...newConversation, aiMsg];
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
if (isExistingSession && activeChatId) {
|
|
749
|
+
dispatch({ type: 'SET_CONVERSATION', payload: updatedConversation });
|
|
750
|
+
const isExistingSession = state.activeChatId && state.conversation.length > 0;
|
|
751
|
+
if (isExistingSession && state.activeChatId) {
|
|
1620
752
|
const updatedSession = {
|
|
1621
|
-
id: activeChatId,
|
|
1622
|
-
title: activeTitle,
|
|
1623
|
-
fileName: completion.fileName || activeChatId,
|
|
753
|
+
id: state.activeChatId,
|
|
754
|
+
title: state.activeTitle,
|
|
755
|
+
fileName: completion.fileName || state.activeChatId,
|
|
1624
756
|
conversation: updatedConversation,
|
|
1625
757
|
lastUpdated: Date.now(),
|
|
1626
758
|
};
|
|
1627
759
|
const chats = loadChats();
|
|
1628
|
-
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
1629
|
-
if (chatIndex !== -1)
|
|
760
|
+
const chatIndex = chats.findIndex(c => c.id === state.activeChatId);
|
|
761
|
+
if (chatIndex !== -1)
|
|
1630
762
|
chats[chatIndex] = updatedSession;
|
|
1631
|
-
}
|
|
1632
763
|
saveChats(chats);
|
|
1633
|
-
|
|
764
|
+
dispatch({ type: 'SET_RECENT_CHATS', payload: chats });
|
|
1634
765
|
}
|
|
1635
766
|
else {
|
|
1636
767
|
const chatId = completion.storyId || completion.fileName || Date.now().toString();
|
|
1637
768
|
const chatTitle = completion.title || userInput;
|
|
1638
|
-
|
|
1639
|
-
setActiveTitle(chatTitle);
|
|
769
|
+
dispatch({ type: 'SET_ACTIVE_CHAT', payload: { id: chatId, title: chatTitle } });
|
|
1640
770
|
const newSession = {
|
|
1641
771
|
id: chatId,
|
|
1642
772
|
title: chatTitle,
|
|
@@ -1646,66 +776,48 @@ function StoryUIPanel() {
|
|
|
1646
776
|
};
|
|
1647
777
|
const chats = loadChats().filter(c => c.id !== chatId);
|
|
1648
778
|
chats.unshift(newSession);
|
|
1649
|
-
if (chats.length > MAX_RECENT_CHATS)
|
|
779
|
+
if (chats.length > MAX_RECENT_CHATS)
|
|
1650
780
|
chats.splice(MAX_RECENT_CHATS);
|
|
1651
|
-
}
|
|
1652
781
|
saveChats(chats);
|
|
1653
|
-
|
|
1654
|
-
// Auto-navigate to the newly created story after HMR processes the file
|
|
1655
|
-
// This prevents the "Couldn't find story after HMR" error by refreshing
|
|
1656
|
-
// after the file system has been updated and HMR has processed the change
|
|
1657
|
-
navigateToNewStory(chatTitle, completion.code);
|
|
782
|
+
dispatch({ type: 'SET_RECENT_CHATS', payload: chats });
|
|
1658
783
|
}
|
|
1659
|
-
}, [activeChatId, activeTitle, conversation.length]);
|
|
784
|
+
}, [state.activeChatId, state.activeTitle, state.conversation.length]);
|
|
785
|
+
// Handle send
|
|
1660
786
|
const handleSend = async (e) => {
|
|
1661
787
|
if (e)
|
|
1662
788
|
e.preventDefault();
|
|
1663
|
-
|
|
1664
|
-
if (!input.trim() && attachedImages.length === 0)
|
|
789
|
+
if (!state.input.trim() && state.attachedImages.length === 0)
|
|
1665
790
|
return;
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
setStreamingState(null);
|
|
1671
|
-
// 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 });
|
|
1672
795
|
const connectionTest = await testMCPConnection();
|
|
1673
|
-
|
|
796
|
+
dispatch({ type: 'SET_CONNECTION_STATUS', payload: connectionTest });
|
|
1674
797
|
if (!connectionTest.connected) {
|
|
1675
|
-
|
|
1676
|
-
|
|
798
|
+
dispatch({ type: 'SET_ERROR', payload: `Cannot connect to MCP server: ${connectionTest.error || 'Server not running'}` });
|
|
799
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
1677
800
|
return;
|
|
1678
801
|
}
|
|
1679
|
-
|
|
1680
|
-
const imagesToSend = [...attachedImages];
|
|
802
|
+
const imagesToSend = [...state.attachedImages];
|
|
1681
803
|
const hasImages = imagesToSend.length > 0;
|
|
1682
|
-
// Create user message with images
|
|
1683
804
|
const userMessage = {
|
|
1684
805
|
role: 'user',
|
|
1685
806
|
content: userInput,
|
|
1686
|
-
attachedImages: hasImages ? imagesToSend : undefined
|
|
807
|
+
attachedImages: hasImages ? imagesToSend : undefined,
|
|
1687
808
|
};
|
|
1688
|
-
const newConversation = [...conversation, userMessage];
|
|
1689
|
-
|
|
1690
|
-
|
|
809
|
+
const newConversation = [...state.conversation, userMessage];
|
|
810
|
+
dispatch({ type: 'SET_CONVERSATION', payload: newConversation });
|
|
811
|
+
dispatch({ type: 'SET_INPUT', payload: '' });
|
|
1691
812
|
clearAttachedImages();
|
|
1692
|
-
// Use streaming if enabled
|
|
1693
813
|
if (USE_STREAMING) {
|
|
1694
814
|
try {
|
|
1695
|
-
|
|
1696
|
-
if (abortControllerRef.current) {
|
|
815
|
+
if (abortControllerRef.current)
|
|
1697
816
|
abortControllerRef.current.abort();
|
|
1698
|
-
}
|
|
1699
817
|
abortControllerRef.current = new AbortController();
|
|
1700
|
-
|
|
1701
|
-
setStreamingState({});
|
|
1702
|
-
// Prepare images for API request
|
|
818
|
+
dispatch({ type: 'SET_STREAMING_STATE', payload: {} });
|
|
1703
819
|
const imagePayload = hasImages
|
|
1704
|
-
? imagesToSend.map(img => ({
|
|
1705
|
-
type: 'base64',
|
|
1706
|
-
data: img.base64,
|
|
1707
|
-
mediaType: img.file.type,
|
|
1708
|
-
}))
|
|
820
|
+
? imagesToSend.map(img => ({ type: 'base64', data: img.base64, mediaType: img.file.type }))
|
|
1709
821
|
: undefined;
|
|
1710
822
|
const response = await fetch(MCP_STREAM_API, {
|
|
1711
823
|
method: 'POST',
|
|
@@ -1713,25 +825,23 @@ function StoryUIPanel() {
|
|
|
1713
825
|
body: JSON.stringify({
|
|
1714
826
|
prompt: userInput,
|
|
1715
827
|
conversation: newConversation,
|
|
1716
|
-
fileName: activeChatId || undefined,
|
|
1717
|
-
isUpdate: activeChatId && conversation.length > 0,
|
|
1718
|
-
originalTitle: activeTitle || undefined,
|
|
1719
|
-
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,
|
|
1720
832
|
images: imagePayload,
|
|
1721
833
|
visionMode: hasImages ? 'screenshot_to_story' : undefined,
|
|
1722
|
-
provider: selectedProvider || undefined,
|
|
1723
|
-
model: selectedModel || undefined,
|
|
1724
|
-
considerations: considerations || undefined,
|
|
834
|
+
provider: state.selectedProvider || undefined,
|
|
835
|
+
model: state.selectedModel || undefined,
|
|
836
|
+
considerations: state.considerations || undefined,
|
|
1725
837
|
}),
|
|
1726
838
|
signal: abortControllerRef.current.signal,
|
|
1727
839
|
});
|
|
1728
|
-
if (!response.ok)
|
|
840
|
+
if (!response.ok)
|
|
1729
841
|
throw new Error(`Streaming request failed: ${response.status}`);
|
|
1730
|
-
}
|
|
1731
842
|
const reader = response.body?.getReader();
|
|
1732
|
-
if (!reader)
|
|
843
|
+
if (!reader)
|
|
1733
844
|
throw new Error('No response body');
|
|
1734
|
-
}
|
|
1735
845
|
const decoder = new TextDecoder();
|
|
1736
846
|
let buffer = '';
|
|
1737
847
|
let completionData = null;
|
|
@@ -1741,215 +851,109 @@ function StoryUIPanel() {
|
|
|
1741
851
|
if (done)
|
|
1742
852
|
break;
|
|
1743
853
|
buffer += decoder.decode(value, { stream: true });
|
|
1744
|
-
// Parse SSE events from buffer
|
|
1745
854
|
const lines = buffer.split('\n');
|
|
1746
|
-
buffer = lines.pop() || '';
|
|
855
|
+
buffer = lines.pop() || '';
|
|
1747
856
|
for (const line of lines) {
|
|
1748
857
|
if (line.startsWith('data: ')) {
|
|
1749
858
|
try {
|
|
1750
859
|
const event = JSON.parse(line.slice(6));
|
|
1751
|
-
// Update streaming state based on event type
|
|
1752
860
|
switch (event.type) {
|
|
1753
861
|
case 'intent':
|
|
1754
|
-
|
|
862
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { intent: event.data } });
|
|
1755
863
|
break;
|
|
1756
864
|
case 'progress':
|
|
1757
|
-
|
|
865
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { progress: event.data } });
|
|
1758
866
|
break;
|
|
1759
867
|
case 'validation':
|
|
1760
|
-
|
|
868
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { validation: event.data } });
|
|
1761
869
|
break;
|
|
1762
870
|
case 'retry':
|
|
1763
|
-
|
|
871
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { retry: event.data } });
|
|
1764
872
|
break;
|
|
1765
873
|
case 'completion':
|
|
1766
874
|
completionData = event.data;
|
|
1767
|
-
|
|
875
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { completion: completionData } });
|
|
1768
876
|
break;
|
|
1769
877
|
case 'error':
|
|
1770
878
|
errorData = event.data;
|
|
1771
|
-
|
|
879
|
+
dispatch({ type: 'UPDATE_STREAMING_STATE', payload: { error: errorData } });
|
|
1772
880
|
break;
|
|
1773
881
|
}
|
|
1774
882
|
}
|
|
1775
|
-
catch
|
|
1776
|
-
console.warn('Failed to parse SSE event:', line
|
|
883
|
+
catch {
|
|
884
|
+
console.warn('Failed to parse SSE event:', line);
|
|
1777
885
|
}
|
|
1778
886
|
}
|
|
1779
887
|
}
|
|
1780
888
|
}
|
|
1781
|
-
// Handle completion or error
|
|
1782
889
|
if (completionData) {
|
|
1783
890
|
finalizeStreamingConversation(newConversation, completionData, userInput);
|
|
1784
891
|
}
|
|
1785
892
|
else if (errorData) {
|
|
1786
|
-
|
|
893
|
+
dispatch({ type: 'SET_ERROR', payload: errorData.message });
|
|
1787
894
|
const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorData.message}\n\n${errorData.suggestion || ''}` }];
|
|
1788
|
-
|
|
895
|
+
dispatch({ type: 'SET_CONVERSATION', payload: errorConversation });
|
|
1789
896
|
}
|
|
1790
897
|
}
|
|
1791
898
|
catch (err) {
|
|
1792
|
-
if (err.name === 'AbortError')
|
|
1793
|
-
console.log('Request aborted');
|
|
899
|
+
if (err.name === 'AbortError')
|
|
1794
900
|
return;
|
|
1795
|
-
}
|
|
1796
|
-
// Fall back to non-streaming on error
|
|
1797
901
|
console.warn('Streaming failed, falling back to non-streaming:', err);
|
|
1798
|
-
|
|
902
|
+
dispatch({ type: 'SET_STREAMING_STATE', payload: null });
|
|
1799
903
|
try {
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
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.`;
|
|
1811
920
|
const aiMsg = { role: 'ai', content: responseMessage };
|
|
1812
921
|
const updatedConversation = [...newConversation, aiMsg];
|
|
1813
|
-
|
|
1814
|
-
// Update chat session
|
|
1815
|
-
const isUpdate = activeChatId && conversation.length > 0;
|
|
1816
|
-
if (isUpdate && activeChatId) {
|
|
1817
|
-
const updatedSession = {
|
|
1818
|
-
id: activeChatId,
|
|
1819
|
-
title: activeTitle,
|
|
1820
|
-
fileName: data.fileName || activeChatId,
|
|
1821
|
-
conversation: updatedConversation,
|
|
1822
|
-
lastUpdated: Date.now(),
|
|
1823
|
-
};
|
|
1824
|
-
const chats = loadChats();
|
|
1825
|
-
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
1826
|
-
if (chatIndex !== -1)
|
|
1827
|
-
chats[chatIndex] = updatedSession;
|
|
1828
|
-
saveChats(chats);
|
|
1829
|
-
setRecentChats(chats);
|
|
1830
|
-
}
|
|
1831
|
-
else {
|
|
1832
|
-
const chatId = data.storyId || data.fileName || Date.now().toString();
|
|
1833
|
-
const chatTitle = data.title || userInput;
|
|
1834
|
-
setActiveChatId(chatId);
|
|
1835
|
-
setActiveTitle(chatTitle);
|
|
1836
|
-
const newSession = {
|
|
1837
|
-
id: chatId,
|
|
1838
|
-
title: chatTitle,
|
|
1839
|
-
fileName: data.fileName || '',
|
|
1840
|
-
conversation: updatedConversation,
|
|
1841
|
-
lastUpdated: Date.now(),
|
|
1842
|
-
};
|
|
1843
|
-
const chats = loadChats().filter(c => c.id !== chatId);
|
|
1844
|
-
chats.unshift(newSession);
|
|
1845
|
-
if (chats.length > MAX_RECENT_CHATS)
|
|
1846
|
-
chats.splice(MAX_RECENT_CHATS);
|
|
1847
|
-
saveChats(chats);
|
|
1848
|
-
setRecentChats(chats);
|
|
1849
|
-
// Auto-navigate to the newly created story
|
|
1850
|
-
navigateToNewStory(chatTitle, data.code);
|
|
1851
|
-
}
|
|
922
|
+
dispatch({ type: 'SET_CONVERSATION', payload: updatedConversation });
|
|
1852
923
|
}
|
|
1853
924
|
catch (fallbackErr) {
|
|
1854
925
|
const errorMessage = fallbackErr instanceof Error ? fallbackErr.message : 'Unknown error';
|
|
1855
|
-
|
|
926
|
+
dispatch({ type: 'SET_ERROR', payload: errorMessage });
|
|
1856
927
|
const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorMessage}` }];
|
|
1857
|
-
|
|
928
|
+
dispatch({ type: 'SET_CONVERSATION', payload: errorConversation });
|
|
1858
929
|
}
|
|
1859
930
|
}
|
|
1860
931
|
finally {
|
|
1861
|
-
|
|
1862
|
-
|
|
932
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
933
|
+
dispatch({ type: 'SET_STREAMING_STATE', payload: null });
|
|
1863
934
|
abortControllerRef.current = null;
|
|
1864
935
|
}
|
|
1865
936
|
}
|
|
1866
|
-
else {
|
|
1867
|
-
// Non-streaming mode (original implementation)
|
|
1868
|
-
try {
|
|
1869
|
-
const data = await handleSendNonStreaming(userInput, newConversation);
|
|
1870
|
-
let responseMessage;
|
|
1871
|
-
const statusMarker = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '[WRENCH]' : '[TIP]') : '[SUCCESS]';
|
|
1872
|
-
// Build conversational response for non-streaming mode
|
|
1873
|
-
if (data.isUpdate) {
|
|
1874
|
-
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._`;
|
|
1875
|
-
}
|
|
1876
|
-
else {
|
|
1877
|
-
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).`;
|
|
1878
|
-
}
|
|
1879
|
-
const aiMsg = { role: 'ai', content: responseMessage };
|
|
1880
|
-
const updatedConversation = [...newConversation, aiMsg];
|
|
1881
|
-
setConversation(updatedConversation);
|
|
1882
|
-
const isUpdate = activeChatId && conversation.length > 0;
|
|
1883
|
-
if (isUpdate && activeChatId) {
|
|
1884
|
-
const updatedSession = {
|
|
1885
|
-
id: activeChatId,
|
|
1886
|
-
title: activeTitle,
|
|
1887
|
-
fileName: data.fileName || activeChatId,
|
|
1888
|
-
conversation: updatedConversation,
|
|
1889
|
-
lastUpdated: Date.now(),
|
|
1890
|
-
};
|
|
1891
|
-
const chats = loadChats();
|
|
1892
|
-
const chatIndex = chats.findIndex(c => c.id === activeChatId);
|
|
1893
|
-
if (chatIndex !== -1)
|
|
1894
|
-
chats[chatIndex] = updatedSession;
|
|
1895
|
-
saveChats(chats);
|
|
1896
|
-
setRecentChats(chats);
|
|
1897
|
-
}
|
|
1898
|
-
else {
|
|
1899
|
-
const chatId = data.storyId || data.fileName || Date.now().toString();
|
|
1900
|
-
const chatTitle = data.title || userInput;
|
|
1901
|
-
setActiveChatId(chatId);
|
|
1902
|
-
setActiveTitle(chatTitle);
|
|
1903
|
-
const newSession = {
|
|
1904
|
-
id: chatId,
|
|
1905
|
-
title: chatTitle,
|
|
1906
|
-
fileName: data.fileName || '',
|
|
1907
|
-
conversation: updatedConversation,
|
|
1908
|
-
lastUpdated: Date.now(),
|
|
1909
|
-
};
|
|
1910
|
-
const chats = loadChats().filter(c => c.id !== chatId);
|
|
1911
|
-
chats.unshift(newSession);
|
|
1912
|
-
if (chats.length > MAX_RECENT_CHATS)
|
|
1913
|
-
chats.splice(MAX_RECENT_CHATS);
|
|
1914
|
-
saveChats(chats);
|
|
1915
|
-
setRecentChats(chats);
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
catch (err) {
|
|
1919
|
-
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
1920
|
-
setError(errorMessage);
|
|
1921
|
-
const errorConversation = [...newConversation, { role: 'ai', content: `Error: ${errorMessage}` }];
|
|
1922
|
-
setConversation(errorConversation);
|
|
1923
|
-
}
|
|
1924
|
-
finally {
|
|
1925
|
-
setLoading(false);
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
937
|
};
|
|
938
|
+
// Chat management
|
|
1929
939
|
const handleSelectChat = (chat) => {
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
setActiveTitle(chat.title);
|
|
1933
|
-
};
|
|
1934
|
-
const handleNewChat = () => {
|
|
1935
|
-
setConversation([]);
|
|
1936
|
-
setActiveChatId(null);
|
|
1937
|
-
setActiveTitle('');
|
|
940
|
+
dispatch({ type: 'SET_CONVERSATION', payload: chat.conversation });
|
|
941
|
+
dispatch({ type: 'SET_ACTIVE_CHAT', payload: { id: chat.id, title: chat.title } });
|
|
1938
942
|
};
|
|
943
|
+
const handleNewChat = () => dispatch({ type: 'NEW_CHAT' });
|
|
1939
944
|
const handleDeleteChat = async (chatId, e) => {
|
|
1940
|
-
e
|
|
945
|
+
if (e)
|
|
946
|
+
e.stopPropagation();
|
|
947
|
+
setContextMenuId(null);
|
|
1941
948
|
if (confirm('Delete this story and chat? This action cannot be undone.')) {
|
|
1942
949
|
const success = await deleteStoryAndChat(chatId);
|
|
1943
950
|
if (success) {
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
// If we deleted the active chat, switch to another or clear
|
|
1948
|
-
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) {
|
|
1949
954
|
if (updatedChats.length > 0) {
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
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 } });
|
|
1953
957
|
}
|
|
1954
958
|
else {
|
|
1955
959
|
handleNewChat();
|
|
@@ -1961,202 +965,124 @@ function StoryUIPanel() {
|
|
|
1961
965
|
}
|
|
1962
966
|
}
|
|
1963
967
|
};
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
const newProvider = e.target.value;
|
|
2083
|
-
setSelectedProvider(newProvider);
|
|
2084
|
-
// Reset model to first available for new provider
|
|
2085
|
-
const provider = availableProviders.find(p => p.type === newProvider);
|
|
2086
|
-
if (provider && provider.models.length > 0) {
|
|
2087
|
-
setSelectedModel(provider.models[0]);
|
|
2088
|
-
}
|
|
2089
|
-
}, style: {
|
|
2090
|
-
background: '#1e293b',
|
|
2091
|
-
border: '1px solid #334155',
|
|
2092
|
-
borderRadius: '6px',
|
|
2093
|
-
color: '#e2e8f0',
|
|
2094
|
-
padding: '4px 8px',
|
|
2095
|
-
fontSize: '12px',
|
|
2096
|
-
cursor: 'pointer'
|
|
2097
|
-
}, 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: {
|
|
2098
|
-
background: '#1e293b',
|
|
2099
|
-
border: '1px solid #334155',
|
|
2100
|
-
borderRadius: '6px',
|
|
2101
|
-
color: '#e2e8f0',
|
|
2102
|
-
padding: '4px 8px',
|
|
2103
|
-
fontSize: '12px',
|
|
2104
|
-
cursor: 'pointer',
|
|
2105
|
-
maxWidth: '200px'
|
|
2106
|
-
}, children: availableProviders
|
|
2107
|
-
.find(p => p.type === selectedProvider)
|
|
2108
|
-
?.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
|
|
2109
|
-
? `data:${img.mediaType || 'image/png'};base64,${img.base64}`
|
|
2110
|
-
: 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: {
|
|
2111
|
-
...STYLES.inputForm,
|
|
2112
|
-
...(attachedImages.length > 0 ? {
|
|
2113
|
-
marginTop: 0,
|
|
2114
|
-
borderTopLeftRadius: 0,
|
|
2115
|
-
borderTopRightRadius: 0,
|
|
2116
|
-
} : {})
|
|
2117
|
-
}, children: [_jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), disabled: loading || attachedImages.length >= MAX_IMAGES, style: {
|
|
2118
|
-
...STYLES.uploadButton,
|
|
2119
|
-
...(attachedImages.length >= MAX_IMAGES ? {
|
|
2120
|
-
opacity: 0.5,
|
|
2121
|
-
cursor: 'not-allowed',
|
|
2122
|
-
} : {})
|
|
2123
|
-
}, title: attachedImages.length >= MAX_IMAGES
|
|
2124
|
-
? `Maximum ${MAX_IMAGES} images`
|
|
2125
|
-
: 'Attach images (screenshots, designs)', onMouseEnter: (e) => {
|
|
2126
|
-
if (attachedImages.length < MAX_IMAGES && !loading) {
|
|
2127
|
-
e.currentTarget.style.background = 'rgba(59, 130, 246, 0.2)';
|
|
2128
|
-
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
|
|
2129
|
-
}
|
|
2130
|
-
}, onMouseLeave: (e) => {
|
|
2131
|
-
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
|
|
2132
|
-
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
|
|
2133
|
-
}, 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
|
|
2134
|
-
? "Describe what to create from these images..."
|
|
2135
|
-
: "Describe a UI component...", style: STYLES.textInput, onFocus: (e) => {
|
|
2136
|
-
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
|
|
2137
|
-
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.1)';
|
|
2138
|
-
}, onBlur: (e) => {
|
|
2139
|
-
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
|
|
2140
|
-
e.currentTarget.style.boxShadow = 'none';
|
|
2141
|
-
} }), _jsxs("button", { type: "submit", disabled: loading || (!input.trim() && attachedImages.length === 0), style: {
|
|
2142
|
-
...STYLES.sendButton,
|
|
2143
|
-
...(loading || (!input.trim() && attachedImages.length === 0) ? {
|
|
2144
|
-
opacity: 0.4,
|
|
2145
|
-
cursor: 'not-allowed',
|
|
2146
|
-
background: '#64748b',
|
|
2147
|
-
boxShadow: 'none'
|
|
2148
|
-
} : {})
|
|
2149
|
-
}, onMouseEnter: (e) => {
|
|
2150
|
-
if (!loading && (input.trim() || attachedImages.length > 0)) {
|
|
2151
|
-
e.currentTarget.style.background = '#2563eb';
|
|
2152
|
-
e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.5)';
|
|
2153
|
-
}
|
|
2154
|
-
}, onMouseLeave: (e) => {
|
|
2155
|
-
if (!loading && (input.trim() || attachedImages.length > 0)) {
|
|
2156
|
-
e.currentTarget.style.background = '#3b82f6';
|
|
2157
|
-
e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.35)';
|
|
2158
|
-
}
|
|
2159
|
-
}, 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" })] })] })] })] }));
|
|
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() } });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
setRenamingChatId(null);
|
|
991
|
+
setRenameValue('');
|
|
992
|
+
};
|
|
993
|
+
const handleCancelRename = () => {
|
|
994
|
+
setRenamingChatId(null);
|
|
995
|
+
setRenameValue('');
|
|
996
|
+
};
|
|
997
|
+
// Orphan story handlers
|
|
998
|
+
const toggleSelectAll = () => {
|
|
999
|
+
if (state.selectedStoryIds.size === state.orphanStories.length) {
|
|
1000
|
+
dispatch({ type: 'SET_SELECTED_STORY_IDS', payload: new Set() });
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
dispatch({ type: 'SET_SELECTED_STORY_IDS', payload: new Set(state.orphanStories.map(s => s.id)) });
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const handleBulkDelete = async () => {
|
|
1007
|
+
if (state.selectedStoryIds.size === 0)
|
|
1008
|
+
return;
|
|
1009
|
+
const count = state.selectedStoryIds.size;
|
|
1010
|
+
if (!confirm(`Delete ${count} selected ${count === 1 ? 'story' : 'stories'}?`))
|
|
1011
|
+
return;
|
|
1012
|
+
dispatch({ type: 'SET_BULK_DELETING', payload: true });
|
|
1013
|
+
try {
|
|
1014
|
+
const response = await fetch(`${STORIES_API}/delete-bulk`, {
|
|
1015
|
+
method: 'POST',
|
|
1016
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1017
|
+
body: JSON.stringify({ ids: Array.from(state.selectedStoryIds) }),
|
|
1018
|
+
});
|
|
1019
|
+
if (response.ok) {
|
|
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() });
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
alert('Failed to delete some stories.');
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
catch {
|
|
1028
|
+
alert('Failed to delete stories.');
|
|
1029
|
+
}
|
|
1030
|
+
finally {
|
|
1031
|
+
dispatch({ type: 'SET_BULK_DELETING', payload: false });
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
const handleClearAll = async () => {
|
|
1035
|
+
if (state.orphanStories.length === 0)
|
|
1036
|
+
return;
|
|
1037
|
+
if (!confirm(`Delete ALL ${state.orphanStories.length} generated stories?`))
|
|
1038
|
+
return;
|
|
1039
|
+
dispatch({ type: 'SET_BULK_DELETING', payload: true });
|
|
1040
|
+
try {
|
|
1041
|
+
const response = await fetch(STORIES_API, { method: 'DELETE' });
|
|
1042
|
+
if (response.ok) {
|
|
1043
|
+
dispatch({ type: 'SET_ORPHAN_STORIES', payload: [] });
|
|
1044
|
+
dispatch({ type: 'SET_SELECTED_STORY_IDS', payload: new Set() });
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
alert('Failed to clear stories.');
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
catch {
|
|
1051
|
+
alert('Failed to clear stories.');
|
|
1052
|
+
}
|
|
1053
|
+
finally {
|
|
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);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
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 })] })] }) })] })] }));
|
|
2160
1086
|
}
|
|
2161
1087
|
export default StoryUIPanel;
|
|
2162
1088
|
export { StoryUIPanel };
|