@eventcatalog/core 4.10.12 → 4.10.13

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 (37) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-S4LLGOO4.js → chunk-34RMFKFB.js} +1 -1
  6. package/dist/{chunk-Y5XU3YLT.js → chunk-IIECZFXN.js} +1 -1
  7. package/dist/{chunk-VFSEVDCQ.js → chunk-JHUICVBT.js} +1 -1
  8. package/dist/{chunk-A53VCEEA.js → chunk-PADMH2RJ.js} +1 -1
  9. package/dist/{chunk-7URQ754Q.js → chunk-SSSN5FXC.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +208 -119
  19. package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +28 -8
  20. package/eventcatalog/src/components/ChatPanel/OfflineReply.tsx +45 -0
  21. package/eventcatalog/src/components/Header.astro +11 -6
  22. package/eventcatalog/src/components/MDX/Design/Design.astro +2 -2
  23. package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +2 -2
  24. package/eventcatalog/src/components/MDX/Flow/Flow.astro +2 -2
  25. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -2
  26. package/eventcatalog/src/components/Search/Search.astro +11 -4
  27. package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +29 -24
  28. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +3 -3
  29. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +2 -2
  30. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +5 -3
  31. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +10 -4
  32. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +2 -2
  33. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +3 -3
  34. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +2 -2
  35. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -2
  36. package/eventcatalog/src/utils/feature.ts +1 -0
  37. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef, useCallback, useState, useMemo, memo } from 'react';
2
- import { X, Square, Trash2, BookOpen, Copy, Check, Maximize2, Minimize2, Wrench, ChevronDown, MessageSquare } from 'lucide-react';
2
+ import { X, Square, Trash2, Sparkles, Copy, Check, Maximize2, Minimize2, Wrench, ChevronDown, MessageSquare } from 'lucide-react';
3
3
  import { useChat } from '@ai-sdk/react';
4
4
  import { DefaultChatTransport } from 'ai';
5
5
  import ReactMarkdown from 'react-markdown';
@@ -9,6 +9,7 @@ import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
9
9
  import * as Dialog from '@radix-ui/react-dialog';
10
10
  import * as Popover from '@radix-ui/react-popover';
11
11
  import { buildUrl } from '@utils/url-builder';
12
+ import OfflineReply, { offlineReplyText } from './OfflineReply';
12
13
 
13
14
  interface ToolMetadata {
14
15
  name: string;
@@ -18,6 +19,44 @@ interface ToolMetadata {
18
19
 
19
20
  // CSS keyframes - defined once outside component to avoid re-injection
20
21
  const CHAT_PANEL_STYLES = `
22
+ @keyframes ec-chat-breathe {
23
+ 0%, 100% { transform: scale(1); opacity: .7; }
24
+ 50% { transform: scale(1.06); opacity: 1; }
25
+ }
26
+ @keyframes ec-chat-dot {
27
+ 0%, 80%, 100% { transform: translateY(0); opacity: .35; }
28
+ 40% { transform: translateY(-3px); opacity: 1; }
29
+ }
30
+ .ec-chat-surface { font-weight: 400; }
31
+ .ec-chat-avatar { background: rgb(var(--ec-accent) / .06); animation: ec-chat-breathe 5s ease-in-out infinite; }
32
+ .ec-chat-suggestion { width: fit-content; max-width: 100%; transition: background 180ms, transform 180ms; }
33
+ .ec-chat-suggestion:hover { transform: translateX(3px); }
34
+ .ec-chat-message { animation: fadeInUp 280ms ease-out both; }
35
+ .ec-chat-offline > * { animation: fadeInUp 350ms ease-out both; }
36
+ .ec-chat-offline > :nth-child(2) { animation-delay: 100ms; }
37
+ .ec-chat-offline > :nth-child(3) { animation-delay: 220ms; }
38
+ .ec-chat-offline > :nth-child(4) { animation-delay: 340ms; }
39
+ .ec-chat-dot { width: 4px; height: 4px; border-radius: 50%; background: rgb(var(--ec-accent)); animation: ec-chat-dot 1.2s infinite; }
40
+ .ec-chat-dot:nth-child(2) { animation-delay: 150ms; }
41
+ .ec-chat-dot:nth-child(3) { animation-delay: 300ms; }
42
+ .ec-chat-composer {
43
+ border-radius: 20px;
44
+ border-color: rgb(var(--ec-accent) / .22);
45
+ background: rgb(var(--ec-page-bg));
46
+ box-shadow: 0 0 0 3px rgb(var(--ec-accent) / .04), 0 8px 28px rgb(var(--ec-accent) / .04);
47
+ transition: border-color 200ms, box-shadow 200ms;
48
+ }
49
+ .ec-chat-composer:focus-within { border-color: rgb(var(--ec-accent) / .5); box-shadow: 0 0 0 3px rgb(var(--ec-accent) / .09); }
50
+ .ec-chat-composer textarea { display: block; resize: none; min-height: 104px; padding: 14px 16px 46px; border-radius: 20px; }
51
+ .ec-chat-composer-actions { top: auto; bottom: 10px; transform: none; }
52
+ @media (prefers-reduced-motion: reduce) {
53
+ .ec-chat-surface, .ec-chat-surface *, #eventcatalog-header, #eventcatalog-application {
54
+ animation: none !important;
55
+ transition: none !important;
56
+ scroll-behavior: auto !important;
57
+ }
58
+ }
59
+
21
60
  @keyframes fadeIn {
22
61
  from { opacity: 0; }
23
62
  to { opacity: 1; }
@@ -335,26 +374,27 @@ const getSuggestedQuestions = (pathname: string): SuggestedQuestion[] => {
335
374
  };
336
375
 
337
376
  interface ChatPanelProps {
377
+ configured?: boolean;
338
378
  isOpen: boolean;
339
379
  onClose: () => void;
340
380
  }
341
381
 
342
382
  const PANEL_WIDTH = 400;
343
383
 
344
- // Staggered fade-in animation styles (delays account for 800ms panel slide)
384
+ // Staggered fade-in animation styles (delays account for 420ms panel slide)
345
385
  const fadeInStyles = {
346
386
  header: {
347
387
  animation: 'fadeIn 0.5s ease-out 0.3s both',
348
388
  },
349
389
  welcome: {
350
- animation: 'fadeIn 0.6s ease-out 0.4s both',
390
+ animation: 'fadeInUp 0.45s ease-out 0.12s both',
351
391
  },
352
392
  // Label and questions will be staggered individually in the component
353
393
  questionsLabel: {
354
394
  animation: 'fadeIn 0.5s ease-out 0.7s both',
355
395
  },
356
396
  getQuestionStyle: (index: number) => ({
357
- animation: `fadeIn 0.5s ease-out ${0.85 + index * 0.12}s both`,
397
+ animation: `fadeIn 0.5s ease-out ${0.22 + index * 0.07}s both`,
358
398
  }),
359
399
  inputFocus: {
360
400
  animation: 'focusIn 0.6s ease-out 1.4s both',
@@ -436,12 +476,14 @@ const getCompletedTools = (message: { parts?: Array<any> }): string[] => {
436
476
  };
437
477
 
438
478
  // Skeleton loading component - memoized since it never changes
439
- const SkeletonLoader = memo(() => (
440
- <div className="animate-pulse space-y-3">
441
- <div className="h-4 bg-[rgb(var(--ec-content-hover))] rounded w-[90%]" />
442
- <div className="h-4 bg-[rgb(var(--ec-content-hover))] rounded w-[75%]" />
443
- <div className="h-4 bg-[rgb(var(--ec-content-hover))] rounded w-[85%]" />
444
- <div className="h-4 bg-[rgb(var(--ec-content-hover))] rounded w-[60%]" />
479
+ const SkeletonLoader = memo(({ label = 'Thinking it through…' }: { label?: string }) => (
480
+ <div role="status" className="ec-chat-message flex items-center gap-3 py-3 text-xs text-[rgb(var(--ec-page-text-muted))]">
481
+ <span className="flex items-center gap-1" aria-hidden="true">
482
+ <span className="ec-chat-dot" />
483
+ <span className="ec-chat-dot" />
484
+ <span className="ec-chat-dot" />
485
+ </span>
486
+ {label}
445
487
  </div>
446
488
  ));
447
489
  SkeletonLoader.displayName = 'SkeletonLoader';
@@ -517,13 +559,22 @@ const modalMarkdownComponents = {
517
559
  ),
518
560
  };
519
561
 
520
- const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
521
- const inputRef = useRef<HTMLInputElement>(null);
522
- const modalInputRef = useRef<HTMLInputElement>(null);
562
+ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
563
+ const inputRef = useRef<HTMLTextAreaElement>(null);
564
+ const modalInputRef = useRef<HTMLTextAreaElement>(null);
523
565
  const messagesEndRef = useRef<HTMLDivElement>(null);
524
566
  const modalMessagesEndRef = useRef<HTMLDivElement>(null);
525
567
  const messagesContainerRef = useRef<HTMLDivElement>(null);
526
568
  const [inputValue, setInputValue] = useState('');
569
+ const [isSetupThinking, setIsSetupThinking] = useState(false);
570
+ const setupReplyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
571
+
572
+ useEffect(
573
+ () => () => {
574
+ if (setupReplyTimer.current !== null) clearTimeout(setupReplyTimer.current);
575
+ },
576
+ []
577
+ );
527
578
  const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
528
579
  const [pathname, setPathname] = useState('');
529
580
  const [isFullscreen, setIsFullscreen] = useState(false);
@@ -543,7 +594,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
543
594
 
544
595
  // Fetch available tools when panel opens
545
596
  useEffect(() => {
546
- if (isOpen && tools.length === 0) {
597
+ if (configured && isOpen && tools.length === 0) {
547
598
  fetch(chatApiUrl)
548
599
  .then((res) => res.json())
549
600
  .then((data) => {
@@ -555,7 +606,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
555
606
  // Silently fail - tools info is optional
556
607
  });
557
608
  }
558
- }, [isOpen, tools.length]);
609
+ }, [configured, isOpen, tools.length, chatApiUrl]);
559
610
 
560
611
  // Get current URL (pathname + search) on mount and when panel opens
561
612
  useEffect(() => {
@@ -624,7 +675,9 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
624
675
 
625
676
  const isStreaming = status === 'streaming' && assistantHasContent;
626
677
  const isThinking =
627
- isWaitingForResponse || (messages.length > 0 && (status === 'submitted' || status === 'streaming') && !assistantHasContent);
678
+ isSetupThinking ||
679
+ isWaitingForResponse ||
680
+ (messages.length > 0 && (status === 'submitted' || status === 'streaming') && !assistantHasContent);
628
681
  const isLoading = isThinking || isStreaming;
629
682
 
630
683
  // Get currently running tools from the last assistant message
@@ -643,12 +696,13 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
643
696
 
644
697
  // Focus modal input when fullscreen opens
645
698
  useEffect(() => {
646
- if (isFullscreen && !isLoading) {
647
- setTimeout(() => {
699
+ if (isOpen && isFullscreen && !isLoading) {
700
+ const timer = setTimeout(() => {
648
701
  modalInputRef.current?.focus();
649
702
  }, 100);
703
+ return () => clearTimeout(timer);
650
704
  }
651
- }, [isFullscreen, isLoading]);
705
+ }, [isOpen, isFullscreen, isLoading]);
652
706
 
653
707
  // Handle escape key to close
654
708
  useEffect(() => {
@@ -657,23 +711,24 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
657
711
  onClose();
658
712
  }
659
713
  // Focus input on CMD+I or CTRL+I
660
- if ((e.metaKey || e.ctrlKey) && e.key === 'i' && isOpen) {
714
+ if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'i' && isOpen) {
661
715
  e.preventDefault();
662
- inputRef.current?.focus();
716
+ (isFullscreen ? modalInputRef : inputRef).current?.focus();
663
717
  }
664
718
  };
665
719
  document.addEventListener('keydown', handleEscape);
666
720
  return () => document.removeEventListener('keydown', handleEscape);
667
- }, [isOpen, onClose]);
721
+ }, [isOpen, isFullscreen, onClose]);
668
722
 
669
723
  // Focus input when opened and not loading
670
724
  useEffect(() => {
671
- if (isOpen && !isLoading) {
672
- setTimeout(() => {
725
+ if (isOpen && !isFullscreen && !isLoading) {
726
+ const timer = setTimeout(() => {
673
727
  inputRef.current?.focus();
674
728
  }, 100);
729
+ return () => clearTimeout(timer);
675
730
  }
676
- }, [isOpen, isLoading]);
731
+ }, [isOpen, isFullscreen, isLoading]);
677
732
 
678
733
  // Add/remove padding to main application and header when sidebar panel is open
679
734
  useEffect(() => {
@@ -683,31 +738,23 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
683
738
 
684
739
  const elements = [appEl, headerEl].filter(Boolean) as HTMLElement[];
685
740
 
686
- elements.forEach((el) => {
687
- // Add transition if not already present
688
- if (!el.style.transition) {
689
- el.style.transition = 'padding-right 800ms cubic-bezier(0.16, 1, 0.3, 1)';
690
- }
691
-
692
- // Only add padding when panel is open AND not in fullscreen mode
693
- if (isOpen && !isFullscreen) {
694
- el.style.paddingRight = `${PANEL_WIDTH}px`;
695
- } else {
696
- el.style.paddingRight = '0';
697
- }
698
- });
699
-
700
- // Hide docs sidebar when chat panel is open
701
- if (docsSidebarEl) {
702
- if (isOpen && !isFullscreen) {
703
- docsSidebarEl.style.display = 'none';
704
- } else {
705
- docsSidebarEl.style.display = '';
706
- }
707
- }
741
+ const desktop = window.matchMedia('(min-width: 1024px)');
742
+ const updateLayout = () => {
743
+ const pushContent = desktop.matches && isOpen && !isFullscreen;
744
+ elements.forEach((el) => {
745
+ if (!el.style.transition) {
746
+ el.style.transition = 'padding-right 420ms cubic-bezier(0.16, 1, 0.3, 1)';
747
+ }
748
+ el.style.paddingRight = pushContent ? `${PANEL_WIDTH}px` : '0';
749
+ });
750
+ if (docsSidebarEl) docsSidebarEl.style.display = pushContent ? 'none' : '';
751
+ };
752
+ updateLayout();
753
+ desktop.addEventListener('change', updateLayout);
708
754
 
709
755
  // Cleanup on unmount
710
756
  return () => {
757
+ desktop.removeEventListener('change', updateLayout);
711
758
  elements.forEach((el) => {
712
759
  el.style.paddingRight = '0';
713
760
  });
@@ -722,12 +769,49 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
722
769
  (text: string) => {
723
770
  if (!text.trim() || isLoading) return;
724
771
  setInputValue('');
772
+ if (!configured) {
773
+ setMessages((previous) => [...previous, { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text }] }]);
774
+ setIsSetupThinking(true);
775
+ setupReplyTimer.current = setTimeout(() => {
776
+ setMessages((previous) => [
777
+ ...previous,
778
+ {
779
+ id: crypto.randomUUID(),
780
+ role: 'assistant',
781
+ parts: [
782
+ {
783
+ type: 'text',
784
+ text: offlineReplyText,
785
+ },
786
+ ],
787
+ },
788
+ ]);
789
+ setupReplyTimer.current = null;
790
+ setIsSetupThinking(false);
791
+ }, 1200);
792
+ return;
793
+ }
725
794
  setIsWaitingForResponse(true);
726
795
  sendMessage({ text });
727
796
  },
728
- [isLoading, sendMessage]
797
+ [configured, isLoading, sendMessage, setMessages]
729
798
  );
730
799
 
800
+ const stopResponse = useCallback(() => {
801
+ if (setupReplyTimer.current !== null) {
802
+ clearTimeout(setupReplyTimer.current);
803
+ setupReplyTimer.current = null;
804
+ }
805
+ setIsSetupThinking(false);
806
+ setIsWaitingForResponse(false);
807
+ stop();
808
+ }, [stop]);
809
+
810
+ const clearMessages = useCallback(() => {
811
+ stopResponse();
812
+ setMessages([]);
813
+ }, [stopResponse, setMessages]);
814
+
731
815
  // Handle suggested action clicks
732
816
  const handleSuggestedAction = useCallback(
733
817
  (prompt: string) => {
@@ -738,7 +822,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
738
822
 
739
823
  // Handle input enter key
740
824
  const handleInputKeyDown = useCallback(
741
- (e: React.KeyboardEvent<HTMLInputElement>) => {
825
+ (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
742
826
  if (e.key === 'Enter' && !e.shiftKey) {
743
827
  e.preventDefault();
744
828
  submitMessage(inputValue);
@@ -769,13 +853,17 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
769
853
  {/* Panel - hidden when fullscreen modal is open */}
770
854
  {!isFullscreen && (
771
855
  <div
772
- className="fixed top-0 right-0 h-[100vh] z-[200] border-l border-[rgb(var(--ec-page-border))] flex flex-col overflow-hidden"
856
+ ref={(element) => {
857
+ element?.toggleAttribute('inert', !isOpen);
858
+ }}
859
+ aria-hidden={!isOpen}
860
+ className="ec-chat-surface fixed top-0 right-0 h-[100dvh] z-[200] border-l border-[rgb(var(--ec-page-border))] flex flex-col overflow-hidden"
773
861
  style={{
774
- width: `${PANEL_WIDTH}px`,
775
- transform: isOpen ? 'translateX(0)' : `translateX(${PANEL_WIDTH}px)`,
776
- transition: 'transform 800ms cubic-bezier(0.16, 1, 0.3, 1)',
862
+ width: `min(${PANEL_WIDTH}px, 100vw)`,
863
+ transform: isOpen ? 'translateX(0)' : 'translateX(100%)',
864
+ transition: 'transform 420ms cubic-bezier(0.16, 1, 0.3, 1)',
777
865
  background: `
778
- radial-gradient(ellipse 100% 40% at 50% 100%, rgb(var(--ec-accent) / 0.15) 0%, transparent 100%),
866
+ radial-gradient(ellipse 100% 40% at 50% 100%, rgb(var(--ec-accent) / 0.045) 0%, transparent 100%),
779
867
  rgb(var(--ec-page-bg))
780
868
  `,
781
869
  }}
@@ -784,8 +872,8 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
784
872
  <div className="flex-none shrink-0 border-b border-[rgb(var(--ec-page-border))]">
785
873
  <div className="flex items-center justify-between px-4 py-3">
786
874
  <div className="flex items-center space-x-2">
787
- <BookOpen size={16} className="text-[rgb(var(--ec-accent))]" />
788
- <span className="font-medium text-[rgb(var(--ec-header-text))] text-sm">EventCatalog Assistant</span>
875
+ <Sparkles size={16} className="text-[rgb(var(--ec-accent))]" />
876
+ <span className="font-medium text-[rgb(var(--ec-header-text))] text-sm">Assistant</span>
789
877
  </div>
790
878
  <div className="flex items-center space-x-1">
791
879
  {tools.length > 0 && (
@@ -838,7 +926,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
838
926
  </button>
839
927
  {hasMessages && (
840
928
  <button
841
- onClick={() => setMessages([])}
929
+ onClick={clearMessages}
842
930
  className="p-2 rounded-lg hover:bg-[rgb(var(--ec-header-border))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-header-text))] transition-colors"
843
931
  aria-label="Clear chat"
844
932
  title="Clear chat"
@@ -855,21 +943,6 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
855
943
  </button>
856
944
  </div>
857
945
  </div>
858
- {/* Thinking indicator */}
859
- {isThinking && (
860
- <div className="px-4 pb-2 flex items-center gap-2">
861
- <div className="w-1.5 h-1.5 bg-[rgb(var(--ec-accent))] rounded-full animate-pulse" />
862
- <span className="text-xs text-[rgb(var(--ec-icon-color))]">
863
- {runningTools.length > 0 ? (
864
- <>
865
- Using <span className="font-medium text-[rgb(var(--ec-accent))]">{runningTools[0]}</span>...
866
- </>
867
- ) : (
868
- 'Thinking...'
869
- )}
870
- </span>
871
- </div>
872
- )}
873
946
  </div>
874
947
 
875
948
  {/* Content */}
@@ -886,23 +959,23 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
886
959
  >
887
960
  {/* Icon with circular background */}
888
961
  <div className="relative mb-6">
889
- <div className="w-32 h-32 rounded-full bg-[rgb(var(--ec-accent)/0.15)] flex items-center justify-center">
890
- <MessageSquare size={56} className="text-[rgb(var(--ec-accent))]" strokeWidth={1.5} />
962
+ <div className="ec-chat-avatar w-24 h-24 rounded-full flex items-center justify-center">
963
+ <MessageSquare size={42} className="text-[rgb(var(--ec-accent))]" strokeWidth={1.5} />
891
964
  </div>
892
965
  </div>
893
- <h2 className="text-lg font-semibold text-[rgb(var(--ec-accent))] mb-1">{greeting}</h2>
966
+ <h2 className="text-lg font-medium text-[rgb(var(--ec-page-text))] mb-1">{greeting}</h2>
894
967
  <p className="text-sm font-normal text-[rgb(var(--ec-content-text))]">
895
968
  I'm here to help with your architecture
896
969
  </p>
897
970
  </div>
898
971
 
899
972
  {/* Suggested questions - pill style */}
900
- <div className="flex-none space-y-2">
973
+ <div className="flex-none flex flex-col items-start gap-2">
901
974
  {suggestedQuestions.map((question, index) => (
902
975
  <button
903
976
  key={index}
904
977
  onClick={() => handleSuggestedAction(question.prompt)}
905
- className="w-full text-left px-4 py-2.5 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-page-text)/0.05)] hover:bg-[rgb(var(--ec-accent)/0.15)] rounded-full transition-all duration-200"
978
+ className="ec-chat-suggestion text-left px-3 py-2 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-accent)/0.04)] hover:bg-[rgb(var(--ec-accent)/0.09)] rounded-full transition-all duration-200"
906
979
  style={isOpen ? fadeInStyles.getQuestionStyle(index) : undefined}
907
980
  >
908
981
  {question.label}
@@ -919,11 +992,16 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
919
992
  const completedTools = message.role === 'assistant' ? getCompletedTools(message) : [];
920
993
  const isLastMessage = messageIndex === messages.length - 1;
921
994
  return (
922
- <div key={message.id} className={`flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'}`}>
995
+ <div
996
+ key={message.id}
997
+ className={`ec-chat-message ${!configured && message.role === 'assistant' ? 'ec-chat-setup' : ''} flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'}`}
998
+ >
923
999
  {message.role === 'user' ? (
924
1000
  <div className="max-w-[85%] rounded-2xl rounded-br-md px-4 py-2.5 bg-[rgb(var(--ec-page-text)/0.05)]">
925
1001
  <p className="text-sm font-normal whitespace-pre-wrap text-[rgb(var(--ec-page-text))]">{content}</p>
926
1002
  </div>
1003
+ ) : !configured ? (
1004
+ <OfflineReply />
927
1005
  ) : (
928
1006
  <>
929
1007
  {/* Tools used indicator */}
@@ -956,7 +1034,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
956
1034
  <button
957
1035
  key={index}
958
1036
  onClick={() => handleSuggestedAction(suggestion)}
959
- className="px-4 py-2.5 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-page-text)/0.05)] hover:bg-[rgb(var(--ec-accent)/0.15)] rounded-full transition-all duration-200 text-left"
1037
+ className="ec-chat-suggestion px-3 py-2 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-accent)/0.04)] hover:bg-[rgb(var(--ec-accent)/0.09)] rounded-full transition-all duration-200 text-left"
960
1038
  style={fadeInStyles.getFollowUpStyle(index)}
961
1039
  >
962
1040
  {suggestion}
@@ -973,7 +1051,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
973
1051
  {/* Skeleton loading indicator */}
974
1052
  {isThinking && (
975
1053
  <div className="w-full">
976
- <SkeletonLoader />
1054
+ <SkeletonLoader label={runningTools.length > 0 ? `Using ${runningTools[0]}…` : undefined} />
977
1055
  </div>
978
1056
  )}
979
1057
 
@@ -1008,10 +1086,11 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1008
1086
  {/* Input area (Fixed at bottom) */}
1009
1087
  <div className="flex-none px-4 py-3" key={isOpen ? 'input-open' : 'input-closed'}>
1010
1088
  <form onSubmit={handleSubmit}>
1011
- <div className="relative bg-[rgb(var(--ec-page-bg)/0.5)] backdrop-blur-sm rounded-xl border border-[rgb(var(--ec-accent)/0.3)] focus-within:border-[rgb(var(--ec-accent)/0.5)] focus-within:ring-2 focus-within:ring-[rgb(var(--ec-accent)/0.1)] transition-all">
1012
- <input
1089
+ <div className="ec-chat-composer relative bg-[rgb(var(--ec-page-bg)/0.5)] backdrop-blur-sm rounded-xl border border-[rgb(var(--ec-accent)/0.3)] focus-within:border-[rgb(var(--ec-accent)/0.5)] focus-within:ring-2 focus-within:ring-[rgb(var(--ec-accent)/0.1)] transition-all">
1090
+ <textarea
1013
1091
  ref={inputRef}
1014
- type="text"
1092
+ rows={2}
1093
+ aria-label="Ask AI a question"
1015
1094
  value={inputValue}
1016
1095
  onChange={(e) => setInputValue(e.target.value)}
1017
1096
  onKeyDown={handleInputKeyDown}
@@ -1019,11 +1098,21 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1019
1098
  disabled={isLoading}
1020
1099
  className="w-full px-4 py-3 pr-16 bg-transparent text-[rgb(var(--ec-input-text))] placeholder-[rgb(var(--ec-input-placeholder))] focus:outline-hidden text-sm disabled:opacity-50 rounded-xl"
1021
1100
  />
1022
- <div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
1023
- {isStreaming ? (
1101
+ <div className="absolute bottom-4 left-4 right-20 truncate text-[10px] text-[rgb(var(--ec-page-text-muted))]">
1102
+ <span className="mr-1.5 rounded bg-[rgb(var(--ec-accent)/0.08)] px-1 py-0.5 font-medium text-[rgb(var(--ec-accent))]">
1103
+ AI
1104
+ </span>
1105
+ {configured
1106
+ ? pageContext
1107
+ ? `Based on ${pageContext.name}`
1108
+ : 'Your architecture, connected'
1109
+ : 'Explore your catalog with AI'}
1110
+ </div>
1111
+ <div className="ec-chat-composer-actions absolute right-2 z-10">
1112
+ {isStreaming || isSetupThinking ? (
1024
1113
  <button
1025
1114
  type="button"
1026
- onClick={() => stop()}
1115
+ onClick={stopResponse}
1027
1116
  className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
1028
1117
  aria-label="Stop generating"
1029
1118
  >
@@ -1074,10 +1163,10 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1074
1163
  <Dialog.Portal>
1075
1164
  <Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[300]" />
1076
1165
  <Dialog.Content
1077
- className="fixed inset-y-4 left-1/2 -translate-x-1/2 w-[95%] max-w-4xl md:inset-y-8 rounded-xl shadow-2xl z-[301] flex flex-col overflow-hidden focus:outline-hidden border border-[rgb(var(--ec-page-border))]"
1166
+ className="ec-chat-surface fixed inset-y-4 left-1/2 -translate-x-1/2 w-[95%] max-w-4xl md:inset-y-8 rounded-xl shadow-2xl z-[301] flex flex-col overflow-hidden focus:outline-hidden border border-[rgb(var(--ec-page-border))]"
1078
1167
  style={{
1079
1168
  background: `
1080
- radial-gradient(ellipse 100% 40% at 50% 100%, rgb(var(--ec-accent) / 0.15) 0%, transparent 100%),
1169
+ radial-gradient(ellipse 100% 40% at 50% 100%, rgb(var(--ec-accent) / 0.045) 0%, transparent 100%),
1081
1170
  rgb(var(--ec-page-bg))
1082
1171
  `,
1083
1172
  }}
@@ -1086,7 +1175,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1086
1175
  <div className="flex items-center justify-between px-5 py-3 border-b border-[rgb(var(--ec-page-border))] flex-shrink-0">
1087
1176
  <div className="flex items-center space-x-2.5">
1088
1177
  <div className="p-1.5 bg-[rgb(var(--ec-accent-subtle))] rounded-lg">
1089
- <BookOpen size={18} className="text-[rgb(var(--ec-accent))]" />
1178
+ <Sparkles size={18} className="text-[rgb(var(--ec-accent))]" />
1090
1179
  </div>
1091
1180
  <Dialog.Title className="text-base font-medium text-[rgb(var(--ec-page-text))]">Ask AI</Dialog.Title>
1092
1181
  </div>
@@ -1137,7 +1226,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1137
1226
  )}
1138
1227
  {hasMessages && (
1139
1228
  <button
1140
- onClick={() => setMessages([])}
1229
+ onClick={clearMessages}
1141
1230
  className="p-2 rounded-lg hover:bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] transition-colors"
1142
1231
  aria-label="Clear chat"
1143
1232
  title="Clear chat"
@@ -1166,22 +1255,6 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1166
1255
  </div>
1167
1256
  </div>
1168
1257
 
1169
- {/* Thinking indicator */}
1170
- {isThinking && (
1171
- <div className="px-5 py-2 flex items-center gap-2 border-b border-[rgb(var(--ec-page-border))]">
1172
- <div className="w-1.5 h-1.5 bg-[rgb(var(--ec-accent))] rounded-full animate-pulse" />
1173
- <span className="text-sm text-[rgb(var(--ec-page-text-muted))]">
1174
- {runningTools.length > 0 ? (
1175
- <>
1176
- Using <span className="font-medium text-[rgb(var(--ec-accent))]">{runningTools[0]}</span>...
1177
- </>
1178
- ) : (
1179
- 'Thinking...'
1180
- )}
1181
- </span>
1182
- </div>
1183
- )}
1184
-
1185
1258
  {/* Modal Content */}
1186
1259
  <div className="flex-1 overflow-y-auto px-6 py-6">
1187
1260
  {!hasMessages ? (
@@ -1191,11 +1264,11 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1191
1264
  <div className="flex-1 flex flex-col justify-center items-center text-center">
1192
1265
  {/* Icon with circular background */}
1193
1266
  <div className="relative mb-8">
1194
- <div className="w-40 h-40 rounded-full bg-[rgb(var(--ec-accent)/0.15)] flex items-center justify-center">
1195
- <MessageSquare size={72} className="text-[rgb(var(--ec-accent))]" strokeWidth={1.5} />
1267
+ <div className="ec-chat-avatar w-28 h-28 rounded-full flex items-center justify-center">
1268
+ <MessageSquare size={48} className="text-[rgb(var(--ec-accent))]" strokeWidth={1.5} />
1196
1269
  </div>
1197
1270
  </div>
1198
- <h2 className="text-2xl font-semibold text-[rgb(var(--ec-accent))] mb-2">{greeting}</h2>
1271
+ <h2 className="text-2xl font-medium text-[rgb(var(--ec-page-text))] mb-2">{greeting}</h2>
1199
1272
  <p className="font-normal text-[rgb(var(--ec-content-text))] text-center">
1200
1273
  I'm here to help with your architecture
1201
1274
  </p>
@@ -1207,7 +1280,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1207
1280
  <button
1208
1281
  key={index}
1209
1282
  onClick={() => handleSuggestedAction(question.prompt)}
1210
- className="px-4 py-2.5 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-page-text)/0.05)] hover:bg-[rgb(var(--ec-accent)/0.15)] rounded-full transition-all duration-200 text-left"
1283
+ className="ec-chat-suggestion px-3 py-2 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-accent)/0.04)] hover:bg-[rgb(var(--ec-accent)/0.09)] rounded-full transition-all duration-200 text-left"
1211
1284
  >
1212
1285
  {question.label}
1213
1286
  </button>
@@ -1223,11 +1296,16 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1223
1296
  const completedTools = message.role === 'assistant' ? getCompletedTools(message) : [];
1224
1297
  const isLastMessage = messageIndex === messages.length - 1;
1225
1298
  return (
1226
- <div key={message.id} className={`flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'}`}>
1299
+ <div
1300
+ key={message.id}
1301
+ className={`ec-chat-message ${!configured && message.role === 'assistant' ? 'ec-chat-setup' : ''} flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'}`}
1302
+ >
1227
1303
  {message.role === 'user' ? (
1228
1304
  <div className="max-w-[75%] rounded-2xl rounded-br-md px-4 py-2.5 bg-[rgb(var(--ec-page-text)/0.05)]">
1229
1305
  <p className="text-sm font-normal whitespace-pre-wrap text-[rgb(var(--ec-page-text))]">{content}</p>
1230
1306
  </div>
1307
+ ) : !configured ? (
1308
+ <OfflineReply />
1231
1309
  ) : (
1232
1310
  <>
1233
1311
  {/* Tools used indicator */}
@@ -1260,7 +1338,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1260
1338
  <button
1261
1339
  key={index}
1262
1340
  onClick={() => handleSuggestedAction(suggestion)}
1263
- className="px-4 py-2.5 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-page-text)/0.05)] hover:bg-[rgb(var(--ec-accent)/0.15)] rounded-full transition-all duration-200 text-left"
1341
+ className="ec-chat-suggestion px-3 py-2 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-accent)/0.04)] hover:bg-[rgb(var(--ec-accent)/0.09)] rounded-full transition-all duration-200 text-left"
1264
1342
  style={fadeInStyles.getFollowUpStyle(index)}
1265
1343
  >
1266
1344
  {suggestion}
@@ -1276,7 +1354,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1276
1354
 
1277
1355
  {isThinking && (
1278
1356
  <div className="w-full max-w-md">
1279
- <SkeletonLoader />
1357
+ <SkeletonLoader label={runningTools.length > 0 ? `Using ${runningTools[0]}…` : undefined} />
1280
1358
  </div>
1281
1359
  )}
1282
1360
 
@@ -1300,10 +1378,11 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1300
1378
  {/* Modal Input area */}
1301
1379
  <div className="flex-shrink-0 px-6 py-4">
1302
1380
  <form onSubmit={handleSubmit} className="max-w-3xl mx-auto">
1303
- <div className="relative bg-[rgb(var(--ec-page-bg)/0.5)] backdrop-blur-sm rounded-xl border border-[rgb(var(--ec-accent)/0.3)] focus-within:border-[rgb(var(--ec-accent)/0.5)] focus-within:ring-2 focus-within:ring-[rgb(var(--ec-accent)/0.1)] transition-all">
1304
- <input
1381
+ <div className="ec-chat-composer relative bg-[rgb(var(--ec-page-bg)/0.5)] backdrop-blur-sm rounded-xl border border-[rgb(var(--ec-accent)/0.3)] focus-within:border-[rgb(var(--ec-accent)/0.5)] focus-within:ring-2 focus-within:ring-[rgb(var(--ec-accent)/0.1)] transition-all">
1382
+ <textarea
1305
1383
  ref={modalInputRef}
1306
- type="text"
1384
+ rows={2}
1385
+ aria-label="Ask AI a question"
1307
1386
  value={inputValue}
1308
1387
  onChange={(e) => setInputValue(e.target.value)}
1309
1388
  onKeyDown={handleInputKeyDown}
@@ -1311,11 +1390,21 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
1311
1390
  disabled={isLoading}
1312
1391
  className="w-full px-4 py-3.5 pr-20 bg-transparent text-[rgb(var(--ec-input-text))] placeholder-[rgb(var(--ec-input-placeholder))] focus:outline-hidden text-sm disabled:opacity-50 rounded-xl"
1313
1392
  />
1314
- <div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
1315
- {isStreaming ? (
1393
+ <div className="absolute bottom-4 left-4 right-20 truncate text-[10px] text-[rgb(var(--ec-page-text-muted))]">
1394
+ <span className="mr-1.5 rounded bg-[rgb(var(--ec-accent)/0.08)] px-1 py-0.5 font-medium text-[rgb(var(--ec-accent))]">
1395
+ AI
1396
+ </span>
1397
+ {configured
1398
+ ? pageContext
1399
+ ? `Based on ${pageContext.name}`
1400
+ : 'Your architecture, connected'
1401
+ : 'Explore your catalog with AI'}
1402
+ </div>
1403
+ <div className="ec-chat-composer-actions absolute right-2 z-10">
1404
+ {isStreaming || isSetupThinking ? (
1316
1405
  <button
1317
1406
  type="button"
1318
- onClick={() => stop()}
1407
+ onClick={stopResponse}
1319
1408
  className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
1320
1409
  aria-label="Stop generating"
1321
1410
  >