mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
2dcec4178d
Re-enable the stream check feature that was hidden in v3.11.0. All backend code, database schema, and i18n keys were preserved; only the frontend UI needed uncommenting across 4 files. OpenCode and OpenClaw are excluded as the backend does not support them.
308 lines
8.5 KiB
TypeScript
308 lines
8.5 KiB
TypeScript
import {
|
||
BarChart3,
|
||
Check,
|
||
Copy,
|
||
Edit,
|
||
Loader2,
|
||
Minus,
|
||
Play,
|
||
Plus,
|
||
Terminal,
|
||
TestTube2,
|
||
Trash2,
|
||
Zap,
|
||
} from "lucide-react";
|
||
import { useTranslation } from "react-i18next";
|
||
import { Button } from "@/components/ui/button";
|
||
import { cn } from "@/lib/utils";
|
||
import type { AppId } from "@/lib/api";
|
||
|
||
interface ProviderActionsProps {
|
||
appId?: AppId;
|
||
isCurrent: boolean;
|
||
isInConfig?: boolean;
|
||
isTesting?: boolean;
|
||
isProxyTakeover?: boolean;
|
||
isOmo?: boolean;
|
||
onSwitch: () => void;
|
||
onEdit: () => void;
|
||
onDuplicate: () => void;
|
||
onTest?: () => void;
|
||
onConfigureUsage: () => void;
|
||
onDelete: () => void;
|
||
onRemoveFromConfig?: () => void;
|
||
onDisableOmo?: () => void;
|
||
onOpenTerminal?: () => void;
|
||
isAutoFailoverEnabled?: boolean;
|
||
isInFailoverQueue?: boolean;
|
||
onToggleFailover?: (enabled: boolean) => void;
|
||
// OpenClaw: default model
|
||
isDefaultModel?: boolean;
|
||
onSetAsDefault?: () => void;
|
||
}
|
||
|
||
export function ProviderActions({
|
||
appId,
|
||
isCurrent,
|
||
isInConfig = false,
|
||
isTesting,
|
||
isProxyTakeover = false,
|
||
isOmo = false,
|
||
onSwitch,
|
||
onEdit,
|
||
onDuplicate,
|
||
onTest,
|
||
onConfigureUsage,
|
||
onDelete,
|
||
onRemoveFromConfig,
|
||
onDisableOmo,
|
||
onOpenTerminal,
|
||
isAutoFailoverEnabled = false,
|
||
isInFailoverQueue = false,
|
||
onToggleFailover,
|
||
// OpenClaw: default model
|
||
isDefaultModel = false,
|
||
onSetAsDefault,
|
||
}: ProviderActionsProps) {
|
||
const { t } = useTranslation();
|
||
const iconButtonClass = "h-8 w-8 p-1";
|
||
|
||
// 累加模式应用(OpenCode 非 OMO 和 OpenClaw)
|
||
const isAdditiveMode =
|
||
(appId === "opencode" && !isOmo) || appId === "openclaw";
|
||
|
||
// 故障转移模式下的按钮逻辑(累加模式和 OMO 应用不支持故障转移)
|
||
const isFailoverMode =
|
||
!isAdditiveMode && !isOmo && isAutoFailoverEnabled && onToggleFailover;
|
||
|
||
const handleMainButtonClick = () => {
|
||
if (isOmo) {
|
||
if (isCurrent) {
|
||
onDisableOmo?.();
|
||
} else {
|
||
onSwitch();
|
||
}
|
||
} else if (isAdditiveMode) {
|
||
// 累加模式:切换配置状态(添加/移除)
|
||
if (isInConfig) {
|
||
if (onRemoveFromConfig) {
|
||
onRemoveFromConfig();
|
||
} else {
|
||
onDelete();
|
||
}
|
||
} else {
|
||
onSwitch(); // 添加到配置
|
||
}
|
||
} else if (isFailoverMode) {
|
||
onToggleFailover(!isInFailoverQueue);
|
||
} else {
|
||
onSwitch();
|
||
}
|
||
};
|
||
|
||
const getMainButtonState = () => {
|
||
if (isOmo) {
|
||
if (isCurrent) {
|
||
return {
|
||
disabled: false,
|
||
variant: "secondary" as const,
|
||
className:
|
||
"bg-gray-200 text-muted-foreground hover:bg-gray-200 hover:text-muted-foreground dark:bg-gray-700 dark:hover:bg-gray-700",
|
||
icon: <Check className="h-4 w-4" />,
|
||
text: t("provider.inUse"),
|
||
};
|
||
}
|
||
return {
|
||
disabled: false,
|
||
variant: "default" as const,
|
||
className: "",
|
||
icon: <Play className="h-4 w-4" />,
|
||
text: t("provider.enable"),
|
||
};
|
||
}
|
||
|
||
// 累加模式(OpenCode 非 OMO / OpenClaw)
|
||
if (isAdditiveMode) {
|
||
if (isInConfig) {
|
||
return {
|
||
disabled: isDefaultModel === true,
|
||
variant: "secondary" as const,
|
||
className: cn(
|
||
"bg-orange-100 text-orange-600 hover:bg-orange-200 dark:bg-orange-900/50 dark:text-orange-400 dark:hover:bg-orange-900/70",
|
||
isDefaultModel && "opacity-40 cursor-not-allowed",
|
||
),
|
||
icon: <Minus className="h-4 w-4" />,
|
||
text: t("provider.removeFromConfig", { defaultValue: "移除" }),
|
||
};
|
||
}
|
||
return {
|
||
disabled: false,
|
||
variant: "default" as const,
|
||
className:
|
||
"bg-emerald-500 hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700",
|
||
icon: <Plus className="h-4 w-4" />,
|
||
text: t("provider.addToConfig", { defaultValue: "添加" }),
|
||
};
|
||
}
|
||
|
||
if (isFailoverMode) {
|
||
if (isInFailoverQueue) {
|
||
return {
|
||
disabled: false,
|
||
variant: "secondary" as const,
|
||
className:
|
||
"bg-blue-100 text-blue-600 hover:bg-blue-200 dark:bg-blue-900/50 dark:text-blue-400 dark:hover:bg-blue-900/70",
|
||
icon: <Check className="h-4 w-4" />,
|
||
text: t("failover.inQueue", { defaultValue: "已加入" }),
|
||
};
|
||
}
|
||
return {
|
||
disabled: false,
|
||
variant: "default" as const,
|
||
className:
|
||
"bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700",
|
||
icon: <Plus className="h-4 w-4" />,
|
||
text: t("failover.addQueue", { defaultValue: "加入" }),
|
||
};
|
||
}
|
||
|
||
if (isCurrent) {
|
||
return {
|
||
disabled: true,
|
||
variant: "secondary" as const,
|
||
className:
|
||
"bg-gray-200 text-muted-foreground hover:bg-gray-200 hover:text-muted-foreground dark:bg-gray-700 dark:hover:bg-gray-700",
|
||
icon: <Check className="h-4 w-4" />,
|
||
text: t("provider.inUse"),
|
||
};
|
||
}
|
||
|
||
return {
|
||
disabled: false,
|
||
variant: "default" as const,
|
||
className: isProxyTakeover
|
||
? "bg-emerald-500 hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700"
|
||
: "",
|
||
icon: <Play className="h-4 w-4" />,
|
||
text: t("provider.enable"),
|
||
};
|
||
};
|
||
|
||
const buttonState = getMainButtonState();
|
||
|
||
const canDelete = isOmo || isAdditiveMode ? true : !isCurrent;
|
||
|
||
return (
|
||
<div className="flex items-center gap-1.5">
|
||
{appId === "openclaw" && isInConfig && onSetAsDefault && (
|
||
<Button
|
||
size="sm"
|
||
variant={isDefaultModel ? "secondary" : "default"}
|
||
onClick={isDefaultModel ? undefined : onSetAsDefault}
|
||
disabled={isDefaultModel}
|
||
className={cn(
|
||
"w-fit px-2.5",
|
||
isDefaultModel
|
||
? "bg-gray-200 text-muted-foreground dark:bg-gray-700 opacity-60 cursor-not-allowed"
|
||
: "bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700",
|
||
)}
|
||
>
|
||
<Zap className="h-4 w-4" />
|
||
{isDefaultModel
|
||
? t("provider.isDefault", { defaultValue: "当前默认" })
|
||
: t("provider.setAsDefault", { defaultValue: "设为默认" })}
|
||
</Button>
|
||
)}
|
||
|
||
<Button
|
||
size="sm"
|
||
variant={buttonState.variant}
|
||
onClick={handleMainButtonClick}
|
||
disabled={buttonState.disabled}
|
||
className={cn("w-[4.5rem] px-2.5", buttonState.className)}
|
||
>
|
||
{buttonState.icon}
|
||
{buttonState.text}
|
||
</Button>
|
||
|
||
<div className="flex items-center gap-1">
|
||
<Button
|
||
size="icon"
|
||
variant="ghost"
|
||
onClick={onEdit}
|
||
title={t("common.edit")}
|
||
className={iconButtonClass}
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
</Button>
|
||
|
||
<Button
|
||
size="icon"
|
||
variant="ghost"
|
||
onClick={onDuplicate}
|
||
title={t("provider.duplicate")}
|
||
className={iconButtonClass}
|
||
>
|
||
<Copy className="h-4 w-4" />
|
||
</Button>
|
||
|
||
{onTest && (
|
||
<Button
|
||
size="icon"
|
||
variant="ghost"
|
||
onClick={onTest}
|
||
disabled={isTesting}
|
||
title={t("modelTest.testProvider", "测试模型")}
|
||
className={iconButtonClass}
|
||
>
|
||
{isTesting ? (
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
) : (
|
||
<TestTube2 className="h-4 w-4" />
|
||
)}
|
||
</Button>
|
||
)}
|
||
|
||
<Button
|
||
size="icon"
|
||
variant="ghost"
|
||
onClick={onConfigureUsage}
|
||
title={t("provider.configureUsage")}
|
||
className={iconButtonClass}
|
||
>
|
||
<BarChart3 className="h-4 w-4" />
|
||
</Button>
|
||
|
||
{onOpenTerminal && (
|
||
<Button
|
||
size="icon"
|
||
variant="ghost"
|
||
onClick={onOpenTerminal}
|
||
title={t("provider.openTerminal", "打开终端")}
|
||
className={cn(
|
||
iconButtonClass,
|
||
"hover:text-emerald-600 dark:hover:text-emerald-400",
|
||
)}
|
||
>
|
||
<Terminal className="h-4 w-4" />
|
||
</Button>
|
||
)}
|
||
|
||
<Button
|
||
size="icon"
|
||
variant="ghost"
|
||
onClick={canDelete ? onDelete : undefined}
|
||
title={t("common.delete")}
|
||
className={cn(
|
||
iconButtonClass,
|
||
canDelete && "hover:text-red-500 dark:hover:text-red-400",
|
||
!canDelete && "opacity-40 cursor-not-allowed text-muted-foreground",
|
||
)}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|