@tpitre/story-ui 4.1.2 → 4.3.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/README.md +14 -8
- package/dist/cli/index.js +0 -0
- package/dist/mcp-server/index.js +289 -0
- package/dist/mcp-server/mcp-stdio-server.js +2 -5
- package/dist/story-generator/framework-adapters/index.js +1 -1
- 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/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Story UI revolutionizes component documentation by automatically generating Stor
|
|
|
10
10
|
## Why Story UI?
|
|
11
11
|
|
|
12
12
|
- **Framework Agnostic**: Works with React, Vue, Angular, Svelte, and Web Components
|
|
13
|
-
- **Multi-Provider AI**: Choose between Claude (Anthropic), GPT-
|
|
13
|
+
- **Multi-Provider AI**: Choose between Claude (Anthropic), GPT-4o (OpenAI), or Gemini (Google)
|
|
14
14
|
- **Design System Aware**: Learns your component library and generates appropriate code
|
|
15
15
|
- **Production Ready**: Deploy as a standalone web app with full MCP integration
|
|
16
16
|
- **Zero Lock-in**: Use any component library - Mantine, Vuetify, Angular Material, Shoelace, or your own
|
|
@@ -47,6 +47,12 @@ Story UI will guide you through:
|
|
|
47
47
|
- **TypeScript Support**: Full type-aware story generation
|
|
48
48
|
- **Vision Support**: Attach screenshots for visual component requests
|
|
49
49
|
|
|
50
|
+
### Story Management
|
|
51
|
+
- **Edit Existing Stories**: Modify any generated story through conversation
|
|
52
|
+
- **Delete Stories**: Remove stories directly from the Story UI panel
|
|
53
|
+
- **Orphan Detection**: Find and clean up stories without associated chat history
|
|
54
|
+
- **Full MCP Integration**: Manage stories via Claude Desktop or any MCP-compatible client
|
|
55
|
+
|
|
50
56
|
### Multi-Framework Support
|
|
51
57
|
|
|
52
58
|
| Framework | Design Systems | Status |
|
|
@@ -54,16 +60,16 @@ Story UI will guide you through:
|
|
|
54
60
|
| React | Mantine, Chakra UI, Material UI, Custom | Fully Supported |
|
|
55
61
|
| Vue | Vuetify, Custom | Fully Supported |
|
|
56
62
|
| Angular | Angular Material, Custom | Fully Supported |
|
|
57
|
-
| Svelte |
|
|
63
|
+
| Svelte | Flowbite-Svelte, Custom | Fully Supported |
|
|
58
64
|
| Web Components | Shoelace, Custom | Fully Supported |
|
|
59
65
|
|
|
60
66
|
### Multi-Provider LLM Support
|
|
61
67
|
|
|
62
68
|
| Provider | Models | Best For |
|
|
63
69
|
|----------|--------|----------|
|
|
64
|
-
| **Claude** (Anthropic) |
|
|
65
|
-
| **GPT** (OpenAI) |
|
|
66
|
-
| **Gemini** (Google) |
|
|
70
|
+
| **Claude** (Anthropic) | claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 | Complex reasoning, code quality |
|
|
71
|
+
| **GPT** (OpenAI) | gpt-4o, gpt-4o-mini, o1 | Versatility, speed |
|
|
72
|
+
| **Gemini** (Google) | gemini-2.0-flash, gemini-1.5-pro | Fast generation, cost efficiency |
|
|
67
73
|
|
|
68
74
|
### Production Deployment
|
|
69
75
|
- **Railway**: Node.js backend with file-based story persistence
|
|
@@ -115,7 +121,7 @@ The interactive installer will ask:
|
|
|
115
121
|
```
|
|
116
122
|
? Which AI provider do you prefer?
|
|
117
123
|
> Claude (Anthropic) - Recommended
|
|
118
|
-
OpenAI (GPT-
|
|
124
|
+
OpenAI (GPT-4o)
|
|
119
125
|
Google Gemini
|
|
120
126
|
|
|
121
127
|
? Enter your API key:
|
|
@@ -319,7 +325,7 @@ Once connected, you can use these commands in Claude Desktop:
|
|
|
319
325
|
|
|
320
326
|
## Production Deployment
|
|
321
327
|
|
|
322
|
-
Story UI
|
|
328
|
+
Story UI can be deployed as a standalone web application accessible from anywhere.
|
|
323
329
|
|
|
324
330
|
### Architecture
|
|
325
331
|
|
|
@@ -461,7 +467,7 @@ npx story-ui mcp
|
|
|
461
467
|
|
|
462
468
|
## Upgrading from v2
|
|
463
469
|
|
|
464
|
-
Story UI
|
|
470
|
+
Story UI v4 is backwards compatible with previous configurations. However, to take advantage of new features:
|
|
465
471
|
|
|
466
472
|
1. **Multi-Provider Support**: Add `llmProvider` to your config
|
|
467
473
|
2. **Framework Detection**: Add `framework` to your config for non-React projects
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/mcp-server/index.js
CHANGED
|
@@ -78,6 +78,176 @@ app.get('/mcp/frameworks/detect', detectCurrentFramework);
|
|
|
78
78
|
app.get('/mcp/frameworks/:type', getFrameworkDetails);
|
|
79
79
|
app.post('/mcp/frameworks/validate', validateStoryForFramework);
|
|
80
80
|
app.post('/mcp/frameworks/post-process', postProcessStoryForFramework);
|
|
81
|
+
// MCP story management routes - for Claude Desktop and other MCP clients
|
|
82
|
+
// List all stories
|
|
83
|
+
app.get('/mcp/stories', async (req, res) => {
|
|
84
|
+
try {
|
|
85
|
+
const storiesPath = config.generatedStoriesPath;
|
|
86
|
+
if (!fs.existsSync(storiesPath)) {
|
|
87
|
+
return res.json({ stories: [] });
|
|
88
|
+
}
|
|
89
|
+
const files = fs.readdirSync(storiesPath);
|
|
90
|
+
const stories = files
|
|
91
|
+
.filter(file => file.endsWith('.stories.tsx') || file.endsWith('.stories.ts') || file.endsWith('.stories.svelte'))
|
|
92
|
+
.map(file => {
|
|
93
|
+
const filePath = path.join(storiesPath, file);
|
|
94
|
+
const stats = fs.statSync(filePath);
|
|
95
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
96
|
+
// Extract title from story file
|
|
97
|
+
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
98
|
+
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : file.replace(/\.stories\.(tsx|ts|svelte)$/, '');
|
|
99
|
+
// Remove hash suffix from display title
|
|
100
|
+
title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
|
|
101
|
+
return {
|
|
102
|
+
id: file.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
|
|
103
|
+
storyId: file.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
|
|
104
|
+
fileName: file,
|
|
105
|
+
title,
|
|
106
|
+
lastUpdated: stats.mtime.getTime(),
|
|
107
|
+
createdAt: stats.birthtime.getTime(),
|
|
108
|
+
content
|
|
109
|
+
};
|
|
110
|
+
})
|
|
111
|
+
.sort((a, b) => b.lastUpdated - a.lastUpdated);
|
|
112
|
+
return res.json({ stories });
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error('Error listing stories:', error);
|
|
116
|
+
return res.status(500).json({ error: 'Failed to list stories' });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// Get a specific story by ID
|
|
120
|
+
app.get('/mcp/stories/:storyId', async (req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
const { storyId } = req.params;
|
|
123
|
+
const storiesPath = config.generatedStoriesPath;
|
|
124
|
+
if (!fs.existsSync(storiesPath)) {
|
|
125
|
+
return res.status(404).json({ error: 'Stories directory not found' });
|
|
126
|
+
}
|
|
127
|
+
const files = fs.readdirSync(storiesPath);
|
|
128
|
+
// Extract hash from story ID if in legacy format (story-a1b2c3d4)
|
|
129
|
+
const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
|
|
130
|
+
const hash = hashMatch ? hashMatch[1] : null;
|
|
131
|
+
// Find matching file
|
|
132
|
+
const matchingFile = files.find(file => {
|
|
133
|
+
// Match by hash suffix
|
|
134
|
+
if (hash && file.includes(`-${hash}.stories.`))
|
|
135
|
+
return true;
|
|
136
|
+
// Match by exact ID
|
|
137
|
+
if (file.startsWith(`${storyId}.stories.`))
|
|
138
|
+
return true;
|
|
139
|
+
// Match by fileName
|
|
140
|
+
if (file === storyId)
|
|
141
|
+
return true;
|
|
142
|
+
// Match by ID without extension
|
|
143
|
+
if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
|
|
144
|
+
return true;
|
|
145
|
+
return false;
|
|
146
|
+
});
|
|
147
|
+
if (!matchingFile) {
|
|
148
|
+
return res.status(404).json({ error: `Story with ID ${storyId} not found` });
|
|
149
|
+
}
|
|
150
|
+
const filePath = path.join(storiesPath, matchingFile);
|
|
151
|
+
const stats = fs.statSync(filePath);
|
|
152
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
153
|
+
// Extract title from story file
|
|
154
|
+
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
155
|
+
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, '');
|
|
156
|
+
title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
|
|
157
|
+
return res.json({
|
|
158
|
+
id: matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
|
|
159
|
+
storyId: matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
|
|
160
|
+
fileName: matchingFile,
|
|
161
|
+
title,
|
|
162
|
+
lastUpdated: stats.mtime.getTime(),
|
|
163
|
+
createdAt: stats.birthtime.getTime(),
|
|
164
|
+
timestamp: stats.mtime.getTime(),
|
|
165
|
+
content,
|
|
166
|
+
story: content
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
console.error('Error getting story:', error);
|
|
171
|
+
return res.status(500).json({ error: 'Failed to get story' });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// Get story content (raw code)
|
|
175
|
+
app.get('/mcp/stories/:storyId/content', async (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const { storyId } = req.params;
|
|
178
|
+
const storiesPath = config.generatedStoriesPath;
|
|
179
|
+
if (!fs.existsSync(storiesPath)) {
|
|
180
|
+
return res.status(404).send('Stories directory not found');
|
|
181
|
+
}
|
|
182
|
+
const files = fs.readdirSync(storiesPath);
|
|
183
|
+
// Extract hash from story ID if in legacy format
|
|
184
|
+
const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
|
|
185
|
+
const hash = hashMatch ? hashMatch[1] : null;
|
|
186
|
+
// Find matching file
|
|
187
|
+
const matchingFile = files.find(file => {
|
|
188
|
+
if (hash && file.includes(`-${hash}.stories.`))
|
|
189
|
+
return true;
|
|
190
|
+
if (file.startsWith(`${storyId}.stories.`))
|
|
191
|
+
return true;
|
|
192
|
+
if (file === storyId)
|
|
193
|
+
return true;
|
|
194
|
+
if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
|
|
195
|
+
return true;
|
|
196
|
+
return false;
|
|
197
|
+
});
|
|
198
|
+
if (!matchingFile) {
|
|
199
|
+
return res.status(404).send(`Story with ID ${storyId} not found`);
|
|
200
|
+
}
|
|
201
|
+
const filePath = path.join(storiesPath, matchingFile);
|
|
202
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
203
|
+
res.type('text/plain').send(content);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.error('Error getting story content:', error);
|
|
207
|
+
return res.status(500).send('Failed to get story content');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// Delete a story by ID
|
|
211
|
+
app.delete('/mcp/stories/:storyId', async (req, res) => {
|
|
212
|
+
try {
|
|
213
|
+
const { storyId } = req.params;
|
|
214
|
+
const storiesPath = config.generatedStoriesPath;
|
|
215
|
+
if (!fs.existsSync(storiesPath)) {
|
|
216
|
+
return res.status(404).json({ error: 'Stories directory not found' });
|
|
217
|
+
}
|
|
218
|
+
const files = fs.readdirSync(storiesPath);
|
|
219
|
+
// Extract hash from story ID if in legacy format
|
|
220
|
+
const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
|
|
221
|
+
const hash = hashMatch ? hashMatch[1] : null;
|
|
222
|
+
// Find matching file
|
|
223
|
+
const matchingFile = files.find(file => {
|
|
224
|
+
if (hash && file.includes(`-${hash}.stories.`))
|
|
225
|
+
return true;
|
|
226
|
+
if (file.startsWith(`${storyId}.stories.`))
|
|
227
|
+
return true;
|
|
228
|
+
if (file === storyId)
|
|
229
|
+
return true;
|
|
230
|
+
if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
|
|
231
|
+
return true;
|
|
232
|
+
return false;
|
|
233
|
+
});
|
|
234
|
+
if (!matchingFile) {
|
|
235
|
+
return res.status(404).json({ error: `Story with ID ${storyId} not found` });
|
|
236
|
+
}
|
|
237
|
+
const filePath = path.join(storiesPath, matchingFile);
|
|
238
|
+
fs.unlinkSync(filePath);
|
|
239
|
+
console.log(`🗑️ Deleted story via MCP endpoint: ${matchingFile}`);
|
|
240
|
+
return res.json({
|
|
241
|
+
success: true,
|
|
242
|
+
deleted: matchingFile,
|
|
243
|
+
message: `Story "${matchingFile}" has been deleted successfully.`
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.error('Error deleting story:', error);
|
|
248
|
+
return res.status(500).json({ error: 'Failed to delete story' });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
81
251
|
// File-based story routes - stories are generated as .stories.tsx files
|
|
82
252
|
// Storybook discovers these automatically via its native file system watching
|
|
83
253
|
// Proxy routes for frontend compatibility (maps /story-ui/ to /mcp/)
|
|
@@ -399,6 +569,125 @@ app.delete('/story-ui/stories', async (req, res) => {
|
|
|
399
569
|
return res.status(500).json({ error: 'Failed to clear stories' });
|
|
400
570
|
}
|
|
401
571
|
});
|
|
572
|
+
// Orphan stories management - find and delete stories without associated chats
|
|
573
|
+
// POST to get list of orphans, DELETE to remove them
|
|
574
|
+
app.post('/story-ui/orphan-stories', async (req, res) => {
|
|
575
|
+
try {
|
|
576
|
+
const { chatFileNames } = req.body;
|
|
577
|
+
if (!chatFileNames || !Array.isArray(chatFileNames)) {
|
|
578
|
+
return res.status(400).json({ error: 'chatFileNames array is required' });
|
|
579
|
+
}
|
|
580
|
+
const storiesPath = config.generatedStoriesPath;
|
|
581
|
+
if (!fs.existsSync(storiesPath)) {
|
|
582
|
+
return res.json({ orphans: [], count: 0 });
|
|
583
|
+
}
|
|
584
|
+
const files = fs.readdirSync(storiesPath);
|
|
585
|
+
const storyFiles = files.filter(file => file.endsWith('.stories.tsx') ||
|
|
586
|
+
file.endsWith('.stories.ts') ||
|
|
587
|
+
file.endsWith('.stories.svelte'));
|
|
588
|
+
// Find orphans: stories that don't match any chat fileName
|
|
589
|
+
const orphans = storyFiles.filter(storyFile => {
|
|
590
|
+
// Extract the base name without extension for comparison
|
|
591
|
+
const storyBase = storyFile
|
|
592
|
+
.replace('.stories.tsx', '')
|
|
593
|
+
.replace('.stories.ts', '')
|
|
594
|
+
.replace('.stories.svelte', '');
|
|
595
|
+
// Check if any chat fileName matches this story
|
|
596
|
+
return !chatFileNames.some(chatFileName => {
|
|
597
|
+
if (!chatFileName)
|
|
598
|
+
return false;
|
|
599
|
+
const chatBase = chatFileName
|
|
600
|
+
.replace('.stories.tsx', '')
|
|
601
|
+
.replace('.stories.ts', '')
|
|
602
|
+
.replace('.stories.svelte', '');
|
|
603
|
+
return storyBase === chatBase || storyFile === chatFileName;
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
// Get details for each orphan
|
|
607
|
+
const orphanDetails = orphans.map(fileName => {
|
|
608
|
+
const filePath = path.join(storiesPath, fileName);
|
|
609
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
610
|
+
const stats = fs.statSync(filePath);
|
|
611
|
+
// Extract title from story file
|
|
612
|
+
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
613
|
+
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : fileName.replace(/\.stories\.[a-z]+$/, '');
|
|
614
|
+
// Remove hash suffix from display title
|
|
615
|
+
title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
|
|
616
|
+
return {
|
|
617
|
+
fileName,
|
|
618
|
+
title,
|
|
619
|
+
lastUpdated: stats.mtime.getTime()
|
|
620
|
+
};
|
|
621
|
+
});
|
|
622
|
+
console.log(`📋 Found ${orphans.length} orphan stories out of ${storyFiles.length} total`);
|
|
623
|
+
return res.json({
|
|
624
|
+
orphans: orphanDetails,
|
|
625
|
+
count: orphans.length,
|
|
626
|
+
totalStories: storyFiles.length
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
console.error('Error finding orphan stories:', error);
|
|
631
|
+
return res.status(500).json({ error: 'Failed to find orphan stories' });
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
app.delete('/story-ui/orphan-stories', async (req, res) => {
|
|
635
|
+
try {
|
|
636
|
+
const { chatFileNames } = req.body;
|
|
637
|
+
if (!chatFileNames || !Array.isArray(chatFileNames)) {
|
|
638
|
+
return res.status(400).json({ error: 'chatFileNames array is required' });
|
|
639
|
+
}
|
|
640
|
+
const storiesPath = config.generatedStoriesPath;
|
|
641
|
+
if (!fs.existsSync(storiesPath)) {
|
|
642
|
+
return res.json({ deleted: [], count: 0 });
|
|
643
|
+
}
|
|
644
|
+
const files = fs.readdirSync(storiesPath);
|
|
645
|
+
const storyFiles = files.filter(file => file.endsWith('.stories.tsx') ||
|
|
646
|
+
file.endsWith('.stories.ts') ||
|
|
647
|
+
file.endsWith('.stories.svelte'));
|
|
648
|
+
// Find orphans: stories that don't match any chat fileName
|
|
649
|
+
const orphans = storyFiles.filter(storyFile => {
|
|
650
|
+
const storyBase = storyFile
|
|
651
|
+
.replace('.stories.tsx', '')
|
|
652
|
+
.replace('.stories.ts', '')
|
|
653
|
+
.replace('.stories.svelte', '');
|
|
654
|
+
return !chatFileNames.some(chatFileName => {
|
|
655
|
+
if (!chatFileName)
|
|
656
|
+
return false;
|
|
657
|
+
const chatBase = chatFileName
|
|
658
|
+
.replace('.stories.tsx', '')
|
|
659
|
+
.replace('.stories.ts', '')
|
|
660
|
+
.replace('.stories.svelte', '');
|
|
661
|
+
return storyBase === chatBase || storyFile === chatFileName;
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
// Delete orphans
|
|
665
|
+
const deleted = [];
|
|
666
|
+
const errors = [];
|
|
667
|
+
for (const fileName of orphans) {
|
|
668
|
+
try {
|
|
669
|
+
const filePath = path.join(storiesPath, fileName);
|
|
670
|
+
fs.unlinkSync(filePath);
|
|
671
|
+
deleted.push(fileName);
|
|
672
|
+
console.log(`🗑️ Deleted orphan story: ${fileName}`);
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
errors.push(fileName);
|
|
676
|
+
console.error(`❌ Error deleting orphan ${fileName}:`, err);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
console.log(`✅ Deleted ${deleted.length} orphan stories`);
|
|
680
|
+
return res.json({
|
|
681
|
+
deleted,
|
|
682
|
+
count: deleted.length,
|
|
683
|
+
errors: errors.length > 0 ? errors : undefined
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
console.error('Error deleting orphan stories:', error);
|
|
688
|
+
return res.status(500).json({ error: 'Failed to delete orphan stories' });
|
|
689
|
+
}
|
|
690
|
+
});
|
|
402
691
|
// MCP Remote HTTP transport routes (for Claude Desktop remote connections)
|
|
403
692
|
// Provides Streamable HTTP and legacy SSE endpoints for remote MCP access
|
|
404
693
|
app.use('/mcp-remote', mcpRemoteRouter);
|
|
@@ -27,7 +27,8 @@ dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
|
|
27
27
|
process.env.STORY_UI_MCP_MODE = 'true';
|
|
28
28
|
// Get HTTP server port from environment variables (check multiple possible names)
|
|
29
29
|
const HTTP_PORT = process.env.VITE_STORY_UI_PORT || process.env.STORY_UI_HTTP_PORT || process.env.PORT || '4001';
|
|
30
|
-
|
|
30
|
+
// Allow configurable base URL for Railway/cloud deployments, fallback to localhost for local dev
|
|
31
|
+
const HTTP_BASE_URL = process.env.STORY_UI_HTTP_BASE_URL || `http://localhost:${HTTP_PORT}`;
|
|
31
32
|
// Initialize configuration
|
|
32
33
|
const config = loadUserConfig();
|
|
33
34
|
// Create MCP server instance
|
|
@@ -181,8 +182,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
181
182
|
throw new Error(`Failed to generate story: ${error}`);
|
|
182
183
|
}
|
|
183
184
|
const result = await response.json();
|
|
184
|
-
// Debug log to see what we're getting
|
|
185
|
-
console.error('Story generation result:', JSON.stringify(result, null, 2));
|
|
186
185
|
return {
|
|
187
186
|
content: [{
|
|
188
187
|
type: "text",
|
|
@@ -509,8 +508,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
509
508
|
throw new Error(`Failed to update story: ${error}`);
|
|
510
509
|
}
|
|
511
510
|
const result = await response.json();
|
|
512
|
-
// Debug log to see what we're getting
|
|
513
|
-
console.error('Story update result:', JSON.stringify(result, null, 2));
|
|
514
511
|
return {
|
|
515
512
|
content: [{
|
|
516
513
|
type: "text",
|
|
@@ -150,7 +150,7 @@ class AdapterRegistry {
|
|
|
150
150
|
return {
|
|
151
151
|
systemPrompt: adapter.generateSystemPrompt(config, options),
|
|
152
152
|
componentReference: adapter.generateComponentReference(components, config),
|
|
153
|
-
layoutInstructions: '',
|
|
153
|
+
layoutInstructions: '',
|
|
154
154
|
examples: adapter.generateExamples(config),
|
|
155
155
|
sampleStory: adapter.generateSampleStory(config, components),
|
|
156
156
|
framework: this.detectedFramework || {
|
|
@@ -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
|
+
}
|