@eventcatalog/core 2.34.7 → 2.35.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/README.md +2 -1
- 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-RHCB6E6X.js → chunk-DGRAYXHN.js} +1 -1
- package/dist/{chunk-ZPWE3CVX.js → chunk-HDG7YSFG.js} +9 -0
- package/dist/{chunk-F22TOAQN.js → chunk-J4VCEL32.js} +1 -1
- package/dist/{chunk-Y6K4D4LS.js → chunk-LP6AXVOF.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +21 -2
- package/dist/eventcatalog.config.d.cts +1 -0
- package/dist/eventcatalog.config.d.ts +1 -0
- package/dist/eventcatalog.js +22 -6
- package/dist/features.cjs +44 -2
- package/dist/features.d.cts +2 -1
- package/dist/features.d.ts +2 -1
- package/dist/features.js +6 -3
- package/eventcatalog/astro.config.mjs +8 -2
- package/eventcatalog/src/components/Lists/ProtocolList.tsx +1 -1
- package/eventcatalog/src/components/SideBars/ChannelSideBar.astro +3 -3
- package/eventcatalog/src/components/SideBars/DomainSideBar.astro +1 -1
- package/eventcatalog/src/components/SideBars/FlowSideBar.astro +2 -2
- package/eventcatalog/src/components/SideBars/MessageSideBar.astro +5 -5
- package/eventcatalog/src/components/SideBars/ServiceSideBar.astro +2 -2
- package/eventcatalog/src/components/Tables/columns/DomainTableColumns.tsx +1 -2
- package/eventcatalog/src/content.config.ts +13 -2
- package/eventcatalog/src/enterprise/collections/chat-prompts.ts +32 -0
- package/eventcatalog/src/enterprise/collections/custom-pages.ts +12 -15
- package/eventcatalog/src/enterprise/collections/index.ts +2 -0
- package/eventcatalog/src/enterprise/custom-documentation/collection.ts +1 -1
- package/eventcatalog/src/enterprise/eventcatalog-chat/EventCatalogVectorStore.ts +50 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx +50 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx +231 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/InputModal.tsx +233 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/MentionInput.tsx +211 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/WelcomePromptArea.tsx +88 -0
- package/eventcatalog/src/enterprise/{ai-assistant/components/ChatWindow.tsx → eventcatalog-chat/components/windows/ChatWindow.client.tsx} +3 -5
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.server.tsx +499 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/ai/chat.ts +56 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/ai/resources.ts +42 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +189 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/utils/ai.ts +151 -0
- package/eventcatalog/src/enterprise/eventcatalog-chat/utils/chat-prompts.ts +50 -0
- package/eventcatalog/src/pages/chat/index.astro +2 -168
- package/eventcatalog/src/types/react-syntax-highlighter.d.ts +1 -0
- package/package.json +8 -1
- package/eventcatalog/src/enterprise/ai-assistant/components/Chat.tsx +0 -16
- /package/eventcatalog/src/{shared-collections.ts → content.config-shared-collections.ts} +0 -0
- /package/eventcatalog/src/enterprise/{ai-assistant → eventcatalog-chat}/components/ChatSidebar.tsx +0 -0
- /package/eventcatalog/src/enterprise/{ai-assistant → eventcatalog-chat}/components/hooks/ChatProvider.tsx +0 -0
- /package/eventcatalog/src/enterprise/{ai-assistant → eventcatalog-chat}/components/workers/document-importer.ts +0 -0
- /package/eventcatalog/src/enterprise/{ai-assistant → eventcatalog-chat}/components/workers/engine.ts +0 -0
package/eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.server.tsx
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
|
2
|
+
import { Send } from 'lucide-react';
|
|
3
|
+
import { useChat, type Message } from '../hooks/ChatProvider';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import MentionInput from '../MentionInput';
|
|
6
|
+
import InputModal from '../InputModal';
|
|
7
|
+
import type { ChatPromptCategoryGroup, ChatPrompt } from '@enterprise/eventcatalog-chat/utils/chat-prompts';
|
|
8
|
+
import { useMutation } from '@tanstack/react-query';
|
|
9
|
+
import WelcomePromptArea from '../WelcomePromptArea';
|
|
10
|
+
import ChatMessage from '../ChatMessage'; // Import the new component
|
|
11
|
+
|
|
12
|
+
// Update Message type to include resources
|
|
13
|
+
interface Resource {
|
|
14
|
+
id: string;
|
|
15
|
+
type: string;
|
|
16
|
+
url: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ChatWindowProps {
|
|
22
|
+
model?: string;
|
|
23
|
+
max_tokens?: number;
|
|
24
|
+
similarityResults?: number;
|
|
25
|
+
resources: Resource[];
|
|
26
|
+
chatPrompts: ChatPromptCategoryGroup[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ChatWindow = ({
|
|
30
|
+
model = 'o4-mini',
|
|
31
|
+
max_tokens = 4096,
|
|
32
|
+
similarityResults = 50,
|
|
33
|
+
resources: mentionInputResources = [],
|
|
34
|
+
chatPrompts,
|
|
35
|
+
}: ChatWindowProps) => {
|
|
36
|
+
const [messages, setMessages] = useState<Array<Message>>([]);
|
|
37
|
+
const [inputValue, setInputValue] = useState('');
|
|
38
|
+
const [showWelcome, setShowWelcome] = useState(true);
|
|
39
|
+
const [isThinking, setIsThinking] = useState(false);
|
|
40
|
+
const completionRef = useRef<any>(null);
|
|
41
|
+
const outputRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
43
|
+
const [activeCategory, setActiveCategory] = useState<string>(chatPrompts?.[0]?.label || '');
|
|
44
|
+
|
|
45
|
+
// --- New state for input modal ---
|
|
46
|
+
const [promptForInput, setPromptForInput] = useState<ChatPrompt | null>(null);
|
|
47
|
+
const [isInputModalOpen, setIsInputModalOpen] = useState(false);
|
|
48
|
+
// --- End new state ---
|
|
49
|
+
|
|
50
|
+
const { currentSession, storeMessagesToSession, updateSession, isStreaming, setIsStreaming } = useChat();
|
|
51
|
+
|
|
52
|
+
// If the messages change add them to the session
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (currentSession) {
|
|
55
|
+
storeMessagesToSession(currentSession.id, messages);
|
|
56
|
+
}
|
|
57
|
+
}, [messages]);
|
|
58
|
+
|
|
59
|
+
const mutation = useMutation({
|
|
60
|
+
mutationFn: async (input: { question: string; additionalContext?: string }) => {
|
|
61
|
+
const history = messages.map((message) => ({
|
|
62
|
+
createdAt: new Date(message.timestamp),
|
|
63
|
+
content: message.content,
|
|
64
|
+
role: message.isUser ? 'user' : 'assistant', // Correct role mapping
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const chatPromise = fetch('/api/server/ai/chat', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ question: input.question, messages: history, additionalContext: input.additionalContext }),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const resourcesPromise = fetch('/api/server/ai/resources', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({ question: input.question }),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const [chatResponse, resourcesResponse] = await Promise.all([chatPromise, resourcesPromise]);
|
|
80
|
+
const { resources } = await resourcesResponse.json();
|
|
81
|
+
|
|
82
|
+
if (!chatResponse.ok) {
|
|
83
|
+
const chatResponseJson = await chatResponse.json();
|
|
84
|
+
if (chatResponseJson?.error) {
|
|
85
|
+
throw new Error(`Chat API request failed with status ${chatResponse.status}: ${chatResponseJson.error}`);
|
|
86
|
+
} else {
|
|
87
|
+
throw new Error(`Chat API request failed with status ${chatResponse.status}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!chatResponse.body) {
|
|
91
|
+
throw new Error('No response body from chat API');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const reader = chatResponse.body.getReader();
|
|
95
|
+
const decoder = new TextDecoder();
|
|
96
|
+
let responseText = '';
|
|
97
|
+
let isFirstChunk = true;
|
|
98
|
+
|
|
99
|
+
// Start processing the stream
|
|
100
|
+
// eslint-disable-next-line no-constant-condition
|
|
101
|
+
while (true) {
|
|
102
|
+
try {
|
|
103
|
+
const { done, value } = await reader.read();
|
|
104
|
+
if (done) break;
|
|
105
|
+
|
|
106
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
107
|
+
responseText += chunk;
|
|
108
|
+
|
|
109
|
+
if (isFirstChunk) {
|
|
110
|
+
setIsThinking(false);
|
|
111
|
+
setMessages((prev) => [...prev, { content: responseText, isUser: false, timestamp: Date.now() }]);
|
|
112
|
+
isFirstChunk = false;
|
|
113
|
+
} else {
|
|
114
|
+
setMessages((prev) => {
|
|
115
|
+
const newMessages = [...prev];
|
|
116
|
+
const lastMessageIndex = newMessages.length - 1;
|
|
117
|
+
if (lastMessageIndex >= 0 && !newMessages[lastMessageIndex].isUser) {
|
|
118
|
+
newMessages[lastMessageIndex] = {
|
|
119
|
+
...newMessages[lastMessageIndex],
|
|
120
|
+
content: responseText,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return newMessages;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Defer scroll until after state update seems complete
|
|
127
|
+
requestAnimationFrame(() => scrollToBottom(false)); // Use non-smooth scroll during stream
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('Error reading stream:', error);
|
|
130
|
+
setIsThinking(false);
|
|
131
|
+
setIsStreaming(false);
|
|
132
|
+
// Potentially set an error message state here
|
|
133
|
+
throw error; // Re-throw to allow mutation's onError to catch it
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Final state update including resources
|
|
138
|
+
let finalMessages: Message[] = [];
|
|
139
|
+
setMessages((prev) => {
|
|
140
|
+
const newMessages = [...prev];
|
|
141
|
+
const lastMessageIndex = newMessages.length - 1;
|
|
142
|
+
if (lastMessageIndex >= 0 && !newMessages[lastMessageIndex].isUser) {
|
|
143
|
+
newMessages[lastMessageIndex] = {
|
|
144
|
+
...newMessages[lastMessageIndex],
|
|
145
|
+
content: responseText, // Ensure final content is set
|
|
146
|
+
resources: resources,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
finalMessages = newMessages; // Capture the final state
|
|
150
|
+
return newMessages;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Store messages to session AFTER streaming is complete and state is updated
|
|
154
|
+
if (currentSession) {
|
|
155
|
+
storeMessagesToSession(currentSession.id, finalMessages);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Reset flags and scroll smoothly to the end
|
|
159
|
+
setIsThinking(false);
|
|
160
|
+
setIsStreaming(false);
|
|
161
|
+
completionRef.current = null; // Clear ref if needed
|
|
162
|
+
scrollToBottom(); // Smooth scroll after completion
|
|
163
|
+
|
|
164
|
+
return responseText; // Return the complete text
|
|
165
|
+
},
|
|
166
|
+
onError: (error) => {
|
|
167
|
+
console.error('Chat mutation error:', error);
|
|
168
|
+
// Handle error state in UI, e.g., show an error message to the user
|
|
169
|
+
setIsThinking(false);
|
|
170
|
+
setIsStreaming(false);
|
|
171
|
+
// Maybe add an error message to the chat
|
|
172
|
+
setMessages((prev) => [
|
|
173
|
+
...prev,
|
|
174
|
+
{ content: `Sorry, an error occurred: ${error.message}`, isUser: false, timestamp: Date.now() },
|
|
175
|
+
]);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Load messages when session changes
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (currentSession) {
|
|
182
|
+
setMessages(currentSession.messages);
|
|
183
|
+
setShowWelcome(false);
|
|
184
|
+
} else {
|
|
185
|
+
setMessages([]);
|
|
186
|
+
setShowWelcome(true);
|
|
187
|
+
}
|
|
188
|
+
}, [currentSession]);
|
|
189
|
+
|
|
190
|
+
// Add effect to focus input when streaming stops
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (!isStreaming && inputRef.current) {
|
|
193
|
+
inputRef.current.focus();
|
|
194
|
+
}
|
|
195
|
+
}, [isStreaming]);
|
|
196
|
+
|
|
197
|
+
// Helper function to stop the current completion
|
|
198
|
+
const handleStop = useCallback(async () => {
|
|
199
|
+
if (completionRef.current) {
|
|
200
|
+
try {
|
|
201
|
+
setIsStreaming(false);
|
|
202
|
+
setIsThinking(false);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('Error stopping completion:', error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}, []);
|
|
208
|
+
|
|
209
|
+
// New function to handle submitting a question (user input or predefined)
|
|
210
|
+
const submitQuestion = useCallback(
|
|
211
|
+
async (question: string, additionalContext?: string) => {
|
|
212
|
+
if (!question.trim() || isStreaming || isThinking) return;
|
|
213
|
+
|
|
214
|
+
const userMessage: Message = { content: question, isUser: true, timestamp: Date.now() };
|
|
215
|
+
const isFirstMessage = messages.length === 0;
|
|
216
|
+
|
|
217
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
218
|
+
setShowWelcome(false);
|
|
219
|
+
setIsThinking(true);
|
|
220
|
+
setIsStreaming(true);
|
|
221
|
+
setInputValue('');
|
|
222
|
+
|
|
223
|
+
// Scroll to bottom immediately after adding user message and setting thinking state
|
|
224
|
+
requestAnimationFrame(() => scrollToBottom(false));
|
|
225
|
+
|
|
226
|
+
if (currentSession && isFirstMessage) {
|
|
227
|
+
updateSession({
|
|
228
|
+
...currentSession,
|
|
229
|
+
// Use the submitted question (potentially the prompt title) for the session title
|
|
230
|
+
title: question.length > 25 ? `${question.substring(0, 22)}...` : question,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
mutation.mutate({ question, additionalContext });
|
|
235
|
+
},
|
|
236
|
+
[
|
|
237
|
+
currentSession,
|
|
238
|
+
mutation,
|
|
239
|
+
updateSession,
|
|
240
|
+
setIsStreaming,
|
|
241
|
+
setIsThinking,
|
|
242
|
+
setMessages,
|
|
243
|
+
setInputValue,
|
|
244
|
+
messages.length,
|
|
245
|
+
isStreaming,
|
|
246
|
+
isThinking,
|
|
247
|
+
messages,
|
|
248
|
+
]
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// --- New handler for submitting from the modal ---
|
|
252
|
+
const handleSubmitWithInputs = useCallback(
|
|
253
|
+
(prompt: ChatPrompt, inputValues: Record<string, string>) => {
|
|
254
|
+
let finalBody = prompt.body || ''; // Start with the original body
|
|
255
|
+
|
|
256
|
+
// Ensure prompt and prompt.data exist before accessing properties
|
|
257
|
+
if (!prompt || !prompt.data) {
|
|
258
|
+
console.error('handleSubmitWithInputs called without a valid prompt.');
|
|
259
|
+
setIsInputModalOpen(false); // Close modal even on error
|
|
260
|
+
setPromptForInput(null);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const [key, value] of Object.entries(inputValues)) {
|
|
265
|
+
const placeholder = `{{${key}}}`;
|
|
266
|
+
// Replace all occurrences of the placeholder in the body
|
|
267
|
+
finalBody = finalBody.replaceAll(placeholder, value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Submit the original title and the processed body as additional context
|
|
271
|
+
submitQuestion(prompt.data.title, finalBody);
|
|
272
|
+
|
|
273
|
+
setIsInputModalOpen(false); // Close modal
|
|
274
|
+
setPromptForInput(null); // Clear stored prompt
|
|
275
|
+
},
|
|
276
|
+
[submitQuestion]
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// --- Modified handler for clicking a predefined question ---
|
|
280
|
+
const handlePredefinedQuestionClick = useCallback(
|
|
281
|
+
(prompt: ChatPrompt) => {
|
|
282
|
+
// Ensure prompt and prompt.data exist
|
|
283
|
+
if (!prompt || !prompt.data) {
|
|
284
|
+
console.error('handlePredefinedQuestionClick called with invalid prompt:', prompt);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Check if prompt.data and prompt.data.inputs exist and have length > 0
|
|
288
|
+
if (prompt.data?.inputs && prompt.data.inputs.length > 0) {
|
|
289
|
+
setPromptForInput(prompt); // Store the prompt
|
|
290
|
+
setIsInputModalOpen(true); // Open the modal
|
|
291
|
+
} else {
|
|
292
|
+
// No inputs needed, submit directly using title and body
|
|
293
|
+
submitQuestion(prompt.data.title, prompt.body);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
[submitQuestion]
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Handler for standard input submission
|
|
300
|
+
const handleSubmit = useCallback(
|
|
301
|
+
(e?: React.FormEvent) => {
|
|
302
|
+
e?.preventDefault();
|
|
303
|
+
submitQuestion(inputValue); // Use standard input value, no additional context here
|
|
304
|
+
},
|
|
305
|
+
[inputValue, submitQuestion]
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Add new function to handle smooth scrolling
|
|
309
|
+
const scrollToBottom = useCallback((smooth = true) => {
|
|
310
|
+
if (outputRef.current) {
|
|
311
|
+
outputRef.current.scrollTo({
|
|
312
|
+
top: outputRef.current.scrollHeight,
|
|
313
|
+
behavior: smooth ? 'smooth' : 'auto',
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}, []);
|
|
317
|
+
|
|
318
|
+
// Add effect to scroll when messages change or thinking state changes
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
// Scroll immediately for new messages or when thinking starts/stops
|
|
321
|
+
requestAnimationFrame(() => scrollToBottom(messages.length > 0 && !isThinking));
|
|
322
|
+
}, [messages, isThinking, scrollToBottom]);
|
|
323
|
+
|
|
324
|
+
// Memoize the messages list with the new ChatMessage component
|
|
325
|
+
const messagesList = useMemo(
|
|
326
|
+
() => (
|
|
327
|
+
<div className="space-y-4 max-w-[900px] mx-auto">
|
|
328
|
+
{messages.map((message, index) => (
|
|
329
|
+
<ChatMessage key={message.timestamp} message={message} />
|
|
330
|
+
))}
|
|
331
|
+
{isThinking && (
|
|
332
|
+
<div className="flex justify-start mb-4">
|
|
333
|
+
<div className="flex items-center space-x-2 max-w-[80%] rounded-lg p-3 bg-gray-100 text-gray-800 rounded-bl-none">
|
|
334
|
+
<div className="flex space-x-1">
|
|
335
|
+
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
|
336
|
+
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
|
337
|
+
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
),
|
|
344
|
+
[messages, isThinking]
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Memoize the input change handler
|
|
348
|
+
const handleInputChange = useCallback((newValue: string) => {
|
|
349
|
+
setInputValue(newValue);
|
|
350
|
+
}, []);
|
|
351
|
+
|
|
352
|
+
// Memoize the key press handler for MentionInput
|
|
353
|
+
const handleInputKeyPress = useCallback(
|
|
354
|
+
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
355
|
+
// Submit only if Enter is pressed WITHOUT Shift and suggestions are NOT shown
|
|
356
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
357
|
+
// The MentionInput's internal onKeyDown will handle preventDefault
|
|
358
|
+
// if suggestions are shown and Enter is pressed for selection.
|
|
359
|
+
// We check here if it *wasn't* handled for selection, meaning we should submit.
|
|
360
|
+
if (!e.defaultPrevented) {
|
|
361
|
+
handleSubmit();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
[handleSubmit]
|
|
366
|
+
); // Include handleSubmit in dependencies
|
|
367
|
+
|
|
368
|
+
// Effect to update activeCategory if chatPrompts load after initial render
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
if (!activeCategory && chatPrompts && chatPrompts.length > 0) {
|
|
371
|
+
setActiveCategory(chatPrompts[0].label);
|
|
372
|
+
}
|
|
373
|
+
}, [chatPrompts, activeCategory]);
|
|
374
|
+
|
|
375
|
+
// Add Effect for clipboard copy functionality
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
const outputElement = outputRef.current;
|
|
378
|
+
if (!outputElement) return;
|
|
379
|
+
|
|
380
|
+
const handleClick = async (event: MouseEvent) => {
|
|
381
|
+
const button = (event.target as Element).closest<HTMLButtonElement>('[data-copy-button="true"]');
|
|
382
|
+
if (!button) return;
|
|
383
|
+
|
|
384
|
+
const codeToCopy = button.dataset.code;
|
|
385
|
+
if (!codeToCopy) return;
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
await navigator.clipboard.writeText(codeToCopy);
|
|
389
|
+
// Visual feedback: change icon to Check for a short time
|
|
390
|
+
const originalIcon = button.innerHTML;
|
|
391
|
+
button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
|
|
392
|
+
button.disabled = true;
|
|
393
|
+
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
button.innerHTML = originalIcon;
|
|
396
|
+
button.disabled = false;
|
|
397
|
+
}, 1500); // Reset after 1.5 seconds
|
|
398
|
+
} catch (err) {
|
|
399
|
+
console.error('Failed to copy code: ', err);
|
|
400
|
+
// Optional: Provide error feedback to the user
|
|
401
|
+
const originalTitle = button.title;
|
|
402
|
+
button.title = 'Failed to copy!';
|
|
403
|
+
setTimeout(() => {
|
|
404
|
+
button.title = originalTitle;
|
|
405
|
+
}, 1500);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
outputElement.addEventListener('click', handleClick);
|
|
410
|
+
|
|
411
|
+
// Cleanup listener on component unmount
|
|
412
|
+
return () => {
|
|
413
|
+
outputElement.removeEventListener('click', handleClick);
|
|
414
|
+
};
|
|
415
|
+
}, []); // Empty dependency array ensures this runs only once on mount
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<div className="flex-1 flex flex-col overflow-hidden h-[calc(100vh-60px)] w-full bg-white">
|
|
419
|
+
{/* Main content area - renders messages or predefined questions */}
|
|
420
|
+
<div ref={outputRef} className="flex-1 overflow-y-auto">
|
|
421
|
+
{' '}
|
|
422
|
+
{/* Outer container handles scroll OR centering */}
|
|
423
|
+
{messages.length > 0 ? (
|
|
424
|
+
// Render messages when they exist
|
|
425
|
+
<div id="output" className="p-4 space-y-4 w-full max-w-[900px] mx-auto h-full pb-10">
|
|
426
|
+
{messagesList}
|
|
427
|
+
</div>
|
|
428
|
+
) : (
|
|
429
|
+
// Render centered predefined questions when chat is empty
|
|
430
|
+
<WelcomePromptArea
|
|
431
|
+
chatPrompts={chatPrompts}
|
|
432
|
+
activeCategory={activeCategory}
|
|
433
|
+
setActiveCategory={setActiveCategory}
|
|
434
|
+
onPromptClick={handlePredefinedQuestionClick} // Pass the existing handler
|
|
435
|
+
isProcessing={isThinking || isStreaming} // Combine thinking/streaming state
|
|
436
|
+
/>
|
|
437
|
+
)}
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
{/* Input Area (remains at the bottom) */}
|
|
441
|
+
<div className="border-t border-gray-200 p-4 bg-white">
|
|
442
|
+
<div className="max-w-[900px] mx-auto relative">
|
|
443
|
+
{/* Replace standard input with MentionInput */}
|
|
444
|
+
<MentionInput
|
|
445
|
+
suggestions={mentionInputResources.map((resource) => ({
|
|
446
|
+
id: resource.id,
|
|
447
|
+
name: resource.name || '',
|
|
448
|
+
type: resource.type,
|
|
449
|
+
}))}
|
|
450
|
+
trigger="@"
|
|
451
|
+
type="text"
|
|
452
|
+
value={inputValue}
|
|
453
|
+
onChange={handleInputChange}
|
|
454
|
+
onKeyDown={handleInputKeyPress}
|
|
455
|
+
placeholder="Type your message or '@' for events..."
|
|
456
|
+
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"
|
|
457
|
+
disabled={isStreaming || isThinking} // Disable input while streaming/thinking
|
|
458
|
+
/>
|
|
459
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
460
|
+
{isStreaming ? (
|
|
461
|
+
<button
|
|
462
|
+
onClick={handleStop}
|
|
463
|
+
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium"
|
|
464
|
+
>
|
|
465
|
+
Stop
|
|
466
|
+
</button>
|
|
467
|
+
) : (
|
|
468
|
+
<button
|
|
469
|
+
onClick={handleSubmit}
|
|
470
|
+
disabled={!inputValue.trim() || isThinking} // Disable send if input empty or thinking
|
|
471
|
+
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"
|
|
472
|
+
>
|
|
473
|
+
{/* Add icon */}
|
|
474
|
+
<Send size={16} strokeWidth={1.5} className="mr-2" />
|
|
475
|
+
Send
|
|
476
|
+
</button>
|
|
477
|
+
)}
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
<div className="max-w-[900px] mx-auto flex justify-between">
|
|
481
|
+
{/* show what model is loaded */}
|
|
482
|
+
<p className="text-xs text-gray-400 mt-2">Model: {model}</p>
|
|
483
|
+
<p className="text-xs text-gray-500 mt-2">EventCatalog AI can make mistakes. Check important info.</p>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
{/* --- Render Input Modal --- */}
|
|
488
|
+
<InputModal
|
|
489
|
+
isOpen={isInputModalOpen}
|
|
490
|
+
onClose={() => setIsInputModalOpen(false)} // Allow closing the modal
|
|
491
|
+
prompt={promptForInput}
|
|
492
|
+
onSubmit={handleSubmitWithInputs}
|
|
493
|
+
resources={mentionInputResources} // Pass resources here
|
|
494
|
+
/>
|
|
495
|
+
</div>
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
export default ChatWindow;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { askQuestion } from '@enterprise/eventcatalog-chat/utils/ai';
|
|
3
|
+
import config from '@config';
|
|
4
|
+
const output = config.output || 'static';
|
|
5
|
+
|
|
6
|
+
// Map the Keys to use in the SDK, astro exports as import.meta.env
|
|
7
|
+
process.env.OPENAI_API_KEY = import.meta.env.OPENAI_API_KEY || '';
|
|
8
|
+
|
|
9
|
+
interface Message {
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const GET = async ({ request }: APIContext<{ question: string; messages: Message[]; additionalContext?: string }>) => {
|
|
14
|
+
// return 404
|
|
15
|
+
return new Response(JSON.stringify({ error: 'Not found' }), {
|
|
16
|
+
status: 404,
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const POST = async ({ request }: APIContext<{ question: string; messages: Message[]; additionalContext?: string }>) => {
|
|
22
|
+
if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === '' || process.env.OPENAI_API_KEY === undefined) {
|
|
23
|
+
return new Response(JSON.stringify({ error: 'OPENAI_API_KEY is not set' }), {
|
|
24
|
+
status: 500,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const { question, messages, additionalContext } = await request.json();
|
|
31
|
+
|
|
32
|
+
if (!question) {
|
|
33
|
+
return new Response(JSON.stringify({ error: 'Question is required' }), {
|
|
34
|
+
status: 400,
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Assuming askQuestion returns a ReadableStream
|
|
40
|
+
const answerStream = await askQuestion(question, messages, additionalContext);
|
|
41
|
+
|
|
42
|
+
return answerStream.toTextStreamResponse({
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Encoding': 'none',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
} catch (error: any) {
|
|
48
|
+
console.error('Error processing POST request:', error);
|
|
49
|
+
return new Response(JSON.stringify({ error: 'Failed to process request: ' + error.message }), {
|
|
50
|
+
status: 500,
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const prerender = false;
|
|
@@ -0,0 +1,42 @@
|
|
|
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;
|