@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.
Files changed (54) hide show
  1. package/README.md +2 -1
  2. package/dist/analytics/analytics.cjs +1 -1
  3. package/dist/analytics/analytics.js +2 -2
  4. package/dist/analytics/log-build.cjs +1 -1
  5. package/dist/analytics/log-build.js +3 -3
  6. package/dist/{chunk-RHCB6E6X.js → chunk-DGRAYXHN.js} +1 -1
  7. package/dist/{chunk-ZPWE3CVX.js → chunk-HDG7YSFG.js} +9 -0
  8. package/dist/{chunk-F22TOAQN.js → chunk-J4VCEL32.js} +1 -1
  9. package/dist/{chunk-Y6K4D4LS.js → chunk-LP6AXVOF.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +21 -2
  13. package/dist/eventcatalog.config.d.cts +1 -0
  14. package/dist/eventcatalog.config.d.ts +1 -0
  15. package/dist/eventcatalog.js +22 -6
  16. package/dist/features.cjs +44 -2
  17. package/dist/features.d.cts +2 -1
  18. package/dist/features.d.ts +2 -1
  19. package/dist/features.js +6 -3
  20. package/eventcatalog/astro.config.mjs +8 -2
  21. package/eventcatalog/src/components/Lists/ProtocolList.tsx +1 -1
  22. package/eventcatalog/src/components/SideBars/ChannelSideBar.astro +3 -3
  23. package/eventcatalog/src/components/SideBars/DomainSideBar.astro +1 -1
  24. package/eventcatalog/src/components/SideBars/FlowSideBar.astro +2 -2
  25. package/eventcatalog/src/components/SideBars/MessageSideBar.astro +5 -5
  26. package/eventcatalog/src/components/SideBars/ServiceSideBar.astro +2 -2
  27. package/eventcatalog/src/components/Tables/columns/DomainTableColumns.tsx +1 -2
  28. package/eventcatalog/src/content.config.ts +13 -2
  29. package/eventcatalog/src/enterprise/collections/chat-prompts.ts +32 -0
  30. package/eventcatalog/src/enterprise/collections/custom-pages.ts +12 -15
  31. package/eventcatalog/src/enterprise/collections/index.ts +2 -0
  32. package/eventcatalog/src/enterprise/custom-documentation/collection.ts +1 -1
  33. package/eventcatalog/src/enterprise/eventcatalog-chat/EventCatalogVectorStore.ts +50 -0
  34. package/eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx +50 -0
  35. package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx +231 -0
  36. package/eventcatalog/src/enterprise/eventcatalog-chat/components/InputModal.tsx +233 -0
  37. package/eventcatalog/src/enterprise/eventcatalog-chat/components/MentionInput.tsx +211 -0
  38. package/eventcatalog/src/enterprise/eventcatalog-chat/components/WelcomePromptArea.tsx +88 -0
  39. package/eventcatalog/src/enterprise/{ai-assistant/components/ChatWindow.tsx → eventcatalog-chat/components/windows/ChatWindow.client.tsx} +3 -5
  40. package/eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.server.tsx +499 -0
  41. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/ai/chat.ts +56 -0
  42. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/ai/resources.ts +42 -0
  43. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +189 -0
  44. package/eventcatalog/src/enterprise/eventcatalog-chat/utils/ai.ts +151 -0
  45. package/eventcatalog/src/enterprise/eventcatalog-chat/utils/chat-prompts.ts +50 -0
  46. package/eventcatalog/src/pages/chat/index.astro +2 -168
  47. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +1 -0
  48. package/package.json +8 -1
  49. package/eventcatalog/src/enterprise/ai-assistant/components/Chat.tsx +0 -16
  50. /package/eventcatalog/src/{shared-collections.ts → content.config-shared-collections.ts} +0 -0
  51. /package/eventcatalog/src/enterprise/{ai-assistant → eventcatalog-chat}/components/ChatSidebar.tsx +0 -0
  52. /package/eventcatalog/src/enterprise/{ai-assistant → eventcatalog-chat}/components/hooks/ChatProvider.tsx +0 -0
  53. /package/eventcatalog/src/enterprise/{ai-assistant → eventcatalog-chat}/components/workers/document-importer.ts +0 -0
  54. /package/eventcatalog/src/enterprise/{ai-assistant → eventcatalog-chat}/components/workers/engine.ts +0 -0
@@ -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;