From d5e4e8d13330927ad0069074cc4899b909c136dc Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 28 Feb 2026 09:13:32 +0800 Subject: [PATCH] refactor: move proxy toggle into panel and surface app takeover options Move the proxy on/off switch from the accordion header into the panel content area, placing it right above the app takeover section. This ensures users see the takeover options immediately after enabling the proxy, preventing the common pitfall of running the proxy without actually taking over any app. - Simplify accordion trigger to standard style with Badge only - Add AnimatePresence animation for takeover section reveal - Remove duplicate takeover switches from running info card - Update stoppedDescription i18n to reference "above toggle" - Add proxy.takeover.hint key in zh/en/ja --- src/components/proxy/ProxyPanel.tsx | 157 ++++++++++++++------ src/components/settings/ProxyTabContent.tsx | 58 +++----- src/i18n/locales/en.json | 8 +- src/i18n/locales/ja.json | 8 +- src/i18n/locales/zh.json | 8 +- 5 files changed, 153 insertions(+), 86 deletions(-) diff --git a/src/components/proxy/ProxyPanel.tsx b/src/components/proxy/ProxyPanel.tsx index e874e0cdb..1052f2ece 100644 --- a/src/components/proxy/ProxyPanel.tsx +++ b/src/components/proxy/ProxyPanel.tsx @@ -7,11 +7,14 @@ import { ListOrdered, Save, Loader2, + Zap, + Power, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; +import { ToggleRow } from "@/components/ui/toggle-row"; import { useProxyStatus } from "@/hooks/useProxyStatus"; import { toast } from "sonner"; import { useFailoverQueue } from "@/lib/query/failover"; @@ -25,8 +28,21 @@ import { } from "@/lib/query/proxy"; import type { ProxyStatus } from "@/types/proxy"; import { useTranslation } from "react-i18next"; +import { AnimatePresence, motion } from "framer-motion"; -export function ProxyPanel() { +interface ProxyPanelProps { + enableLocalProxy: boolean; + onEnableLocalProxyChange: (checked: boolean) => void; + onToggleProxy: (checked: boolean) => Promise; + isProxyPending: boolean; +} + +export function ProxyPanel({ + enableLocalProxy, + onEnableLocalProxyChange, + onToggleProxy, + isProxyPending, +}: ProxyPanelProps) { const { t } = useTranslation(); const { status, isRunning } = useProxyStatus(); @@ -183,9 +199,98 @@ export function ProxyPanel() { return ( <> -
+
+ {/* [1] Enable proxy button on main page — always visible */} + } + title={t("settings.advanced.proxy.enableFeature")} + description={t("settings.advanced.proxy.enableFeatureDescription")} + checked={enableLocalProxy} + onCheckedChange={onEnableLocalProxyChange} + /> + + {/* [2] Proxy service toggle — always visible */} +
+
+
+ +
+
+

+ {t("proxyConfig.proxyEnabled", { + defaultValue: "代理服务", + })} +

+

+ {isRunning + ? t("settings.advanced.proxy.running") + : t("settings.advanced.proxy.stopped")} +

+
+
+ +
+ + {/* [3] App takeover switches — animated, visible only when proxy is running */} + + {isRunning && ( + +
+

+ {t("proxyConfig.appTakeover", { + defaultValue: "应用接管", + })} +

+
+ {(["claude", "codex", "gemini"] as const).map((appType) => { + const isEnabled = + takeoverStatus?.[ + appType as keyof typeof takeoverStatus + ] ?? false; + return ( +
+ + {appType} + + + handleTakeoverChange(appType, checked) + } + disabled={setTakeoverForApp.isPending} + /> +
+ ); + })} +
+

+ {t("proxy.takeover.hint", { + defaultValue: + "选择要接管的应用,启用后该应用的请求将通过本地代理转发", + })} +

+
+
+ )} +
+ + {/* Running state: service info + stats */} {isRunning && status ? (
+ {/* [4] Running info: address + current provider */}

@@ -263,41 +368,7 @@ export function ProxyPanel() { )}

- {/* 应用接管开关 */} -
-

- {t("proxyConfig.appTakeover", { - defaultValue: "应用接管", - })} -

-
- {(["claude", "codex", "gemini"] as const).map((appType) => { - const isEnabled = - takeoverStatus?.[ - appType as keyof typeof takeoverStatus - ] ?? false; - return ( -
- - {appType} - - - handleTakeoverChange(appType, checked) - } - disabled={setTakeoverForApp.isPending} - /> -
- ); - })} -
-
- - {/* 日志记录开关 */} + {/* [5] Logging toggle */}
@@ -320,7 +391,7 @@ export function ProxyPanel() {
- {/* 供应商队列 - 按应用类型分组展示 */} + {/* [6] Provider queues */} {(claudeQueue.length > 0 || codexQueue.length > 0 || geminiQueue.length > 0) && ( @@ -332,7 +403,6 @@ export function ProxyPanel() {

- {/* Claude 队列 */} {claudeQueue.length > 0 && ( )} - {/* Codex 队列 */} {codexQueue.length > 0 && ( )} - {/* Gemini 队列 */} {geminiQueue.length > 0 && ( + {/* [7] Stats cards */}
} @@ -408,7 +477,7 @@ export function ProxyPanel() {
) : (
- {/* 基础设置 - 监听地址/端口 */} + {/* [8] Basic settings — address/port (only when stopped) */}

@@ -496,7 +565,7 @@ export function ProxyPanel() {

- {/* 代理服务已停止提示 */} + {/* Stopped hint */}
@@ -508,7 +577,7 @@ export function ProxyPanel() {

{t("proxy.panel.stoppedDescription", { - defaultValue: "使用右上角开关即可启动服务", + defaultValue: "使用上方开关即可启动服务", })}

diff --git a/src/components/settings/ProxyTabContent.tsx b/src/components/settings/ProxyTabContent.tsx index 825f34b3e..3d99aae35 100644 --- a/src/components/settings/ProxyTabContent.tsx +++ b/src/components/settings/ProxyTabContent.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { Server, Activity, ChevronDown, Zap, Globe } from "lucide-react"; +import { Server, Activity, Zap, Globe } from "lucide-react"; import { motion } from "framer-motion"; import { useTranslation } from "react-i18next"; import { @@ -10,8 +9,6 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Switch } from "@/components/ui/switch"; -import { ToggleRow } from "@/components/ui/toggle-row"; import { Badge } from "@/components/ui/badge"; import { ProxyPanel } from "@/components/proxy"; import { AutoFailoverConfigPanel } from "@/components/proxy/AutoFailoverConfigPanel"; @@ -76,28 +73,22 @@ export function ProxyTabContent({ {/* Local Proxy */} - - -
- -
-

- {t("settings.advanced.proxy.title")} -

-

- {t("settings.advanced.proxy.description")} -

-
+ +
+ +
+

+ {t("settings.advanced.proxy.title")} +

+

+ {t("settings.advanced.proxy.description")} +

- - - -
-
- + - } - title={t("settings.advanced.proxy.enableFeature")} - description={t( - "settings.advanced.proxy.enableFeatureDescription", - )} - checked={settings?.enableLocalProxy ?? false} - onCheckedChange={(checked) => + onAutoSave({ enableLocalProxy: checked }) } + onToggleProxy={handleToggleProxy} + isProxyPending={isProxyPending} /> -
- -
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6ed487f3d..fccbaa456 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1582,7 +1582,7 @@ "currentProvider": "Current Provider:", "waitingFirstRequest": "Current Provider: Waiting for first request...", "stoppedTitle": "Proxy Service Stopped", - "stoppedDescription": "Use the toggle in the top right to start the service", + "stoppedDescription": "Use the toggle above to start the service", "openSettings": "Configure Proxy Service", "stats": { "activeConnections": "Active Connections", @@ -1673,6 +1673,12 @@ "restartRequired": "Restart proxy service for address or port changes to take effect" }, "switchFailed": "Switch failed: {{error}}", + "takeover": { + "hint": "Select apps to take over — once enabled, requests from that app will be routed through the local proxy", + "enabled": "{{app}} takeover enabled", + "disabled": "{{app}} takeover disabled", + "failed": "Failed to toggle takeover" + }, "failover": { "proxyRequired": "Proxy service must be started to configure failover", "autoSwitch": "Auto Failover", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 0b985a535..f6b5508d6 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1582,7 +1582,7 @@ "currentProvider": "現在のプロバイダー:", "waitingFirstRequest": "現在のプロバイダー: 最初のリクエスト待ち...", "stoppedTitle": "プロキシサービス停止中", - "stoppedDescription": "右上のトグルでサービスを開始できます", + "stoppedDescription": "上のトグルでサービスを開始できます", "openSettings": "プロキシサービスを設定", "stats": { "activeConnections": "アクティブ接続", @@ -1673,6 +1673,12 @@ "restartRequired": "アドレスまたはポートの変更を反映するにはプロキシサービスの再起動が必要です" }, "switchFailed": "切り替えに失敗しました: {{error}}", + "takeover": { + "hint": "テイクオーバーするアプリを選択します。有効にすると、そのアプリのリクエストはローカルプロキシ経由で転送されます", + "enabled": "{{app}} テイクオーバー有効", + "disabled": "{{app}} テイクオーバー無効", + "failed": "テイクオーバーの切り替えに失敗しました" + }, "failover": { "proxyRequired": "フェイルオーバーを設定するには、プロキシサービスを先に起動する必要があります", "autoSwitch": "自動フェイルオーバー", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d4351bdf6..0fe3901c3 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1582,7 +1582,7 @@ "currentProvider": "当前 Provider:", "waitingFirstRequest": "当前 Provider:等待首次请求…", "stoppedTitle": "代理服务已停止", - "stoppedDescription": "使用右上角开关即可启动服务", + "stoppedDescription": "使用上方开关即可启动服务", "openSettings": "配置代理服务", "stats": { "activeConnections": "活跃连接", @@ -1673,6 +1673,12 @@ "restartRequired": "修改地址或端口后需要重启代理服务才能生效" }, "switchFailed": "切换失败: {{error}}", + "takeover": { + "hint": "选择要接管的应用,启用后该应用的请求将通过本地代理转发", + "enabled": "{{app}} 接管已启用", + "disabled": "{{app}} 接管已关闭", + "failed": "切换接管状态失败" + }, "failover": { "proxyRequired": "需要先启动代理服务才能配置故障转移", "autoSwitch": "自动故障转移",