@tpitre/story-ui 2.1.5 → 2.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.
@@ -1,6 +1,7 @@
1
1
  import fetch from 'node-fetch';
2
2
  import { generateStory } from '../../story-generator/generateStory.js';
3
3
  import * as crypto from 'crypto';
4
+ import * as path from 'path';
4
5
  import { buildClaudePrompt as buildFlexiblePrompt } from '../../story-generator/promptGenerator.js';
5
6
  import { loadUserConfig, validateConfig } from '../../story-generator/configLoader.js';
6
7
  import { setupProductionGitignore } from '../../story-generator/productionGitignoreManager.js';
@@ -13,6 +14,8 @@ import { getDocumentation } from '../../story-generator/documentation-sources.js
13
14
  import { postProcessStory } from '../../story-generator/postProcessStory.js';
14
15
  import { validateStory } from '../../story-generator/storyValidator.js';
15
16
  import { StoryHistoryManager } from '../../story-generator/storyHistory.js';
17
+ import { logger } from '../../story-generator/logger.js';
18
+ import { UrlRedirectService } from '../../story-generator/urlRedirectService.js';
16
19
  const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages';
17
20
  const CLAUDE_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-4-20250514';
18
21
  // Legacy constants - now using dynamic discovery
@@ -32,13 +35,13 @@ async function buildClaudePromptWithContext(userPrompt, config, conversation, pr
32
35
  const discovery = new EnhancedComponentDiscovery(config);
33
36
  const components = await discovery.discoverAll();
34
37
  // Always start with component discovery as the authoritative source
35
- console.log(`📦 Discovered ${components.length} components from ${config.importPath}`);
38
+ logger.log(`📦 Discovered ${components.length} components from ${config.importPath}`);
36
39
  const availableComponents = components.map(c => c.name).join(', ');
37
- console.log(`✅ Available components: ${availableComponents}`);
40
+ logger.log(`✅ Available components: ${availableComponents}`);
38
41
  // Build base prompt with discovered components (always required)
39
42
  let prompt = await buildFlexiblePrompt(userPrompt, config, components);
40
43
  // Try to enhance with bundled documentation for usage patterns and design tokens
41
- console.log('📋 Using bundled documentation for enhancement');
44
+ logger.log('📋 Using bundled documentation for enhancement');
42
45
  const documentation = getDocumentation(config.importPath);
43
46
  if (documentation) {
44
47
  const bundledEnhancement = `
@@ -313,7 +316,7 @@ function fileNameFromTitle(title, hash) {
313
316
  return `${base}-${hash}.stories.tsx`;
314
317
  }
315
318
  export async function generateStoryFromPrompt(req, res) {
316
- const { prompt, fileName, conversation } = req.body;
319
+ const { prompt, fileName, conversation, isUpdate, originalTitle, storyId: providedStoryId } = req.body;
317
320
  if (!prompt)
318
321
  return res.status(400).json({ error: 'Missing prompt' });
319
322
  try {
@@ -334,17 +337,36 @@ export async function generateStoryFromPrompt(req, res) {
334
337
  const storyTracker = new StoryTracker(config);
335
338
  // Initialize history manager - use the current working directory
336
339
  const historyManager = new StoryHistoryManager(process.cwd());
340
+ // Initialize URL redirect service
341
+ // Use the same directory as the stories to ensure consistency
342
+ const redirectDir = isProduction ? process.cwd() : path.dirname(config.generatedStoriesPath);
343
+ const redirectService = new UrlRedirectService(redirectDir);
337
344
  // Check if this is an update to an existing story
338
- const isUpdate = fileName && conversation && conversation.length > 2;
345
+ // Use the explicit isUpdate flag from request, or fallback to old logic
346
+ const isActualUpdate = req.body.isUpdate || (fileName && conversation && conversation.length > 2);
339
347
  // Get previous code if this is an update
340
348
  let previousCode;
341
349
  let parentVersionId;
342
- if (isUpdate && fileName) {
350
+ let oldTitle;
351
+ let oldStoryUrl;
352
+ if (isActualUpdate && fileName) {
343
353
  const currentVersion = historyManager.getCurrentVersion(fileName);
344
354
  if (currentVersion) {
345
355
  previousCode = currentVersion.code;
346
356
  parentVersionId = currentVersion.id;
347
- console.log('🔄 Found previous version for iteration');
357
+ logger.log('🔄 Found previous version for iteration');
358
+ // Extract the old title from previous code
359
+ const titleMatch = previousCode.match(/title:\s*["']([^"']+)['"]/);
360
+ if (titleMatch) {
361
+ oldTitle = titleMatch[1];
362
+ // Remove the prefix to get clean title for URL generation
363
+ const cleanOldTitle = oldTitle.replace(config.storyPrefix || 'Generated/', '');
364
+ // Convert title to Storybook URL format
365
+ oldStoryUrl = `/story/${cleanOldTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-')}--primary`;
366
+ logger.log('📌 Previous title:', oldTitle);
367
+ logger.log('📌 Clean title for URL:', cleanOldTitle);
368
+ logger.log('📌 Previous URL:', oldStoryUrl);
369
+ }
348
370
  }
349
371
  }
350
372
  // --- Start of Validation and Retry Loop ---
@@ -356,13 +378,13 @@ export async function generateStoryFromPrompt(req, res) {
356
378
  const messages = [{ role: 'user', content: initialPrompt }];
357
379
  while (attempts < maxRetries) {
358
380
  attempts++;
359
- console.log(`--- Story Generation Attempt ${attempts} ---`);
381
+ logger.log(`--- Story Generation Attempt ${attempts} ---`);
360
382
  const claudeResponse = await callClaude(messages);
361
383
  const extractedCode = extractCodeBlock(claudeResponse);
362
384
  if (!extractedCode) {
363
385
  aiText = claudeResponse; // Use raw response if no code block
364
386
  if (attempts < maxRetries) {
365
- console.log('No code block found, retrying...');
387
+ logger.log('No code block found, retrying...');
366
388
  messages.push({ role: 'assistant', content: aiText });
367
389
  messages.push({ role: 'user', content: 'You did not provide a code block. Please provide the complete story in a single `tsx` code block.' });
368
390
  continue;
@@ -377,11 +399,11 @@ export async function generateStoryFromPrompt(req, res) {
377
399
  }
378
400
  validationErrors = validateStory(aiText);
379
401
  if (validationErrors.length === 0) {
380
- console.log('✅ Validation successful!');
402
+ logger.log('✅ Validation successful!');
381
403
  break; // Exit loop on success
382
404
  }
383
- console.log(`❌ Validation failed with ${validationErrors.length} errors:`);
384
- validationErrors.forEach(err => console.log(` - Line ${err.line}: ${err.message}`));
405
+ logger.log(`❌ Validation failed with ${validationErrors.length} errors:`);
406
+ validationErrors.forEach(err => logger.log(` - Line ${err.line}: ${err.message}`));
385
407
  if (attempts < maxRetries) {
386
408
  const errorFeedback = validationErrors
387
409
  .map(err => `- Line ${err.line}: ${err.message}`)
@@ -397,7 +419,7 @@ export async function generateStoryFromPrompt(req, res) {
397
419
  // For now, we'll proceed with the last attempt and let the user see the result
398
420
  }
399
421
  // --- End of Validation and Retry Loop ---
400
- console.log('Claude final response:', aiText);
422
+ logger.log('Claude final response:', aiText);
401
423
  // Create enhanced component discovery for validation
402
424
  const discovery = new EnhancedComponentDiscovery(config);
403
425
  await discovery.discoverAll();
@@ -416,7 +438,7 @@ export async function generateStoryFromPrompt(req, res) {
416
438
  const validationResult = extractAndValidateCodeBlock(aiText, config);
417
439
  let fileContents;
418
440
  let hasValidationWarnings = false;
419
- console.log('Validation result:', {
441
+ logger.log('Validation result:', {
420
442
  isValid: validationResult.isValid,
421
443
  errors: validationResult.errors,
422
444
  warnings: validationResult.warnings,
@@ -425,7 +447,7 @@ export async function generateStoryFromPrompt(req, res) {
425
447
  if (!validationResult.isValid && !validationResult.fixedCode) {
426
448
  console.error('Generated code validation failed:', validationResult.errors);
427
449
  // Create fallback story only if we can't fix the code
428
- console.log('Creating fallback story due to validation failure');
450
+ logger.log('Creating fallback story due to validation failure');
429
451
  fileContents = createFallbackStory(prompt, config);
430
452
  hasValidationWarnings = true;
431
453
  }
@@ -434,7 +456,7 @@ export async function generateStoryFromPrompt(req, res) {
434
456
  if (validationResult.fixedCode) {
435
457
  fileContents = validationResult.fixedCode;
436
458
  hasValidationWarnings = true;
437
- console.log('Using auto-fixed code');
459
+ logger.log('Using auto-fixed code');
438
460
  }
439
461
  else {
440
462
  // Extract the validated code
@@ -455,7 +477,7 @@ export async function generateStoryFromPrompt(req, res) {
455
477
  }
456
478
  if (validationResult.warnings && validationResult.warnings.length > 0) {
457
479
  hasValidationWarnings = true;
458
- console.log('Validation warnings:', validationResult.warnings);
480
+ logger.log('Validation warnings:', validationResult.warnings);
459
481
  }
460
482
  }
461
483
  if (!fileContents) {
@@ -470,12 +492,18 @@ export async function generateStoryFromPrompt(req, res) {
470
492
  let fixedFileContents = postProcessStory(fileContents, config.importPath);
471
493
  // Generate title based on conversation context
472
494
  let aiTitle;
473
- if (isUpdate) {
474
- // For updates, try to keep the original title or modify it slightly
495
+ if (isActualUpdate && originalTitle) {
496
+ // For updates, preserve the original title
497
+ aiTitle = originalTitle;
498
+ logger.log('📝 Preserving original title for update:', aiTitle);
499
+ }
500
+ else if (isActualUpdate) {
501
+ // For updates without original title, try to keep the original title or modify it slightly
475
502
  const originalPrompt = conversation.find((msg) => msg.role === 'user')?.content || prompt;
476
503
  aiTitle = await getClaudeTitle(originalPrompt);
477
504
  }
478
505
  else {
506
+ // For new stories, generate a new title
479
507
  aiTitle = await getClaudeTitle(prompt);
480
508
  }
481
509
  if (!aiTitle || aiTitle.length < 2) {
@@ -486,20 +514,26 @@ export async function generateStoryFromPrompt(req, res) {
486
514
  const prettyPrompt = escapeTitleForTS(aiTitle);
487
515
  // Fix title with storyPrefix - handle both single-line and multi-line formats
488
516
  fixedFileContents = fixedFileContents.replace(/(const\s+meta\s*=\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
489
- const title = config.storyPrefix + prettyPrompt;
490
- return p1 + title + p3;
517
+ // Check if the title already has the prefix to avoid double prefixing
518
+ const titleToUse = prettyPrompt.startsWith(config.storyPrefix)
519
+ ? prettyPrompt
520
+ : config.storyPrefix + prettyPrompt;
521
+ return p1 + titleToUse + p3;
491
522
  });
492
523
  // Fallback: export default { title: "..." } format
493
524
  if (!fixedFileContents.includes(config.storyPrefix)) {
494
525
  fixedFileContents = fixedFileContents.replace(/(export\s+default\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
495
- const title = config.storyPrefix + prettyPrompt;
496
- return p1 + title + p3;
526
+ // Check if the title already has the prefix to avoid double prefixing
527
+ const titleToUse = prettyPrompt.startsWith(config.storyPrefix)
528
+ ? prettyPrompt
529
+ : config.storyPrefix + prettyPrompt;
530
+ return p1 + titleToUse + p3;
497
531
  });
498
532
  }
499
533
  // Check if this is an update to an existing story
500
534
  // ONLY consider it an update if we're in the same conversation context
501
535
  let existingStory = null;
502
- if (isUpdate && fileName) {
536
+ if (isActualUpdate && fileName) {
503
537
  // When updating within a conversation, look for the story by fileName
504
538
  existingStory = storyTracker.findByTitle(aiTitle);
505
539
  if (existingStory && existingStory.fileName !== fileName) {
@@ -510,15 +544,23 @@ export async function generateStoryFromPrompt(req, res) {
510
544
  // Remove the automatic "find by prompt" logic that was preventing duplicates
511
545
  // Generate unique ID and filename
512
546
  let hash, finalFileName, storyId;
513
- let isActuallyUpdate = false;
514
- if (isUpdate && fileName) {
515
- // For conversation-based updates, use existing fileName and ID
547
+ if (isActualUpdate && (fileName || providedStoryId)) {
548
+ // For updates, preserve the existing fileName and ID
516
549
  finalFileName = fileName;
517
- // Extract hash from existing fileName if possible
518
- const hashMatch = fileName.match(/-([a-f0-9]{8})(?:\.stories\.tsx)?$/);
519
- hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
520
- storyId = `story-${hash}`;
521
- isActuallyUpdate = true;
550
+ // Use provided storyId or extract from fileName
551
+ if (providedStoryId) {
552
+ storyId = providedStoryId;
553
+ // Extract hash from storyId
554
+ const hashMatch = providedStoryId.match(/^story-([a-f0-9]{8})$/);
555
+ hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
556
+ }
557
+ else {
558
+ // Extract hash from existing fileName if possible
559
+ const hashMatch = fileName.match(/-([a-f0-9]{8})(?:\.stories\.tsx)?$/);
560
+ hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
561
+ storyId = `story-${hash}`;
562
+ }
563
+ logger.log('📌 Preserving story identity for update:', { storyId, fileName: finalFileName });
522
564
  }
523
565
  else {
524
566
  // For new stories, ALWAYS generate new IDs with timestamp to ensure uniqueness
@@ -526,18 +568,18 @@ export async function generateStoryFromPrompt(req, res) {
526
568
  hash = crypto.createHash('sha1').update(prompt + timestamp).digest('hex').slice(0, 8);
527
569
  finalFileName = fileName || fileNameFromTitle(aiTitle, hash);
528
570
  storyId = `story-${hash}`;
529
- isActuallyUpdate = false;
571
+ logger.log('🆕 Creating new story:', { storyId, fileName: finalFileName });
530
572
  }
531
573
  if (isProduction) {
532
574
  // Production: Store in memory
533
575
  const generatedStory = {
534
576
  id: storyId,
535
577
  title: aiTitle,
536
- description: isActuallyUpdate ? `Updated: ${prompt}` : prompt,
578
+ description: isActualUpdate ? `Updated: ${prompt}` : prompt,
537
579
  content: fixedFileContents,
538
- createdAt: isActuallyUpdate ? (new Date()) : new Date(),
580
+ createdAt: isActualUpdate ? (new Date()) : new Date(),
539
581
  lastAccessed: new Date(),
540
- prompt: isActuallyUpdate ? conversation.map((msg) => `${msg.role}: ${msg.content}`).join('\n\n') : prompt,
582
+ prompt: isActualUpdate ? conversation.map((msg) => `${msg.role}: ${msg.content}`).join('\n\n') : prompt,
541
583
  components: extractComponentsFromContent(fixedFileContents)
542
584
  };
543
585
  storyService.storeStory(generatedStory);
@@ -554,7 +596,23 @@ export async function generateStoryFromPrompt(req, res) {
554
596
  storyTracker.registerStory(mapping);
555
597
  // Save to history
556
598
  historyManager.addVersion(finalFileName, prompt, fixedFileContents, parentVersionId);
557
- console.log(`Story ${isActuallyUpdate ? 'updated' : 'stored'} in memory: ${storyId}`);
599
+ logger.log(`Story ${isActualUpdate ? 'updated' : 'stored'} in memory: ${storyId}`);
600
+ // Track URL redirect if this is an update and the title changed
601
+ if (isActualUpdate && oldTitle && oldStoryUrl) {
602
+ // Extract the new title from the fixed content
603
+ const newTitleMatch = fixedFileContents.match(/title:\s*["']([^"']+)['"]/);
604
+ if (newTitleMatch) {
605
+ const newTitle = newTitleMatch[1];
606
+ // Remove the prefix to get clean title for URL
607
+ const cleanNewTitle = newTitle.replace(config.storyPrefix, '');
608
+ const cleanOldTitle = oldTitle.replace(config.storyPrefix, '');
609
+ const newStoryUrl = `/story/${cleanNewTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-')}--primary`;
610
+ if (oldStoryUrl !== newStoryUrl) {
611
+ redirectService.addRedirect(oldStoryUrl, newStoryUrl, cleanOldTitle, cleanNewTitle, storyId);
612
+ logger.log(`🔀 Added redirect: ${oldStoryUrl} → ${newStoryUrl}`);
613
+ }
614
+ }
615
+ }
558
616
  res.json({
559
617
  success: true,
560
618
  fileName: finalFileName,
@@ -563,7 +621,7 @@ export async function generateStoryFromPrompt(req, res) {
563
621
  story: fileContents,
564
622
  environment: 'production',
565
623
  storage: 'in-memory',
566
- isUpdate: isActuallyUpdate,
624
+ isUpdate: isActualUpdate,
567
625
  validation: {
568
626
  hasWarnings: hasValidationWarnings,
569
627
  errors: validationResult?.errors || [],
@@ -591,16 +649,33 @@ export async function generateStoryFromPrompt(req, res) {
591
649
  storyTracker.registerStory(mapping);
592
650
  // Save to history
593
651
  historyManager.addVersion(finalFileName, prompt, fixedFileContents, parentVersionId);
594
- console.log(`Story ${isActuallyUpdate ? 'updated' : 'written'} to:`, outPath);
652
+ logger.log(`Story ${isActualUpdate ? 'updated' : 'written'} to:`, outPath);
653
+ // Track URL redirect if this is an update and the title changed
654
+ if (isActualUpdate && oldTitle && oldStoryUrl) {
655
+ // Extract the new title from the fixed content
656
+ const newTitleMatch = fixedFileContents.match(/title:\s*["']([^"']+)['"]/);
657
+ if (newTitleMatch) {
658
+ const newTitle = newTitleMatch[1];
659
+ // Remove the prefix to get clean title for URL
660
+ const cleanNewTitle = newTitle.replace(config.storyPrefix, '');
661
+ const cleanOldTitle = oldTitle.replace(config.storyPrefix, '');
662
+ const newStoryUrl = `/story/${cleanNewTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-')}--primary`;
663
+ if (oldStoryUrl !== newStoryUrl) {
664
+ redirectService.addRedirect(oldStoryUrl, newStoryUrl, cleanOldTitle, cleanNewTitle, storyId);
665
+ logger.log(`🔀 Added redirect: ${oldStoryUrl} → ${newStoryUrl}`);
666
+ }
667
+ }
668
+ }
595
669
  res.json({
596
670
  success: true,
597
671
  fileName: finalFileName,
672
+ storyId,
598
673
  outPath,
599
674
  title: aiTitle,
600
675
  story: fileContents,
601
676
  environment: 'development',
602
677
  storage: 'file-system',
603
- isUpdate: isActuallyUpdate,
678
+ isUpdate: isActualUpdate,
604
679
  validation: {
605
680
  hasWarnings: hasValidationWarnings,
606
681
  errors: validationResult?.errors || [],
@@ -0,0 +1,214 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getInMemoryStoryService } from '../../story-generator/inMemoryStoryService.js';
4
+ import { loadUserConfig } from '../../story-generator/configLoader.js';
5
+ import { setupProductionGitignore } from '../../story-generator/productionGitignoreManager.js';
6
+ /**
7
+ * Get all stories from both memory and file system
8
+ */
9
+ export function getAllStories(req, res) {
10
+ try {
11
+ const config = loadUserConfig();
12
+ const gitignoreManager = setupProductionGitignore(config);
13
+ const storyService = getInMemoryStoryService(config);
14
+ let allStories = [];
15
+ // Get stories from memory
16
+ const memoryStories = storyService.getStoryMetadata();
17
+ allStories = [...memoryStories];
18
+ // In development mode, also check file system
19
+ if (!gitignoreManager.isProductionMode() && config.generatedStoriesPath) {
20
+ try {
21
+ if (fs.existsSync(config.generatedStoriesPath)) {
22
+ const files = fs.readdirSync(config.generatedStoriesPath);
23
+ const fileStories = files
24
+ .filter(file => file.endsWith('.stories.tsx'))
25
+ .map(file => {
26
+ const fileName = file;
27
+ const hash = file.match(/-([a-f0-9]{8})\.stories\.tsx$/)?.[1] || '';
28
+ const storyId = hash ? `story-${hash}` : file.replace('.stories.tsx', '');
29
+ // Try to read the file to get the title
30
+ let title = file.replace('.stories.tsx', '').replace(/-/g, ' ');
31
+ try {
32
+ const filePath = path.join(config.generatedStoriesPath, file);
33
+ const content = fs.readFileSync(filePath, 'utf-8');
34
+ const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
35
+ if (titleMatch) {
36
+ title = titleMatch[1].replace('Generated/', '');
37
+ }
38
+ }
39
+ catch (e) {
40
+ // Use filename as fallback
41
+ }
42
+ return {
43
+ id: storyId,
44
+ fileName,
45
+ title,
46
+ createdAt: fs.statSync(path.join(config.generatedStoriesPath, file)).birthtime,
47
+ storage: 'file-system'
48
+ };
49
+ });
50
+ // Merge, avoiding duplicates
51
+ const memoryIds = new Set(memoryStories.map(s => s.id));
52
+ const uniqueFileStories = fileStories.filter(s => !memoryIds.has(s.id));
53
+ allStories = [...allStories, ...uniqueFileStories];
54
+ }
55
+ }
56
+ catch (error) {
57
+ console.error('Error reading file system stories:', error);
58
+ }
59
+ }
60
+ res.json(allStories);
61
+ }
62
+ catch (error) {
63
+ console.error('Error in getAllStories:', error);
64
+ res.status(500).json({ error: 'Failed to retrieve stories' });
65
+ }
66
+ }
67
+ /**
68
+ * Get a specific story by ID from memory or file system
69
+ */
70
+ export function getStoryById(req, res) {
71
+ try {
72
+ const { id } = req.params;
73
+ const config = loadUserConfig();
74
+ const gitignoreManager = setupProductionGitignore(config);
75
+ const storyService = getInMemoryStoryService(config);
76
+ // First try memory
77
+ const memoryStory = storyService.getStory(id);
78
+ if (memoryStory) {
79
+ return res.json(memoryStory);
80
+ }
81
+ // In development, try file system
82
+ if (!gitignoreManager.isProductionMode() && config.generatedStoriesPath) {
83
+ // Try to find by story ID pattern
84
+ const files = fs.readdirSync(config.generatedStoriesPath);
85
+ // Extract hash from story ID (e.g., "story-abc123" -> "abc123")
86
+ const hashMatch = id.match(/^story-([a-f0-9]{8})$/);
87
+ const hash = hashMatch ? hashMatch[1] : null;
88
+ // Find file by hash or exact match
89
+ const matchingFile = files.find(file => {
90
+ if (hash && file.includes(`-${hash}.stories.tsx`))
91
+ return true;
92
+ if (file === `${id}.stories.tsx`)
93
+ return true;
94
+ if (file === id)
95
+ return true;
96
+ return false;
97
+ });
98
+ if (matchingFile) {
99
+ const filePath = path.join(config.generatedStoriesPath, matchingFile);
100
+ const content = fs.readFileSync(filePath, 'utf-8');
101
+ const stats = fs.statSync(filePath);
102
+ // Extract title from content
103
+ let title = matchingFile.replace('.stories.tsx', '').replace(/-/g, ' ');
104
+ const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
105
+ if (titleMatch) {
106
+ title = titleMatch[1].replace('Generated/', '');
107
+ }
108
+ return res.json({
109
+ id,
110
+ fileName: matchingFile,
111
+ title,
112
+ content,
113
+ createdAt: stats.birthtime,
114
+ storage: 'file-system'
115
+ });
116
+ }
117
+ }
118
+ res.status(404).json({ error: 'Story not found' });
119
+ }
120
+ catch (error) {
121
+ console.error('Error in getStoryById:', error);
122
+ res.status(500).json({ error: 'Failed to retrieve story' });
123
+ }
124
+ }
125
+ /**
126
+ * Get story content by ID
127
+ */
128
+ export function getStoryContent(req, res) {
129
+ try {
130
+ const { id } = req.params;
131
+ const config = loadUserConfig();
132
+ const gitignoreManager = setupProductionGitignore(config);
133
+ const storyService = getInMemoryStoryService(config);
134
+ // First try memory
135
+ const content = storyService.getStoryContent(id);
136
+ if (content) {
137
+ res.setHeader('Content-Type', 'text/plain');
138
+ return res.send(content);
139
+ }
140
+ // In development, try file system
141
+ if (!gitignoreManager.isProductionMode() && config.generatedStoriesPath) {
142
+ const files = fs.readdirSync(config.generatedStoriesPath);
143
+ // Extract hash from story ID
144
+ const hashMatch = id.match(/^story-([a-f0-9]{8})$/);
145
+ const hash = hashMatch ? hashMatch[1] : null;
146
+ const matchingFile = files.find(file => {
147
+ if (hash && file.includes(`-${hash}.stories.tsx`))
148
+ return true;
149
+ if (file === `${id}.stories.tsx`)
150
+ return true;
151
+ if (file === id)
152
+ return true;
153
+ return false;
154
+ });
155
+ if (matchingFile) {
156
+ const filePath = path.join(config.generatedStoriesPath, matchingFile);
157
+ const content = fs.readFileSync(filePath, 'utf-8');
158
+ res.setHeader('Content-Type', 'text/plain');
159
+ return res.send(content);
160
+ }
161
+ }
162
+ res.status(404).json({ error: 'Story content not found' });
163
+ }
164
+ catch (error) {
165
+ console.error('Error in getStoryContent:', error);
166
+ res.status(500).json({ error: 'Failed to retrieve story content' });
167
+ }
168
+ }
169
+ /**
170
+ * Delete a story by ID
171
+ */
172
+ export function deleteStory(req, res) {
173
+ try {
174
+ const { id } = req.params;
175
+ const config = loadUserConfig();
176
+ const gitignoreManager = setupProductionGitignore(config);
177
+ const storyService = getInMemoryStoryService(config);
178
+ console.log(`🗑️ Attempting to delete story: ${id}`);
179
+ // First try memory
180
+ const memoryDeleted = storyService.deleteStory(id);
181
+ if (memoryDeleted) {
182
+ console.log(`✅ Deleted story from memory: ${id}`);
183
+ return res.json({ success: true, message: 'Story deleted from memory' });
184
+ }
185
+ // In development, try file system
186
+ if (!gitignoreManager.isProductionMode() && config.generatedStoriesPath) {
187
+ const files = fs.readdirSync(config.generatedStoriesPath);
188
+ // Extract hash from story ID
189
+ const hashMatch = id.match(/^story-([a-f0-9]{8})$/);
190
+ const hash = hashMatch ? hashMatch[1] : null;
191
+ const matchingFile = files.find(file => {
192
+ if (hash && file.includes(`-${hash}.stories.tsx`))
193
+ return true;
194
+ if (file === `${id}.stories.tsx`)
195
+ return true;
196
+ if (file === id)
197
+ return true;
198
+ return false;
199
+ });
200
+ if (matchingFile) {
201
+ const filePath = path.join(config.generatedStoriesPath, matchingFile);
202
+ fs.unlinkSync(filePath);
203
+ console.log(`✅ Deleted story file: ${filePath}`);
204
+ return res.json({ success: true, message: 'Story deleted from file system' });
205
+ }
206
+ }
207
+ console.log(`❌ Story not found: ${id}`);
208
+ res.status(404).json({ error: 'Story not found' });
209
+ }
210
+ catch (error) {
211
+ console.error('Error in deleteStory:', error);
212
+ res.status(500).json({ error: 'Failed to delete story' });
213
+ }
214
+ }
@@ -1,11 +1,12 @@
1
1
  import { getInMemoryStoryService } from '../../story-generator/inMemoryStoryService.js';
2
- import { STORY_UI_CONFIG } from '../../story-ui.config.js';
2
+ import { loadUserConfig } from '../../story-generator/configLoader.js';
3
3
  /**
4
4
  * Get all stories metadata
5
5
  */
6
6
  export function getStoriesMetadata(req, res) {
7
7
  try {
8
- const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
8
+ const config = loadUserConfig();
9
+ const storyService = getInMemoryStoryService(config);
9
10
  const metadata = storyService.getStoryMetadata();
10
11
  res.json({
11
12
  success: true,
@@ -26,7 +27,8 @@ export function getStoriesMetadata(req, res) {
26
27
  export function getStoryById(req, res) {
27
28
  try {
28
29
  const { id } = req.params;
29
- const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
30
+ const config = loadUserConfig();
31
+ const storyService = getInMemoryStoryService(config);
30
32
  const story = storyService.getStory(id);
31
33
  if (!story) {
32
34
  return res.status(404).json({
@@ -52,7 +54,8 @@ export function getStoryById(req, res) {
52
54
  export function getStoryContent(req, res) {
53
55
  try {
54
56
  const { id } = req.params;
55
- const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
57
+ const config = loadUserConfig();
58
+ const storyService = getInMemoryStoryService(config);
56
59
  const content = storyService.getStoryContent(id);
57
60
  if (!content) {
58
61
  return res.status(404).json({
@@ -77,7 +80,8 @@ export function getStoryContent(req, res) {
77
80
  export function deleteStory(req, res) {
78
81
  try {
79
82
  const { id } = req.params;
80
- const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
83
+ const config = loadUserConfig();
84
+ const storyService = getInMemoryStoryService(config);
81
85
  const deleted = storyService.deleteStory(id);
82
86
  if (!deleted) {
83
87
  return res.status(404).json({
@@ -102,7 +106,8 @@ export function deleteStory(req, res) {
102
106
  */
103
107
  export function clearAllStories(req, res) {
104
108
  try {
105
- const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
109
+ const config = loadUserConfig();
110
+ const storyService = getInMemoryStoryService(config);
106
111
  storyService.clearAllStories();
107
112
  res.json({
108
113
  success: true,
@@ -121,7 +126,8 @@ export function clearAllStories(req, res) {
121
126
  */
122
127
  export function getMemoryStats(req, res) {
123
128
  try {
124
- const storyService = getInMemoryStoryService(STORY_UI_CONFIG);
129
+ const config = loadUserConfig();
130
+ const storyService = getInMemoryStoryService(config);
125
131
  const stats = storyService.getMemoryStats();
126
132
  res.json({
127
133
  success: true,