@eventcatalog/core 3.0.0-beta.2 → 3.0.0-beta.21
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 +10 -0
- package/dist/__mocks__/astro-content.cjs +32 -0
- package/dist/__mocks__/astro-content.d.cts +13 -0
- package/dist/__mocks__/astro-content.d.ts +13 -0
- package/dist/__mocks__/astro-content.js +7 -0
- 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-JSONCD7V.js → chunk-2FUEBPD3.js} +1 -1
- package/dist/{chunk-3W6JYTHP.js → chunk-HABY2LVH.js} +6 -2
- package/dist/{chunk-H4QHE5YZ.js → chunk-KQAMO3R4.js} +1 -1
- package/dist/chunk-Q6KRYWPV.js +44 -0
- package/dist/{chunk-PQL6O5YA.js → chunk-RRP2B7BL.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +84 -65
- package/dist/eventcatalog.config.d.cts +4 -0
- package/dist/eventcatalog.config.d.ts +4 -0
- package/dist/eventcatalog.js +45 -57
- package/dist/generate.cjs +48 -2
- package/dist/generate.js +3 -1
- package/dist/utils/cli-logger.cjs +82 -0
- package/dist/utils/cli-logger.d.cts +10 -0
- package/dist/utils/cli-logger.d.ts +10 -0
- package/dist/utils/cli-logger.js +7 -0
- package/eventcatalog/astro.config.mjs +4 -1
- package/eventcatalog/integrations/ecstudio-watcher.mjs +1 -1
- package/eventcatalog/integrations/eventcatalog-features.ts +69 -0
- package/eventcatalog/public/icons/asyncapi-black.svg +2 -0
- package/eventcatalog/public/icons/graphql-black.svg +1 -0
- package/eventcatalog/public/icons/openapi-black.svg +1 -0
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +821 -0
- package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +24 -0
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +1 -3
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +8 -8
- package/eventcatalog/src/components/Header.astro +25 -5
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +14 -3
- package/eventcatalog/src/components/Search/Search.astro +2 -2
- package/eventcatalog/src/components/Search/SearchModal.tsx +16 -7
- package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +9 -2
- package/eventcatalog/src/components/SideNav/NestedSideBar/builders/domain.ts +7 -6
- package/eventcatalog/src/components/SideNav/NestedSideBar/builders/service.ts +6 -3
- package/eventcatalog/src/components/SideNav/NestedSideBar/builders/shared.ts +1 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +23 -8
- package/eventcatalog/src/components/SideNav/NestedSideBar/sidebar-builder.ts +57 -11
- package/eventcatalog/src/content.config.ts +1 -10
- package/eventcatalog/src/enterprise/ai/chat-api.ts +262 -0
- package/eventcatalog/src/enterprise/auth/[...auth].ts +3 -0
- package/eventcatalog/src/enterprise/auth/login.astro +420 -0
- package/eventcatalog/src/enterprise/collections/index.ts +0 -1
- package/eventcatalog/src/layouts/Footer.astro +8 -5
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +30 -19
- package/eventcatalog/src/pages/_index.astro +8 -9
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +19 -3
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +7 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +5 -5
- package/eventcatalog/src/pages/docs/teams/[id].mdx.ts +36 -0
- package/eventcatalog/src/pages/docs/users/[id].mdx.ts +36 -0
- package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +178 -0
- package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -155
- package/eventcatalog/src/remark-plugins/directives.ts +30 -9
- package/eventcatalog/src/utils/collections/schemas.ts +31 -7
- package/eventcatalog/src/utils/feature.ts +8 -4
- package/eventcatalog/src/utils/resource-files.ts +86 -0
- package/package.json +12 -15
- package/default-files-for-collections/changelogs.md +0 -5
- package/default-files-for-collections/channels.md +0 -8
- package/default-files-for-collections/commands.md +0 -8
- package/default-files-for-collections/domains.md +0 -8
- package/default-files-for-collections/events.md +0 -8
- package/default-files-for-collections/flows.md +0 -11
- package/default-files-for-collections/queries.md +0 -8
- package/default-files-for-collections/services.md +0 -8
- package/default-files-for-collections/ubiquitousLanguages.md +0 -7
- package/eventcatalog/src/enterprise/collections/chat-prompts.ts +0 -32
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx +0 -60
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx +0 -414
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatSidebar.tsx +0 -169
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/InputModal.tsx +0 -244
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/MentionInput.tsx +0 -211
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/WelcomePromptArea.tsx +0 -176
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/default-prompts.ts +0 -93
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/hooks/ChatProvider.tsx +0 -143
- package/eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.server.tsx +0 -387
- package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/chat.ts +0 -59
- package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +0 -104
- package/eventcatalog/src/enterprise/eventcatalog-chat/providers/ai-provider.ts +0 -140
- package/eventcatalog/src/enterprise/eventcatalog-chat/providers/anthropic.ts +0 -28
- package/eventcatalog/src/enterprise/eventcatalog-chat/providers/google.ts +0 -41
- package/eventcatalog/src/enterprise/eventcatalog-chat/providers/index.ts +0 -26
- package/eventcatalog/src/enterprise/eventcatalog-chat/providers/openai.ts +0 -61
- package/eventcatalog/src/enterprise/eventcatalog-chat/utils/chat-prompts.ts +0 -50
- package/eventcatalog/src/pages/auth/login.astro +0 -280
- package/eventcatalog/src/pages/chat/feature.astro +0 -179
- package/eventcatalog/src/pages/chat/index.astro +0 -10
- package/eventcatalog/src/pages/nav-index.json.ts +0 -30
- /package/eventcatalog/src/{pages → enterprise}/auth/error.astro +0 -0
- /package/eventcatalog/src/{middleware-auth.ts → enterprise/auth/middleware/middleware-auth.ts} +0 -0
- /package/eventcatalog/src/{middleware.ts → enterprise/auth/middleware/middleware.ts} +0 -0
- /package/eventcatalog/src/{pages/unauthorized/index.astro → enterprise/auth/unauthorized.astro} +0 -0
- /package/eventcatalog/src/{pages → enterprise}/plans/index.astro +0 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
2
|
+
import { X, Sparkles, Square, Trash2, BookOpen, Copy, Check, Maximize2, Minimize2 } from 'lucide-react';
|
|
3
|
+
import { useChat } from '@ai-sdk/react';
|
|
4
|
+
import ReactMarkdown from 'react-markdown';
|
|
5
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
6
|
+
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
|
7
|
+
import { lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
|
|
8
|
+
import * as Dialog from '@radix-ui/react-dialog';
|
|
9
|
+
|
|
10
|
+
// Code block component with copy functionality
|
|
11
|
+
const CodeBlock = ({ language, children }: { language: string; children: string }) => {
|
|
12
|
+
const [copied, setCopied] = useState(false);
|
|
13
|
+
|
|
14
|
+
const handleCopy = async () => {
|
|
15
|
+
await navigator.clipboard.writeText(children);
|
|
16
|
+
setCopied(true);
|
|
17
|
+
setTimeout(() => setCopied(false), 2000);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="relative group my-2">
|
|
22
|
+
<button
|
|
23
|
+
onClick={handleCopy}
|
|
24
|
+
className="absolute right-2 top-2 p-1.5 rounded bg-gray-700 hover:bg-gray-600 text-gray-300 opacity-0 group-hover:opacity-100 transition-opacity z-10"
|
|
25
|
+
aria-label="Copy code"
|
|
26
|
+
>
|
|
27
|
+
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
28
|
+
</button>
|
|
29
|
+
<SyntaxHighlighter
|
|
30
|
+
language={language}
|
|
31
|
+
style={oneDark}
|
|
32
|
+
customStyle={{
|
|
33
|
+
margin: 0,
|
|
34
|
+
borderRadius: '0.375rem',
|
|
35
|
+
fontSize: '12px',
|
|
36
|
+
padding: '1rem',
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</SyntaxHighlighter>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Get time-based greeting
|
|
46
|
+
const getGreeting = () => {
|
|
47
|
+
const hour = new Date().getHours();
|
|
48
|
+
if (hour < 12) return 'Good morning';
|
|
49
|
+
if (hour < 18) return 'Good afternoon';
|
|
50
|
+
return 'Good evening';
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ============================================
|
|
54
|
+
// SUGGESTED QUESTIONS CONFIGURATION
|
|
55
|
+
// ============================================
|
|
56
|
+
// Each config has a pattern (regex) to match the URL path
|
|
57
|
+
// and an array of questions to show. Questions are checked
|
|
58
|
+
// in order - first matching pattern wins.
|
|
59
|
+
// ============================================
|
|
60
|
+
|
|
61
|
+
interface SuggestedQuestion {
|
|
62
|
+
label: string;
|
|
63
|
+
prompt: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface QuestionConfig {
|
|
67
|
+
pattern: RegExp;
|
|
68
|
+
questions: SuggestedQuestion[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const suggestedQuestionsConfig: QuestionConfig[] = [
|
|
72
|
+
// Message pages (events, commands, queries) - most specific first
|
|
73
|
+
{
|
|
74
|
+
pattern: /^\/docs\/(events|commands|queries)\/.+/,
|
|
75
|
+
questions: [
|
|
76
|
+
{ label: 'Which services publish this?', prompt: 'Who produces this message?' },
|
|
77
|
+
{ label: 'Which services subscribe to this?', prompt: 'Who consumes this message?' },
|
|
78
|
+
{ label: 'View the message schema', prompt: 'Show me the schema for this message' },
|
|
79
|
+
{ label: 'What breaks if this changes?', prompt: 'What services would be affected if this message changes?' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
// AsyncAPI specification page
|
|
83
|
+
{
|
|
84
|
+
pattern: /^\/docs\/services\/.+\/asyncapi\/.+/,
|
|
85
|
+
questions: [
|
|
86
|
+
{ label: 'Summarize this API', prompt: 'Help me understand this AsyncAPI specification' },
|
|
87
|
+
{ label: 'Show all channels', prompt: 'What channels are defined in this AsyncAPI spec?' },
|
|
88
|
+
{ label: 'How do I authenticate?', prompt: 'What authentication is required for this service?' },
|
|
89
|
+
{ label: 'What message formats are used?', prompt: 'What are the message formats and schemas?' },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
// OpenAPI specification page
|
|
93
|
+
{
|
|
94
|
+
pattern: /^\/docs\/services\/.+\/spec\/.+/,
|
|
95
|
+
questions: [
|
|
96
|
+
{ label: 'Summarize this API', prompt: 'Help me understand this OpenAPI specification' },
|
|
97
|
+
{ label: 'Show all endpoints', prompt: 'What endpoints are available in this API?' },
|
|
98
|
+
{ label: 'How do I authenticate?', prompt: 'What authentication is required for this API?' },
|
|
99
|
+
{ label: 'What are the request & response formats?', prompt: 'What are the request and response formats?' },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
// Services page
|
|
103
|
+
{
|
|
104
|
+
pattern: /^\/docs\/services\/.+/,
|
|
105
|
+
questions: [
|
|
106
|
+
{ label: 'Who owns this service?', prompt: 'Who owns this service and how do I contact them?' },
|
|
107
|
+
{ label: 'What does this depend on?', prompt: 'What are the upstream and downstream dependencies of this service?' },
|
|
108
|
+
{ label: 'How do I integrate with this?', prompt: 'How do I integrate with this service?' },
|
|
109
|
+
{ label: 'What messages does this publish?', prompt: 'What messages does this service produce?' },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
// Domains page
|
|
113
|
+
{
|
|
114
|
+
pattern: /^\/docs\/domains\/.+/,
|
|
115
|
+
questions: [
|
|
116
|
+
{ label: 'What services are in this domain?', prompt: 'What services belong to this domain?' },
|
|
117
|
+
{ label: 'What business capability is this?', prompt: 'What business capability does this domain represent?' },
|
|
118
|
+
{ label: 'What events come from this domain?', prompt: 'What events are published by this domain?' },
|
|
119
|
+
{ label: 'Who owns this domain?', prompt: 'Who owns this domain and how do I contact them?' },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
// Any other docs page
|
|
123
|
+
{
|
|
124
|
+
pattern: /^\/docs\/.+/,
|
|
125
|
+
questions: [
|
|
126
|
+
{ label: 'Tell me about this', prompt: 'Tell me more about this page' },
|
|
127
|
+
{ label: 'Who is responsible for this?', prompt: 'Who owns this resource?' },
|
|
128
|
+
{ label: 'What else is related to this?', prompt: 'What other resources are related to this?' },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
// Default questions (fallback)
|
|
132
|
+
{
|
|
133
|
+
pattern: /.*/,
|
|
134
|
+
questions: [
|
|
135
|
+
{ label: 'What domains do we have?', prompt: 'What domains are in my catalog?' },
|
|
136
|
+
{ label: 'Show me all services', prompt: 'What services do I have?' },
|
|
137
|
+
{ label: 'What changed recently?', prompt: 'What are the most recent changes in the catalog?' },
|
|
138
|
+
{ label: 'How does data flow between services?', prompt: 'Show me how data flows between services' },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Get suggested questions based on current URL path
|
|
144
|
+
const getSuggestedQuestions = (pathname: string): SuggestedQuestion[] => {
|
|
145
|
+
for (const config of suggestedQuestionsConfig) {
|
|
146
|
+
if (config.pattern.test(pathname)) {
|
|
147
|
+
return config.questions;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Fallback to last config (default)
|
|
151
|
+
return suggestedQuestionsConfig[suggestedQuestionsConfig.length - 1].questions;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
interface ChatPanelProps {
|
|
155
|
+
isOpen: boolean;
|
|
156
|
+
onClose: () => void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const PANEL_WIDTH = 400;
|
|
160
|
+
|
|
161
|
+
// Staggered fade-in animation styles
|
|
162
|
+
const fadeInStyles = {
|
|
163
|
+
header: {
|
|
164
|
+
animation: 'fadeInDown 0.3s ease-out 0.1s both',
|
|
165
|
+
},
|
|
166
|
+
content: {
|
|
167
|
+
animation: 'fadeInDown 0.3s ease-out 0.2s both',
|
|
168
|
+
},
|
|
169
|
+
input: {
|
|
170
|
+
animation: 'fadeInDown 0.3s ease-out 0.3s both',
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Helper to extract text content from message parts
|
|
175
|
+
const getMessageContent = (message: { parts?: Array<{ type: string; text?: string }> }): string => {
|
|
176
|
+
if (!message.parts) return '';
|
|
177
|
+
return message.parts
|
|
178
|
+
.filter((part): part is { type: 'text'; text: string } => part.type === 'text' && typeof part.text === 'string')
|
|
179
|
+
.map((part) => part.text)
|
|
180
|
+
.join('');
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Skeleton loading component
|
|
184
|
+
const SkeletonLoader = () => (
|
|
185
|
+
<div className="animate-pulse space-y-3">
|
|
186
|
+
<div className="h-4 bg-gray-200 rounded w-[90%]" />
|
|
187
|
+
<div className="h-4 bg-gray-200 rounded w-[75%]" />
|
|
188
|
+
<div className="h-4 bg-gray-200 rounded w-[85%]" />
|
|
189
|
+
<div className="h-4 bg-gray-200 rounded w-[60%]" />
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => {
|
|
194
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
195
|
+
const modalInputRef = useRef<HTMLInputElement>(null);
|
|
196
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
197
|
+
const modalMessagesEndRef = useRef<HTMLDivElement>(null);
|
|
198
|
+
const [inputValue, setInputValue] = useState('');
|
|
199
|
+
const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
|
|
200
|
+
const [pathname, setPathname] = useState('');
|
|
201
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
202
|
+
|
|
203
|
+
// Get current pathname on mount and when panel opens
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
setPathname(window.location.pathname);
|
|
206
|
+
}, [isOpen]);
|
|
207
|
+
|
|
208
|
+
const suggestedQuestions = getSuggestedQuestions(pathname);
|
|
209
|
+
|
|
210
|
+
const { messages, sendMessage, stop, status, setMessages } = useChat({
|
|
211
|
+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Check if the assistant has started outputting content
|
|
215
|
+
const lastAssistantMessage = [...messages].reverse().find((m) => m.role === 'assistant');
|
|
216
|
+
const assistantHasContent = lastAssistantMessage?.parts?.some(
|
|
217
|
+
(p) => p.type === 'text' && (p as { type: 'text'; text: string }).text.length > 0
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Clear waiting state once assistant starts outputting or on error
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (assistantHasContent || status === 'error') {
|
|
223
|
+
setIsWaitingForResponse(false);
|
|
224
|
+
}
|
|
225
|
+
}, [assistantHasContent, status]);
|
|
226
|
+
|
|
227
|
+
const isStreaming = status === 'streaming' && assistantHasContent;
|
|
228
|
+
const isThinking = isWaitingForResponse || ((status === 'submitted' || status === 'streaming') && !assistantHasContent);
|
|
229
|
+
const isLoading = isThinking || isStreaming;
|
|
230
|
+
|
|
231
|
+
// Scroll to bottom when new messages arrive
|
|
232
|
+
const scrollToBottom = useCallback(() => {
|
|
233
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
234
|
+
}, []);
|
|
235
|
+
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
scrollToBottom();
|
|
238
|
+
// Also scroll modal messages
|
|
239
|
+
modalMessagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
240
|
+
}, [messages, scrollToBottom]);
|
|
241
|
+
|
|
242
|
+
// Focus modal input when fullscreen opens
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (isFullscreen && !isLoading) {
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
modalInputRef.current?.focus();
|
|
247
|
+
}, 100);
|
|
248
|
+
}
|
|
249
|
+
}, [isFullscreen, isLoading]);
|
|
250
|
+
|
|
251
|
+
// Handle escape key to close
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
254
|
+
if (e.key === 'Escape' && isOpen) {
|
|
255
|
+
onClose();
|
|
256
|
+
}
|
|
257
|
+
// Focus input on CMD+I or CTRL+I
|
|
258
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'i' && isOpen) {
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
inputRef.current?.focus();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
document.addEventListener('keydown', handleEscape);
|
|
264
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
265
|
+
}, [isOpen, onClose]);
|
|
266
|
+
|
|
267
|
+
// Focus input when opened and not loading
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
if (isOpen && !isLoading) {
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
inputRef.current?.focus();
|
|
272
|
+
}, 100);
|
|
273
|
+
}
|
|
274
|
+
}, [isOpen, isLoading]);
|
|
275
|
+
|
|
276
|
+
// Add/remove padding to main content when sidebar panel is open
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
const contentEl = document.getElementById('content');
|
|
279
|
+
if (!contentEl) return;
|
|
280
|
+
|
|
281
|
+
// Add transition if not already present
|
|
282
|
+
if (!contentEl.style.transition) {
|
|
283
|
+
contentEl.style.transition = 'padding-right 300ms cubic-bezier(0.16, 1, 0.3, 1)';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Only add padding when panel is open AND not in fullscreen mode
|
|
287
|
+
if (isOpen && !isFullscreen) {
|
|
288
|
+
contentEl.style.paddingRight = '23rem';
|
|
289
|
+
} else {
|
|
290
|
+
contentEl.style.paddingRight = '0';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Cleanup on unmount
|
|
294
|
+
return () => {
|
|
295
|
+
contentEl.style.paddingRight = '0';
|
|
296
|
+
};
|
|
297
|
+
}, [isOpen, isFullscreen]);
|
|
298
|
+
|
|
299
|
+
// Submit message handler
|
|
300
|
+
const submitMessage = useCallback(
|
|
301
|
+
(text: string) => {
|
|
302
|
+
if (!text.trim() || isLoading) return;
|
|
303
|
+
setInputValue('');
|
|
304
|
+
setIsWaitingForResponse(true);
|
|
305
|
+
sendMessage({ text });
|
|
306
|
+
},
|
|
307
|
+
[isLoading, sendMessage]
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Handle suggested action clicks
|
|
311
|
+
const handleSuggestedAction = useCallback(
|
|
312
|
+
(prompt: string) => {
|
|
313
|
+
submitMessage(prompt);
|
|
314
|
+
},
|
|
315
|
+
[submitMessage]
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Handle textarea enter key
|
|
319
|
+
const handleKeyDown = useCallback(
|
|
320
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
321
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
322
|
+
e.preventDefault();
|
|
323
|
+
submitMessage(inputValue);
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
[inputValue, submitMessage]
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// Handle form submit
|
|
330
|
+
const handleSubmit = useCallback(
|
|
331
|
+
(e: React.FormEvent) => {
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
submitMessage(inputValue);
|
|
334
|
+
},
|
|
335
|
+
[inputValue, submitMessage]
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const hasMessages = messages.length > 0;
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<>
|
|
342
|
+
{/* Keyframes for fade-in animation */}
|
|
343
|
+
<style>{`
|
|
344
|
+
@keyframes fadeInDown {
|
|
345
|
+
from {
|
|
346
|
+
opacity: 0;
|
|
347
|
+
transform: translateY(-8px);
|
|
348
|
+
}
|
|
349
|
+
to {
|
|
350
|
+
opacity: 1;
|
|
351
|
+
transform: translateY(0);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
@keyframes pulse-glow {
|
|
355
|
+
0%, 100% { box-shadow: 0 0 8px rgba(147, 51, 234, 0.3); }
|
|
356
|
+
50% { box-shadow: 0 0 16px rgba(147, 51, 234, 0.5); }
|
|
357
|
+
}
|
|
358
|
+
`}</style>
|
|
359
|
+
|
|
360
|
+
{/* Panel - hidden when fullscreen modal is open */}
|
|
361
|
+
{!isFullscreen && (
|
|
362
|
+
<div
|
|
363
|
+
className="fixed top-0 right-0 h-[100vh] z-[200] bg-gradient-to-b from-white via-white to-gray-50/80 border-l border-gray-200/80 flex flex-col overflow-hidden"
|
|
364
|
+
style={{
|
|
365
|
+
width: `${PANEL_WIDTH}px`,
|
|
366
|
+
transform: isOpen ? 'translateX(0)' : `translateX(${PANEL_WIDTH}px)`,
|
|
367
|
+
transition: 'transform 300ms cubic-bezier(0.16, 1, 0.3, 1)',
|
|
368
|
+
boxShadow: '-8px 0 24px -4px rgba(0, 0, 0, 0.1)',
|
|
369
|
+
}}
|
|
370
|
+
>
|
|
371
|
+
{/* Purple accent line at top */}
|
|
372
|
+
<div className="h-1 bg-gradient-to-r from-purple-500 via-purple-600 to-indigo-600" />
|
|
373
|
+
|
|
374
|
+
{/* Header */}
|
|
375
|
+
<div
|
|
376
|
+
className="flex-none bg-gradient-to-b from-purple-50/50 to-transparent shrink-0"
|
|
377
|
+
style={isOpen ? fadeInStyles.header : undefined}
|
|
378
|
+
key={isOpen ? 'header-open' : 'header-closed'}
|
|
379
|
+
>
|
|
380
|
+
<div className="flex items-center justify-between px-5 py-3">
|
|
381
|
+
<div className="flex items-center space-x-2.5">
|
|
382
|
+
<div className="p-1.5 bg-purple-100 rounded-lg relative">
|
|
383
|
+
<BookOpen size={16} className="text-purple-600" />
|
|
384
|
+
<Sparkles size={8} className="text-purple-400 absolute -top-0.5 -right-0.5" />
|
|
385
|
+
</div>
|
|
386
|
+
<span className="font-semibold text-gray-900 text-[15px]">EventCatalog Assistant</span>
|
|
387
|
+
</div>
|
|
388
|
+
<div className="flex items-center space-x-1">
|
|
389
|
+
<button
|
|
390
|
+
onClick={() => setIsFullscreen(true)}
|
|
391
|
+
className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
|
|
392
|
+
aria-label="Expand to fullscreen"
|
|
393
|
+
title="Expand"
|
|
394
|
+
>
|
|
395
|
+
<Maximize2 size={16} />
|
|
396
|
+
</button>
|
|
397
|
+
{hasMessages && (
|
|
398
|
+
<button
|
|
399
|
+
onClick={() => setMessages([])}
|
|
400
|
+
className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
|
|
401
|
+
aria-label="Clear chat"
|
|
402
|
+
title="Clear chat"
|
|
403
|
+
>
|
|
404
|
+
<Trash2 size={18} />
|
|
405
|
+
</button>
|
|
406
|
+
)}
|
|
407
|
+
<button
|
|
408
|
+
onClick={onClose}
|
|
409
|
+
className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
|
|
410
|
+
aria-label="Close chat panel"
|
|
411
|
+
>
|
|
412
|
+
<X size={18} />
|
|
413
|
+
</button>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
{/* Thinking indicator */}
|
|
417
|
+
{isThinking && (
|
|
418
|
+
<div className="px-5 pb-2 flex items-center gap-2">
|
|
419
|
+
<div className="w-2 h-2 bg-purple-500 rounded-full animate-pulse" />
|
|
420
|
+
<span className="text-xs text-gray-500">Thinking...</span>
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
{/* Content */}
|
|
426
|
+
<div
|
|
427
|
+
className="flex-1 flex flex-col min-h-0 relative overflow-hidden"
|
|
428
|
+
style={isOpen ? fadeInStyles.content : undefined}
|
|
429
|
+
key={isOpen ? 'content-open' : 'content-closed'}
|
|
430
|
+
>
|
|
431
|
+
{/* Messages or Welcome area */}
|
|
432
|
+
<div className="flex-1 overflow-y-auto px-6 scrollbar-hide">
|
|
433
|
+
{!hasMessages ? (
|
|
434
|
+
/* Welcome area */
|
|
435
|
+
<div className="flex flex-col h-full justify-between pt-6 pb-2">
|
|
436
|
+
{/* Center content */}
|
|
437
|
+
<div className="flex-1 flex flex-col items-center justify-center">
|
|
438
|
+
{/* Animated Icon */}
|
|
439
|
+
<div className="relative mb-6">
|
|
440
|
+
<div className="absolute inset-0 bg-purple-400/20 rounded-2xl blur-xl animate-pulse" />
|
|
441
|
+
<div className="relative w-16 h-16 rounded-2xl bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
|
|
442
|
+
<BookOpen size={28} className="text-white" strokeWidth={1.5} />
|
|
443
|
+
<Sparkles size={12} className="text-purple-200 absolute -top-1 -right-1 animate-pulse" />
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
{/* Greeting with gradient */}
|
|
448
|
+
<h2 className="text-xl font-semibold bg-gradient-to-r from-purple-600 to-indigo-600 bg-clip-text text-transparent mb-1">
|
|
449
|
+
{getGreeting()}
|
|
450
|
+
</h2>
|
|
451
|
+
<p className="text-sm text-gray-500 text-center">I'm here to help you explore your architecture.</p>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
{/* Suggested questions */}
|
|
455
|
+
<div className="flex flex-wrap gap-2 mt-6">
|
|
456
|
+
{suggestedQuestions.map((question, index) => (
|
|
457
|
+
<button
|
|
458
|
+
key={index}
|
|
459
|
+
onClick={() => handleSuggestedAction(question.prompt)}
|
|
460
|
+
className="px-3 py-1.5 text-xs text-gray-600 bg-white border border-gray-200 hover:border-purple-300 hover:bg-purple-50 hover:text-purple-700 rounded-full transition-all shadow-sm"
|
|
461
|
+
>
|
|
462
|
+
{question.label}
|
|
463
|
+
</button>
|
|
464
|
+
))}
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
) : (
|
|
468
|
+
/* Messages area */
|
|
469
|
+
<div className="py-4 space-y-4">
|
|
470
|
+
{messages.map((message) => {
|
|
471
|
+
const content = getMessageContent(message);
|
|
472
|
+
return (
|
|
473
|
+
<div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
474
|
+
{message.role === 'user' ? (
|
|
475
|
+
<div className="max-w-[85%] rounded-2xl rounded-br-md px-4 py-2.5 bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-sm">
|
|
476
|
+
<p className="text-sm font-light whitespace-pre-wrap">{content}</p>
|
|
477
|
+
</div>
|
|
478
|
+
) : (
|
|
479
|
+
<div className="w-full text-gray-700">
|
|
480
|
+
<div className="prose prose-sm max-w-none prose-p:my-2 prose-p:font-normal prose-p:text-[13px] prose-headings:my-3 prose-headings:font-medium prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5 prose-li:text-[13px] prose-li:font-normal text-[13px] font-light">
|
|
481
|
+
<ReactMarkdown
|
|
482
|
+
components={{
|
|
483
|
+
a: ({ ...props }) => (
|
|
484
|
+
<a
|
|
485
|
+
{...props}
|
|
486
|
+
target="_blank"
|
|
487
|
+
rel="noopener noreferrer"
|
|
488
|
+
className="text-purple-600 hover:text-purple-800 underline"
|
|
489
|
+
/>
|
|
490
|
+
),
|
|
491
|
+
code: ({ children, className, ...props }) => {
|
|
492
|
+
const isInline = !className;
|
|
493
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
494
|
+
const language = match ? match[1] : 'text';
|
|
495
|
+
const codeString = String(children).replace(/\n$/, '');
|
|
496
|
+
|
|
497
|
+
return isInline ? (
|
|
498
|
+
<code
|
|
499
|
+
className="px-1 py-0.5 rounded text-xs font-mono bg-gray-100 text-gray-800"
|
|
500
|
+
{...props}
|
|
501
|
+
>
|
|
502
|
+
{children}
|
|
503
|
+
</code>
|
|
504
|
+
) : (
|
|
505
|
+
<CodeBlock language={language}>{codeString}</CodeBlock>
|
|
506
|
+
);
|
|
507
|
+
},
|
|
508
|
+
}}
|
|
509
|
+
>
|
|
510
|
+
{content}
|
|
511
|
+
</ReactMarkdown>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
);
|
|
517
|
+
})}
|
|
518
|
+
|
|
519
|
+
{/* Skeleton loading indicator */}
|
|
520
|
+
{isThinking && (
|
|
521
|
+
<div className="w-full">
|
|
522
|
+
<SkeletonLoader />
|
|
523
|
+
</div>
|
|
524
|
+
)}
|
|
525
|
+
|
|
526
|
+
{/* Error message as chat bubble */}
|
|
527
|
+
{status === 'error' && (
|
|
528
|
+
<div className="flex justify-start">
|
|
529
|
+
<div className="w-full">
|
|
530
|
+
<div className="flex items-start gap-2 text-red-600 text-sm">
|
|
531
|
+
<span className="shrink-0">⚠️</span>
|
|
532
|
+
<span>Something went wrong. Please try again.</span>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
)}
|
|
537
|
+
|
|
538
|
+
<div ref={messagesEndRef} />
|
|
539
|
+
</div>
|
|
540
|
+
)}
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
{/* Input area (Fixed at bottom) */}
|
|
544
|
+
<div
|
|
545
|
+
className="flex-none px-4 py-3 pb-2 bg-gradient-to-t from-gray-50 to-transparent border-t border-gray-100"
|
|
546
|
+
style={isOpen ? fadeInStyles.input : undefined}
|
|
547
|
+
key={isOpen ? 'input-open' : 'input-closed'}
|
|
548
|
+
>
|
|
549
|
+
<form onSubmit={handleSubmit}>
|
|
550
|
+
<div className="relative bg-white rounded-xl border border-gray-200 focus-within:border-purple-300 focus-within:ring-2 focus-within:ring-purple-100 transition-all shadow-sm">
|
|
551
|
+
<input
|
|
552
|
+
ref={inputRef}
|
|
553
|
+
type="text"
|
|
554
|
+
value={inputValue}
|
|
555
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
556
|
+
onKeyDown={(e) => {
|
|
557
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
558
|
+
e.preventDefault();
|
|
559
|
+
submitMessage(inputValue);
|
|
560
|
+
}
|
|
561
|
+
}}
|
|
562
|
+
placeholder="Ask anything about your architecture..."
|
|
563
|
+
disabled={isLoading}
|
|
564
|
+
className="w-full px-3 py-2.5 pr-16 bg-transparent text-gray-900 placeholder-gray-400 focus:outline-none text-sm disabled:opacity-50 rounded-xl"
|
|
565
|
+
/>
|
|
566
|
+
<div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-10">
|
|
567
|
+
{isStreaming ? (
|
|
568
|
+
<button
|
|
569
|
+
type="button"
|
|
570
|
+
onClick={() => stop()}
|
|
571
|
+
className="p-1.5 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
|
572
|
+
aria-label="Stop generating"
|
|
573
|
+
>
|
|
574
|
+
<Square size={12} fill="currentColor" />
|
|
575
|
+
</button>
|
|
576
|
+
) : (
|
|
577
|
+
<button
|
|
578
|
+
type="submit"
|
|
579
|
+
disabled={!inputValue.trim() || isLoading}
|
|
580
|
+
className="px-3 py-1 bg-purple-600 text-white text-xs font-medium rounded-lg hover:bg-purple-700 disabled:bg-gray-200 disabled:text-gray-400 transition-colors"
|
|
581
|
+
aria-label="Send message"
|
|
582
|
+
>
|
|
583
|
+
Send
|
|
584
|
+
</button>
|
|
585
|
+
)}
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
</form>
|
|
589
|
+
|
|
590
|
+
<p className="text-[9px] text-gray-400 mt-2 text-center">AI can make mistakes. Verify important info.</p>
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
)}
|
|
595
|
+
|
|
596
|
+
{/* Fullscreen Modal */}
|
|
597
|
+
<Dialog.Root
|
|
598
|
+
open={isFullscreen}
|
|
599
|
+
onOpenChange={(open) => {
|
|
600
|
+
setIsFullscreen(open);
|
|
601
|
+
// If modal is being closed (clicking outside, etc.), close the chat entirely
|
|
602
|
+
if (!open) {
|
|
603
|
+
onClose();
|
|
604
|
+
}
|
|
605
|
+
}}
|
|
606
|
+
>
|
|
607
|
+
<Dialog.Portal>
|
|
608
|
+
<Dialog.Overlay className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[300]" />
|
|
609
|
+
<Dialog.Content className="fixed inset-y-4 left-1/2 -translate-x-1/2 w-[95%] max-w-5xl md:inset-y-8 rounded-2xl bg-white shadow-2xl z-[301] flex flex-col overflow-hidden focus:outline-none border border-gray-200">
|
|
610
|
+
{/* Purple accent line at top */}
|
|
611
|
+
<div className="h-1 bg-gradient-to-r from-purple-500 via-purple-600 to-indigo-600" />
|
|
612
|
+
|
|
613
|
+
{/* Modal Header */}
|
|
614
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100 bg-gradient-to-b from-purple-50/50 to-transparent flex-shrink-0">
|
|
615
|
+
<div className="flex items-center space-x-3">
|
|
616
|
+
<div className="p-2 bg-purple-100 rounded-xl relative">
|
|
617
|
+
<BookOpen size={20} className="text-purple-600" />
|
|
618
|
+
<Sparkles size={10} className="text-purple-400 absolute -top-0.5 -right-0.5" />
|
|
619
|
+
</div>
|
|
620
|
+
<Dialog.Title className="text-lg font-semibold text-gray-900">EventCatalog Assistant</Dialog.Title>
|
|
621
|
+
</div>
|
|
622
|
+
<div className="flex items-center space-x-2">
|
|
623
|
+
{hasMessages && (
|
|
624
|
+
<button
|
|
625
|
+
onClick={() => setMessages([])}
|
|
626
|
+
className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
|
|
627
|
+
aria-label="Clear chat"
|
|
628
|
+
title="Clear chat"
|
|
629
|
+
>
|
|
630
|
+
<Trash2 size={18} />
|
|
631
|
+
</button>
|
|
632
|
+
)}
|
|
633
|
+
<button
|
|
634
|
+
onClick={() => setIsFullscreen(false)}
|
|
635
|
+
className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
|
|
636
|
+
aria-label="Exit fullscreen"
|
|
637
|
+
title="Exit fullscreen"
|
|
638
|
+
>
|
|
639
|
+
<Minimize2 size={18} />
|
|
640
|
+
</button>
|
|
641
|
+
<button
|
|
642
|
+
onClick={() => {
|
|
643
|
+
setIsFullscreen(false);
|
|
644
|
+
onClose();
|
|
645
|
+
}}
|
|
646
|
+
className="p-2 rounded-lg hover:bg-gray-100 text-gray-400 hover:text-gray-600 transition-colors"
|
|
647
|
+
aria-label="Close"
|
|
648
|
+
>
|
|
649
|
+
<X size={18} />
|
|
650
|
+
</button>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
|
|
654
|
+
{/* Thinking indicator */}
|
|
655
|
+
{isThinking && (
|
|
656
|
+
<div className="px-6 py-2 flex items-center gap-2 border-b border-gray-100">
|
|
657
|
+
<div className="w-2 h-2 bg-purple-500 rounded-full animate-pulse" />
|
|
658
|
+
<span className="text-sm text-gray-500">Thinking...</span>
|
|
659
|
+
</div>
|
|
660
|
+
)}
|
|
661
|
+
|
|
662
|
+
{/* Modal Content */}
|
|
663
|
+
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
664
|
+
{!hasMessages ? (
|
|
665
|
+
/* Welcome area */
|
|
666
|
+
<div className="flex flex-col h-full justify-center items-center">
|
|
667
|
+
{/* Animated Icon */}
|
|
668
|
+
<div className="relative mb-8">
|
|
669
|
+
<div className="absolute inset-0 bg-purple-400/20 rounded-2xl blur-xl animate-pulse" />
|
|
670
|
+
<div className="relative w-20 h-20 rounded-2xl bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
|
|
671
|
+
<BookOpen size={36} className="text-white" strokeWidth={1.5} />
|
|
672
|
+
<Sparkles size={14} className="text-purple-200 absolute -top-1 -right-1 animate-pulse" />
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
{/* Greeting with gradient */}
|
|
677
|
+
<h2 className="text-2xl font-semibold bg-gradient-to-r from-purple-600 to-indigo-600 bg-clip-text text-transparent mb-2">
|
|
678
|
+
{getGreeting()}
|
|
679
|
+
</h2>
|
|
680
|
+
<p className="text-gray-500 text-center mb-10">I'm here to help you explore your architecture.</p>
|
|
681
|
+
|
|
682
|
+
{/* Suggested questions */}
|
|
683
|
+
<div className="flex flex-wrap gap-2 justify-center max-w-lg">
|
|
684
|
+
{suggestedQuestions.map((question, index) => (
|
|
685
|
+
<button
|
|
686
|
+
key={index}
|
|
687
|
+
onClick={() => handleSuggestedAction(question.prompt)}
|
|
688
|
+
className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 hover:border-purple-300 hover:bg-purple-50 hover:text-purple-700 rounded-full transition-all shadow-sm"
|
|
689
|
+
>
|
|
690
|
+
{question.label}
|
|
691
|
+
</button>
|
|
692
|
+
))}
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
) : (
|
|
696
|
+
/* Messages area */
|
|
697
|
+
<div className="max-w-3xl mx-auto space-y-4">
|
|
698
|
+
{messages.map((message) => {
|
|
699
|
+
const content = getMessageContent(message);
|
|
700
|
+
return (
|
|
701
|
+
<div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
702
|
+
{message.role === 'user' ? (
|
|
703
|
+
<div className="max-w-[75%] rounded-2xl rounded-br-md px-5 py-3 bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-sm">
|
|
704
|
+
<p className="text-sm font-light whitespace-pre-wrap">{content}</p>
|
|
705
|
+
</div>
|
|
706
|
+
) : (
|
|
707
|
+
<div className="w-full text-gray-700">
|
|
708
|
+
<div className="prose prose-sm max-w-none">
|
|
709
|
+
<ReactMarkdown
|
|
710
|
+
components={{
|
|
711
|
+
a: ({ ...props }) => (
|
|
712
|
+
<a
|
|
713
|
+
{...props}
|
|
714
|
+
target="_blank"
|
|
715
|
+
rel="noopener noreferrer"
|
|
716
|
+
className="text-purple-600 hover:text-purple-800 underline"
|
|
717
|
+
/>
|
|
718
|
+
),
|
|
719
|
+
code: ({ children, className, ...props }) => {
|
|
720
|
+
const isInline = !className;
|
|
721
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
722
|
+
const language = match ? match[1] : 'text';
|
|
723
|
+
const codeString = String(children).replace(/\n$/, '');
|
|
724
|
+
|
|
725
|
+
return isInline ? (
|
|
726
|
+
<code
|
|
727
|
+
className="px-1.5 py-0.5 rounded text-sm font-mono bg-gray-100 text-gray-800"
|
|
728
|
+
{...props}
|
|
729
|
+
>
|
|
730
|
+
{children}
|
|
731
|
+
</code>
|
|
732
|
+
) : (
|
|
733
|
+
<CodeBlock language={language}>{codeString}</CodeBlock>
|
|
734
|
+
);
|
|
735
|
+
},
|
|
736
|
+
}}
|
|
737
|
+
>
|
|
738
|
+
{content}
|
|
739
|
+
</ReactMarkdown>
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
</div>
|
|
744
|
+
);
|
|
745
|
+
})}
|
|
746
|
+
|
|
747
|
+
{isThinking && (
|
|
748
|
+
<div className="w-full max-w-md">
|
|
749
|
+
<SkeletonLoader />
|
|
750
|
+
</div>
|
|
751
|
+
)}
|
|
752
|
+
|
|
753
|
+
{/* Error message as chat bubble */}
|
|
754
|
+
{status === 'error' && (
|
|
755
|
+
<div className="flex justify-start">
|
|
756
|
+
<div className="w-full">
|
|
757
|
+
<div className="flex items-start gap-2 text-red-600 text-sm">
|
|
758
|
+
<span className="shrink-0">⚠️</span>
|
|
759
|
+
<span>Something went wrong. Please try again.</span>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
)}
|
|
764
|
+
|
|
765
|
+
<div ref={modalMessagesEndRef} />
|
|
766
|
+
</div>
|
|
767
|
+
)}
|
|
768
|
+
</div>
|
|
769
|
+
|
|
770
|
+
{/* Modal Input area */}
|
|
771
|
+
<div className="flex-shrink-0 px-6 py-4 border-t border-gray-100 bg-gradient-to-t from-gray-50 to-transparent">
|
|
772
|
+
<form onSubmit={handleSubmit} className="max-w-3xl mx-auto">
|
|
773
|
+
<div className="relative bg-white rounded-xl border border-gray-200 focus-within:border-purple-300 focus-within:ring-2 focus-within:ring-purple-100 transition-all shadow-sm">
|
|
774
|
+
<input
|
|
775
|
+
ref={modalInputRef}
|
|
776
|
+
type="text"
|
|
777
|
+
value={inputValue}
|
|
778
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
779
|
+
onKeyDown={(e) => {
|
|
780
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
781
|
+
e.preventDefault();
|
|
782
|
+
submitMessage(inputValue);
|
|
783
|
+
}
|
|
784
|
+
}}
|
|
785
|
+
placeholder="Ask anything about your architecture..."
|
|
786
|
+
disabled={isLoading}
|
|
787
|
+
className="w-full px-4 py-3 pr-20 bg-transparent text-gray-900 placeholder-gray-400 focus:outline-none text-sm disabled:opacity-50 rounded-xl"
|
|
788
|
+
/>
|
|
789
|
+
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
|
|
790
|
+
{isStreaming ? (
|
|
791
|
+
<button
|
|
792
|
+
type="button"
|
|
793
|
+
onClick={() => stop()}
|
|
794
|
+
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
|
795
|
+
aria-label="Stop generating"
|
|
796
|
+
>
|
|
797
|
+
<Square size={16} fill="currentColor" />
|
|
798
|
+
</button>
|
|
799
|
+
) : (
|
|
800
|
+
<button
|
|
801
|
+
type="submit"
|
|
802
|
+
disabled={!inputValue.trim() || isLoading}
|
|
803
|
+
className="px-4 py-1.5 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700 disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed transition-colors"
|
|
804
|
+
aria-label="Send message"
|
|
805
|
+
>
|
|
806
|
+
Send
|
|
807
|
+
</button>
|
|
808
|
+
)}
|
|
809
|
+
</div>
|
|
810
|
+
</div>
|
|
811
|
+
</form>
|
|
812
|
+
<p className="text-xs text-gray-400 mt-2 text-center">AI can make mistakes. Verify important info.</p>
|
|
813
|
+
</div>
|
|
814
|
+
</Dialog.Content>
|
|
815
|
+
</Dialog.Portal>
|
|
816
|
+
</Dialog.Root>
|
|
817
|
+
</>
|
|
818
|
+
);
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
export default ChatPanel;
|