@hed-hog/core 0.0.328 → 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 (206) 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 +1 -1
  6. package/dist/core/system.service.d.ts +1 -1
  7. package/dist/core.module.d.ts.map +1 -1
  8. package/dist/core.module.js +9 -2
  9. package/dist/core.module.js.map +1 -1
  10. package/dist/dashboard/dashboard/dashboard.controller.d.ts +84 -84
  11. package/dist/dashboard/dashboard/dashboard.service.d.ts +84 -84
  12. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +29 -29
  13. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +29 -29
  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 +11 -11
  17. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +11 -11
  18. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +6 -6
  19. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +6 -6
  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 +10 -10
  25. package/dist/file/file.service.d.ts +22 -22
  26. package/dist/index.d.ts +11 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +12 -4
  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/decorators/mcp-tool.decorator.d.ts +4 -0
  37. package/dist/mcp/decorators/mcp-tool.decorator.d.ts.map +1 -0
  38. package/dist/mcp/decorators/mcp-tool.decorator.js +8 -0
  39. package/dist/mcp/decorators/mcp-tool.decorator.js.map +1 -0
  40. package/dist/mcp/guards/mcp-auth.guard.d.ts +9 -0
  41. package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -0
  42. package/dist/mcp/guards/mcp-auth.guard.js +41 -0
  43. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -0
  44. package/dist/mcp/guards/mcp-permission.guard.d.ts +8 -0
  45. package/dist/mcp/guards/mcp-permission.guard.d.ts.map +1 -0
  46. package/dist/mcp/guards/mcp-permission.guard.js +57 -0
  47. package/dist/mcp/guards/mcp-permission.guard.js.map +1 -0
  48. package/dist/mcp/interfaces/mcp-tool-definition.interface.d.ts +7 -0
  49. package/dist/mcp/interfaces/mcp-tool-definition.interface.d.ts.map +1 -0
  50. package/dist/mcp/interfaces/mcp-tool-definition.interface.js +3 -0
  51. package/dist/mcp/interfaces/mcp-tool-definition.interface.js.map +1 -0
  52. package/dist/mcp/mcp-audit.service.d.ts +17 -0
  53. package/dist/mcp/mcp-audit.service.d.ts.map +1 -0
  54. package/dist/mcp/mcp-audit.service.js +46 -0
  55. package/dist/mcp/mcp-audit.service.js.map +1 -0
  56. package/dist/mcp/mcp-registry.service.d.ts +21 -0
  57. package/dist/mcp/mcp-registry.service.d.ts.map +1 -0
  58. package/dist/mcp/mcp-registry.service.js +69 -0
  59. package/dist/mcp/mcp-registry.service.js.map +1 -0
  60. package/dist/mcp/mcp-server.service.d.ts +12 -0
  61. package/dist/mcp/mcp-server.service.d.ts.map +1 -0
  62. package/dist/mcp/mcp-server.service.js +94 -0
  63. package/dist/mcp/mcp-server.service.js.map +1 -0
  64. package/dist/mcp/mcp.controller.d.ts +8 -0
  65. package/dist/mcp/mcp.controller.d.ts.map +1 -0
  66. package/dist/mcp/mcp.controller.js +54 -0
  67. package/dist/mcp/mcp.controller.js.map +1 -0
  68. package/dist/mcp/mcp.module.d.ts +3 -0
  69. package/dist/mcp/mcp.module.d.ts.map +1 -0
  70. package/dist/mcp/mcp.module.js +49 -0
  71. package/dist/mcp/mcp.module.js.map +1 -0
  72. package/dist/mcp/tools/core-settings.mcp-tools.d.ts +12 -0
  73. package/dist/mcp/tools/core-settings.mcp-tools.d.ts.map +1 -0
  74. package/dist/mcp/tools/core-settings.mcp-tools.js +66 -0
  75. package/dist/mcp/tools/core-settings.mcp-tools.js.map +1 -0
  76. package/dist/mcp/tools/core-users.mcp-tools.d.ts +55 -0
  77. package/dist/mcp/tools/core-users.mcp-tools.d.ts.map +1 -0
  78. package/dist/mcp/tools/core-users.mcp-tools.js +295 -0
  79. package/dist/mcp/tools/core-users.mcp-tools.js.map +1 -0
  80. package/dist/mcp/types/mcp-context.type.d.ts +7 -0
  81. package/dist/mcp/types/mcp-context.type.d.ts.map +1 -0
  82. package/dist/mcp/types/mcp-context.type.js +3 -0
  83. package/dist/mcp/types/mcp-context.type.js.map +1 -0
  84. package/dist/mcp-chat/dto/create-conversation.dto.d.ts +4 -0
  85. package/dist/mcp-chat/dto/create-conversation.dto.d.ts.map +1 -0
  86. package/dist/mcp-chat/dto/create-conversation.dto.js +22 -0
  87. package/dist/mcp-chat/dto/create-conversation.dto.js.map +1 -0
  88. package/dist/mcp-chat/dto/send-message.dto.d.ts +4 -0
  89. package/dist/mcp-chat/dto/send-message.dto.d.ts.map +1 -0
  90. package/dist/mcp-chat/dto/send-message.dto.js +22 -0
  91. package/dist/mcp-chat/dto/send-message.dto.js.map +1 -0
  92. package/dist/mcp-chat/mcp-chat.controller.d.ts +55 -0
  93. package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -0
  94. package/dist/mcp-chat/mcp-chat.controller.js +90 -0
  95. package/dist/mcp-chat/mcp-chat.controller.js.map +1 -0
  96. package/dist/mcp-chat/mcp-chat.module.d.ts +3 -0
  97. package/dist/mcp-chat/mcp-chat.module.d.ts.map +1 -0
  98. package/dist/mcp-chat/mcp-chat.module.js +30 -0
  99. package/dist/mcp-chat/mcp-chat.module.js.map +1 -0
  100. package/dist/mcp-chat/mcp-chat.service.d.ts +50 -0
  101. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -0
  102. package/dist/mcp-chat/mcp-chat.service.js +290 -0
  103. package/dist/mcp-chat/mcp-chat.service.js.map +1 -0
  104. package/dist/menu/menu.controller.d.ts +8 -8
  105. package/dist/menu/menu.service.d.ts +8 -8
  106. package/dist/oauth/oauth.service.js +1 -1
  107. package/dist/oauth/oauth.service.js.map +1 -1
  108. package/dist/pdf/components/index.d.ts +57 -0
  109. package/dist/pdf/components/index.d.ts.map +1 -0
  110. package/dist/pdf/components/index.js +258 -0
  111. package/dist/pdf/components/index.js.map +1 -0
  112. package/dist/pdf/dto/generate-pdf.dto.d.ts +5 -0
  113. package/dist/pdf/dto/generate-pdf.dto.d.ts.map +1 -0
  114. package/dist/pdf/dto/generate-pdf.dto.js +27 -0
  115. package/dist/pdf/dto/generate-pdf.dto.js.map +1 -0
  116. package/dist/pdf/interfaces/pdf-theme-config.interface.d.ts +10 -0
  117. package/dist/pdf/interfaces/pdf-theme-config.interface.d.ts.map +1 -0
  118. package/dist/pdf/interfaces/pdf-theme-config.interface.js +3 -0
  119. package/dist/pdf/interfaces/pdf-theme-config.interface.js.map +1 -0
  120. package/dist/pdf/pdf.controller.d.ts +16 -0
  121. package/dist/pdf/pdf.controller.d.ts.map +1 -0
  122. package/dist/pdf/pdf.controller.js +100 -0
  123. package/dist/pdf/pdf.controller.js.map +1 -0
  124. package/dist/pdf/pdf.module.d.ts +3 -0
  125. package/dist/pdf/pdf.module.d.ts.map +1 -0
  126. package/dist/pdf/pdf.module.js +29 -0
  127. package/dist/pdf/pdf.module.js.map +1 -0
  128. package/dist/pdf/pdf.service.d.ts +44 -0
  129. package/dist/pdf/pdf.service.d.ts.map +1 -0
  130. package/dist/pdf/pdf.service.js +185 -0
  131. package/dist/pdf/pdf.service.js.map +1 -0
  132. package/dist/pdf/templates/hcode-institutional.template.d.ts +36 -0
  133. package/dist/pdf/templates/hcode-institutional.template.d.ts.map +1 -0
  134. package/dist/pdf/templates/hcode-institutional.template.js +67 -0
  135. package/dist/pdf/templates/hcode-institutional.template.js.map +1 -0
  136. package/dist/pdf/themes/hcode.theme.d.ts +25 -0
  137. package/dist/pdf/themes/hcode.theme.d.ts.map +1 -0
  138. package/dist/pdf/themes/hcode.theme.js +504 -0
  139. package/dist/pdf/themes/hcode.theme.js.map +1 -0
  140. package/dist/profile/profile.controller.d.ts +16 -16
  141. package/dist/profile/profile.service.d.ts +16 -16
  142. package/dist/role/role.controller.d.ts +12 -12
  143. package/dist/role/role.service.d.ts +12 -12
  144. package/dist/route/dto/create.dto.d.ts +6 -2
  145. package/dist/route/dto/create.dto.d.ts.map +1 -1
  146. package/dist/route/dto/create.dto.js +19 -1
  147. package/dist/route/dto/create.dto.js.map +1 -1
  148. package/dist/route/dto/update.dto.d.ts +4 -0
  149. package/dist/route/dto/update.dto.d.ts.map +1 -1
  150. package/dist/route/dto/update.dto.js +19 -3
  151. package/dist/route/dto/update.dto.js.map +1 -1
  152. package/dist/route/route.service.d.ts +1 -1
  153. package/dist/route/route.service.d.ts.map +1 -1
  154. package/dist/route/route.service.js +18 -11
  155. package/dist/route/route.service.js.map +1 -1
  156. package/dist/screen/screen.controller.d.ts +3 -3
  157. package/dist/screen/screen.service.d.ts +3 -3
  158. package/dist/session/session.controller.d.ts +4 -4
  159. package/dist/session/session.service.d.ts +12 -12
  160. package/dist/setting/setting.controller.d.ts +13 -13
  161. package/dist/setting/setting.service.d.ts +13 -13
  162. package/dist/user/user.controller.d.ts +24 -24
  163. package/dist/user/user.service.d.ts +55 -55
  164. package/hedhog/data/menu.yaml +40 -26
  165. package/hedhog/data/route.yaml +583 -152
  166. package/hedhog/data/setting_group.yaml +60 -0
  167. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +574 -0
  168. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +5 -9
  169. package/hedhog/frontend/messages/en.json +19 -0
  170. package/hedhog/frontend/messages/pt.json +19 -0
  171. package/hedhog/table/mcp_conversation.yaml +12 -0
  172. package/hedhog/table/mcp_conversation_message.yaml +23 -0
  173. package/hedhog/table/mcp_tool_call_log.yaml +22 -0
  174. package/hedhog/table/route.yaml +33 -22
  175. package/package.json +7 -6
  176. package/src/core.module.ts +18 -11
  177. package/src/index.ts +22 -12
  178. package/src/mcp/decorators/mcp-tool.decorator.ts +7 -0
  179. package/src/mcp/guards/mcp-auth.guard.ts +32 -0
  180. package/src/mcp/guards/mcp-permission.guard.ts +51 -0
  181. package/src/mcp/interfaces/mcp-tool-definition.interface.ts +6 -0
  182. package/src/mcp/mcp-audit.service.ts +36 -0
  183. package/src/mcp/mcp-registry.service.ts +69 -0
  184. package/src/mcp/mcp-server.service.ts +87 -0
  185. package/src/mcp/mcp.controller.ts +30 -0
  186. package/src/mcp/mcp.module.ts +36 -0
  187. package/src/mcp/tools/core-settings.mcp-tools.ts +53 -0
  188. package/src/mcp/tools/core-users.mcp-tools.ts +267 -0
  189. package/src/mcp/types/mcp-context.type.ts +6 -0
  190. package/src/mcp-chat/dto/create-conversation.dto.ts +7 -0
  191. package/src/mcp-chat/dto/send-message.dto.ts +7 -0
  192. package/src/mcp-chat/mcp-chat.controller.ts +60 -0
  193. package/src/mcp-chat/mcp-chat.module.ts +17 -0
  194. package/src/mcp-chat/mcp-chat.service.ts +332 -0
  195. package/src/oauth/oauth.service.ts +12 -12
  196. package/src/pdf/components/index.ts +316 -0
  197. package/src/pdf/dto/generate-pdf.dto.ts +11 -0
  198. package/src/pdf/interfaces/pdf-theme-config.interface.ts +10 -0
  199. package/src/pdf/pdf.controller.ts +86 -0
  200. package/src/pdf/pdf.module.ts +17 -0
  201. package/src/pdf/pdf.service.ts +229 -0
  202. package/src/pdf/templates/hcode-institutional.template.ts +122 -0
  203. package/src/pdf/themes/hcode.theme.ts +515 -0
  204. package/src/route/dto/create.dto.ts +32 -14
  205. package/src/route/dto/update.dto.ts +31 -16
  206. package/src/route/route.service.ts +23 -12
@@ -1857,4 +1857,64 @@
1857
1857
  where:
1858
1858
  slug: retry-config
1859
1859
 
1860
+ - slug: pdf
1861
+ icon: file-text
1862
+ name:
1863
+ en: PDF
1864
+ pt: PDF
1865
+ description:
1866
+ en: Visual identity settings for PDF document generation
1867
+ pt: Configurações de identidade visual para geração de documentos PDF
1868
+ relations:
1869
+ setting:
1870
+ - slug: pdf-company-name
1871
+ type: string
1872
+ name:
1873
+ en: Company Name
1874
+ pt: Nome da Empresa
1875
+ description:
1876
+ en: Company name displayed in PDF headers and footers
1877
+ pt: Nome da empresa exibido nos cabeçalhos e rodapés dos PDFs
1878
+ value: hcode
1879
+ user_override: false
1880
+ - slug: pdf-logo
1881
+ type: string
1882
+ name:
1883
+ en: Logo URL
1884
+ pt: URL do Logo
1885
+ description:
1886
+ en: URL or data URI of the logo image for PDFs. Leave empty to use default CSS wordmark.
1887
+ pt: URL ou data URI da imagem do logo para PDFs. Deixe vazio para usar o wordmark CSS padrão.
1888
+ value: ''
1889
+ user_override: false
1890
+ - slug: pdf-consulting-color
1891
+ type: string
1892
+ name:
1893
+ en: Consulting Primary Color
1894
+ pt: Cor Principal Consulting
1895
+ description:
1896
+ en: Primary hex color for Consulting area PDFs (default #2D669E)
1897
+ pt: Cor hexadecimal principal para PDFs da área Consulting (padrão #2D669E)
1898
+ value: '#2D669E'
1899
+ user_override: false
1900
+ - slug: pdf-training-color
1901
+ type: string
1902
+ name:
1903
+ en: Training Primary Color
1904
+ pt: Cor Principal Training
1905
+ description:
1906
+ en: Primary hex color for Training area PDFs (default #FF760C)
1907
+ pt: Cor hexadecimal principal para PDFs da área Training (padrão #FF760C)
1908
+ value: '#FF760C'
1909
+ user_override: false
1910
+ - slug: pdf-research-color
1911
+ type: string
1912
+ name:
1913
+ en: Research Primary Color
1914
+ pt: Cor Principal Research
1915
+ description:
1916
+ en: Primary hex color for Research area PDFs (default #9400B5)
1917
+ pt: Cor hexadecimal principal para PDFs da área Research (padrão #9400B5)
1918
+ value: '#9400B5'
1919
+ user_override: false
1860
1920
 
@@ -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>{t('cancel')}</AlertDialogCancel>
566
+ <AlertDialogAction onClick={handleDeleteConversation}>
567
+ {t('deleteConversation')}
568
+ </AlertDialogAction>
569
+ </AlertDialogFooter>
570
+ </AlertDialogContent>
571
+ </AlertDialog>
572
+ </div>
573
+ );
574
+ }
@@ -212,10 +212,8 @@ function UserCreateSheet({
212
212
  getSettingValue,
213
213
  currentLocaleCode
214
214
  );
215
- return currentLocaleCode === 'en'
216
- ? `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`
217
- : `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`;
218
- }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
215
+ return t('draftStatus', { relativeLabel, absoluteLabel });
216
+ }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft, t]);
219
217
 
220
218
  useEffect(() => {
221
219
  if (!open) return;
@@ -241,7 +239,7 @@ function UserCreateSheet({
241
239
  e?.response?.data?.message ||
242
240
  e?.response?.data?.error ||
243
241
  e?.message ||
244
- 'Server error';
242
+ t('serverError');
245
243
  setFormError(String(msg));
246
244
  }
247
245
  };
@@ -453,10 +451,8 @@ function UserEditSheet({
453
451
  getSettingValue,
454
452
  currentLocaleCode
455
453
  );
456
- return currentLocaleCode === 'en'
457
- ? `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`
458
- : `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`;
459
- }, [editDraftSavedAt, currentLocaleCode, getSettingValue, hasEditDraft]);
454
+ return t('draftStatus', { relativeLabel, absoluteLabel });
455
+ }, [editDraftSavedAt, currentLocaleCode, getSettingValue, hasEditDraft, t]);
460
456
 
461
457
  useEffect(() => {
462
458
  if (!editingUser) return;