From 27add7922072fe17682a4326aa607f5f974e8ebd Mon Sep 17 00:00:00 2001 From: Dihubopen <130813726+Dihubopen@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:29:35 +0800 Subject: [PATCH] feat(web): Update config page (#1173) * feat(web): Update config page * fix(web): useEffect resets editorValue whenever config changes * fix(web): react-hooks/set-state-in-effect error & pnpm lint #1173 --- .gitignore | 1 + web/frontend/package.json | 2 + web/frontend/pnpm-lock.yaml | 53 +++++ web/frontend/src/components/app-layout.tsx | 4 +- web/frontend/src/components/ui/card.tsx | 103 ++++++++++ web/frontend/src/i18n/locales/en.json | 20 +- web/frontend/src/i18n/locales/zh.json | 21 +- web/frontend/src/main.tsx | 19 +- web/frontend/src/routes/config.tsx | 217 +++++++++++++++++++-- 9 files changed, 421 insertions(+), 19 deletions(-) create mode 100644 web/frontend/src/components/ui/card.tsx diff --git a/.gitignore b/.gitignore index a52b8d25a..e94c04f6e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ docs/plans/ # Added by goreleaser init: dist/ +*.vite/ # Windows Application Icon/Resource *.syso diff --git a/web/frontend/package.json b/web/frontend/package.json index be2c0c5cb..ee46cdcda 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -16,6 +16,7 @@ "@fontsource-variable/inter": "^5.2.8", "@tabler/icons-react": "^3.38.0", "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-query": "^5.90.21", "@tanstack/react-router": "^1.163.3", "@tanstack/react-router-devtools": "^1.163.3", "class-variance-authority": "^0.7.1", @@ -32,6 +33,7 @@ "react-textarea-autosize": "^8.5.9", "remark-gfm": "^4.0.1", "shadcn": "^3.8.5", + "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", "tw-animate-css": "^1.4.0" diff --git a/web/frontend/pnpm-lock.yaml b/web/frontend/pnpm-lock.yaml index 0b824fde7..8e89cbbe5 100644 --- a/web/frontend/pnpm-lock.yaml +++ b/web/frontend/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@tailwindcss/vite': specifier: ^4.2.1 version: 4.2.1(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) + '@tanstack/react-query': + specifier: ^5.90.21 + version: 5.90.21(react@19.2.4) '@tanstack/react-router': specifier: ^1.163.3 version: 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -65,6 +68,9 @@ importers: shadcn: specifier: ^3.8.5 version: 3.8.5(@types/node@24.11.0)(typescript@5.9.3) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwind-merge: specifier: ^3.5.0 version: 3.5.0 @@ -1353,66 +1359,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -1497,24 +1516,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -1558,6 +1581,14 @@ packages: resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==} engines: {node: '>=20.19'} + '@tanstack/query-core@5.90.20': + resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} + + '@tanstack/react-query@5.90.21': + resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} + peerDependencies: + react: ^18 || ^19 + '@tanstack/react-router-devtools@1.163.3': resolution: {integrity: sha512-42VMkV/2Z8ro7xzblPBRNZIEmCNXMzm2jD68G52p2qhjXm38wGpg46qneAESN9FtTQeVWk5aSXs47/jt7lkzmw==} engines: {node: '>=20.19'} @@ -2754,24 +2785,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} @@ -3504,6 +3539,12 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -5323,6 +5364,13 @@ snapshots: '@tanstack/history@1.161.4': {} + '@tanstack/query-core@5.90.20': {} + + '@tanstack/react-query@5.90.21(react@19.2.4)': + dependencies: + '@tanstack/query-core': 5.90.20 + react: 19.2.4 + '@tanstack/react-router-devtools@1.163.3(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.163.3)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/react-router': 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -7570,6 +7618,11 @@ snapshots: sisteransi@1.0.5: {} + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + source-map-js@1.2.1: {} source-map@0.6.1: {} diff --git a/web/frontend/src/components/app-layout.tsx b/web/frontend/src/components/app-layout.tsx index fee061abd..cfa7e019f 100644 --- a/web/frontend/src/components/app-layout.tsx +++ b/web/frontend/src/components/app-layout.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react" +import { Toaster } from "sonner" import { AppHeader } from "@/components/app-header" import { AppSidebar } from "@/components/app-sidebar" @@ -19,7 +20,8 @@ export function AppLayout({ children }: { children: ReactNode }) { + ) -} +} \ No newline at end of file diff --git a/web/frontend/src/components/ui/card.tsx b/web/frontend/src/components/ui/card.tsx new file mode 100644 index 000000000..13610b23d --- /dev/null +++ b/web/frontend/src/components/ui/card.tsx @@ -0,0 +1,103 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ + className, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { + return ( +
img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/web/frontend/src/i18n/locales/en.json b/web/frontend/src/i18n/locales/en.json index 57eeacebe..73ed46685 100644 --- a/web/frontend/src/i18n/locales/en.json +++ b/web/frontend/src/i18n/locales/en.json @@ -70,7 +70,10 @@ }, "common": { "cancel": "Cancel", - "save": "Save" + "save": "Save", + "saving": "Saving...", + "reset": "Reset", + "confirm": "Confirm" }, "credentials": { "description": "Manage OAuth and token-based credentials for supported providers.", @@ -223,7 +226,20 @@ "description": "Securely manage your API keys and credentials." }, "config": { - "description": "System configuration and preferences." + "description": "System configuration and preferences.", + "raw_json_title": "Raw JSON Configuration", + "raw_json_desc": "Advanced users can directly edit the raw JSON configuration below.", + "json_placeholder": "Enter valid JSON configuration...", + "save_success": "Configuration saved successfully.", + "save_error": "Failed to save configuration.", + "reset_confirm_title": "Reset Changes", + "reset_confirm_desc": "Are you sure you want to reset your unsaved changes back to the last saved state?", + "reset_success": "Changes have been reset to the last saved state.", + "invalid_json": "Invalid JSON format.", + "format_success": "JSON formatted successfully.", + "format_error": "Invalid JSON format.", + "format": "Format", + "lose_unsaved_changes": "You have unsaved changes. Are you sure you want to reset and lose these changes?" }, "logs": { "description": "System logs and monitoring." diff --git a/web/frontend/src/i18n/locales/zh.json b/web/frontend/src/i18n/locales/zh.json index 3897b48fe..0fe345a0b 100644 --- a/web/frontend/src/i18n/locales/zh.json +++ b/web/frontend/src/i18n/locales/zh.json @@ -70,7 +70,10 @@ }, "common": { "cancel": "取消", - "save": "保存" + "save": "保存", + "saving": "保存中...", + "reset": "重置", + "confirm": "确认" }, "credentials": { "description": "管理已支持服务商的 OAuth 与 Token 凭据。", @@ -223,7 +226,21 @@ "description": "安全管理您的 API 密钥与访问凭据。" }, "config": { - "description": "系统配置和偏好设置。" + "description": "系统配置和偏好设置。", + "raw_json_title": "原始 JSON 配置", + "raw_json_desc": "高级用户可以直接编辑下方的原始 JSON 配置。", + "json_placeholder": "请输入有效的 JSON 配置...", + "save_success": "配置保存成功。", + "save_error": "配置保存失败。", + "reset_confirm_title": "重置更改", + "reset_confirm_desc": "您确定要重置回上次保存的状态吗?", + "reset_success": "更改已重置为上次保存的状态。", + "invalid_json": "JSON 格式无效。", + "format_success": "JSON 格式化成功。", + "format_error": "JSON 格式无效。", + "format": "格式化", + "lose_unsaved_changes": "您有未保存的更改。确定要重置并丢失这些更改吗?", + "unsaved_changes": "您有未保存的更改。" }, "logs": { "description": "系统日志和监控。" diff --git a/web/frontend/src/main.tsx b/web/frontend/src/main.tsx index 4eaf72e3e..eae0cb89a 100644 --- a/web/frontend/src/main.tsx +++ b/web/frontend/src/main.tsx @@ -1,3 +1,7 @@ +import { + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query" import { RouterProvider, createRouter } from "@tanstack/react-router" import { StrictMode } from "react" import ReactDOM from "react-dom/client" @@ -6,7 +10,14 @@ import "./i18n" import "./index.css" import { routeTree } from "./routeTree.gen" -const router = createRouter({ routeTree }) +const queryClient = new QueryClient() + +const router = createRouter({ + routeTree, + context: { + queryClient, + }, +}) declare module "@tanstack/react-router" { interface Register { @@ -19,7 +30,9 @@ if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement) root.render( - + + + , ) -} +} \ No newline at end of file diff --git a/web/frontend/src/routes/config.tsx b/web/frontend/src/routes/config.tsx index 5d4a39c1d..0a7255596 100644 --- a/web/frontend/src/routes/config.tsx +++ b/web/frontend/src/routes/config.tsx @@ -1,7 +1,31 @@ import { createFileRoute } from "@tanstack/react-router" import { useTranslation } from "react-i18next" +import { toast } from "sonner" import { PageHeader } from "@/components/page-header" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Textarea } from "@/components/ui/textarea" +import { ScrollArea } from "@/components/ui/scroll-area" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { useState } from "react" export const Route = createFileRoute("/config")({ component: ConfigPage, @@ -12,19 +36,190 @@ function ConfigPage() { return (
-
-
-

- {t("navigation.config", "Config")} -

-

- {t( - "pages.config.description", - "System configuration and preferences.", - )} -

+
+
+
) } + +function RawJsonPanel() { + const { t } = useTranslation() + const queryClient = useQueryClient() + + const { data: config, isLoading } = useQuery({ + queryKey: ["config"], + queryFn: async () => { + const res = await fetch("/api/config") + if (!res.ok) { + throw new Error("Failed to fetch config") + } + return res.json() + }, + }) + + const mutation = useMutation({ + mutationFn: async (newConfig: string) => { + const res = await fetch("/api/config", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: newConfig, + }) + if (!res.ok) { + throw new Error("Failed to save config") + } + }, + onSuccess: () => { + toast.success(t("pages.config.save_success", "Configuration saved successfully.")) + // Update last saved config and reset dirty state + try { + const savedConfig = JSON.parse(editorValue) + setLastSavedConfig(savedConfig) + setIsDirty(false) + // Important: Invalidate the query to refresh the cached data + queryClient.invalidateQueries({ queryKey: ["config"] }) + } catch { + // If JSON parsing fails, invalidate to get fresh data + queryClient.invalidateQueries({ queryKey: ["config"] }) + } + }, + onError: () => { + toast.error(t("pages.config.save_error", "Failed to save configuration.")) + }, + }) + + const [editorValue, setEditorValue] = useState("") + const [isDirty, setIsDirty] = useState(false) + + // Store the last saved config to detect changes + const [lastSavedConfig, setLastSavedConfig] = useState | null>(null) + + // Initialize editor value when config is first loaded + const getInitialEditorValue = () => { + if (config && !editorValue) { + return JSON.stringify(config, null, 2) + } + return editorValue + } + + const displayValue = getInitialEditorValue() + + const handleSave = () => { + try { + // Validate JSON before saving + JSON.parse(editorValue) + mutation.mutate(editorValue) + } catch (error) { + toast.error(t("pages.config.invalid_json", error instanceof Error ? error.message : "Invalid JSON format.")) + } + } + + const handleFormat = () => { + try { + const formatted = JSON.stringify(JSON.parse(editorValue), null, 2) + setEditorValue(formatted) + toast.success(t("pages.config.format_success", "JSON formatted successfully.")) + } catch (error) { + toast.error(t("pages.config.format_error", error instanceof Error ? error.message : "Invalid JSON format.")) + } + } + + const [showResetDialog, setShowResetDialog] = useState(false) + + const confirmReset = () => { + // Reset editor content to the last saved configuration + if (lastSavedConfig) { + setEditorValue(JSON.stringify(lastSavedConfig, null, 2)) + } else if (config) { + // Fallback to current config if no last saved config + setEditorValue(JSON.stringify(config, null, 2)) + } + setIsDirty(false) + toast.info(t("pages.config.reset_success", "Changes have been reset to the last saved state.")) + setShowResetDialog(false) + } + + return ( + + + + {t("pages.config.raw_json_title", "Raw JSON Configuration")} + + + {t( + "pages.config.raw_json_desc", + "Advanced users can directly edit the raw JSON configuration below.", + )} + + + + {isLoading ? ( +
+

{t("labels.loading", "Loading...")}

+
+ ) : ( +
+ {isDirty && ( +
+ {t("pages.config.unsaved_changes", "You have unsaved changes.")} +
+ )} +
+ +