feat(frontend): add factory reset button with confirmation dialog

Add resetAppConfig API function, AlertDialog-confirmed factory reset
button in config page, and i18n keys for en/zh/pt-br locales.
This commit is contained in:
SiYue-ZO 2026-05-18 13:53:34 +08:00
parent f53222f6a4
commit 3f653161e3
7 changed files with 110 additions and 11 deletions

View file

@ -16,8 +16,8 @@ import (
"github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
configcmd "github.com/sipeed/picoclaw/cmd/picoclaw/internal/config"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
configcmd "github.com/sipeed/picoclaw/cmd/picoclaw/internal/config"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/mcp" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/mcp"

View file

@ -39,6 +39,7 @@ func TestNewPicoclawCommand(t *testing.T) {
allowedCommands := []string{ allowedCommands := []string{
"agent", "agent",
"auth", "auth",
"config",
"cron", "cron",
"gateway", "gateway",
"mcp", "mcp",

View file

@ -77,6 +77,12 @@ export async function patchAppConfig(
}) })
} }
export async function resetAppConfig(): Promise<ConfigActionResponse> {
return request<ConfigActionResponse>("/api/config/reset", {
method: "POST",
})
}
// WeChat QR login flow API // WeChat QR login flow API
export interface WeixinFlowResponse { export interface WeixinFlowResponse {

View file

@ -5,7 +5,7 @@ import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
import { patchAppConfig } from "@/api/channels" import { patchAppConfig, resetAppConfig } from "@/api/channels"
import { launcherFetch } from "@/api/http" import { launcherFetch } from "@/api/http"
import { postLauncherDashboardSetup } from "@/api/launcher-auth" import { postLauncherDashboardSetup } from "@/api/launcher-auth"
import { import {
@ -41,6 +41,17 @@ import {
} from "@/components/config/form-model" } from "@/components/config/form-model"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { showSaveSuccessOrRestartToast } from "@/lib/restart-required" import { showSaveSuccessOrRestartToast } from "@/lib/restart-required"
import { refreshGatewayState } from "@/store/gateway" import { refreshGatewayState } from "@/store/gateway"
@ -72,15 +83,24 @@ export function ConfigPage() {
const [autoStartEnabled, setAutoStartEnabled] = useState(false) const [autoStartEnabled, setAutoStartEnabled] = useState(false)
const [autoStartBaseline, setAutoStartBaseline] = useState(false) const [autoStartBaseline, setAutoStartBaseline] = useState(false)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [showFactoryResetDialog, setShowFactoryResetDialog] = useState(false)
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
queryKey: ["config"], queryKey: ["config"],
queryFn: async () => { queryFn: async () => {
const res = await launcherFetch("/api/config") const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5000)
try {
const res = await launcherFetch("/api/config", {
signal: controller.signal,
})
if (!res.ok) { if (!res.ok) {
throw new Error("Failed to load config") throw new Error("Failed to load config")
} }
return res.json() return res.json()
} finally {
clearTimeout(timer)
}
}, },
}) })
@ -208,6 +228,27 @@ export function ConfigPage() {
toast.info(t("pages.config.reset_success")) toast.info(t("pages.config.reset_success"))
} }
const handleFactoryReset = async () => {
try {
await resetAppConfig()
const fresh = await launcherFetch("/api/config").then((r) => r.json())
const parsed = buildFormFromConfig(fresh)
setForm(parsed)
setBaseline(parsed)
await queryClient.invalidateQueries({ queryKey: ["config"] })
await refreshGatewayState()
toast.success(t("pages.config.factory_reset_success"))
} catch (err) {
toast.error(
err instanceof Error
? err.message
: t("pages.config.factory_reset_error"),
)
} finally {
setShowFactoryResetDialog(false)
}
}
const handleSave = async () => { const handleSave = async () => {
try { try {
setSaving(true) setSaving(true)
@ -625,8 +666,38 @@ export function ConfigPage() {
} }
} }
const factoryResetButton = (
<AlertDialog
open={showFactoryResetDialog}
onOpenChange={setShowFactoryResetDialog}
>
<AlertDialogTrigger asChild>
<Button variant="destructive" disabled={saving}>
{t("pages.config.factory_reset")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("pages.config.factory_reset_confirm_title")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("pages.config.factory_reset_confirm_desc")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleFactoryReset}>
{t("pages.config.factory_reset_confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
const actionButtons = ( const actionButtons = (
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
{factoryResetButton}
<Button <Button
variant="outline" variant="outline"
onClick={handleReset} onClick={handleReset}
@ -672,9 +743,12 @@ export function ConfigPage() {
{t("labels.loading")} {t("labels.loading")}
</div> </div>
) : error ? ( ) : error ? (
<div className="space-y-4">
<div className="text-destructive py-6 text-sm"> <div className="text-destructive py-6 text-sm">
{t("pages.config.load_error")} {t("pages.config.load_error")}
</div> </div>
<div className="flex justify-end">{factoryResetButton}</div>
</div>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
<LauncherSection <LauncherSection

View file

@ -883,7 +883,13 @@
"format_success": "JSON formatted successfully.", "format_success": "JSON formatted successfully.",
"format_error": "Invalid JSON format.", "format_error": "Invalid JSON format.",
"format": "Format", "format": "Format",
"unsaved_changes": "You have unsaved changes." "unsaved_changes": "You have unsaved changes.",
"factory_reset": "Factory Reset",
"factory_reset_confirm_title": "Reset to Factory Defaults",
"factory_reset_confirm_desc": "This will reset all configuration to factory defaults. API keys and security credentials will be preserved. A backup of the current config will be created.",
"factory_reset_confirm": "Reset to Defaults",
"factory_reset_success": "Configuration has been reset to factory defaults.",
"factory_reset_error": "Failed to reset configuration."
}, },
"logs": { "logs": {
"log_level_error": "Failed to update log level.", "log_level_error": "Failed to update log level.",

View file

@ -750,7 +750,13 @@
"format_success": "JSON formatado com sucesso.", "format_success": "JSON formatado com sucesso.",
"format_error": "Formato JSON inválido.", "format_error": "Formato JSON inválido.",
"format": "Formatar", "format": "Formatar",
"unsaved_changes": "Você tem alterações não salvas." "unsaved_changes": "Você tem alterações não salvas.",
"factory_reset": "Restaurar Padrões",
"factory_reset_confirm_title": "Restaurar Configurações de Fábrica",
"factory_reset_confirm_desc": "Isso redefinirá todas as configurações para os padrões de fábrica. As chaves de API e credenciais de segurança serão preservadas. Um backup da configuração atual será criado.",
"factory_reset_confirm": "Redefinir para Padrões",
"factory_reset_success": "A configuração foi redefinida para os padrões de fábrica.",
"factory_reset_error": "Falha ao redefinir a configuração."
}, },
"logs": { "logs": {
"log_level_error": "Falha ao atualizar nível de log.", "log_level_error": "Falha ao atualizar nível de log.",

View file

@ -884,7 +884,13 @@
"format_success": "JSON 格式化成功", "format_success": "JSON 格式化成功",
"format_error": "JSON 格式无效", "format_error": "JSON 格式无效",
"format": "格式化", "format": "格式化",
"unsaved_changes": "您有未保存的更改" "unsaved_changes": "您有未保存的更改",
"factory_reset": "恢复出厂设置",
"factory_reset_confirm_title": "恢复出厂设置",
"factory_reset_confirm_desc": "这将把所有配置重置为出厂默认值。API 密钥和安全凭证将被保留。当前配置将被备份。",
"factory_reset_confirm": "确认重置",
"factory_reset_success": "配置已重置为出厂默认值。",
"factory_reset_error": "重置配置失败。"
}, },
"logs": { "logs": {
"log_level_error": "更新日志等级失败。", "log_level_error": "更新日志等级失败。",