@eventcatalog/core 2.60.0 → 2.61.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-WASYSRBV.js → chunk-3CAAQQUB.js} +1 -1
- package/dist/{chunk-QMWI6M2I.js → chunk-I2B6B3XZ.js} +1 -1
- package/dist/{chunk-6FRDYEMA.js → chunk-OEWPHKD6.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 +0 -37
- 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 +7 -10
- 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
package/eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.client.tsx
DELETED
|
@@ -1,540 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
|
2
|
-
import { BookOpen, Send } from 'lucide-react';
|
|
3
|
-
import { CreateWebWorkerMLCEngine, type InitProgressReport } from '@mlc-ai/web-llm';
|
|
4
|
-
import { useChat, type Message } from '../hooks/ChatProvider';
|
|
5
|
-
import React from 'react';
|
|
6
|
-
// Update Message type to include resources
|
|
7
|
-
interface Resource {
|
|
8
|
-
id: string;
|
|
9
|
-
type: string;
|
|
10
|
-
url: string;
|
|
11
|
-
title?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Move formatMessageContent outside component since it doesn't use any component state or props
|
|
15
|
-
const formatMessageContent = (content: string, resources?: Resource[]): string => {
|
|
16
|
-
// First handle any full resource tags by replacing them with just their ID/title
|
|
17
|
-
content = content.replace(/<resource[^>]*?id="([^"]*)"[^>]*?>/g, '$1');
|
|
18
|
-
|
|
19
|
-
// First escape <resource> tags
|
|
20
|
-
let formattedContent = content.replace(/<resource[^>]*>/g, (match) => {
|
|
21
|
-
return match.replace(/</g, '<').replace(/>/g, '>');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// If we have resources, convert matching IDs to links
|
|
25
|
-
if (resources?.length) {
|
|
26
|
-
// Create a regex pattern that matches any resource ID/title
|
|
27
|
-
const resourceMatches = resources.map((r) => ({
|
|
28
|
-
pattern: r.title || r.id,
|
|
29
|
-
url: r.url,
|
|
30
|
-
type: r.type,
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
// Sort by length (longest first) to prevent partial matches
|
|
34
|
-
resourceMatches.sort((a, b) => b.pattern.length - a.pattern.length);
|
|
35
|
-
|
|
36
|
-
// Replace matches with links, but skip if already inside an HTML tag
|
|
37
|
-
for (const { pattern, url, type } of resourceMatches) {
|
|
38
|
-
// Updated regex to match whole words only using word boundaries \b
|
|
39
|
-
const regex = new RegExp(`(?<!<[^>]*)\\b(${pattern})\\b(?![^<]*>)`, 'g');
|
|
40
|
-
formattedContent = formattedContent.replace(
|
|
41
|
-
regex,
|
|
42
|
-
`<a href="${url}" class="text-purple-600 hover:text-purple-800" target="_blank" rel="noopener noreferrer">$1 (${type})</a>`
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Handle code blocks
|
|
48
|
-
formattedContent = formattedContent.replace(/```([\s\S]*?)```/g, (match, codeContent) => {
|
|
49
|
-
const escapedCode = codeContent.trim().replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
50
|
-
return `<pre class="bg-gray-800 border border-gray-700 p-4 my-3 rounded-lg overflow-x-auto"><code class="text-sm font-mono text-gray-200">${escapedCode}</code></pre>`;
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Handle inline code
|
|
54
|
-
formattedContent = formattedContent.replace(/(?<!`)`([^`]+)`(?!`)/g, (match, code) => {
|
|
55
|
-
const escapedCode = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
56
|
-
return `<code class="bg-gray-500 border border-gray-700 px-2 py-0.5 rounded text-sm font-mono text-gray-200">${escapedCode}</code>`;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Handle bold text with double asterisks
|
|
60
|
-
formattedContent = formattedContent.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
61
|
-
|
|
62
|
-
// Convert newlines to <br>
|
|
63
|
-
formattedContent = formattedContent.replace(/\n(?!<\/code>)/g, '<br>');
|
|
64
|
-
|
|
65
|
-
return formattedContent;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Create a memoized Message component
|
|
69
|
-
const ChatMessage = React.memo(({ message }: { message: Message }) => (
|
|
70
|
-
<div className={`flex ${message.isUser ? 'justify-end' : 'justify-start'} mb-4`}>
|
|
71
|
-
<div
|
|
72
|
-
className={`max-w-[80%] rounded-lg p-3 ${
|
|
73
|
-
message.isUser ? 'bg-purple-600 text-white rounded-br-none' : 'bg-gray-100 text-gray-800 rounded-bl-none'
|
|
74
|
-
}`}
|
|
75
|
-
>
|
|
76
|
-
<div dangerouslySetInnerHTML={{ __html: formatMessageContent(message.content, message.resources) }} />
|
|
77
|
-
{!message.isUser && message.resources && message.resources.length > 0 && (
|
|
78
|
-
<div className="mt-3 pt-3 border-t border-gray-200">
|
|
79
|
-
<p className="text-xs text-gray-500 mb-1">Referenced Resources:</p>
|
|
80
|
-
<div className="text-[10px]">
|
|
81
|
-
{message.resources.map((resource, idx) => (
|
|
82
|
-
<span key={resource.id}>
|
|
83
|
-
<a
|
|
84
|
-
href={resource.url}
|
|
85
|
-
className="text-purple-600 hover:text-purple-800"
|
|
86
|
-
target="_blank"
|
|
87
|
-
rel="noopener noreferrer"
|
|
88
|
-
>
|
|
89
|
-
{resource.title || resource.id} ({resource.type})
|
|
90
|
-
</a>
|
|
91
|
-
{idx < (message.resources?.length || 0) - 1 ? ', ' : ''}
|
|
92
|
-
</span>
|
|
93
|
-
))}
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
)}
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
));
|
|
100
|
-
|
|
101
|
-
ChatMessage.displayName = 'ChatMessage';
|
|
102
|
-
|
|
103
|
-
interface ChatWindowProps {
|
|
104
|
-
model?: string;
|
|
105
|
-
max_tokens?: number;
|
|
106
|
-
similarityResults?: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const ChatWindow = ({
|
|
110
|
-
model = 'Hermes-3-Llama-3.2-3B-q4f16_1-MLC',
|
|
111
|
-
max_tokens = 4096,
|
|
112
|
-
similarityResults = 50,
|
|
113
|
-
}: ChatWindowProps) => {
|
|
114
|
-
const [loading, setLoading] = useState(true);
|
|
115
|
-
const [loadingProgress, setLoadingProgress] = useState(0);
|
|
116
|
-
const [engine, setEngine] = useState<any>(null);
|
|
117
|
-
const [messages, setMessages] = useState<Array<Message>>([]);
|
|
118
|
-
const [inputValue, setInputValue] = useState('');
|
|
119
|
-
const [showWelcome, setShowWelcome] = useState(true);
|
|
120
|
-
const [vectorWorker, setVectorWorker] = useState<Worker | null>(null);
|
|
121
|
-
const [isThinking, setIsThinking] = useState(false);
|
|
122
|
-
const completionRef = useRef<any>(null);
|
|
123
|
-
const outputRef = useRef<HTMLDivElement>(null);
|
|
124
|
-
const [sendDefaultQuestionToLLM, setSendDefaultQuestionToLLM] = useState(false);
|
|
125
|
-
|
|
126
|
-
const { currentSession, storeMessagesToSession, updateSession, isStreaming, setIsStreaming } = useChat();
|
|
127
|
-
|
|
128
|
-
// Load messages when session changes
|
|
129
|
-
// Load messages when session changes
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (currentSession) {
|
|
132
|
-
console.log('currentSession', currentSession.messages);
|
|
133
|
-
setMessages(currentSession.messages);
|
|
134
|
-
setShowWelcome(false);
|
|
135
|
-
} else {
|
|
136
|
-
setMessages([]);
|
|
137
|
-
setShowWelcome(true);
|
|
138
|
-
}
|
|
139
|
-
}, [currentSession]);
|
|
140
|
-
|
|
141
|
-
// If the messages change add them to the session
|
|
142
|
-
useEffect(() => {
|
|
143
|
-
if (currentSession) {
|
|
144
|
-
storeMessagesToSession(currentSession.id, messages);
|
|
145
|
-
}
|
|
146
|
-
}, [messages]);
|
|
147
|
-
|
|
148
|
-
// Helper function to stop the current completion
|
|
149
|
-
const handleStop = useCallback(async () => {
|
|
150
|
-
if (completionRef.current) {
|
|
151
|
-
try {
|
|
152
|
-
await engine.interruptGenerate();
|
|
153
|
-
completionRef.current = null;
|
|
154
|
-
setIsStreaming(false);
|
|
155
|
-
setIsThinking(false);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
console.error('Error stopping completion:', error);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}, [engine]);
|
|
161
|
-
|
|
162
|
-
const handleSubmit = useCallback(async () => {
|
|
163
|
-
console.log('handleSubmit', inputValue);
|
|
164
|
-
if (!inputValue.trim() || !engine) return;
|
|
165
|
-
|
|
166
|
-
// Add to messages
|
|
167
|
-
setMessages((prev) => [...prev, { content: inputValue, isUser: true, timestamp: Date.now() }]);
|
|
168
|
-
|
|
169
|
-
setIsThinking(true);
|
|
170
|
-
setIsStreaming(true);
|
|
171
|
-
setInputValue('');
|
|
172
|
-
|
|
173
|
-
// if the first message, update the session title
|
|
174
|
-
if (currentSession) {
|
|
175
|
-
updateSession({
|
|
176
|
-
...currentSession,
|
|
177
|
-
title: inputValue.length > 25 ? `${inputValue.substring(0, 22)}...` : inputValue,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Add input to vector store
|
|
182
|
-
vectorWorker?.postMessage({ input: inputValue, similarityResults });
|
|
183
|
-
|
|
184
|
-
// @ts-ignore
|
|
185
|
-
vectorWorker.onmessage = async (event) => {
|
|
186
|
-
if (event.data.action === 'search-results') {
|
|
187
|
-
console.log('Results', event?.data?.results);
|
|
188
|
-
|
|
189
|
-
// Extract resources from results and ensure uniqueness by ID
|
|
190
|
-
const resources = Array.from(
|
|
191
|
-
new Map(
|
|
192
|
-
event.data.results.map((result: any) => {
|
|
193
|
-
const metadata = result[0].metadata;
|
|
194
|
-
const resource: Resource = {
|
|
195
|
-
id: metadata.id,
|
|
196
|
-
type: metadata.type,
|
|
197
|
-
url: `/docs/${metadata.type}s/${metadata.id}`,
|
|
198
|
-
title: metadata.title || metadata.id,
|
|
199
|
-
};
|
|
200
|
-
return [metadata.id, resource]; // Use ID as key for Map
|
|
201
|
-
})
|
|
202
|
-
).values()
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
console.log('resources', resources);
|
|
206
|
-
|
|
207
|
-
const qaPrompt = `\n".
|
|
208
|
-
|
|
209
|
-
You are an expert in event-driven architecture and domain-driven design, specializing in documentation for EventCatalog.
|
|
210
|
-
|
|
211
|
-
You assist developers, architects, and business stakeholders who need information about their event-driven system catalog. You help with questions about:
|
|
212
|
-
- Events (asynchronous messages that notify about something that has happened)
|
|
213
|
-
- Commands (requests to perform an action)
|
|
214
|
-
- Queries (requests for information)
|
|
215
|
-
- Services (bounded contexts or applications that produce/consume events)
|
|
216
|
-
- Domains (business capabilities or functional areas)
|
|
217
|
-
|
|
218
|
-
IMPORTANT RULES:
|
|
219
|
-
1. Resources will be provided to you in <resource> tags. ONLY use these resources to answer questions.
|
|
220
|
-
2. NEVER include ANY <resource> tags in your responses. This is a strict requirement.
|
|
221
|
-
3. ALWAYS refer to resources by their name/ID/title attributes only.
|
|
222
|
-
4. If asked about specific resource types (e.g., "What domains do we have?"), simply list their names without elaboration.
|
|
223
|
-
5. NEVER invent or make up resources that aren't provided to you.
|
|
224
|
-
|
|
225
|
-
RESPONSE FORMAT EXAMPLES:
|
|
226
|
-
✓ CORRECT: "The SubscriptionService produces the UserSubscribed event."
|
|
227
|
-
✗ INCORRECT: "<resource id="SubscriptionService">...</resource> produces events."
|
|
228
|
-
|
|
229
|
-
When responding:
|
|
230
|
-
1. Use only information from the provided resources
|
|
231
|
-
2. Explain connections between resources when relevant
|
|
232
|
-
3. Use appropriate technical terminology
|
|
233
|
-
4. Use clear formatting with headings and bullet points when helpful
|
|
234
|
-
5. State clearly when information is missing rather than making assumptions
|
|
235
|
-
6. Don't provide code examples unless specifically requested
|
|
236
|
-
|
|
237
|
-
Your primary goal is to help users understand their event-driven system through accurate documentation interpretation.
|
|
238
|
-
|
|
239
|
-
==========
|
|
240
|
-
${(resources as Resource[])
|
|
241
|
-
.map((resource: Resource) => {
|
|
242
|
-
return `<resource ${Object.entries(resource)
|
|
243
|
-
.filter(([key, value]) => key !== 'markdown' && key !== 'loc')
|
|
244
|
-
.map(([key, value]) => {
|
|
245
|
-
return `${key}="${value}"`;
|
|
246
|
-
})
|
|
247
|
-
.join(' ')} />`;
|
|
248
|
-
})
|
|
249
|
-
.join('\n')}\n
|
|
250
|
-
==========
|
|
251
|
-
|
|
252
|
-
""
|
|
253
|
-
`;
|
|
254
|
-
|
|
255
|
-
console.log('qaPrompt', qaPrompt);
|
|
256
|
-
try {
|
|
257
|
-
// Get completion
|
|
258
|
-
const completion = await engine.chat.completions.create({
|
|
259
|
-
messages: [
|
|
260
|
-
{
|
|
261
|
-
role: 'system',
|
|
262
|
-
content: qaPrompt,
|
|
263
|
-
},
|
|
264
|
-
// previous messages
|
|
265
|
-
...messages.map((msg) => ({
|
|
266
|
-
role: msg.isUser ? 'user' : 'assistant',
|
|
267
|
-
content: msg.content,
|
|
268
|
-
})),
|
|
269
|
-
{
|
|
270
|
-
role: 'user',
|
|
271
|
-
content: inputValue,
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
stream: true,
|
|
275
|
-
temperature: 0.1,
|
|
276
|
-
max_tokens,
|
|
277
|
-
top_p: 0.9,
|
|
278
|
-
top_k: 40,
|
|
279
|
-
frequency_penalty: 0.1,
|
|
280
|
-
presence_penalty: 0,
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// Store completion reference for potential cancellation
|
|
284
|
-
completionRef.current = completion;
|
|
285
|
-
|
|
286
|
-
let isFirstChunk = true;
|
|
287
|
-
let responseText = '';
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
for await (const chunk of completion) {
|
|
291
|
-
const content = chunk.choices[0]?.delta?.content || '';
|
|
292
|
-
if (content) {
|
|
293
|
-
responseText += content;
|
|
294
|
-
|
|
295
|
-
if (isFirstChunk) {
|
|
296
|
-
setIsThinking(false);
|
|
297
|
-
setMessages((prev) => [
|
|
298
|
-
...prev,
|
|
299
|
-
{
|
|
300
|
-
content: responseText,
|
|
301
|
-
isUser: false,
|
|
302
|
-
timestamp: Date.now(),
|
|
303
|
-
},
|
|
304
|
-
]);
|
|
305
|
-
isFirstChunk = false;
|
|
306
|
-
} else {
|
|
307
|
-
setMessages((prev) => {
|
|
308
|
-
const newMessages = [...prev];
|
|
309
|
-
newMessages[newMessages.length - 1] = {
|
|
310
|
-
...newMessages[newMessages.length - 1],
|
|
311
|
-
content: responseText,
|
|
312
|
-
};
|
|
313
|
-
return newMessages;
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
scrollToBottom();
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Add resources after streaming is complete
|
|
321
|
-
setMessages((prev) => {
|
|
322
|
-
const newMessages = [...prev];
|
|
323
|
-
newMessages[newMessages.length - 1] = {
|
|
324
|
-
...newMessages[newMessages.length - 1],
|
|
325
|
-
content: responseText,
|
|
326
|
-
resources: resources as { id: string; type: string; url: string; title?: string }[],
|
|
327
|
-
};
|
|
328
|
-
return newMessages;
|
|
329
|
-
});
|
|
330
|
-
} catch (error: any) {
|
|
331
|
-
if (error.message?.includes('cancelled')) {
|
|
332
|
-
console.log('Completion was stopped by the user');
|
|
333
|
-
} else {
|
|
334
|
-
throw error;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
setIsThinking(false);
|
|
339
|
-
setIsStreaming(false);
|
|
340
|
-
completionRef.current = null;
|
|
341
|
-
} catch (error: any) {
|
|
342
|
-
console.error('Error:', error);
|
|
343
|
-
const errorMessage = {
|
|
344
|
-
content: 'Sorry, there was an error processing your request.',
|
|
345
|
-
isUser: false,
|
|
346
|
-
timestamp: Date.now(),
|
|
347
|
-
};
|
|
348
|
-
setMessages((prev) => [...prev, errorMessage]);
|
|
349
|
-
setIsThinking(false);
|
|
350
|
-
setIsStreaming(false);
|
|
351
|
-
completionRef.current = null;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
}, [inputValue, engine, messages, currentSession, vectorWorker]);
|
|
356
|
-
|
|
357
|
-
const initProgressCallback = (report: InitProgressReport) => {
|
|
358
|
-
console.log('Loading LLM locally', report);
|
|
359
|
-
setLoadingProgress(Math.round(report.progress * 100));
|
|
360
|
-
if (report.progress === 1) {
|
|
361
|
-
setLoading(false);
|
|
362
|
-
}
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
useEffect(() => {
|
|
366
|
-
if (!loading && sendDefaultQuestionToLLM) {
|
|
367
|
-
handleSubmit();
|
|
368
|
-
setSendDefaultQuestionToLLM(false);
|
|
369
|
-
}
|
|
370
|
-
}, [sendDefaultQuestionToLLM, loading]);
|
|
371
|
-
|
|
372
|
-
useEffect(() => {
|
|
373
|
-
const initEngine = async () => {
|
|
374
|
-
try {
|
|
375
|
-
// Cache the LLMs text file
|
|
376
|
-
const engineCreator = CreateWebWorkerMLCEngine;
|
|
377
|
-
const newEngine = await engineCreator(
|
|
378
|
-
new Worker(new URL('../workers/engine.ts', import.meta.url), { type: 'module' }),
|
|
379
|
-
model,
|
|
380
|
-
{ initProgressCallback }
|
|
381
|
-
);
|
|
382
|
-
setEngine(newEngine);
|
|
383
|
-
} catch (error) {
|
|
384
|
-
console.error('Failed to initialize:', error);
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
const importDocuments = async () => {
|
|
389
|
-
const worker = new Worker(new URL('../workers/document-importer.ts', import.meta.url), { type: 'module' });
|
|
390
|
-
worker.postMessage({ init: true });
|
|
391
|
-
setVectorWorker(worker);
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
importDocuments();
|
|
395
|
-
initEngine();
|
|
396
|
-
}, []);
|
|
397
|
-
|
|
398
|
-
// Add new function to handle smooth scrolling
|
|
399
|
-
const scrollToBottom = useCallback((smooth = true) => {
|
|
400
|
-
if (outputRef.current) {
|
|
401
|
-
outputRef.current.scrollTo({
|
|
402
|
-
top: outputRef.current.scrollHeight,
|
|
403
|
-
behavior: smooth ? 'smooth' : 'auto',
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
}, []);
|
|
407
|
-
|
|
408
|
-
// Add effect to scroll when messages change
|
|
409
|
-
useEffect(() => {
|
|
410
|
-
scrollToBottom();
|
|
411
|
-
}, [messages]);
|
|
412
|
-
|
|
413
|
-
// Memoize static JSX elements
|
|
414
|
-
const welcomeMessage = useMemo(
|
|
415
|
-
() => (
|
|
416
|
-
<div id="welcomeMessage" className="flex justify-center items-center h-full">
|
|
417
|
-
<div className="text-center space-y-6 max-w-2xl px-4">
|
|
418
|
-
<div className="flex justify-center">
|
|
419
|
-
<BookOpen size={48} strokeWidth={1.5} className="text-gray-400" />
|
|
420
|
-
</div>
|
|
421
|
-
<div className="space-y-4">
|
|
422
|
-
<h1 className="text-3xl font-semibold text-gray-800">Ask questions about your architecture</h1>
|
|
423
|
-
<p className="text-sm text-gray-500">AI Models are local and do not leave your device.</p>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
</div>
|
|
427
|
-
),
|
|
428
|
-
[]
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
// Memoize the messages list with the new ChatMessage component
|
|
432
|
-
const messagesList = useMemo(
|
|
433
|
-
() => (
|
|
434
|
-
<div className="space-y-4 max-w-[900px] mx-auto">
|
|
435
|
-
{messages.map((message, index) => (
|
|
436
|
-
<ChatMessage key={index} message={message} />
|
|
437
|
-
))}
|
|
438
|
-
{isThinking && (
|
|
439
|
-
<div className="flex justify-start mb-4">
|
|
440
|
-
<div className="flex items-center space-x-2 max-w-[80%] rounded-lg p-3 bg-gray-100 text-gray-800 rounded-bl-none">
|
|
441
|
-
<div className="flex space-x-1">
|
|
442
|
-
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
|
443
|
-
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
|
444
|
-
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
|
445
|
-
</div>
|
|
446
|
-
</div>
|
|
447
|
-
</div>
|
|
448
|
-
)}
|
|
449
|
-
</div>
|
|
450
|
-
),
|
|
451
|
-
[messages, isThinking]
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
// Memoize the input change handler
|
|
455
|
-
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
456
|
-
setInputValue(e.target.value);
|
|
457
|
-
}, []);
|
|
458
|
-
|
|
459
|
-
// If we have an initial query, set the input value
|
|
460
|
-
useEffect(() => {
|
|
461
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
462
|
-
const query = urlParams.get('query');
|
|
463
|
-
if (query) {
|
|
464
|
-
setInputValue(query);
|
|
465
|
-
setSendDefaultQuestionToLLM(true);
|
|
466
|
-
}
|
|
467
|
-
}, []);
|
|
468
|
-
|
|
469
|
-
return (
|
|
470
|
-
<div className="flex-1 flex flex-col overflow-hidden h-[calc(100vh-60px)] w-full">
|
|
471
|
-
{/* Chat Messages */}
|
|
472
|
-
<div id="output" ref={outputRef} className="flex-1 overflow-y-auto p-4 space-y-4 w-full mx-auto">
|
|
473
|
-
{showWelcome || messages.length === 0 ? welcomeMessage : messagesList}
|
|
474
|
-
</div>
|
|
475
|
-
|
|
476
|
-
{/* Loading Status */}
|
|
477
|
-
{loading && (
|
|
478
|
-
<div className="max-w-[900px] mx-auto w-full px-4">
|
|
479
|
-
<div id="loadingStatus" className="mb-2 py-2 px-4 bg-gray-100 text-gray-700 rounded-lg text-center loading-status">
|
|
480
|
-
<span className="block">
|
|
481
|
-
Initializing AI model...
|
|
482
|
-
{loadingProgress > 0 && `(${loadingProgress}%)`}
|
|
483
|
-
</span>
|
|
484
|
-
<span className="block text-xs text-gray-500">
|
|
485
|
-
Loading model into your browser, this may take a minute or two. The first time it will take longer then the model is
|
|
486
|
-
cached.
|
|
487
|
-
</span>
|
|
488
|
-
</div>
|
|
489
|
-
</div>
|
|
490
|
-
)}
|
|
491
|
-
|
|
492
|
-
{/* Input Area */}
|
|
493
|
-
<div className="border-t border-gray-200 p-4 bg-white">
|
|
494
|
-
<div className="max-w-[900px] mx-auto relative">
|
|
495
|
-
<input
|
|
496
|
-
type="text"
|
|
497
|
-
value={inputValue}
|
|
498
|
-
onChange={handleInputChange}
|
|
499
|
-
onKeyPress={(e) => {
|
|
500
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
501
|
-
e.preventDefault();
|
|
502
|
-
handleSubmit();
|
|
503
|
-
}
|
|
504
|
-
}}
|
|
505
|
-
placeholder="What events do we have?"
|
|
506
|
-
className="w-full px-4 py-3 bg-white text-gray-800 rounded-lg border border-gray-200 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 disabled:bg-gray-50 disabled:cursor-not-allowed pr-24"
|
|
507
|
-
disabled={loading || isStreaming}
|
|
508
|
-
/>
|
|
509
|
-
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
510
|
-
{isStreaming ? (
|
|
511
|
-
<button
|
|
512
|
-
onClick={handleStop}
|
|
513
|
-
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium"
|
|
514
|
-
>
|
|
515
|
-
Stop
|
|
516
|
-
</button>
|
|
517
|
-
) : (
|
|
518
|
-
<button
|
|
519
|
-
onClick={handleSubmit}
|
|
520
|
-
disabled={loading || isStreaming}
|
|
521
|
-
className="px-4 py-2 flex items-center bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:bg-gray-200 disabled:cursor-not-allowed text-sm font-medium"
|
|
522
|
-
>
|
|
523
|
-
{/* Add icon */}
|
|
524
|
-
<Send size={16} strokeWidth={1.5} className="mr-2" />
|
|
525
|
-
Send
|
|
526
|
-
</button>
|
|
527
|
-
)}
|
|
528
|
-
</div>
|
|
529
|
-
</div>
|
|
530
|
-
<div className="max-w-[900px] mx-auto flex justify-between">
|
|
531
|
-
{/* show what model is loaded */}
|
|
532
|
-
<p className="text-xs text-gray-400 mt-2">Model: {model}</p>
|
|
533
|
-
<p className="text-xs text-gray-500 mt-2">EventCatalog Chat can make mistakes. Check important info.</p>
|
|
534
|
-
</div>
|
|
535
|
-
</div>
|
|
536
|
-
</div>
|
|
537
|
-
);
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
export default ChatWindow;
|
package/eventcatalog/src/enterprise/eventcatalog-chat/components/workers/document-importer.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { HuggingFaceTransformersEmbeddings } from '@langchain/community/embeddings/huggingface_transformers';
|
|
2
|
-
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
|
|
3
|
-
|
|
4
|
-
const embeddingsInstance = new HuggingFaceTransformersEmbeddings({ model: 'Xenova/all-MiniLM-L6-v2' });
|
|
5
|
-
|
|
6
|
-
// Create the vector store
|
|
7
|
-
const vectorStore = new MemoryVectorStore(embeddingsInstance);
|
|
8
|
-
|
|
9
|
-
let documents: any;
|
|
10
|
-
let embeddings: any;
|
|
11
|
-
|
|
12
|
-
self.onmessage = async (event) => {
|
|
13
|
-
try {
|
|
14
|
-
// Initialize the vector store
|
|
15
|
-
if (event?.data?.init && !documents && !embeddings) {
|
|
16
|
-
const documentsImport = await fetch(`/ai/documents.json`);
|
|
17
|
-
const embeddingsImport = await fetch(`/ai/embeddings.json`);
|
|
18
|
-
|
|
19
|
-
documents = await documentsImport.json();
|
|
20
|
-
embeddings = await embeddingsImport.json();
|
|
21
|
-
|
|
22
|
-
await vectorStore.addVectors(embeddings, documents);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (!event?.data?.input) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Get the results
|
|
30
|
-
const results = await vectorStore.similaritySearchWithScore(event.data.input, event?.data?.similarityResults || 10);
|
|
31
|
-
// Filter out results that don't have a score less than 0.5
|
|
32
|
-
const filteredResults = results.filter((result) => result[1] > 0.1);
|
|
33
|
-
postMessage({ results: filteredResults, action: 'search-results' });
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.log(error);
|
|
36
|
-
self.postMessage({ error: (error as Error).message });
|
|
37
|
-
}
|
|
38
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { APIContext } from 'astro';
|
|
2
|
-
import { askQuestion } from '@enterprise/eventcatalog-chat/utils/ai';
|
|
3
|
-
|
|
4
|
-
// Map the Keys to use in the SDK, astro exports as import.meta.env
|
|
5
|
-
process.env.OPENAI_API_KEY = import.meta.env.OPENAI_API_KEY || '';
|
|
6
|
-
|
|
7
|
-
interface Message {
|
|
8
|
-
content: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const GET = async ({ request }: APIContext<{ question: string; messages: Message[]; additionalContext?: string }>) => {
|
|
12
|
-
// return 404
|
|
13
|
-
return new Response(JSON.stringify({ error: 'Not found' }), {
|
|
14
|
-
status: 404,
|
|
15
|
-
headers: { 'Content-Type': 'application/json' },
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const POST = async ({ request }: APIContext<{ question: string; messages: Message[]; additionalContext?: string }>) => {
|
|
20
|
-
if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === '' || process.env.OPENAI_API_KEY === undefined) {
|
|
21
|
-
return new Response(JSON.stringify({ error: 'OPENAI_API_KEY is not set' }), {
|
|
22
|
-
status: 500,
|
|
23
|
-
headers: { 'Content-Type': 'application/json' },
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const { question, messages, additionalContext } = await request.json();
|
|
29
|
-
|
|
30
|
-
if (!question) {
|
|
31
|
-
return new Response(JSON.stringify({ error: 'Question is required' }), {
|
|
32
|
-
status: 400,
|
|
33
|
-
headers: { 'Content-Type': 'application/json' },
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Assuming askQuestion returns a ReadableStream
|
|
38
|
-
const answerStream = await askQuestion(question, messages, additionalContext);
|
|
39
|
-
|
|
40
|
-
return answerStream.toTextStreamResponse({
|
|
41
|
-
headers: {
|
|
42
|
-
'Content-Encoding': 'none',
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
} catch (error: any) {
|
|
46
|
-
console.error('Error processing POST request:', error);
|
|
47
|
-
return new Response(JSON.stringify({ error: 'Failed to process request: ' + error.message }), {
|
|
48
|
-
status: 500,
|
|
49
|
-
headers: { 'Content-Type': 'application/json' },
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export const prerender = false;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { APIContext } from 'astro';
|
|
2
|
-
import { getResources } from '@enterprise/eventcatalog-chat/utils/ai';
|
|
3
|
-
|
|
4
|
-
interface Message {
|
|
5
|
-
content: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const GET = async ({ request }: APIContext<{ question: string; messages: Message[]; additionalContext?: string }>) => {
|
|
9
|
-
// return 404
|
|
10
|
-
return new Response(JSON.stringify({ error: 'Not found' }), {
|
|
11
|
-
status: 404,
|
|
12
|
-
headers: { 'Content-Type': 'application/json' },
|
|
13
|
-
});
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const POST = async ({ request }: APIContext<{ question: string; messages: Message[] }>) => {
|
|
17
|
-
try {
|
|
18
|
-
const { question } = await request.json();
|
|
19
|
-
|
|
20
|
-
if (!question) {
|
|
21
|
-
return new Response(JSON.stringify({ error: 'Question is required' }), {
|
|
22
|
-
status: 400,
|
|
23
|
-
headers: { 'Content-Type': 'application/json' },
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
// // Assuming askQuestion returns a ReadableStream
|
|
27
|
-
const resources = await getResources(question);
|
|
28
|
-
|
|
29
|
-
return new Response(JSON.stringify({ resources }), {
|
|
30
|
-
status: 200,
|
|
31
|
-
headers: { 'Content-Type': 'application/json' },
|
|
32
|
-
});
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error('Error processing POST request:', error);
|
|
35
|
-
return new Response(JSON.stringify({ error: 'Failed to process request' }), {
|
|
36
|
-
status: 500,
|
|
37
|
-
headers: { 'Content-Type': 'application/json' },
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const prerender = false;
|