@eventcatalog/core 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-ESUL7UE6.js → chunk-AA47DJ43.js} +1 -1
- package/dist/{chunk-KBO4IL2D.js → chunk-GGRXP5WM.js} +1 -1
- package/dist/{chunk-2WGZFERB.js → chunk-L3QRQT7U.js} +1 -1
- package/dist/{chunk-URR33SNK.js → chunk-RWYEP5SD.js} +1 -1
- package/dist/{chunk-I3CW5KQI.js → chunk-VPQCMMRM.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +488 -231
- package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +2 -2
- package/eventcatalog/src/enterprise/ai/chat-api.ts +24 -3
- package/package.json +7 -6
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
|
|
2
|
-
import { X,
|
|
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
|
-
//
|
|
18
|
-
const
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
244
|
-
const
|
|
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-
|
|
247
|
-
<div className="h-4 bg-
|
|
248
|
-
<div className="h-4 bg-
|
|
249
|
-
<div className="h-4 bg-
|
|
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
|
-
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
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 =
|
|
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
|
|
427
|
-
const
|
|
428
|
-
(e: React.KeyboardEvent<
|
|
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]
|
|
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
|
|
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
|
-
<
|
|
491
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
567
|
-
<span className="text-xs text-[rgb(var(--ec-
|
|
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-
|
|
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
|
|
579
|
-
{/*
|
|
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
|
-
{/*
|
|
585
|
-
<div className="relative mb-
|
|
586
|
-
<div className="
|
|
587
|
-
|
|
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-
|
|
593
|
-
<p className="text-sm text-[rgb(var(--ec-
|
|
594
|
-
|
|
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-
|
|
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-
|
|
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' ? '
|
|
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-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
className="
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
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-
|
|
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={
|
|
707
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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={
|
|
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-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
|
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
|
|
849
|
-
<span className="text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
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-
|
|
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
|
|
858
|
-
{/*
|
|
859
|
-
<div className="
|
|
860
|
-
|
|
861
|
-
<div className="relative
|
|
862
|
-
<
|
|
863
|
-
|
|
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
|
|
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-
|
|
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' ? '
|
|
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-
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
className="
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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={
|
|
967
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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>
|