@eventcatalog/core 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) 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-VO5WYA44.js → chunk-AA47DJ43.js} +1 -1
  6. package/dist/{chunk-EKGR533N.js → chunk-GGRXP5WM.js} +1 -1
  7. package/dist/{chunk-E5Q7TZYT.js → chunk-L3QRQT7U.js} +1 -1
  8. package/dist/{chunk-BYP43AAT.js → chunk-RWYEP5SD.js} +1 -1
  9. package/dist/{chunk-KF5PARQK.js → chunk-VPQCMMRM.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.config.d.cts +7 -0
  14. package/dist/eventcatalog.config.d.ts +7 -0
  15. package/dist/eventcatalog.js +5 -5
  16. package/dist/generate.cjs +1 -1
  17. package/dist/generate.js +3 -3
  18. package/dist/utils/cli-logger.cjs +1 -1
  19. package/dist/utils/cli-logger.js +2 -2
  20. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +520 -247
  21. package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +3 -3
  22. package/eventcatalog/src/components/Checkbox.astro +7 -4
  23. package/eventcatalog/src/components/CopyAsMarkdown.tsx +15 -15
  24. package/eventcatalog/src/components/EnvironmentDropdown.tsx +15 -7
  25. package/eventcatalog/src/components/FavoriteButton.tsx +1 -1
  26. package/eventcatalog/src/components/Grids/DomainGrid.tsx +72 -60
  27. package/eventcatalog/src/components/Grids/MessageGrid.tsx +68 -48
  28. package/eventcatalog/src/components/Header.astro +15 -10
  29. package/eventcatalog/src/components/Lists/OwnersList.tsx +17 -10
  30. package/eventcatalog/src/components/Lists/PillListFlat.styles.css +12 -0
  31. package/eventcatalog/src/components/Lists/PillListFlat.tsx +15 -15
  32. package/eventcatalog/src/components/Lists/VersionList.astro +15 -5
  33. package/eventcatalog/src/components/MDX/Accordion/Accordion.tsx +3 -3
  34. package/eventcatalog/src/components/MDX/Admonition.tsx +49 -9
  35. package/eventcatalog/src/components/MDX/Attachments.astro +15 -11
  36. package/eventcatalog/src/components/MDX/ChannelInformation/ChannelInformation.tsx +29 -15
  37. package/eventcatalog/src/components/MDX/EntityPropertiesTable/EntityPropertiesTable.astro +20 -13
  38. package/eventcatalog/src/components/MDX/Link/Link.astro +1 -1
  39. package/eventcatalog/src/components/MDX/MessageTable/MessageTable.client.tsx +50 -29
  40. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +14 -12
  41. package/eventcatalog/src/components/MDX/NodeGraph/StepWalkthrough.tsx +4 -4
  42. package/eventcatalog/src/components/MDX/NodeGraph/StudioModal.tsx +4 -4
  43. package/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx +2 -2
  44. package/eventcatalog/src/components/MDX/ResourceGroupTable/ResourceGroupTable.client.tsx +54 -33
  45. package/eventcatalog/src/components/MDX/ResourceLink/ResourceLink.astro +1 -1
  46. package/eventcatalog/src/components/MDX/Steps/Step.astro +2 -2
  47. package/eventcatalog/src/components/MDX/Steps/Steps.astro +3 -3
  48. package/eventcatalog/src/components/MDX/Tabs/Tabs.astro +13 -9
  49. package/eventcatalog/src/components/MDX/Tiles/Tile.astro +25 -13
  50. package/eventcatalog/src/components/MDX/Tiles/Tiles.astro +1 -1
  51. package/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx +3 -3
  52. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +3 -3
  53. package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +29 -25
  54. package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +3 -3
  55. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +61 -42
  56. package/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx +13 -9
  57. package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +6 -4
  58. package/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx +17 -13
  59. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +35 -8
  60. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +33 -21
  61. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +1 -1
  62. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +41 -33
  63. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +19 -7
  64. package/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +8 -8
  65. package/eventcatalog/src/components/Search/Search.astro +3 -3
  66. package/eventcatalog/src/components/Search/SearchModal.tsx +65 -36
  67. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +31 -21
  68. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +92 -59
  69. package/eventcatalog/src/components/Tables/Table.tsx +25 -24
  70. package/eventcatalog/src/components/Tables/columns/ContainersTableColumns.tsx +22 -16
  71. package/eventcatalog/src/components/Tables/columns/DomainTableColumns.tsx +14 -11
  72. package/eventcatalog/src/components/Tables/columns/FlowTableColumns.tsx +6 -6
  73. package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +22 -16
  74. package/eventcatalog/src/components/Tables/columns/ServiceTableColumns.tsx +22 -16
  75. package/eventcatalog/src/components/Tables/columns/SharedColumns.tsx +4 -4
  76. package/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx +21 -13
  77. package/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx +30 -19
  78. package/eventcatalog/src/components/ThemeToggle.tsx +18 -0
  79. package/eventcatalog/src/enterprise/ai/chat-api.ts +24 -3
  80. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NestedItem.tsx +15 -7
  81. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NoResultsFound.tsx +2 -2
  82. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx +19 -15
  83. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +50 -17
  84. package/eventcatalog/src/layouts/DirectoryLayout.astro +11 -6
  85. package/eventcatalog/src/layouts/DiscoverLayout.astro +13 -8
  86. package/eventcatalog/src/layouts/Footer.astro +6 -6
  87. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +62 -14
  88. package/eventcatalog/src/pages/_index.astro +135 -179
  89. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +2 -2
  90. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +4 -4
  91. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +77 -63
  92. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +23 -24
  93. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +66 -50
  94. package/eventcatalog/src/pages/docs/custom/index.astro +2 -2
  95. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +25 -21
  96. package/eventcatalog/src/pages/docs/users/[id]/index.astro +25 -21
  97. package/eventcatalog/src/pages/schemas/explorer/index.astro +1 -1
  98. package/eventcatalog/src/pages/studio.astro +59 -31
  99. package/eventcatalog/src/remark-plugins/directives.ts +6 -6
  100. package/eventcatalog/src/remark-plugins/mermaid.ts +2 -2
  101. package/eventcatalog/src/stores/theme-store.ts +93 -0
  102. package/eventcatalog/src/styles/theme.css +255 -0
  103. package/eventcatalog/src/styles/themes/forest.css +230 -0
  104. package/eventcatalog/src/styles/themes/ocean.css +235 -0
  105. package/eventcatalog/src/styles/themes/sapphire.css +230 -0
  106. package/eventcatalog/src/styles/themes/sunset.css +230 -0
  107. package/eventcatalog/src/utils/feature.ts +4 -0
  108. package/eventcatalog/tailwind.config.mjs +6 -3
  109. package/package.json +7 -6
@@ -1,10 +1,10 @@
1
- import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
2
- import { X, Sparkles, Square, Trash2, BookOpen, Copy, Check, Maximize2, Minimize2, Wrench } from 'lucide-react';
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';
3
3
  import { useChat } from '@ai-sdk/react';
4
4
  import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
5
6
  import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
6
7
  import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
7
- import { lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
8
8
  import * as Dialog from '@radix-ui/react-dialog';
9
9
  import * as Popover from '@radix-ui/react-popover';
10
10
 
@@ -14,15 +14,65 @@ interface ToolMetadata {
14
14
  isCustom?: boolean;
15
15
  }
16
16
 
17
- // Code block component with copy functionality
18
- const CodeBlock = ({ language, children }: { language: string; children: string }) => {
17
+ // CSS keyframes - defined once outside component to avoid re-injection
18
+ const CHAT_PANEL_STYLES = `
19
+ @keyframes fadeIn {
20
+ from { opacity: 0; }
21
+ to { opacity: 1; }
22
+ }
23
+ @keyframes fadeInUp {
24
+ from {
25
+ opacity: 0;
26
+ transform: translateY(8px);
27
+ }
28
+ to {
29
+ opacity: 1;
30
+ transform: translateY(0);
31
+ }
32
+ }
33
+ @keyframes focusIn {
34
+ from {
35
+ box-shadow: 0 0 0 0 rgba(168, 85, 247, 0);
36
+ border-color: #e5e7eb;
37
+ }
38
+ to {
39
+ box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.15);
40
+ border-color: #c084fc;
41
+ }
42
+ }
43
+ @keyframes pulse-glow {
44
+ 0%, 100% { box-shadow: 0 0 8px rgba(147, 51, 234, 0.3); }
45
+ 50% { box-shadow: 0 0 16px rgba(147, 51, 234, 0.5); }
46
+ }
47
+ `;
48
+
49
+ // Stable style object for syntax highlighter
50
+ const CODE_BLOCK_STYLE = {
51
+ margin: 0,
52
+ borderRadius: '0.375rem',
53
+ fontSize: '12px',
54
+ padding: '1rem',
55
+ };
56
+
57
+ // Code block component with copy functionality - memoized
58
+ const CodeBlock = memo(({ language, children }: { language: string; children: string }) => {
19
59
  const [copied, setCopied] = useState(false);
20
60
 
21
- const handleCopy = async () => {
22
- await navigator.clipboard.writeText(children);
23
- setCopied(true);
24
- setTimeout(() => setCopied(false), 2000);
25
- };
61
+ const handleCopy = useCallback(async () => {
62
+ try {
63
+ await navigator.clipboard.writeText(children);
64
+ setCopied(true);
65
+ } catch {
66
+ // Clipboard API can fail in some contexts
67
+ }
68
+ }, [children]);
69
+
70
+ // Clear copied state after 2 seconds with proper cleanup
71
+ useEffect(() => {
72
+ if (!copied) return;
73
+ const timer = setTimeout(() => setCopied(false), 2000);
74
+ return () => clearTimeout(timer);
75
+ }, [copied]);
26
76
 
27
77
  return (
28
78
  <div className="relative group my-2">
@@ -33,21 +83,13 @@ const CodeBlock = ({ language, children }: { language: string; children: string
33
83
  >
34
84
  {copied ? <Check size={14} /> : <Copy size={14} />}
35
85
  </button>
36
- <SyntaxHighlighter
37
- language={language}
38
- style={oneDark}
39
- customStyle={{
40
- margin: 0,
41
- borderRadius: '0.375rem',
42
- fontSize: '12px',
43
- padding: '1rem',
44
- }}
45
- >
86
+ <SyntaxHighlighter language={language} style={oneDark} customStyle={CODE_BLOCK_STYLE}>
46
87
  {children}
47
88
  </SyntaxHighlighter>
48
89
  </div>
49
90
  );
50
- };
91
+ });
92
+ CodeBlock.displayName = 'CodeBlock';
51
93
 
52
94
  // Get time-based greeting
53
95
  const getGreeting = () => {
@@ -111,9 +153,12 @@ const suggestedQuestionsConfig: QuestionConfig[] = [
111
153
  pattern: /^\/docs\/services\/.+/,
112
154
  questions: [
113
155
  { label: 'Who owns this service?', prompt: 'Who owns this service and how do I contact them?' },
114
- { label: 'What does this depend on?', prompt: 'What are the upstream and downstream dependencies of this service?' },
115
- { label: 'How do I integrate with this?', prompt: 'How do I integrate with this service?' },
116
- { label: 'What messages does this publish?', prompt: 'What messages does this service produce?' },
156
+ {
157
+ label: 'What does this service depend on?',
158
+ prompt: 'What are the upstream and downstream dependencies of this service?',
159
+ },
160
+ { label: 'How do I integrate with this service?', prompt: 'How do I integrate with this service?' },
161
+ { label: 'What messages are published and consumed?', prompt: 'What messages does this service produce and consume?' },
117
162
  ],
118
163
  },
119
164
  // Domains page
@@ -229,37 +274,176 @@ const fadeInStyles = {
229
274
  inputFocus: {
230
275
  animation: 'focusIn 0.6s ease-out 1.4s both',
231
276
  },
277
+ // Follow-up suggestions animate with staggered fade-in-up
278
+ getFollowUpStyle: (index: number) => ({
279
+ animation: `fadeInUp 0.4s ease-out ${index * 0.1}s both`,
280
+ }),
281
+ };
282
+
283
+ // Preprocess markdown to fix common formatting issues
284
+ const preprocessMarkdown = (text: string): string => {
285
+ // Add newlines before headings if they're directly after text (no newline)
286
+ // This fixes cases like "some text.## Heading" → "some text.\n\n## Heading"
287
+ return text.replace(/([^\n])(#{1,6}\s)/g, '$1\n\n$2');
232
288
  };
233
289
 
234
290
  // Helper to extract text content from message parts
235
291
  const getMessageContent = (message: { parts?: Array<{ type: string; text?: string }> }): string => {
236
292
  if (!message.parts) return '';
237
- return message.parts
293
+ const rawContent = message.parts
238
294
  .filter((part): part is { type: 'text'; text: string } => part.type === 'text' && typeof part.text === 'string')
239
295
  .map((part) => part.text)
240
296
  .join('');
297
+ return preprocessMarkdown(rawContent);
241
298
  };
242
299
 
243
- // Skeleton loading component
244
- const SkeletonLoader = () => (
300
+ // Helper to extract follow-up suggestions from message parts
301
+ const getFollowUpSuggestions = (message: { parts?: Array<any> }): string[] => {
302
+ if (!message.parts) return [];
303
+
304
+ for (const part of message.parts) {
305
+ // AI SDK format: type is "tool-{toolName}" and result is in "output"
306
+ if (part.type === 'tool-suggestFollowUpQuestions' && part.state === 'output-available') {
307
+ const suggestions = part.output?.suggestions;
308
+ if (suggestions && Array.isArray(suggestions)) {
309
+ return suggestions;
310
+ }
311
+ }
312
+ }
313
+ return [];
314
+ };
315
+
316
+ // Helper to extract currently running tools from message parts
317
+ const getRunningTools = (message: { parts?: Array<any> }): string[] => {
318
+ if (!message.parts) return [];
319
+
320
+ const runningTools: string[] = [];
321
+ for (const part of message.parts) {
322
+ // Tool parts have type like "tool-{toolName}" and state indicates progress
323
+ if (part.type?.startsWith('tool-') && part.state !== 'output-available') {
324
+ // Extract tool name from type (e.g., "tool-getServiceHealth" -> "getServiceHealth")
325
+ const toolName = part.type.replace('tool-', '');
326
+ // Skip the follow-up suggestions tool as it's internal
327
+ if (toolName !== 'suggestFollowUpQuestions') {
328
+ runningTools.push(toolName);
329
+ }
330
+ }
331
+ }
332
+ return runningTools;
333
+ };
334
+
335
+ // Helper to extract completed tools from message parts (for showing after completion)
336
+ const getCompletedTools = (message: { parts?: Array<any> }): string[] => {
337
+ if (!message.parts) return [];
338
+
339
+ const completedTools: string[] = [];
340
+ for (const part of message.parts) {
341
+ // Tool parts have type like "tool-{toolName}" and state 'output-available' when done
342
+ if (part.type?.startsWith('tool-') && part.state === 'output-available') {
343
+ const toolName = part.type.replace('tool-', '');
344
+ // Skip internal tools
345
+ if (toolName !== 'suggestFollowUpQuestions') {
346
+ completedTools.push(toolName);
347
+ }
348
+ }
349
+ }
350
+ return completedTools;
351
+ };
352
+
353
+ // Skeleton loading component - memoized since it never changes
354
+ const SkeletonLoader = memo(() => (
245
355
  <div className="animate-pulse space-y-3">
246
- <div className="h-4 bg-gray-200 rounded w-[90%]" />
247
- <div className="h-4 bg-gray-200 rounded w-[75%]" />
248
- <div className="h-4 bg-gray-200 rounded w-[85%]" />
249
- <div className="h-4 bg-gray-200 rounded w-[60%]" />
356
+ <div className="h-4 bg-[rgb(var(--ec-content-hover))] rounded w-[90%]" />
357
+ <div className="h-4 bg-[rgb(var(--ec-content-hover))] rounded w-[75%]" />
358
+ <div className="h-4 bg-[rgb(var(--ec-content-hover))] rounded w-[85%]" />
359
+ <div className="h-4 bg-[rgb(var(--ec-content-hover))] rounded w-[60%]" />
250
360
  </div>
251
- );
361
+ ));
362
+ SkeletonLoader.displayName = 'SkeletonLoader';
363
+
364
+ // Memoized markdown components to prevent re-renders
365
+ const markdownComponents = {
366
+ a: ({ ...props }: any) => (
367
+ <a
368
+ {...props}
369
+ target="_blank"
370
+ rel="noopener noreferrer"
371
+ className="text-[rgb(var(--ec-accent))] hover:text-[rgb(var(--ec-accent-hover))] underline"
372
+ />
373
+ ),
374
+ code: ({ children, className, ...props }: any) => {
375
+ const isInline = !className;
376
+ const match = /language-(\w+)/.exec(className || '');
377
+ const language = match ? match[1] : 'text';
378
+ const codeString = String(children).replace(/\n$/, '');
379
+
380
+ return isInline ? (
381
+ <code
382
+ className="px-1 py-0.5 rounded text-xs font-mono bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-page-text))]"
383
+ {...props}
384
+ >
385
+ {children}
386
+ </code>
387
+ ) : (
388
+ <CodeBlock language={language}>{codeString}</CodeBlock>
389
+ );
390
+ },
391
+ table: ({ ...props }: any) => (
392
+ <div className="overflow-x-auto my-3">
393
+ <table className="min-w-full text-xs border-collapse border border-[rgb(var(--ec-page-border))]" {...props} />
394
+ </div>
395
+ ),
396
+ thead: ({ ...props }: any) => <thead className="bg-[rgb(var(--ec-content-hover))]" {...props} />,
397
+ th: ({ ...props }: any) => (
398
+ <th
399
+ className="px-3 py-2 text-left font-medium text-[rgb(var(--ec-page-text))] border border-[rgb(var(--ec-page-border))]"
400
+ {...props}
401
+ />
402
+ ),
403
+ td: ({ ...props }: any) => (
404
+ <td className="px-3 py-2 text-[rgb(var(--ec-page-text-muted))] border border-[rgb(var(--ec-page-border))]" {...props} />
405
+ ),
406
+ };
407
+
408
+ // Modal version with slightly different code styling
409
+ const modalMarkdownComponents = {
410
+ ...markdownComponents,
411
+ code: ({ children, className, ...props }: any) => {
412
+ const isInline = !className;
413
+ const match = /language-(\w+)/.exec(className || '');
414
+ const language = match ? match[1] : 'text';
415
+ const codeString = String(children).replace(/\n$/, '');
416
+
417
+ return isInline ? (
418
+ <code
419
+ className="px-1.5 py-0.5 rounded text-sm font-mono bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-page-text))]"
420
+ {...props}
421
+ >
422
+ {children}
423
+ </code>
424
+ ) : (
425
+ <CodeBlock language={language}>{codeString}</CodeBlock>
426
+ );
427
+ },
428
+ table: ({ ...props }: any) => (
429
+ <div className="overflow-x-auto my-3">
430
+ <table className="min-w-full text-sm border-collapse border border-[rgb(var(--ec-page-border))]" {...props} />
431
+ </div>
432
+ ),
433
+ };
252
434
 
253
435
  const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
254
436
  const inputRef = useRef<HTMLInputElement>(null);
255
437
  const modalInputRef = useRef<HTMLInputElement>(null);
256
438
  const messagesEndRef = useRef<HTMLDivElement>(null);
257
439
  const modalMessagesEndRef = useRef<HTMLDivElement>(null);
440
+ const messagesContainerRef = useRef<HTMLDivElement>(null);
258
441
  const [inputValue, setInputValue] = useState('');
259
442
  const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
260
443
  const [pathname, setPathname] = useState('');
261
444
  const [isFullscreen, setIsFullscreen] = useState(false);
262
445
  const [tools, setTools] = useState<ToolMetadata[]>([]);
446
+ const [showScrollButton, setShowScrollButton] = useState(false);
263
447
 
264
448
  // Sort tools with custom ones first
265
449
  const sortedTools = useMemo(() => {
@@ -291,19 +475,55 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
291
475
  setPathname(window.location.pathname + window.location.search);
292
476
  }, [isOpen]);
293
477
 
294
- const suggestedQuestions = getSuggestedQuestions(pathname);
478
+ // Memoize suggested questions to avoid recalculating on every render
479
+ const suggestedQuestions = useMemo(() => getSuggestedQuestions(pathname), [pathname]);
295
480
 
296
- const { messages, sendMessage, stop, status, setMessages, error } = useChat({
297
- sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
298
- });
481
+ // Memoize page context to avoid recalculating on every render
482
+ const pageContext = useMemo(() => {
483
+ const match = pathname.match(
484
+ /^\/(docs|visualiser|architecture)\/(events|services|commands|queries|flows|domains|channels|entities|containers)\/([^/]+)(?:\/([^/]+))?/
485
+ );
486
+ if (match) {
487
+ const [, , collection, id, version] = match;
488
+ const collectionNames: Record<string, string> = {
489
+ events: 'Event',
490
+ services: 'Service',
491
+ commands: 'Command',
492
+ queries: 'Query',
493
+ flows: 'Flow',
494
+ domains: 'Domain',
495
+ channels: 'Channel',
496
+ entities: 'Entity',
497
+ containers: 'Container',
498
+ };
499
+ return {
500
+ type: collectionNames[collection] || collection,
501
+ name: id,
502
+ version: version || 'latest',
503
+ };
504
+ }
505
+ return null;
506
+ }, [pathname]);
507
+
508
+ // Handle scroll to detect if user scrolled up
509
+ const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
510
+ const target = e.target as HTMLDivElement;
511
+ const isNearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 100;
512
+ setShowScrollButton(!isNearBottom);
513
+ }, []);
514
+
515
+ const { messages, sendMessage, stop, status, setMessages, error } = useChat();
299
516
 
300
517
  // Extract user-friendly error message
301
518
  const errorMessage = error?.message || 'Something went wrong. Please try again.';
302
519
 
520
+ // Memoize last assistant message to avoid array operations on every render
521
+ const lastAssistantMessage = useMemo(() => messages.findLast((m) => m.role === 'assistant'), [messages]);
522
+
303
523
  // Check if the assistant has started outputting content
304
- const lastAssistantMessage = [...messages].reverse().find((m) => m.role === 'assistant');
305
- const assistantHasContent = lastAssistantMessage?.parts?.some(
306
- (p) => p.type === 'text' && (p as { type: 'text'; text: string }).text.length > 0
524
+ const assistantHasContent = useMemo(
525
+ () => lastAssistantMessage?.parts?.some((p) => p.type === 'text' && (p as { type: 'text'; text: string }).text.length > 0),
526
+ [lastAssistantMessage]
307
527
  );
308
528
 
309
529
  // Clear waiting state once assistant starts outputting or on error
@@ -314,9 +534,13 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
314
534
  }, [assistantHasContent, status]);
315
535
 
316
536
  const isStreaming = status === 'streaming' && assistantHasContent;
317
- const isThinking = isWaitingForResponse || ((status === 'submitted' || status === 'streaming') && !assistantHasContent);
537
+ const isThinking =
538
+ isWaitingForResponse || (messages.length > 0 && (status === 'submitted' || status === 'streaming') && !assistantHasContent);
318
539
  const isLoading = isThinking || isStreaming;
319
540
 
541
+ // Get currently running tools from the last assistant message
542
+ const runningTools = useMemo(() => (lastAssistantMessage ? getRunningTools(lastAssistantMessage) : []), [lastAssistantMessage]);
543
+
320
544
  // Scroll to bottom when new messages arrive
321
545
  const scrollToBottom = useCallback(() => {
322
546
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -423,9 +647,9 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
423
647
  [submitMessage]
424
648
  );
425
649
 
426
- // Handle textarea enter key
427
- const handleKeyDown = useCallback(
428
- (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
650
+ // Handle input enter key
651
+ const handleInputKeyDown = useCallback(
652
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
429
653
  if (e.key === 'Enter' && !e.shiftKey) {
430
654
  e.preventDefault();
431
655
  submitMessage(inputValue);
@@ -445,59 +669,41 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
445
669
 
446
670
  const hasMessages = messages.length > 0;
447
671
 
672
+ // Memoize greeting - only changes when hour changes (effectively stable during session)
673
+ const greeting = useMemo(() => getGreeting(), []);
674
+
448
675
  return (
449
676
  <>
450
- {/* Keyframes for fade-in animation */}
451
- <style>{`
452
- @keyframes fadeIn {
453
- from {
454
- opacity: 0;
455
- }
456
- to {
457
- opacity: 1;
458
- }
459
- }
460
- @keyframes focusIn {
461
- from {
462
- box-shadow: 0 0 0 0 rgba(168, 85, 247, 0);
463
- border-color: #e5e7eb;
464
- }
465
- to {
466
- box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.15);
467
- border-color: #c084fc;
468
- }
469
- }
470
- @keyframes pulse-glow {
471
- 0%, 100% { box-shadow: 0 0 8px rgba(147, 51, 234, 0.3); }
472
- 50% { box-shadow: 0 0 16px rgba(147, 51, 234, 0.5); }
473
- }
474
- `}</style>
677
+ {/* Keyframes for fade-in animation - using constant to avoid re-injection */}
678
+ <style>{CHAT_PANEL_STYLES}</style>
475
679
 
476
680
  {/* Panel - hidden when fullscreen modal is open */}
477
681
  {!isFullscreen && (
478
682
  <div
479
- className="fixed top-0 right-0 h-[100vh] z-[200] bg-white border-l border-gray-200 flex flex-col overflow-hidden"
683
+ className="fixed top-0 right-0 h-[100vh] z-[200] border-l border-[rgb(var(--ec-page-border))] flex flex-col overflow-hidden"
480
684
  style={{
481
685
  width: `${PANEL_WIDTH}px`,
482
686
  transform: isOpen ? 'translateX(0)' : `translateX(${PANEL_WIDTH}px)`,
483
687
  transition: 'transform 800ms cubic-bezier(0.16, 1, 0.3, 1)',
688
+ background: `
689
+ radial-gradient(ellipse 100% 40% at 50% 100%, rgb(var(--ec-accent) / 0.15) 0%, transparent 100%),
690
+ rgb(var(--ec-page-bg))
691
+ `,
484
692
  }}
485
693
  >
486
694
  {/* Header */}
487
- <div className="flex-none border-b border-gray-100 shrink-0 pb-1">
695
+ <div className="flex-none shrink-0 pb-1">
488
696
  <div className="flex items-center justify-between px-4 py-3">
489
697
  <div className="flex items-center space-x-2">
490
- <div className="p-1.5 bg-purple-50 rounded-md">
491
- <BookOpen size={14} className="text-purple-600" />
492
- </div>
493
- <span className="font-medium text-gray-900 text-sm">EventCatalog Assistant</span>
698
+ <BookOpen size={16} className="text-[rgb(var(--ec-accent))]" />
699
+ <span className="font-medium text-[rgb(var(--ec-header-text))] text-sm">EventCatalog Assistant</span>
494
700
  </div>
495
701
  <div className="flex items-center space-x-1">
496
702
  {tools.length > 0 && (
497
703
  <Popover.Root>
498
704
  <Popover.Trigger asChild>
499
705
  <button
500
- className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
706
+ 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"
501
707
  aria-label="View available tools"
502
708
  title="Available tools"
503
709
  >
@@ -506,34 +712,36 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
506
712
  </Popover.Trigger>
507
713
  <Popover.Portal>
508
714
  <Popover.Content
509
- className="w-72 bg-white rounded-lg shadow-lg border border-gray-200 p-3 z-[250]"
715
+ className="w-72 bg-[rgb(var(--ec-dropdown-bg))] rounded-lg shadow-lg border border-[rgb(var(--ec-dropdown-border))] p-3 z-[250]"
510
716
  sideOffset={5}
511
717
  align="end"
512
718
  >
513
- <div className="text-[10px] font-medium text-gray-500 mb-2">Available Tools ({sortedTools.length})</div>
514
- <div className="max-h-64 overflow-y-auto divide-y divide-gray-100">
719
+ <div className="text-[10px] font-medium text-[rgb(var(--ec-page-text-muted))] mb-2">
720
+ Available Tools ({sortedTools.length})
721
+ </div>
722
+ <div className="max-h-64 overflow-y-auto divide-y divide-[rgb(var(--ec-page-border))]">
515
723
  {sortedTools.map((tool) => (
516
724
  <div key={tool.name} className="py-1.5 first:pt-0 last:pb-0">
517
725
  <div className="flex items-center gap-1.5">
518
- <span className="text-[11px] font-medium text-gray-700">{tool.name}</span>
726
+ <span className="text-[11px] font-medium text-[rgb(var(--ec-dropdown-text))]">{tool.name}</span>
519
727
  {tool.isCustom && (
520
- <span className="px-1.5 py-0.5 text-[9px] font-medium bg-purple-100 text-purple-700 rounded">
728
+ <span className="px-1.5 py-0.5 text-[9px] font-medium bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent-text))] rounded">
521
729
  Custom
522
730
  </span>
523
731
  )}
524
732
  </div>
525
- <div className="text-[10px] text-gray-500 mt-0.5">{tool.description}</div>
733
+ <div className="text-[10px] text-[rgb(var(--ec-page-text-muted))] mt-0.5">{tool.description}</div>
526
734
  </div>
527
735
  ))}
528
736
  </div>
529
- <Popover.Arrow className="fill-white" />
737
+ <Popover.Arrow className="fill-[rgb(var(--ec-dropdown-bg))]" />
530
738
  </Popover.Content>
531
739
  </Popover.Portal>
532
740
  </Popover.Root>
533
741
  )}
534
742
  <button
535
743
  onClick={() => setIsFullscreen(true)}
536
- className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
744
+ 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"
537
745
  aria-label="Expand to fullscreen"
538
746
  title="Expand"
539
747
  >
@@ -542,7 +750,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
542
750
  {hasMessages && (
543
751
  <button
544
752
  onClick={() => setMessages([])}
545
- className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
753
+ 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"
546
754
  aria-label="Clear chat"
547
755
  title="Clear chat"
548
756
  >
@@ -551,7 +759,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
551
759
  )}
552
760
  <button
553
761
  onClick={onClose}
554
- className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
762
+ 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"
555
763
  aria-label="Close chat panel"
556
764
  >
557
765
  <X size={18} />
@@ -561,8 +769,16 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
561
769
  {/* Thinking indicator */}
562
770
  {isThinking && (
563
771
  <div className="px-4 pb-2 flex items-center gap-2">
564
- <div className="w-1.5 h-1.5 bg-purple-500 rounded-full animate-pulse" />
565
- <span className="text-xs text-gray-500">Thinking...</span>
772
+ <div className="w-1.5 h-1.5 bg-[rgb(var(--ec-accent))] rounded-full animate-pulse" />
773
+ <span className="text-xs text-[rgb(var(--ec-icon-color))]">
774
+ {runningTools.length > 0 ? (
775
+ <>
776
+ Using <span className="font-medium text-[rgb(var(--ec-accent))]">{runningTools[0]}</span>...
777
+ </>
778
+ ) : (
779
+ 'Thinking...'
780
+ )}
781
+ </span>
566
782
  </div>
567
783
  )}
568
784
  </div>
@@ -570,40 +786,34 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
570
786
  {/* Content */}
571
787
  <div className="flex-1 flex flex-col min-h-0 relative overflow-hidden" key={isOpen ? 'content-open' : 'content-closed'}>
572
788
  {/* Messages or Welcome area */}
573
- <div className="flex-1 overflow-y-auto px-4 scrollbar-hide">
789
+ <div ref={messagesContainerRef} onScroll={handleScroll} className="flex-1 overflow-y-auto px-5 scrollbar-hide">
574
790
  {!hasMessages ? (
575
- /* Welcome area */
576
- <div className="flex flex-col h-full justify-between pt-4 pb-2">
577
- {/* Center content */}
791
+ /* Welcome area - Clean GitBook-inspired design */
792
+ <div className="flex flex-col h-full py-6">
793
+ {/* Greeting section - centered */}
578
794
  <div
579
- className="flex-1 flex flex-col items-center justify-center"
795
+ className="flex-1 flex flex-col items-center justify-center text-center"
580
796
  style={isOpen ? fadeInStyles.welcome : undefined}
581
797
  >
582
- {/* Animated Icon */}
583
- <div className="relative mb-5">
584
- <div className="absolute inset-0 bg-purple-400/20 rounded-2xl blur-xl animate-pulse" />
585
- <div className="relative w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center shadow-lg">
586
- <BookOpen size={26} className="text-white" strokeWidth={1.5} />
587
- <Sparkles size={10} className="text-purple-200 absolute -top-1 -right-1 animate-pulse" />
798
+ {/* Icon with circular background */}
799
+ <div className="relative mb-6">
800
+ <div className="w-32 h-32 rounded-full bg-[rgb(var(--ec-accent)/0.15)] flex items-center justify-center">
801
+ <MessageSquare size={56} className="text-[rgb(var(--ec-accent))]" strokeWidth={1.5} />
588
802
  </div>
589
803
  </div>
590
- <h2 className="text-base font-medium text-gray-900 mb-1">{getGreeting()}</h2>
591
- <p className="text-sm text-gray-500 text-center">Ask me anything about your catalog.</p>
804
+ <h2 className="text-lg font-semibold text-[rgb(var(--ec-accent))] mb-1">{greeting}</h2>
805
+ <p className="text-sm font-normal text-[rgb(var(--ec-content-text))]">
806
+ I'm here to help with your architecture
807
+ </p>
592
808
  </div>
593
809
 
594
- {/* Suggested questions */}
595
- <div className="space-y-1.5 mt-4">
596
- <p
597
- className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2"
598
- style={isOpen ? fadeInStyles.questionsLabel : undefined}
599
- >
600
- Example questions
601
- </p>
810
+ {/* Suggested questions - pill style */}
811
+ <div className="flex-none space-y-2">
602
812
  {suggestedQuestions.map((question, index) => (
603
813
  <button
604
814
  key={index}
605
815
  onClick={() => handleSuggestedAction(question.prompt)}
606
- className="w-full text-left px-3 py-2.5 text-sm text-gray-700 bg-gray-100 hover:bg-purple-50 hover:text-purple-700 border border-gray-200 hover:border-purple-200 rounded-lg transition-colors"
816
+ 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"
607
817
  style={isOpen ? fadeInStyles.getQuestionStyle(index) : undefined}
608
818
  >
609
819
  {question.label}
@@ -614,50 +824,58 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
614
824
  ) : (
615
825
  /* Messages area */
616
826
  <div className="py-4 space-y-4">
617
- {messages.map((message) => {
827
+ {messages.map((message, messageIndex) => {
618
828
  const content = getMessageContent(message);
829
+ const followUpSuggestions = message.role === 'assistant' ? getFollowUpSuggestions(message) : [];
830
+ const completedTools = message.role === 'assistant' ? getCompletedTools(message) : [];
831
+ const isLastMessage = messageIndex === messages.length - 1;
619
832
  return (
620
- <div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
833
+ <div key={message.id} className={`flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'}`}>
621
834
  {message.role === 'user' ? (
622
- <div className="max-w-[85%] rounded-2xl rounded-br-md px-4 py-2.5 bg-purple-600 text-white">
623
- <p className="text-sm whitespace-pre-wrap">{content}</p>
835
+ <div className="max-w-[85%] rounded-2xl rounded-br-md px-4 py-2.5 bg-[rgb(var(--ec-page-text)/0.05)]">
836
+ <p className="text-sm font-normal whitespace-pre-wrap text-[rgb(var(--ec-page-text))]">{content}</p>
624
837
  </div>
625
838
  ) : (
626
- <div className="w-full text-gray-700">
627
- <div className="prose prose-sm max-w-none prose-p:my-2 prose-p:font-normal prose-p:text-[13px] prose-headings:my-3 prose-headings:font-medium prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5 prose-li:text-[13px] prose-li:font-normal text-[13px] font-light">
628
- <ReactMarkdown
629
- components={{
630
- a: ({ ...props }) => (
631
- <a
632
- {...props}
633
- target="_blank"
634
- rel="noopener noreferrer"
635
- className="text-purple-600 hover:text-purple-800 underline"
636
- />
637
- ),
638
- code: ({ children, className, ...props }) => {
639
- const isInline = !className;
640
- const match = /language-(\w+)/.exec(className || '');
641
- const language = match ? match[1] : 'text';
642
- const codeString = String(children).replace(/\n$/, '');
643
-
644
- return isInline ? (
645
- <code
646
- className="px-1 py-0.5 rounded text-xs font-mono bg-gray-100 text-gray-800"
647
- {...props}
648
- >
649
- {children}
650
- </code>
651
- ) : (
652
- <CodeBlock language={language}>{codeString}</CodeBlock>
653
- );
654
- },
655
- }}
656
- >
657
- {content}
658
- </ReactMarkdown>
839
+ <>
840
+ {/* Tools used indicator */}
841
+ {completedTools.length > 0 && (
842
+ <div className="flex items-center gap-1.5 mb-2">
843
+ <Wrench size={10} className="text-[rgb(var(--ec-icon-color))]" />
844
+ <span className="text-[10px] text-[rgb(var(--ec-icon-color))]">
845
+ Used{' '}
846
+ {completedTools.slice(0, 2).map((tool, i) => (
847
+ <span key={tool}>
848
+ <span className="font-medium text-[rgb(var(--ec-accent))]">{tool}</span>
849
+ {i < Math.min(completedTools.length, 2) - 1 && ', '}
850
+ </span>
851
+ ))}
852
+ {completedTools.length > 2 && <span> +{completedTools.length - 2} more</span>}
853
+ </span>
854
+ </div>
855
+ )}
856
+ <div className="w-full text-[rgb(var(--ec-content-text))]">
857
+ <div className="prose prose-sm max-w-none prose-p:my-2 prose-p:font-normal prose-p:text-[13px] prose-p:text-[rgb(var(--ec-content-text))] prose-headings:my-2 prose-headings:font-semibold prose-headings:text-[rgb(var(--ec-page-text))] prose-h1:text-base prose-h2:text-sm prose-h3:text-[13px] prose-h4:text-[13px] prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5 prose-li:text-[13px] prose-li:font-normal prose-li:text-[rgb(var(--ec-content-text))] text-[13px] font-light">
858
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
859
+ {content}
860
+ </ReactMarkdown>
861
+ </div>
659
862
  </div>
660
- </div>
863
+ {/* Follow-up suggestions - only show for last assistant message when not loading */}
864
+ {isLastMessage && followUpSuggestions.length > 0 && !isLoading && (
865
+ <div className="flex flex-wrap gap-2 mt-3 w-full">
866
+ {followUpSuggestions.map((suggestion, index) => (
867
+ <button
868
+ key={index}
869
+ onClick={() => handleSuggestedAction(suggestion)}
870
+ 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"
871
+ style={fadeInStyles.getFollowUpStyle(index)}
872
+ >
873
+ {suggestion}
874
+ </button>
875
+ ))}
876
+ </div>
877
+ )}
878
+ </>
661
879
  )}
662
880
  </div>
663
881
  );
@@ -687,40 +905,46 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
687
905
  )}
688
906
  </div>
689
907
 
908
+ {/* Scroll to bottom button */}
909
+ {hasMessages && showScrollButton && (
910
+ <button
911
+ onClick={scrollToBottom}
912
+ className="absolute bottom-24 right-4 flex items-center gap-1.5 px-3 py-1.5 bg-[rgb(var(--ec-card-bg))] text-[rgb(var(--ec-page-text-muted))] text-xs font-medium rounded-full shadow-lg border border-[rgb(var(--ec-page-border))] hover:bg-[rgb(var(--ec-content-hover))] transition-all z-10"
913
+ >
914
+ <ChevronDown size={14} />
915
+ <span>Scroll to bottom</span>
916
+ </button>
917
+ )}
918
+
690
919
  {/* Input area (Fixed at bottom) */}
691
- <div className="flex-none px-4 py-3 border-t border-gray-100" key={isOpen ? 'input-open' : 'input-closed'}>
920
+ <div className="flex-none px-4 py-3" key={isOpen ? 'input-open' : 'input-closed'}>
692
921
  <form onSubmit={handleSubmit}>
693
- <div className="relative bg-gray-50 rounded-lg border-2 border-gray-200 focus-within:border-purple-300 focus-within:bg-white transition-all">
922
+ <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">
694
923
  <input
695
924
  ref={inputRef}
696
925
  type="text"
697
926
  value={inputValue}
698
927
  onChange={(e) => setInputValue(e.target.value)}
699
- onKeyDown={(e) => {
700
- if (e.key === 'Enter' && !e.shiftKey) {
701
- e.preventDefault();
702
- submitMessage(inputValue);
703
- }
704
- }}
705
- placeholder="Ask a question..."
928
+ onKeyDown={handleInputKeyDown}
929
+ placeholder="Ask, search, or explain..."
706
930
  disabled={isLoading}
707
- className="w-full px-3 py-2.5 pr-14 bg-transparent text-gray-900 placeholder-gray-400 focus:outline-none text-sm disabled:opacity-50 rounded-lg"
931
+ 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-none text-sm disabled:opacity-50 rounded-xl"
708
932
  />
709
- <div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-10">
933
+ <div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
710
934
  {isStreaming ? (
711
935
  <button
712
936
  type="button"
713
937
  onClick={() => stop()}
714
- className="p-1.5 text-red-500 hover:bg-red-50 rounded-md transition-colors"
938
+ className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
715
939
  aria-label="Stop generating"
716
940
  >
717
- <Square size={12} fill="currentColor" />
941
+ <Square size={14} fill="currentColor" />
718
942
  </button>
719
943
  ) : (
720
944
  <button
721
945
  type="submit"
722
946
  disabled={!inputValue.trim() || isLoading}
723
- className="px-2.5 py-1 bg-purple-600 text-white text-xs font-medium rounded-md hover:bg-purple-700 disabled:bg-gray-200 disabled:text-gray-400 transition-colors"
947
+ className="px-3 py-1.5 bg-[rgb(var(--ec-accent))] text-white text-xs font-medium rounded-lg hover:bg-[rgb(var(--ec-accent-hover))] disabled:bg-transparent disabled:text-[rgb(var(--ec-icon-color))] transition-colors"
724
948
  aria-label="Send message"
725
949
  >
726
950
  Send
@@ -729,7 +953,19 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
729
953
  </div>
730
954
  </div>
731
955
  </form>
732
- <p className="text-[9px] text-gray-400 mt-2 text-center">AI can make mistakes. Verify important info.</p>
956
+ {/* Context indicator */}
957
+ <div className="flex items-center justify-center gap-1.5 mt-2">
958
+ {pageContext ? (
959
+ <span className="text-[10px] text-[rgb(var(--ec-icon-color))] flex items-center gap-1">
960
+ <span className="w-1.5 h-1.5 rounded-full bg-[rgb(var(--ec-accent))]" />
961
+ Based on {pageContext.type}: {pageContext.name}
962
+ </span>
963
+ ) : (
964
+ <span className="text-[10px] text-[rgb(var(--ec-icon-color))]">
965
+ AI can make mistakes. Verify important info.
966
+ </span>
967
+ )}
968
+ </div>
733
969
  </div>
734
970
  </div>
735
971
  </div>
@@ -747,22 +983,30 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
747
983
  }}
748
984
  >
749
985
  <Dialog.Portal>
750
- <Dialog.Overlay className="fixed inset-0 bg-gray-900/50 backdrop-blur-sm z-[300]" />
751
- <Dialog.Content className="fixed inset-y-4 left-1/2 -translate-x-1/2 w-[95%] max-w-4xl md:inset-y-8 rounded-xl bg-white shadow-2xl z-[301] flex flex-col overflow-hidden focus:outline-none border border-gray-200">
986
+ <Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[300]" />
987
+ <Dialog.Content
988
+ 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-none border border-[rgb(var(--ec-page-border))]"
989
+ style={{
990
+ background: `
991
+ radial-gradient(ellipse 100% 40% at 50% 100%, rgb(var(--ec-accent) / 0.15) 0%, transparent 100%),
992
+ rgb(var(--ec-page-bg))
993
+ `,
994
+ }}
995
+ >
752
996
  {/* Modal Header */}
753
- <div className="flex items-center justify-between px-5 py-3 border-b border-gray-100 flex-shrink-0">
997
+ <div className="flex items-center justify-between px-5 py-3 border-b border-[rgb(var(--ec-page-border))] flex-shrink-0">
754
998
  <div className="flex items-center space-x-2.5">
755
- <div className="p-1.5 bg-purple-50 rounded-lg">
756
- <BookOpen size={18} className="text-purple-600" />
999
+ <div className="p-1.5 bg-[rgb(var(--ec-accent-subtle))] rounded-lg">
1000
+ <BookOpen size={18} className="text-[rgb(var(--ec-accent))]" />
757
1001
  </div>
758
- <Dialog.Title className="text-base font-medium text-gray-900">Ask AI</Dialog.Title>
1002
+ <Dialog.Title className="text-base font-medium text-[rgb(var(--ec-page-text))]">Ask AI</Dialog.Title>
759
1003
  </div>
760
1004
  <div className="flex items-center space-x-2">
761
1005
  {tools.length > 0 && (
762
1006
  <Popover.Root>
763
1007
  <Popover.Trigger asChild>
764
1008
  <button
765
- className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
1009
+ 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"
766
1010
  aria-label="View available tools"
767
1011
  title="Available tools"
768
1012
  >
@@ -771,28 +1015,33 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
771
1015
  </Popover.Trigger>
772
1016
  <Popover.Portal>
773
1017
  <Popover.Content
774
- className="w-80 bg-white rounded-lg shadow-lg border border-gray-200 p-4 z-[350]"
1018
+ className="w-80 bg-[rgb(var(--ec-dropdown-bg))] rounded-lg shadow-lg border border-[rgb(var(--ec-dropdown-border))] p-4 z-[350]"
775
1019
  sideOffset={5}
776
1020
  align="end"
777
1021
  style={{ maxHeight: 'calc(100vh - 200px)' }}
778
1022
  >
779
- <div className="text-[11px] font-medium text-gray-500 mb-2">Available Tools ({sortedTools.length})</div>
780
- <div className="overflow-y-auto divide-y divide-gray-100" style={{ maxHeight: 'calc(100vh - 280px)' }}>
1023
+ <div className="text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] mb-2">
1024
+ Available Tools ({sortedTools.length})
1025
+ </div>
1026
+ <div
1027
+ className="overflow-y-auto divide-y divide-[rgb(var(--ec-page-border))]"
1028
+ style={{ maxHeight: 'calc(100vh - 280px)' }}
1029
+ >
781
1030
  {sortedTools.map((tool) => (
782
1031
  <div key={tool.name} className="py-2 first:pt-0 last:pb-0">
783
1032
  <div className="flex items-center gap-1.5">
784
- <span className="text-xs font-medium text-gray-700">{tool.name}</span>
1033
+ <span className="text-xs font-medium text-[rgb(var(--ec-dropdown-text))]">{tool.name}</span>
785
1034
  {tool.isCustom && (
786
- <span className="px-1.5 py-0.5 text-[9px] font-medium bg-purple-100 text-purple-700 rounded">
1035
+ <span className="px-1.5 py-0.5 text-[9px] font-medium bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent-text))] rounded">
787
1036
  Custom
788
1037
  </span>
789
1038
  )}
790
1039
  </div>
791
- <div className="text-[11px] text-gray-500 mt-0.5">{tool.description}</div>
1040
+ <div className="text-[11px] text-[rgb(var(--ec-page-text-muted))] mt-0.5">{tool.description}</div>
792
1041
  </div>
793
1042
  ))}
794
1043
  </div>
795
- <Popover.Arrow className="fill-white" />
1044
+ <Popover.Arrow className="fill-[rgb(var(--ec-dropdown-bg))]" />
796
1045
  </Popover.Content>
797
1046
  </Popover.Portal>
798
1047
  </Popover.Root>
@@ -800,7 +1049,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
800
1049
  {hasMessages && (
801
1050
  <button
802
1051
  onClick={() => setMessages([])}
803
- className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
1052
+ 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"
804
1053
  aria-label="Clear chat"
805
1054
  title="Clear chat"
806
1055
  >
@@ -809,7 +1058,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
809
1058
  )}
810
1059
  <button
811
1060
  onClick={() => setIsFullscreen(false)}
812
- className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
1061
+ 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"
813
1062
  aria-label="Exit fullscreen"
814
1063
  title="Exit fullscreen"
815
1064
  >
@@ -820,7 +1069,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
820
1069
  setIsFullscreen(false);
821
1070
  onClose();
822
1071
  }}
823
- className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
1072
+ 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"
824
1073
  aria-label="Close"
825
1074
  >
826
1075
  <X size={18} />
@@ -830,35 +1079,46 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
830
1079
 
831
1080
  {/* Thinking indicator */}
832
1081
  {isThinking && (
833
- <div className="px-5 py-2 flex items-center gap-2 border-b border-gray-100">
834
- <div className="w-1.5 h-1.5 bg-purple-500 rounded-full animate-pulse" />
835
- <span className="text-sm text-gray-500">Thinking...</span>
1082
+ <div className="px-5 py-2 flex items-center gap-2 border-b border-[rgb(var(--ec-page-border))]">
1083
+ <div className="w-1.5 h-1.5 bg-[rgb(var(--ec-accent))] rounded-full animate-pulse" />
1084
+ <span className="text-sm text-[rgb(var(--ec-page-text-muted))]">
1085
+ {runningTools.length > 0 ? (
1086
+ <>
1087
+ Using <span className="font-medium text-[rgb(var(--ec-accent))]">{runningTools[0]}</span>...
1088
+ </>
1089
+ ) : (
1090
+ 'Thinking...'
1091
+ )}
1092
+ </span>
836
1093
  </div>
837
1094
  )}
838
1095
 
839
1096
  {/* Modal Content */}
840
- <div className="flex-1 overflow-y-auto px-5 py-4">
1097
+ <div className="flex-1 overflow-y-auto px-6 py-6">
841
1098
  {!hasMessages ? (
842
- /* Welcome area */
843
- <div className="flex flex-col h-full justify-center items-center">
844
- {/* Animated Icon */}
845
- <div className="relative mb-6">
846
- <div className="absolute inset-0 bg-purple-400/20 rounded-2xl blur-xl animate-pulse" />
847
- <div className="relative w-16 h-16 rounded-2xl bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center shadow-lg">
848
- <BookOpen size={30} className="text-white" strokeWidth={1.5} />
849
- <Sparkles size={12} className="text-purple-200 absolute -top-1 -right-1 animate-pulse" />
1099
+ /* Welcome area - Clean design */
1100
+ <div className="flex flex-col h-full max-w-2xl mx-auto">
1101
+ {/* Greeting section - centered */}
1102
+ <div className="flex-1 flex flex-col justify-center items-center text-center">
1103
+ {/* Icon with circular background */}
1104
+ <div className="relative mb-8">
1105
+ <div className="w-40 h-40 rounded-full bg-[rgb(var(--ec-accent)/0.15)] flex items-center justify-center">
1106
+ <MessageSquare size={72} className="text-[rgb(var(--ec-accent))]" strokeWidth={1.5} />
1107
+ </div>
850
1108
  </div>
1109
+ <h2 className="text-2xl font-semibold text-[rgb(var(--ec-accent))] mb-2">{greeting}</h2>
1110
+ <p className="font-normal text-[rgb(var(--ec-content-text))] text-center">
1111
+ I'm here to help with your architecture
1112
+ </p>
851
1113
  </div>
852
- <h2 className="text-xl font-medium text-gray-900 mb-1">{getGreeting()}</h2>
853
- <p className="text-gray-500 text-center mb-8">Ask me anything about your catalog.</p>
854
1114
 
855
- {/* Suggested questions */}
856
- <div className="grid grid-cols-2 gap-2 max-w-lg">
1115
+ {/* Suggested questions - pill style */}
1116
+ <div className="flex-none grid grid-cols-2 gap-2">
857
1117
  {suggestedQuestions.map((question, index) => (
858
1118
  <button
859
1119
  key={index}
860
1120
  onClick={() => handleSuggestedAction(question.prompt)}
861
- className="px-4 py-2.5 text-sm text-gray-600 bg-gray-50 hover:bg-gray-100 rounded-lg transition-colors text-left"
1121
+ 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"
862
1122
  >
863
1123
  {question.label}
864
1124
  </button>
@@ -868,50 +1128,58 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
868
1128
  ) : (
869
1129
  /* Messages area */
870
1130
  <div className="max-w-3xl mx-auto space-y-4">
871
- {messages.map((message) => {
1131
+ {messages.map((message, messageIndex) => {
872
1132
  const content = getMessageContent(message);
1133
+ const followUpSuggestions = message.role === 'assistant' ? getFollowUpSuggestions(message) : [];
1134
+ const completedTools = message.role === 'assistant' ? getCompletedTools(message) : [];
1135
+ const isLastMessage = messageIndex === messages.length - 1;
873
1136
  return (
874
- <div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
1137
+ <div key={message.id} className={`flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'}`}>
875
1138
  {message.role === 'user' ? (
876
- <div className="max-w-[75%] rounded-2xl rounded-br-md px-4 py-2.5 bg-purple-600 text-white">
877
- <p className="text-sm whitespace-pre-wrap">{content}</p>
1139
+ <div className="max-w-[75%] rounded-2xl rounded-br-md px-4 py-2.5 bg-[rgb(var(--ec-page-text)/0.05)]">
1140
+ <p className="text-sm font-normal whitespace-pre-wrap text-[rgb(var(--ec-page-text))]">{content}</p>
878
1141
  </div>
879
1142
  ) : (
880
- <div className="w-full text-gray-700">
881
- <div className="prose prose-sm max-w-none">
882
- <ReactMarkdown
883
- components={{
884
- a: ({ ...props }) => (
885
- <a
886
- {...props}
887
- target="_blank"
888
- rel="noopener noreferrer"
889
- className="text-purple-600 hover:text-purple-800 underline"
890
- />
891
- ),
892
- code: ({ children, className, ...props }) => {
893
- const isInline = !className;
894
- const match = /language-(\w+)/.exec(className || '');
895
- const language = match ? match[1] : 'text';
896
- const codeString = String(children).replace(/\n$/, '');
897
-
898
- return isInline ? (
899
- <code
900
- className="px-1.5 py-0.5 rounded text-sm font-mono bg-gray-100 text-gray-800"
901
- {...props}
902
- >
903
- {children}
904
- </code>
905
- ) : (
906
- <CodeBlock language={language}>{codeString}</CodeBlock>
907
- );
908
- },
909
- }}
910
- >
911
- {content}
912
- </ReactMarkdown>
1143
+ <>
1144
+ {/* Tools used indicator */}
1145
+ {completedTools.length > 0 && (
1146
+ <div className="flex items-center gap-1.5 mb-2">
1147
+ <Wrench size={12} className="text-[rgb(var(--ec-icon-color))]" />
1148
+ <span className="text-[11px] text-[rgb(var(--ec-icon-color))]">
1149
+ Used{' '}
1150
+ {completedTools.slice(0, 3).map((tool, i) => (
1151
+ <span key={tool}>
1152
+ <span className="font-medium text-[rgb(var(--ec-accent))]">{tool}</span>
1153
+ {i < Math.min(completedTools.length, 3) - 1 && ', '}
1154
+ </span>
1155
+ ))}
1156
+ {completedTools.length > 3 && <span> +{completedTools.length - 3} more</span>}
1157
+ </span>
1158
+ </div>
1159
+ )}
1160
+ <div className="w-full text-[rgb(var(--ec-content-text))]">
1161
+ <div className="prose prose-sm max-w-none prose-p:text-[rgb(var(--ec-content-text))] prose-headings:my-2 prose-headings:font-semibold prose-headings:text-[rgb(var(--ec-page-text))] prose-h1:text-lg prose-h2:text-base prose-h3:text-sm prose-h4:text-sm prose-li:text-[rgb(var(--ec-content-text))]">
1162
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={modalMarkdownComponents}>
1163
+ {content}
1164
+ </ReactMarkdown>
1165
+ </div>
913
1166
  </div>
914
- </div>
1167
+ {/* Follow-up suggestions - only show for last assistant message when not loading */}
1168
+ {isLastMessage && followUpSuggestions.length > 0 && !isLoading && (
1169
+ <div className="flex flex-wrap gap-2 mt-3 w-full">
1170
+ {followUpSuggestions.map((suggestion, index) => (
1171
+ <button
1172
+ key={index}
1173
+ onClick={() => handleSuggestedAction(suggestion)}
1174
+ 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"
1175
+ style={fadeInStyles.getFollowUpStyle(index)}
1176
+ >
1177
+ {suggestion}
1178
+ </button>
1179
+ ))}
1180
+ </div>
1181
+ )}
1182
+ </>
915
1183
  )}
916
1184
  </div>
917
1185
  );
@@ -941,30 +1209,25 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
941
1209
  </div>
942
1210
 
943
1211
  {/* Modal Input area */}
944
- <div className="flex-shrink-0 px-5 py-4 border-t border-gray-100">
1212
+ <div className="flex-shrink-0 px-6 py-4">
945
1213
  <form onSubmit={handleSubmit} className="max-w-3xl mx-auto">
946
- <div className="relative bg-gray-50 rounded-lg border border-gray-200 focus-within:border-purple-300 focus-within:bg-white transition-all">
1214
+ <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">
947
1215
  <input
948
1216
  ref={modalInputRef}
949
1217
  type="text"
950
1218
  value={inputValue}
951
1219
  onChange={(e) => setInputValue(e.target.value)}
952
- onKeyDown={(e) => {
953
- if (e.key === 'Enter' && !e.shiftKey) {
954
- e.preventDefault();
955
- submitMessage(inputValue);
956
- }
957
- }}
958
- placeholder="Ask a question..."
1220
+ onKeyDown={handleInputKeyDown}
1221
+ placeholder="Ask, search, or explain..."
959
1222
  disabled={isLoading}
960
- className="w-full px-4 py-3 pr-16 bg-transparent text-gray-900 placeholder-gray-400 focus:outline-none text-sm disabled:opacity-50 rounded-lg"
1223
+ 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-none text-sm disabled:opacity-50 rounded-xl"
961
1224
  />
962
1225
  <div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
963
1226
  {isStreaming ? (
964
1227
  <button
965
1228
  type="button"
966
1229
  onClick={() => stop()}
967
- className="p-1.5 text-red-500 hover:bg-red-50 rounded-md transition-colors"
1230
+ className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
968
1231
  aria-label="Stop generating"
969
1232
  >
970
1233
  <Square size={14} fill="currentColor" />
@@ -973,7 +1236,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
973
1236
  <button
974
1237
  type="submit"
975
1238
  disabled={!inputValue.trim() || isLoading}
976
- className="px-3 py-1.5 bg-purple-600 text-white text-sm font-medium rounded-md hover:bg-purple-700 disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed transition-colors"
1239
+ className="px-4 py-2 bg-[rgb(var(--ec-accent))] text-white text-sm font-medium rounded-lg hover:bg-[rgb(var(--ec-accent-hover))] disabled:bg-transparent disabled:text-[rgb(var(--ec-icon-color))] disabled:cursor-not-allowed transition-colors"
977
1240
  aria-label="Send message"
978
1241
  >
979
1242
  Send
@@ -982,7 +1245,17 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
982
1245
  </div>
983
1246
  </div>
984
1247
  </form>
985
- <p className="text-xs text-gray-400 mt-2 text-center">AI can make mistakes. Verify important info.</p>
1248
+ {/* Context indicator */}
1249
+ <div className="flex items-center justify-center gap-1.5 mt-3 max-w-3xl mx-auto">
1250
+ {pageContext ? (
1251
+ <span className="text-xs text-[rgb(var(--ec-icon-color))] flex items-center gap-1.5">
1252
+ <span className="w-1.5 h-1.5 rounded-full bg-[rgb(var(--ec-accent))]" />
1253
+ Based on {pageContext.type}: {pageContext.name}
1254
+ </span>
1255
+ ) : (
1256
+ <span className="text-xs text-[rgb(var(--ec-icon-color))]">AI can make mistakes. Verify important info.</span>
1257
+ )}
1258
+ </div>
986
1259
  </div>
987
1260
  </Dialog.Content>
988
1261
  </Dialog.Portal>