Files
ai-video-fullstack/frontend/src/components/test-cases/input-mode-picker.tsx

189 lines
4.8 KiB
TypeScript

"use client";
/**
* 测试用例顶部紧凑模式选择器:固定脚本 / 用户模拟。
*/
import {
AudioLines,
Check,
ChevronDown,
MessageSquareText,
Mic,
Type,
Volume2,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
TEST_CASE_INPUT_MODE_LABEL,
type TestCaseInputMode,
} from "@/data/test-suites";
import { cn } from "@/lib/utils";
const FIXED_SCRIPT_OPTIONS: {
value: TestCaseInputMode;
title: string;
description: string;
icon: typeof Type;
available: boolean;
}[] = [
{
value: "fixed_script_text",
title: "文字",
description: "使用固定文字逐轮测试",
icon: Type,
available: true,
},
{
value: "fixed_script_turn_voice",
title: "逐轮语音",
description: "固定文字自动合成为语音",
icon: Volume2,
available: false,
},
{
value: "fixed_script_continuous_voice",
title: "连续语音",
description: "按指定时序连续发送语音",
icon: AudioLines,
available: false,
},
];
const USER_SIM_OPTIONS: {
value: TestCaseInputMode;
title: string;
description: string;
icon: typeof MessageSquareText;
available: boolean;
}[] = [
{
value: "user_sim_text",
title: "文字",
description: "使用模型模拟文字用户",
icon: MessageSquareText,
available: false,
},
{
value: "user_sim_voice",
title: "语音",
description: "使用模型模拟语音用户",
icon: Mic,
available: false,
},
];
export function InputModePicker({
value,
onChange,
}: {
value: TestCaseInputMode;
onChange: (mode: TestCaseInputMode) => void;
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className="h-9 max-w-[220px] shrink-0 gap-1.5 rounded-full border-hairline-strong bg-background px-3 text-xs font-normal text-foreground"
>
<span className="truncate">{TEST_CASE_INPUT_MODE_LABEL[value]}</span>
<ChevronDown size={14} className="shrink-0 text-muted-soft" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-72 p-1.5">
<DropdownMenuLabel className="px-2.5 py-1.5 text-[11px] font-medium tracking-wide text-muted-soft uppercase">
</DropdownMenuLabel>
{FIXED_SCRIPT_OPTIONS.map((option) => (
<ModeMenuItem
key={option.value}
selected={value === option.value}
title={option.title}
description={option.description}
icon={option.icon}
available={option.available}
onSelect={() => onChange(option.value)}
/>
))}
<DropdownMenuSeparator className="my-1.5" />
<DropdownMenuLabel className="px-2.5 py-1.5 text-[11px] font-medium tracking-wide text-muted-soft uppercase">
</DropdownMenuLabel>
{USER_SIM_OPTIONS.map((option) => (
<ModeMenuItem
key={option.value}
selected={value === option.value}
title={option.title}
description={option.description}
icon={option.icon}
available={option.available}
onSelect={() => onChange(option.value)}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
function ModeMenuItem({
selected,
title,
description,
icon: Icon,
available,
onSelect,
}: {
selected: boolean;
title: string;
description: string;
icon: typeof Type;
available: boolean;
onSelect: () => void;
}) {
return (
<button
type="button"
disabled={!available}
aria-disabled={!available}
title={available ? undefined : `${title}模式即将支持`}
onClick={available ? onSelect : undefined}
className={cn(
"flex w-full items-start gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors",
available
? "hover:bg-surface-strong"
: "cursor-not-allowed opacity-55",
selected && "bg-surface-strong/80",
)}
>
<Icon size={15} className="mt-0.5 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1">
<span className="flex items-center gap-1.5 text-sm font-medium text-foreground">
{title}
{selected && <Check size={13} className="text-muted-foreground" />}
{!available && (
<span className="ml-auto text-[11px] font-normal text-muted-soft">
</span>
)}
</span>
<span className="mt-0.5 block text-xs leading-4 text-muted-soft">
{description}
</span>
</span>
</button>
);
}