feat(settings): add first-run confirmation dialogs for proxy and usage features

Prevent accidental activation of advanced features by showing a one-time
info dialog. Once confirmed, the flag is persisted in settings.json and
the dialog never appears again.

- Proxy: confirmation when toggling proxy server ON for the first time
- Usage: confirmation when enabling usage query inside UsageScriptModal
- Enhanced ConfirmDialog with "info" variant (blue icon + default button)
- Added i18n translations for zh, en, ja
This commit is contained in:
Jason
2026-02-21 21:27:26 +08:00
parent 6b4ba64bbd
commit 5ebc879f09
8 changed files with 119 additions and 10 deletions
+12 -3
View File
@@ -7,7 +7,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { AlertTriangle } from "lucide-react";
import { AlertTriangle, Info } from "lucide-react";
import { useTranslation } from "react-i18next";
interface ConfirmDialogProps {
@@ -16,6 +16,7 @@ interface ConfirmDialogProps {
message: string;
confirmText?: string;
cancelText?: string;
variant?: "destructive" | "info";
onConfirm: () => void;
onCancel: () => void;
}
@@ -26,11 +27,16 @@ export function ConfirmDialog({
message,
confirmText,
cancelText,
variant = "destructive",
onConfirm,
onCancel,
}: ConfirmDialogProps) {
const { t } = useTranslation();
const IconComponent = variant === "info" ? Info : AlertTriangle;
const iconClass =
variant === "info" ? "h-5 w-5 text-blue-500" : "h-5 w-5 text-destructive";
return (
<Dialog
open={isOpen}
@@ -43,7 +49,7 @@ export function ConfirmDialog({
<DialogContent className="max-w-sm" zIndex="alert">
<DialogHeader className="space-y-3 border-b-0 bg-transparent pb-0">
<DialogTitle className="flex items-center gap-2 text-lg font-semibold">
<AlertTriangle className="h-5 w-5 text-destructive" />
<IconComponent className={iconClass} />
{title}
</DialogTitle>
<DialogDescription className="whitespace-pre-line text-sm leading-relaxed">
@@ -54,7 +60,10 @@ export function ConfirmDialog({
<Button variant="outline" onClick={onCancel}>
{cancelText || t("common.cancel")}
</Button>
<Button variant="destructive" onClick={onConfirm}>
<Button
variant={variant === "info" ? "default" : "destructive"}
onClick={onConfirm}
>
{confirmText || t("common.confirm")}
</Button>
</DialogFooter>
+37 -4
View File
@@ -4,7 +4,8 @@ import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import { Provider, UsageScript, UsageData } from "@/types";
import { usageApi, type AppId } from "@/lib/api";
import { usageApi, settingsApi, type AppId } from "@/lib/api";
import { useSettingsQuery } from "@/lib/query";
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
import JsonEditor from "./JsonEditor";
import * as prettier from "prettier/standalone";
@@ -15,6 +16,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { cn } from "@/lib/utils";
interface UsageScriptModalProps {
@@ -112,6 +114,8 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
}) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { data: settingsData } = useSettingsQuery();
const [showUsageConfirm, setShowUsageConfirm] = useState(false);
// 生成带国际化的预设模板
const PRESET_TEMPLATES = generatePresetTemplates(t);
@@ -247,6 +251,27 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
const [showApiKey, setShowApiKey] = useState(false);
const [showAccessToken, setShowAccessToken] = useState(false);
const handleEnableToggle = (checked: boolean) => {
if (checked && !settingsData?.usageConfirmed) {
setShowUsageConfirm(true);
} else {
setScript({ ...script, enabled: checked });
}
};
const handleUsageConfirm = async () => {
setShowUsageConfirm(false);
try {
if (settingsData) {
await settingsApi.save({ ...settingsData, usageConfirmed: true });
await queryClient.invalidateQueries({ queryKey: ["settings"] });
}
} catch (error) {
console.error("Failed to save usage confirmed:", error);
}
setScript({ ...script, enabled: true });
};
const handleSave = () => {
if (script.enabled && !script.code.trim()) {
toast.error(t("usageScript.scriptEmpty"));
@@ -436,9 +461,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
</p>
<Switch
checked={script.enabled}
onCheckedChange={(checked) =>
setScript({ ...script, enabled: checked })
}
onCheckedChange={handleEnableToggle}
aria-label={t("usageScript.enableUsageQuery")}
/>
</div>
@@ -844,6 +867,16 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
</div>
</div>
)}
<ConfirmDialog
isOpen={showUsageConfirm}
variant="info"
title={t("confirm.usage.title")}
message={t("confirm.usage.message")}
confirmText={t("confirm.usage.confirm")}
onConfirm={() => void handleUsageConfirm()}
onCancel={() => setShowUsageConfirm(false)}
/>
</FullScreenPanel>
);
};
@@ -1,3 +1,4 @@
import { useState } from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { Server, Activity, ChevronDown, Zap, Globe } from "lucide-react";
import { motion } from "framer-motion";
@@ -17,6 +18,7 @@ import { AutoFailoverConfigPanel } from "@/components/proxy/AutoFailoverConfigPa
import { FailoverQueueManager } from "@/components/proxy/FailoverQueueManager";
import { RectifierConfigPanel } from "@/components/settings/RectifierConfigPanel";
import { GlobalProxySettings } from "@/components/settings/GlobalProxySettings";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import type { SettingsFormState } from "@/hooks/useSettings";
@@ -30,6 +32,7 @@ export function ProxyTabContent({
onAutoSave,
}: ProxyTabContentProps) {
const { t } = useTranslation();
const [showProxyConfirm, setShowProxyConfirm] = useState(false);
const {
isRunning,
@@ -42,6 +45,8 @@ export function ProxyTabContent({
try {
if (!checked) {
await stopWithRestore();
} else if (!settings?.proxyConfirmed) {
setShowProxyConfirm(true);
} else {
await startProxyServer();
}
@@ -50,6 +55,16 @@ export function ProxyTabContent({
}
};
const handleProxyConfirm = async () => {
setShowProxyConfirm(false);
try {
await onAutoSave({ proxyConfirmed: true });
await startProxyServer();
} catch (error) {
console.error("Proxy confirm failed:", error);
}
};
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
@@ -269,6 +284,16 @@ export function ProxyTabContent({
</AccordionContent>
</AccordionItem>
</Accordion>
<ConfirmDialog
isOpen={showProxyConfirm}
variant="info"
title={t("confirm.proxy.title")}
message={t("confirm.proxy.message")}
confirmText={t("confirm.proxy.confirm")}
onConfirm={() => void handleProxyConfirm()}
onCancel={() => setShowProxyConfirm(false)}
/>
</motion.div>
);
}