@eventcatalog/core 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-VO5WYA44.js → chunk-AA47DJ43.js} +1 -1
- package/dist/{chunk-EKGR533N.js → chunk-GGRXP5WM.js} +1 -1
- package/dist/{chunk-E5Q7TZYT.js → chunk-L3QRQT7U.js} +1 -1
- package/dist/{chunk-BYP43AAT.js → chunk-RWYEP5SD.js} +1 -1
- package/dist/{chunk-KF5PARQK.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.config.d.cts +7 -0
- package/dist/eventcatalog.config.d.ts +7 -0
- 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 +520 -247
- package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +3 -3
- package/eventcatalog/src/components/Checkbox.astro +7 -4
- package/eventcatalog/src/components/CopyAsMarkdown.tsx +15 -15
- package/eventcatalog/src/components/EnvironmentDropdown.tsx +15 -7
- package/eventcatalog/src/components/FavoriteButton.tsx +1 -1
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +72 -60
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +68 -48
- package/eventcatalog/src/components/Header.astro +15 -10
- package/eventcatalog/src/components/Lists/OwnersList.tsx +17 -10
- package/eventcatalog/src/components/Lists/PillListFlat.styles.css +12 -0
- package/eventcatalog/src/components/Lists/PillListFlat.tsx +15 -15
- package/eventcatalog/src/components/Lists/VersionList.astro +15 -5
- package/eventcatalog/src/components/MDX/Accordion/Accordion.tsx +3 -3
- package/eventcatalog/src/components/MDX/Admonition.tsx +49 -9
- package/eventcatalog/src/components/MDX/Attachments.astro +15 -11
- package/eventcatalog/src/components/MDX/ChannelInformation/ChannelInformation.tsx +29 -15
- package/eventcatalog/src/components/MDX/EntityPropertiesTable/EntityPropertiesTable.astro +20 -13
- package/eventcatalog/src/components/MDX/Link/Link.astro +1 -1
- package/eventcatalog/src/components/MDX/MessageTable/MessageTable.client.tsx +50 -29
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +14 -12
- package/eventcatalog/src/components/MDX/NodeGraph/StepWalkthrough.tsx +4 -4
- package/eventcatalog/src/components/MDX/NodeGraph/StudioModal.tsx +4 -4
- package/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx +2 -2
- package/eventcatalog/src/components/MDX/ResourceGroupTable/ResourceGroupTable.client.tsx +54 -33
- package/eventcatalog/src/components/MDX/ResourceLink/ResourceLink.astro +1 -1
- package/eventcatalog/src/components/MDX/Steps/Step.astro +2 -2
- package/eventcatalog/src/components/MDX/Steps/Steps.astro +3 -3
- package/eventcatalog/src/components/MDX/Tabs/Tabs.astro +13 -9
- package/eventcatalog/src/components/MDX/Tiles/Tile.astro +25 -13
- package/eventcatalog/src/components/MDX/Tiles/Tiles.astro +1 -1
- package/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx +3 -3
- package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +3 -3
- package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +29 -25
- package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +3 -3
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +61 -42
- package/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx +13 -9
- package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +6 -4
- package/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx +17 -13
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +35 -8
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +33 -21
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +1 -1
- package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +41 -33
- package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +19 -7
- package/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +8 -8
- package/eventcatalog/src/components/Search/Search.astro +3 -3
- package/eventcatalog/src/components/Search/SearchModal.tsx +65 -36
- package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +31 -21
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +92 -59
- package/eventcatalog/src/components/Tables/Table.tsx +25 -24
- package/eventcatalog/src/components/Tables/columns/ContainersTableColumns.tsx +22 -16
- package/eventcatalog/src/components/Tables/columns/DomainTableColumns.tsx +14 -11
- package/eventcatalog/src/components/Tables/columns/FlowTableColumns.tsx +6 -6
- package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +22 -16
- package/eventcatalog/src/components/Tables/columns/ServiceTableColumns.tsx +22 -16
- package/eventcatalog/src/components/Tables/columns/SharedColumns.tsx +4 -4
- package/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx +21 -13
- package/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx +30 -19
- package/eventcatalog/src/components/ThemeToggle.tsx +18 -0
- package/eventcatalog/src/enterprise/ai/chat-api.ts +24 -3
- package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NestedItem.tsx +15 -7
- package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NoResultsFound.tsx +2 -2
- package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx +19 -15
- package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +50 -17
- package/eventcatalog/src/layouts/DirectoryLayout.astro +11 -6
- package/eventcatalog/src/layouts/DiscoverLayout.astro +13 -8
- package/eventcatalog/src/layouts/Footer.astro +6 -6
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +62 -14
- package/eventcatalog/src/pages/_index.astro +135 -179
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +2 -2
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +4 -4
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +77 -63
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +23 -24
- package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +66 -50
- package/eventcatalog/src/pages/docs/custom/index.astro +2 -2
- package/eventcatalog/src/pages/docs/teams/[id]/index.astro +25 -21
- package/eventcatalog/src/pages/docs/users/[id]/index.astro +25 -21
- package/eventcatalog/src/pages/schemas/explorer/index.astro +1 -1
- package/eventcatalog/src/pages/studio.astro +59 -31
- package/eventcatalog/src/remark-plugins/directives.ts +6 -6
- package/eventcatalog/src/remark-plugins/mermaid.ts +2 -2
- package/eventcatalog/src/stores/theme-store.ts +93 -0
- package/eventcatalog/src/styles/theme.css +255 -0
- package/eventcatalog/src/styles/themes/forest.css +230 -0
- package/eventcatalog/src/styles/themes/ocean.css +235 -0
- package/eventcatalog/src/styles/themes/sapphire.css +230 -0
- package/eventcatalog/src/styles/themes/sunset.css +230 -0
- package/eventcatalog/src/utils/feature.ts +4 -0
- package/eventcatalog/tailwind.config.mjs +6 -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);
|
|
241
298
|
};
|
|
242
299
|
|
|
243
|
-
//
|
|
244
|
-
const
|
|
300
|
+
// Helper to extract follow-up suggestions from message parts
|
|
301
|
+
const getFollowUpSuggestions = (message: { parts?: Array<any> }): string[] => {
|
|
302
|
+
if (!message.parts) return [];
|
|
303
|
+
|
|
304
|
+
for (const part of message.parts) {
|
|
305
|
+
// AI SDK format: type is "tool-{toolName}" and result is in "output"
|
|
306
|
+
if (part.type === 'tool-suggestFollowUpQuestions' && part.state === 'output-available') {
|
|
307
|
+
const suggestions = part.output?.suggestions;
|
|
308
|
+
if (suggestions && Array.isArray(suggestions)) {
|
|
309
|
+
return suggestions;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return [];
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Helper to extract currently running tools from message parts
|
|
317
|
+
const getRunningTools = (message: { parts?: Array<any> }): string[] => {
|
|
318
|
+
if (!message.parts) return [];
|
|
319
|
+
|
|
320
|
+
const runningTools: string[] = [];
|
|
321
|
+
for (const part of message.parts) {
|
|
322
|
+
// Tool parts have type like "tool-{toolName}" and state indicates progress
|
|
323
|
+
if (part.type?.startsWith('tool-') && part.state !== 'output-available') {
|
|
324
|
+
// Extract tool name from type (e.g., "tool-getServiceHealth" -> "getServiceHealth")
|
|
325
|
+
const toolName = part.type.replace('tool-', '');
|
|
326
|
+
// Skip the follow-up suggestions tool as it's internal
|
|
327
|
+
if (toolName !== 'suggestFollowUpQuestions') {
|
|
328
|
+
runningTools.push(toolName);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return runningTools;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// Helper to extract completed tools from message parts (for showing after completion)
|
|
336
|
+
const getCompletedTools = (message: { parts?: Array<any> }): string[] => {
|
|
337
|
+
if (!message.parts) return [];
|
|
338
|
+
|
|
339
|
+
const completedTools: string[] = [];
|
|
340
|
+
for (const part of message.parts) {
|
|
341
|
+
// Tool parts have type like "tool-{toolName}" and state 'output-available' when done
|
|
342
|
+
if (part.type?.startsWith('tool-') && part.state === 'output-available') {
|
|
343
|
+
const toolName = part.type.replace('tool-', '');
|
|
344
|
+
// Skip internal tools
|
|
345
|
+
if (toolName !== 'suggestFollowUpQuestions') {
|
|
346
|
+
completedTools.push(toolName);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return completedTools;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// Skeleton loading component - memoized since it never changes
|
|
354
|
+
const SkeletonLoader = memo(() => (
|
|
245
355
|
<div className="animate-pulse space-y-3">
|
|
246
|
-
<div className="h-4 bg-
|
|
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]);
|
|
295
480
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
481
|
+
// Memoize page context to avoid recalculating on every render
|
|
482
|
+
const pageContext = useMemo(() => {
|
|
483
|
+
const match = pathname.match(
|
|
484
|
+
/^\/(docs|visualiser|architecture)\/(events|services|commands|queries|flows|domains|channels|entities|containers)\/([^/]+)(?:\/([^/]+))?/
|
|
485
|
+
);
|
|
486
|
+
if (match) {
|
|
487
|
+
const [, , collection, id, version] = match;
|
|
488
|
+
const collectionNames: Record<string, string> = {
|
|
489
|
+
events: 'Event',
|
|
490
|
+
services: 'Service',
|
|
491
|
+
commands: 'Command',
|
|
492
|
+
queries: 'Query',
|
|
493
|
+
flows: 'Flow',
|
|
494
|
+
domains: 'Domain',
|
|
495
|
+
channels: 'Channel',
|
|
496
|
+
entities: 'Entity',
|
|
497
|
+
containers: 'Container',
|
|
498
|
+
};
|
|
499
|
+
return {
|
|
500
|
+
type: collectionNames[collection] || collection,
|
|
501
|
+
name: id,
|
|
502
|
+
version: version || 'latest',
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}, [pathname]);
|
|
507
|
+
|
|
508
|
+
// Handle scroll to detect if user scrolled up
|
|
509
|
+
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
|
510
|
+
const target = e.target as HTMLDivElement;
|
|
511
|
+
const isNearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 100;
|
|
512
|
+
setShowScrollButton(!isNearBottom);
|
|
513
|
+
}, []);
|
|
514
|
+
|
|
515
|
+
const { messages, sendMessage, stop, status, setMessages, error } = useChat();
|
|
299
516
|
|
|
300
517
|
// Extract user-friendly error message
|
|
301
518
|
const errorMessage = error?.message || 'Something went wrong. Please try again.';
|
|
302
519
|
|
|
520
|
+
// Memoize last assistant message to avoid array operations on every render
|
|
521
|
+
const lastAssistantMessage = useMemo(() => messages.findLast((m) => m.role === 'assistant'), [messages]);
|
|
522
|
+
|
|
303
523
|
// Check if the assistant has started outputting content
|
|
304
|
-
const
|
|
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-gray-900 text-sm">EventCatalog Assistant</span>
|
|
698
|
+
<BookOpen size={16} className="text-[rgb(var(--ec-accent))]" />
|
|
699
|
+
<span className="font-medium text-[rgb(var(--ec-header-text))] text-sm">EventCatalog Assistant</span>
|
|
494
700
|
</div>
|
|
495
701
|
<div className="flex items-center space-x-1">
|
|
496
702
|
{tools.length > 0 && (
|
|
497
703
|
<Popover.Root>
|
|
498
704
|
<Popover.Trigger asChild>
|
|
499
705
|
<button
|
|
500
|
-
className="p-2 rounded-lg hover:bg-
|
|
706
|
+
className="p-2 rounded-lg hover:bg-[rgb(var(--ec-header-border))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-header-text))] transition-colors"
|
|
501
707
|
aria-label="View available tools"
|
|
502
708
|
title="Available tools"
|
|
503
709
|
>
|
|
@@ -506,34 +712,36 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
506
712
|
</Popover.Trigger>
|
|
507
713
|
<Popover.Portal>
|
|
508
714
|
<Popover.Content
|
|
509
|
-
className="w-72 bg-
|
|
715
|
+
className="w-72 bg-[rgb(var(--ec-dropdown-bg))] rounded-lg shadow-lg border border-[rgb(var(--ec-dropdown-border))] p-3 z-[250]"
|
|
510
716
|
sideOffset={5}
|
|
511
717
|
align="end"
|
|
512
718
|
>
|
|
513
|
-
<div className="text-[10px] font-medium text-
|
|
514
|
-
|
|
719
|
+
<div className="text-[10px] font-medium text-[rgb(var(--ec-page-text-muted))] mb-2">
|
|
720
|
+
Available Tools ({sortedTools.length})
|
|
721
|
+
</div>
|
|
722
|
+
<div className="max-h-64 overflow-y-auto divide-y divide-[rgb(var(--ec-page-border))]">
|
|
515
723
|
{sortedTools.map((tool) => (
|
|
516
724
|
<div key={tool.name} className="py-1.5 first:pt-0 last:pb-0">
|
|
517
725
|
<div className="flex items-center gap-1.5">
|
|
518
|
-
<span className="text-[11px] font-medium text-
|
|
726
|
+
<span className="text-[11px] font-medium text-[rgb(var(--ec-dropdown-text))]">{tool.name}</span>
|
|
519
727
|
{tool.isCustom && (
|
|
520
|
-
<span className="px-1.5 py-0.5 text-[9px] font-medium bg-
|
|
728
|
+
<span className="px-1.5 py-0.5 text-[9px] font-medium bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent-text))] rounded">
|
|
521
729
|
Custom
|
|
522
730
|
</span>
|
|
523
731
|
)}
|
|
524
732
|
</div>
|
|
525
|
-
<div className="text-[10px] text-
|
|
733
|
+
<div className="text-[10px] text-[rgb(var(--ec-page-text-muted))] mt-0.5">{tool.description}</div>
|
|
526
734
|
</div>
|
|
527
735
|
))}
|
|
528
736
|
</div>
|
|
529
|
-
<Popover.Arrow className="fill-
|
|
737
|
+
<Popover.Arrow className="fill-[rgb(var(--ec-dropdown-bg))]" />
|
|
530
738
|
</Popover.Content>
|
|
531
739
|
</Popover.Portal>
|
|
532
740
|
</Popover.Root>
|
|
533
741
|
)}
|
|
534
742
|
<button
|
|
535
743
|
onClick={() => setIsFullscreen(true)}
|
|
536
|
-
className="p-2 rounded-lg hover:bg-
|
|
744
|
+
className="p-2 rounded-lg hover:bg-[rgb(var(--ec-header-border))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-header-text))] transition-colors"
|
|
537
745
|
aria-label="Expand to fullscreen"
|
|
538
746
|
title="Expand"
|
|
539
747
|
>
|
|
@@ -542,7 +750,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
542
750
|
{hasMessages && (
|
|
543
751
|
<button
|
|
544
752
|
onClick={() => setMessages([])}
|
|
545
|
-
className="p-2 rounded-lg hover:bg-
|
|
753
|
+
className="p-2 rounded-lg hover:bg-[rgb(var(--ec-header-border))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-header-text))] transition-colors"
|
|
546
754
|
aria-label="Clear chat"
|
|
547
755
|
title="Clear chat"
|
|
548
756
|
>
|
|
@@ -551,7 +759,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
551
759
|
)}
|
|
552
760
|
<button
|
|
553
761
|
onClick={onClose}
|
|
554
|
-
className="p-2 rounded-lg hover:bg-
|
|
762
|
+
className="p-2 rounded-lg hover:bg-[rgb(var(--ec-header-border))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-header-text))] transition-colors"
|
|
555
763
|
aria-label="Close chat panel"
|
|
556
764
|
>
|
|
557
765
|
<X size={18} />
|
|
@@ -561,8 +769,16 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
561
769
|
{/* Thinking indicator */}
|
|
562
770
|
{isThinking && (
|
|
563
771
|
<div className="px-4 pb-2 flex items-center gap-2">
|
|
564
|
-
<div className="w-1.5 h-1.5 bg-
|
|
565
|
-
<span className="text-xs text-
|
|
772
|
+
<div className="w-1.5 h-1.5 bg-[rgb(var(--ec-accent))] rounded-full animate-pulse" />
|
|
773
|
+
<span className="text-xs text-[rgb(var(--ec-icon-color))]">
|
|
774
|
+
{runningTools.length > 0 ? (
|
|
775
|
+
<>
|
|
776
|
+
Using <span className="font-medium text-[rgb(var(--ec-accent))]">{runningTools[0]}</span>...
|
|
777
|
+
</>
|
|
778
|
+
) : (
|
|
779
|
+
'Thinking...'
|
|
780
|
+
)}
|
|
781
|
+
</span>
|
|
566
782
|
</div>
|
|
567
783
|
)}
|
|
568
784
|
</div>
|
|
@@ -570,40 +786,34 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
570
786
|
{/* Content */}
|
|
571
787
|
<div className="flex-1 flex flex-col min-h-0 relative overflow-hidden" key={isOpen ? 'content-open' : 'content-closed'}>
|
|
572
788
|
{/* Messages or Welcome area */}
|
|
573
|
-
<div className="flex-1 overflow-y-auto px-
|
|
789
|
+
<div ref={messagesContainerRef} onScroll={handleScroll} className="flex-1 overflow-y-auto px-5 scrollbar-hide">
|
|
574
790
|
{!hasMessages ? (
|
|
575
|
-
/* Welcome area */
|
|
576
|
-
<div className="flex flex-col h-full
|
|
577
|
-
{/*
|
|
791
|
+
/* Welcome area - Clean GitBook-inspired design */
|
|
792
|
+
<div className="flex flex-col h-full py-6">
|
|
793
|
+
{/* Greeting section - centered */}
|
|
578
794
|
<div
|
|
579
|
-
className="flex-1 flex flex-col items-center justify-center"
|
|
795
|
+
className="flex-1 flex flex-col items-center justify-center text-center"
|
|
580
796
|
style={isOpen ? fadeInStyles.welcome : undefined}
|
|
581
797
|
>
|
|
582
|
-
{/*
|
|
583
|
-
<div className="relative mb-
|
|
584
|
-
<div className="
|
|
585
|
-
|
|
586
|
-
<BookOpen size={26} className="text-white" strokeWidth={1.5} />
|
|
587
|
-
<Sparkles size={10} className="text-purple-200 absolute -top-1 -right-1 animate-pulse" />
|
|
798
|
+
{/* Icon with circular background */}
|
|
799
|
+
<div className="relative mb-6">
|
|
800
|
+
<div className="w-32 h-32 rounded-full bg-[rgb(var(--ec-accent)/0.15)] flex items-center justify-center">
|
|
801
|
+
<MessageSquare size={56} className="text-[rgb(var(--ec-accent))]" strokeWidth={1.5} />
|
|
588
802
|
</div>
|
|
589
803
|
</div>
|
|
590
|
-
<h2 className="text-
|
|
591
|
-
<p className="text-sm text-
|
|
804
|
+
<h2 className="text-lg font-semibold text-[rgb(var(--ec-accent))] mb-1">{greeting}</h2>
|
|
805
|
+
<p className="text-sm font-normal text-[rgb(var(--ec-content-text))]">
|
|
806
|
+
I'm here to help with your architecture
|
|
807
|
+
</p>
|
|
592
808
|
</div>
|
|
593
809
|
|
|
594
|
-
{/* Suggested questions */}
|
|
595
|
-
<div className="space-y-
|
|
596
|
-
<p
|
|
597
|
-
className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2"
|
|
598
|
-
style={isOpen ? fadeInStyles.questionsLabel : undefined}
|
|
599
|
-
>
|
|
600
|
-
Example questions
|
|
601
|
-
</p>
|
|
810
|
+
{/* Suggested questions - pill style */}
|
|
811
|
+
<div className="flex-none space-y-2">
|
|
602
812
|
{suggestedQuestions.map((question, index) => (
|
|
603
813
|
<button
|
|
604
814
|
key={index}
|
|
605
815
|
onClick={() => handleSuggestedAction(question.prompt)}
|
|
606
|
-
className="w-full text-left px-
|
|
816
|
+
className="w-full text-left px-4 py-2.5 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-page-text)/0.05)] hover:bg-[rgb(var(--ec-accent)/0.15)] rounded-full transition-all duration-200"
|
|
607
817
|
style={isOpen ? fadeInStyles.getQuestionStyle(index) : undefined}
|
|
608
818
|
>
|
|
609
819
|
{question.label}
|
|
@@ -614,50 +824,58 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
614
824
|
) : (
|
|
615
825
|
/* Messages area */
|
|
616
826
|
<div className="py-4 space-y-4">
|
|
617
|
-
{messages.map((message) => {
|
|
827
|
+
{messages.map((message, messageIndex) => {
|
|
618
828
|
const content = getMessageContent(message);
|
|
829
|
+
const followUpSuggestions = message.role === 'assistant' ? getFollowUpSuggestions(message) : [];
|
|
830
|
+
const completedTools = message.role === 'assistant' ? getCompletedTools(message) : [];
|
|
831
|
+
const isLastMessage = messageIndex === messages.length - 1;
|
|
619
832
|
return (
|
|
620
|
-
<div key={message.id} className={`flex ${message.role === 'user' ? '
|
|
833
|
+
<div key={message.id} className={`flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'}`}>
|
|
621
834
|
{message.role === 'user' ? (
|
|
622
|
-
<div className="max-w-[85%] rounded-2xl rounded-br-md px-4 py-2.5 bg-
|
|
623
|
-
<p className="text-sm whitespace-pre-wrap">{content}</p>
|
|
835
|
+
<div className="max-w-[85%] rounded-2xl rounded-br-md px-4 py-2.5 bg-[rgb(var(--ec-page-text)/0.05)]">
|
|
836
|
+
<p className="text-sm font-normal whitespace-pre-wrap text-[rgb(var(--ec-page-text))]">{content}</p>
|
|
624
837
|
</div>
|
|
625
838
|
) : (
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
className="
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
{children}
|
|
650
|
-
</code>
|
|
651
|
-
) : (
|
|
652
|
-
<CodeBlock language={language}>{codeString}</CodeBlock>
|
|
653
|
-
);
|
|
654
|
-
},
|
|
655
|
-
}}
|
|
656
|
-
>
|
|
657
|
-
{content}
|
|
658
|
-
</ReactMarkdown>
|
|
839
|
+
<>
|
|
840
|
+
{/* Tools used indicator */}
|
|
841
|
+
{completedTools.length > 0 && (
|
|
842
|
+
<div className="flex items-center gap-1.5 mb-2">
|
|
843
|
+
<Wrench size={10} className="text-[rgb(var(--ec-icon-color))]" />
|
|
844
|
+
<span className="text-[10px] text-[rgb(var(--ec-icon-color))]">
|
|
845
|
+
Used{' '}
|
|
846
|
+
{completedTools.slice(0, 2).map((tool, i) => (
|
|
847
|
+
<span key={tool}>
|
|
848
|
+
<span className="font-medium text-[rgb(var(--ec-accent))]">{tool}</span>
|
|
849
|
+
{i < Math.min(completedTools.length, 2) - 1 && ', '}
|
|
850
|
+
</span>
|
|
851
|
+
))}
|
|
852
|
+
{completedTools.length > 2 && <span> +{completedTools.length - 2} more</span>}
|
|
853
|
+
</span>
|
|
854
|
+
</div>
|
|
855
|
+
)}
|
|
856
|
+
<div className="w-full text-[rgb(var(--ec-content-text))]">
|
|
857
|
+
<div className="prose prose-sm max-w-none prose-p:my-2 prose-p:font-normal prose-p:text-[13px] prose-p:text-[rgb(var(--ec-content-text))] prose-headings:my-2 prose-headings:font-semibold prose-headings:text-[rgb(var(--ec-page-text))] prose-h1:text-base prose-h2:text-sm prose-h3:text-[13px] prose-h4:text-[13px] prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5 prose-li:text-[13px] prose-li:font-normal prose-li:text-[rgb(var(--ec-content-text))] text-[13px] font-light">
|
|
858
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
859
|
+
{content}
|
|
860
|
+
</ReactMarkdown>
|
|
861
|
+
</div>
|
|
659
862
|
</div>
|
|
660
|
-
|
|
863
|
+
{/* Follow-up suggestions - only show for last assistant message when not loading */}
|
|
864
|
+
{isLastMessage && followUpSuggestions.length > 0 && !isLoading && (
|
|
865
|
+
<div className="flex flex-wrap gap-2 mt-3 w-full">
|
|
866
|
+
{followUpSuggestions.map((suggestion, index) => (
|
|
867
|
+
<button
|
|
868
|
+
key={index}
|
|
869
|
+
onClick={() => handleSuggestedAction(suggestion)}
|
|
870
|
+
className="px-4 py-2.5 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-page-text)/0.05)] hover:bg-[rgb(var(--ec-accent)/0.15)] rounded-full transition-all duration-200 text-left"
|
|
871
|
+
style={fadeInStyles.getFollowUpStyle(index)}
|
|
872
|
+
>
|
|
873
|
+
{suggestion}
|
|
874
|
+
</button>
|
|
875
|
+
))}
|
|
876
|
+
</div>
|
|
877
|
+
)}
|
|
878
|
+
</>
|
|
661
879
|
)}
|
|
662
880
|
</div>
|
|
663
881
|
);
|
|
@@ -687,40 +905,46 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
687
905
|
)}
|
|
688
906
|
</div>
|
|
689
907
|
|
|
908
|
+
{/* Scroll to bottom button */}
|
|
909
|
+
{hasMessages && showScrollButton && (
|
|
910
|
+
<button
|
|
911
|
+
onClick={scrollToBottom}
|
|
912
|
+
className="absolute bottom-24 right-4 flex items-center gap-1.5 px-3 py-1.5 bg-[rgb(var(--ec-card-bg))] text-[rgb(var(--ec-page-text-muted))] text-xs font-medium rounded-full shadow-lg border border-[rgb(var(--ec-page-border))] hover:bg-[rgb(var(--ec-content-hover))] transition-all z-10"
|
|
913
|
+
>
|
|
914
|
+
<ChevronDown size={14} />
|
|
915
|
+
<span>Scroll to bottom</span>
|
|
916
|
+
</button>
|
|
917
|
+
)}
|
|
918
|
+
|
|
690
919
|
{/* Input area (Fixed at bottom) */}
|
|
691
|
-
<div className="flex-none px-4 py-3
|
|
920
|
+
<div className="flex-none px-4 py-3" key={isOpen ? 'input-open' : 'input-closed'}>
|
|
692
921
|
<form onSubmit={handleSubmit}>
|
|
693
|
-
<div className="relative bg-
|
|
922
|
+
<div className="relative bg-[rgb(var(--ec-page-bg)/0.5)] backdrop-blur-sm rounded-xl border border-[rgb(var(--ec-accent)/0.3)] focus-within:border-[rgb(var(--ec-accent)/0.5)] focus-within:ring-2 focus-within:ring-[rgb(var(--ec-accent)/0.1)] transition-all">
|
|
694
923
|
<input
|
|
695
924
|
ref={inputRef}
|
|
696
925
|
type="text"
|
|
697
926
|
value={inputValue}
|
|
698
927
|
onChange={(e) => setInputValue(e.target.value)}
|
|
699
|
-
onKeyDown={
|
|
700
|
-
|
|
701
|
-
e.preventDefault();
|
|
702
|
-
submitMessage(inputValue);
|
|
703
|
-
}
|
|
704
|
-
}}
|
|
705
|
-
placeholder="Ask a question..."
|
|
928
|
+
onKeyDown={handleInputKeyDown}
|
|
929
|
+
placeholder="Ask, search, or explain..."
|
|
706
930
|
disabled={isLoading}
|
|
707
|
-
className="w-full px-
|
|
931
|
+
className="w-full px-4 py-3 pr-16 bg-transparent text-[rgb(var(--ec-input-text))] placeholder-[rgb(var(--ec-input-placeholder))] focus:outline-none text-sm disabled:opacity-50 rounded-xl"
|
|
708
932
|
/>
|
|
709
|
-
<div className="absolute right-
|
|
933
|
+
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
|
|
710
934
|
{isStreaming ? (
|
|
711
935
|
<button
|
|
712
936
|
type="button"
|
|
713
937
|
onClick={() => stop()}
|
|
714
|
-
className="p-
|
|
938
|
+
className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
|
|
715
939
|
aria-label="Stop generating"
|
|
716
940
|
>
|
|
717
|
-
<Square size={
|
|
941
|
+
<Square size={14} fill="currentColor" />
|
|
718
942
|
</button>
|
|
719
943
|
) : (
|
|
720
944
|
<button
|
|
721
945
|
type="submit"
|
|
722
946
|
disabled={!inputValue.trim() || isLoading}
|
|
723
|
-
className="px-
|
|
947
|
+
className="px-3 py-1.5 bg-[rgb(var(--ec-accent))] text-white text-xs font-medium rounded-lg hover:bg-[rgb(var(--ec-accent-hover))] disabled:bg-transparent disabled:text-[rgb(var(--ec-icon-color))] transition-colors"
|
|
724
948
|
aria-label="Send message"
|
|
725
949
|
>
|
|
726
950
|
Send
|
|
@@ -729,7 +953,19 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
729
953
|
</div>
|
|
730
954
|
</div>
|
|
731
955
|
</form>
|
|
732
|
-
|
|
956
|
+
{/* Context indicator */}
|
|
957
|
+
<div className="flex items-center justify-center gap-1.5 mt-2">
|
|
958
|
+
{pageContext ? (
|
|
959
|
+
<span className="text-[10px] text-[rgb(var(--ec-icon-color))] flex items-center gap-1">
|
|
960
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[rgb(var(--ec-accent))]" />
|
|
961
|
+
Based on {pageContext.type}: {pageContext.name}
|
|
962
|
+
</span>
|
|
963
|
+
) : (
|
|
964
|
+
<span className="text-[10px] text-[rgb(var(--ec-icon-color))]">
|
|
965
|
+
AI can make mistakes. Verify important info.
|
|
966
|
+
</span>
|
|
967
|
+
)}
|
|
968
|
+
</div>
|
|
733
969
|
</div>
|
|
734
970
|
</div>
|
|
735
971
|
</div>
|
|
@@ -747,22 +983,30 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
747
983
|
}}
|
|
748
984
|
>
|
|
749
985
|
<Dialog.Portal>
|
|
750
|
-
<Dialog.Overlay className="fixed inset-0 bg-
|
|
751
|
-
<Dialog.Content
|
|
986
|
+
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[300]" />
|
|
987
|
+
<Dialog.Content
|
|
988
|
+
className="fixed inset-y-4 left-1/2 -translate-x-1/2 w-[95%] max-w-4xl md:inset-y-8 rounded-xl shadow-2xl z-[301] flex flex-col overflow-hidden focus:outline-none border border-[rgb(var(--ec-page-border))]"
|
|
989
|
+
style={{
|
|
990
|
+
background: `
|
|
991
|
+
radial-gradient(ellipse 100% 40% at 50% 100%, rgb(var(--ec-accent) / 0.15) 0%, transparent 100%),
|
|
992
|
+
rgb(var(--ec-page-bg))
|
|
993
|
+
`,
|
|
994
|
+
}}
|
|
995
|
+
>
|
|
752
996
|
{/* Modal Header */}
|
|
753
|
-
<div className="flex items-center justify-between px-5 py-3 border-b border-
|
|
997
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-[rgb(var(--ec-page-border))] flex-shrink-0">
|
|
754
998
|
<div className="flex items-center space-x-2.5">
|
|
755
|
-
<div className="p-1.5 bg-
|
|
756
|
-
<BookOpen size={18} className="text-
|
|
999
|
+
<div className="p-1.5 bg-[rgb(var(--ec-accent-subtle))] rounded-lg">
|
|
1000
|
+
<BookOpen size={18} className="text-[rgb(var(--ec-accent))]" />
|
|
757
1001
|
</div>
|
|
758
|
-
<Dialog.Title className="text-base font-medium text-
|
|
1002
|
+
<Dialog.Title className="text-base font-medium text-[rgb(var(--ec-page-text))]">Ask AI</Dialog.Title>
|
|
759
1003
|
</div>
|
|
760
1004
|
<div className="flex items-center space-x-2">
|
|
761
1005
|
{tools.length > 0 && (
|
|
762
1006
|
<Popover.Root>
|
|
763
1007
|
<Popover.Trigger asChild>
|
|
764
1008
|
<button
|
|
765
|
-
className="p-2 rounded-lg hover:bg-
|
|
1009
|
+
className="p-2 rounded-lg hover:bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] transition-colors"
|
|
766
1010
|
aria-label="View available tools"
|
|
767
1011
|
title="Available tools"
|
|
768
1012
|
>
|
|
@@ -771,28 +1015,33 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
771
1015
|
</Popover.Trigger>
|
|
772
1016
|
<Popover.Portal>
|
|
773
1017
|
<Popover.Content
|
|
774
|
-
className="w-80 bg-
|
|
1018
|
+
className="w-80 bg-[rgb(var(--ec-dropdown-bg))] rounded-lg shadow-lg border border-[rgb(var(--ec-dropdown-border))] p-4 z-[350]"
|
|
775
1019
|
sideOffset={5}
|
|
776
1020
|
align="end"
|
|
777
1021
|
style={{ maxHeight: 'calc(100vh - 200px)' }}
|
|
778
1022
|
>
|
|
779
|
-
<div className="text-[11px] font-medium text-
|
|
780
|
-
|
|
1023
|
+
<div className="text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] mb-2">
|
|
1024
|
+
Available Tools ({sortedTools.length})
|
|
1025
|
+
</div>
|
|
1026
|
+
<div
|
|
1027
|
+
className="overflow-y-auto divide-y divide-[rgb(var(--ec-page-border))]"
|
|
1028
|
+
style={{ maxHeight: 'calc(100vh - 280px)' }}
|
|
1029
|
+
>
|
|
781
1030
|
{sortedTools.map((tool) => (
|
|
782
1031
|
<div key={tool.name} className="py-2 first:pt-0 last:pb-0">
|
|
783
1032
|
<div className="flex items-center gap-1.5">
|
|
784
|
-
<span className="text-xs font-medium text-
|
|
1033
|
+
<span className="text-xs font-medium text-[rgb(var(--ec-dropdown-text))]">{tool.name}</span>
|
|
785
1034
|
{tool.isCustom && (
|
|
786
|
-
<span className="px-1.5 py-0.5 text-[9px] font-medium bg-
|
|
1035
|
+
<span className="px-1.5 py-0.5 text-[9px] font-medium bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent-text))] rounded">
|
|
787
1036
|
Custom
|
|
788
1037
|
</span>
|
|
789
1038
|
)}
|
|
790
1039
|
</div>
|
|
791
|
-
<div className="text-[11px] text-
|
|
1040
|
+
<div className="text-[11px] text-[rgb(var(--ec-page-text-muted))] mt-0.5">{tool.description}</div>
|
|
792
1041
|
</div>
|
|
793
1042
|
))}
|
|
794
1043
|
</div>
|
|
795
|
-
<Popover.Arrow className="fill-
|
|
1044
|
+
<Popover.Arrow className="fill-[rgb(var(--ec-dropdown-bg))]" />
|
|
796
1045
|
</Popover.Content>
|
|
797
1046
|
</Popover.Portal>
|
|
798
1047
|
</Popover.Root>
|
|
@@ -800,7 +1049,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
800
1049
|
{hasMessages && (
|
|
801
1050
|
<button
|
|
802
1051
|
onClick={() => setMessages([])}
|
|
803
|
-
className="p-2 rounded-lg hover:bg-
|
|
1052
|
+
className="p-2 rounded-lg hover:bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] transition-colors"
|
|
804
1053
|
aria-label="Clear chat"
|
|
805
1054
|
title="Clear chat"
|
|
806
1055
|
>
|
|
@@ -809,7 +1058,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
809
1058
|
)}
|
|
810
1059
|
<button
|
|
811
1060
|
onClick={() => setIsFullscreen(false)}
|
|
812
|
-
className="p-2 rounded-lg hover:bg-
|
|
1061
|
+
className="p-2 rounded-lg hover:bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] transition-colors"
|
|
813
1062
|
aria-label="Exit fullscreen"
|
|
814
1063
|
title="Exit fullscreen"
|
|
815
1064
|
>
|
|
@@ -820,7 +1069,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
820
1069
|
setIsFullscreen(false);
|
|
821
1070
|
onClose();
|
|
822
1071
|
}}
|
|
823
|
-
className="p-2 rounded-lg hover:bg-
|
|
1072
|
+
className="p-2 rounded-lg hover:bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] transition-colors"
|
|
824
1073
|
aria-label="Close"
|
|
825
1074
|
>
|
|
826
1075
|
<X size={18} />
|
|
@@ -830,35 +1079,46 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
830
1079
|
|
|
831
1080
|
{/* Thinking indicator */}
|
|
832
1081
|
{isThinking && (
|
|
833
|
-
<div className="px-5 py-2 flex items-center gap-2 border-b border-
|
|
834
|
-
<div className="w-1.5 h-1.5 bg-
|
|
835
|
-
<span className="text-sm text-
|
|
1082
|
+
<div className="px-5 py-2 flex items-center gap-2 border-b border-[rgb(var(--ec-page-border))]">
|
|
1083
|
+
<div className="w-1.5 h-1.5 bg-[rgb(var(--ec-accent))] rounded-full animate-pulse" />
|
|
1084
|
+
<span className="text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
1085
|
+
{runningTools.length > 0 ? (
|
|
1086
|
+
<>
|
|
1087
|
+
Using <span className="font-medium text-[rgb(var(--ec-accent))]">{runningTools[0]}</span>...
|
|
1088
|
+
</>
|
|
1089
|
+
) : (
|
|
1090
|
+
'Thinking...'
|
|
1091
|
+
)}
|
|
1092
|
+
</span>
|
|
836
1093
|
</div>
|
|
837
1094
|
)}
|
|
838
1095
|
|
|
839
1096
|
{/* Modal Content */}
|
|
840
|
-
<div className="flex-1 overflow-y-auto px-
|
|
1097
|
+
<div className="flex-1 overflow-y-auto px-6 py-6">
|
|
841
1098
|
{!hasMessages ? (
|
|
842
|
-
/* Welcome area */
|
|
843
|
-
<div className="flex flex-col h-full
|
|
844
|
-
{/*
|
|
845
|
-
<div className="
|
|
846
|
-
|
|
847
|
-
<div className="relative
|
|
848
|
-
<
|
|
849
|
-
|
|
1099
|
+
/* Welcome area - Clean design */
|
|
1100
|
+
<div className="flex flex-col h-full max-w-2xl mx-auto">
|
|
1101
|
+
{/* Greeting section - centered */}
|
|
1102
|
+
<div className="flex-1 flex flex-col justify-center items-center text-center">
|
|
1103
|
+
{/* Icon with circular background */}
|
|
1104
|
+
<div className="relative mb-8">
|
|
1105
|
+
<div className="w-40 h-40 rounded-full bg-[rgb(var(--ec-accent)/0.15)] flex items-center justify-center">
|
|
1106
|
+
<MessageSquare size={72} className="text-[rgb(var(--ec-accent))]" strokeWidth={1.5} />
|
|
1107
|
+
</div>
|
|
850
1108
|
</div>
|
|
1109
|
+
<h2 className="text-2xl font-semibold text-[rgb(var(--ec-accent))] mb-2">{greeting}</h2>
|
|
1110
|
+
<p className="font-normal text-[rgb(var(--ec-content-text))] text-center">
|
|
1111
|
+
I'm here to help with your architecture
|
|
1112
|
+
</p>
|
|
851
1113
|
</div>
|
|
852
|
-
<h2 className="text-xl font-medium text-gray-900 mb-1">{getGreeting()}</h2>
|
|
853
|
-
<p className="text-gray-500 text-center mb-8">Ask me anything about your catalog.</p>
|
|
854
1114
|
|
|
855
|
-
{/* Suggested questions */}
|
|
856
|
-
<div className="grid grid-cols-2 gap-2
|
|
1115
|
+
{/* Suggested questions - pill style */}
|
|
1116
|
+
<div className="flex-none grid grid-cols-2 gap-2">
|
|
857
1117
|
{suggestedQuestions.map((question, index) => (
|
|
858
1118
|
<button
|
|
859
1119
|
key={index}
|
|
860
1120
|
onClick={() => handleSuggestedAction(question.prompt)}
|
|
861
|
-
className="px-4 py-2.5 text-
|
|
1121
|
+
className="px-4 py-2.5 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-page-text)/0.05)] hover:bg-[rgb(var(--ec-accent)/0.15)] rounded-full transition-all duration-200 text-left"
|
|
862
1122
|
>
|
|
863
1123
|
{question.label}
|
|
864
1124
|
</button>
|
|
@@ -868,50 +1128,58 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
868
1128
|
) : (
|
|
869
1129
|
/* Messages area */
|
|
870
1130
|
<div className="max-w-3xl mx-auto space-y-4">
|
|
871
|
-
{messages.map((message) => {
|
|
1131
|
+
{messages.map((message, messageIndex) => {
|
|
872
1132
|
const content = getMessageContent(message);
|
|
1133
|
+
const followUpSuggestions = message.role === 'assistant' ? getFollowUpSuggestions(message) : [];
|
|
1134
|
+
const completedTools = message.role === 'assistant' ? getCompletedTools(message) : [];
|
|
1135
|
+
const isLastMessage = messageIndex === messages.length - 1;
|
|
873
1136
|
return (
|
|
874
|
-
<div key={message.id} className={`flex ${message.role === 'user' ? '
|
|
1137
|
+
<div key={message.id} className={`flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'}`}>
|
|
875
1138
|
{message.role === 'user' ? (
|
|
876
|
-
<div className="max-w-[75%] rounded-2xl rounded-br-md px-4 py-2.5 bg-
|
|
877
|
-
<p className="text-sm whitespace-pre-wrap">{content}</p>
|
|
1139
|
+
<div className="max-w-[75%] rounded-2xl rounded-br-md px-4 py-2.5 bg-[rgb(var(--ec-page-text)/0.05)]">
|
|
1140
|
+
<p className="text-sm font-normal whitespace-pre-wrap text-[rgb(var(--ec-page-text))]">{content}</p>
|
|
878
1141
|
</div>
|
|
879
1142
|
) : (
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
className="
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
{children}
|
|
904
|
-
</code>
|
|
905
|
-
) : (
|
|
906
|
-
<CodeBlock language={language}>{codeString}</CodeBlock>
|
|
907
|
-
);
|
|
908
|
-
},
|
|
909
|
-
}}
|
|
910
|
-
>
|
|
911
|
-
{content}
|
|
912
|
-
</ReactMarkdown>
|
|
1143
|
+
<>
|
|
1144
|
+
{/* Tools used indicator */}
|
|
1145
|
+
{completedTools.length > 0 && (
|
|
1146
|
+
<div className="flex items-center gap-1.5 mb-2">
|
|
1147
|
+
<Wrench size={12} className="text-[rgb(var(--ec-icon-color))]" />
|
|
1148
|
+
<span className="text-[11px] text-[rgb(var(--ec-icon-color))]">
|
|
1149
|
+
Used{' '}
|
|
1150
|
+
{completedTools.slice(0, 3).map((tool, i) => (
|
|
1151
|
+
<span key={tool}>
|
|
1152
|
+
<span className="font-medium text-[rgb(var(--ec-accent))]">{tool}</span>
|
|
1153
|
+
{i < Math.min(completedTools.length, 3) - 1 && ', '}
|
|
1154
|
+
</span>
|
|
1155
|
+
))}
|
|
1156
|
+
{completedTools.length > 3 && <span> +{completedTools.length - 3} more</span>}
|
|
1157
|
+
</span>
|
|
1158
|
+
</div>
|
|
1159
|
+
)}
|
|
1160
|
+
<div className="w-full text-[rgb(var(--ec-content-text))]">
|
|
1161
|
+
<div className="prose prose-sm max-w-none prose-p:text-[rgb(var(--ec-content-text))] prose-headings:my-2 prose-headings:font-semibold prose-headings:text-[rgb(var(--ec-page-text))] prose-h1:text-lg prose-h2:text-base prose-h3:text-sm prose-h4:text-sm prose-li:text-[rgb(var(--ec-content-text))]">
|
|
1162
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={modalMarkdownComponents}>
|
|
1163
|
+
{content}
|
|
1164
|
+
</ReactMarkdown>
|
|
1165
|
+
</div>
|
|
913
1166
|
</div>
|
|
914
|
-
|
|
1167
|
+
{/* Follow-up suggestions - only show for last assistant message when not loading */}
|
|
1168
|
+
{isLastMessage && followUpSuggestions.length > 0 && !isLoading && (
|
|
1169
|
+
<div className="flex flex-wrap gap-2 mt-3 w-full">
|
|
1170
|
+
{followUpSuggestions.map((suggestion, index) => (
|
|
1171
|
+
<button
|
|
1172
|
+
key={index}
|
|
1173
|
+
onClick={() => handleSuggestedAction(suggestion)}
|
|
1174
|
+
className="px-4 py-2.5 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-page-text)/0.05)] hover:bg-[rgb(var(--ec-accent)/0.15)] rounded-full transition-all duration-200 text-left"
|
|
1175
|
+
style={fadeInStyles.getFollowUpStyle(index)}
|
|
1176
|
+
>
|
|
1177
|
+
{suggestion}
|
|
1178
|
+
</button>
|
|
1179
|
+
))}
|
|
1180
|
+
</div>
|
|
1181
|
+
)}
|
|
1182
|
+
</>
|
|
915
1183
|
)}
|
|
916
1184
|
</div>
|
|
917
1185
|
);
|
|
@@ -941,30 +1209,25 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
941
1209
|
</div>
|
|
942
1210
|
|
|
943
1211
|
{/* Modal Input area */}
|
|
944
|
-
<div className="flex-shrink-0 px-
|
|
1212
|
+
<div className="flex-shrink-0 px-6 py-4">
|
|
945
1213
|
<form onSubmit={handleSubmit} className="max-w-3xl mx-auto">
|
|
946
|
-
<div className="relative bg-
|
|
1214
|
+
<div className="relative bg-[rgb(var(--ec-page-bg)/0.5)] backdrop-blur-sm rounded-xl border border-[rgb(var(--ec-accent)/0.3)] focus-within:border-[rgb(var(--ec-accent)/0.5)] focus-within:ring-2 focus-within:ring-[rgb(var(--ec-accent)/0.1)] transition-all">
|
|
947
1215
|
<input
|
|
948
1216
|
ref={modalInputRef}
|
|
949
1217
|
type="text"
|
|
950
1218
|
value={inputValue}
|
|
951
1219
|
onChange={(e) => setInputValue(e.target.value)}
|
|
952
|
-
onKeyDown={
|
|
953
|
-
|
|
954
|
-
e.preventDefault();
|
|
955
|
-
submitMessage(inputValue);
|
|
956
|
-
}
|
|
957
|
-
}}
|
|
958
|
-
placeholder="Ask a question..."
|
|
1220
|
+
onKeyDown={handleInputKeyDown}
|
|
1221
|
+
placeholder="Ask, search, or explain..."
|
|
959
1222
|
disabled={isLoading}
|
|
960
|
-
className="w-full px-4 py-3 pr-
|
|
1223
|
+
className="w-full px-4 py-3.5 pr-20 bg-transparent text-[rgb(var(--ec-input-text))] placeholder-[rgb(var(--ec-input-placeholder))] focus:outline-none text-sm disabled:opacity-50 rounded-xl"
|
|
961
1224
|
/>
|
|
962
1225
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
|
|
963
1226
|
{isStreaming ? (
|
|
964
1227
|
<button
|
|
965
1228
|
type="button"
|
|
966
1229
|
onClick={() => stop()}
|
|
967
|
-
className="p-
|
|
1230
|
+
className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
|
|
968
1231
|
aria-label="Stop generating"
|
|
969
1232
|
>
|
|
970
1233
|
<Square size={14} fill="currentColor" />
|
|
@@ -973,7 +1236,7 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
973
1236
|
<button
|
|
974
1237
|
type="submit"
|
|
975
1238
|
disabled={!inputValue.trim() || isLoading}
|
|
976
|
-
className="px-
|
|
1239
|
+
className="px-4 py-2 bg-[rgb(var(--ec-accent))] text-white text-sm font-medium rounded-lg hover:bg-[rgb(var(--ec-accent-hover))] disabled:bg-transparent disabled:text-[rgb(var(--ec-icon-color))] disabled:cursor-not-allowed transition-colors"
|
|
977
1240
|
aria-label="Send message"
|
|
978
1241
|
>
|
|
979
1242
|
Send
|
|
@@ -982,7 +1245,17 @@ const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
|
982
1245
|
</div>
|
|
983
1246
|
</div>
|
|
984
1247
|
</form>
|
|
985
|
-
|
|
1248
|
+
{/* Context indicator */}
|
|
1249
|
+
<div className="flex items-center justify-center gap-1.5 mt-3 max-w-3xl mx-auto">
|
|
1250
|
+
{pageContext ? (
|
|
1251
|
+
<span className="text-xs text-[rgb(var(--ec-icon-color))] flex items-center gap-1.5">
|
|
1252
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[rgb(var(--ec-accent))]" />
|
|
1253
|
+
Based on {pageContext.type}: {pageContext.name}
|
|
1254
|
+
</span>
|
|
1255
|
+
) : (
|
|
1256
|
+
<span className="text-xs text-[rgb(var(--ec-icon-color))]">AI can make mistakes. Verify important info.</span>
|
|
1257
|
+
)}
|
|
1258
|
+
</div>
|
|
986
1259
|
</div>
|
|
987
1260
|
</Dialog.Content>
|
|
988
1261
|
</Dialog.Portal>
|