@tpitre/story-ui 1.0.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/.env.sample +17 -0
- package/LICENSE +21 -0
- package/README.md +531 -0
- package/dist/cli/index.js +250 -0
- package/dist/cli/setup.js +289 -0
- package/dist/index.js +12 -0
- package/dist/mcp-server/index.js +64 -0
- package/dist/mcp-server/routes/claude.js +30 -0
- package/dist/mcp-server/routes/components.js +26 -0
- package/dist/mcp-server/routes/generateStory.js +289 -0
- package/dist/mcp-server/routes/memoryStories.js +141 -0
- package/dist/mcp-server/routes/storySync.js +147 -0
- package/dist/story-generator/componentDiscovery.js +222 -0
- package/dist/story-generator/configLoader.js +482 -0
- package/dist/story-generator/generateStory.js +19 -0
- package/dist/story-generator/gitignoreManager.js +182 -0
- package/dist/story-generator/inMemoryStoryService.js +128 -0
- package/dist/story-generator/productionGitignoreManager.js +333 -0
- package/dist/story-generator/promptGenerator.js +201 -0
- package/dist/story-generator/storySync.js +201 -0
- package/dist/story-ui.config.js +114 -0
- package/dist/story-ui.config.loader.js +205 -0
- package/package.json +80 -0
- package/templates/README.md +32 -0
- package/templates/StoryUI/StoryUIPanel.stories.tsx +28 -0
- package/templates/StoryUI/StoryUIPanel.tsx +870 -0
- package/templates/StoryUI/index.tsx +2 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import { generateStory } from '../../story-generator/generateStory.js';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { discoverComponents } from '../../story-generator/componentDiscovery.js';
|
|
5
|
+
import { buildClaudePrompt as buildFlexiblePrompt } from '../../story-generator/promptGenerator.js';
|
|
6
|
+
import { loadUserConfig, validateConfig } from '../../story-generator/configLoader.js';
|
|
7
|
+
import { setupProductionGitignore } from '../../story-generator/productionGitignoreManager.js';
|
|
8
|
+
import { getInMemoryStoryService } from '../../story-generator/inMemoryStoryService.js';
|
|
9
|
+
const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages';
|
|
10
|
+
const CLAUDE_MODEL = process.env.CLAUDE_MODEL || 'claude-3-opus-20240229';
|
|
11
|
+
// Legacy constants - now using dynamic discovery
|
|
12
|
+
const COMPONENT_LIST = [];
|
|
13
|
+
const SAMPLE_STORY = '';
|
|
14
|
+
// Legacy component reference - now using dynamic discovery
|
|
15
|
+
const COMPONENT_REFERENCE = '';
|
|
16
|
+
// Legacy function - now uses flexible system
|
|
17
|
+
function buildClaudePrompt(userPrompt) {
|
|
18
|
+
const config = loadUserConfig();
|
|
19
|
+
const components = discoverComponents(config);
|
|
20
|
+
return buildFlexiblePrompt(userPrompt, config, components);
|
|
21
|
+
}
|
|
22
|
+
// Enhanced function that includes conversation context
|
|
23
|
+
function buildClaudePromptWithContext(userPrompt, config, conversation) {
|
|
24
|
+
const components = discoverComponents(config);
|
|
25
|
+
// If no conversation context, use the standard prompt
|
|
26
|
+
if (!conversation || conversation.length <= 1) {
|
|
27
|
+
return buildFlexiblePrompt(userPrompt, config, components);
|
|
28
|
+
}
|
|
29
|
+
// Extract conversation context for modifications
|
|
30
|
+
const conversationContext = conversation
|
|
31
|
+
.slice(0, -1) // Remove the current message (last one)
|
|
32
|
+
.map((msg) => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`)
|
|
33
|
+
.join('\n\n');
|
|
34
|
+
// Get the base prompt
|
|
35
|
+
const basePrompt = buildFlexiblePrompt(userPrompt, config, components);
|
|
36
|
+
// Add conversation context to the prompt
|
|
37
|
+
const contextualPrompt = basePrompt.replace('User request:', `CONVERSATION CONTEXT (for modifications/updates):
|
|
38
|
+
${conversationContext}
|
|
39
|
+
|
|
40
|
+
IMPORTANT: The user is asking to modify/update the story based on the above conversation.
|
|
41
|
+
- Keep the SAME layout structure (number of columns, grid setup) unless explicitly asked to change it
|
|
42
|
+
- Only modify the specific aspects mentioned in the latest request
|
|
43
|
+
- Maintain the overall story concept from the original request
|
|
44
|
+
|
|
45
|
+
Current modification request:`);
|
|
46
|
+
return contextualPrompt;
|
|
47
|
+
}
|
|
48
|
+
function slugify(str) {
|
|
49
|
+
return str
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
52
|
+
.replace(/^-+|-+$/g, '');
|
|
53
|
+
}
|
|
54
|
+
function extractCodeBlock(text) {
|
|
55
|
+
// More flexible code block extraction - accept various language identifiers
|
|
56
|
+
const codeBlock = text.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?([\s\S]*?)```/i);
|
|
57
|
+
return codeBlock ? codeBlock[1].trim() : null;
|
|
58
|
+
}
|
|
59
|
+
async function callClaude(prompt) {
|
|
60
|
+
const apiKey = process.env.CLAUDE_API_KEY;
|
|
61
|
+
if (!apiKey)
|
|
62
|
+
throw new Error('Claude API key not set');
|
|
63
|
+
const response = await fetch(CLAUDE_API_URL, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'x-api-key': apiKey,
|
|
67
|
+
'content-type': 'application/json',
|
|
68
|
+
'anthropic-version': '2023-06-01',
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
model: CLAUDE_MODEL,
|
|
72
|
+
max_tokens: 1024,
|
|
73
|
+
messages: [{ role: 'user', content: prompt }],
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
// Try to extract the main content
|
|
78
|
+
return data?.content?.[0]?.text || data?.completion || '';
|
|
79
|
+
}
|
|
80
|
+
function cleanPromptForTitle(prompt) {
|
|
81
|
+
// Remove common leading phrases (case-insensitive)
|
|
82
|
+
const leadingPhrases = [
|
|
83
|
+
/^generate (a|an|the)? /i,
|
|
84
|
+
/^build (a|an|the)? /i,
|
|
85
|
+
/^create (a|an|the)? /i,
|
|
86
|
+
/^make (a|an|the)? /i,
|
|
87
|
+
/^design (a|an|the)? /i,
|
|
88
|
+
/^show (me )?(a|an|the)? /i,
|
|
89
|
+
/^write (a|an|the)? /i,
|
|
90
|
+
/^produce (a|an|the)? /i,
|
|
91
|
+
/^construct (a|an|the)? /i,
|
|
92
|
+
/^draft (a|an|the)? /i,
|
|
93
|
+
/^compose (a|an|the)? /i,
|
|
94
|
+
/^implement (a|an|the)? /i,
|
|
95
|
+
/^build out (a|an|the)? /i,
|
|
96
|
+
/^add (a|an|the)? /i,
|
|
97
|
+
/^render (a|an|the)? /i,
|
|
98
|
+
/^display (a|an|the)? /i,
|
|
99
|
+
];
|
|
100
|
+
let cleaned = prompt.trim();
|
|
101
|
+
for (const regex of leadingPhrases) {
|
|
102
|
+
cleaned = cleaned.replace(regex, '');
|
|
103
|
+
}
|
|
104
|
+
// More careful punctuation handling - preserve meaningful punctuation in quotes
|
|
105
|
+
return cleaned
|
|
106
|
+
// Replace problematic characters but preserve quoted content structure
|
|
107
|
+
.replace(/[^\w\s'"?!-]/g, ' ') // Keep letters, numbers, spaces, quotes, and basic punctuation
|
|
108
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
109
|
+
.trim()
|
|
110
|
+
.replace(/\b\w/g, c => c.toUpperCase()); // Capitalize words
|
|
111
|
+
}
|
|
112
|
+
async function getClaudeTitle(userPrompt) {
|
|
113
|
+
const titlePrompt = [
|
|
114
|
+
"Given the following UI description, generate a short, clear, human-friendly title suitable for a Storybook navigation item.",
|
|
115
|
+
"Requirements:",
|
|
116
|
+
"- Do not include words like 'Generate', 'Build', or 'Create'",
|
|
117
|
+
"- Keep it under 50 characters",
|
|
118
|
+
"- Use simple, clear language",
|
|
119
|
+
"- Avoid special characters that could break code (use letters, numbers, spaces, hyphens, and basic punctuation only)",
|
|
120
|
+
'',
|
|
121
|
+
'UI description:',
|
|
122
|
+
userPrompt,
|
|
123
|
+
'',
|
|
124
|
+
'Title:'
|
|
125
|
+
].join('\n');
|
|
126
|
+
const aiText = await callClaude(titlePrompt);
|
|
127
|
+
// Take the first non-empty line, trim, and remove quotes if present
|
|
128
|
+
const lines = aiText.split('\n').map(l => l.trim()).filter(Boolean);
|
|
129
|
+
if (lines.length > 0) {
|
|
130
|
+
let title = lines[0].replace(/^['\"]|['\"]$/g, '').trim();
|
|
131
|
+
// Additional sanitization for safety
|
|
132
|
+
title = title
|
|
133
|
+
.replace(/[^\w\s'"?!-]/g, ' ') // Remove problematic characters
|
|
134
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
135
|
+
.trim()
|
|
136
|
+
.slice(0, 50); // Limit length
|
|
137
|
+
return title;
|
|
138
|
+
}
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
function escapeTitleForTS(title) {
|
|
142
|
+
// Escape all characters that could break TypeScript string literals
|
|
143
|
+
return title
|
|
144
|
+
.replace(/\\/g, '\\\\') // Escape backslashes
|
|
145
|
+
.replace(/"/g, '\\"') // Escape double quotes
|
|
146
|
+
.replace(/'/g, "\\'") // Escape single quotes
|
|
147
|
+
.replace(/`/g, '\\`') // Escape backticks
|
|
148
|
+
.replace(/\n/g, '\\n') // Escape newlines
|
|
149
|
+
.replace(/\r/g, '\\r') // Escape carriage returns
|
|
150
|
+
.replace(/\t/g, '\\t'); // Escape tabs
|
|
151
|
+
}
|
|
152
|
+
function fileNameFromTitle(title, hash) {
|
|
153
|
+
// Lowercase, replace spaces/special chars with dashes, remove quotes, truncate
|
|
154
|
+
let base = title
|
|
155
|
+
.toLowerCase()
|
|
156
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
157
|
+
.replace(/^-+|-+$/g, '')
|
|
158
|
+
.replace(/"|'/g, '')
|
|
159
|
+
.slice(0, 60);
|
|
160
|
+
return `${base}-${hash}.stories.tsx`;
|
|
161
|
+
}
|
|
162
|
+
export async function generateStoryFromPrompt(req, res) {
|
|
163
|
+
const { prompt, fileName, conversation } = req.body;
|
|
164
|
+
if (!prompt)
|
|
165
|
+
return res.status(400).json({ error: 'Missing prompt' });
|
|
166
|
+
try {
|
|
167
|
+
// Load and validate configuration
|
|
168
|
+
const config = loadUserConfig();
|
|
169
|
+
const validation = validateConfig(config);
|
|
170
|
+
if (!validation.isValid) {
|
|
171
|
+
return res.status(400).json({
|
|
172
|
+
error: 'Configuration validation failed',
|
|
173
|
+
details: validation.errors
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Set up production-ready environment
|
|
177
|
+
const gitignoreManager = setupProductionGitignore(config);
|
|
178
|
+
const storyService = getInMemoryStoryService(config);
|
|
179
|
+
const isProduction = gitignoreManager.isProductionMode();
|
|
180
|
+
// Check if this is an update to an existing story
|
|
181
|
+
const isUpdate = fileName && conversation && conversation.length > 2;
|
|
182
|
+
// Build prompt with conversation context if available
|
|
183
|
+
const fullPrompt = buildClaudePromptWithContext(prompt, config, conversation);
|
|
184
|
+
console.log('Layout configuration:', JSON.stringify(config.layoutRules, null, 2));
|
|
185
|
+
console.log('Claude prompt:', fullPrompt);
|
|
186
|
+
const aiText = await callClaude(fullPrompt);
|
|
187
|
+
console.log('Claude raw response:', aiText);
|
|
188
|
+
let fileContents = extractCodeBlock(aiText);
|
|
189
|
+
if (!fileContents) {
|
|
190
|
+
// Fallback: try to extract from first import statement onward
|
|
191
|
+
const importIdx = aiText.indexOf('import');
|
|
192
|
+
if (importIdx !== -1) {
|
|
193
|
+
fileContents = aiText.slice(importIdx).trim();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!fileContents || !fileContents.startsWith('import')) {
|
|
197
|
+
console.error('No valid code block or import found in Claude response. Skipping file write.');
|
|
198
|
+
return res.status(500).json({ error: 'Claude did not return a valid code block.' });
|
|
199
|
+
}
|
|
200
|
+
// Generate title based on conversation context
|
|
201
|
+
let aiTitle;
|
|
202
|
+
if (isUpdate) {
|
|
203
|
+
// For updates, try to keep the original title or modify it slightly
|
|
204
|
+
const originalPrompt = conversation.find((msg) => msg.role === 'user')?.content || prompt;
|
|
205
|
+
aiTitle = await getClaudeTitle(originalPrompt);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
aiTitle = await getClaudeTitle(prompt);
|
|
209
|
+
}
|
|
210
|
+
if (!aiTitle || aiTitle.length < 2) {
|
|
211
|
+
// Fallback to cleaned prompt if Claude fails
|
|
212
|
+
aiTitle = cleanPromptForTitle(prompt);
|
|
213
|
+
}
|
|
214
|
+
// Escape the title for TypeScript
|
|
215
|
+
const prettyPrompt = escapeTitleForTS(aiTitle);
|
|
216
|
+
const fixedFileContents = fileContents.replace(/(export default \{\s*\n\s*title:\s*["'])([^"']+)(["'])/, (match, p1, _p2, p3) => {
|
|
217
|
+
const title = config.storyPrefix + prettyPrompt;
|
|
218
|
+
return p1 + title + p3;
|
|
219
|
+
});
|
|
220
|
+
// Generate unique ID and filename
|
|
221
|
+
let hash, finalFileName, storyId;
|
|
222
|
+
if (isUpdate && fileName) {
|
|
223
|
+
// For updates, use existing fileName and ID
|
|
224
|
+
finalFileName = fileName;
|
|
225
|
+
// Extract hash from existing fileName if possible
|
|
226
|
+
const hashMatch = fileName.match(/-([a-f0-9]{8})(?:\.stories\.tsx)?$/);
|
|
227
|
+
hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
|
|
228
|
+
storyId = `story-${hash}`;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// For new stories, generate new IDs
|
|
232
|
+
hash = crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
|
|
233
|
+
finalFileName = fileName || fileNameFromTitle(aiTitle, hash);
|
|
234
|
+
storyId = `story-${hash}`;
|
|
235
|
+
}
|
|
236
|
+
if (isProduction) {
|
|
237
|
+
// Production: Store in memory
|
|
238
|
+
const generatedStory = {
|
|
239
|
+
id: storyId,
|
|
240
|
+
title: aiTitle,
|
|
241
|
+
description: isUpdate ? `Updated: ${prompt}` : prompt,
|
|
242
|
+
content: fixedFileContents,
|
|
243
|
+
createdAt: isUpdate ? (new Date()) : new Date(),
|
|
244
|
+
lastAccessed: new Date(),
|
|
245
|
+
prompt: isUpdate ? conversation.map((msg) => `${msg.role}: ${msg.content}`).join('\n\n') : prompt,
|
|
246
|
+
components: extractComponentsFromContent(fixedFileContents)
|
|
247
|
+
};
|
|
248
|
+
storyService.storeStory(generatedStory);
|
|
249
|
+
console.log(`Story ${isUpdate ? 'updated' : 'stored'} in memory: ${storyId}`);
|
|
250
|
+
res.json({
|
|
251
|
+
success: true,
|
|
252
|
+
fileName: finalFileName,
|
|
253
|
+
storyId,
|
|
254
|
+
title: aiTitle,
|
|
255
|
+
story: fileContents,
|
|
256
|
+
environment: 'production',
|
|
257
|
+
storage: 'in-memory',
|
|
258
|
+
isUpdate
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// Development: Write to file system
|
|
263
|
+
const outPath = generateStory({ fileContents: fixedFileContents, fileName: finalFileName });
|
|
264
|
+
console.log(`Story ${isUpdate ? 'updated' : 'written'} to:`, outPath);
|
|
265
|
+
res.json({
|
|
266
|
+
success: true,
|
|
267
|
+
fileName: finalFileName,
|
|
268
|
+
outPath,
|
|
269
|
+
title: aiTitle,
|
|
270
|
+
story: fileContents,
|
|
271
|
+
environment: 'development',
|
|
272
|
+
storage: 'file-system',
|
|
273
|
+
isUpdate
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
res.status(500).json({ error: err.message || 'Story generation failed' });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Extracts component names from story content
|
|
283
|
+
*/
|
|
284
|
+
function extractComponentsFromContent(content) {
|
|
285
|
+
const componentMatches = content.match(/<[A-Z][A-Za-z0-9]*\s/g);
|
|
286
|
+
if (!componentMatches)
|
|
287
|
+
return [];
|
|
288
|
+
return Array.from(new Set(componentMatches.map(match => match.replace(/[<\s]/g, ''))));
|
|
289
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { getInMemoryStoryService } from '../../story-generator/inMemoryStoryService.js';
|
|
2
|
+
import { STORY_UI_CONFIG } from '../../story-ui.config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Get all stories metadata
|
|
5
|
+
*/
|
|
6
|
+
export function getStoriesMetadata(req, res) {
|
|
7
|
+
try {
|
|
8
|
+
const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
|
|
9
|
+
const metadata = storyService.getStoryMetadata();
|
|
10
|
+
res.json({
|
|
11
|
+
success: true,
|
|
12
|
+
stories: metadata,
|
|
13
|
+
count: metadata.length
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
res.status(500).json({
|
|
18
|
+
success: false,
|
|
19
|
+
error: 'Failed to retrieve stories metadata'
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get a specific story by ID
|
|
25
|
+
*/
|
|
26
|
+
export function getStoryById(req, res) {
|
|
27
|
+
try {
|
|
28
|
+
const { id } = req.params;
|
|
29
|
+
const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
|
|
30
|
+
const story = storyService.getStory(id);
|
|
31
|
+
if (!story) {
|
|
32
|
+
return res.status(404).json({
|
|
33
|
+
success: false,
|
|
34
|
+
error: 'Story not found'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
res.json({
|
|
38
|
+
success: true,
|
|
39
|
+
story
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
res.status(500).json({
|
|
44
|
+
success: false,
|
|
45
|
+
error: 'Failed to retrieve story'
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get story content for Storybook integration
|
|
51
|
+
*/
|
|
52
|
+
export function getStoryContent(req, res) {
|
|
53
|
+
try {
|
|
54
|
+
const { id } = req.params;
|
|
55
|
+
const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
|
|
56
|
+
const content = storyService.getStoryContent(id);
|
|
57
|
+
if (!content) {
|
|
58
|
+
return res.status(404).json({
|
|
59
|
+
success: false,
|
|
60
|
+
error: 'Story content not found'
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Return as TypeScript/JSX content
|
|
64
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
65
|
+
res.send(content);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
res.status(500).json({
|
|
69
|
+
success: false,
|
|
70
|
+
error: 'Failed to retrieve story content'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Delete a story by ID
|
|
76
|
+
*/
|
|
77
|
+
export function deleteStory(req, res) {
|
|
78
|
+
try {
|
|
79
|
+
const { id } = req.params;
|
|
80
|
+
const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
|
|
81
|
+
const deleted = storyService.deleteStory(id);
|
|
82
|
+
if (!deleted) {
|
|
83
|
+
return res.status(404).json({
|
|
84
|
+
success: false,
|
|
85
|
+
error: 'Story not found'
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
res.json({
|
|
89
|
+
success: true,
|
|
90
|
+
message: 'Story deleted successfully'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
res.status(500).json({
|
|
95
|
+
success: false,
|
|
96
|
+
error: 'Failed to delete story'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Clear all stories
|
|
102
|
+
*/
|
|
103
|
+
export function clearAllStories(req, res) {
|
|
104
|
+
try {
|
|
105
|
+
const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
|
|
106
|
+
storyService.clearAllStories();
|
|
107
|
+
res.json({
|
|
108
|
+
success: true,
|
|
109
|
+
message: 'All stories cleared successfully'
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
res.status(500).json({
|
|
114
|
+
success: false,
|
|
115
|
+
error: 'Failed to clear stories'
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get memory usage statistics
|
|
121
|
+
*/
|
|
122
|
+
export function getMemoryStats(req, res) {
|
|
123
|
+
try {
|
|
124
|
+
const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
|
|
125
|
+
const stats = storyService.getMemoryStats();
|
|
126
|
+
res.json({
|
|
127
|
+
success: true,
|
|
128
|
+
stats: {
|
|
129
|
+
...stats,
|
|
130
|
+
totalSizeMB: Math.round(stats.totalSizeBytes / 1024 / 1024 * 100) / 100,
|
|
131
|
+
averageSizeKB: Math.round(stats.averageSizeBytes / 1024 * 100) / 100
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
res.status(500).json({
|
|
137
|
+
success: false,
|
|
138
|
+
error: 'Failed to retrieve memory statistics'
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { getStorySyncService } from '../../story-generator/storySync.js';
|
|
2
|
+
import { loadUserConfig } from '../../story-generator/configLoader.js';
|
|
3
|
+
/**
|
|
4
|
+
* Get all synchronized stories (from both file system and memory)
|
|
5
|
+
*/
|
|
6
|
+
export async function getSyncedStories(req, res) {
|
|
7
|
+
try {
|
|
8
|
+
const config = loadUserConfig();
|
|
9
|
+
const syncService = getStorySyncService(config);
|
|
10
|
+
const stories = await syncService.getAllStories();
|
|
11
|
+
res.json({
|
|
12
|
+
success: true,
|
|
13
|
+
stories: stories.map(story => ({
|
|
14
|
+
id: story.id,
|
|
15
|
+
title: story.title,
|
|
16
|
+
fileName: story.fileName,
|
|
17
|
+
description: story.description,
|
|
18
|
+
createdAt: story.createdAt,
|
|
19
|
+
lastAccessed: story.lastAccessed,
|
|
20
|
+
source: story.source
|
|
21
|
+
})),
|
|
22
|
+
count: stories.length
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
res.status(500).json({
|
|
27
|
+
success: false,
|
|
28
|
+
error: 'Failed to retrieve synchronized stories'
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Delete a story from both file system and memory
|
|
34
|
+
*/
|
|
35
|
+
export async function deleteSyncedStory(req, res) {
|
|
36
|
+
try {
|
|
37
|
+
const { id } = req.params;
|
|
38
|
+
const config = loadUserConfig();
|
|
39
|
+
const syncService = getStorySyncService(config);
|
|
40
|
+
const deleted = await syncService.deleteStory(id);
|
|
41
|
+
if (!deleted) {
|
|
42
|
+
return res.status(404).json({
|
|
43
|
+
success: false,
|
|
44
|
+
error: 'Story not found'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
res.json({
|
|
48
|
+
success: true,
|
|
49
|
+
message: 'Story deleted successfully from all sources'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
res.status(500).json({
|
|
54
|
+
success: false,
|
|
55
|
+
error: 'Failed to delete story'
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Clear all stories from both file system and memory
|
|
61
|
+
*/
|
|
62
|
+
export async function clearAllSyncedStories(req, res) {
|
|
63
|
+
try {
|
|
64
|
+
const config = loadUserConfig();
|
|
65
|
+
const syncService = getStorySyncService(config);
|
|
66
|
+
await syncService.clearAllStories();
|
|
67
|
+
res.json({
|
|
68
|
+
success: true,
|
|
69
|
+
message: 'All stories cleared successfully'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
res.status(500).json({
|
|
74
|
+
success: false,
|
|
75
|
+
error: 'Failed to clear stories'
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Sync chat history with actual stories
|
|
81
|
+
*/
|
|
82
|
+
export async function syncChatHistory(req, res) {
|
|
83
|
+
try {
|
|
84
|
+
const config = loadUserConfig();
|
|
85
|
+
const syncService = getStorySyncService(config);
|
|
86
|
+
const syncResult = await syncService.syncChatHistory();
|
|
87
|
+
res.json({
|
|
88
|
+
success: true,
|
|
89
|
+
...syncResult
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
res.status(500).json({
|
|
94
|
+
success: false,
|
|
95
|
+
error: 'Failed to sync chat history'
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Validate that a chat session corresponds to an actual story
|
|
101
|
+
*/
|
|
102
|
+
export async function validateChatSession(req, res) {
|
|
103
|
+
try {
|
|
104
|
+
const { id } = req.params;
|
|
105
|
+
const config = loadUserConfig();
|
|
106
|
+
const syncService = getStorySyncService(config);
|
|
107
|
+
const isValid = await syncService.validateChatSession(id);
|
|
108
|
+
res.json({
|
|
109
|
+
success: true,
|
|
110
|
+
isValid,
|
|
111
|
+
message: isValid ? 'Chat session is valid' : 'Chat session has no corresponding story'
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
res.status(500).json({
|
|
116
|
+
success: false,
|
|
117
|
+
error: 'Failed to validate chat session'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get a specific synced story by ID
|
|
123
|
+
*/
|
|
124
|
+
export async function getSyncedStoryById(req, res) {
|
|
125
|
+
try {
|
|
126
|
+
const { id } = req.params;
|
|
127
|
+
const config = loadUserConfig();
|
|
128
|
+
const syncService = getStorySyncService(config);
|
|
129
|
+
const story = await syncService.getStory(id);
|
|
130
|
+
if (!story) {
|
|
131
|
+
return res.status(404).json({
|
|
132
|
+
success: false,
|
|
133
|
+
error: 'Story not found'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
res.json({
|
|
137
|
+
success: true,
|
|
138
|
+
story
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
res.status(500).json({
|
|
143
|
+
success: false,
|
|
144
|
+
error: 'Failed to retrieve story'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|