@eventcatalog/core 3.1.0 → 3.2.1

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.
@@ -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);
298
+ };
299
+
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;
241
333
  };
242
334
 
243
- // Skeleton loading component
244
- const SkeletonLoader = () => (
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]);
480
+
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
+ }, []);
295
514
 
296
- const { messages, sendMessage, stop, status, setMessages, error } = useChat({
297
- sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
298
- });
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-[rgb(var(--ec-accent-subtle))] rounded-md">
491
- <BookOpen size={14} className="text-[rgb(var(--ec-accent))]" />
492
- </div>
493
- <span className="font-medium text-[rgb(var(--ec-page-text))] 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-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] 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
  >
@@ -535,7 +741,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
535
741
  )}
536
742
  <button
537
743
  onClick={() => setIsFullscreen(true)}
538
- 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"
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"
539
745
  aria-label="Expand to fullscreen"
540
746
  title="Expand"
541
747
  >
@@ -544,7 +750,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
544
750
  {hasMessages && (
545
751
  <button
546
752
  onClick={() => setMessages([])}
547
- 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"
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"
548
754
  aria-label="Clear chat"
549
755
  title="Clear chat"
550
756
  >
@@ -553,7 +759,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
553
759
  )}
554
760
  <button
555
761
  onClick={onClose}
556
- 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"
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"
557
763
  aria-label="Close chat panel"
558
764
  >
559
765
  <X size={18} />
@@ -563,8 +769,16 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
563
769
  {/* Thinking indicator */}
564
770
  {isThinking && (
565
771
  <div className="px-4 pb-2 flex items-center gap-2">
566
- <div className="w-1.5 h-1.5 bg-[rgb(var(--ec-accent-subtle))]0 rounded-full animate-pulse" />
567
- <span className="text-xs text-[rgb(var(--ec-page-text-muted))]">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>
568
782
  </div>
569
783
  )}
570
784
  </div>
@@ -572,42 +786,34 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
572
786
  {/* Content */}
573
787
  <div className="flex-1 flex flex-col min-h-0 relative overflow-hidden" key={isOpen ? 'content-open' : 'content-closed'}>
574
788
  {/* Messages or Welcome area */}
575
- <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">
576
790
  {!hasMessages ? (
577
- /* Welcome area */
578
- <div className="flex flex-col h-full justify-between pt-4 pb-2">
579
- {/* Center content */}
791
+ /* Welcome area - Clean GitBook-inspired design */
792
+ <div className="flex flex-col h-full py-6">
793
+ {/* Greeting section - centered */}
580
794
  <div
581
- className="flex-1 flex flex-col items-center justify-center"
795
+ className="flex-1 flex flex-col items-center justify-center text-center"
582
796
  style={isOpen ? fadeInStyles.welcome : undefined}
583
797
  >
584
- {/* Animated Icon */}
585
- <div className="relative mb-5">
586
- <div className="absolute inset-0 bg-[rgb(var(--ec-accent)/0.2)] rounded-2xl blur-xl animate-pulse" />
587
- <div className="relative w-14 h-14 rounded-2xl bg-gradient-to-br from-[rgb(var(--ec-accent-gradient-from))] to-[rgb(var(--ec-accent-gradient-to))] flex items-center justify-center shadow-lg">
588
- <BookOpen size={26} className="text-white" strokeWidth={1.5} />
589
- <Sparkles size={10} className="text-[rgb(var(--ec-accent)/0.4)] 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} />
590
802
  </div>
591
803
  </div>
592
- <h2 className="text-base font-medium text-[rgb(var(--ec-page-text))] mb-1">{getGreeting()}</h2>
593
- <p className="text-sm text-[rgb(var(--ec-page-text-muted))] text-center">
594
- Ask me anything about your catalog.
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
595
807
  </p>
596
808
  </div>
597
809
 
598
- {/* Suggested questions */}
599
- <div className="space-y-1.5 mt-4">
600
- <p
601
- className="text-xs font-medium text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wide mb-2"
602
- style={isOpen ? fadeInStyles.questionsLabel : undefined}
603
- >
604
- Example questions
605
- </p>
810
+ {/* Suggested questions - pill style */}
811
+ <div className="flex-none space-y-2">
606
812
  {suggestedQuestions.map((question, index) => (
607
813
  <button
608
814
  key={index}
609
815
  onClick={() => handleSuggestedAction(question.prompt)}
610
- className="w-full text-left px-3 py-2.5 text-sm text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-content-hover))] hover:bg-[rgb(var(--ec-accent-subtle))] hover:text-[rgb(var(--ec-accent-text))] border border-[rgb(var(--ec-page-border))] hover:border-[rgb(var(--ec-accent)/0.3)] 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"
611
817
  style={isOpen ? fadeInStyles.getQuestionStyle(index) : undefined}
612
818
  >
613
819
  {question.label}
@@ -618,50 +824,58 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
618
824
  ) : (
619
825
  /* Messages area */
620
826
  <div className="py-4 space-y-4">
621
- {messages.map((message) => {
827
+ {messages.map((message, messageIndex) => {
622
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;
623
832
  return (
624
- <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'}`}>
625
834
  {message.role === 'user' ? (
626
- <div className="max-w-[85%] rounded-2xl rounded-br-md px-4 py-2.5 bg-[rgb(var(--ec-accent))] text-white">
627
- <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>
628
837
  </div>
629
838
  ) : (
630
- <div className="w-full text-[rgb(var(--ec-page-text))]">
631
- <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">
632
- <ReactMarkdown
633
- components={{
634
- a: ({ ...props }) => (
635
- <a
636
- {...props}
637
- target="_blank"
638
- rel="noopener noreferrer"
639
- className="text-[rgb(var(--ec-accent))] hover:text-[rgb(var(--ec-accent-hover))] underline"
640
- />
641
- ),
642
- code: ({ children, className, ...props }) => {
643
- const isInline = !className;
644
- const match = /language-(\w+)/.exec(className || '');
645
- const language = match ? match[1] : 'text';
646
- const codeString = String(children).replace(/\n$/, '');
647
-
648
- return isInline ? (
649
- <code
650
- className="px-1 py-0.5 rounded text-xs font-mono bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-page-text))]"
651
- {...props}
652
- >
653
- {children}
654
- </code>
655
- ) : (
656
- <CodeBlock language={language}>{codeString}</CodeBlock>
657
- );
658
- },
659
- }}
660
- >
661
- {content}
662
- </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>
663
862
  </div>
664
- </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
+ </>
665
879
  )}
666
880
  </div>
667
881
  );
@@ -691,43 +905,46 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
691
905
  )}
692
906
  </div>
693
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
+
694
919
  {/* Input area (Fixed at bottom) */}
695
- <div
696
- className="flex-none px-4 py-3 border-t border-[rgb(var(--ec-page-border))]"
697
- key={isOpen ? 'input-open' : 'input-closed'}
698
- >
920
+ <div className="flex-none px-4 py-3" key={isOpen ? 'input-open' : 'input-closed'}>
699
921
  <form onSubmit={handleSubmit}>
700
- <div className="relative bg-[rgb(var(--ec-input-bg))] rounded-lg border-2 border-[rgb(var(--ec-input-border))] focus-within:border-[rgb(var(--ec-accent)/0.5)] 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">
701
923
  <input
702
924
  ref={inputRef}
703
925
  type="text"
704
926
  value={inputValue}
705
927
  onChange={(e) => setInputValue(e.target.value)}
706
- onKeyDown={(e) => {
707
- if (e.key === 'Enter' && !e.shiftKey) {
708
- e.preventDefault();
709
- submitMessage(inputValue);
710
- }
711
- }}
712
- placeholder="Ask a question..."
928
+ onKeyDown={handleInputKeyDown}
929
+ placeholder="Ask, search, or explain..."
713
930
  disabled={isLoading}
714
- className="w-full px-3 py-2.5 pr-14 bg-transparent text-[rgb(var(--ec-input-text))] placeholder-[rgb(var(--ec-input-placeholder))] 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"
715
932
  />
716
- <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">
717
934
  {isStreaming ? (
718
935
  <button
719
936
  type="button"
720
937
  onClick={() => stop()}
721
- className="p-1.5 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-md transition-colors"
938
+ className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
722
939
  aria-label="Stop generating"
723
940
  >
724
- <Square size={12} fill="currentColor" />
941
+ <Square size={14} fill="currentColor" />
725
942
  </button>
726
943
  ) : (
727
944
  <button
728
945
  type="submit"
729
946
  disabled={!inputValue.trim() || isLoading}
730
- className="px-2.5 py-1 bg-[rgb(var(--ec-accent))] text-white text-xs font-medium rounded-md hover:bg-[rgb(var(--ec-accent-hover))] disabled:bg-[rgb(var(--ec-content-hover))] disabled:text-[rgb(var(--ec-icon-color))] 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"
731
948
  aria-label="Send message"
732
949
  >
733
950
  Send
@@ -736,9 +953,19 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
736
953
  </div>
737
954
  </div>
738
955
  </form>
739
- <p className="text-[9px] text-[rgb(var(--ec-icon-color))] mt-2 text-center">
740
- AI can make mistakes. Verify important info.
741
- </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>
742
969
  </div>
743
970
  </div>
744
971
  </div>
@@ -757,7 +984,15 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
757
984
  >
758
985
  <Dialog.Portal>
759
986
  <Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[300]" />
760
- <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-[rgb(var(--ec-page-bg))] shadow-2xl z-[301] flex flex-col overflow-hidden focus:outline-none border border-[rgb(var(--ec-page-border))]">
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
+ >
761
996
  {/* Modal Header */}
762
997
  <div className="flex items-center justify-between px-5 py-3 border-b border-[rgb(var(--ec-page-border))] flex-shrink-0">
763
998
  <div className="flex items-center space-x-2.5">
@@ -845,34 +1080,45 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
845
1080
  {/* Thinking indicator */}
846
1081
  {isThinking && (
847
1082
  <div className="px-5 py-2 flex items-center gap-2 border-b border-[rgb(var(--ec-page-border))]">
848
- <div className="w-1.5 h-1.5 bg-[rgb(var(--ec-accent-subtle))]0 rounded-full animate-pulse" />
849
- <span className="text-sm text-[rgb(var(--ec-page-text-muted))]">Thinking...</span>
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>
850
1093
  </div>
851
1094
  )}
852
1095
 
853
1096
  {/* Modal Content */}
854
- <div className="flex-1 overflow-y-auto px-5 py-4">
1097
+ <div className="flex-1 overflow-y-auto px-6 py-6">
855
1098
  {!hasMessages ? (
856
- /* Welcome area */
857
- <div className="flex flex-col h-full justify-center items-center">
858
- {/* Animated Icon */}
859
- <div className="relative mb-6">
860
- <div className="absolute inset-0 bg-[rgb(var(--ec-accent)/0.2)] rounded-2xl blur-xl animate-pulse" />
861
- <div className="relative w-16 h-16 rounded-2xl bg-gradient-to-br from-[rgb(var(--ec-accent-gradient-from))] to-[rgb(var(--ec-accent-gradient-to))] flex items-center justify-center shadow-lg">
862
- <BookOpen size={30} className="text-white" strokeWidth={1.5} />
863
- <Sparkles size={12} className="text-[rgb(var(--ec-accent)/0.4)] 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>
864
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>
865
1113
  </div>
866
- <h2 className="text-xl font-medium text-[rgb(var(--ec-page-text))] mb-1">{getGreeting()}</h2>
867
- <p className="text-[rgb(var(--ec-page-text-muted))] text-center mb-8">Ask me anything about your catalog.</p>
868
1114
 
869
- {/* Suggested questions */}
870
- <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">
871
1117
  {suggestedQuestions.map((question, index) => (
872
1118
  <button
873
1119
  key={index}
874
1120
  onClick={() => handleSuggestedAction(question.prompt)}
875
- className="px-4 py-2.5 text-sm text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-content-hover))] hover:bg-[rgb(var(--ec-accent-subtle))] hover:text-[rgb(var(--ec-accent-text))] 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"
876
1122
  >
877
1123
  {question.label}
878
1124
  </button>
@@ -882,50 +1128,58 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
882
1128
  ) : (
883
1129
  /* Messages area */
884
1130
  <div className="max-w-3xl mx-auto space-y-4">
885
- {messages.map((message) => {
1131
+ {messages.map((message, messageIndex) => {
886
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;
887
1136
  return (
888
- <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'}`}>
889
1138
  {message.role === 'user' ? (
890
- <div className="max-w-[75%] rounded-2xl rounded-br-md px-4 py-2.5 bg-[rgb(var(--ec-accent))] text-white">
891
- <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>
892
1141
  </div>
893
1142
  ) : (
894
- <div className="w-full text-[rgb(var(--ec-page-text))]">
895
- <div className="prose prose-sm max-w-none">
896
- <ReactMarkdown
897
- components={{
898
- a: ({ ...props }) => (
899
- <a
900
- {...props}
901
- target="_blank"
902
- rel="noopener noreferrer"
903
- className="text-[rgb(var(--ec-accent))] hover:text-[rgb(var(--ec-accent-hover))] underline"
904
- />
905
- ),
906
- code: ({ children, className, ...props }) => {
907
- const isInline = !className;
908
- const match = /language-(\w+)/.exec(className || '');
909
- const language = match ? match[1] : 'text';
910
- const codeString = String(children).replace(/\n$/, '');
911
-
912
- return isInline ? (
913
- <code
914
- className="px-1.5 py-0.5 rounded text-sm font-mono bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-page-text))]"
915
- {...props}
916
- >
917
- {children}
918
- </code>
919
- ) : (
920
- <CodeBlock language={language}>{codeString}</CodeBlock>
921
- );
922
- },
923
- }}
924
- >
925
- {content}
926
- </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>
927
1166
  </div>
928
- </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
+ </>
929
1183
  )}
930
1184
  </div>
931
1185
  );
@@ -955,30 +1209,25 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
955
1209
  </div>
956
1210
 
957
1211
  {/* Modal Input area */}
958
- <div className="flex-shrink-0 px-5 py-4 border-t border-[rgb(var(--ec-page-border))]">
1212
+ <div className="flex-shrink-0 px-6 py-4">
959
1213
  <form onSubmit={handleSubmit} className="max-w-3xl mx-auto">
960
- <div className="relative bg-[rgb(var(--ec-input-bg))] rounded-lg border border-[rgb(var(--ec-input-border))] focus-within:border-[rgb(var(--ec-accent)/0.5)] 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">
961
1215
  <input
962
1216
  ref={modalInputRef}
963
1217
  type="text"
964
1218
  value={inputValue}
965
1219
  onChange={(e) => setInputValue(e.target.value)}
966
- onKeyDown={(e) => {
967
- if (e.key === 'Enter' && !e.shiftKey) {
968
- e.preventDefault();
969
- submitMessage(inputValue);
970
- }
971
- }}
972
- placeholder="Ask a question..."
1220
+ onKeyDown={handleInputKeyDown}
1221
+ placeholder="Ask, search, or explain..."
973
1222
  disabled={isLoading}
974
- 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-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"
975
1224
  />
976
1225
  <div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
977
1226
  {isStreaming ? (
978
1227
  <button
979
1228
  type="button"
980
1229
  onClick={() => stop()}
981
- className="p-1.5 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-md transition-colors"
1230
+ className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
982
1231
  aria-label="Stop generating"
983
1232
  >
984
1233
  <Square size={14} fill="currentColor" />
@@ -987,7 +1236,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
987
1236
  <button
988
1237
  type="submit"
989
1238
  disabled={!inputValue.trim() || isLoading}
990
- className="px-3 py-1.5 bg-[rgb(var(--ec-accent))] text-white text-sm font-medium rounded-md hover:bg-[rgb(var(--ec-accent-hover))] disabled:bg-[rgb(var(--ec-content-hover))] disabled:text-[rgb(var(--ec-icon-color))] 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"
991
1240
  aria-label="Send message"
992
1241
  >
993
1242
  Send
@@ -996,9 +1245,17 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
996
1245
  </div>
997
1246
  </div>
998
1247
  </form>
999
- <p className="text-xs text-[rgb(var(--ec-icon-color))] mt-2 text-center">
1000
- AI can make mistakes. Verify important info.
1001
- </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>
1002
1259
  </div>
1003
1260
  </Dialog.Content>
1004
1261
  </Dialog.Portal>