@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.
- package/dist/ai/ai.service.d.ts +1 -1
- package/dist/auth/auth.controller.d.ts +23 -23
- package/dist/auth/auth.service.d.ts +41 -41
- package/dist/challenge/challenge.service.d.ts +14 -14
- package/dist/core/system.controller.d.ts +1 -1
- package/dist/core/system.service.d.ts +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +9 -2
- package/dist/core.module.js.map +1 -1
- package/dist/dashboard/dashboard/dashboard.controller.d.ts +84 -84
- package/dist/dashboard/dashboard/dashboard.service.d.ts +84 -84
- package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +29 -29
- package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +29 -29
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +42 -42
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +42 -42
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +11 -11
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +11 -11
- package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +6 -6
- package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +6 -6
- package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +33 -33
- package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +33 -33
- package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
- package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
- package/dist/file/file.controller.d.ts +10 -10
- package/dist/file/file.service.d.ts +22 -22
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/mail/mail.controller.d.ts +15 -15
- package/dist/mail/mail.service.d.ts +15 -15
- package/dist/mail-profile/mail-profile.controller.d.ts +9 -9
- package/dist/mail-profile/mail-profile.service.d.ts +9 -9
- package/dist/mail-sent/mail-sent.controller.d.ts +17 -17
- package/dist/mail-sent/mail-sent.service.d.ts +17 -17
- package/dist/mcp/decorators/mcp-tool.decorator.d.ts +4 -0
- package/dist/mcp/decorators/mcp-tool.decorator.d.ts.map +1 -0
- package/dist/mcp/decorators/mcp-tool.decorator.js +8 -0
- package/dist/mcp/decorators/mcp-tool.decorator.js.map +1 -0
- package/dist/mcp/guards/mcp-auth.guard.d.ts +9 -0
- package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -0
- package/dist/mcp/guards/mcp-auth.guard.js +41 -0
- package/dist/mcp/guards/mcp-auth.guard.js.map +1 -0
- package/dist/mcp/guards/mcp-permission.guard.d.ts +8 -0
- package/dist/mcp/guards/mcp-permission.guard.d.ts.map +1 -0
- package/dist/mcp/guards/mcp-permission.guard.js +57 -0
- package/dist/mcp/guards/mcp-permission.guard.js.map +1 -0
- package/dist/mcp/interfaces/mcp-tool-definition.interface.d.ts +7 -0
- package/dist/mcp/interfaces/mcp-tool-definition.interface.d.ts.map +1 -0
- package/dist/mcp/interfaces/mcp-tool-definition.interface.js +3 -0
- package/dist/mcp/interfaces/mcp-tool-definition.interface.js.map +1 -0
- package/dist/mcp/mcp-audit.service.d.ts +17 -0
- package/dist/mcp/mcp-audit.service.d.ts.map +1 -0
- package/dist/mcp/mcp-audit.service.js +46 -0
- package/dist/mcp/mcp-audit.service.js.map +1 -0
- package/dist/mcp/mcp-registry.service.d.ts +21 -0
- package/dist/mcp/mcp-registry.service.d.ts.map +1 -0
- package/dist/mcp/mcp-registry.service.js +69 -0
- package/dist/mcp/mcp-registry.service.js.map +1 -0
- package/dist/mcp/mcp-server.service.d.ts +12 -0
- package/dist/mcp/mcp-server.service.d.ts.map +1 -0
- package/dist/mcp/mcp-server.service.js +94 -0
- package/dist/mcp/mcp-server.service.js.map +1 -0
- package/dist/mcp/mcp.controller.d.ts +8 -0
- package/dist/mcp/mcp.controller.d.ts.map +1 -0
- package/dist/mcp/mcp.controller.js +54 -0
- package/dist/mcp/mcp.controller.js.map +1 -0
- package/dist/mcp/mcp.module.d.ts +3 -0
- package/dist/mcp/mcp.module.d.ts.map +1 -0
- package/dist/mcp/mcp.module.js +49 -0
- package/dist/mcp/mcp.module.js.map +1 -0
- package/dist/mcp/tools/core-settings.mcp-tools.d.ts +12 -0
- package/dist/mcp/tools/core-settings.mcp-tools.d.ts.map +1 -0
- package/dist/mcp/tools/core-settings.mcp-tools.js +66 -0
- package/dist/mcp/tools/core-settings.mcp-tools.js.map +1 -0
- package/dist/mcp/tools/core-users.mcp-tools.d.ts +55 -0
- package/dist/mcp/tools/core-users.mcp-tools.d.ts.map +1 -0
- package/dist/mcp/tools/core-users.mcp-tools.js +295 -0
- package/dist/mcp/tools/core-users.mcp-tools.js.map +1 -0
- package/dist/mcp/types/mcp-context.type.d.ts +7 -0
- package/dist/mcp/types/mcp-context.type.d.ts.map +1 -0
- package/dist/mcp/types/mcp-context.type.js +3 -0
- package/dist/mcp/types/mcp-context.type.js.map +1 -0
- package/dist/mcp-chat/dto/create-conversation.dto.d.ts +4 -0
- package/dist/mcp-chat/dto/create-conversation.dto.d.ts.map +1 -0
- package/dist/mcp-chat/dto/create-conversation.dto.js +22 -0
- package/dist/mcp-chat/dto/create-conversation.dto.js.map +1 -0
- package/dist/mcp-chat/dto/send-message.dto.d.ts +4 -0
- package/dist/mcp-chat/dto/send-message.dto.d.ts.map +1 -0
- package/dist/mcp-chat/dto/send-message.dto.js +22 -0
- package/dist/mcp-chat/dto/send-message.dto.js.map +1 -0
- package/dist/mcp-chat/mcp-chat.controller.d.ts +55 -0
- package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -0
- package/dist/mcp-chat/mcp-chat.controller.js +90 -0
- package/dist/mcp-chat/mcp-chat.controller.js.map +1 -0
- package/dist/mcp-chat/mcp-chat.module.d.ts +3 -0
- package/dist/mcp-chat/mcp-chat.module.d.ts.map +1 -0
- package/dist/mcp-chat/mcp-chat.module.js +30 -0
- package/dist/mcp-chat/mcp-chat.module.js.map +1 -0
- package/dist/mcp-chat/mcp-chat.service.d.ts +50 -0
- package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -0
- package/dist/mcp-chat/mcp-chat.service.js +290 -0
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -0
- package/dist/menu/menu.controller.d.ts +8 -8
- package/dist/menu/menu.service.d.ts +8 -8
- package/dist/oauth/oauth.service.js +1 -1
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/pdf/components/index.d.ts +57 -0
- package/dist/pdf/components/index.d.ts.map +1 -0
- package/dist/pdf/components/index.js +258 -0
- package/dist/pdf/components/index.js.map +1 -0
- package/dist/pdf/dto/generate-pdf.dto.d.ts +5 -0
- package/dist/pdf/dto/generate-pdf.dto.d.ts.map +1 -0
- package/dist/pdf/dto/generate-pdf.dto.js +27 -0
- package/dist/pdf/dto/generate-pdf.dto.js.map +1 -0
- package/dist/pdf/interfaces/pdf-theme-config.interface.d.ts +10 -0
- package/dist/pdf/interfaces/pdf-theme-config.interface.d.ts.map +1 -0
- package/dist/pdf/interfaces/pdf-theme-config.interface.js +3 -0
- package/dist/pdf/interfaces/pdf-theme-config.interface.js.map +1 -0
- package/dist/pdf/pdf.controller.d.ts +16 -0
- package/dist/pdf/pdf.controller.d.ts.map +1 -0
- package/dist/pdf/pdf.controller.js +100 -0
- package/dist/pdf/pdf.controller.js.map +1 -0
- package/dist/pdf/pdf.module.d.ts +3 -0
- package/dist/pdf/pdf.module.d.ts.map +1 -0
- package/dist/pdf/pdf.module.js +29 -0
- package/dist/pdf/pdf.module.js.map +1 -0
- package/dist/pdf/pdf.service.d.ts +44 -0
- package/dist/pdf/pdf.service.d.ts.map +1 -0
- package/dist/pdf/pdf.service.js +185 -0
- package/dist/pdf/pdf.service.js.map +1 -0
- package/dist/pdf/templates/hcode-institutional.template.d.ts +36 -0
- package/dist/pdf/templates/hcode-institutional.template.d.ts.map +1 -0
- package/dist/pdf/templates/hcode-institutional.template.js +67 -0
- package/dist/pdf/templates/hcode-institutional.template.js.map +1 -0
- package/dist/pdf/themes/hcode.theme.d.ts +25 -0
- package/dist/pdf/themes/hcode.theme.d.ts.map +1 -0
- package/dist/pdf/themes/hcode.theme.js +504 -0
- package/dist/pdf/themes/hcode.theme.js.map +1 -0
- package/dist/profile/profile.controller.d.ts +16 -16
- package/dist/profile/profile.service.d.ts +16 -16
- package/dist/role/role.controller.d.ts +12 -12
- package/dist/role/role.service.d.ts +12 -12
- package/dist/route/dto/create.dto.d.ts +6 -2
- package/dist/route/dto/create.dto.d.ts.map +1 -1
- package/dist/route/dto/create.dto.js +19 -1
- package/dist/route/dto/create.dto.js.map +1 -1
- package/dist/route/dto/update.dto.d.ts +4 -0
- package/dist/route/dto/update.dto.d.ts.map +1 -1
- package/dist/route/dto/update.dto.js +19 -3
- package/dist/route/dto/update.dto.js.map +1 -1
- package/dist/route/route.service.d.ts +1 -1
- package/dist/route/route.service.d.ts.map +1 -1
- package/dist/route/route.service.js +18 -11
- package/dist/route/route.service.js.map +1 -1
- package/dist/screen/screen.controller.d.ts +3 -3
- package/dist/screen/screen.service.d.ts +3 -3
- package/dist/session/session.controller.d.ts +4 -4
- package/dist/session/session.service.d.ts +12 -12
- package/dist/setting/setting.controller.d.ts +13 -13
- package/dist/setting/setting.service.d.ts +13 -13
- package/dist/user/user.controller.d.ts +24 -24
- package/dist/user/user.service.d.ts +55 -55
- package/hedhog/data/menu.yaml +40 -26
- package/hedhog/data/route.yaml +583 -152
- package/hedhog/data/setting_group.yaml +60 -0
- package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +574 -0
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +5 -9
- package/hedhog/frontend/messages/en.json +19 -0
- package/hedhog/frontend/messages/pt.json +19 -0
- package/hedhog/table/mcp_conversation.yaml +12 -0
- package/hedhog/table/mcp_conversation_message.yaml +23 -0
- package/hedhog/table/mcp_tool_call_log.yaml +22 -0
- package/hedhog/table/route.yaml +33 -22
- package/package.json +7 -6
- package/src/core.module.ts +18 -11
- package/src/index.ts +22 -12
- package/src/mcp/decorators/mcp-tool.decorator.ts +7 -0
- package/src/mcp/guards/mcp-auth.guard.ts +32 -0
- package/src/mcp/guards/mcp-permission.guard.ts +51 -0
- package/src/mcp/interfaces/mcp-tool-definition.interface.ts +6 -0
- package/src/mcp/mcp-audit.service.ts +36 -0
- package/src/mcp/mcp-registry.service.ts +69 -0
- package/src/mcp/mcp-server.service.ts +87 -0
- package/src/mcp/mcp.controller.ts +30 -0
- package/src/mcp/mcp.module.ts +36 -0
- package/src/mcp/tools/core-settings.mcp-tools.ts +53 -0
- package/src/mcp/tools/core-users.mcp-tools.ts +267 -0
- package/src/mcp/types/mcp-context.type.ts +6 -0
- package/src/mcp-chat/dto/create-conversation.dto.ts +7 -0
- package/src/mcp-chat/dto/send-message.dto.ts +7 -0
- package/src/mcp-chat/mcp-chat.controller.ts +60 -0
- package/src/mcp-chat/mcp-chat.module.ts +17 -0
- package/src/mcp-chat/mcp-chat.service.ts +332 -0
- package/src/oauth/oauth.service.ts +12 -12
- package/src/pdf/components/index.ts +316 -0
- package/src/pdf/dto/generate-pdf.dto.ts +11 -0
- package/src/pdf/interfaces/pdf-theme-config.interface.ts +10 -0
- package/src/pdf/pdf.controller.ts +86 -0
- package/src/pdf/pdf.module.ts +17 -0
- package/src/pdf/pdf.service.ts +229 -0
- package/src/pdf/templates/hcode-institutional.template.ts +122 -0
- package/src/pdf/themes/hcode.theme.ts +515 -0
- package/src/route/dto/create.dto.ts +32 -14
- package/src/route/dto/update.dto.ts +31 -16
- 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
|
|
216
|
-
|
|
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
|
-
'
|
|
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
|
|
457
|
-
|
|
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;
|