@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
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { projectDirBase } from '../../content.config';
|
|
1
|
+
import { z } from 'astro:content';
|
|
2
|
+
import { badge, ownerReference } from '../../content.config-shared-collections';
|
|
4
3
|
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
id: z.string(),
|
|
12
|
-
title: z.string(),
|
|
13
|
-
description: z.string(),
|
|
14
|
-
sidebar: z.object({
|
|
4
|
+
export const customPagesSchema = z.object({
|
|
5
|
+
title: z.string(),
|
|
6
|
+
summary: z.string(),
|
|
7
|
+
slug: z.string().optional(),
|
|
8
|
+
sidebar: z
|
|
9
|
+
.object({
|
|
15
10
|
label: z.string(),
|
|
16
11
|
order: z.number(),
|
|
17
|
-
})
|
|
18
|
-
|
|
12
|
+
})
|
|
13
|
+
.optional(),
|
|
14
|
+
owners: z.array(ownerReference).optional(),
|
|
15
|
+
badges: z.array(badge).optional(),
|
|
19
16
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { HuggingFaceTransformersEmbeddings } from '@langchain/community/embeddings/huggingface_transformers';
|
|
2
|
+
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
|
|
3
|
+
import { Document as LangChainDocument } from 'langchain/document';
|
|
4
|
+
|
|
5
|
+
const embeddingsInstance = new HuggingFaceTransformersEmbeddings({ model: 'Xenova/all-MiniLM-L6-v2' });
|
|
6
|
+
|
|
7
|
+
export interface Resource {
|
|
8
|
+
id: string;
|
|
9
|
+
type: string;
|
|
10
|
+
url: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class EventCatalogVectorStore {
|
|
15
|
+
private vectorStore: MemoryVectorStore;
|
|
16
|
+
|
|
17
|
+
// Make the constructor private so it can only be called from within the class
|
|
18
|
+
private constructor(vectorStore: MemoryVectorStore) {
|
|
19
|
+
this.vectorStore = vectorStore;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Static async factory method
|
|
23
|
+
public static async create(documents: LangChainDocument[], embeddings: number[][]): Promise<EventCatalogVectorStore> {
|
|
24
|
+
const vectorStore = new MemoryVectorStore(embeddingsInstance);
|
|
25
|
+
await vectorStore.addVectors(embeddings, documents);
|
|
26
|
+
return new EventCatalogVectorStore(vectorStore);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async search(query: string) {
|
|
30
|
+
return this.vectorStore.similaritySearch(query, 10);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getEventCatalogResources(query: string): Promise<Resource[]> {
|
|
34
|
+
const results = await this.vectorStore.similaritySearchWithScore(query, 50);
|
|
35
|
+
return Array.from(
|
|
36
|
+
new Map(
|
|
37
|
+
results.map((result: any) => {
|
|
38
|
+
const metadata = result[0].metadata;
|
|
39
|
+
const resource: Resource = {
|
|
40
|
+
id: metadata.id,
|
|
41
|
+
type: metadata.type,
|
|
42
|
+
url: `/docs/${metadata.type}s/${metadata.id}`,
|
|
43
|
+
title: metadata.title || metadata.id,
|
|
44
|
+
};
|
|
45
|
+
return [metadata.id, resource]; // Use ID as key for Map
|
|
46
|
+
})
|
|
47
|
+
).values()
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Sidebar from './ChatSidebar';
|
|
2
|
+
import { ChatProvider } from './hooks/ChatProvider';
|
|
3
|
+
import ChatWindowWebLLM from './windows/ChatWindow.client';
|
|
4
|
+
import ChatWindowServer from './windows/ChatWindow.server';
|
|
5
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
6
|
+
import type { ChatPromptCategoryGroup } from '@enterprise/eventcatalog-chat/utils/chat-prompts';
|
|
7
|
+
import config from '@config';
|
|
8
|
+
const output = config.output || 'static';
|
|
9
|
+
interface Resource {
|
|
10
|
+
id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Chat component has two modes:
|
|
17
|
+
* - Client: ChatWindow.client.tsx (uses webllm)
|
|
18
|
+
* - Server: ChatWindow.server.tsx (uses server-side code, bring your own API key)
|
|
19
|
+
*
|
|
20
|
+
* The mode is determined by the config.output property in the eventcatalog.config.js file.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const Chat = ({
|
|
24
|
+
chatConfig,
|
|
25
|
+
resources,
|
|
26
|
+
chatPrompts,
|
|
27
|
+
}: {
|
|
28
|
+
chatConfig: any;
|
|
29
|
+
resources: Resource[];
|
|
30
|
+
chatPrompts: ChatPromptCategoryGroup[];
|
|
31
|
+
}) => {
|
|
32
|
+
const queryClient = new QueryClient();
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ChatProvider>
|
|
36
|
+
<div className="flex overflow-hidden w-full">
|
|
37
|
+
<Sidebar />
|
|
38
|
+
{output === 'server' ? (
|
|
39
|
+
<QueryClientProvider client={queryClient}>
|
|
40
|
+
<ChatWindowServer {...chatConfig} resources={resources} chatPrompts={chatPrompts} />
|
|
41
|
+
</QueryClientProvider>
|
|
42
|
+
) : (
|
|
43
|
+
<ChatWindowWebLLM {...chatConfig} />
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
</ChatProvider>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default Chat;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import ReactMarkdown, { type ExtraProps } from 'react-markdown';
|
|
3
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
4
|
+
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
5
|
+
import type { Message } from '@enterprise/eventcatalog-chat/components/hooks/ChatProvider';
|
|
6
|
+
import * as Dialog from '@radix-ui/react-dialog';
|
|
7
|
+
import { Fullscreen, X, Clipboard, Check, ChevronDown, ChevronRight } from 'lucide-react'; // Add Clipboard, Check, ChevronDown, ChevronRight icons
|
|
8
|
+
|
|
9
|
+
// Define Resource type locally
|
|
10
|
+
interface Resource {
|
|
11
|
+
id: string;
|
|
12
|
+
type: string;
|
|
13
|
+
url: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
name?: string | null; // Allow null for name
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ChatMessageProps {
|
|
19
|
+
message: Message;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Function to escape special characters for regex
|
|
23
|
+
function escapeRegex(string: string): string {
|
|
24
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Define props for the code component explicitly
|
|
28
|
+
interface CodeComponentProps extends React.HTMLAttributes<HTMLElement>, ExtraProps {
|
|
29
|
+
inline?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ChatMessage = React.memo(({ message }: ChatMessageProps) => {
|
|
33
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
34
|
+
const [modalContent, setModalContent] = useState<{ language: string; code: string } | null>(null);
|
|
35
|
+
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({}); // State for copy feedback
|
|
36
|
+
const [isResourcesCollapsed, setIsResourcesCollapsed] = useState(true); // State for resource section collapse
|
|
37
|
+
|
|
38
|
+
// Helper to get display name for resource, ensuring a fallback
|
|
39
|
+
const getResourceDisplayName = (resource: Resource): string => {
|
|
40
|
+
return resource.title || resource.name || resource.id || 'Resource'; // Added fallback
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleCopy = (codeToCopy: string, id: string) => {
|
|
44
|
+
navigator.clipboard
|
|
45
|
+
.writeText(codeToCopy)
|
|
46
|
+
.then(() => {
|
|
47
|
+
setCopiedStates((prev) => ({ ...prev, [id]: true }));
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
setCopiedStates((prev) => ({ ...prev, [id]: false }));
|
|
50
|
+
}, 1500); // Reset after 1.5 seconds
|
|
51
|
+
})
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
console.error('Failed to copy code: ', err);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className={`flex ${message.isUser ? 'justify-end' : 'justify-start'} mb-4`}>
|
|
59
|
+
<div
|
|
60
|
+
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'}`}
|
|
61
|
+
>
|
|
62
|
+
{/* Apply prose styles, including prose-invert for user messages for better text contrast */}
|
|
63
|
+
<div className={`prose prose-sm max-w-none ${message.isUser ? 'prose-invert' : ''}`}>
|
|
64
|
+
<ReactMarkdown
|
|
65
|
+
components={{
|
|
66
|
+
code({ node, className, children, ...props }: CodeComponentProps) {
|
|
67
|
+
const inline = props.inline;
|
|
68
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
69
|
+
const codeString = String(children);
|
|
70
|
+
const language = match ? match[1] : 'text';
|
|
71
|
+
const codeBlockId = `code-${React.useId()}`;
|
|
72
|
+
const isCopied = copiedStates[codeBlockId];
|
|
73
|
+
|
|
74
|
+
const handleOpenModal = () => {
|
|
75
|
+
setModalContent({ language, code: codeString.replace(/\n$/, '') });
|
|
76
|
+
setIsModalOpen(true);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Heuristic: Treat as inline if it doesn't contain newlines OR if explicitly inline.
|
|
80
|
+
// This handles parser quirks with single-line snippets in lists, etc.
|
|
81
|
+
const treatAsInline = inline || !codeString.includes('\n');
|
|
82
|
+
|
|
83
|
+
return !treatAsInline ? (
|
|
84
|
+
<div className="code-block bg-[#1e1e1e] rounded-md overflow-hidden my-4 relative group">
|
|
85
|
+
<div className="flex justify-between items-center px-4 py-1.5 bg-gray-700 text-gray-300 text-xs">
|
|
86
|
+
<span>{language}</span>
|
|
87
|
+
<div className="flex items-center gap-2">
|
|
88
|
+
<button
|
|
89
|
+
onClick={() => handleCopy(codeString.replace(/\n$/, ''), codeBlockId)}
|
|
90
|
+
className="text-gray-300 hover:text-white flex items-center"
|
|
91
|
+
aria-label={isCopied ? 'Copied' : 'Copy code'}
|
|
92
|
+
>
|
|
93
|
+
{isCopied ? <Check size={14} className="text-green-400" /> : <Clipboard size={14} />}
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
onClick={handleOpenModal}
|
|
97
|
+
className="text-gray-300 hover:text-white"
|
|
98
|
+
aria-label="View code fullscreen"
|
|
99
|
+
>
|
|
100
|
+
<Fullscreen size={14} />
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<SyntaxHighlighter
|
|
105
|
+
style={vscDarkPlus as any}
|
|
106
|
+
language={language}
|
|
107
|
+
PreTag="div"
|
|
108
|
+
showLineNumbers
|
|
109
|
+
wrapLines={true}
|
|
110
|
+
customStyle={{ margin: 0, borderRadius: '0 0 0.375rem 0.375rem', padding: '1rem' }}
|
|
111
|
+
>
|
|
112
|
+
{codeString.replace(/\n$/, '')}
|
|
113
|
+
</SyntaxHighlighter>
|
|
114
|
+
</div>
|
|
115
|
+
) : (
|
|
116
|
+
<code
|
|
117
|
+
className={`px-1 py-0.5 rounded text-xs font-mono ${
|
|
118
|
+
message.isUser
|
|
119
|
+
? 'bg-purple-800/70 text-purple-100' // Darker purple bg, light text for user
|
|
120
|
+
: 'bg-gray-300/70 text-gray-900' // Darker gray bg, dark text for assistant
|
|
121
|
+
} ${className || ''}`}
|
|
122
|
+
{...props}
|
|
123
|
+
>
|
|
124
|
+
{/* Render trimmed version for inline code */}
|
|
125
|
+
{codeString.trim()}
|
|
126
|
+
</code>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
a: ({ node, ...props }) => (
|
|
130
|
+
<a {...props} target="_blank" rel="noopener noreferrer" className="text-purple-600 hover:text-purple-800" />
|
|
131
|
+
),
|
|
132
|
+
p: ({ node, ...props }) => <p {...props} className="mb-2 last:mb-0" />,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{/* Use raw content again */}
|
|
136
|
+
{message.content || ''}
|
|
137
|
+
</ReactMarkdown>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{/* Resource section */}
|
|
141
|
+
{!message.isUser && message.resources && message.resources.length > 0 && (
|
|
142
|
+
<div className="mt-3 pt-3 border-t border-gray-200">
|
|
143
|
+
{/* Make the title clickable to toggle collapse */}
|
|
144
|
+
<button
|
|
145
|
+
className="flex items-center text-xs text-gray-500 mb-1 w-full text-left focus:outline-none"
|
|
146
|
+
onClick={() => setIsResourcesCollapsed(!isResourcesCollapsed)}
|
|
147
|
+
aria-expanded={!isResourcesCollapsed}
|
|
148
|
+
aria-controls="resource-list"
|
|
149
|
+
>
|
|
150
|
+
{isResourcesCollapsed ? <ChevronRight size={14} className="mr-1" /> : <ChevronDown size={14} className="mr-1" />}
|
|
151
|
+
Referenced Resources:
|
|
152
|
+
</button>
|
|
153
|
+
{/* Conditionally render the list based on the collapsed state */}
|
|
154
|
+
{!isResourcesCollapsed && (
|
|
155
|
+
<div className="text-[10px] mt-1 pl-5" id="resource-list">
|
|
156
|
+
{' '}
|
|
157
|
+
{/* Added pl-5 for indentation */}
|
|
158
|
+
{(message.resources as Resource[]).map((resource: Resource, idx: number) => (
|
|
159
|
+
<span key={resource.id || `res-${idx}`}>
|
|
160
|
+
<a
|
|
161
|
+
href={resource.url}
|
|
162
|
+
className="text-purple-600 hover:text-purple-800"
|
|
163
|
+
target="_blank"
|
|
164
|
+
rel="noopener noreferrer"
|
|
165
|
+
>
|
|
166
|
+
{/* Use helper function for clarity and add parentheses for operator precedence */}
|
|
167
|
+
{getResourceDisplayName(resource)} ({resource.type})
|
|
168
|
+
</a>
|
|
169
|
+
{idx < (message.resources?.length || 0) - 1 ? ', ' : ''}
|
|
170
|
+
</span>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Fullscreen Code Modal */}
|
|
179
|
+
<Dialog.Root open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
180
|
+
<Dialog.Portal>
|
|
181
|
+
{/* Add z-index and animations to Overlay */}
|
|
182
|
+
{/* NOTE: Define overlayShow/overlayHide keyframes in CSS/Tailwind config */}
|
|
183
|
+
<Dialog.Overlay className="fixed inset-0 bg-black/60 z-50 data-[state=open]:animate-overlayShow data-[state=closed]:animate-overlayHide" />
|
|
184
|
+
{/* Add z-index and animations to Content */}
|
|
185
|
+
{/* NOTE: Define contentShow/contentHide keyframes in CSS/Tailwind config */}
|
|
186
|
+
<Dialog.Content className="fixed left-1/2 top-1/2 z-[100] w-[90vw] max-w-4xl max-h-[85vh] -translate-x-1/2 -translate-y-1/2 rounded-lg bg-[#1e1e1e] p-0 shadow-lg focus:outline-none data-[state=open]:animate-contentShow data-[state=closed]:animate-contentHide overflow-hidden flex flex-col">
|
|
187
|
+
{/* Modal Header with Copy Button */}
|
|
188
|
+
<div className="flex justify-between items-center px-4 py-2.5 bg-gray-700 text-gray-300 text-sm flex-shrink-0 border-b border-gray-600">
|
|
189
|
+
<Dialog.Title className="font-medium">{modalContent?.language}</Dialog.Title>
|
|
190
|
+
<div className="flex items-center gap-3">
|
|
191
|
+
{modalContent && (
|
|
192
|
+
<button
|
|
193
|
+
onClick={() => handleCopy(modalContent.code, 'modal-copy')}
|
|
194
|
+
className="text-gray-300 hover:text-white flex items-center gap-1 text-xs"
|
|
195
|
+
aria-label={copiedStates['modal-copy'] ? 'Copied' : 'Copy code'}
|
|
196
|
+
>
|
|
197
|
+
{copiedStates['modal-copy'] ? <Check size={14} className="text-green-400" /> : <Clipboard size={14} />}
|
|
198
|
+
{copiedStates['modal-copy'] ? 'Copied!' : 'Copy'}
|
|
199
|
+
</button>
|
|
200
|
+
)}
|
|
201
|
+
<Dialog.Close asChild>
|
|
202
|
+
<button className="text-gray-300 hover:text-white" aria-label="Close">
|
|
203
|
+
<X size={18} />
|
|
204
|
+
</button>
|
|
205
|
+
</Dialog.Close>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="flex-grow overflow-auto">
|
|
209
|
+
{modalContent && (
|
|
210
|
+
<SyntaxHighlighter
|
|
211
|
+
style={vscDarkPlus as any}
|
|
212
|
+
language={modalContent.language}
|
|
213
|
+
PreTag="div"
|
|
214
|
+
showLineNumbers
|
|
215
|
+
wrapLines={true}
|
|
216
|
+
customStyle={{ margin: 0, height: '100%', padding: '1rem' }} // Ensure it fills height and has padding
|
|
217
|
+
>
|
|
218
|
+
{modalContent.code}
|
|
219
|
+
</SyntaxHighlighter>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
</Dialog.Content>
|
|
223
|
+
</Dialog.Portal>
|
|
224
|
+
</Dialog.Root>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
ChatMessage.displayName = 'ChatMessage';
|
|
230
|
+
|
|
231
|
+
export default ChatMessage;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import React, { useEffect, useState, useMemo } from 'react';
|
|
2
|
+
import * as Dialog from '@radix-ui/react-dialog';
|
|
3
|
+
// TODO: Import ChatPrompt and ChatPromptInput from a central types location
|
|
4
|
+
import type { ChatPrompt } from '@enterprise/eventcatalog-chat/utils/chat-prompts';
|
|
5
|
+
|
|
6
|
+
// Define the possible resource list types
|
|
7
|
+
type ResourceListType = 'resource-list-events' | 'resource-list-commands' | 'resource-list-services' | 'resource-list-queries';
|
|
8
|
+
|
|
9
|
+
// Define ChatPromptInput type based on usage
|
|
10
|
+
// NOTE: This should ideally match the schema defined for Astro content collections
|
|
11
|
+
interface ChatPromptInput {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
type: 'text' | ResourceListType | 'select' | 'code' | 'text-area'; // Use the union type, add 'select', 'code', 'text-area'
|
|
15
|
+
options?: string[]; // Add optional options for select type
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Define Resource type based on usage in ChatWindow
|
|
19
|
+
interface Resource {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string; // e.g., 'event', 'command', 'service', 'query'
|
|
22
|
+
url: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Input Modal Component ---
|
|
28
|
+
interface InputModalProps {
|
|
29
|
+
isOpen: boolean;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
prompt: ChatPrompt | null;
|
|
32
|
+
onSubmit: (prompt: ChatPrompt, inputValues: Record<string, string>) => void;
|
|
33
|
+
resources: Resource[]; // Add resources prop
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Helper to extract resource type from input type string
|
|
37
|
+
const getResourceTypeFromInputType = (inputType: ResourceListType): string => {
|
|
38
|
+
return inputType.replace('resource-list-', '');
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const InputModal: React.FC<InputModalProps> = ({ isOpen, onClose, prompt, onSubmit, resources }) => {
|
|
42
|
+
const [inputValues, setInputValues] = useState<Record<string, string>>({});
|
|
43
|
+
|
|
44
|
+
// Memoize resource filtering based on prompt inputs
|
|
45
|
+
const filteredResourcesByType = useMemo(() => {
|
|
46
|
+
const resourceMap: Record<string, Resource[]> = {};
|
|
47
|
+
prompt?.data?.inputs?.forEach((input) => {
|
|
48
|
+
if (input.type.startsWith('resource-list-')) {
|
|
49
|
+
const resourceType = getResourceTypeFromInputType(input.type as ResourceListType);
|
|
50
|
+
const filtered = resources.filter((r) => r.type === resourceType);
|
|
51
|
+
resourceMap[input.id] = filtered;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return resourceMap;
|
|
55
|
+
}, [prompt, resources]);
|
|
56
|
+
|
|
57
|
+
// Reset input values when the prompt or resources change
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (prompt?.data?.inputs) {
|
|
60
|
+
const initialValues: Record<string, string> = {};
|
|
61
|
+
prompt.data.inputs.forEach((input) => {
|
|
62
|
+
const relevantResources = filteredResourcesByType[input.id];
|
|
63
|
+
if (input.type.startsWith('resource-list-') && relevantResources?.length > 0) {
|
|
64
|
+
initialValues[input.id] = relevantResources[0].id; // Pre-select the first available resource
|
|
65
|
+
} else {
|
|
66
|
+
initialValues[input.id] = ''; // Initialize others with empty strings
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
setInputValues(initialValues);
|
|
70
|
+
} else {
|
|
71
|
+
setInputValues({}); // Clear if no inputs
|
|
72
|
+
}
|
|
73
|
+
}, [prompt, filteredResourcesByType]); // Depend on memoized filtered resources
|
|
74
|
+
|
|
75
|
+
if (!prompt || !prompt.data?.inputs) {
|
|
76
|
+
return null; // Don't render if no prompt or inputs
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handleInputChange = (id: string, value: string) => {
|
|
80
|
+
setInputValues((prev) => ({ ...prev, [id]: value }));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
// Basic validation: Check if all required fields are filled
|
|
86
|
+
const allFilled = prompt?.data?.inputs?.every((input) => {
|
|
87
|
+
if (input.type.startsWith('resource-list-')) {
|
|
88
|
+
return inputValues[input.id] && inputValues[input.id] !== '';
|
|
89
|
+
}
|
|
90
|
+
return inputValues[input.id]?.trim();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (allFilled) {
|
|
94
|
+
// Pass the selected resource NAME for resource lists, not the ID
|
|
95
|
+
const processedValues = { ...inputValues };
|
|
96
|
+
prompt.data.inputs?.forEach((input) => {
|
|
97
|
+
if (input.type.startsWith('resource-list-')) {
|
|
98
|
+
const relevantResources = filteredResourcesByType[input.id] || [];
|
|
99
|
+
const selectedResource = relevantResources.find((r) => r.id === processedValues[input.id]);
|
|
100
|
+
// Use resource name if available, otherwise fallback to the submitted ID (which might be name if no resource found)
|
|
101
|
+
processedValues[input.id] = selectedResource?.name || processedValues[input.id];
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
onSubmit(prompt, processedValues);
|
|
105
|
+
} else {
|
|
106
|
+
alert('Please fill in all required fields.');
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Dialog.Root open={isOpen} onOpenChange={onClose}>
|
|
112
|
+
<Dialog.Portal>
|
|
113
|
+
<Dialog.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-overlayShow z-50" />
|
|
114
|
+
<Dialog.Content className="fixed top-1/2 left-1/2 w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6 shadow-lg focus:outline-none data-[state=open]:animate-contentShow z-[100]">
|
|
115
|
+
<Dialog.Title className="text-lg font-semibold text-gray-900">{prompt.data.title}</Dialog.Title>
|
|
116
|
+
<Dialog.Description className="mt-1 mb-5 text-sm text-gray-500">
|
|
117
|
+
Please provide the following details:
|
|
118
|
+
</Dialog.Description>
|
|
119
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
120
|
+
{prompt.data.inputs.map((input: ChatPromptInput) => {
|
|
121
|
+
const isResourceList = input.type.startsWith('resource-list-');
|
|
122
|
+
const isSelect = input.type === 'select';
|
|
123
|
+
const isCode = input.type === 'code';
|
|
124
|
+
const isTextArea = input.type === 'text-area'; // Added for potential future use or consistency
|
|
125
|
+
const resourceType = isResourceList ? getResourceTypeFromInputType(input.type as ResourceListType) : '';
|
|
126
|
+
const relevantResources = filteredResourcesByType[input.id] || [];
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div key={input.id}>
|
|
130
|
+
<label htmlFor={input.id} className="block text-sm font-medium text-gray-700 mb-1">
|
|
131
|
+
{input.label}
|
|
132
|
+
</label>
|
|
133
|
+
{isResourceList ? (
|
|
134
|
+
<select
|
|
135
|
+
id={input.id}
|
|
136
|
+
name={input.id}
|
|
137
|
+
value={inputValues[input.id] || ''}
|
|
138
|
+
onChange={(e) => handleInputChange(input.id, e.target.value)}
|
|
139
|
+
required
|
|
140
|
+
className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500 sm:text-sm"
|
|
141
|
+
>
|
|
142
|
+
<option value="" disabled>
|
|
143
|
+
Select a {resourceType}...
|
|
144
|
+
</option>
|
|
145
|
+
{relevantResources.map((resource) => (
|
|
146
|
+
<option key={resource.id} value={resource.id}>
|
|
147
|
+
{resource.name || resource.id} {/* Display name or ID as fallback */}
|
|
148
|
+
</option>
|
|
149
|
+
))}
|
|
150
|
+
{relevantResources.length === 0 && (
|
|
151
|
+
<option value="" disabled>
|
|
152
|
+
No {resourceType}s found
|
|
153
|
+
</option>
|
|
154
|
+
)}
|
|
155
|
+
</select>
|
|
156
|
+
) : isSelect ? (
|
|
157
|
+
<select
|
|
158
|
+
id={input.id}
|
|
159
|
+
name={input.id}
|
|
160
|
+
value={inputValues[input.id] || ''}
|
|
161
|
+
onChange={(e) => handleInputChange(input.id, e.target.value)}
|
|
162
|
+
required
|
|
163
|
+
className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500 sm:text-sm"
|
|
164
|
+
>
|
|
165
|
+
<option value="" disabled>
|
|
166
|
+
Select an option...
|
|
167
|
+
</option>
|
|
168
|
+
{input.options?.map((option) => (
|
|
169
|
+
<option key={option} value={option}>
|
|
170
|
+
{option}
|
|
171
|
+
</option>
|
|
172
|
+
))}
|
|
173
|
+
{(!input.options || input.options.length === 0) && (
|
|
174
|
+
<option value="" disabled>
|
|
175
|
+
No options available
|
|
176
|
+
</option>
|
|
177
|
+
)}
|
|
178
|
+
</select>
|
|
179
|
+
) : (
|
|
180
|
+
<>
|
|
181
|
+
{isCode || isTextArea ? (
|
|
182
|
+
<textarea
|
|
183
|
+
id={input.id}
|
|
184
|
+
name={input.id}
|
|
185
|
+
value={inputValues[input.id] || ''}
|
|
186
|
+
onChange={(e) => handleInputChange(input.id, e.target.value)}
|
|
187
|
+
required
|
|
188
|
+
rows={isCode ? 6 : 3} // More rows for code input
|
|
189
|
+
className={`w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500 sm:text-sm ${isCode ? 'font-mono text-sm' : ''}`} // Basic monospaced font for code
|
|
190
|
+
placeholder={isCode ? 'Paste your code here...' : ''}
|
|
191
|
+
/>
|
|
192
|
+
) : (
|
|
193
|
+
<input
|
|
194
|
+
type="text"
|
|
195
|
+
id={input.id}
|
|
196
|
+
name={input.id}
|
|
197
|
+
value={inputValues[input.id] || ''}
|
|
198
|
+
onChange={(e) => handleInputChange(input.id, e.target.value)}
|
|
199
|
+
required
|
|
200
|
+
className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500 sm:text-sm"
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
</>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
})}
|
|
208
|
+
<div className="mt-6 flex justify-end space-x-2">
|
|
209
|
+
<Dialog.Close asChild>
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
|
|
213
|
+
onClick={onClose} // Ensure onClose is called when Cancel is clicked
|
|
214
|
+
>
|
|
215
|
+
Cancel
|
|
216
|
+
</button>
|
|
217
|
+
</Dialog.Close>
|
|
218
|
+
<button
|
|
219
|
+
type="submit"
|
|
220
|
+
className="inline-flex justify-center rounded-md border border-transparent bg-purple-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
|
|
221
|
+
>
|
|
222
|
+
Submit
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
</form>
|
|
226
|
+
</Dialog.Content>
|
|
227
|
+
</Dialog.Portal>
|
|
228
|
+
</Dialog.Root>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
// --- End Input Modal Component ---
|
|
232
|
+
|
|
233
|
+
export default InputModal;
|