@eventcatalog/core 3.0.0-beta.2 → 3.0.0-beta.21

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 (103) hide show
  1. package/README.md +10 -0
  2. package/dist/__mocks__/astro-content.cjs +32 -0
  3. package/dist/__mocks__/astro-content.d.cts +13 -0
  4. package/dist/__mocks__/astro-content.d.ts +13 -0
  5. package/dist/__mocks__/astro-content.js +7 -0
  6. package/dist/analytics/analytics.cjs +1 -1
  7. package/dist/analytics/analytics.js +2 -2
  8. package/dist/analytics/log-build.cjs +1 -1
  9. package/dist/analytics/log-build.js +3 -3
  10. package/dist/{chunk-JSONCD7V.js → chunk-2FUEBPD3.js} +1 -1
  11. package/dist/{chunk-3W6JYTHP.js → chunk-HABY2LVH.js} +6 -2
  12. package/dist/{chunk-H4QHE5YZ.js → chunk-KQAMO3R4.js} +1 -1
  13. package/dist/chunk-Q6KRYWPV.js +44 -0
  14. package/dist/{chunk-PQL6O5YA.js → chunk-RRP2B7BL.js} +1 -1
  15. package/dist/constants.cjs +1 -1
  16. package/dist/constants.js +1 -1
  17. package/dist/eventcatalog.cjs +84 -65
  18. package/dist/eventcatalog.config.d.cts +4 -0
  19. package/dist/eventcatalog.config.d.ts +4 -0
  20. package/dist/eventcatalog.js +45 -57
  21. package/dist/generate.cjs +48 -2
  22. package/dist/generate.js +3 -1
  23. package/dist/utils/cli-logger.cjs +82 -0
  24. package/dist/utils/cli-logger.d.cts +10 -0
  25. package/dist/utils/cli-logger.d.ts +10 -0
  26. package/dist/utils/cli-logger.js +7 -0
  27. package/eventcatalog/astro.config.mjs +4 -1
  28. package/eventcatalog/integrations/ecstudio-watcher.mjs +1 -1
  29. package/eventcatalog/integrations/eventcatalog-features.ts +69 -0
  30. package/eventcatalog/public/icons/asyncapi-black.svg +2 -0
  31. package/eventcatalog/public/icons/graphql-black.svg +1 -0
  32. package/eventcatalog/public/icons/openapi-black.svg +1 -0
  33. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +821 -0
  34. package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +24 -0
  35. package/eventcatalog/src/components/Grids/DomainGrid.tsx +1 -3
  36. package/eventcatalog/src/components/Grids/MessageGrid.tsx +8 -8
  37. package/eventcatalog/src/components/Header.astro +25 -5
  38. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +14 -3
  39. package/eventcatalog/src/components/Search/Search.astro +2 -2
  40. package/eventcatalog/src/components/Search/SearchModal.tsx +16 -7
  41. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +9 -2
  42. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/domain.ts +7 -6
  43. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/service.ts +6 -3
  44. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/shared.ts +1 -0
  45. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +23 -8
  46. package/eventcatalog/src/components/SideNav/NestedSideBar/sidebar-builder.ts +57 -11
  47. package/eventcatalog/src/content.config.ts +1 -10
  48. package/eventcatalog/src/enterprise/ai/chat-api.ts +262 -0
  49. package/eventcatalog/src/enterprise/auth/[...auth].ts +3 -0
  50. package/eventcatalog/src/enterprise/auth/login.astro +420 -0
  51. package/eventcatalog/src/enterprise/collections/index.ts +0 -1
  52. package/eventcatalog/src/layouts/Footer.astro +8 -5
  53. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +30 -19
  54. package/eventcatalog/src/pages/_index.astro +8 -9
  55. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +19 -3
  56. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +7 -7
  57. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +1 -1
  58. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +5 -5
  59. package/eventcatalog/src/pages/docs/teams/[id].mdx.ts +36 -0
  60. package/eventcatalog/src/pages/docs/users/[id].mdx.ts +36 -0
  61. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +178 -0
  62. package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -155
  63. package/eventcatalog/src/remark-plugins/directives.ts +30 -9
  64. package/eventcatalog/src/utils/collections/schemas.ts +31 -7
  65. package/eventcatalog/src/utils/feature.ts +8 -4
  66. package/eventcatalog/src/utils/resource-files.ts +86 -0
  67. package/package.json +12 -15
  68. package/default-files-for-collections/changelogs.md +0 -5
  69. package/default-files-for-collections/channels.md +0 -8
  70. package/default-files-for-collections/commands.md +0 -8
  71. package/default-files-for-collections/domains.md +0 -8
  72. package/default-files-for-collections/events.md +0 -8
  73. package/default-files-for-collections/flows.md +0 -11
  74. package/default-files-for-collections/queries.md +0 -8
  75. package/default-files-for-collections/services.md +0 -8
  76. package/default-files-for-collections/ubiquitousLanguages.md +0 -7
  77. package/eventcatalog/src/enterprise/collections/chat-prompts.ts +0 -32
  78. package/eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx +0 -60
  79. package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx +0 -414
  80. package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatSidebar.tsx +0 -169
  81. package/eventcatalog/src/enterprise/eventcatalog-chat/components/InputModal.tsx +0 -244
  82. package/eventcatalog/src/enterprise/eventcatalog-chat/components/MentionInput.tsx +0 -211
  83. package/eventcatalog/src/enterprise/eventcatalog-chat/components/WelcomePromptArea.tsx +0 -176
  84. package/eventcatalog/src/enterprise/eventcatalog-chat/components/default-prompts.ts +0 -93
  85. package/eventcatalog/src/enterprise/eventcatalog-chat/components/hooks/ChatProvider.tsx +0 -143
  86. package/eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.server.tsx +0 -387
  87. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/chat.ts +0 -59
  88. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +0 -104
  89. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/ai-provider.ts +0 -140
  90. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/anthropic.ts +0 -28
  91. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/google.ts +0 -41
  92. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/index.ts +0 -26
  93. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/openai.ts +0 -61
  94. package/eventcatalog/src/enterprise/eventcatalog-chat/utils/chat-prompts.ts +0 -50
  95. package/eventcatalog/src/pages/auth/login.astro +0 -280
  96. package/eventcatalog/src/pages/chat/feature.astro +0 -179
  97. package/eventcatalog/src/pages/chat/index.astro +0 -10
  98. package/eventcatalog/src/pages/nav-index.json.ts +0 -30
  99. /package/eventcatalog/src/{pages → enterprise}/auth/error.astro +0 -0
  100. /package/eventcatalog/src/{middleware-auth.ts → enterprise/auth/middleware/middleware-auth.ts} +0 -0
  101. /package/eventcatalog/src/{middleware.ts → enterprise/auth/middleware/middleware.ts} +0 -0
  102. /package/eventcatalog/src/{pages/unauthorized/index.astro → enterprise/auth/unauthorized.astro} +0 -0
  103. /package/eventcatalog/src/{pages → enterprise}/plans/index.astro +0 -0
@@ -0,0 +1,821 @@
1
+ import { useEffect, useRef, useCallback, useState } from 'react';
2
+ import { X, Sparkles, Square, Trash2, BookOpen, Copy, Check, Maximize2, Minimize2 } from 'lucide-react';
3
+ import { useChat } from '@ai-sdk/react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
6
+ import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
7
+ import { lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
8
+ import * as Dialog from '@radix-ui/react-dialog';
9
+
10
+ // Code block component with copy functionality
11
+ const CodeBlock = ({ language, children }: { language: string; children: string }) => {
12
+ const [copied, setCopied] = useState(false);
13
+
14
+ const handleCopy = async () => {
15
+ await navigator.clipboard.writeText(children);
16
+ setCopied(true);
17
+ setTimeout(() => setCopied(false), 2000);
18
+ };
19
+
20
+ return (
21
+ <div className="relative group my-2">
22
+ <button
23
+ onClick={handleCopy}
24
+ className="absolute right-2 top-2 p-1.5 rounded bg-gray-700 hover:bg-gray-600 text-gray-300 opacity-0 group-hover:opacity-100 transition-opacity z-10"
25
+ aria-label="Copy code"
26
+ >
27
+ {copied ? <Check size={14} /> : <Copy size={14} />}
28
+ </button>
29
+ <SyntaxHighlighter
30
+ language={language}
31
+ style={oneDark}
32
+ customStyle={{
33
+ margin: 0,
34
+ borderRadius: '0.375rem',
35
+ fontSize: '12px',
36
+ padding: '1rem',
37
+ }}
38
+ >
39
+ {children}
40
+ </SyntaxHighlighter>
41
+ </div>
42
+ );
43
+ };
44
+
45
+ // Get time-based greeting
46
+ const getGreeting = () => {
47
+ const hour = new Date().getHours();
48
+ if (hour < 12) return 'Good morning';
49
+ if (hour < 18) return 'Good afternoon';
50
+ return 'Good evening';
51
+ };
52
+
53
+ // ============================================
54
+ // SUGGESTED QUESTIONS CONFIGURATION
55
+ // ============================================
56
+ // Each config has a pattern (regex) to match the URL path
57
+ // and an array of questions to show. Questions are checked
58
+ // in order - first matching pattern wins.
59
+ // ============================================
60
+
61
+ interface SuggestedQuestion {
62
+ label: string;
63
+ prompt: string;
64
+ }
65
+
66
+ interface QuestionConfig {
67
+ pattern: RegExp;
68
+ questions: SuggestedQuestion[];
69
+ }
70
+
71
+ const suggestedQuestionsConfig: QuestionConfig[] = [
72
+ // Message pages (events, commands, queries) - most specific first
73
+ {
74
+ pattern: /^\/docs\/(events|commands|queries)\/.+/,
75
+ questions: [
76
+ { label: 'Which services publish this?', prompt: 'Who produces this message?' },
77
+ { label: 'Which services subscribe to this?', prompt: 'Who consumes this message?' },
78
+ { label: 'View the message schema', prompt: 'Show me the schema for this message' },
79
+ { label: 'What breaks if this changes?', prompt: 'What services would be affected if this message changes?' },
80
+ ],
81
+ },
82
+ // AsyncAPI specification page
83
+ {
84
+ pattern: /^\/docs\/services\/.+\/asyncapi\/.+/,
85
+ questions: [
86
+ { label: 'Summarize this API', prompt: 'Help me understand this AsyncAPI specification' },
87
+ { label: 'Show all channels', prompt: 'What channels are defined in this AsyncAPI spec?' },
88
+ { label: 'How do I authenticate?', prompt: 'What authentication is required for this service?' },
89
+ { label: 'What message formats are used?', prompt: 'What are the message formats and schemas?' },
90
+ ],
91
+ },
92
+ // OpenAPI specification page
93
+ {
94
+ pattern: /^\/docs\/services\/.+\/spec\/.+/,
95
+ questions: [
96
+ { label: 'Summarize this API', prompt: 'Help me understand this OpenAPI specification' },
97
+ { label: 'Show all endpoints', prompt: 'What endpoints are available in this API?' },
98
+ { label: 'How do I authenticate?', prompt: 'What authentication is required for this API?' },
99
+ { label: 'What are the request & response formats?', prompt: 'What are the request and response formats?' },
100
+ ],
101
+ },
102
+ // Services page
103
+ {
104
+ pattern: /^\/docs\/services\/.+/,
105
+ questions: [
106
+ { label: 'Who owns this service?', prompt: 'Who owns this service and how do I contact them?' },
107
+ { label: 'What does this depend on?', prompt: 'What are the upstream and downstream dependencies of this service?' },
108
+ { label: 'How do I integrate with this?', prompt: 'How do I integrate with this service?' },
109
+ { label: 'What messages does this publish?', prompt: 'What messages does this service produce?' },
110
+ ],
111
+ },
112
+ // Domains page
113
+ {
114
+ pattern: /^\/docs\/domains\/.+/,
115
+ questions: [
116
+ { label: 'What services are in this domain?', prompt: 'What services belong to this domain?' },
117
+ { label: 'What business capability is this?', prompt: 'What business capability does this domain represent?' },
118
+ { label: 'What events come from this domain?', prompt: 'What events are published by this domain?' },
119
+ { label: 'Who owns this domain?', prompt: 'Who owns this domain and how do I contact them?' },
120
+ ],
121
+ },
122
+ // Any other docs page
123
+ {
124
+ pattern: /^\/docs\/.+/,
125
+ questions: [
126
+ { label: 'Tell me about this', prompt: 'Tell me more about this page' },
127
+ { label: 'Who is responsible for this?', prompt: 'Who owns this resource?' },
128
+ { label: 'What else is related to this?', prompt: 'What other resources are related to this?' },
129
+ ],
130
+ },
131
+ // Default questions (fallback)
132
+ {
133
+ pattern: /.*/,
134
+ questions: [
135
+ { label: 'What domains do we have?', prompt: 'What domains are in my catalog?' },
136
+ { label: 'Show me all services', prompt: 'What services do I have?' },
137
+ { label: 'What changed recently?', prompt: 'What are the most recent changes in the catalog?' },
138
+ { label: 'How does data flow between services?', prompt: 'Show me how data flows between services' },
139
+ ],
140
+ },
141
+ ];
142
+
143
+ // Get suggested questions based on current URL path
144
+ const getSuggestedQuestions = (pathname: string): SuggestedQuestion[] => {
145
+ for (const config of suggestedQuestionsConfig) {
146
+ if (config.pattern.test(pathname)) {
147
+ return config.questions;
148
+ }
149
+ }
150
+ // Fallback to last config (default)
151
+ return suggestedQuestionsConfig[suggestedQuestionsConfig.length - 1].questions;
152
+ };
153
+
154
+ interface ChatPanelProps {
155
+ isOpen: boolean;
156
+ onClose: () => void;
157
+ }
158
+
159
+ const PANEL_WIDTH = 400;
160
+
161
+ // Staggered fade-in animation styles
162
+ const fadeInStyles = {
163
+ header: {
164
+ animation: 'fadeInDown 0.3s ease-out 0.1s both',
165
+ },
166
+ content: {
167
+ animation: 'fadeInDown 0.3s ease-out 0.2s both',
168
+ },
169
+ input: {
170
+ animation: 'fadeInDown 0.3s ease-out 0.3s both',
171
+ },
172
+ };
173
+
174
+ // Helper to extract text content from message parts
175
+ const getMessageContent = (message: { parts?: Array<{ type: string; text?: string }> }): string => {
176
+ if (!message.parts) return '';
177
+ return message.parts
178
+ .filter((part): part is { type: 'text'; text: string } => part.type === 'text' && typeof part.text === 'string')
179
+ .map((part) => part.text)
180
+ .join('');
181
+ };
182
+
183
+ // Skeleton loading component
184
+ const SkeletonLoader = () => (
185
+ <div className="animate-pulse space-y-3">
186
+ <div className="h-4 bg-gray-200 rounded w-[90%]" />
187
+ <div className="h-4 bg-gray-200 rounded w-[75%]" />
188
+ <div className="h-4 bg-gray-200 rounded w-[85%]" />
189
+ <div className="h-4 bg-gray-200 rounded w-[60%]" />
190
+ </div>
191
+ );
192
+
193
+ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
194
+ const inputRef = useRef<HTMLInputElement>(null);
195
+ const modalInputRef = useRef<HTMLInputElement>(null);
196
+ const messagesEndRef = useRef<HTMLDivElement>(null);
197
+ const modalMessagesEndRef = useRef<HTMLDivElement>(null);
198
+ const [inputValue, setInputValue] = useState('');
199
+ const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
200
+ const [pathname, setPathname] = useState('');
201
+ const [isFullscreen, setIsFullscreen] = useState(false);
202
+
203
+ // Get current pathname on mount and when panel opens
204
+ useEffect(() => {
205
+ setPathname(window.location.pathname);
206
+ }, [isOpen]);
207
+
208
+ const suggestedQuestions = getSuggestedQuestions(pathname);
209
+
210
+ const { messages, sendMessage, stop, status, setMessages } = useChat({
211
+ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
212
+ });
213
+
214
+ // Check if the assistant has started outputting content
215
+ const lastAssistantMessage = [...messages].reverse().find((m) => m.role === 'assistant');
216
+ const assistantHasContent = lastAssistantMessage?.parts?.some(
217
+ (p) => p.type === 'text' && (p as { type: 'text'; text: string }).text.length > 0
218
+ );
219
+
220
+ // Clear waiting state once assistant starts outputting or on error
221
+ useEffect(() => {
222
+ if (assistantHasContent || status === 'error') {
223
+ setIsWaitingForResponse(false);
224
+ }
225
+ }, [assistantHasContent, status]);
226
+
227
+ const isStreaming = status === 'streaming' && assistantHasContent;
228
+ const isThinking = isWaitingForResponse || ((status === 'submitted' || status === 'streaming') && !assistantHasContent);
229
+ const isLoading = isThinking || isStreaming;
230
+
231
+ // Scroll to bottom when new messages arrive
232
+ const scrollToBottom = useCallback(() => {
233
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
234
+ }, []);
235
+
236
+ useEffect(() => {
237
+ scrollToBottom();
238
+ // Also scroll modal messages
239
+ modalMessagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
240
+ }, [messages, scrollToBottom]);
241
+
242
+ // Focus modal input when fullscreen opens
243
+ useEffect(() => {
244
+ if (isFullscreen && !isLoading) {
245
+ setTimeout(() => {
246
+ modalInputRef.current?.focus();
247
+ }, 100);
248
+ }
249
+ }, [isFullscreen, isLoading]);
250
+
251
+ // Handle escape key to close
252
+ useEffect(() => {
253
+ const handleEscape = (e: KeyboardEvent) => {
254
+ if (e.key === 'Escape' && isOpen) {
255
+ onClose();
256
+ }
257
+ // Focus input on CMD+I or CTRL+I
258
+ if ((e.metaKey || e.ctrlKey) && e.key === 'i' && isOpen) {
259
+ e.preventDefault();
260
+ inputRef.current?.focus();
261
+ }
262
+ };
263
+ document.addEventListener('keydown', handleEscape);
264
+ return () => document.removeEventListener('keydown', handleEscape);
265
+ }, [isOpen, onClose]);
266
+
267
+ // Focus input when opened and not loading
268
+ useEffect(() => {
269
+ if (isOpen && !isLoading) {
270
+ setTimeout(() => {
271
+ inputRef.current?.focus();
272
+ }, 100);
273
+ }
274
+ }, [isOpen, isLoading]);
275
+
276
+ // Add/remove padding to main content when sidebar panel is open
277
+ useEffect(() => {
278
+ const contentEl = document.getElementById('content');
279
+ if (!contentEl) return;
280
+
281
+ // Add transition if not already present
282
+ if (!contentEl.style.transition) {
283
+ contentEl.style.transition = 'padding-right 300ms cubic-bezier(0.16, 1, 0.3, 1)';
284
+ }
285
+
286
+ // Only add padding when panel is open AND not in fullscreen mode
287
+ if (isOpen && !isFullscreen) {
288
+ contentEl.style.paddingRight = '23rem';
289
+ } else {
290
+ contentEl.style.paddingRight = '0';
291
+ }
292
+
293
+ // Cleanup on unmount
294
+ return () => {
295
+ contentEl.style.paddingRight = '0';
296
+ };
297
+ }, [isOpen, isFullscreen]);
298
+
299
+ // Submit message handler
300
+ const submitMessage = useCallback(
301
+ (text: string) => {
302
+ if (!text.trim() || isLoading) return;
303
+ setInputValue('');
304
+ setIsWaitingForResponse(true);
305
+ sendMessage({ text });
306
+ },
307
+ [isLoading, sendMessage]
308
+ );
309
+
310
+ // Handle suggested action clicks
311
+ const handleSuggestedAction = useCallback(
312
+ (prompt: string) => {
313
+ submitMessage(prompt);
314
+ },
315
+ [submitMessage]
316
+ );
317
+
318
+ // Handle textarea enter key
319
+ const handleKeyDown = useCallback(
320
+ (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
321
+ if (e.key === 'Enter' && !e.shiftKey) {
322
+ e.preventDefault();
323
+ submitMessage(inputValue);
324
+ }
325
+ },
326
+ [inputValue, submitMessage]
327
+ );
328
+
329
+ // Handle form submit
330
+ const handleSubmit = useCallback(
331
+ (e: React.FormEvent) => {
332
+ e.preventDefault();
333
+ submitMessage(inputValue);
334
+ },
335
+ [inputValue, submitMessage]
336
+ );
337
+
338
+ const hasMessages = messages.length > 0;
339
+
340
+ return (
341
+ <>
342
+ {/* Keyframes for fade-in animation */}
343
+ <style>{`
344
+ @keyframes fadeInDown {
345
+ from {
346
+ opacity: 0;
347
+ transform: translateY(-8px);
348
+ }
349
+ to {
350
+ opacity: 1;
351
+ transform: translateY(0);
352
+ }
353
+ }
354
+ @keyframes pulse-glow {
355
+ 0%, 100% { box-shadow: 0 0 8px rgba(147, 51, 234, 0.3); }
356
+ 50% { box-shadow: 0 0 16px rgba(147, 51, 234, 0.5); }
357
+ }
358
+ `}</style>
359
+
360
+ {/* Panel - hidden when fullscreen modal is open */}
361
+ {!isFullscreen && (
362
+ <div
363
+ className="fixed top-0 right-0 h-[100vh] z-[200] bg-gradient-to-b from-white via-white to-gray-50/80 border-l border-gray-200/80 flex flex-col overflow-hidden"
364
+ style={{
365
+ width: `${PANEL_WIDTH}px`,
366
+ transform: isOpen ? 'translateX(0)' : `translateX(${PANEL_WIDTH}px)`,
367
+ transition: 'transform 300ms cubic-bezier(0.16, 1, 0.3, 1)',
368
+ boxShadow: '-8px 0 24px -4px rgba(0, 0, 0, 0.1)',
369
+ }}
370
+ >
371
+ {/* Purple accent line at top */}
372
+ <div className="h-1 bg-gradient-to-r from-purple-500 via-purple-600 to-indigo-600" />
373
+
374
+ {/* Header */}
375
+ <div
376
+ className="flex-none bg-gradient-to-b from-purple-50/50 to-transparent shrink-0"
377
+ style={isOpen ? fadeInStyles.header : undefined}
378
+ key={isOpen ? 'header-open' : 'header-closed'}
379
+ >
380
+ <div className="flex items-center justify-between px-5 py-3">
381
+ <div className="flex items-center space-x-2.5">
382
+ <div className="p-1.5 bg-purple-100 rounded-lg relative">
383
+ <BookOpen size={16} className="text-purple-600" />
384
+ <Sparkles size={8} className="text-purple-400 absolute -top-0.5 -right-0.5" />
385
+ </div>
386
+ <span className="font-semibold text-gray-900 text-[15px]">EventCatalog Assistant</span>
387
+ </div>
388
+ <div className="flex items-center space-x-1">
389
+ <button
390
+ onClick={() => setIsFullscreen(true)}
391
+ className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
392
+ aria-label="Expand to fullscreen"
393
+ title="Expand"
394
+ >
395
+ <Maximize2 size={16} />
396
+ </button>
397
+ {hasMessages && (
398
+ <button
399
+ onClick={() => setMessages([])}
400
+ className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
401
+ aria-label="Clear chat"
402
+ title="Clear chat"
403
+ >
404
+ <Trash2 size={18} />
405
+ </button>
406
+ )}
407
+ <button
408
+ onClick={onClose}
409
+ className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
410
+ aria-label="Close chat panel"
411
+ >
412
+ <X size={18} />
413
+ </button>
414
+ </div>
415
+ </div>
416
+ {/* Thinking indicator */}
417
+ {isThinking && (
418
+ <div className="px-5 pb-2 flex items-center gap-2">
419
+ <div className="w-2 h-2 bg-purple-500 rounded-full animate-pulse" />
420
+ <span className="text-xs text-gray-500">Thinking...</span>
421
+ </div>
422
+ )}
423
+ </div>
424
+
425
+ {/* Content */}
426
+ <div
427
+ className="flex-1 flex flex-col min-h-0 relative overflow-hidden"
428
+ style={isOpen ? fadeInStyles.content : undefined}
429
+ key={isOpen ? 'content-open' : 'content-closed'}
430
+ >
431
+ {/* Messages or Welcome area */}
432
+ <div className="flex-1 overflow-y-auto px-6 scrollbar-hide">
433
+ {!hasMessages ? (
434
+ /* Welcome area */
435
+ <div className="flex flex-col h-full justify-between pt-6 pb-2">
436
+ {/* Center content */}
437
+ <div className="flex-1 flex flex-col items-center justify-center">
438
+ {/* Animated Icon */}
439
+ <div className="relative mb-6">
440
+ <div className="absolute inset-0 bg-purple-400/20 rounded-2xl blur-xl animate-pulse" />
441
+ <div className="relative w-16 h-16 rounded-2xl bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
442
+ <BookOpen size={28} className="text-white" strokeWidth={1.5} />
443
+ <Sparkles size={12} className="text-purple-200 absolute -top-1 -right-1 animate-pulse" />
444
+ </div>
445
+ </div>
446
+
447
+ {/* Greeting with gradient */}
448
+ <h2 className="text-xl font-semibold bg-gradient-to-r from-purple-600 to-indigo-600 bg-clip-text text-transparent mb-1">
449
+ {getGreeting()}
450
+ </h2>
451
+ <p className="text-sm text-gray-500 text-center">I'm here to help you explore your architecture.</p>
452
+ </div>
453
+
454
+ {/* Suggested questions */}
455
+ <div className="flex flex-wrap gap-2 mt-6">
456
+ {suggestedQuestions.map((question, index) => (
457
+ <button
458
+ key={index}
459
+ onClick={() => handleSuggestedAction(question.prompt)}
460
+ className="px-3 py-1.5 text-xs text-gray-600 bg-white border border-gray-200 hover:border-purple-300 hover:bg-purple-50 hover:text-purple-700 rounded-full transition-all shadow-sm"
461
+ >
462
+ {question.label}
463
+ </button>
464
+ ))}
465
+ </div>
466
+ </div>
467
+ ) : (
468
+ /* Messages area */
469
+ <div className="py-4 space-y-4">
470
+ {messages.map((message) => {
471
+ const content = getMessageContent(message);
472
+ return (
473
+ <div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
474
+ {message.role === 'user' ? (
475
+ <div className="max-w-[85%] rounded-2xl rounded-br-md px-4 py-2.5 bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-sm">
476
+ <p className="text-sm font-light whitespace-pre-wrap">{content}</p>
477
+ </div>
478
+ ) : (
479
+ <div className="w-full text-gray-700">
480
+ <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">
481
+ <ReactMarkdown
482
+ components={{
483
+ a: ({ ...props }) => (
484
+ <a
485
+ {...props}
486
+ target="_blank"
487
+ rel="noopener noreferrer"
488
+ className="text-purple-600 hover:text-purple-800 underline"
489
+ />
490
+ ),
491
+ code: ({ children, className, ...props }) => {
492
+ const isInline = !className;
493
+ const match = /language-(\w+)/.exec(className || '');
494
+ const language = match ? match[1] : 'text';
495
+ const codeString = String(children).replace(/\n$/, '');
496
+
497
+ return isInline ? (
498
+ <code
499
+ className="px-1 py-0.5 rounded text-xs font-mono bg-gray-100 text-gray-800"
500
+ {...props}
501
+ >
502
+ {children}
503
+ </code>
504
+ ) : (
505
+ <CodeBlock language={language}>{codeString}</CodeBlock>
506
+ );
507
+ },
508
+ }}
509
+ >
510
+ {content}
511
+ </ReactMarkdown>
512
+ </div>
513
+ </div>
514
+ )}
515
+ </div>
516
+ );
517
+ })}
518
+
519
+ {/* Skeleton loading indicator */}
520
+ {isThinking && (
521
+ <div className="w-full">
522
+ <SkeletonLoader />
523
+ </div>
524
+ )}
525
+
526
+ {/* Error message as chat bubble */}
527
+ {status === 'error' && (
528
+ <div className="flex justify-start">
529
+ <div className="w-full">
530
+ <div className="flex items-start gap-2 text-red-600 text-sm">
531
+ <span className="shrink-0">⚠️</span>
532
+ <span>Something went wrong. Please try again.</span>
533
+ </div>
534
+ </div>
535
+ </div>
536
+ )}
537
+
538
+ <div ref={messagesEndRef} />
539
+ </div>
540
+ )}
541
+ </div>
542
+
543
+ {/* Input area (Fixed at bottom) */}
544
+ <div
545
+ className="flex-none px-4 py-3 pb-2 bg-gradient-to-t from-gray-50 to-transparent border-t border-gray-100"
546
+ style={isOpen ? fadeInStyles.input : undefined}
547
+ key={isOpen ? 'input-open' : 'input-closed'}
548
+ >
549
+ <form onSubmit={handleSubmit}>
550
+ <div className="relative bg-white rounded-xl border border-gray-200 focus-within:border-purple-300 focus-within:ring-2 focus-within:ring-purple-100 transition-all shadow-sm">
551
+ <input
552
+ ref={inputRef}
553
+ type="text"
554
+ value={inputValue}
555
+ onChange={(e) => setInputValue(e.target.value)}
556
+ onKeyDown={(e) => {
557
+ if (e.key === 'Enter' && !e.shiftKey) {
558
+ e.preventDefault();
559
+ submitMessage(inputValue);
560
+ }
561
+ }}
562
+ placeholder="Ask anything about your architecture..."
563
+ disabled={isLoading}
564
+ className="w-full px-3 py-2.5 pr-16 bg-transparent text-gray-900 placeholder-gray-400 focus:outline-none text-sm disabled:opacity-50 rounded-xl"
565
+ />
566
+ <div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-10">
567
+ {isStreaming ? (
568
+ <button
569
+ type="button"
570
+ onClick={() => stop()}
571
+ className="p-1.5 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
572
+ aria-label="Stop generating"
573
+ >
574
+ <Square size={12} fill="currentColor" />
575
+ </button>
576
+ ) : (
577
+ <button
578
+ type="submit"
579
+ disabled={!inputValue.trim() || isLoading}
580
+ className="px-3 py-1 bg-purple-600 text-white text-xs font-medium rounded-lg hover:bg-purple-700 disabled:bg-gray-200 disabled:text-gray-400 transition-colors"
581
+ aria-label="Send message"
582
+ >
583
+ Send
584
+ </button>
585
+ )}
586
+ </div>
587
+ </div>
588
+ </form>
589
+
590
+ <p className="text-[9px] text-gray-400 mt-2 text-center">AI can make mistakes. Verify important info.</p>
591
+ </div>
592
+ </div>
593
+ </div>
594
+ )}
595
+
596
+ {/* Fullscreen Modal */}
597
+ <Dialog.Root
598
+ open={isFullscreen}
599
+ onOpenChange={(open) => {
600
+ setIsFullscreen(open);
601
+ // If modal is being closed (clicking outside, etc.), close the chat entirely
602
+ if (!open) {
603
+ onClose();
604
+ }
605
+ }}
606
+ >
607
+ <Dialog.Portal>
608
+ <Dialog.Overlay className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[300]" />
609
+ <Dialog.Content className="fixed inset-y-4 left-1/2 -translate-x-1/2 w-[95%] max-w-5xl md:inset-y-8 rounded-2xl bg-white shadow-2xl z-[301] flex flex-col overflow-hidden focus:outline-none border border-gray-200">
610
+ {/* Purple accent line at top */}
611
+ <div className="h-1 bg-gradient-to-r from-purple-500 via-purple-600 to-indigo-600" />
612
+
613
+ {/* Modal Header */}
614
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-100 bg-gradient-to-b from-purple-50/50 to-transparent flex-shrink-0">
615
+ <div className="flex items-center space-x-3">
616
+ <div className="p-2 bg-purple-100 rounded-xl relative">
617
+ <BookOpen size={20} className="text-purple-600" />
618
+ <Sparkles size={10} className="text-purple-400 absolute -top-0.5 -right-0.5" />
619
+ </div>
620
+ <Dialog.Title className="text-lg font-semibold text-gray-900">EventCatalog Assistant</Dialog.Title>
621
+ </div>
622
+ <div className="flex items-center space-x-2">
623
+ {hasMessages && (
624
+ <button
625
+ onClick={() => setMessages([])}
626
+ className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
627
+ aria-label="Clear chat"
628
+ title="Clear chat"
629
+ >
630
+ <Trash2 size={18} />
631
+ </button>
632
+ )}
633
+ <button
634
+ onClick={() => setIsFullscreen(false)}
635
+ className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
636
+ aria-label="Exit fullscreen"
637
+ title="Exit fullscreen"
638
+ >
639
+ <Minimize2 size={18} />
640
+ </button>
641
+ <button
642
+ onClick={() => {
643
+ setIsFullscreen(false);
644
+ onClose();
645
+ }}
646
+ className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
647
+ aria-label="Close"
648
+ >
649
+ <X size={18} />
650
+ </button>
651
+ </div>
652
+ </div>
653
+
654
+ {/* Thinking indicator */}
655
+ {isThinking && (
656
+ <div className="px-6 py-2 flex items-center gap-2 border-b border-gray-100">
657
+ <div className="w-2 h-2 bg-purple-500 rounded-full animate-pulse" />
658
+ <span className="text-sm text-gray-500">Thinking...</span>
659
+ </div>
660
+ )}
661
+
662
+ {/* Modal Content */}
663
+ <div className="flex-1 overflow-y-auto px-6 py-4">
664
+ {!hasMessages ? (
665
+ /* Welcome area */
666
+ <div className="flex flex-col h-full justify-center items-center">
667
+ {/* Animated Icon */}
668
+ <div className="relative mb-8">
669
+ <div className="absolute inset-0 bg-purple-400/20 rounded-2xl blur-xl animate-pulse" />
670
+ <div className="relative w-20 h-20 rounded-2xl bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
671
+ <BookOpen size={36} className="text-white" strokeWidth={1.5} />
672
+ <Sparkles size={14} className="text-purple-200 absolute -top-1 -right-1 animate-pulse" />
673
+ </div>
674
+ </div>
675
+
676
+ {/* Greeting with gradient */}
677
+ <h2 className="text-2xl font-semibold bg-gradient-to-r from-purple-600 to-indigo-600 bg-clip-text text-transparent mb-2">
678
+ {getGreeting()}
679
+ </h2>
680
+ <p className="text-gray-500 text-center mb-10">I'm here to help you explore your architecture.</p>
681
+
682
+ {/* Suggested questions */}
683
+ <div className="flex flex-wrap gap-2 justify-center max-w-lg">
684
+ {suggestedQuestions.map((question, index) => (
685
+ <button
686
+ key={index}
687
+ onClick={() => handleSuggestedAction(question.prompt)}
688
+ className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 hover:border-purple-300 hover:bg-purple-50 hover:text-purple-700 rounded-full transition-all shadow-sm"
689
+ >
690
+ {question.label}
691
+ </button>
692
+ ))}
693
+ </div>
694
+ </div>
695
+ ) : (
696
+ /* Messages area */
697
+ <div className="max-w-3xl mx-auto space-y-4">
698
+ {messages.map((message) => {
699
+ const content = getMessageContent(message);
700
+ return (
701
+ <div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
702
+ {message.role === 'user' ? (
703
+ <div className="max-w-[75%] rounded-2xl rounded-br-md px-5 py-3 bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-sm">
704
+ <p className="text-sm font-light whitespace-pre-wrap">{content}</p>
705
+ </div>
706
+ ) : (
707
+ <div className="w-full text-gray-700">
708
+ <div className="prose prose-sm max-w-none">
709
+ <ReactMarkdown
710
+ components={{
711
+ a: ({ ...props }) => (
712
+ <a
713
+ {...props}
714
+ target="_blank"
715
+ rel="noopener noreferrer"
716
+ className="text-purple-600 hover:text-purple-800 underline"
717
+ />
718
+ ),
719
+ code: ({ children, className, ...props }) => {
720
+ const isInline = !className;
721
+ const match = /language-(\w+)/.exec(className || '');
722
+ const language = match ? match[1] : 'text';
723
+ const codeString = String(children).replace(/\n$/, '');
724
+
725
+ return isInline ? (
726
+ <code
727
+ className="px-1.5 py-0.5 rounded text-sm font-mono bg-gray-100 text-gray-800"
728
+ {...props}
729
+ >
730
+ {children}
731
+ </code>
732
+ ) : (
733
+ <CodeBlock language={language}>{codeString}</CodeBlock>
734
+ );
735
+ },
736
+ }}
737
+ >
738
+ {content}
739
+ </ReactMarkdown>
740
+ </div>
741
+ </div>
742
+ )}
743
+ </div>
744
+ );
745
+ })}
746
+
747
+ {isThinking && (
748
+ <div className="w-full max-w-md">
749
+ <SkeletonLoader />
750
+ </div>
751
+ )}
752
+
753
+ {/* Error message as chat bubble */}
754
+ {status === 'error' && (
755
+ <div className="flex justify-start">
756
+ <div className="w-full">
757
+ <div className="flex items-start gap-2 text-red-600 text-sm">
758
+ <span className="shrink-0">⚠️</span>
759
+ <span>Something went wrong. Please try again.</span>
760
+ </div>
761
+ </div>
762
+ </div>
763
+ )}
764
+
765
+ <div ref={modalMessagesEndRef} />
766
+ </div>
767
+ )}
768
+ </div>
769
+
770
+ {/* Modal Input area */}
771
+ <div className="flex-shrink-0 px-6 py-4 border-t border-gray-100 bg-gradient-to-t from-gray-50 to-transparent">
772
+ <form onSubmit={handleSubmit} className="max-w-3xl mx-auto">
773
+ <div className="relative bg-white rounded-xl border border-gray-200 focus-within:border-purple-300 focus-within:ring-2 focus-within:ring-purple-100 transition-all shadow-sm">
774
+ <input
775
+ ref={modalInputRef}
776
+ type="text"
777
+ value={inputValue}
778
+ onChange={(e) => setInputValue(e.target.value)}
779
+ onKeyDown={(e) => {
780
+ if (e.key === 'Enter' && !e.shiftKey) {
781
+ e.preventDefault();
782
+ submitMessage(inputValue);
783
+ }
784
+ }}
785
+ placeholder="Ask anything about your architecture..."
786
+ disabled={isLoading}
787
+ className="w-full px-4 py-3 pr-20 bg-transparent text-gray-900 placeholder-gray-400 focus:outline-none text-sm disabled:opacity-50 rounded-xl"
788
+ />
789
+ <div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
790
+ {isStreaming ? (
791
+ <button
792
+ type="button"
793
+ onClick={() => stop()}
794
+ className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
795
+ aria-label="Stop generating"
796
+ >
797
+ <Square size={16} fill="currentColor" />
798
+ </button>
799
+ ) : (
800
+ <button
801
+ type="submit"
802
+ disabled={!inputValue.trim() || isLoading}
803
+ className="px-4 py-1.5 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700 disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed transition-colors"
804
+ aria-label="Send message"
805
+ >
806
+ Send
807
+ </button>
808
+ )}
809
+ </div>
810
+ </div>
811
+ </form>
812
+ <p className="text-xs text-gray-400 mt-2 text-center">AI can make mistakes. Verify important info.</p>
813
+ </div>
814
+ </Dialog.Content>
815
+ </Dialog.Portal>
816
+ </Dialog.Root>
817
+ </>
818
+ );
819
+ };
820
+
821
+ export default ChatPanel;