@hed-hog/core 0.0.329 → 0.0.330

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 (103) hide show
  1. package/dist/ai/ai.service.d.ts +1 -1
  2. package/dist/auth/auth.controller.d.ts +23 -23
  3. package/dist/auth/auth.service.d.ts +41 -41
  4. package/dist/challenge/challenge.service.d.ts +14 -14
  5. package/dist/core/system.controller.d.ts +2 -2
  6. package/dist/core/system.service.d.ts +2 -2
  7. package/dist/core.module.d.ts.map +1 -1
  8. package/dist/core.module.js +7 -4
  9. package/dist/core.module.js.map +1 -1
  10. package/dist/dashboard/dashboard/dashboard.controller.d.ts +87 -87
  11. package/dist/dashboard/dashboard/dashboard.service.d.ts +87 -87
  12. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +33 -33
  13. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +33 -33
  14. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +42 -42
  15. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +42 -42
  16. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +12 -12
  17. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +12 -12
  18. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +7 -7
  19. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +7 -7
  20. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +33 -33
  21. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +33 -33
  22. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  23. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  24. package/dist/file/file.controller.d.ts +8 -8
  25. package/dist/file/file.service.d.ts +18 -18
  26. package/dist/index.d.ts +8 -6
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +8 -6
  29. package/dist/index.js.map +1 -1
  30. package/dist/mail/mail.controller.d.ts +15 -15
  31. package/dist/mail/mail.service.d.ts +15 -15
  32. package/dist/mail-profile/mail-profile.controller.d.ts +9 -9
  33. package/dist/mail-profile/mail-profile.service.d.ts +9 -9
  34. package/dist/mail-sent/mail-sent.controller.d.ts +17 -17
  35. package/dist/mail-sent/mail-sent.service.d.ts +17 -17
  36. package/dist/mcp-chat/mcp-chat.controller.d.ts +10 -10
  37. package/dist/mcp-chat/mcp-chat.service.d.ts +10 -10
  38. package/dist/menu/menu.controller.d.ts +11 -11
  39. package/dist/menu/menu.service.d.ts +11 -11
  40. package/dist/oauth/oauth.service.js +1 -1
  41. package/dist/oauth/oauth.service.js.map +1 -1
  42. package/dist/pdf/components/index.d.ts +57 -0
  43. package/dist/pdf/components/index.d.ts.map +1 -0
  44. package/dist/pdf/components/index.js +258 -0
  45. package/dist/pdf/components/index.js.map +1 -0
  46. package/dist/pdf/dto/generate-pdf.dto.d.ts +5 -0
  47. package/dist/pdf/dto/generate-pdf.dto.d.ts.map +1 -0
  48. package/dist/pdf/dto/generate-pdf.dto.js +27 -0
  49. package/dist/pdf/dto/generate-pdf.dto.js.map +1 -0
  50. package/dist/pdf/interfaces/pdf-theme-config.interface.d.ts +10 -0
  51. package/dist/pdf/interfaces/pdf-theme-config.interface.d.ts.map +1 -0
  52. package/dist/pdf/interfaces/pdf-theme-config.interface.js +3 -0
  53. package/dist/pdf/interfaces/pdf-theme-config.interface.js.map +1 -0
  54. package/dist/pdf/pdf.controller.d.ts +16 -0
  55. package/dist/pdf/pdf.controller.d.ts.map +1 -0
  56. package/dist/pdf/pdf.controller.js +100 -0
  57. package/dist/pdf/pdf.controller.js.map +1 -0
  58. package/dist/pdf/pdf.module.d.ts +3 -0
  59. package/dist/pdf/pdf.module.d.ts.map +1 -0
  60. package/dist/pdf/pdf.module.js +29 -0
  61. package/dist/pdf/pdf.module.js.map +1 -0
  62. package/dist/pdf/pdf.service.d.ts +44 -0
  63. package/dist/pdf/pdf.service.d.ts.map +1 -0
  64. package/dist/pdf/pdf.service.js +185 -0
  65. package/dist/pdf/pdf.service.js.map +1 -0
  66. package/dist/pdf/templates/hcode-institutional.template.d.ts +36 -0
  67. package/dist/pdf/templates/hcode-institutional.template.d.ts.map +1 -0
  68. package/dist/pdf/templates/hcode-institutional.template.js +67 -0
  69. package/dist/pdf/templates/hcode-institutional.template.js.map +1 -0
  70. package/dist/pdf/themes/hcode.theme.d.ts +25 -0
  71. package/dist/pdf/themes/hcode.theme.d.ts.map +1 -0
  72. package/dist/pdf/themes/hcode.theme.js +504 -0
  73. package/dist/pdf/themes/hcode.theme.js.map +1 -0
  74. package/dist/profile/profile.controller.d.ts +17 -17
  75. package/dist/profile/profile.service.d.ts +17 -17
  76. package/dist/role/role.controller.d.ts +12 -12
  77. package/dist/role/role.service.d.ts +12 -12
  78. package/dist/screen/screen.controller.d.ts +3 -3
  79. package/dist/screen/screen.service.d.ts +3 -3
  80. package/dist/session/session.controller.d.ts +4 -4
  81. package/dist/session/session.service.d.ts +12 -12
  82. package/dist/setting/setting.controller.d.ts +13 -13
  83. package/dist/setting/setting.service.d.ts +13 -13
  84. package/dist/user/user.controller.d.ts +24 -24
  85. package/dist/user/user.service.d.ts +55 -55
  86. package/hedhog/data/route.yaml +8 -0
  87. package/hedhog/data/setting_group.yaml +60 -0
  88. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +574 -574
  89. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +5 -9
  90. package/hedhog/frontend/messages/en.json +2 -0
  91. package/hedhog/frontend/messages/pt.json +2 -0
  92. package/package.json +4 -4
  93. package/src/core.module.ts +7 -4
  94. package/src/index.ts +9 -6
  95. package/src/oauth/oauth.service.ts +12 -12
  96. package/src/pdf/components/index.ts +316 -0
  97. package/src/pdf/dto/generate-pdf.dto.ts +11 -0
  98. package/src/pdf/interfaces/pdf-theme-config.interface.ts +10 -0
  99. package/src/pdf/pdf.controller.ts +86 -0
  100. package/src/pdf/pdf.module.ts +17 -0
  101. package/src/pdf/pdf.service.ts +229 -0
  102. package/src/pdf/templates/hcode-institutional.template.ts +122 -0
  103. package/src/pdf/themes/hcode.theme.ts +515 -0
@@ -1,574 +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
- }
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>{t('cancel')}</AlertDialogCancel>
566
+ <AlertDialogAction onClick={handleDeleteConversation}>
567
+ {t('deleteConversation')}
568
+ </AlertDialogAction>
569
+ </AlertDialogFooter>
570
+ </AlertDialogContent>
571
+ </AlertDialog>
572
+ </div>
573
+ );
574
+ }