@tpitre/story-ui 3.6.2 → 3.7.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.
Files changed (44) hide show
  1. package/README.md +36 -32
  2. package/dist/cli/index.js +0 -5
  3. package/dist/cli/setup.js +1 -1
  4. package/dist/mcp-server/routes/generateStory.d.ts.map +1 -1
  5. package/dist/mcp-server/routes/generateStory.js +142 -87
  6. package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -1
  7. package/dist/mcp-server/routes/generateStoryStream.js +149 -31
  8. package/dist/story-generator/dynamicPackageDiscovery.d.ts +35 -2
  9. package/dist/story-generator/dynamicPackageDiscovery.d.ts.map +1 -1
  10. package/dist/story-generator/dynamicPackageDiscovery.js +332 -6
  11. package/dist/story-generator/enhancedComponentDiscovery.d.ts.map +1 -1
  12. package/dist/story-generator/enhancedComponentDiscovery.js +149 -2
  13. package/dist/story-generator/framework-adapters/base-adapter.d.ts +1 -0
  14. package/dist/story-generator/framework-adapters/base-adapter.d.ts.map +1 -1
  15. package/dist/story-generator/framework-adapters/base-adapter.js +12 -2
  16. package/dist/story-generator/framework-adapters/react-adapter.d.ts.map +1 -1
  17. package/dist/story-generator/framework-adapters/react-adapter.js +2 -0
  18. package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -1
  19. package/dist/story-generator/framework-adapters/svelte-adapter.js +53 -7
  20. package/dist/story-generator/framework-adapters/vue-adapter.d.ts.map +1 -1
  21. package/dist/story-generator/framework-adapters/vue-adapter.js +21 -1
  22. package/dist/story-generator/framework-adapters/web-components-adapter.d.ts.map +1 -1
  23. package/dist/story-generator/framework-adapters/web-components-adapter.js +4 -0
  24. package/dist/story-generator/llm-providers/openai-provider.js +2 -2
  25. package/dist/story-generator/promptGenerator.d.ts.map +1 -1
  26. package/dist/story-generator/promptGenerator.js +179 -26
  27. package/dist/story-generator/selfHealingLoop.d.ts +112 -0
  28. package/dist/story-generator/selfHealingLoop.d.ts.map +1 -0
  29. package/dist/story-generator/selfHealingLoop.js +202 -0
  30. package/dist/story-generator/validateStory.d.ts.map +1 -1
  31. package/dist/story-generator/validateStory.js +81 -12
  32. package/dist/story-ui.config.d.ts +2 -0
  33. package/dist/story-ui.config.d.ts.map +1 -1
  34. package/dist/templates/StoryUI/StoryUIPanel.d.ts +0 -5
  35. package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
  36. package/dist/templates/StoryUI/StoryUIPanel.js +411 -223
  37. package/package.json +4 -4
  38. package/templates/StoryUI/StoryUIPanel.mdx +84 -0
  39. package/templates/StoryUI/StoryUIPanel.tsx +493 -265
  40. package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts +0 -18
  41. package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts.map +0 -1
  42. package/dist/templates/StoryUI/StoryUIPanel.stories.js +0 -37
  43. package/templates/StoryUI/StoryUIPanel.stories.tsx +0 -44
  44. package/templates/StoryUI/manager.tsx +0 -859
@@ -45,8 +45,8 @@ const renderMarkdown = (text: string): ReactNode => {
45
45
  key={`c-${paragraphIndex}-${keyIndex++}`}
46
46
  style={{
47
47
  background: 'rgba(0,0,0,0.08)',
48
- padding: '1px 5px',
49
- borderRadius: '3px',
48
+ padding: '1px 4px',
49
+ borderRadius: '4px',
50
50
  fontFamily: 'ui-monospace, monospace',
51
51
  fontSize: '0.88em'
52
52
  }}
@@ -86,7 +86,7 @@ const renderMarkdown = (text: string): ReactNode => {
86
86
  return (
87
87
  <>
88
88
  {paragraphs.map((paragraph, index) => (
89
- <div key={`p-${index}`} style={{ marginBottom: index < paragraphs.length - 1 ? '12px' : 0 }}>
89
+ <div key={`p-${index}`} style={{ marginBottom: index < paragraphs.length - 1 ? '8px' : 0 }}>
90
90
  {parseInline(paragraph.trim(), index)}
91
91
  </div>
92
92
  ))}
@@ -246,6 +246,48 @@ interface ChatSession {
246
246
  lastUpdated: number;
247
247
  }
248
248
 
249
+ // Orphan story = a generated story file that exists on disk but has no chat history
250
+ interface OrphanStory {
251
+ id: string;
252
+ fileName: string;
253
+ title: string;
254
+ createdAt: number;
255
+ }
256
+
257
+ // Model display names for friendly UI presentation
258
+ // Maps API model IDs to human-readable names
259
+ const MODEL_DISPLAY_NAMES: Record<string, string> = {
260
+ // Claude models
261
+ 'claude-opus-4-5-20251101': 'Claude Opus 4.5',
262
+ 'claude-sonnet-4-5-20250929': 'Claude Sonnet 4.5',
263
+ 'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
264
+ 'claude-sonnet-4-20250514': 'Claude Sonnet 4',
265
+ 'claude-opus-4-20250514': 'Claude Opus 4',
266
+ 'claude-3-7-sonnet-20250219': 'Claude 3.7 Sonnet',
267
+ 'claude-3-5-sonnet-20241022': 'Claude 3.5 Sonnet',
268
+ 'claude-3-5-haiku-20241022': 'Claude 3.5 Haiku',
269
+ // OpenAI models
270
+ 'gpt-5.1': 'GPT-5.1',
271
+ 'gpt-5.1-thinking': 'GPT-5.1 Thinking',
272
+ 'gpt-5': 'GPT-5',
273
+ 'gpt-4o': 'GPT-4o',
274
+ 'gpt-4o-mini': 'GPT-4o Mini',
275
+ 'o1': 'o1',
276
+ 'o1-mini': 'o1 Mini',
277
+ // Gemini models
278
+ 'gemini-3-pro': 'Gemini 3 Pro',
279
+ 'gemini-3-pro-preview': 'Gemini 3 Pro Preview',
280
+ 'gemini-2.0-flash-exp': 'Gemini 2.0 Flash Exp',
281
+ 'gemini-2.0-flash': 'Gemini 2.0 Flash',
282
+ 'gemini-1.5-pro': 'Gemini 1.5 Pro',
283
+ 'gemini-1.5-flash': 'Gemini 1.5 Flash',
284
+ };
285
+
286
+ // Get friendly display name for a model, falling back to the API name if not found
287
+ const getModelDisplayName = (modelId: string): string => {
288
+ return MODEL_DISPLAY_NAMES[modelId] || modelId;
289
+ };
290
+
249
291
  // Determine the MCP API base URL.
250
292
  // Priority order:
251
293
  // 1. VITE_STORY_UI_EDGE_URL - Edge Worker URL for cloud deployments
@@ -303,58 +345,13 @@ const titleToStoryPath = (title: string): string => {
303
345
  return `generated-${kebabTitle}--default`;
304
346
  };
305
347
 
306
- // Extend window to include our code cache for the Source Code panel
307
- declare global {
308
- interface Window {
309
- __STORY_UI_GENERATED_CODE__?: Record<string, string>;
310
- }
311
- }
312
-
313
- // Helper to store generated code for the Source Code panel to display
314
- const storeGeneratedCode = (storyId: string, code: string, title?: string) => {
315
- const topWindow = window.top || window;
316
-
317
- // Store code in the top window so it's accessible from manager frame
318
- if (!topWindow.__STORY_UI_GENERATED_CODE__) {
319
- topWindow.__STORY_UI_GENERATED_CODE__ = {};
320
- }
321
-
322
- // Store with story ID
323
- topWindow.__STORY_UI_GENERATED_CODE__[storyId] = code;
324
-
325
- // Also store in localStorage for persistence across page reloads
326
- try {
327
- const stored = JSON.parse(localStorage.getItem('storyui_generated_code') || '{}');
328
- stored[storyId] = code;
329
-
330
- // Also store with the title as key for easier lookup
331
- if (title) {
332
- const storyPath = titleToStoryPath(title);
333
- stored[storyPath] = code;
334
- stored[title] = code;
335
- stored[title.replace(/\s+/g, '')] = code;
336
- topWindow.__STORY_UI_GENERATED_CODE__[storyPath] = code;
337
- }
338
-
339
- localStorage.setItem('storyui_generated_code', JSON.stringify(stored));
340
- console.log(`[Story UI] Stored code for story "${storyId}" in window cache and localStorage`);
341
- } catch (e) {
342
- console.warn('[Story UI] Failed to store code in localStorage:', e);
343
- }
344
- };
345
-
346
348
  // Helper to navigate to a newly created story after generation completes
347
349
  // In dev mode with HMR, this prevents the "Couldn't find story after HMR" error
348
350
  // In all modes, this provides a better UX by auto-navigating to the new story
349
- const navigateToNewStory = (title: string, code?: string, delayMs: number = 1500) => {
351
+ const navigateToNewStory = (title: string, _code?: string, delayMs: number = 1500) => {
350
352
  const storyPath = titleToStoryPath(title);
351
353
  console.log(`[Story UI] Will navigate to story "${storyPath}" in ${delayMs}ms...`);
352
354
 
353
- // Store the code for the Source Code panel if provided
354
- if (code) {
355
- storeGeneratedCode(title, code, title);
356
- }
357
-
358
355
  setTimeout(() => {
359
356
  // Navigate the TOP window (parent Storybook UI), not the iframe
360
357
  // The Story UI panel runs inside an iframe, so we need window.top to escape it
@@ -471,10 +468,10 @@ const syncWithActualStories = async (): Promise<ChatSession[]> => {
471
468
  // Update or add memory stories
472
469
  memoryStories.forEach((story: any) => {
473
470
  const storyId = story.storyId || story.fileName;
474
-
471
+
475
472
  // Look for existing chat by ID or by matching fileName
476
473
  let existingChat = chatMap.get(storyId);
477
-
474
+
478
475
  // If not found by ID, search by fileName
479
476
  if (!existingChat && story.fileName) {
480
477
  for (const [id, chat] of chatMap.entries()) {
@@ -501,7 +498,7 @@ const syncWithActualStories = async (): Promise<ChatSession[]> => {
501
498
  content: story.prompt || `Generate ${story.title}`
502
499
  }, {
503
500
  role: 'ai',
504
- content: `✅ Created story: "${story.title}"\n\nThis story was recovered from memory. You can continue updating it or view it in Storybook.`
501
+ content: `[SUCCESS] Created story: "${story.title}"\n\nThis story was recovered from memory. You can continue updating it or view it in Storybook.`
505
502
  }],
506
503
  lastUpdated: new Date(story.updatedAt || story.createdAt).getTime()
507
504
  };
@@ -617,16 +614,69 @@ const testMCPConnection = async (): Promise<{ connected: boolean; error?: string
617
614
  }
618
615
  };
619
616
 
617
+ // Fetch orphan stories (stories on disk without corresponding chat history)
618
+ const fetchOrphanStories = async (): Promise<OrphanStory[]> => {
619
+ try {
620
+ const response = await fetch(STORIES_API);
621
+ if (!response.ok) {
622
+ console.error('Failed to fetch stories from backend for orphan detection');
623
+ return [];
624
+ }
625
+
626
+ const contentType = response.headers.get('content-type');
627
+ if (!contentType || !contentType.includes('application/json')) {
628
+ console.error('Server returned non-JSON response for orphan detection');
629
+ return [];
630
+ }
631
+
632
+ const data = await response.json();
633
+ const serverStories = data.stories || [];
634
+
635
+ // Load current chats from localStorage
636
+ const existingChats = loadChats();
637
+ const chatIds = new Set(existingChats.map(chat => chat.id));
638
+ const chatFileNames = new Set(existingChats.map(chat => chat.fileName).filter(Boolean));
639
+
640
+ // Find stories that don't have a matching chat
641
+ const orphans: OrphanStory[] = [];
642
+
643
+ serverStories.forEach((story: any) => {
644
+ const storyId = story.id || story.storyId || story.fileName;
645
+ const fileName = story.fileName || '';
646
+
647
+ // Check if this story has a corresponding chat
648
+ const hasMatchingChat = chatIds.has(storyId) || chatFileNames.has(fileName);
649
+
650
+ if (!hasMatchingChat && fileName) {
651
+ orphans.push({
652
+ id: storyId,
653
+ fileName: fileName,
654
+ title: story.title || fileName.replace(/\.stories\.(tsx|ts|jsx|js)$/, ''),
655
+ createdAt: new Date(story.createdAt || Date.now()).getTime(),
656
+ });
657
+ }
658
+ });
659
+
660
+ // Sort by creation date, newest first
661
+ return orphans.sort((a, b) => b.createdAt - a.createdAt);
662
+ } catch (error) {
663
+ console.error('Error fetching orphan stories:', error);
664
+ return [];
665
+ }
666
+ };
667
+
620
668
  // Component styles
621
669
  const STYLES = {
622
670
  container: {
623
671
  display: 'flex',
624
672
  flexDirection: 'row' as const,
625
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
673
+ fontFamily: '"IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif',
626
674
  height: '100vh',
627
675
  overflow: 'hidden',
628
676
  background: 'linear-gradient(135deg, #1e293b 0%, #334155 100%)',
629
677
  color: '#e2e8f0',
678
+ fontSize: '14px',
679
+ lineHeight: '1.5',
630
680
  },
631
681
 
632
682
  // Sidebar
@@ -647,46 +697,46 @@ const STYLES = {
647
697
 
648
698
  sidebarToggle: {
649
699
  width: '100%',
650
- padding: '8px 12px',
651
- background: '#3b82f6',
652
- color: 'white',
653
- border: 'none',
654
- borderRadius: '6px',
655
- fontSize: '13px',
656
- fontWeight: '500',
700
+ padding: '10px 14px',
701
+ background: 'rgba(59, 130, 246, 0.15)',
702
+ color: '#e2e8f0',
703
+ border: '1px solid rgba(59, 130, 246, 0.3)',
704
+ borderRadius: '8px',
705
+ fontSize: '14px',
706
+ fontWeight: '600',
657
707
  cursor: 'pointer',
658
- marginBottom: '6px',
708
+ marginBottom: '8px',
659
709
  transition: 'all 0.2s ease',
660
710
  boxShadow: 'none',
661
711
  display: 'flex',
662
712
  alignItems: 'center',
663
- justifyContent: 'center',
664
- gap: '6px',
713
+ justifyContent: 'flex-start',
714
+ gap: '10px',
665
715
  lineHeight: '1',
666
716
  },
667
717
 
668
718
  newChatButton: {
669
719
  width: '100%',
670
- padding: '8px 12px',
720
+ padding: '10px 14px',
671
721
  background: '#3b82f6',
672
722
  color: 'white',
673
723
  border: 'none',
674
- borderRadius: '6px',
675
- fontSize: '13px',
676
- fontWeight: '500',
724
+ borderRadius: '8px',
725
+ fontSize: '14px',
726
+ fontWeight: '600',
677
727
  cursor: 'pointer',
678
- marginBottom: '12px',
728
+ marginBottom: '16px',
679
729
  transition: 'all 0.2s ease',
680
- boxShadow: 'none',
730
+ boxShadow: '0 2px 8px rgba(59, 130, 246, 0.25)',
681
731
  display: 'flex',
682
732
  alignItems: 'center',
683
- justifyContent: 'center',
684
- gap: '6px',
733
+ justifyContent: 'flex-start',
734
+ gap: '10px',
685
735
  lineHeight: '1',
686
736
  },
687
737
 
688
738
  chatItem: {
689
- padding: '8px 10px',
739
+ padding: '8px 12px',
690
740
  marginBottom: '4px',
691
741
  background: 'rgba(255, 255, 255, 0.05)',
692
742
  borderRadius: '6px',
@@ -702,7 +752,7 @@ const STYLES = {
702
752
  },
703
753
 
704
754
  chatItemTitle: {
705
- fontSize: '13px',
755
+ fontSize: '14px',
706
756
  fontWeight: '500',
707
757
  marginBottom: '2px',
708
758
  whiteSpace: 'nowrap' as const,
@@ -711,7 +761,7 @@ const STYLES = {
711
761
  },
712
762
 
713
763
  chatItemTime: {
714
- fontSize: '11px',
764
+ fontSize: '12px',
715
765
  color: '#94a3b8',
716
766
  },
717
767
 
@@ -757,11 +807,10 @@ const STYLES = {
757
807
  color: '#94a3b8',
758
808
  textAlign: 'center' as const,
759
809
  marginTop: '60px',
760
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
761
810
  },
762
811
 
763
812
  emptyStateTitle: {
764
- fontSize: '16px',
813
+ fontSize: '15px',
765
814
  fontWeight: '500',
766
815
  marginBottom: '8px',
767
816
  color: '#cbd5e1',
@@ -775,21 +824,21 @@ const STYLES = {
775
824
  // Message bubbles
776
825
  messageContainer: {
777
826
  display: 'flex',
778
- marginBottom: '10px',
827
+ marginBottom: '8px',
779
828
  },
780
829
 
781
830
  userMessage: {
782
- background: '#3b82f6',
783
- color: '#ffffff',
831
+ background: 'rgba(59, 130, 246, 0.12)',
832
+ color: '#e2e8f0',
784
833
  borderRadius: '16px 16px 4px 16px',
785
834
  padding: '10px 14px',
786
835
  maxWidth: '85%',
787
836
  marginLeft: 'auto',
788
837
  fontSize: '14px',
789
- lineHeight: '1.5',
790
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
838
+ lineHeight: '1.45',
791
839
  boxShadow: 'none',
792
840
  wordWrap: 'break-word' as const,
841
+ border: '1px solid rgba(59, 130, 246, 0.2)',
793
842
  },
794
843
 
795
844
  aiMessage: {
@@ -797,45 +846,47 @@ const STYLES = {
797
846
  color: '#1f2937',
798
847
  borderRadius: '16px 16px 16px 4px',
799
848
  padding: '10px 14px',
800
- maxWidth: '85%',
849
+ maxWidth: '90%',
801
850
  fontSize: '14px',
802
- lineHeight: '1.5',
803
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
804
- boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
851
+ lineHeight: '1.45',
852
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)',
853
+ border: '1px solid rgba(0, 0, 0, 0.08)',
805
854
  wordWrap: 'break-word' as const,
806
855
  whiteSpace: 'pre-wrap' as const,
807
856
  },
808
857
 
809
858
  loadingMessage: {
810
- background: 'rgba(255, 255, 255, 0.9)',
811
- color: '#6b7280',
859
+ background: 'rgba(255, 255, 255, 0.95)',
860
+ color: '#4b5563',
812
861
  borderRadius: '16px 16px 16px 4px',
813
862
  padding: '10px 14px',
814
863
  fontSize: '14px',
815
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
864
+ lineHeight: '1.45',
816
865
  display: 'flex',
817
866
  alignItems: 'center',
818
- gap: '6px',
867
+ gap: '8px',
868
+ border: '1px solid rgba(0, 0, 0, 0.08)',
869
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)',
819
870
  },
820
871
 
821
872
  // Input form
822
873
  inputForm: {
823
874
  display: 'flex',
824
875
  alignItems: 'center',
825
- gap: '10px',
876
+ gap: '12px',
826
877
  margin: '0 16px 16px 16px',
827
- padding: '10px',
878
+ padding: '12px',
828
879
  background: 'rgba(255, 255, 255, 0.03)',
829
- borderRadius: '10px',
880
+ borderRadius: '12px',
830
881
  border: '1px solid rgba(255, 255, 255, 0.08)',
831
882
  backdropFilter: 'blur(10px)',
832
883
  },
833
884
 
834
885
  textInput: {
835
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
886
+ font: 'inherit',
836
887
  flex: 1,
837
- padding: '10px 14px',
838
- borderRadius: '6px',
888
+ padding: '12px 16px',
889
+ borderRadius: '8px',
839
890
  border: '1px solid rgba(255, 255, 255, 0.15)',
840
891
  fontSize: '13px',
841
892
  color: '#1f2937',
@@ -846,20 +897,21 @@ const STYLES = {
846
897
  },
847
898
 
848
899
  sendButton: {
849
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
900
+ font: 'inherit',
850
901
  padding: '10px 16px',
851
- borderRadius: '6px',
902
+ borderRadius: '10px',
852
903
  border: 'none',
853
- background: '#10b981',
854
- color: '#ffffff',
855
- fontSize: '13px',
856
- fontWeight: '500',
904
+ background: '#3b82f6',
905
+ color: 'white',
906
+ fontSize: '14px',
907
+ fontWeight: '600',
857
908
  cursor: 'pointer',
858
909
  display: 'flex',
859
910
  alignItems: 'center',
860
- gap: '5px',
861
- transition: 'all 0.15s ease',
862
- boxShadow: 'none',
911
+ gap: '6px',
912
+ transition: 'all 0.2s ease',
913
+ boxShadow: '0 2px 8px rgba(59, 130, 246, 0.35)',
914
+ flexShrink: 0,
863
915
  },
864
916
 
865
917
  errorMessage: {
@@ -868,7 +920,7 @@ const STYLES = {
868
920
  padding: '8px 12px',
869
921
  borderRadius: '6px',
870
922
  fontSize: '13px',
871
- marginBottom: '10px',
923
+ marginBottom: '8px',
872
924
  border: '1px solid rgba(248, 113, 113, 0.2)',
873
925
  },
874
926
 
@@ -886,13 +938,13 @@ const STYLES = {
886
938
 
887
939
  codeBlock: {
888
940
  background: '#1e293b',
889
- padding: '10px 12px',
890
- borderRadius: '6px',
941
+ padding: '12px',
942
+ borderRadius: '8px',
891
943
  fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
892
944
  fontSize: '12px',
893
945
  lineHeight: '1.5',
894
946
  overflowX: 'auto' as const,
895
- marginTop: '6px',
947
+ marginTop: '8px',
896
948
  border: '1px solid rgba(255, 255, 255, 0.08)',
897
949
  },
898
950
 
@@ -900,27 +952,30 @@ const STYLES = {
900
952
  streamingContainer: {
901
953
  background: 'rgba(255, 255, 255, 0.95)',
902
954
  borderRadius: '16px 16px 16px 4px',
903
- padding: '12px',
904
- maxWidth: '85%',
905
- boxShadow: '0 1px 3px rgba(0, 0, 0, 0.08)',
955
+ padding: '10px 14px',
956
+ maxWidth: '90%',
957
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)',
958
+ border: '1px solid rgba(0, 0, 0, 0.08)',
959
+ fontSize: '14px',
960
+ lineHeight: '1.45',
906
961
  },
907
962
 
908
963
  intentPreview: {
909
- background: 'rgba(59, 130, 246, 0.08)',
964
+ background: 'rgba(59, 130, 246, 0.06)',
910
965
  borderRadius: '8px',
911
- padding: '10px',
966
+ padding: '10px 12px',
912
967
  marginBottom: '10px',
913
- border: '1px solid rgba(59, 130, 246, 0.15)',
968
+ border: '1px solid rgba(59, 130, 246, 0.12)',
914
969
  },
915
970
 
916
971
  intentTitle: {
917
972
  fontSize: '13px',
918
973
  fontWeight: '600',
919
974
  color: '#1e40af',
920
- marginBottom: '6px',
975
+ marginBottom: '8px',
921
976
  display: 'flex',
922
977
  alignItems: 'center',
923
- gap: '5px',
978
+ gap: '4px',
924
979
  },
925
980
 
926
981
  intentStrategy: {
@@ -933,24 +988,24 @@ const STYLES = {
933
988
  display: 'flex',
934
989
  flexWrap: 'wrap' as const,
935
990
  gap: '4px',
936
- marginTop: '6px',
991
+ marginTop: '8px',
937
992
  },
938
993
 
939
994
  componentTag: {
940
995
  background: 'rgba(59, 130, 246, 0.12)',
941
996
  color: '#1d4ed8',
942
- fontSize: '10px',
943
- padding: '2px 6px',
997
+ fontSize: '11px',
998
+ padding: '2px 8px',
944
999
  borderRadius: '10px',
945
1000
  fontWeight: '500',
946
1001
  },
947
1002
 
948
1003
  progressBar: {
949
1004
  background: 'rgba(0, 0, 0, 0.08)',
950
- borderRadius: '3px',
1005
+ borderRadius: '4px',
951
1006
  height: '4px',
952
- marginTop: '10px',
953
- marginBottom: '6px',
1007
+ marginTop: '12px',
1008
+ marginBottom: '8px',
954
1009
  overflow: 'hidden',
955
1010
  },
956
1011
 
@@ -962,15 +1017,17 @@ const STYLES = {
962
1017
  },
963
1018
 
964
1019
  progressPhase: {
965
- fontSize: '11px',
966
- color: '#6b7280',
1020
+ fontSize: '14px',
1021
+ color: '#4b5563',
967
1022
  display: 'flex',
968
1023
  alignItems: 'center',
969
- gap: '5px',
1024
+ gap: '6px',
1025
+ fontWeight: '500',
1026
+ lineHeight: '1.45',
970
1027
  },
971
1028
 
972
1029
  phaseIcon: {
973
- fontSize: '12px',
1030
+ fontSize: '14px',
974
1031
  },
975
1032
 
976
1033
  validationBox: {
@@ -1001,19 +1058,19 @@ const STYLES = {
1001
1058
  retryBadge: {
1002
1059
  background: 'rgba(245, 158, 11, 0.12)',
1003
1060
  color: '#b45309',
1004
- fontSize: '10px',
1061
+ fontSize: '11px',
1005
1062
  padding: '2px 8px',
1006
1063
  borderRadius: '10px',
1007
1064
  display: 'inline-flex',
1008
1065
  alignItems: 'center',
1009
- gap: '3px',
1010
- marginTop: '6px',
1066
+ gap: '4px',
1067
+ marginTop: '8px',
1011
1068
  },
1012
1069
 
1013
1070
  completionSummary: {
1014
1071
  marginTop: '10px',
1015
1072
  paddingTop: '10px',
1016
- borderTop: '1px solid rgba(0, 0, 0, 0.08)',
1073
+ borderTop: '1px solid rgba(0, 0, 0, 0.06)',
1017
1074
  },
1018
1075
 
1019
1076
  summaryTitle: {
@@ -1024,26 +1081,27 @@ const STYLES = {
1024
1081
  display: 'flex',
1025
1082
  alignItems: 'center',
1026
1083
  gap: '6px',
1084
+ lineHeight: '1.45',
1027
1085
  },
1028
1086
 
1029
1087
  summaryDescription: {
1030
- fontSize: '12px',
1088
+ fontSize: '14px',
1031
1089
  color: '#4b5563',
1032
- lineHeight: '1.5',
1090
+ lineHeight: '1.45',
1033
1091
  },
1034
1092
 
1035
1093
  metricsRow: {
1036
1094
  display: 'flex',
1037
- gap: '12px',
1038
- marginTop: '8px',
1039
- fontSize: '10px',
1095
+ gap: '10px',
1096
+ marginTop: '6px',
1097
+ fontSize: '13px',
1040
1098
  color: '#6b7280',
1041
1099
  },
1042
1100
 
1043
1101
  metric: {
1044
1102
  display: 'flex',
1045
1103
  alignItems: 'center',
1046
- gap: '3px',
1104
+ gap: '4px',
1047
1105
  },
1048
1106
 
1049
1107
  // Code viewer styles for generated stories
@@ -1073,7 +1131,7 @@ const STYLES = {
1073
1131
  },
1074
1132
 
1075
1133
  codeViewerContent: {
1076
- marginTop: '10px',
1134
+ marginTop: '12px',
1077
1135
  background: '#1e293b',
1078
1136
  borderRadius: '8px',
1079
1137
  overflow: 'hidden',
@@ -1096,7 +1154,7 @@ const STYLES = {
1096
1154
  },
1097
1155
 
1098
1156
  copyButton: {
1099
- padding: '4px 10px',
1157
+ padding: '4px 12px',
1100
1158
  fontSize: '11px',
1101
1159
  fontWeight: '500',
1102
1160
  color: '#e2e8f0',
@@ -1149,12 +1207,12 @@ const STYLES = {
1149
1207
  imagePreviewContainer: {
1150
1208
  display: 'flex',
1151
1209
  flexWrap: 'wrap' as const,
1152
- gap: '6px',
1210
+ gap: '8px',
1153
1211
  padding: '8px 12px',
1154
1212
  background: 'rgba(255, 255, 255, 0.03)',
1155
1213
  borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
1156
1214
  margin: '0 16px',
1157
- borderRadius: '6px 6px 0 0',
1215
+ borderRadius: '8px 8px 0 0',
1158
1216
  },
1159
1217
 
1160
1218
  imagePreviewItem: {
@@ -1194,7 +1252,7 @@ const STYLES = {
1194
1252
  imagePreviewLabel: {
1195
1253
  display: 'flex',
1196
1254
  alignItems: 'center',
1197
- gap: '6px',
1255
+ gap: '8px',
1198
1256
  fontSize: '12px',
1199
1257
  color: '#94a3b8',
1200
1258
  marginRight: 'auto',
@@ -1202,8 +1260,8 @@ const STYLES = {
1202
1260
 
1203
1261
  userMessageImages: {
1204
1262
  display: 'flex',
1205
- gap: '6px',
1206
- marginTop: '6px',
1263
+ gap: '8px',
1264
+ marginTop: '8px',
1207
1265
  flexWrap: 'wrap' as const,
1208
1266
  },
1209
1267
 
@@ -1246,25 +1304,143 @@ const STYLES = {
1246
1304
  },
1247
1305
  };
1248
1306
 
1249
- // Add custom style for loading animation
1250
- const styleSheet = document.createElement('style');
1251
- styleSheet.textContent = `
1252
- @keyframes loadingDots {
1253
- 0%, 20% { content: "."; }
1254
- 40% { content: ".."; }
1255
- 60%, 100% { content: "..."; }
1256
- }
1307
+ // Add custom style for loading animation and IBM Plex Sans font
1308
+ // Use a unique ID to prevent duplicate stylesheets during HMR
1309
+ const STYLESHEET_ID = 'story-ui-panel-styles';
1310
+ if (!document.getElementById(STYLESHEET_ID)) {
1311
+ // Load IBM Plex Sans font
1312
+ const fontLink = document.createElement('link');
1313
+ fontLink.rel = 'stylesheet';
1314
+ fontLink.href = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap';
1315
+ document.head.appendChild(fontLink);
1316
+
1317
+ const styleSheet = document.createElement('style');
1318
+ styleSheet.id = STYLESHEET_ID;
1319
+ styleSheet.textContent = `
1320
+ @keyframes loadingDots {
1321
+ 0%, 20% { content: "."; }
1322
+ 40% { content: ".."; }
1323
+ 60%, 100% { content: "..."; }
1324
+ }
1257
1325
 
1258
- .loading-dots::after {
1259
- content: ".";
1260
- animation: loadingDots 1.4s infinite;
1261
- }
1262
- `;
1263
- document.head.appendChild(styleSheet);
1326
+ .loading-dots::after {
1327
+ content: ".";
1328
+ animation: loadingDots 1.4s infinite;
1329
+ }
1330
+
1331
+ /* Override Storybook's default styles with !important */
1332
+ .story-ui-panel,
1333
+ .story-ui-panel * {
1334
+ font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
1335
+ }
1336
+
1337
+ .story-ui-panel {
1338
+ font-size: 14px !important;
1339
+ line-height: 1.5 !important;
1340
+ }
1341
+
1342
+ /* Message bubbles - consistent styling */
1343
+ .story-ui-message {
1344
+ font-size: 16px !important;
1345
+ line-height: 1.45 !important;
1346
+ padding: 12px 16px !important;
1347
+ }
1348
+
1349
+ .story-ui-user-message {
1350
+ background: rgba(59, 130, 246, 0.12) !important;
1351
+ color: #e2e8f0 !important;
1352
+ border-radius: 18px 18px 4px 18px !important;
1353
+ border: 1px solid rgba(59, 130, 246, 0.2) !important;
1354
+ }
1355
+
1356
+ .story-ui-ai-message {
1357
+ background: rgba(255, 255, 255, 0.97) !important;
1358
+ color: #1f2937 !important;
1359
+ border-radius: 18px 18px 18px 4px !important;
1360
+ border: 1px solid rgba(0, 0, 0, 0.08) !important;
1361
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
1362
+ }
1363
+
1364
+ /* Override nested elements in AI messages (from renderMarkdown)
1365
+ .story-ui-ai-message p,
1366
+ .story-ui-ai-message span,
1367
+ .story-ui-ai-message strong,
1368
+ .story-ui-ai-message em,
1369
+ .story-ui-ai-message li,
1370
+ .story-ui-ai-message ul,
1371
+ .story-ui-ai-message ol {
1372
+ font-size: 14px !important;
1373
+ line-height: 1.45 !important;
1374
+ margin: 0 !important;
1375
+ } */
1376
+
1377
+ .story-ui-ai-message p + p {
1378
+ margin-top: 8px !important;
1379
+ }
1380
+
1381
+ /* Status text */
1382
+ .story-ui-status {
1383
+ font-size: 13px !important;
1384
+ font-weight: 400 !important;
1385
+ }
1386
+
1387
+ .story-ui-status-connected {
1388
+ color: #10b981 !important;
1389
+ }
1390
+
1391
+ .story-ui-status-disconnected {
1392
+ color: #ef4444 !important;
1393
+ }
1394
+
1395
+ /* Sidebar buttons */
1396
+ .story-ui-sidebar button {
1397
+ font-size: 14px !important;
1398
+ font-weight: 600 !important;
1399
+ }
1400
+
1401
+ /* Header text */
1402
+ .story-ui-header h1 {
1403
+ font-size: 24px !important;
1404
+ font-weight: 700 !important;
1405
+ }
1406
+
1407
+ .story-ui-header p {
1408
+ font-size: 14px !important;
1409
+ color: #94a3b8 !important;
1410
+ }
1411
+
1412
+ /* Input field */
1413
+ .story-ui-input {
1414
+ font-size: 14px !important;
1415
+ font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
1416
+ }
1417
+
1418
+ /* Code blocks in messages */
1419
+ .story-ui-ai-message code {
1420
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace !important;
1421
+ font-size: 13px !important;
1422
+ background: rgba(0, 0, 0, 0.06) !important;
1423
+ padding: 2px 6px !important;
1424
+ border-radius: 4px !important;
1425
+ }
1426
+ `;
1427
+ document.head.appendChild(styleSheet);
1428
+ }
1264
1429
 
1265
1430
  // Helper function to format timestamp
1266
1431
  const formatTime = (timestamp: number): string => {
1432
+ // Handle invalid timestamps
1433
+ if (!timestamp || isNaN(timestamp) || timestamp <= 0) {
1434
+ return '';
1435
+ }
1436
+
1267
1437
  const date = new Date(timestamp);
1438
+
1439
+ // Check if the date is valid
1440
+ if (isNaN(date.getTime())) {
1441
+ return '';
1442
+ }
1443
+
1268
1444
  const now = new Date();
1269
1445
  const diffMs = now.getTime() - date.getTime();
1270
1446
  const diffMins = Math.floor(diffMs / 60000);
@@ -1278,19 +1454,19 @@ const formatTime = (timestamp: number): string => {
1278
1454
  return date.toLocaleDateString();
1279
1455
  };
1280
1456
 
1281
- // Helper to get phase icon and text
1282
- const getPhaseInfo = (phase: ProgressUpdate['phase']): { icon: string; text: string } => {
1283
- const phases: Record<ProgressUpdate['phase'], { icon: string; text: string }> = {
1284
- config_loaded: { icon: '⚙️', text: 'Loading configuration' },
1285
- components_discovered: { icon: '🔍', text: 'Discovering components' },
1286
- prompt_built: { icon: '📝', text: 'Building prompt' },
1287
- llm_thinking: { icon: '🤔', text: 'AI is thinking' },
1288
- code_extracted: { icon: '📦', text: 'Extracting code' },
1289
- validating: { icon: '✅', text: 'Validating output' },
1290
- post_processing: { icon: '🔧', text: 'Processing' },
1291
- saving: { icon: '💾', text: 'Saving story' },
1457
+ // Helper to get phase text (no icons - cleaner UI)
1458
+ const getPhaseInfo = (phase: ProgressUpdate['phase']): { text: string } => {
1459
+ const phases: Record<ProgressUpdate['phase'], { text: string }> = {
1460
+ config_loaded: { text: 'Loading configuration' },
1461
+ components_discovered: { text: 'Discovering components' },
1462
+ prompt_built: { text: 'Building prompt' },
1463
+ llm_thinking: { text: 'AI is thinking' },
1464
+ code_extracted: { text: 'Extracting code' },
1465
+ validating: { text: 'Validating output' },
1466
+ post_processing: { text: 'Processing' },
1467
+ saving: { text: 'Saving story' },
1292
1468
  };
1293
- return phases[phase] || { icon: '⏳', text: 'Working' };
1469
+ return phases[phase] || { text: 'Working' };
1294
1470
  };
1295
1471
 
1296
1472
  // Streaming Progress Message Component
@@ -1316,7 +1492,7 @@ const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({
1316
1492
  <div style={STYLES.streamingContainer}>
1317
1493
  <div style={STYLES.completionSummary}>
1318
1494
  <div style={STYLES.summaryTitle}>
1319
- {completion.success ? '✅' : '❌'} {completion.title}
1495
+ {completion.success ? StatusIcons.success : StatusIcons.error} {completion.title}
1320
1496
  </div>
1321
1497
  <div style={STYLES.summaryDescription}>
1322
1498
  {completion.summary.description}
@@ -1324,8 +1500,8 @@ const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({
1324
1500
 
1325
1501
  {/* Components Used */}
1326
1502
  {completion.componentsUsed.length > 0 && (
1327
- <div style={{ marginTop: '10px' }}>
1328
- <div style={{ fontSize: '12px', color: '#6b7280', marginBottom: '6px' }}>Components used:</div>
1503
+ <div style={{ marginTop: '12px' }}>
1504
+ <div style={{ fontSize: '12px', color: '#6b7280', marginBottom: '8px' }}>Components used:</div>
1329
1505
  <div style={STYLES.intentComponents}>
1330
1506
  {completion.componentsUsed.map((comp, i) => (
1331
1507
  <span key={i} style={STYLES.componentTag}>{comp.name}</span>
@@ -1336,8 +1512,8 @@ const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({
1336
1512
 
1337
1513
  {/* Layout Choices */}
1338
1514
  {completion.layoutChoices.length > 0 && (
1339
- <div style={{ marginTop: '10px' }}>
1340
- <div style={{ fontSize: '12px', color: '#6b7280', marginBottom: '6px' }}>Layout:</div>
1515
+ <div style={{ marginTop: '12px' }}>
1516
+ <div style={{ fontSize: '12px', color: '#6b7280', marginBottom: '8px' }}>Layout:</div>
1341
1517
  <div style={{ fontSize: '12px', color: '#4b5563' }}>
1342
1518
  {completion.layoutChoices.map(l => l.pattern).join(', ')}
1343
1519
  </div>
@@ -1347,22 +1523,22 @@ const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({
1347
1523
  {/* Validation Status */}
1348
1524
  {completion.validation && !completion.validation.isValid && (
1349
1525
  <div style={{ ...STYLES.validationBox, ...STYLES.validationWarning }}>
1350
- ⚠️ {completion.validation.autoFixApplied ? 'Auto-fixed issues' : 'Minor issues detected'}
1526
+ {completion.validation.autoFixApplied ? 'Auto-fixed issues' : 'Minor issues detected'}
1351
1527
  </div>
1352
1528
  )}
1353
1529
 
1354
1530
  {/* Suggestions */}
1355
1531
  {completion.suggestions && completion.suggestions.length > 0 && (
1356
- <div style={{ marginTop: '10px', fontSize: '12px', color: '#6b7280' }}>
1357
- 💡 {completion.suggestions[0]}
1532
+ <div style={{ marginTop: '12px', fontSize: '12px', color: '#6b7280', display: 'flex', alignItems: 'flex-start', gap: '6px' }}>
1533
+ {StatusIcons.tip} <span>{completion.suggestions[0]}</span>
1358
1534
  </div>
1359
1535
  )}
1360
1536
 
1361
1537
  {/* Metrics */}
1362
1538
  {completion.metrics && (
1363
1539
  <div style={STYLES.metricsRow}>
1364
- <span style={STYLES.metric}>⏱️ {(completion.metrics.totalTimeMs / 1000).toFixed(1)}s</span>
1365
- <span style={STYLES.metric}>🔄 {completion.metrics.llmCallsCount} LLM calls</span>
1540
+ <span style={STYLES.metric}>{(completion.metrics.totalTimeMs / 1000).toFixed(1)}s</span>
1541
+ <span style={STYLES.metric}>{completion.metrics.llmCallsCount} LLM calls</span>
1366
1542
  </div>
1367
1543
  )}
1368
1544
 
@@ -1390,7 +1566,7 @@ const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({
1390
1566
  }}
1391
1567
  onClick={() => handleCopyCode(completion.code)}
1392
1568
  >
1393
- {copyStatus === 'copied' ? 'Copied!' : 'Copy Code'}
1569
+ {copyStatus === 'copied' ? 'Copied' : 'Copy'}
1394
1570
  </button>
1395
1571
  </div>
1396
1572
  <pre style={STYLES.codeViewerPre}>
@@ -1410,9 +1586,9 @@ const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({
1410
1586
  return (
1411
1587
  <div style={STYLES.streamingContainer}>
1412
1588
  <div style={{ ...STYLES.validationBox, ...STYLES.validationError }}>
1413
- <strong>❌ {error.message}</strong>
1589
+ <strong style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>{StatusIcons.error} {error.message}</strong>
1414
1590
  {error.details && <div style={{ marginTop: '4px' }}>{error.details}</div>}
1415
- {error.suggestion && <div style={{ marginTop: '8px' }}>💡 {error.suggestion}</div>}
1591
+ {error.suggestion && <div style={{ marginTop: '8px', display: 'flex', alignItems: 'flex-start', gap: '6px' }}>{StatusIcons.tip} <span>{error.suggestion}</span></div>}
1416
1592
  </div>
1417
1593
  </div>
1418
1594
  );
@@ -1424,8 +1600,7 @@ const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({
1424
1600
  {/* Simple progress indicator */}
1425
1601
  <div style={STYLES.intentPreview}>
1426
1602
  <div style={STYLES.progressPhase}>
1427
- <span style={STYLES.phaseIcon}>🤖</span>
1428
- <span>AI is generating your story...</span>
1603
+ <span>Generating story...</span>
1429
1604
  {progress && (
1430
1605
  <span style={{ marginLeft: 'auto', color: '#9ca3af' }}>
1431
1606
  {progress.step}/{progress.totalSteps}
@@ -1449,7 +1624,7 @@ const StreamingProgressMessage: React.FC<{ streamingData: StreamingState }> = ({
1449
1624
  {/* Retry Badge - only show if retrying */}
1450
1625
  {retry && (
1451
1626
  <div style={STYLES.retryBadge}>
1452
- 🔄 Retry {retry.attempt}/{retry.maxAttempts}: {retry.reason}
1627
+ Retry {retry.attempt}/{retry.maxAttempts}: {retry.reason}
1453
1628
  </div>
1454
1629
  )}
1455
1630
 
@@ -1480,6 +1655,7 @@ function StoryUIPanel() {
1480
1655
  const [streamingState, setStreamingState] = useState<StreamingState | null>(null);
1481
1656
  const [attachedImages, setAttachedImages] = useState<AttachedImage[]>([]);
1482
1657
  const [considerations, setConsiderations] = useState<string>('');
1658
+ const [orphanStories, setOrphanStories] = useState<OrphanStory[]>([]);
1483
1659
  const chatEndRef = useRef<HTMLDivElement | null>(null);
1484
1660
  const inputRef = useRef<HTMLInputElement | null>(null);
1485
1661
  const fileInputRef = useRef<HTMLInputElement | null>(null);
@@ -1723,7 +1899,7 @@ function StoryUIPanel() {
1723
1899
  // Test connection first
1724
1900
  const connectionTest = await testMCPConnection();
1725
1901
  setConnectionStatus(connectionTest);
1726
-
1902
+
1727
1903
  if (connectionTest.connected) {
1728
1904
  // Fetch available providers
1729
1905
  try {
@@ -1765,6 +1941,10 @@ function StoryUIPanel() {
1765
1941
  setActiveChatId(sortedChats[0].id);
1766
1942
  setActiveTitle(sortedChats[0].title);
1767
1943
  }
1944
+
1945
+ // Fetch orphan stories (stories on disk without chat history)
1946
+ const orphans = await fetchOrphanStories();
1947
+ setOrphanStories(orphans);
1768
1948
  } else {
1769
1949
  // Load from local storage if server is not available
1770
1950
  const localChats = loadChats();
@@ -1884,9 +2064,9 @@ function StoryUIPanel() {
1884
2064
  // In Edge mode, stories are stored in Durable Objects, not on filesystem
1885
2065
  if (!isUpdate && !hasShownRefreshHint.current) {
1886
2066
  if (isEdgeMode()) {
1887
- parts.push(`\n\n_Story saved to cloud. View code in chat history above._`);
2067
+ parts.push(`\n\n_Story saved to cloud. View code in chat history recent chats navigation._`);
1888
2068
  } else {
1889
- parts.push(`\n\n_Refresh Storybook (Cmd/Ctrl + R) to see new stories in the sidebar._`);
2069
+ parts.push(`\n\n_Might need toefresh Storybook (Cmd/Ctrl + R) to see new stories in the sidebar._`);
1890
2070
  }
1891
2071
  hasShownRefreshHint.current = true;
1892
2072
  }
@@ -1933,11 +2113,6 @@ function StoryUIPanel() {
1933
2113
  }
1934
2114
  saveChats(chats);
1935
2115
  setRecentChats(chats);
1936
-
1937
- // Store code for Source Code panel
1938
- if (completion.code) {
1939
- storeGeneratedCode(activeChatId, completion.code, activeTitle || completion.title);
1940
- }
1941
2116
  } else {
1942
2117
  const chatId = completion.storyId || completion.fileName || Date.now().toString();
1943
2118
  const chatTitle = completion.title || userInput;
@@ -1964,11 +2139,6 @@ function StoryUIPanel() {
1964
2139
  // This prevents the "Couldn't find story after HMR" error by refreshing
1965
2140
  // after the file system has been updated and HMR has processed the change
1966
2141
  navigateToNewStory(chatTitle, completion.code);
1967
-
1968
- // Store code for Source Code panel
1969
- if (completion.code) {
1970
- storeGeneratedCode(chatId, completion.code, chatTitle);
1971
- }
1972
2142
  }
1973
2143
  }, [activeChatId, activeTitle, conversation.length]);
1974
2144
 
@@ -2131,13 +2301,13 @@ function StoryUIPanel() {
2131
2301
 
2132
2302
  // Process non-streaming response (same as before)
2133
2303
  let responseMessage: string;
2134
- const statusIcon = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '🔧' : '⚠️') : '';
2304
+ const statusMarker = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '[WRENCH]' : '[TIP]') : '[SUCCESS]';
2135
2305
 
2136
2306
  // Build conversational response for fallback
2137
2307
  if (data.isUpdate) {
2138
- responseMessage = `${statusIcon} **Updated: "${data.title}"**\n\nI've made the requested changes to your component. You can view the updated version in Storybook.\n\n_Check the Docs tab to see both the rendered component and its code._`;
2308
+ responseMessage = `${statusMarker} **Updated: "${data.title}"**\n\nI've made the requested changes to your component. You can view the updated version in Storybook.\n\n_Check the Docs tab to see both the rendered component and its code._`;
2139
2309
  } else {
2140
- responseMessage = `${statusIcon} **Created: "${data.title}"**\n\nI've generated the component with the requested features. You can view it in Storybook where you'll see both the rendered component and its markup.\n\n💡 **Note**: If you don't see the story immediately, you may need to refresh your Storybook page (Cmd/Ctrl + R).`;
2310
+ responseMessage = `${statusMarker} **Created: "${data.title}"**\n\nI've generated the component with the requested features. You can view it in Storybook where you'll see both the rendered component and its markup.\n\n[TIP] **Note**: If you don't see the story immediately, you may need to refresh your Storybook page (Cmd/Ctrl + R).`;
2141
2311
  }
2142
2312
 
2143
2313
  const aiMsg: Message = { role: 'ai', content: responseMessage };
@@ -2159,11 +2329,6 @@ function StoryUIPanel() {
2159
2329
  if (chatIndex !== -1) chats[chatIndex] = updatedSession;
2160
2330
  saveChats(chats);
2161
2331
  setRecentChats(chats);
2162
-
2163
- // Store code for Source Code panel
2164
- if (data.code) {
2165
- storeGeneratedCode(activeChatId, data.code, activeTitle || data.title);
2166
- }
2167
2332
  } else {
2168
2333
  const chatId = data.storyId || data.fileName || Date.now().toString();
2169
2334
  const chatTitle = data.title || userInput;
@@ -2184,11 +2349,6 @@ function StoryUIPanel() {
2184
2349
 
2185
2350
  // Auto-navigate to the newly created story
2186
2351
  navigateToNewStory(chatTitle, data.code);
2187
-
2188
- // Store code for Source Code panel
2189
- if (data.code) {
2190
- storeGeneratedCode(chatId, data.code, chatTitle);
2191
- }
2192
2352
  }
2193
2353
  } catch (fallbackErr: unknown) {
2194
2354
  const errorMessage = fallbackErr instanceof Error ? fallbackErr.message : 'Unknown error';
@@ -2207,13 +2367,13 @@ function StoryUIPanel() {
2207
2367
  const data = await handleSendNonStreaming(userInput, newConversation);
2208
2368
 
2209
2369
  let responseMessage: string;
2210
- const statusIcon = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '🔧' : '⚠️') : '';
2370
+ const statusMarker = data.validation?.hasWarnings ? (data.validation.errors?.length > 0 ? '[WRENCH]' : '[TIP]') : '[SUCCESS]';
2211
2371
 
2212
2372
  // Build conversational response for non-streaming mode
2213
2373
  if (data.isUpdate) {
2214
- responseMessage = `${statusIcon} **Updated: "${data.title}"**\n\nI've made the requested changes to your component. You can view the updated version in Storybook.\n\n_Check the Docs tab to see both the rendered component and its code._`;
2374
+ responseMessage = `${statusMarker} **Updated: "${data.title}"**\n\nI've made the requested changes to your component. You can view the updated version in Storybook.\n\n_Check the Docs tab to see both the rendered component and its code._`;
2215
2375
  } else {
2216
- responseMessage = `${statusIcon} **Created: "${data.title}"**\n\nI've generated the component with the requested features. You can view it in Storybook where you'll see both the rendered component and its markup.\n\n💡 **Note**: If you don't see the story immediately, you may need to refresh your Storybook page (Cmd/Ctrl + R).`;
2376
+ responseMessage = `${statusMarker} **Created: "${data.title}"**\n\nI've generated the component with the requested features. You can view it in Storybook where you'll see both the rendered component and its markup.\n\n[TIP] **Note**: If you don't see the story immediately, you may need to refresh your Storybook page (Cmd/Ctrl + R).`;
2217
2377
  }
2218
2378
 
2219
2379
  const aiMsg: Message = { role: 'ai', content: responseMessage };
@@ -2234,11 +2394,6 @@ function StoryUIPanel() {
2234
2394
  if (chatIndex !== -1) chats[chatIndex] = updatedSession;
2235
2395
  saveChats(chats);
2236
2396
  setRecentChats(chats);
2237
-
2238
- // Store code for Source Code panel
2239
- if (data.code) {
2240
- storeGeneratedCode(activeChatId, data.code, activeTitle || data.title);
2241
- }
2242
2397
  } else {
2243
2398
  const chatId = data.storyId || data.fileName || Date.now().toString();
2244
2399
  const chatTitle = data.title || userInput;
@@ -2256,11 +2411,6 @@ function StoryUIPanel() {
2256
2411
  if (chats.length > MAX_RECENT_CHATS) chats.splice(MAX_RECENT_CHATS);
2257
2412
  saveChats(chats);
2258
2413
  setRecentChats(chats);
2259
-
2260
- // Store code for Source Code panel
2261
- if (data.code) {
2262
- storeGeneratedCode(chatId, data.code, chatTitle);
2263
- }
2264
2414
  }
2265
2415
  } catch (err: unknown) {
2266
2416
  const errorMessage = err instanceof Error ? err.message : 'Unknown error';
@@ -2313,7 +2463,7 @@ function StoryUIPanel() {
2313
2463
  };
2314
2464
 
2315
2465
  return (
2316
- <div style={STYLES.container}>
2466
+ <div className="story-ui-panel" style={STYLES.container}>
2317
2467
  {/* Sidebar */}
2318
2468
  <div style={{
2319
2469
  ...STYLES.sidebar,
@@ -2326,30 +2476,35 @@ function StoryUIPanel() {
2326
2476
  style={STYLES.sidebarToggle}
2327
2477
  title="Collapse sidebar"
2328
2478
  onMouseEnter={(e) => {
2329
- e.currentTarget.style.transform = 'translateY(-1px)';
2330
- e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.4)';
2479
+ e.currentTarget.style.background = 'rgba(59, 130, 246, 0.25)';
2480
+ e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
2331
2481
  }}
2332
2482
  onMouseLeave={(e) => {
2333
- e.currentTarget.style.transform = 'translateY(0)';
2334
- e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.3)';
2483
+ e.currentTarget.style.background = 'rgba(59, 130, 246, 0.15)';
2484
+ e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.3)';
2335
2485
  }}
2336
2486
  >
2337
- <span style={{ lineHeight: '0.5', display: 'inline-block', alignItems: 'center', width: '10px', height: '10px' }}>☰</span>
2487
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
2488
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
2489
+ </svg>
2338
2490
  <span>Chats</span>
2339
2491
  </button>
2340
2492
  <button
2341
2493
  onClick={handleNewChat}
2342
2494
  style={STYLES.newChatButton}
2343
2495
  onMouseEnter={(e) => {
2344
- e.currentTarget.style.transform = 'translateY(-1px)';
2345
- e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.4)';
2496
+ e.currentTarget.style.background = '#2563eb';
2497
+ e.currentTarget.style.boxShadow = '0 4px 12px rgba(59, 130, 246, 0.4)';
2346
2498
  }}
2347
2499
  onMouseLeave={(e) => {
2348
- e.currentTarget.style.transform = 'translateY(0)';
2349
- e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.2)';
2500
+ e.currentTarget.style.background = '#3b82f6';
2501
+ e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.25)';
2350
2502
  }}
2351
2503
  >
2352
- <span style={{ lineHeight: '0.5', display: 'inline-block', alignItems: 'center', width: '10px', height: '10px' }}>+</span>
2504
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
2505
+ <line x1="12" y1="5" x2="12" y2="19"/>
2506
+ <line x1="5" y1="12" x2="19" y2="12"/>
2507
+ </svg>
2353
2508
  <span>New Chat</span>
2354
2509
  </button>
2355
2510
  {recentChats.length > 0 && (
@@ -2395,10 +2550,74 @@ function StoryUIPanel() {
2395
2550
  style={STYLES.deleteButton}
2396
2551
  title="Delete chat"
2397
2552
  >
2398
-
2553
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
2399
2554
  </button>
2400
2555
  </div>
2401
2556
  ))}
2557
+
2558
+ {/* Generated Files Section - orphan stories without chat history */}
2559
+ {orphanStories.length > 0 && (
2560
+ <>
2561
+ <div style={{
2562
+ color: '#64748b',
2563
+ fontSize: '12px',
2564
+ marginTop: '16px',
2565
+ marginBottom: '8px',
2566
+ fontWeight: '500',
2567
+ textTransform: 'uppercase',
2568
+ letterSpacing: '0.05em',
2569
+ }}>
2570
+ Generated Files
2571
+ </div>
2572
+ {orphanStories.map(story => (
2573
+ <div
2574
+ key={story.id}
2575
+ style={{
2576
+ ...STYLES.chatItem,
2577
+ background: 'rgba(251, 191, 36, 0.1)',
2578
+ borderLeft: '3px solid rgba(251, 191, 36, 0.5)',
2579
+ }}
2580
+ onMouseEnter={(e) => {
2581
+ e.currentTarget.style.background = 'rgba(251, 191, 36, 0.15)';
2582
+ const deleteBtn = e.currentTarget.querySelector('.delete-orphan-btn') as HTMLElement;
2583
+ if (deleteBtn) deleteBtn.style.opacity = '1';
2584
+ }}
2585
+ onMouseLeave={(e) => {
2586
+ e.currentTarget.style.background = 'rgba(251, 191, 36, 0.1)';
2587
+ const deleteBtn = e.currentTarget.querySelector('.delete-orphan-btn') as HTMLElement;
2588
+ if (deleteBtn) deleteBtn.style.opacity = '0';
2589
+ }}
2590
+ >
2591
+ <div style={STYLES.chatItemTitle}>{story.title}</div>
2592
+ <div style={{ ...STYLES.chatItemTime, fontSize: '11px' }}>
2593
+ {story.fileName}
2594
+ </div>
2595
+ <button
2596
+ className="delete-orphan-btn"
2597
+ onClick={async (e) => {
2598
+ e.stopPropagation();
2599
+ try {
2600
+ const response = await fetch(`${STORIES_API}/${story.id}`, {
2601
+ method: 'DELETE',
2602
+ });
2603
+ if (response.ok) {
2604
+ setOrphanStories(prev => prev.filter(s => s.id !== story.id));
2605
+ } else {
2606
+ console.error('Failed to delete orphan story');
2607
+ }
2608
+ } catch (err) {
2609
+ console.error('Error deleting orphan story:', err);
2610
+ }
2611
+ }}
2612
+ style={STYLES.deleteButton}
2613
+ title="Delete generated file"
2614
+ >
2615
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
2616
+ </button>
2617
+ </div>
2618
+ ))}
2619
+ </>
2620
+ )}
2402
2621
  </div>
2403
2622
  )}
2404
2623
  {!sidebarOpen && (
@@ -2423,7 +2642,7 @@ function StoryUIPanel() {
2423
2642
  e.currentTarget.style.background = '#3b82f6';
2424
2643
  }}
2425
2644
  >
2426
- <span style={{ lineHeight: '0.4', display: 'inline-block', height: '10px' }}>☰</span>
2645
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
2427
2646
  </button>
2428
2647
  </div>
2429
2648
  )}
@@ -2451,33 +2670,37 @@ function StoryUIPanel() {
2451
2670
 
2452
2671
  <div style={STYLES.chatHeader}>
2453
2672
  <h1 style={{
2454
- fontSize: '24px',
2673
+ fontSize: '22px',
2455
2674
  margin: 0,
2456
- fontWeight: '600',
2675
+ fontWeight: '700',
2457
2676
  background: 'linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%)',
2458
2677
  WebkitBackgroundClip: 'text',
2459
2678
  WebkitTextFillColor: 'transparent',
2460
- display: 'inline-block'
2679
+ display: 'inline-block',
2680
+ letterSpacing: '-0.02em'
2461
2681
  }}>
2462
2682
  Story UI
2463
2683
  </h1>
2464
- <p style={{ fontSize: '14px', margin: '4px 0 0 0', color: '#94a3b8' }}>
2684
+ <p style={{ fontSize: '14px', margin: '6px 0 0 0', color: '#94a3b8', fontWeight: '500' }}>
2465
2685
  Generate Storybook stories with AI
2466
2686
  </p>
2467
- <div style={{
2468
- display: 'flex',
2469
- alignItems: 'center',
2470
- gap: '8px',
2471
- marginTop: '8px',
2472
- fontSize: '12px'
2687
+ <div style={{
2688
+ display: 'flex',
2689
+ alignItems: 'center',
2690
+ gap: '6px',
2691
+ marginTop: '10px',
2692
+ fontSize: '11px'
2473
2693
  }}>
2474
2694
  <div style={{
2475
- width: '8px',
2476
- height: '8px',
2695
+ width: '6px',
2696
+ height: '6px',
2477
2697
  borderRadius: '50%',
2478
2698
  backgroundColor: connectionStatus.connected ? '#10b981' : '#f87171'
2479
2699
  }}></div>
2480
- <span style={{ color: connectionStatus.connected ? '#10b981' : '#f87171' }}>
2700
+ <span
2701
+ className={`story-ui-status ${connectionStatus.connected ? 'story-ui-status-connected' : 'story-ui-status-disconnected'}`}
2702
+ style={{ color: connectionStatus.connected ? '#10b981' : '#ef4444', fontWeight: '400' }}
2703
+ >
2481
2704
  {connectionStatus.connected
2482
2705
  ? `Connected to ${getConnectionDisplayText()}`
2483
2706
  : `Disconnected: ${connectionStatus.error || 'Server not running'}`
@@ -2493,8 +2716,8 @@ function StoryUIPanel() {
2493
2716
  marginTop: '12px',
2494
2717
  flexWrap: 'wrap'
2495
2718
  }}>
2496
- <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
2497
- <label style={{ fontSize: '12px', color: '#94a3b8' }}>Provider:</label>
2719
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
2720
+ <label style={{ fontSize: '12px', color: '#94a3b8', fontWeight: '500' }}>Provider:</label>
2498
2721
  <select
2499
2722
  value={selectedProvider}
2500
2723
  onChange={(e) => {
@@ -2521,8 +2744,8 @@ function StoryUIPanel() {
2521
2744
  ))}
2522
2745
  </select>
2523
2746
  </div>
2524
- <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
2525
- <label style={{ fontSize: '12px', color: '#94a3b8' }}>Model:</label>
2747
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
2748
+ <label style={{ fontSize: '12px', color: '#94a3b8', fontWeight: '500' }}>Model:</label>
2526
2749
  <select
2527
2750
  value={selectedModel}
2528
2751
  onChange={(e) => setSelectedModel(e.target.value)}
@@ -2540,7 +2763,7 @@ function StoryUIPanel() {
2540
2763
  {availableProviders
2541
2764
  .find(p => p.type === selectedProvider)
2542
2765
  ?.models.map(model => (
2543
- <option key={model} value={model}>{model}</option>
2766
+ <option key={model} value={model}>{getModelDisplayName(model)}</option>
2544
2767
  ))}
2545
2768
  </select>
2546
2769
  </div>
@@ -2566,7 +2789,10 @@ function StoryUIPanel() {
2566
2789
 
2567
2790
  {conversation.map((msg, i) => (
2568
2791
  <div key={i} style={STYLES.messageContainer}>
2569
- <div style={msg.role === 'user' ? STYLES.userMessage : STYLES.aiMessage}>
2792
+ <div
2793
+ className={`story-ui-message ${msg.role === 'user' ? 'story-ui-user-message' : 'story-ui-ai-message'}`}
2794
+ style={msg.role === 'user' ? STYLES.userMessage : STYLES.aiMessage}
2795
+ >
2570
2796
  {msg.role === 'ai' ? renderMarkdown(msg.content) : msg.content}
2571
2797
  {/* Show attached images in user messages */}
2572
2798
  {msg.role === 'user' && msg.attachedImages && msg.attachedImages.length > 0 && (
@@ -2631,7 +2857,7 @@ function StoryUIPanel() {
2631
2857
  onClick={() => removeAttachedImage(img.id)}
2632
2858
  title="Remove image"
2633
2859
  >
2634
- ×
2860
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
2635
2861
  </button>
2636
2862
  </div>
2637
2863
  ))}
@@ -2704,27 +2930,29 @@ function StoryUIPanel() {
2704
2930
  style={{
2705
2931
  ...STYLES.sendButton,
2706
2932
  ...(loading || (!input.trim() && attachedImages.length === 0) ? {
2707
- opacity: 0.5,
2933
+ opacity: 0.4,
2708
2934
  cursor: 'not-allowed',
2709
- background: '#6b7280',
2935
+ background: '#64748b',
2710
2936
  boxShadow: 'none'
2711
2937
  } : {})
2712
2938
  }}
2713
2939
  onMouseEnter={(e) => {
2714
2940
  if (!loading && (input.trim() || attachedImages.length > 0)) {
2715
- e.currentTarget.style.transform = 'scale(1.05)';
2716
- e.currentTarget.style.boxShadow = '0 4px 16px rgba(16, 185, 129, 0.4)';
2941
+ e.currentTarget.style.background = '#2563eb';
2942
+ e.currentTarget.style.boxShadow = '0 4px 16px rgba(59, 130, 246, 0.5)';
2717
2943
  }
2718
2944
  }}
2719
2945
  onMouseLeave={(e) => {
2720
- e.currentTarget.style.transform = 'scale(1)';
2721
- e.currentTarget.style.boxShadow = '0 2px 8px rgba(16, 185, 129, 0.3)';
2946
+ if (!loading && (input.trim() || attachedImages.length > 0)) {
2947
+ e.currentTarget.style.background = '#3b82f6';
2948
+ e.currentTarget.style.boxShadow = '0 2px 8px rgba(59, 130, 246, 0.35)';
2949
+ }
2722
2950
  }}
2723
2951
  >
2724
- <span>Send</span>
2725
- <svg width={16} height={16} viewBox="0 0 24 24" fill="currentColor">
2952
+ <svg width={18} height={18} viewBox="0 0 24 24" fill="currentColor">
2726
2953
  <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
2727
2954
  </svg>
2955
+ <span>Send</span>
2728
2956
  </button>
2729
2957
  </form>
2730
2958
  </div>