@eventcatalog/core 2.60.0 → 2.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-WASYSRBV.js → chunk-CMVDQ42N.js} +1 -1
- package/dist/{chunk-6FRDYEMA.js → chunk-MDWG55PH.js} +1 -1
- package/dist/{chunk-QMWI6M2I.js → chunk-RPWUOJ3F.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +2 -2
- package/dist/eventcatalog.js +4 -4
- package/eventcatalog/astro.config.mjs +1 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx +19 -9
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx +227 -69
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/hooks/ChatProvider.tsx +4 -3
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.server.tsx +46 -176
- package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/chat.ts +59 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +12 -112
- package/eventcatalog/src/enterprise/eventcatalog-chat/providers/ai-provider.ts +83 -4
- package/eventcatalog/src/middleware-auth.ts +13 -2
- package/eventcatalog/src/pages/chat/feature.astro +24 -16
- package/eventcatalog/src/pages/docs/[type]/[id]/[version].mdx.ts +22 -11
- package/eventcatalog/src/pages/docs/llm/llms-full.txt.ts +3 -3
- package/eventcatalog/src/utils/feature.ts +2 -1
- package/eventcatalog/src/utils/url-builder.ts +9 -0
- package/package.json +12 -15
- package/eventcatalog/src/enterprise/eventcatalog-chat/EventCatalogVectorStore.ts +0 -66
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.client.tsx +0 -540
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/workers/document-importer.ts +0 -38
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/workers/engine.ts +0 -7
- package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/ai/chat.ts +0 -54
- package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/ai/resources.ts +0 -42
- package/eventcatalog/src/enterprise/eventcatalog-chat/utils/ai.ts +0 -112
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-MDWG55PH.js";
|
|
4
|
+
import "../chunk-RPWUOJ3F.js";
|
|
5
|
+
import "../chunk-CMVDQ42N.js";
|
|
6
6
|
import "../chunk-UPONRQSN.js";
|
|
7
7
|
export {
|
|
8
8
|
log_build_default as default
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
package/dist/eventcatalog.cjs
CHANGED
|
@@ -157,7 +157,7 @@ var import_axios = __toESM(require("axios"), 1);
|
|
|
157
157
|
var import_os = __toESM(require("os"), 1);
|
|
158
158
|
|
|
159
159
|
// package.json
|
|
160
|
-
var version = "2.
|
|
160
|
+
var version = "2.61.1";
|
|
161
161
|
|
|
162
162
|
// src/constants.ts
|
|
163
163
|
var VERSION = version;
|
|
@@ -563,7 +563,7 @@ var copyServerFiles = async () => {
|
|
|
563
563
|
if (!isServerOutput) {
|
|
564
564
|
return;
|
|
565
565
|
}
|
|
566
|
-
import_fs2.default.cpSync((0, import_node_path6.join)(eventCatalogDir, "src/enterprise/eventcatalog-chat/pages/api"), (0, import_node_path6.join)(core, "src/pages/api
|
|
566
|
+
import_fs2.default.cpSync((0, import_node_path6.join)(eventCatalogDir, "src/enterprise/eventcatalog-chat/pages/api"), (0, import_node_path6.join)(core, "src/pages/api"), {
|
|
567
567
|
recursive: true
|
|
568
568
|
});
|
|
569
569
|
};
|
package/dist/eventcatalog.js
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
} from "./chunk-PLNJC7NZ.js";
|
|
7
7
|
import {
|
|
8
8
|
log_build_default
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-MDWG55PH.js";
|
|
10
|
+
import "./chunk-RPWUOJ3F.js";
|
|
11
11
|
import {
|
|
12
12
|
catalogToAstro,
|
|
13
13
|
checkAndConvertMdToMdx
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import "./chunk-55D645EH.js";
|
|
16
16
|
import {
|
|
17
17
|
VERSION
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-CMVDQ42N.js";
|
|
19
19
|
import {
|
|
20
20
|
getProjectOutDir,
|
|
21
21
|
isAuthEnabled,
|
|
@@ -78,7 +78,7 @@ var copyServerFiles = async () => {
|
|
|
78
78
|
if (!isServerOutput) {
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
|
-
fs.cpSync(join(eventCatalogDir, "src/enterprise/eventcatalog-chat/pages/api"), join(core, "src/pages/api
|
|
81
|
+
fs.cpSync(join(eventCatalogDir, "src/enterprise/eventcatalog-chat/pages/api"), join(core, "src/pages/api"), {
|
|
82
82
|
recursive: true
|
|
83
83
|
});
|
|
84
84
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import Sidebar from './ChatSidebar';
|
|
2
2
|
import { ChatProvider } from './hooks/ChatProvider';
|
|
3
|
-
import ChatWindowWebLLM from './windows/ChatWindow.client';
|
|
4
3
|
import ChatWindowServer from './windows/ChatWindow.server';
|
|
5
4
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
6
5
|
import type { ChatPromptCategoryGroup } from '@enterprise/eventcatalog-chat/utils/chat-prompts';
|
|
@@ -12,7 +11,6 @@ interface Resource {
|
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Chat component has two modes:
|
|
15
|
-
* - Client: ChatWindow.client.tsx (uses webllm)
|
|
16
14
|
* - Server: ChatWindow.server.tsx (uses server-side code, bring your own API key)
|
|
17
15
|
*
|
|
18
16
|
* The mode is determined by the config.output property in the eventcatalog.config.js file.
|
|
@@ -31,17 +29,29 @@ const Chat = ({
|
|
|
31
29
|
}) => {
|
|
32
30
|
const queryClient = new QueryClient();
|
|
33
31
|
|
|
32
|
+
if (output !== 'server') {
|
|
33
|
+
return (
|
|
34
|
+
// Message to turn on server side
|
|
35
|
+
<div className="flex justify-center items-center h-full bg-gray-100 p-4 rounded-lg text-center">
|
|
36
|
+
<div className="space-y-4">
|
|
37
|
+
<h1 className="text-3xl font-semibold text-gray-800">Chat is only supported on server side</h1>
|
|
38
|
+
<p className="text-sm text-gray-500">
|
|
39
|
+
Please switch to server side by setting the <code className="font-mono bg-gray-100 p-0.5 rounded">output</code>{' '}
|
|
40
|
+
property to <code className="font-mono bg-gray-100 p-0.5 rounded">server</code> in your{' '}
|
|
41
|
+
<code className="font-mono bg-gray-100 p-0.5 rounded">eventcatalog.config.js</code> file.
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
return (
|
|
35
49
|
<ChatProvider>
|
|
36
50
|
<div className="flex overflow-hidden w-full">
|
|
37
51
|
<Sidebar />
|
|
38
|
-
{
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
</QueryClientProvider>
|
|
42
|
-
) : (
|
|
43
|
-
<ChatWindowWebLLM {...chatConfig} />
|
|
44
|
-
)}
|
|
52
|
+
<QueryClientProvider client={queryClient}>
|
|
53
|
+
<ChatWindowServer {...chatConfig} resources={resources} chatPrompts={chatPrompts} />
|
|
54
|
+
</QueryClientProvider>
|
|
45
55
|
</div>
|
|
46
56
|
</ChatProvider>
|
|
47
57
|
);
|
|
@@ -24,45 +24,21 @@ function escapeRegex(string: string): string {
|
|
|
24
24
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// Function to parse and render content with iframes
|
|
28
|
+
function parseContentWithIframes(content: string): React.ReactNode[] {
|
|
29
|
+
const iframeRegex = /<iframe[^>]*>.*?<\/iframe>/gi;
|
|
30
|
+
const parts: React.ReactNode[] = [];
|
|
31
|
+
let lastIndex = 0;
|
|
32
|
+
let match;
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Helper to get display name for resource, ensuring a fallback
|
|
40
|
-
const getResourceDisplayName = (resource: Resource): string => {
|
|
41
|
-
return resource.title || resource.name || resource.id || 'Resource'; // Added fallback
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const handleCopy = (codeToCopy: string, id: string) => {
|
|
45
|
-
navigator.clipboard
|
|
46
|
-
.writeText(codeToCopy)
|
|
47
|
-
.then(() => {
|
|
48
|
-
setCopiedStates((prev) => ({ ...prev, [id]: true }));
|
|
49
|
-
setTimeout(() => {
|
|
50
|
-
setCopiedStates((prev) => ({ ...prev, [id]: false }));
|
|
51
|
-
}, 1500); // Reset after 1.5 seconds
|
|
52
|
-
})
|
|
53
|
-
.catch((err) => {
|
|
54
|
-
console.error('Failed to copy code: ', err);
|
|
55
|
-
});
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div className={`flex ${message.isUser ? 'justify-end' : 'justify-start'} mb-4`}>
|
|
60
|
-
<div
|
|
61
|
-
className={`max-w-[80%] rounded-lg p-3 ${message.isUser ? 'bg-purple-600 text-white rounded-br-none' : 'bg-gray-100 text-gray-800 rounded-bl-none'}`}
|
|
62
|
-
>
|
|
63
|
-
{/* Apply prose styles, including prose-invert for user messages for better text contrast */}
|
|
64
|
-
<div className={`prose prose-sm max-w-none ${message.isUser ? 'prose-invert' : ''}`}>
|
|
34
|
+
while ((match = iframeRegex.exec(content)) !== null) {
|
|
35
|
+
// Add text before iframe as markdown
|
|
36
|
+
if (match.index > lastIndex) {
|
|
37
|
+
const textBefore = content.slice(lastIndex, match.index);
|
|
38
|
+
if (textBefore.trim()) {
|
|
39
|
+
parts.push(
|
|
65
40
|
<ReactMarkdown
|
|
41
|
+
key={`text-${lastIndex}`}
|
|
66
42
|
components={{
|
|
67
43
|
code({ node, className, children, ...props }: CodeComponentProps) {
|
|
68
44
|
const inline = props.inline;
|
|
@@ -70,37 +46,14 @@ const ChatMessage = React.memo(({ message }: ChatMessageProps) => {
|
|
|
70
46
|
const codeString = String(children);
|
|
71
47
|
const language = match ? match[1] : 'text';
|
|
72
48
|
const codeBlockId = `code-${React.useId()}`;
|
|
73
|
-
const isCopied =
|
|
49
|
+
const isCopied = false; // We'll handle this in the parent component
|
|
74
50
|
|
|
75
|
-
const handleOpenModal = () => {
|
|
76
|
-
setModalContent({ language, code: codeString.replace(/\n$/, '') });
|
|
77
|
-
setIsModalOpen(true);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// Heuristic: Treat as inline if it doesn't contain newlines OR if explicitly inline.
|
|
81
|
-
// This handles parser quirks with single-line snippets in lists, etc.
|
|
82
51
|
const treatAsInline = inline || !codeString.includes('\n');
|
|
83
52
|
|
|
84
53
|
return !treatAsInline ? (
|
|
85
54
|
<div className="code-block bg-[#1e1e1e] rounded-md overflow-hidden my-4 relative group">
|
|
86
55
|
<div className="flex justify-between items-center px-4 py-1.5 bg-gray-700 text-gray-300 text-xs">
|
|
87
56
|
<span>{language}</span>
|
|
88
|
-
<div className="flex items-center gap-2">
|
|
89
|
-
<button
|
|
90
|
-
onClick={() => handleCopy(codeString.replace(/\n$/, ''), codeBlockId)}
|
|
91
|
-
className="text-gray-300 hover:text-white flex items-center"
|
|
92
|
-
aria-label={isCopied ? 'Copied' : 'Copy code'}
|
|
93
|
-
>
|
|
94
|
-
{isCopied ? <Check size={14} className="text-green-400" /> : <Clipboard size={14} />}
|
|
95
|
-
</button>
|
|
96
|
-
<button
|
|
97
|
-
onClick={handleOpenModal}
|
|
98
|
-
className="text-gray-300 hover:text-white"
|
|
99
|
-
aria-label="View code fullscreen"
|
|
100
|
-
>
|
|
101
|
-
<Fullscreen size={14} />
|
|
102
|
-
</button>
|
|
103
|
-
</div>
|
|
104
57
|
</div>
|
|
105
58
|
<SyntaxHighlighter
|
|
106
59
|
style={vscDarkPlus as any}
|
|
@@ -115,14 +68,9 @@ const ChatMessage = React.memo(({ message }: ChatMessageProps) => {
|
|
|
115
68
|
</div>
|
|
116
69
|
) : (
|
|
117
70
|
<code
|
|
118
|
-
className={`px-1 py-0.5 rounded text-xs font-mono ${
|
|
119
|
-
message.isUser
|
|
120
|
-
? 'bg-purple-800/70 text-purple-100' // Darker purple bg, light text for user
|
|
121
|
-
: 'bg-gray-300/70 text-gray-900' // Darker gray bg, dark text for assistant
|
|
122
|
-
} ${className || ''}`}
|
|
71
|
+
className={`px-1 py-0.5 rounded text-xs font-mono bg-gray-300/70 text-gray-900 ${className || ''}`}
|
|
123
72
|
{...props}
|
|
124
73
|
>
|
|
125
|
-
{/* Render trimmed version for inline code */}
|
|
126
74
|
{codeString.trim()}
|
|
127
75
|
</code>
|
|
128
76
|
);
|
|
@@ -133,9 +81,219 @@ const ChatMessage = React.memo(({ message }: ChatMessageProps) => {
|
|
|
133
81
|
p: ({ node, ...props }) => <p {...props} className="mb-2 last:mb-0" />,
|
|
134
82
|
}}
|
|
135
83
|
>
|
|
136
|
-
{
|
|
137
|
-
{message.content || ''}
|
|
84
|
+
{textBefore}
|
|
138
85
|
</ReactMarkdown>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Parse iframe attributes
|
|
91
|
+
const iframeHtml = match[0];
|
|
92
|
+
const srcMatch = iframeHtml.match(/src=["']([^"']+)["']/i);
|
|
93
|
+
const widthMatch = iframeHtml.match(/width=["']([^"']+)["']/i);
|
|
94
|
+
const heightMatch = iframeHtml.match(/height=["']([^"']+)["']/i);
|
|
95
|
+
const titleMatch = iframeHtml.match(/title=["']([^"']+)["']/i);
|
|
96
|
+
|
|
97
|
+
if (srcMatch) {
|
|
98
|
+
parts.push(
|
|
99
|
+
<iframe
|
|
100
|
+
key={`iframe-${match.index}`}
|
|
101
|
+
src={srcMatch[1]}
|
|
102
|
+
width={widthMatch ? widthMatch[1] : '100%'}
|
|
103
|
+
height={heightMatch ? heightMatch[1] : '400'}
|
|
104
|
+
title={titleMatch ? titleMatch[1] : 'Embedded content'}
|
|
105
|
+
className="w-full h-96 border rounded-lg my-4"
|
|
106
|
+
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
|
|
107
|
+
loading="lazy"
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lastIndex = match.index + match[0].length;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Add remaining text after last iframe
|
|
116
|
+
if (lastIndex < content.length) {
|
|
117
|
+
const textAfter = content.slice(lastIndex);
|
|
118
|
+
if (textAfter.trim()) {
|
|
119
|
+
parts.push(
|
|
120
|
+
<ReactMarkdown
|
|
121
|
+
key={`text-${lastIndex}`}
|
|
122
|
+
components={{
|
|
123
|
+
code({ node, className, children, ...props }: CodeComponentProps) {
|
|
124
|
+
const inline = props.inline;
|
|
125
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
126
|
+
const codeString = String(children);
|
|
127
|
+
const language = match ? match[1] : 'text';
|
|
128
|
+
const codeBlockId = `code-${React.useId()}`;
|
|
129
|
+
const isCopied = false;
|
|
130
|
+
|
|
131
|
+
const treatAsInline = inline || !codeString.includes('\n');
|
|
132
|
+
|
|
133
|
+
return !treatAsInline ? (
|
|
134
|
+
<div className="code-block bg-[#1e1e1e] rounded-md overflow-hidden my-4 relative group">
|
|
135
|
+
<div className="flex justify-between items-center px-4 py-1.5 bg-gray-700 text-gray-300 text-xs">
|
|
136
|
+
<span>{language}</span>
|
|
137
|
+
</div>
|
|
138
|
+
<SyntaxHighlighter
|
|
139
|
+
style={vscDarkPlus as any}
|
|
140
|
+
language={language}
|
|
141
|
+
PreTag="div"
|
|
142
|
+
showLineNumbers
|
|
143
|
+
wrapLines={true}
|
|
144
|
+
customStyle={{ margin: 0, borderRadius: '0 0 0.375rem 0.375rem', padding: '1rem' }}
|
|
145
|
+
>
|
|
146
|
+
{codeString.replace(/\n$/, '')}
|
|
147
|
+
</SyntaxHighlighter>
|
|
148
|
+
</div>
|
|
149
|
+
) : (
|
|
150
|
+
<code
|
|
151
|
+
className={`px-1 py-0.5 rounded text-xs font-mono bg-gray-300/70 text-gray-900 ${className || ''}`}
|
|
152
|
+
{...props}
|
|
153
|
+
>
|
|
154
|
+
{codeString.trim()}
|
|
155
|
+
</code>
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
a: ({ node, ...props }) => (
|
|
159
|
+
<a {...props} target="_blank" rel="noopener noreferrer" className="text-purple-600 hover:text-purple-800" />
|
|
160
|
+
),
|
|
161
|
+
p: ({ node, ...props }) => <p {...props} className="mb-2 last:mb-0" />,
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{textAfter}
|
|
165
|
+
</ReactMarkdown>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return parts;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Define props for the code component explicitly
|
|
174
|
+
interface CodeComponentProps extends React.HTMLAttributes<HTMLElement>, ExtraProps {
|
|
175
|
+
inline?: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const ChatMessage = React.memo(({ message }: ChatMessageProps) => {
|
|
179
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
180
|
+
const [modalContent, setModalContent] = useState<{ language: string; code: string } | null>(null);
|
|
181
|
+
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({}); // State for copy feedback
|
|
182
|
+
const [isResourcesCollapsed, setIsResourcesCollapsed] = useState(true); // State for resource section collapse
|
|
183
|
+
const [isContextCollapsed, setIsContextCollapsed] = useState(true); // State for additional context collapse
|
|
184
|
+
|
|
185
|
+
// Helper to get display name for resource, ensuring a fallback
|
|
186
|
+
const getResourceDisplayName = (resource: Resource): string => {
|
|
187
|
+
return resource.title || resource.name || resource.id || 'Resource'; // Added fallback
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleCopy = (codeToCopy: string, id: string) => {
|
|
191
|
+
navigator.clipboard
|
|
192
|
+
.writeText(codeToCopy)
|
|
193
|
+
.then(() => {
|
|
194
|
+
setCopiedStates((prev) => ({ ...prev, [id]: true }));
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
setCopiedStates((prev) => ({ ...prev, [id]: false }));
|
|
197
|
+
}, 1500); // Reset after 1.5 seconds
|
|
198
|
+
})
|
|
199
|
+
.catch((err) => {
|
|
200
|
+
console.error('Failed to copy code: ', err);
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className={`flex ${message.isUser ? 'justify-end' : 'justify-start'} mb-4`}>
|
|
206
|
+
<div
|
|
207
|
+
className={`max-w-[80%] rounded-lg p-3 ${message.isUser ? 'bg-purple-600 text-white rounded-br-none' : 'bg-gray-100 text-gray-800 rounded-bl-none'}`}
|
|
208
|
+
>
|
|
209
|
+
{/* Apply prose styles, including prose-invert for user messages for better text contrast */}
|
|
210
|
+
<div className={`prose prose-sm max-w-none ${message.isUser ? 'prose-invert' : ''}`}>
|
|
211
|
+
{(() => {
|
|
212
|
+
const content = message.content || '';
|
|
213
|
+
const iframeRegex = /<iframe[^>]*>.*?<\/iframe>/gi;
|
|
214
|
+
|
|
215
|
+
// If content contains iframes, use custom parser
|
|
216
|
+
if (iframeRegex.test(content)) {
|
|
217
|
+
return parseContentWithIframes(content);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Otherwise, use regular ReactMarkdown
|
|
221
|
+
return (
|
|
222
|
+
<ReactMarkdown
|
|
223
|
+
components={{
|
|
224
|
+
code({ node, className, children, ...props }: CodeComponentProps) {
|
|
225
|
+
const inline = props.inline;
|
|
226
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
227
|
+
const codeString = String(children);
|
|
228
|
+
const language = match ? match[1] : 'text';
|
|
229
|
+
const codeBlockId = `code-${React.useId()}`;
|
|
230
|
+
const isCopied = copiedStates[codeBlockId];
|
|
231
|
+
|
|
232
|
+
const handleOpenModal = () => {
|
|
233
|
+
setModalContent({ language, code: codeString.replace(/\n$/, '') });
|
|
234
|
+
setIsModalOpen(true);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Heuristic: Treat as inline if it doesn't contain newlines OR if explicitly inline.
|
|
238
|
+
// This handles parser quirks with single-line snippets in lists, etc.
|
|
239
|
+
const treatAsInline = inline || !codeString.includes('\n');
|
|
240
|
+
|
|
241
|
+
return !treatAsInline ? (
|
|
242
|
+
<div className="code-block bg-[#1e1e1e] rounded-md overflow-hidden my-4 relative group">
|
|
243
|
+
<div className="flex justify-between items-center px-4 py-1.5 bg-gray-700 text-gray-300 text-xs">
|
|
244
|
+
<span>{language}</span>
|
|
245
|
+
<div className="flex items-center gap-2">
|
|
246
|
+
<button
|
|
247
|
+
onClick={() => handleCopy(codeString.replace(/\n$/, ''), codeBlockId)}
|
|
248
|
+
className="text-gray-300 hover:text-white flex items-center"
|
|
249
|
+
aria-label={isCopied ? 'Copied' : 'Copy code'}
|
|
250
|
+
>
|
|
251
|
+
{isCopied ? <Check size={14} className="text-green-400" /> : <Clipboard size={14} />}
|
|
252
|
+
</button>
|
|
253
|
+
<button
|
|
254
|
+
onClick={handleOpenModal}
|
|
255
|
+
className="text-gray-300 hover:text-white"
|
|
256
|
+
aria-label="View code fullscreen"
|
|
257
|
+
>
|
|
258
|
+
<Fullscreen size={14} />
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<SyntaxHighlighter
|
|
263
|
+
style={vscDarkPlus as any}
|
|
264
|
+
language={language}
|
|
265
|
+
PreTag="div"
|
|
266
|
+
showLineNumbers
|
|
267
|
+
wrapLines={true}
|
|
268
|
+
customStyle={{ margin: 0, borderRadius: '0 0 0.375rem 0.375rem', padding: '1rem' }}
|
|
269
|
+
>
|
|
270
|
+
{codeString.replace(/\n$/, '')}
|
|
271
|
+
</SyntaxHighlighter>
|
|
272
|
+
</div>
|
|
273
|
+
) : (
|
|
274
|
+
<code
|
|
275
|
+
className={`px-1 py-0.5 rounded text-xs font-mono ${
|
|
276
|
+
message.isUser
|
|
277
|
+
? 'bg-purple-800/70 text-purple-100' // Darker purple bg, light text for user
|
|
278
|
+
: 'bg-gray-300/70 text-gray-900' // Darker gray bg, dark text for assistant
|
|
279
|
+
} ${className || ''}`}
|
|
280
|
+
{...props}
|
|
281
|
+
>
|
|
282
|
+
{/* Render trimmed version for inline code */}
|
|
283
|
+
{codeString.trim()}
|
|
284
|
+
</code>
|
|
285
|
+
);
|
|
286
|
+
},
|
|
287
|
+
a: ({ node, ...props }) => (
|
|
288
|
+
<a {...props} target="_blank" rel="noopener noreferrer" className="text-purple-600 hover:text-purple-800" />
|
|
289
|
+
),
|
|
290
|
+
p: ({ node, ...props }) => <p {...props} className="mb-2 last:mb-0" />,
|
|
291
|
+
}}
|
|
292
|
+
>
|
|
293
|
+
{content}
|
|
294
|
+
</ReactMarkdown>
|
|
295
|
+
);
|
|
296
|
+
})()}
|
|
139
297
|
</div>
|
|
140
298
|
|
|
141
299
|
{/* Additional Context section (for user messages) */}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UIMessage } from '@ai-sdk/react';
|
|
1
2
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
2
3
|
import type { ReactNode } from 'react';
|
|
3
4
|
|
|
@@ -31,7 +32,7 @@ interface ChatContextType {
|
|
|
31
32
|
setCurrentSession: (session: ChatSession | null) => void;
|
|
32
33
|
isStreaming: boolean;
|
|
33
34
|
setIsStreaming: (isStreaming: boolean) => void;
|
|
34
|
-
storeMessagesToSession: (sessionId: string, messages:
|
|
35
|
+
storeMessagesToSession: (sessionId: string, messages: UIMessage[]) => void;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
|
@@ -93,13 +94,13 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|
|
93
94
|
};
|
|
94
95
|
|
|
95
96
|
// Set all messages to the session
|
|
96
|
-
const storeMessagesToSession = (sessionId: string, messages:
|
|
97
|
+
const storeMessagesToSession = (sessionId: string, messages: UIMessage[]) => {
|
|
97
98
|
setSessions((prev) => {
|
|
98
99
|
const index = prev.findIndex((s) => s.id === sessionId);
|
|
99
100
|
if (index === -1) return prev;
|
|
100
101
|
|
|
101
102
|
const updated = [...prev];
|
|
102
|
-
updated[index].messages = messages;
|
|
103
|
+
updated[index].messages = messages as unknown as Message[];
|
|
103
104
|
updated[index].lastUpdated = Date.now();
|
|
104
105
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
105
106
|
return updated;
|