@eventcatalog/core 3.0.0-beta.23 → 3.0.0-beta.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-F6UWJOIP.js → chunk-3SWCGDD7.js} +1 -1
  6. package/dist/{chunk-C3DPW2RT.js → chunk-DFHXF3VF.js} +1 -1
  7. package/dist/{chunk-U5LM336G.js → chunk-NAW6EPCS.js} +1 -1
  8. package/dist/{chunk-AJIDWPJI.js → chunk-OJA6CNVO.js} +1 -1
  9. package/dist/{chunk-XPNI6MQE.js → chunk-RA6AYXGJ.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +289 -116
  19. package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +3 -3
  20. package/eventcatalog/src/components/Search/SearchDataLoader.astro +25 -0
  21. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +1 -1
  22. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +1 -1
  23. package/eventcatalog/src/components/SideNav/SideNav.astro +0 -15
  24. package/eventcatalog/src/enterprise/ai/chat-api.ts +242 -144
  25. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +126 -121
  26. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +5 -2
  27. package/eventcatalog/src/pages/schemas/explorer/index.astro +2 -2
  28. package/eventcatalog/src/stores/{sidebar-store.ts → sidebar-store/index.ts} +1 -1
  29. package/eventcatalog/src/utils/collections/schemas.ts +14 -0
  30. package/package.json +2 -1
  31. /package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/container.ts +0 -0
  32. /package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/domain.ts +0 -0
  33. /package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/flow.ts +0 -0
  34. /package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/message.ts +0 -0
  35. /package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/service.ts +0 -0
  36. /package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/shared.ts +0 -0
  37. /package/eventcatalog/src/{components/SideNav/NestedSideBar/sidebar-builder.ts → stores/sidebar-store/state.ts} +0 -0
@@ -1,11 +1,18 @@
1
- import { useEffect, useRef, useCallback, useState } from 'react';
2
- import { X, Sparkles, Square, Trash2, BookOpen, Copy, Check, Maximize2, Minimize2 } from 'lucide-react';
1
+ import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
2
+ import { X, Sparkles, Square, Trash2, BookOpen, Copy, Check, Maximize2, Minimize2, Wrench } from 'lucide-react';
3
3
  import { useChat } from '@ai-sdk/react';
4
4
  import ReactMarkdown from 'react-markdown';
5
5
  import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
6
6
  import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
7
7
  import { lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
8
8
  import * as Dialog from '@radix-ui/react-dialog';
9
+ import * as Popover from '@radix-ui/react-popover';
10
+
11
+ interface ToolMetadata {
12
+ name: string;
13
+ description: string;
14
+ isCustom?: boolean;
15
+ }
9
16
 
10
17
  // Code block component with copy functionality
11
18
  const CodeBlock = ({ language, children }: { language: string; children: string }) => {
@@ -119,6 +126,52 @@ const suggestedQuestionsConfig: QuestionConfig[] = [
119
126
  { label: 'Who owns this domain?', prompt: 'Who owns this domain and how do I contact them?' },
120
127
  ],
121
128
  },
129
+ // Match /schemas with specType=graphql as query parameter
130
+ {
131
+ pattern: /^\/schemas.*[?&]specType=graphql/,
132
+ questions: [
133
+ { label: 'Tell me more about this GraphQL schema', prompt: 'Tell me more about this GraphQL schema' },
134
+ { label: 'What queries are available?', prompt: 'What queries are available in this GraphQL schema?' },
135
+ { label: 'What mutations are available?', prompt: 'What mutations are available in this GraphQL schema?' },
136
+ { label: 'Show me the types defined', prompt: 'Show me the types defined in this GraphQL schema' },
137
+ ],
138
+ },
139
+ {
140
+ pattern: /^\/schemas.*[?&]specType=openapi/,
141
+ questions: [
142
+ { label: 'Tell me more about this OpenAPI schema', prompt: 'Tell me more about this OpenAPI schema' },
143
+ { label: 'Show me the endpoints available', prompt: 'Show me the endpoints available in this OpenAPI schema' },
144
+ {
145
+ label: 'Show me the request and response formats',
146
+ prompt: 'Show me the request and response formats in this OpenAPI schema',
147
+ },
148
+ ],
149
+ },
150
+ {
151
+ pattern: /^\/schemas.*[?&]specType=asyncapi/,
152
+ questions: [
153
+ { label: 'Tell me more about this AsyncAPI schema', prompt: 'Tell me more about this AsyncAPI schema' },
154
+ { label: 'Show me the channels available', prompt: 'Show me the channels available in this AsyncAPI schema' },
155
+ { label: 'Show me the messages available', prompt: 'Show me the messages available in this AsyncAPI schema' },
156
+ {
157
+ label: 'Show me the request and response formats',
158
+ prompt: 'Show me the request and response formats in this AsyncAPI schema',
159
+ },
160
+ ],
161
+ },
162
+ {
163
+ pattern: /^\/schemas/,
164
+ questions: [
165
+ { label: 'Tell me more about this schema?', prompt: 'Tell me more about this schema' },
166
+ { label: 'Who producers or consumes this schema?', prompt: 'Who producers or consumes this schema?' },
167
+ { label: 'What fields are required?', prompt: 'What fields are required for this schema?' },
168
+ {
169
+ label: 'Generate code using this schema',
170
+ prompt: 'Create a code example of using this schema, ask me for the programming language I want the example in.',
171
+ },
172
+ ],
173
+ },
174
+
122
175
  // Any other docs page
123
176
  {
124
177
  pattern: /^\/docs\/.+/,
@@ -158,16 +211,23 @@ interface ChatPanelProps {
158
211
 
159
212
  const PANEL_WIDTH = 400;
160
213
 
161
- // Staggered fade-in animation styles
214
+ // Staggered fade-in animation styles (delays account for 800ms panel slide)
162
215
  const fadeInStyles = {
163
216
  header: {
164
- animation: 'fadeInDown 0.3s ease-out 0.1s both',
217
+ animation: 'fadeIn 0.5s ease-out 0.3s both',
165
218
  },
166
- content: {
167
- animation: 'fadeInDown 0.3s ease-out 0.2s both',
219
+ welcome: {
220
+ animation: 'fadeIn 0.6s ease-out 0.4s both',
168
221
  },
169
- input: {
170
- animation: 'fadeInDown 0.3s ease-out 0.3s both',
222
+ // Label and questions will be staggered individually in the component
223
+ questionsLabel: {
224
+ animation: 'fadeIn 0.5s ease-out 0.7s both',
225
+ },
226
+ getQuestionStyle: (index: number) => ({
227
+ animation: `fadeIn 0.5s ease-out ${0.85 + index * 0.12}s both`,
228
+ }),
229
+ inputFocus: {
230
+ animation: 'focusIn 0.6s ease-out 1.4s both',
171
231
  },
172
232
  };
173
233
 
@@ -199,18 +259,47 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
199
259
  const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
200
260
  const [pathname, setPathname] = useState('');
201
261
  const [isFullscreen, setIsFullscreen] = useState(false);
262
+ const [tools, setTools] = useState<ToolMetadata[]>([]);
263
+
264
+ // Sort tools with custom ones first
265
+ const sortedTools = useMemo(() => {
266
+ return [...tools].sort((a, b) => {
267
+ if (a.isCustom && !b.isCustom) return -1;
268
+ if (!a.isCustom && b.isCustom) return 1;
269
+ return 0;
270
+ });
271
+ }, [tools]);
272
+
273
+ // Fetch available tools when panel opens
274
+ useEffect(() => {
275
+ if (isOpen && tools.length === 0) {
276
+ fetch('/api/chat')
277
+ .then((res) => res.json())
278
+ .then((data) => {
279
+ if (data.tools) {
280
+ setTools(data.tools);
281
+ }
282
+ })
283
+ .catch(() => {
284
+ // Silently fail - tools info is optional
285
+ });
286
+ }
287
+ }, [isOpen, tools.length]);
202
288
 
203
- // Get current pathname on mount and when panel opens
289
+ // Get current URL (pathname + search) on mount and when panel opens
204
290
  useEffect(() => {
205
- setPathname(window.location.pathname);
291
+ setPathname(window.location.pathname + window.location.search);
206
292
  }, [isOpen]);
207
293
 
208
294
  const suggestedQuestions = getSuggestedQuestions(pathname);
209
295
 
210
- const { messages, sendMessage, stop, status, setMessages } = useChat({
296
+ const { messages, sendMessage, stop, status, setMessages, error } = useChat({
211
297
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
212
298
  });
213
299
 
300
+ // Extract user-friendly error message
301
+ const errorMessage = error?.message || 'Something went wrong. Please try again.';
302
+
214
303
  // Check if the assistant has started outputting content
215
304
  const lastAssistantMessage = [...messages].reverse().find((m) => m.role === 'assistant');
216
305
  const assistantHasContent = lastAssistantMessage?.parts?.some(
@@ -273,26 +362,45 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
273
362
  }
274
363
  }, [isOpen, isLoading]);
275
364
 
276
- // Add/remove padding to main content when sidebar panel is open
365
+ // Add/remove padding to main application and header when sidebar panel is open
277
366
  useEffect(() => {
278
- const contentEl = document.getElementById('content');
279
- if (!contentEl) return;
367
+ const appEl = document.getElementById('eventcatalog-application');
368
+ const headerEl = document.getElementById('eventcatalog-header');
369
+ const docsSidebarEl = document.getElementById('eventcatalog-docs-sidebar');
280
370
 
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
- }
371
+ const elements = [appEl, headerEl].filter(Boolean) as HTMLElement[];
285
372
 
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';
373
+ elements.forEach((el) => {
374
+ // Add transition if not already present
375
+ if (!el.style.transition) {
376
+ el.style.transition = 'padding-right 800ms cubic-bezier(0.16, 1, 0.3, 1)';
377
+ }
378
+
379
+ // Only add padding when panel is open AND not in fullscreen mode
380
+ if (isOpen && !isFullscreen) {
381
+ el.style.paddingRight = `${PANEL_WIDTH}px`;
382
+ } else {
383
+ el.style.paddingRight = '0';
384
+ }
385
+ });
386
+
387
+ // Hide docs sidebar when chat panel is open
388
+ if (docsSidebarEl) {
389
+ if (isOpen && !isFullscreen) {
390
+ docsSidebarEl.style.display = 'none';
391
+ } else {
392
+ docsSidebarEl.style.display = '';
393
+ }
291
394
  }
292
395
 
293
396
  // Cleanup on unmount
294
397
  return () => {
295
- contentEl.style.paddingRight = '0';
398
+ elements.forEach((el) => {
399
+ el.style.paddingRight = '0';
400
+ });
401
+ if (docsSidebarEl) {
402
+ docsSidebarEl.style.display = '';
403
+ }
296
404
  };
297
405
  }, [isOpen, isFullscreen]);
298
406
 
@@ -341,14 +449,22 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
341
449
  <>
342
450
  {/* Keyframes for fade-in animation */}
343
451
  <style>{`
344
- @keyframes fadeInDown {
452
+ @keyframes fadeIn {
345
453
  from {
346
454
  opacity: 0;
347
- transform: translateY(-8px);
348
455
  }
349
456
  to {
350
457
  opacity: 1;
351
- transform: translateY(0);
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;
352
468
  }
353
469
  }
354
470
  @keyframes pulse-glow {
@@ -360,32 +476,61 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
360
476
  {/* Panel - hidden when fullscreen modal is open */}
361
477
  {!isFullscreen && (
362
478
  <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"
479
+ className="fixed top-0 right-0 h-[100vh] z-[200] bg-white border-l border-gray-200 flex flex-col overflow-hidden"
364
480
  style={{
365
481
  width: `${PANEL_WIDTH}px`,
366
482
  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)',
483
+ transition: 'transform 800ms cubic-bezier(0.16, 1, 0.3, 1)',
369
484
  }}
370
485
  >
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
486
  {/* 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" />
487
+ <div className="flex-none border-b border-gray-100 shrink-0 pb-1">
488
+ <div className="flex items-center justify-between px-4 py-3">
489
+ <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" />
385
492
  </div>
386
- <span className="font-semibold text-gray-900 text-[15px]">EventCatalog Assistant</span>
493
+ <span className="font-medium text-gray-900 text-sm">EventCatalog Assistant</span>
387
494
  </div>
388
495
  <div className="flex items-center space-x-1">
496
+ {tools.length > 0 && (
497
+ <Popover.Root>
498
+ <Popover.Trigger asChild>
499
+ <button
500
+ className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
501
+ aria-label="View available tools"
502
+ title="Available tools"
503
+ >
504
+ <Wrench size={16} />
505
+ </button>
506
+ </Popover.Trigger>
507
+ <Popover.Portal>
508
+ <Popover.Content
509
+ className="w-72 bg-white rounded-lg shadow-lg border border-gray-200 p-3 z-[250]"
510
+ sideOffset={5}
511
+ align="end"
512
+ >
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">
515
+ {sortedTools.map((tool) => (
516
+ <div key={tool.name} className="py-1.5 first:pt-0 last:pb-0">
517
+ <div className="flex items-center gap-1.5">
518
+ <span className="text-[11px] font-medium text-gray-700">{tool.name}</span>
519
+ {tool.isCustom && (
520
+ <span className="px-1.5 py-0.5 text-[9px] font-medium bg-purple-100 text-purple-700 rounded">
521
+ Custom
522
+ </span>
523
+ )}
524
+ </div>
525
+ <div className="text-[10px] text-gray-500 mt-0.5">{tool.description}</div>
526
+ </div>
527
+ ))}
528
+ </div>
529
+ <Popover.Arrow className="fill-white" />
530
+ </Popover.Content>
531
+ </Popover.Portal>
532
+ </Popover.Root>
533
+ )}
389
534
  <button
390
535
  onClick={() => setIsFullscreen(true)}
391
536
  className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
@@ -415,49 +560,51 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
415
560
  </div>
416
561
  {/* Thinking indicator */}
417
562
  {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" />
563
+ <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" />
420
565
  <span className="text-xs text-gray-500">Thinking...</span>
421
566
  </div>
422
567
  )}
423
568
  </div>
424
569
 
425
570
  {/* 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
- >
571
+ <div className="flex-1 flex flex-col min-h-0 relative overflow-hidden" key={isOpen ? 'content-open' : 'content-closed'}>
431
572
  {/* Messages or Welcome area */}
432
- <div className="flex-1 overflow-y-auto px-6 scrollbar-hide">
573
+ <div className="flex-1 overflow-y-auto px-4 scrollbar-hide">
433
574
  {!hasMessages ? (
434
575
  /* Welcome area */
435
- <div className="flex flex-col h-full justify-between pt-6 pb-2">
576
+ <div className="flex flex-col h-full justify-between pt-4 pb-2">
436
577
  {/* Center content */}
437
- <div className="flex-1 flex flex-col items-center justify-center">
578
+ <div
579
+ className="flex-1 flex flex-col items-center justify-center"
580
+ style={isOpen ? fadeInStyles.welcome : undefined}
581
+ >
438
582
  {/* Animated Icon */}
439
- <div className="relative mb-6">
583
+ <div className="relative mb-5">
440
584
  <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" />
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" />
444
588
  </div>
445
589
  </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>
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>
452
592
  </div>
453
593
 
454
594
  {/* Suggested questions */}
455
- <div className="flex flex-wrap gap-2 mt-6">
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>
456
602
  {suggestedQuestions.map((question, index) => (
457
603
  <button
458
604
  key={index}
459
605
  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"
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"
607
+ style={isOpen ? fadeInStyles.getQuestionStyle(index) : undefined}
461
608
  >
462
609
  {question.label}
463
610
  </button>
@@ -472,8 +619,8 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
472
619
  return (
473
620
  <div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
474
621
  {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>
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>
477
624
  </div>
478
625
  ) : (
479
626
  <div className="w-full text-gray-700">
@@ -529,7 +676,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
529
676
  <div className="w-full">
530
677
  <div className="flex items-start gap-2 text-red-600 text-sm">
531
678
  <span className="shrink-0">⚠️</span>
532
- <span>Something went wrong. Please try again.</span>
679
+ <span>{errorMessage}</span>
533
680
  </div>
534
681
  </div>
535
682
  </div>
@@ -541,13 +688,9 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
541
688
  </div>
542
689
 
543
690
  {/* 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
- >
691
+ <div className="flex-none px-4 py-3 border-t border-gray-100" key={isOpen ? 'input-open' : 'input-closed'}>
549
692
  <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">
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">
551
694
  <input
552
695
  ref={inputRef}
553
696
  type="text"
@@ -559,16 +702,16 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
559
702
  submitMessage(inputValue);
560
703
  }
561
704
  }}
562
- placeholder="Ask anything about your architecture..."
705
+ placeholder="Ask a question..."
563
706
  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"
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"
565
708
  />
566
709
  <div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-10">
567
710
  {isStreaming ? (
568
711
  <button
569
712
  type="button"
570
713
  onClick={() => stop()}
571
- className="p-1.5 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
714
+ className="p-1.5 text-red-500 hover:bg-red-50 rounded-md transition-colors"
572
715
  aria-label="Stop generating"
573
716
  >
574
717
  <Square size={12} fill="currentColor" />
@@ -577,7 +720,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
577
720
  <button
578
721
  type="submit"
579
722
  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"
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"
581
724
  aria-label="Send message"
582
725
  >
583
726
  Send
@@ -586,7 +729,6 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
586
729
  </div>
587
730
  </div>
588
731
  </form>
589
-
590
732
  <p className="text-[9px] text-gray-400 mt-2 text-center">AI can make mistakes. Verify important info.</p>
591
733
  </div>
592
734
  </div>
@@ -605,21 +747,56 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
605
747
  }}
606
748
  >
607
749
  <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
-
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">
613
752
  {/* 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" />
753
+ <div className="flex items-center justify-between px-5 py-3 border-b border-gray-100 flex-shrink-0">
754
+ <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" />
619
757
  </div>
620
- <Dialog.Title className="text-lg font-semibold text-gray-900">EventCatalog Assistant</Dialog.Title>
758
+ <Dialog.Title className="text-base font-medium text-gray-900">Ask AI</Dialog.Title>
621
759
  </div>
622
760
  <div className="flex items-center space-x-2">
761
+ {tools.length > 0 && (
762
+ <Popover.Root>
763
+ <Popover.Trigger asChild>
764
+ <button
765
+ className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
766
+ aria-label="View available tools"
767
+ title="Available tools"
768
+ >
769
+ <Wrench size={18} />
770
+ </button>
771
+ </Popover.Trigger>
772
+ <Popover.Portal>
773
+ <Popover.Content
774
+ className="w-80 bg-white rounded-lg shadow-lg border border-gray-200 p-4 z-[350]"
775
+ sideOffset={5}
776
+ align="end"
777
+ style={{ maxHeight: 'calc(100vh - 200px)' }}
778
+ >
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)' }}>
781
+ {sortedTools.map((tool) => (
782
+ <div key={tool.name} className="py-2 first:pt-0 last:pb-0">
783
+ <div className="flex items-center gap-1.5">
784
+ <span className="text-xs font-medium text-gray-700">{tool.name}</span>
785
+ {tool.isCustom && (
786
+ <span className="px-1.5 py-0.5 text-[9px] font-medium bg-purple-100 text-purple-700 rounded">
787
+ Custom
788
+ </span>
789
+ )}
790
+ </div>
791
+ <div className="text-[11px] text-gray-500 mt-0.5">{tool.description}</div>
792
+ </div>
793
+ ))}
794
+ </div>
795
+ <Popover.Arrow className="fill-white" />
796
+ </Popover.Content>
797
+ </Popover.Portal>
798
+ </Popover.Root>
799
+ )}
623
800
  {hasMessages && (
624
801
  <button
625
802
  onClick={() => setMessages([])}
@@ -653,39 +830,35 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
653
830
 
654
831
  {/* Thinking indicator */}
655
832
  {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" />
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" />
658
835
  <span className="text-sm text-gray-500">Thinking...</span>
659
836
  </div>
660
837
  )}
661
838
 
662
839
  {/* Modal Content */}
663
- <div className="flex-1 overflow-y-auto px-6 py-4">
840
+ <div className="flex-1 overflow-y-auto px-5 py-4">
664
841
  {!hasMessages ? (
665
842
  /* Welcome area */
666
843
  <div className="flex flex-col h-full justify-center items-center">
667
844
  {/* Animated Icon */}
668
- <div className="relative mb-8">
845
+ <div className="relative mb-6">
669
846
  <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" />
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" />
673
850
  </div>
674
851
  </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>
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>
681
854
 
682
855
  {/* Suggested questions */}
683
- <div className="flex flex-wrap gap-2 justify-center max-w-lg">
856
+ <div className="grid grid-cols-2 gap-2 max-w-lg">
684
857
  {suggestedQuestions.map((question, index) => (
685
858
  <button
686
859
  key={index}
687
860
  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"
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"
689
862
  >
690
863
  {question.label}
691
864
  </button>
@@ -700,8 +873,8 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
700
873
  return (
701
874
  <div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
702
875
  {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>
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>
705
878
  </div>
706
879
  ) : (
707
880
  <div className="w-full text-gray-700">
@@ -756,7 +929,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
756
929
  <div className="w-full">
757
930
  <div className="flex items-start gap-2 text-red-600 text-sm">
758
931
  <span className="shrink-0">⚠️</span>
759
- <span>Something went wrong. Please try again.</span>
932
+ <span>{errorMessage}</span>
760
933
  </div>
761
934
  </div>
762
935
  </div>
@@ -768,9 +941,9 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
768
941
  </div>
769
942
 
770
943
  {/* 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">
944
+ <div className="flex-shrink-0 px-5 py-4 border-t border-gray-100">
772
945
  <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">
946
+ <div className="relative bg-gray-50 rounded-lg border border-gray-200 focus-within:border-purple-300 focus-within:bg-white transition-all">
774
947
  <input
775
948
  ref={modalInputRef}
776
949
  type="text"
@@ -782,25 +955,25 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
782
955
  submitMessage(inputValue);
783
956
  }
784
957
  }}
785
- placeholder="Ask anything about your architecture..."
958
+ placeholder="Ask a question..."
786
959
  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"
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"
788
961
  />
789
962
  <div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
790
963
  {isStreaming ? (
791
964
  <button
792
965
  type="button"
793
966
  onClick={() => stop()}
794
- className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
967
+ className="p-1.5 text-red-500 hover:bg-red-50 rounded-md transition-colors"
795
968
  aria-label="Stop generating"
796
969
  >
797
- <Square size={16} fill="currentColor" />
970
+ <Square size={14} fill="currentColor" />
798
971
  </button>
799
972
  ) : (
800
973
  <button
801
974
  type="submit"
802
975
  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"
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"
804
977
  aria-label="Send message"
805
978
  >
806
979
  Send