@hed-hog/core 0.0.327 → 0.0.329

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 (153) hide show
  1. package/dist/ai/ai.service.d.ts +1 -1
  2. package/dist/auth/auth.controller.d.ts +2 -2
  3. package/dist/auth/auth.service.d.ts +2 -2
  4. package/dist/core/system.controller.d.ts +1 -1
  5. package/dist/core/system.service.d.ts +1 -1
  6. package/dist/core.module.d.ts.map +1 -1
  7. package/dist/core.module.js +4 -0
  8. package/dist/core.module.js.map +1 -1
  9. package/dist/dashboard/dashboard/dashboard.controller.d.ts +21 -21
  10. package/dist/dashboard/dashboard/dashboard.service.d.ts +21 -21
  11. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +11 -11
  12. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +11 -11
  13. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +5 -5
  14. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +5 -5
  15. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +4 -4
  16. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +4 -4
  17. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +4 -4
  18. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +4 -4
  19. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +3 -3
  20. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +3 -3
  21. package/dist/file/file.controller.d.ts +6 -6
  22. package/dist/file/file.service.d.ts +12 -12
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +6 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/mail/mail.controller.d.ts +2 -2
  28. package/dist/mail/mail.service.d.ts +2 -2
  29. package/dist/mail-sent/mail-sent.controller.d.ts +3 -3
  30. package/dist/mail-sent/mail-sent.service.d.ts +3 -3
  31. package/dist/mcp/decorators/mcp-tool.decorator.d.ts +4 -0
  32. package/dist/mcp/decorators/mcp-tool.decorator.d.ts.map +1 -0
  33. package/dist/mcp/decorators/mcp-tool.decorator.js +8 -0
  34. package/dist/mcp/decorators/mcp-tool.decorator.js.map +1 -0
  35. package/dist/mcp/guards/mcp-auth.guard.d.ts +9 -0
  36. package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -0
  37. package/dist/mcp/guards/mcp-auth.guard.js +41 -0
  38. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -0
  39. package/dist/mcp/guards/mcp-permission.guard.d.ts +8 -0
  40. package/dist/mcp/guards/mcp-permission.guard.d.ts.map +1 -0
  41. package/dist/mcp/guards/mcp-permission.guard.js +57 -0
  42. package/dist/mcp/guards/mcp-permission.guard.js.map +1 -0
  43. package/dist/mcp/interfaces/mcp-tool-definition.interface.d.ts +7 -0
  44. package/dist/mcp/interfaces/mcp-tool-definition.interface.d.ts.map +1 -0
  45. package/dist/mcp/interfaces/mcp-tool-definition.interface.js +3 -0
  46. package/dist/mcp/interfaces/mcp-tool-definition.interface.js.map +1 -0
  47. package/dist/mcp/mcp-audit.service.d.ts +17 -0
  48. package/dist/mcp/mcp-audit.service.d.ts.map +1 -0
  49. package/dist/mcp/mcp-audit.service.js +46 -0
  50. package/dist/mcp/mcp-audit.service.js.map +1 -0
  51. package/dist/mcp/mcp-registry.service.d.ts +21 -0
  52. package/dist/mcp/mcp-registry.service.d.ts.map +1 -0
  53. package/dist/mcp/mcp-registry.service.js +69 -0
  54. package/dist/mcp/mcp-registry.service.js.map +1 -0
  55. package/dist/mcp/mcp-server.service.d.ts +12 -0
  56. package/dist/mcp/mcp-server.service.d.ts.map +1 -0
  57. package/dist/mcp/mcp-server.service.js +94 -0
  58. package/dist/mcp/mcp-server.service.js.map +1 -0
  59. package/dist/mcp/mcp.controller.d.ts +8 -0
  60. package/dist/mcp/mcp.controller.d.ts.map +1 -0
  61. package/dist/mcp/mcp.controller.js +54 -0
  62. package/dist/mcp/mcp.controller.js.map +1 -0
  63. package/dist/mcp/mcp.module.d.ts +3 -0
  64. package/dist/mcp/mcp.module.d.ts.map +1 -0
  65. package/dist/mcp/mcp.module.js +49 -0
  66. package/dist/mcp/mcp.module.js.map +1 -0
  67. package/dist/mcp/tools/core-settings.mcp-tools.d.ts +12 -0
  68. package/dist/mcp/tools/core-settings.mcp-tools.d.ts.map +1 -0
  69. package/dist/mcp/tools/core-settings.mcp-tools.js +66 -0
  70. package/dist/mcp/tools/core-settings.mcp-tools.js.map +1 -0
  71. package/dist/mcp/tools/core-users.mcp-tools.d.ts +55 -0
  72. package/dist/mcp/tools/core-users.mcp-tools.d.ts.map +1 -0
  73. package/dist/mcp/tools/core-users.mcp-tools.js +295 -0
  74. package/dist/mcp/tools/core-users.mcp-tools.js.map +1 -0
  75. package/dist/mcp/types/mcp-context.type.d.ts +7 -0
  76. package/dist/mcp/types/mcp-context.type.d.ts.map +1 -0
  77. package/dist/mcp/types/mcp-context.type.js +3 -0
  78. package/dist/mcp/types/mcp-context.type.js.map +1 -0
  79. package/dist/mcp-chat/dto/create-conversation.dto.d.ts +4 -0
  80. package/dist/mcp-chat/dto/create-conversation.dto.d.ts.map +1 -0
  81. package/dist/mcp-chat/dto/create-conversation.dto.js +22 -0
  82. package/dist/mcp-chat/dto/create-conversation.dto.js.map +1 -0
  83. package/dist/mcp-chat/dto/send-message.dto.d.ts +4 -0
  84. package/dist/mcp-chat/dto/send-message.dto.d.ts.map +1 -0
  85. package/dist/mcp-chat/dto/send-message.dto.js +22 -0
  86. package/dist/mcp-chat/dto/send-message.dto.js.map +1 -0
  87. package/dist/mcp-chat/mcp-chat.controller.d.ts +55 -0
  88. package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -0
  89. package/dist/mcp-chat/mcp-chat.controller.js +90 -0
  90. package/dist/mcp-chat/mcp-chat.controller.js.map +1 -0
  91. package/dist/mcp-chat/mcp-chat.module.d.ts +3 -0
  92. package/dist/mcp-chat/mcp-chat.module.d.ts.map +1 -0
  93. package/dist/mcp-chat/mcp-chat.module.js +30 -0
  94. package/dist/mcp-chat/mcp-chat.module.js.map +1 -0
  95. package/dist/mcp-chat/mcp-chat.service.d.ts +50 -0
  96. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -0
  97. package/dist/mcp-chat/mcp-chat.service.js +290 -0
  98. package/dist/mcp-chat/mcp-chat.service.js.map +1 -0
  99. package/dist/menu/menu.controller.d.ts +3 -3
  100. package/dist/menu/menu.service.d.ts +3 -3
  101. package/dist/profile/profile.controller.d.ts +2 -2
  102. package/dist/profile/profile.service.d.ts +2 -2
  103. package/dist/route/dto/create.dto.d.ts +6 -2
  104. package/dist/route/dto/create.dto.d.ts.map +1 -1
  105. package/dist/route/dto/create.dto.js +19 -1
  106. package/dist/route/dto/create.dto.js.map +1 -1
  107. package/dist/route/dto/update.dto.d.ts +4 -0
  108. package/dist/route/dto/update.dto.d.ts.map +1 -1
  109. package/dist/route/dto/update.dto.js +19 -3
  110. package/dist/route/dto/update.dto.js.map +1 -1
  111. package/dist/route/route.service.d.ts +1 -1
  112. package/dist/route/route.service.d.ts.map +1 -1
  113. package/dist/route/route.service.js +18 -11
  114. package/dist/route/route.service.js.map +1 -1
  115. package/dist/setting/setting.controller.d.ts +5 -5
  116. package/dist/setting/setting.service.d.ts +5 -5
  117. package/dist/user/user.controller.d.ts +3 -3
  118. package/dist/user/user.service.d.ts +6 -6
  119. package/hedhog/data/menu.yaml +40 -26
  120. package/hedhog/data/route.yaml +575 -152
  121. package/hedhog/data/setting_group.yaml +1 -1
  122. package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +8 -1
  123. package/hedhog/frontend/app/configurations/layout.tsx.ejs +152 -34
  124. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +574 -0
  125. package/hedhog/frontend/messages/en.json +17 -0
  126. package/hedhog/frontend/messages/pt.json +17 -0
  127. package/hedhog/table/mcp_conversation.yaml +12 -0
  128. package/hedhog/table/mcp_conversation_message.yaml +23 -0
  129. package/hedhog/table/mcp_tool_call_log.yaml +22 -0
  130. package/hedhog/table/route.yaml +33 -22
  131. package/package.json +5 -4
  132. package/src/core.module.ts +14 -10
  133. package/src/index.ts +17 -10
  134. package/src/mcp/decorators/mcp-tool.decorator.ts +7 -0
  135. package/src/mcp/guards/mcp-auth.guard.ts +32 -0
  136. package/src/mcp/guards/mcp-permission.guard.ts +51 -0
  137. package/src/mcp/interfaces/mcp-tool-definition.interface.ts +6 -0
  138. package/src/mcp/mcp-audit.service.ts +36 -0
  139. package/src/mcp/mcp-registry.service.ts +69 -0
  140. package/src/mcp/mcp-server.service.ts +87 -0
  141. package/src/mcp/mcp.controller.ts +30 -0
  142. package/src/mcp/mcp.module.ts +36 -0
  143. package/src/mcp/tools/core-settings.mcp-tools.ts +53 -0
  144. package/src/mcp/tools/core-users.mcp-tools.ts +267 -0
  145. package/src/mcp/types/mcp-context.type.ts +6 -0
  146. package/src/mcp-chat/dto/create-conversation.dto.ts +7 -0
  147. package/src/mcp-chat/dto/send-message.dto.ts +7 -0
  148. package/src/mcp-chat/mcp-chat.controller.ts +60 -0
  149. package/src/mcp-chat/mcp-chat.module.ts +17 -0
  150. package/src/mcp-chat/mcp-chat.service.ts +332 -0
  151. package/src/route/dto/create.dto.ts +32 -14
  152. package/src/route/dto/update.dto.ts +31 -16
  153. package/src/route/route.service.ts +23 -12
@@ -0,0 +1,574 @@
1
+ 'use client';
2
+
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ } from '@/components/ui/alert-dialog';
13
+ import { Button } from '@/components/ui/button';
14
+ import { ScrollArea } from '@/components/ui/scroll-area';
15
+ import { Textarea } from '@/components/ui/textarea';
16
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
17
+ import {
18
+ Bot,
19
+ ChevronDown,
20
+ ChevronRight,
21
+ MessageSquare,
22
+ Plus,
23
+ Send,
24
+ Trash2,
25
+ Wrench,
26
+ } from 'lucide-react';
27
+ import { useTranslations } from 'next-intl';
28
+ import { useEffect, useRef, useState } from 'react';
29
+ import ReactMarkdown from 'react-markdown';
30
+ import remarkGfm from 'remark-gfm';
31
+ import { toast } from 'sonner';
32
+
33
+ type Conversation = {
34
+ id: number;
35
+ title: string | null;
36
+ created_at: string;
37
+ updated_at: string;
38
+ };
39
+
40
+ type Message = {
41
+ id: number;
42
+ conversation_id: number;
43
+ role: 'user' | 'assistant' | 'tool_call' | 'tool_result';
44
+ content: string;
45
+ tool_name: string | null;
46
+ tool_call_id: string | null;
47
+ created_at: string;
48
+ };
49
+
50
+ type MarkdownComponentProps = {
51
+ children?: React.ReactNode;
52
+ className?: string;
53
+ href?: string;
54
+ };
55
+
56
+ const markdownComponents: Record<
57
+ string,
58
+ (props: MarkdownComponentProps) => React.ReactNode
59
+ > = {
60
+ p({ children }) {
61
+ return <p className="mb-2 last:mb-0 leading-relaxed">{children}</p>;
62
+ },
63
+ ul({ children }) {
64
+ return <ul className="mb-2 list-disc pl-4 space-y-1">{children}</ul>;
65
+ },
66
+ ol({ children }) {
67
+ return <ol className="mb-2 list-decimal pl-4 space-y-1">{children}</ol>;
68
+ },
69
+ li({ children }) {
70
+ return <li className="leading-relaxed">{children}</li>;
71
+ },
72
+ strong({ children }) {
73
+ return <strong className="font-semibold">{children}</strong>;
74
+ },
75
+ em({ children }) {
76
+ return <em className="italic">{children}</em>;
77
+ },
78
+ code({ children, className }) {
79
+ const isBlock = className?.includes('language-');
80
+ return isBlock ? (
81
+ <code className="block rounded-md bg-black/10 dark:bg-white/10 px-3 py-2 text-xs font-mono whitespace-pre-wrap my-2">
82
+ {children}
83
+ </code>
84
+ ) : (
85
+ <code className="rounded bg-black/10 dark:bg-white/10 px-1 py-0.5 text-xs font-mono">
86
+ {children}
87
+ </code>
88
+ );
89
+ },
90
+ pre({ children }) {
91
+ return <pre className="my-2 overflow-x-auto rounded-md">{children}</pre>;
92
+ },
93
+ blockquote({ children }) {
94
+ return (
95
+ <blockquote className="border-l-2 border-current/30 pl-3 italic my-2 text-current/70">
96
+ {children}
97
+ </blockquote>
98
+ );
99
+ },
100
+ h1({ children }) {
101
+ return (
102
+ <h1 className="text-base font-bold mb-2 mt-3 first:mt-0">{children}</h1>
103
+ );
104
+ },
105
+ h2({ children }) {
106
+ return (
107
+ <h2 className="text-sm font-bold mb-2 mt-3 first:mt-0">{children}</h2>
108
+ );
109
+ },
110
+ h3({ children }) {
111
+ return (
112
+ <h3 className="text-sm font-semibold mb-1 mt-2 first:mt-0">{children}</h3>
113
+ );
114
+ },
115
+ a({ href, children }) {
116
+ return (
117
+ <a
118
+ href={href}
119
+ target="_blank"
120
+ rel="noopener noreferrer"
121
+ className="underline underline-offset-2 hover:opacity-80"
122
+ >
123
+ {children}
124
+ </a>
125
+ );
126
+ },
127
+ hr() {
128
+ return <hr className="my-3 border-current/20" />;
129
+ },
130
+ table({ children }) {
131
+ return (
132
+ <div className="my-2 overflow-x-auto">
133
+ <table className="text-xs border-collapse w-full">{children}</table>
134
+ </div>
135
+ );
136
+ },
137
+ th({ children }) {
138
+ return (
139
+ <th className="border border-current/20 px-2 py-1 font-semibold text-left bg-current/5">
140
+ {children}
141
+ </th>
142
+ );
143
+ },
144
+ td({ children }) {
145
+ return <td className="border border-current/20 px-2 py-1">{children}</td>;
146
+ },
147
+ };
148
+
149
+ function ToolBadge({
150
+ message,
151
+ variant,
152
+ }: {
153
+ message: Message;
154
+ variant: 'call' | 'result';
155
+ }) {
156
+ const [expanded, setExpanded] = useState(false);
157
+ let parsed: unknown;
158
+ try {
159
+ parsed = JSON.parse(message.content);
160
+ } catch {
161
+ parsed = message.content;
162
+ }
163
+
164
+ const colorClass =
165
+ variant === 'call'
166
+ ? 'border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-800 dark:bg-violet-950 dark:text-violet-300'
167
+ : 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300';
168
+
169
+ const preClass =
170
+ variant === 'call'
171
+ ? 'border-violet-100 bg-violet-50/50 text-violet-800 dark:border-violet-900 dark:bg-violet-950/50 dark:text-violet-200'
172
+ : 'border-emerald-100 bg-emerald-50/50 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/50 dark:text-emerald-200';
173
+
174
+ return (
175
+ <div className="my-1 flex flex-col items-start">
176
+ <button
177
+ type="button"
178
+ className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-colors hover:opacity-80 ${colorClass}`}
179
+ onClick={() => setExpanded((v) => !v)}
180
+ >
181
+ <Wrench className="h-3 w-3 shrink-0" />
182
+ <span className="font-mono">{message.tool_name}</span>
183
+ {expanded ? (
184
+ <ChevronDown className="h-3 w-3 shrink-0" />
185
+ ) : (
186
+ <ChevronRight className="h-3 w-3 shrink-0" />
187
+ )}
188
+ </button>
189
+ {expanded && (
190
+ <pre
191
+ className={`mt-1.5 w-full max-w-lg rounded-lg border px-3 py-2 text-xs font-mono overflow-x-auto whitespace-pre-wrap ${preClass}`}
192
+ >
193
+ {typeof parsed === 'string'
194
+ ? parsed
195
+ : JSON.stringify(parsed, null, 2)}
196
+ </pre>
197
+ )}
198
+ </div>
199
+ );
200
+ }
201
+
202
+ function TypingIndicator() {
203
+ return (
204
+ <div className="flex items-start gap-3">
205
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted ring-2 ring-background">
206
+ <Bot className="h-4 w-4 text-muted-foreground" />
207
+ </div>
208
+ <div className="flex items-center gap-1 rounded-2xl rounded-tl-sm bg-muted px-4 py-3 shadow-sm">
209
+ <span
210
+ className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60 animate-bounce"
211
+ style={{ animationDelay: '0ms' }}
212
+ />
213
+ <span
214
+ className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60 animate-bounce"
215
+ style={{ animationDelay: '150ms' }}
216
+ />
217
+ <span
218
+ className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60 animate-bounce"
219
+ style={{ animationDelay: '300ms' }}
220
+ />
221
+ </div>
222
+ </div>
223
+ );
224
+ }
225
+
226
+ export default function McpChatPage() {
227
+ const { request, currentLocaleCode, refetchUser } = useApp();
228
+ const t = useTranslations('core.McpChatPage');
229
+
230
+ const [selectedConvId, setSelectedConvId] = useState<number | null>(null);
231
+ const [messages, setMessages] = useState<Message[]>([]);
232
+ const [input, setInput] = useState('');
233
+ const [isSending, setIsSending] = useState(false);
234
+ const [convToDelete, setConvToDelete] = useState<Conversation | null>(null);
235
+ const messagesEndRef = useRef<HTMLDivElement>(null);
236
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
237
+
238
+ const { data: conversations, refetch: refetchConvs } = useQuery<
239
+ Conversation[]
240
+ >({
241
+ queryKey: ['mcp-chat-conversations', currentLocaleCode],
242
+ queryFn: async () => {
243
+ const res = await request<Conversation[]>({
244
+ url: '/mcp-chat',
245
+ method: 'GET',
246
+ });
247
+ return res.data;
248
+ },
249
+ });
250
+
251
+ const convList = conversations ?? [];
252
+ const selectedConv = convList.find((c) => c.id === selectedConvId) ?? null;
253
+
254
+ useEffect(() => {
255
+ if (!selectedConvId) return;
256
+ request<Message[]>({
257
+ url: `/mcp-chat/${selectedConvId}/messages`,
258
+ method: 'GET',
259
+ })
260
+ .then((res) => setMessages(res.data))
261
+ .catch(() => setMessages([]));
262
+ }, [selectedConvId]);
263
+
264
+ useEffect(() => {
265
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
266
+ }, [messages, isSending]);
267
+
268
+ // Auto-resize textarea
269
+ useEffect(() => {
270
+ if (textareaRef.current) {
271
+ textareaRef.current.style.height = 'auto';
272
+ textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 300)}px`;
273
+ }
274
+ }, [input]);
275
+
276
+ async function handleNewConversation() {
277
+ try {
278
+ const res = await request<Conversation>({
279
+ url: '/mcp-chat',
280
+ method: 'POST',
281
+ data: {},
282
+ });
283
+ await refetchConvs();
284
+ setSelectedConvId(res.data.id);
285
+ setMessages([]);
286
+ return res.data.id;
287
+ } catch {
288
+ toast.error('Failed to create conversation');
289
+ return null;
290
+ }
291
+ }
292
+
293
+ async function handleSend() {
294
+ if (!input.trim() || isSending) return;
295
+ let activeConvId = selectedConvId;
296
+ if (!activeConvId) {
297
+ activeConvId = await handleNewConversation();
298
+ if (!activeConvId) return;
299
+ }
300
+ setIsSending(true);
301
+ const currentInput = input;
302
+ setInput('');
303
+
304
+ setMessages((prev) => [
305
+ ...prev,
306
+ {
307
+ id: -Date.now(),
308
+ conversation_id: activeConvId!,
309
+ role: 'user',
310
+ content: currentInput,
311
+ tool_name: null,
312
+ tool_call_id: null,
313
+ created_at: new Date().toISOString(),
314
+ },
315
+ ]);
316
+
317
+ try {
318
+ const res = await request<Message[]>({
319
+ url: `/mcp-chat/${activeConvId}/messages`,
320
+ method: 'POST',
321
+ data: { message: currentInput },
322
+ });
323
+ setMessages(res.data);
324
+ await refetchConvs();
325
+ const hadToolUse = res.data.some(
326
+ (m) => m.role === 'tool_call' || m.role === 'tool_result'
327
+ );
328
+ if (hadToolUse) await refetchUser();
329
+ } catch {
330
+ toast.error('Failed to send message');
331
+ setMessages((prev) => prev.filter((m) => m.id !== -Date.now()));
332
+ } finally {
333
+ setIsSending(false);
334
+ }
335
+ }
336
+
337
+ async function handleDeleteConversation() {
338
+ if (!convToDelete) return;
339
+ try {
340
+ await request({
341
+ url: '/mcp-chat',
342
+ method: 'DELETE',
343
+ data: { ids: [convToDelete.id] },
344
+ });
345
+ toast.success('Conversation deleted');
346
+ if (selectedConvId === convToDelete.id) {
347
+ setSelectedConvId(null);
348
+ setMessages([]);
349
+ }
350
+ setConvToDelete(null);
351
+ await refetchConvs();
352
+ } catch {
353
+ toast.error('Failed to delete conversation');
354
+ }
355
+ }
356
+
357
+ const inputArea = (
358
+ <div className="relative flex items-end gap-2 rounded-2xl bg-background px-5 py-4">
359
+ <Textarea
360
+ ref={textareaRef}
361
+ className="resize-none flex-1 border-0 shadow-none focus-visible:ring-0 p-3 min-h-6 max-h-75 overflow-y-auto text-sm leading-relaxed bg-transparent"
362
+ placeholder={t('placeholder')}
363
+ value={input}
364
+ disabled={isSending}
365
+ onChange={(e) => setInput(e.target.value)}
366
+ onKeyDown={(e) => {
367
+ if (e.key === 'Enter' && !e.shiftKey) {
368
+ e.preventDefault();
369
+ void handleSend();
370
+ }
371
+ }}
372
+ rows={1}
373
+ />
374
+ <Button
375
+ size="icon"
376
+ className="shrink-0 h-8 w-8 rounded-xl cursor-pointer"
377
+ disabled={isSending || !input.trim()}
378
+ onClick={() => void handleSend()}
379
+ >
380
+ <Send className="h-3.5 w-3.5" />
381
+ </Button>
382
+ </div>
383
+ );
384
+
385
+ return (
386
+ <div className="flex h-screen overflow-hidden bg-background">
387
+ {/* Sidebar */}
388
+ <aside className="w-64 shrink-0 flex flex-col border-r bg-muted/30">
389
+ <div className="p-3 border-b bg-background/50">
390
+ <Button
391
+ variant="outline"
392
+ className="w-full justify-start gap-2 cursor-pointer rounded-xl h-9 text-sm font-medium"
393
+ onClick={handleNewConversation}
394
+ >
395
+ <Plus className="h-4 w-4" />
396
+ {t('newConversation')}
397
+ </Button>
398
+ </div>
399
+ <div className="px-4 pt-4 pb-1.5">
400
+ <p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
401
+ {t('conversations')}
402
+ </p>
403
+ </div>
404
+ <ScrollArea className="flex-1 px-2 pb-2">
405
+ {convList.length === 0 ? (
406
+ <div className="flex flex-col items-center gap-2 py-8 px-4 text-center">
407
+ <MessageSquare className="h-8 w-8 text-muted-foreground/40" />
408
+ <p className="text-xs text-muted-foreground">
409
+ {t('emptyConversations')}
410
+ </p>
411
+ </div>
412
+ ) : (
413
+ <div className="space-y-0.5 pt-1">
414
+ {convList.map((conv) => (
415
+ <div
416
+ key={conv.id}
417
+ className={`group flex items-center justify-between rounded-xl px-3 py-2 cursor-pointer transition-colors text-sm ${
418
+ selectedConvId === conv.id
419
+ ? 'bg-primary text-primary-foreground'
420
+ : 'hover:bg-accent text-foreground'
421
+ }`}
422
+ onClick={() => {
423
+ setSelectedConvId(conv.id);
424
+ setMessages([]);
425
+ }}
426
+ >
427
+ <div className="flex items-center gap-2 min-w-0 flex-1">
428
+ <MessageSquare className="h-3.5 w-3.5 shrink-0 opacity-60" />
429
+ <span className="truncate text-xs font-medium">
430
+ {conv.title || `#${conv.id}`}
431
+ </span>
432
+ </div>
433
+ <Button
434
+ variant="ghost"
435
+ size="icon"
436
+ className={`h-6 w-6 opacity-0 group-hover:opacity-100 shrink-0 cursor-pointer transition-opacity ${
437
+ selectedConvId === conv.id
438
+ ? 'hover:bg-primary-foreground/20 text-primary-foreground'
439
+ : 'hover:bg-destructive/10 hover:text-destructive'
440
+ }`}
441
+ onClick={(e) => {
442
+ e.stopPropagation();
443
+ setConvToDelete(conv);
444
+ }}
445
+ >
446
+ <Trash2 className="h-3 w-3" />
447
+ </Button>
448
+ </div>
449
+ ))}
450
+ </div>
451
+ )}
452
+ </ScrollArea>
453
+ </aside>
454
+
455
+ {/* Main area */}
456
+ <main className="flex-1 flex flex-col overflow-hidden min-w-0">
457
+ {!selectedConvId ? (
458
+ /* Empty state */
459
+ <div className="flex flex-1 flex-col items-center justify-center gap-8 px-6">
460
+ <div className="flex flex-col items-center gap-3 text-center">
461
+ <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10 ring-1 ring-primary/20">
462
+ <Bot className="h-8 w-8 text-primary" />
463
+ </div>
464
+ <h1 className="text-2xl font-bold tracking-tight">
465
+ {t('title')}
466
+ </h1>
467
+ <p className="text-sm text-muted-foreground max-w-sm">
468
+ {t('description')}
469
+ </p>
470
+ </div>
471
+ <div className="w-full max-w-2xl">{inputArea}</div>
472
+ </div>
473
+ ) : (
474
+ /* Chat state */
475
+ <>
476
+ {/* Header */}
477
+ <header className="h-14 shrink-0 border-b flex items-center px-6 gap-3 bg-background/80 backdrop-blur-sm">
478
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
479
+ <Bot className="h-4 w-4 text-primary" />
480
+ </div>
481
+ <div className="flex flex-col leading-tight">
482
+ <span className="text-sm font-semibold">
483
+ {selectedConv?.title || `Chat #${selectedConvId}`}
484
+ </span>
485
+ <span className="text-xs text-muted-foreground">
486
+ {t('title')}
487
+ </span>
488
+ </div>
489
+ </header>
490
+
491
+ {/* Messages */}
492
+ <ScrollArea className="flex-1 min-h-0 px-4 py-6">
493
+ <div className="flex flex-col gap-4 max-w-3xl mx-auto">
494
+ {messages.map((msg, idx) => {
495
+ if (msg.role === 'tool_call') {
496
+ return (
497
+ <ToolBadge
498
+ key={msg.id ?? idx}
499
+ message={msg}
500
+ variant="call"
501
+ />
502
+ );
503
+ }
504
+ if (msg.role === 'tool_result') {
505
+ return (
506
+ <ToolBadge
507
+ key={msg.id ?? idx}
508
+ message={msg}
509
+ variant="result"
510
+ />
511
+ );
512
+ }
513
+ if (msg.role === 'user') {
514
+ return (
515
+ <div key={msg.id ?? idx} className="flex justify-end">
516
+ <div className="max-w-[75%] rounded-2xl rounded-tr-md bg-primary px-4 py-2.5 text-sm text-primary-foreground shadow-sm">
517
+ {msg.content}
518
+ </div>
519
+ </div>
520
+ );
521
+ }
522
+ return (
523
+ <div key={msg.id ?? idx} className="flex items-start gap-3">
524
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted ring-2 ring-background shadow-sm">
525
+ <Bot className="h-4 w-4 text-muted-foreground" />
526
+ </div>
527
+ <div className="max-w-[75%] rounded-2xl rounded-tl-md bg-muted px-4 py-2.5 text-sm shadow-sm prose-sm">
528
+ <ReactMarkdown
529
+ remarkPlugins={[remarkGfm]}
530
+ components={markdownComponents as never}
531
+ >
532
+ {msg.content}
533
+ </ReactMarkdown>
534
+ </div>
535
+ </div>
536
+ );
537
+ })}
538
+ {isSending && <TypingIndicator />}
539
+ <div ref={messagesEndRef} />
540
+ </div>
541
+ </ScrollArea>
542
+
543
+ {/* Input */}
544
+ <div className="shrink-0 border-t bg-background/80 backdrop-blur-sm px-6 py-4">
545
+ <div className="max-w-3xl mx-auto">{inputArea}</div>
546
+ </div>
547
+ </>
548
+ )}
549
+ </main>
550
+
551
+ <AlertDialog
552
+ open={Boolean(convToDelete)}
553
+ onOpenChange={(open) => {
554
+ if (!open) setConvToDelete(null);
555
+ }}
556
+ >
557
+ <AlertDialogContent>
558
+ <AlertDialogHeader>
559
+ <AlertDialogTitle>{t('deleteConversation')}?</AlertDialogTitle>
560
+ <AlertDialogDescription>
561
+ {convToDelete?.title || `#${convToDelete?.id}`}
562
+ </AlertDialogDescription>
563
+ </AlertDialogHeader>
564
+ <AlertDialogFooter>
565
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
566
+ <AlertDialogAction onClick={handleDeleteConversation}>
567
+ {t('deleteConversation')}
568
+ </AlertDialogAction>
569
+ </AlertDialogFooter>
570
+ </AlertDialogContent>
571
+ </AlertDialog>
572
+ </div>
573
+ );
574
+ }
@@ -1643,5 +1643,22 @@
1643
1643
  "noUsersWithAccess": "No users are currently linked to the assigned roles.",
1644
1644
  "grantedByRole": "Granted by",
1645
1645
  "moreUsers": "+{count} more users"
1646
+ },
1647
+ "McpChatPage": {
1648
+ "title": "MCP Chat",
1649
+ "description": "Chat with AI using MCP tools",
1650
+ "newConversation": "New Conversation",
1651
+ "conversations": "Conversations",
1652
+ "placeholder": "Type your message... (Enter to send, Shift+Enter for new line)",
1653
+ "send": "Send",
1654
+ "sending": "Sending...",
1655
+ "noConversation": "Select or create a conversation",
1656
+ "toolCalled": "Tool called",
1657
+ "toolResult": "Tool result",
1658
+ "deleteConversation": "Delete",
1659
+ "emptyConversations": "No conversations yet",
1660
+ "breadcrumbDashboard": "Dashboard",
1661
+ "breadcrumbManagement": "Management",
1662
+ "breadcrumbCurrent": "MCP Chat"
1646
1663
  }
1647
1664
  }
@@ -1698,5 +1698,22 @@
1698
1698
  "noUsersWithAccess": "Nenhum usuário está vinculado aos cargos atribuídos no momento.",
1699
1699
  "grantedByRole": "Liberado por",
1700
1700
  "moreUsers": "+{count} usuários"
1701
+ },
1702
+ "McpChatPage": {
1703
+ "title": "Chat MCP",
1704
+ "description": "Converse com IA usando ferramentas MCP",
1705
+ "newConversation": "Nova Conversa",
1706
+ "conversations": "Conversas",
1707
+ "placeholder": "Digite sua mensagem... (Enter para enviar, Shift+Enter para nova linha)",
1708
+ "send": "Enviar",
1709
+ "sending": "Enviando...",
1710
+ "noConversation": "Selecione ou crie uma conversa",
1711
+ "toolCalled": "Ferramenta chamada",
1712
+ "toolResult": "Resultado da ferramenta",
1713
+ "deleteConversation": "Excluir",
1714
+ "emptyConversations": "Nenhuma conversa ainda",
1715
+ "breadcrumbDashboard": "Painel",
1716
+ "breadcrumbManagement": "Gerenciamento",
1717
+ "breadcrumbCurrent": "Chat MCP"
1701
1718
  }
1702
1719
  }
@@ -0,0 +1,12 @@
1
+ columns:
2
+ - type: pk
3
+ - name: user_id
4
+ type: fk
5
+ references:
6
+ table: user
7
+ column: id
8
+ onDelete: CASCADE
9
+ - name: title
10
+ isNullable: true
11
+ - type: created_at
12
+ - type: updated_at
@@ -0,0 +1,23 @@
1
+ columns:
2
+ - type: pk
3
+ - name: conversation_id
4
+ type: fk
5
+ references:
6
+ table: mcp_conversation
7
+ column: id
8
+ onDelete: CASCADE
9
+ - name: role
10
+ type: enum
11
+ enum:
12
+ - user
13
+ - assistant
14
+ - tool_call
15
+ - tool_result
16
+ - name: content
17
+ type: text
18
+ - name: tool_name
19
+ isNullable: true
20
+ - name: tool_call_id
21
+ isNullable: true
22
+ - type: created_at
23
+ - type: updated_at
@@ -0,0 +1,22 @@
1
+ columns:
2
+ - type: pk
3
+ - name: user_id
4
+ type: fk
5
+ references:
6
+ table: user
7
+ column: id
8
+ onDelete: SET NULL
9
+ isNullable: true
10
+ - name: tool_name
11
+ - name: input
12
+ type: text
13
+ isNullable: true
14
+ - name: success
15
+ type: boolean
16
+ default: false
17
+ - name: error
18
+ isNullable: true
19
+ - name: duration_ms
20
+ type: int
21
+ - type: created_at
22
+ - type: updated_at