@tpitre/story-ui 3.10.0 → 3.10.2

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 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../cli/update.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AA2SD;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkItF;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CA+BpC"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../cli/update.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAgTD;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkItF;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CA+BpC"}
@@ -12,6 +12,11 @@ const MANAGED_FILES = [
12
12
  target: 'src/stories/StoryUI/StoryUIPanel.tsx',
13
13
  description: 'Main chat panel component'
14
14
  },
15
+ {
16
+ source: 'templates/StoryUI/StoryUIPanel.css',
17
+ target: 'src/stories/StoryUI/StoryUIPanel.css',
18
+ description: 'Panel styles'
19
+ },
15
20
  {
16
21
  source: 'templates/StoryUI/StoryUIPanel.mdx',
17
22
  target: 'src/stories/StoryUI/StoryUIPanel.mdx',
@@ -1 +1 @@
1
- {"version":3,"file":"generateStory.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAgc5C,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,2DAkgBxE"}
1
+ {"version":3,"file":"generateStory.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAgc5C,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,2DAwfxE"}
@@ -605,23 +605,68 @@ export async function generateStoryFromPrompt(req, res) {
605
605
  // Fallback to cleaned prompt if Claude fails
606
606
  aiTitle = cleanPromptForTitle(prompt);
607
607
  }
608
- // Escape the title for TypeScript
608
+ // Generate unique ID and filename FIRST so we can include hash in title
609
+ // This is done early to ensure unique titles prevent Storybook duplicate ID errors
610
+ const fileExtension = frameworkAdapter?.defaultExtension || '.stories.tsx';
611
+ const timestamp = Date.now();
612
+ // Always generate a hash - either from existing IDs or new
613
+ let hash;
614
+ let finalFileName;
615
+ let storyId;
616
+ if (isActualUpdate && (fileName || providedStoryId)) {
617
+ // For updates, preserve the existing fileName and ID
618
+ if (providedStoryId) {
619
+ storyId = providedStoryId;
620
+ const hashMatch = providedStoryId.match(/^story-([a-f0-9]{8})$/);
621
+ hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt + timestamp).digest('hex').slice(0, 8);
622
+ finalFileName = fileName || `${providedStoryId}.stories.tsx`;
623
+ logger.log('📝 Using provided storyId:', finalFileName);
624
+ }
625
+ else if (fileName) {
626
+ const hashMatch = fileName.match(/-([a-f0-9]{8})(?:\.stories\.tsx)?$/);
627
+ hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt + timestamp).digest('hex').slice(0, 8);
628
+ storyId = `story-${hash}`;
629
+ finalFileName = fileName;
630
+ }
631
+ else {
632
+ // Fallback - should not reach here given the if condition, but satisfies TypeScript
633
+ hash = crypto.createHash('sha1').update(prompt + timestamp).digest('hex').slice(0, 8);
634
+ storyId = `story-${hash}`;
635
+ finalFileName = fileNameFromTitle(aiTitle, hash, fileExtension);
636
+ }
637
+ if (!finalFileName.endsWith('.stories.tsx')) {
638
+ finalFileName = finalFileName + '.stories.tsx';
639
+ }
640
+ logger.log('📌 Preserving story identity for update:', { storyId, fileName: finalFileName });
641
+ }
642
+ else {
643
+ // For new stories, ALWAYS generate new IDs with timestamp to ensure uniqueness
644
+ hash = crypto.createHash('sha1').update(prompt + timestamp).digest('hex').slice(0, 8);
645
+ finalFileName = fileName || fileNameFromTitle(aiTitle, hash, fileExtension);
646
+ storyId = `story-${hash}`;
647
+ logger.log('🆕 Creating new story:', { storyId, fileName: finalFileName });
648
+ }
649
+ // Escape the title for TypeScript and append hash for uniqueness
609
650
  const prettyPrompt = escapeTitleForTS(aiTitle);
610
- // Fix title with storyPrefix - handle both single-line and multi-line formats
611
- fixedFileContents = fixedFileContents.replace(/(const\s+meta\s*=\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
651
+ // Append hash to title to prevent Storybook duplicate ID errors
652
+ const uniqueTitle = `${prettyPrompt} (${hash})`;
653
+ // Fix title with storyPrefix and hash - handle both single-line and multi-line formats
654
+ // Note: (?::\s*\w+(?:<[^>]+>)?)? handles TypeScript type annotations including generics
655
+ // e.g., "const meta: Meta = {" or "const meta: Meta<typeof Button> = {"
656
+ fixedFileContents = fixedFileContents.replace(/(const\s+meta\s*(?::\s*\w+(?:<[^>]+>)?)?\s*=\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
612
657
  // Check if the title already has the prefix to avoid double prefixing
613
- const titleToUse = prettyPrompt.startsWith(config.storyPrefix)
614
- ? prettyPrompt
615
- : config.storyPrefix + prettyPrompt;
658
+ const titleToUse = uniqueTitle.startsWith(config.storyPrefix)
659
+ ? uniqueTitle
660
+ : config.storyPrefix + uniqueTitle;
616
661
  return p1 + titleToUse + p3;
617
662
  });
618
663
  // Fallback: export default { title: "..." } format
619
664
  if (!fixedFileContents.includes(config.storyPrefix)) {
620
665
  fixedFileContents = fixedFileContents.replace(/(export\s+default\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
621
666
  // Check if the title already has the prefix to avoid double prefixing
622
- const titleToUse = prettyPrompt.startsWith(config.storyPrefix)
623
- ? prettyPrompt
624
- : config.storyPrefix + prettyPrompt;
667
+ const titleToUse = uniqueTitle.startsWith(config.storyPrefix)
668
+ ? uniqueTitle
669
+ : config.storyPrefix + uniqueTitle;
625
670
  return p1 + titleToUse + p3;
626
671
  });
627
672
  }
@@ -657,60 +702,6 @@ export async function generateStoryFromPrompt(req, res) {
657
702
  else {
658
703
  logger.log('✅ Final validation passed after post-processing');
659
704
  }
660
- // Check if this is an update to an existing story
661
- // ONLY consider it an update if we're in the same conversation context
662
- let existingStory = null;
663
- if (isActualUpdate && fileName) {
664
- // When updating within a conversation, look for the story by fileName
665
- existingStory = storyTracker.findByTitle(aiTitle);
666
- if (existingStory && existingStory.fileName !== fileName) {
667
- // If found story has different fileName, it's not the same story
668
- existingStory = null;
669
- }
670
- }
671
- // Remove the automatic "find by prompt" logic that was preventing duplicates
672
- // Generate unique ID and filename
673
- let hash, finalFileName, storyId;
674
- if (isActualUpdate && (fileName || providedStoryId)) {
675
- // For updates, preserve the existing fileName and ID
676
- // Ensure the filename has the proper .stories.tsx extension
677
- // FIX: Handle case where fileName is undefined but providedStoryId exists
678
- if (fileName) {
679
- finalFileName = fileName;
680
- }
681
- else if (providedStoryId) {
682
- // Generate filename from storyId when fileName not provided
683
- finalFileName = `${providedStoryId}.stories.tsx`;
684
- logger.log('📝 Generated filename from storyId:', finalFileName);
685
- }
686
- if (finalFileName && !finalFileName.endsWith('.stories.tsx')) {
687
- finalFileName = finalFileName + '.stories.tsx';
688
- }
689
- // Use provided storyId or extract from fileName
690
- if (providedStoryId) {
691
- storyId = providedStoryId;
692
- // Extract hash from storyId
693
- const hashMatch = providedStoryId.match(/^story-([a-f0-9]{8})$/);
694
- hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
695
- }
696
- else {
697
- // Extract hash from existing fileName if possible
698
- const hashMatch = fileName.match(/-([a-f0-9]{8})(?:\.stories\.tsx)?$/);
699
- hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
700
- storyId = `story-${hash}`;
701
- }
702
- logger.log('📌 Preserving story identity for update:', { storyId, fileName: finalFileName });
703
- }
704
- else {
705
- // For new stories, ALWAYS generate new IDs with timestamp to ensure uniqueness
706
- const timestamp = Date.now();
707
- hash = crypto.createHash('sha1').update(prompt + timestamp).digest('hex').slice(0, 8);
708
- // Use the framework adapter's defaultExtension for the correct file extension
709
- const fileExtension = frameworkAdapter?.defaultExtension || '.stories.tsx';
710
- finalFileName = fileName || fileNameFromTitle(aiTitle, hash, fileExtension);
711
- storyId = `story-${hash}`;
712
- logger.log('🆕 Creating new story:', { storyId, fileName: finalFileName, extension: fileExtension });
713
- }
714
705
  // Write story to file system
715
706
  const outPath = generateStory({
716
707
  fileContents: fixedFileContents,
@@ -1 +1 @@
1
- {"version":3,"file":"generateStoryStream.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStoryStream.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AA2c5C,wBAAsB,6BAA6B,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,iBAqhB9E"}
1
+ {"version":3,"file":"generateStoryStream.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStoryStream.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AA2c5C,wBAAsB,6BAA6B,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,iBA0hB9E"}
@@ -699,23 +699,7 @@ export async function generateStoryFromPromptStream(req, res) {
699
699
  if (!aiTitle || aiTitle.length < 2) {
700
700
  aiTitle = cleanPromptForTitle(prompt);
701
701
  }
702
- const prettyPrompt = escapeTitleForTS(aiTitle);
703
- // Fix title with storyPrefix
704
- fixedFileContents = fixedFileContents.replace(/(const\s+meta\s*=\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
705
- const titleToUse = prettyPrompt.startsWith(config.storyPrefix)
706
- ? prettyPrompt
707
- : config.storyPrefix + prettyPrompt;
708
- return p1 + titleToUse + p3;
709
- });
710
- if (!fixedFileContents.includes(config.storyPrefix)) {
711
- fixedFileContents = fixedFileContents.replace(/(export\s+default\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
712
- const titleToUse = prettyPrompt.startsWith(config.storyPrefix)
713
- ? prettyPrompt
714
- : config.storyPrefix + prettyPrompt;
715
- return p1 + titleToUse + p3;
716
- });
717
- }
718
- // Generate IDs
702
+ // Generate IDs FIRST so we can include hash in title for uniqueness
719
703
  let hash;
720
704
  let finalFileName;
721
705
  let storyId;
@@ -739,6 +723,27 @@ export async function generateStoryFromPromptStream(req, res) {
739
723
  finalFileName = fileName || fileNameFromTitle(aiTitle, hash);
740
724
  storyId = `story-${hash}`;
741
725
  }
726
+ // Now create title with hash suffix to ensure uniqueness
727
+ const prettyPrompt = escapeTitleForTS(aiTitle);
728
+ // Append hash to title to prevent Storybook duplicate ID errors
729
+ const uniqueTitle = `${prettyPrompt} (${hash})`;
730
+ // Fix title with storyPrefix and hash
731
+ // Note: (?::\s*\w+(?:<[^>]+>)?)? handles TypeScript type annotations including generics
732
+ // e.g., "const meta: Meta = {" or "const meta: Meta<typeof Button> = {"
733
+ fixedFileContents = fixedFileContents.replace(/(const\s+meta\s*(?::\s*\w+(?:<[^>]+>)?)?\s*=\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
734
+ const titleToUse = uniqueTitle.startsWith(config.storyPrefix)
735
+ ? uniqueTitle
736
+ : config.storyPrefix + uniqueTitle;
737
+ return p1 + titleToUse + p3;
738
+ });
739
+ if (!fixedFileContents.includes(config.storyPrefix)) {
740
+ fixedFileContents = fixedFileContents.replace(/(export\s+default\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
741
+ const titleToUse = uniqueTitle.startsWith(config.storyPrefix)
742
+ ? uniqueTitle
743
+ : config.storyPrefix + uniqueTitle;
744
+ return p1 + titleToUse + p3;
745
+ });
746
+ }
742
747
  // Ensure file extension is correct
743
748
  if (finalFileName && !finalFileName.endsWith('.stories.tsx')) {
744
749
  finalFileName = finalFileName + '.stories.tsx';
@@ -1 +1 @@
1
- {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAqtB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CAs5BnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAmuB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CAs5BnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -123,7 +123,11 @@ function getApiBaseUrl() {
123
123
  if (window.__STORY_UI_EDGE_URL__) {
124
124
  return window.__STORY_UI_EDGE_URL__;
125
125
  }
126
- if (window.location.hostname.includes('railway.app')) {
126
+ // Detect cloud deployments: Railway, custom domains, or any non-localhost
127
+ const hostname = window.location.hostname;
128
+ const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.') || hostname.startsWith('10.');
129
+ if (!isLocalhost) {
130
+ // Cloud deployment - use same origin (works for Railway, custom domains, etc.)
127
131
  return window.location.origin;
128
132
  }
129
133
  }
@@ -148,15 +152,27 @@ const PROVIDERS_API = `${API_BASE}/mcp/providers`;
148
152
  const STORIES_API = `${API_BASE}/story-ui/stories`;
149
153
  const CONSIDERATIONS_API = `${API_BASE}/mcp/considerations`;
150
154
  function isEdgeMode() {
151
- const baseUrl = getApiBaseUrl();
152
- return baseUrl.includes('railway.app') || baseUrl.includes('workers.dev');
155
+ if (typeof window !== 'undefined') {
156
+ const hostname = window.location.hostname;
157
+ const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.') || hostname.startsWith('10.');
158
+ return !isLocalhost;
159
+ }
160
+ return false;
153
161
  }
154
162
  function getConnectionDisplayText() {
155
163
  const baseUrl = getApiBaseUrl();
156
- if (baseUrl.includes('railway.app'))
157
- return 'Railway Cloud';
158
- if (baseUrl.includes('workers.dev'))
159
- return 'Cloudflare Edge';
164
+ if (typeof window !== 'undefined') {
165
+ const hostname = window.location.hostname;
166
+ if (hostname.includes('railway.app'))
167
+ return 'Railway Cloud';
168
+ if (hostname.includes('workers.dev'))
169
+ return 'Cloudflare Edge';
170
+ if (hostname.includes('southleft.com'))
171
+ return 'Southleft Cloud';
172
+ const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.') || hostname.startsWith('10.');
173
+ if (!isLocalhost)
174
+ return `Cloud (${hostname})`;
175
+ }
160
176
  const port = baseUrl.match(/:(\d+)/)?.[1] || '4001';
161
177
  return `localhost:${port}`;
162
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "3.10.0",
3
+ "version": "3.10.2",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -308,7 +308,11 @@ function getApiBaseUrl(): string {
308
308
  if ((window as any).__STORY_UI_EDGE_URL__) {
309
309
  return (window as any).__STORY_UI_EDGE_URL__;
310
310
  }
311
- if (window.location.hostname.includes('railway.app')) {
311
+ // Detect cloud deployments: Railway, custom domains, or any non-localhost
312
+ const hostname = window.location.hostname;
313
+ const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.') || hostname.startsWith('10.');
314
+ if (!isLocalhost) {
315
+ // Cloud deployment - use same origin (works for Railway, custom domains, etc.)
312
316
  return window.location.origin;
313
317
  }
314
318
  }
@@ -333,14 +337,24 @@ const STORIES_API = `${API_BASE}/story-ui/stories`;
333
337
  const CONSIDERATIONS_API = `${API_BASE}/mcp/considerations`;
334
338
 
335
339
  function isEdgeMode(): boolean {
336
- const baseUrl = getApiBaseUrl();
337
- return baseUrl.includes('railway.app') || baseUrl.includes('workers.dev');
340
+ if (typeof window !== 'undefined') {
341
+ const hostname = window.location.hostname;
342
+ const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.') || hostname.startsWith('10.');
343
+ return !isLocalhost;
344
+ }
345
+ return false;
338
346
  }
339
347
 
340
348
  function getConnectionDisplayText(): string {
341
349
  const baseUrl = getApiBaseUrl();
342
- if (baseUrl.includes('railway.app')) return 'Railway Cloud';
343
- if (baseUrl.includes('workers.dev')) return 'Cloudflare Edge';
350
+ if (typeof window !== 'undefined') {
351
+ const hostname = window.location.hostname;
352
+ if (hostname.includes('railway.app')) return 'Railway Cloud';
353
+ if (hostname.includes('workers.dev')) return 'Cloudflare Edge';
354
+ if (hostname.includes('southleft.com')) return 'Southleft Cloud';
355
+ const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.') || hostname.startsWith('10.');
356
+ if (!isLocalhost) return `Cloud (${hostname})`;
357
+ }
344
358
  const port = baseUrl.match(/:(\d+)/)?.[1] || '4001';
345
359
  return `localhost:${port}`;
346
360
  }