@tpitre/story-ui 4.1.2 → 4.2.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 +0 -0
- package/dist/mcp-server/index.js +119 -0
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +63 -1
- package/package.json +1 -1
- package/templates/StoryUI/StoryUIPanel.css +56 -0
- package/templates/StoryUI/StoryUIPanel.tsx +83 -0
- package/dist/templates/StoryUI/StoryUIPanel.css +0 -1440
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/mcp-server/index.js
CHANGED
|
@@ -399,6 +399,125 @@ app.delete('/story-ui/stories', async (req, res) => {
|
|
|
399
399
|
return res.status(500).json({ error: 'Failed to clear stories' });
|
|
400
400
|
}
|
|
401
401
|
});
|
|
402
|
+
// Orphan stories management - find and delete stories without associated chats
|
|
403
|
+
// POST to get list of orphans, DELETE to remove them
|
|
404
|
+
app.post('/story-ui/orphan-stories', async (req, res) => {
|
|
405
|
+
try {
|
|
406
|
+
const { chatFileNames } = req.body;
|
|
407
|
+
if (!chatFileNames || !Array.isArray(chatFileNames)) {
|
|
408
|
+
return res.status(400).json({ error: 'chatFileNames array is required' });
|
|
409
|
+
}
|
|
410
|
+
const storiesPath = config.generatedStoriesPath;
|
|
411
|
+
if (!fs.existsSync(storiesPath)) {
|
|
412
|
+
return res.json({ orphans: [], count: 0 });
|
|
413
|
+
}
|
|
414
|
+
const files = fs.readdirSync(storiesPath);
|
|
415
|
+
const storyFiles = files.filter(file => file.endsWith('.stories.tsx') ||
|
|
416
|
+
file.endsWith('.stories.ts') ||
|
|
417
|
+
file.endsWith('.stories.svelte'));
|
|
418
|
+
// Find orphans: stories that don't match any chat fileName
|
|
419
|
+
const orphans = storyFiles.filter(storyFile => {
|
|
420
|
+
// Extract the base name without extension for comparison
|
|
421
|
+
const storyBase = storyFile
|
|
422
|
+
.replace('.stories.tsx', '')
|
|
423
|
+
.replace('.stories.ts', '')
|
|
424
|
+
.replace('.stories.svelte', '');
|
|
425
|
+
// Check if any chat fileName matches this story
|
|
426
|
+
return !chatFileNames.some(chatFileName => {
|
|
427
|
+
if (!chatFileName)
|
|
428
|
+
return false;
|
|
429
|
+
const chatBase = chatFileName
|
|
430
|
+
.replace('.stories.tsx', '')
|
|
431
|
+
.replace('.stories.ts', '')
|
|
432
|
+
.replace('.stories.svelte', '');
|
|
433
|
+
return storyBase === chatBase || storyFile === chatFileName;
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
// Get details for each orphan
|
|
437
|
+
const orphanDetails = orphans.map(fileName => {
|
|
438
|
+
const filePath = path.join(storiesPath, fileName);
|
|
439
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
440
|
+
const stats = fs.statSync(filePath);
|
|
441
|
+
// Extract title from story file
|
|
442
|
+
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
443
|
+
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : fileName.replace(/\.stories\.[a-z]+$/, '');
|
|
444
|
+
// Remove hash suffix from display title
|
|
445
|
+
title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
|
|
446
|
+
return {
|
|
447
|
+
fileName,
|
|
448
|
+
title,
|
|
449
|
+
lastUpdated: stats.mtime.getTime()
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
console.log(`📋 Found ${orphans.length} orphan stories out of ${storyFiles.length} total`);
|
|
453
|
+
return res.json({
|
|
454
|
+
orphans: orphanDetails,
|
|
455
|
+
count: orphans.length,
|
|
456
|
+
totalStories: storyFiles.length
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
console.error('Error finding orphan stories:', error);
|
|
461
|
+
return res.status(500).json({ error: 'Failed to find orphan stories' });
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
app.delete('/story-ui/orphan-stories', async (req, res) => {
|
|
465
|
+
try {
|
|
466
|
+
const { chatFileNames } = req.body;
|
|
467
|
+
if (!chatFileNames || !Array.isArray(chatFileNames)) {
|
|
468
|
+
return res.status(400).json({ error: 'chatFileNames array is required' });
|
|
469
|
+
}
|
|
470
|
+
const storiesPath = config.generatedStoriesPath;
|
|
471
|
+
if (!fs.existsSync(storiesPath)) {
|
|
472
|
+
return res.json({ deleted: [], count: 0 });
|
|
473
|
+
}
|
|
474
|
+
const files = fs.readdirSync(storiesPath);
|
|
475
|
+
const storyFiles = files.filter(file => file.endsWith('.stories.tsx') ||
|
|
476
|
+
file.endsWith('.stories.ts') ||
|
|
477
|
+
file.endsWith('.stories.svelte'));
|
|
478
|
+
// Find orphans: stories that don't match any chat fileName
|
|
479
|
+
const orphans = storyFiles.filter(storyFile => {
|
|
480
|
+
const storyBase = storyFile
|
|
481
|
+
.replace('.stories.tsx', '')
|
|
482
|
+
.replace('.stories.ts', '')
|
|
483
|
+
.replace('.stories.svelte', '');
|
|
484
|
+
return !chatFileNames.some(chatFileName => {
|
|
485
|
+
if (!chatFileName)
|
|
486
|
+
return false;
|
|
487
|
+
const chatBase = chatFileName
|
|
488
|
+
.replace('.stories.tsx', '')
|
|
489
|
+
.replace('.stories.ts', '')
|
|
490
|
+
.replace('.stories.svelte', '');
|
|
491
|
+
return storyBase === chatBase || storyFile === chatFileName;
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
// Delete orphans
|
|
495
|
+
const deleted = [];
|
|
496
|
+
const errors = [];
|
|
497
|
+
for (const fileName of orphans) {
|
|
498
|
+
try {
|
|
499
|
+
const filePath = path.join(storiesPath, fileName);
|
|
500
|
+
fs.unlinkSync(filePath);
|
|
501
|
+
deleted.push(fileName);
|
|
502
|
+
console.log(`🗑️ Deleted orphan story: ${fileName}`);
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
errors.push(fileName);
|
|
506
|
+
console.error(`❌ Error deleting orphan ${fileName}:`, err);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
console.log(`✅ Deleted ${deleted.length} orphan stories`);
|
|
510
|
+
return res.json({
|
|
511
|
+
deleted,
|
|
512
|
+
count: deleted.length,
|
|
513
|
+
errors: errors.length > 0 ? errors : undefined
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
console.error('Error deleting orphan stories:', error);
|
|
518
|
+
return res.status(500).json({ error: 'Failed to delete orphan stories' });
|
|
519
|
+
}
|
|
520
|
+
});
|
|
402
521
|
// MCP Remote HTTP transport routes (for Claude Desktop remote connections)
|
|
403
522
|
// Provides Streamable HTTP and legacy SSE endpoints for remote MCP access
|
|
404
523
|
app.use('/mcp-remote', mcpRemoteRouter);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAwwB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CAwnCnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -151,6 +151,7 @@ const MCP_API = `${API_BASE}/mcp/generate-story`;
|
|
|
151
151
|
const MCP_STREAM_API = `${API_BASE}/mcp/generate-story-stream`;
|
|
152
152
|
const PROVIDERS_API = `${API_BASE}/mcp/providers`;
|
|
153
153
|
const STORIES_API = `${API_BASE}/story-ui/stories`;
|
|
154
|
+
const ORPHAN_STORIES_API = `${API_BASE}/story-ui/orphan-stories`;
|
|
154
155
|
const CONSIDERATIONS_API = `${API_BASE}/mcp/considerations`;
|
|
155
156
|
function isEdgeMode() {
|
|
156
157
|
if (typeof window !== 'undefined') {
|
|
@@ -444,6 +445,8 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
444
445
|
const [contextMenuId, setContextMenuId] = useState(null);
|
|
445
446
|
const [renamingChatId, setRenamingChatId] = useState(null);
|
|
446
447
|
const [renameValue, setRenameValue] = useState('');
|
|
448
|
+
const [orphanCount, setOrphanCount] = useState(0);
|
|
449
|
+
const [isDeletingOrphans, setIsDeletingOrphans] = useState(false);
|
|
447
450
|
const chatEndRef = useRef(null);
|
|
448
451
|
const inputRef = useRef(null);
|
|
449
452
|
const fileInputRef = useRef(null);
|
|
@@ -1144,6 +1147,65 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
1144
1147
|
}
|
|
1145
1148
|
}
|
|
1146
1149
|
};
|
|
1150
|
+
// Check for orphan stories (stories without associated chats)
|
|
1151
|
+
const checkOrphanStories = useCallback(async () => {
|
|
1152
|
+
if (!state.connectionStatus.connected)
|
|
1153
|
+
return;
|
|
1154
|
+
try {
|
|
1155
|
+
const chatFileNames = state.recentChats.map(chat => chat.fileName);
|
|
1156
|
+
const response = await fetch(ORPHAN_STORIES_API, {
|
|
1157
|
+
method: 'POST',
|
|
1158
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1159
|
+
body: JSON.stringify({ chatFileNames }),
|
|
1160
|
+
});
|
|
1161
|
+
if (response.ok) {
|
|
1162
|
+
const data = await response.json();
|
|
1163
|
+
setOrphanCount(data.count || 0);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
catch (error) {
|
|
1167
|
+
console.error('Failed to check orphan stories:', error);
|
|
1168
|
+
}
|
|
1169
|
+
}, [state.connectionStatus.connected, state.recentChats]);
|
|
1170
|
+
// Delete all orphan stories
|
|
1171
|
+
const handleDeleteOrphans = async () => {
|
|
1172
|
+
if (orphanCount === 0)
|
|
1173
|
+
return;
|
|
1174
|
+
if (!confirm(`Delete ${orphanCount} orphan ${orphanCount === 1 ? 'story' : 'stories'}? These are generated story files without associated chats.`)) {
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
setIsDeletingOrphans(true);
|
|
1178
|
+
try {
|
|
1179
|
+
const chatFileNames = state.recentChats.map(chat => chat.fileName);
|
|
1180
|
+
const response = await fetch(ORPHAN_STORIES_API, {
|
|
1181
|
+
method: 'DELETE',
|
|
1182
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1183
|
+
body: JSON.stringify({ chatFileNames }),
|
|
1184
|
+
});
|
|
1185
|
+
if (response.ok) {
|
|
1186
|
+
const data = await response.json();
|
|
1187
|
+
setOrphanCount(0);
|
|
1188
|
+
if (data.count > 0) {
|
|
1189
|
+
// Show success message briefly
|
|
1190
|
+
alert(`Deleted ${data.count} orphan ${data.count === 1 ? 'story' : 'stories'}.`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
alert('Failed to delete orphan stories. Please try again.');
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
catch (error) {
|
|
1198
|
+
console.error('Failed to delete orphan stories:', error);
|
|
1199
|
+
alert('Failed to delete orphan stories. Please try again.');
|
|
1200
|
+
}
|
|
1201
|
+
finally {
|
|
1202
|
+
setIsDeletingOrphans(false);
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
// Check for orphans when chats change or connection is established
|
|
1206
|
+
useEffect(() => {
|
|
1207
|
+
checkOrphanStories();
|
|
1208
|
+
}, [checkOrphanStories]);
|
|
1147
1209
|
const handleStartRename = (chatId, currentTitle, e) => {
|
|
1148
1210
|
if (e)
|
|
1149
1211
|
e.stopPropagation();
|
|
@@ -1255,7 +1317,7 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
1255
1317
|
handleConfirmRename(chat.id);
|
|
1256
1318
|
if (e.key === 'Escape')
|
|
1257
1319
|
handleCancelRename();
|
|
1258
|
-
}, 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 => {
|
|
1320
|
+
}, 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))) }), orphanCount > 0 && (_jsx("div", { className: "sui-orphan-footer", children: _jsx("button", { className: "sui-orphan-delete-btn", onClick: handleDeleteOrphans, disabled: isDeletingOrphans, title: `${orphanCount} story ${orphanCount === 1 ? 'file has' : 'files have'} no associated chat`, children: isDeletingOrphans ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sui-orphan-spinner" }), _jsx("span", { children: "Deleting..." })] })) : (_jsxs(_Fragment, { children: [Icons.trash, _jsxs("span", { children: [orphanCount, " orphan ", orphanCount === 1 ? 'story' : 'stories'] })] })) }) }))] })), !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 => {
|
|
1259
1321
|
const newProvider = e.target.value;
|
|
1260
1322
|
dispatch({ type: 'SET_SELECTED_PROVIDER', payload: newProvider });
|
|
1261
1323
|
const provider = state.availableProviders.find(p => p.type === newProvider);
|
package/package.json
CHANGED
|
@@ -1467,3 +1467,59 @@
|
|
|
1467
1467
|
.sui-hidden {
|
|
1468
1468
|
display: none;
|
|
1469
1469
|
}
|
|
1470
|
+
|
|
1471
|
+
/* ============================================
|
|
1472
|
+
Orphan Stories Footer
|
|
1473
|
+
============================================ */
|
|
1474
|
+
.sui-orphan-footer {
|
|
1475
|
+
flex-shrink: 0;
|
|
1476
|
+
padding-top: var(--space-2);
|
|
1477
|
+
border-top: 1px solid hsl(var(--border));
|
|
1478
|
+
margin-top: auto;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.sui-orphan-delete-btn {
|
|
1482
|
+
display: flex;
|
|
1483
|
+
align-items: center;
|
|
1484
|
+
gap: var(--space-2);
|
|
1485
|
+
width: 100%;
|
|
1486
|
+
padding: var(--space-2) var(--space-3);
|
|
1487
|
+
border: none;
|
|
1488
|
+
background: transparent;
|
|
1489
|
+
color: hsl(var(--muted-foreground));
|
|
1490
|
+
font-size: 0.75rem;
|
|
1491
|
+
cursor: pointer;
|
|
1492
|
+
border-radius: var(--radius-sm);
|
|
1493
|
+
transition: all var(--transition-fast);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
.sui-orphan-delete-btn:hover:not(:disabled) {
|
|
1497
|
+
background: hsl(var(--destructive) / 0.1);
|
|
1498
|
+
color: hsl(var(--destructive));
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
.sui-orphan-delete-btn:disabled {
|
|
1502
|
+
cursor: not-allowed;
|
|
1503
|
+
opacity: 0.6;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
.sui-orphan-delete-btn svg {
|
|
1507
|
+
width: 14px;
|
|
1508
|
+
height: 14px;
|
|
1509
|
+
flex-shrink: 0;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
.sui-orphan-spinner {
|
|
1513
|
+
width: 14px;
|
|
1514
|
+
height: 14px;
|
|
1515
|
+
border: 2px solid hsl(var(--muted-foreground) / 0.3);
|
|
1516
|
+
border-top-color: hsl(var(--muted-foreground));
|
|
1517
|
+
border-radius: 50%;
|
|
1518
|
+
animation: sui-spin 0.6s linear infinite;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
@keyframes sui-spin {
|
|
1522
|
+
to {
|
|
1523
|
+
transform: rotate(360deg);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
@@ -336,6 +336,7 @@ const MCP_API = `${API_BASE}/mcp/generate-story`;
|
|
|
336
336
|
const MCP_STREAM_API = `${API_BASE}/mcp/generate-story-stream`;
|
|
337
337
|
const PROVIDERS_API = `${API_BASE}/mcp/providers`;
|
|
338
338
|
const STORIES_API = `${API_BASE}/story-ui/stories`;
|
|
339
|
+
const ORPHAN_STORIES_API = `${API_BASE}/story-ui/orphan-stories`;
|
|
339
340
|
const CONSIDERATIONS_API = `${API_BASE}/mcp/considerations`;
|
|
340
341
|
|
|
341
342
|
function isEdgeMode(): boolean {
|
|
@@ -791,6 +792,8 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
791
792
|
const [contextMenuId, setContextMenuId] = useState<string | null>(null);
|
|
792
793
|
const [renamingChatId, setRenamingChatId] = useState<string | null>(null);
|
|
793
794
|
const [renameValue, setRenameValue] = useState('');
|
|
795
|
+
const [orphanCount, setOrphanCount] = useState<number>(0);
|
|
796
|
+
const [isDeletingOrphans, setIsDeletingOrphans] = useState<boolean>(false);
|
|
794
797
|
const chatEndRef = useRef<HTMLDivElement>(null);
|
|
795
798
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
796
799
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -1492,6 +1495,62 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1492
1495
|
}
|
|
1493
1496
|
};
|
|
1494
1497
|
|
|
1498
|
+
// Check for orphan stories (stories without associated chats)
|
|
1499
|
+
const checkOrphanStories = useCallback(async () => {
|
|
1500
|
+
if (!state.connectionStatus.connected) return;
|
|
1501
|
+
try {
|
|
1502
|
+
const chatFileNames = state.recentChats.map(chat => chat.fileName);
|
|
1503
|
+
const response = await fetch(ORPHAN_STORIES_API, {
|
|
1504
|
+
method: 'POST',
|
|
1505
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1506
|
+
body: JSON.stringify({ chatFileNames }),
|
|
1507
|
+
});
|
|
1508
|
+
if (response.ok) {
|
|
1509
|
+
const data = await response.json();
|
|
1510
|
+
setOrphanCount(data.count || 0);
|
|
1511
|
+
}
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
console.error('Failed to check orphan stories:', error);
|
|
1514
|
+
}
|
|
1515
|
+
}, [state.connectionStatus.connected, state.recentChats]);
|
|
1516
|
+
|
|
1517
|
+
// Delete all orphan stories
|
|
1518
|
+
const handleDeleteOrphans = async () => {
|
|
1519
|
+
if (orphanCount === 0) return;
|
|
1520
|
+
if (!confirm(`Delete ${orphanCount} orphan ${orphanCount === 1 ? 'story' : 'stories'}? These are generated story files without associated chats.`)) {
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
setIsDeletingOrphans(true);
|
|
1524
|
+
try {
|
|
1525
|
+
const chatFileNames = state.recentChats.map(chat => chat.fileName);
|
|
1526
|
+
const response = await fetch(ORPHAN_STORIES_API, {
|
|
1527
|
+
method: 'DELETE',
|
|
1528
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1529
|
+
body: JSON.stringify({ chatFileNames }),
|
|
1530
|
+
});
|
|
1531
|
+
if (response.ok) {
|
|
1532
|
+
const data = await response.json();
|
|
1533
|
+
setOrphanCount(0);
|
|
1534
|
+
if (data.count > 0) {
|
|
1535
|
+
// Show success message briefly
|
|
1536
|
+
alert(`Deleted ${data.count} orphan ${data.count === 1 ? 'story' : 'stories'}.`);
|
|
1537
|
+
}
|
|
1538
|
+
} else {
|
|
1539
|
+
alert('Failed to delete orphan stories. Please try again.');
|
|
1540
|
+
}
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
console.error('Failed to delete orphan stories:', error);
|
|
1543
|
+
alert('Failed to delete orphan stories. Please try again.');
|
|
1544
|
+
} finally {
|
|
1545
|
+
setIsDeletingOrphans(false);
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
// Check for orphans when chats change or connection is established
|
|
1550
|
+
useEffect(() => {
|
|
1551
|
+
checkOrphanStories();
|
|
1552
|
+
}, [checkOrphanStories]);
|
|
1553
|
+
|
|
1495
1554
|
const handleStartRename = (chatId: string, currentTitle: string, e?: React.MouseEvent) => {
|
|
1496
1555
|
if (e) e.stopPropagation();
|
|
1497
1556
|
setContextMenuId(null);
|
|
@@ -1677,6 +1736,30 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1677
1736
|
))}
|
|
1678
1737
|
</div>
|
|
1679
1738
|
|
|
1739
|
+
{/* Orphan Stories Footer */}
|
|
1740
|
+
{orphanCount > 0 && (
|
|
1741
|
+
<div className="sui-orphan-footer">
|
|
1742
|
+
<button
|
|
1743
|
+
className="sui-orphan-delete-btn"
|
|
1744
|
+
onClick={handleDeleteOrphans}
|
|
1745
|
+
disabled={isDeletingOrphans}
|
|
1746
|
+
title={`${orphanCount} story ${orphanCount === 1 ? 'file has' : 'files have'} no associated chat`}
|
|
1747
|
+
>
|
|
1748
|
+
{isDeletingOrphans ? (
|
|
1749
|
+
<>
|
|
1750
|
+
<span className="sui-orphan-spinner" />
|
|
1751
|
+
<span>Deleting...</span>
|
|
1752
|
+
</>
|
|
1753
|
+
) : (
|
|
1754
|
+
<>
|
|
1755
|
+
{Icons.trash}
|
|
1756
|
+
<span>{orphanCount} orphan {orphanCount === 1 ? 'story' : 'stories'}</span>
|
|
1757
|
+
</>
|
|
1758
|
+
)}
|
|
1759
|
+
</button>
|
|
1760
|
+
</div>
|
|
1761
|
+
)}
|
|
1762
|
+
|
|
1680
1763
|
</div>
|
|
1681
1764
|
)}
|
|
1682
1765
|
{!state.sidebarOpen && (
|