- Introduce a new `turnConfig` field in `AssistantConfig` and `Assistant` models to manage user interaction settings. - Implement `TurnConfig`, `BargeInConfig`, `VadConfig`, and `TurnDetectionConfig` schemas to define turn management strategies. - Update the backend to handle turn configuration in the database and during assistant operations. - Enhance frontend components with a `TurnConfigEditor` for configuring turn settings, including VAD and barge-in strategies. - Modify existing pages to integrate turn configuration, improving user experience and interaction capabilities.
205 lines
7.3 KiB
TypeScript
205 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import type { ReactNode } from "react";
|
|
import { HelpCircle, Settings2 } from "lucide-react";
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import type { TurnConfig } from "@/lib/api";
|
|
|
|
type Props = {
|
|
enabled: boolean;
|
|
config: TurnConfig;
|
|
onEnabledChange: (enabled: boolean) => void;
|
|
onConfigChange: (config: TurnConfig) => void;
|
|
};
|
|
|
|
export function TurnConfigEditor({
|
|
enabled,
|
|
config,
|
|
onEnabledChange,
|
|
onConfigChange,
|
|
}: Props) {
|
|
const updateBargeIn = (patch: Partial<TurnConfig["bargeIn"]>) =>
|
|
onConfigChange({
|
|
...config,
|
|
bargeIn: { ...config.bargeIn, ...patch },
|
|
});
|
|
const updateVad = (patch: Partial<TurnConfig["vad"]>) =>
|
|
onConfigChange({
|
|
...config,
|
|
vad: { ...config.vad, ...patch },
|
|
});
|
|
const updateTurnDetection = (
|
|
patch: Partial<TurnConfig["turnDetection"]>,
|
|
) =>
|
|
onConfigChange({
|
|
...config,
|
|
turnDetection: { ...config.turnDetection, ...patch },
|
|
});
|
|
|
|
return (
|
|
<div className="flex items-center justify-between gap-4 rounded-2xl border border-hairline bg-card p-4 shadow-sm">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="text-sm font-medium text-foreground">
|
|
允许用户打断
|
|
</span>
|
|
<HelpHint text="用户说话时,助手可以停止当前播报并理解新的输入。" />
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<button
|
|
type="button"
|
|
aria-label="打开允许用户打断高级配置"
|
|
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
|
|
>
|
|
<Settings2 size={14} />
|
|
</button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-h-[calc(100vh-3rem)] overflow-y-auto sm:max-w-6xl lg:max-w-[88rem] lg:overflow-hidden">
|
|
<DialogHeader>
|
|
<DialogTitle>高级交互配置</DialogTitle>
|
|
<DialogDescription>
|
|
配置 Pipeline 的语音活动检测、用户抢话与对话轮次结束策略。
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="grid gap-5 lg:grid-cols-2">
|
|
<ConfigSection title="VAD 配置">
|
|
<NumberField label="置信度" value={config.vad.confidence} min={0} max={1} step={0.05} onChange={(confidence) => updateVad({ confidence })} />
|
|
<NumberField label="最低音量" value={config.vad.minVolume} min={0} max={1} step={0.05} onChange={(minVolume) => updateVad({ minVolume })} />
|
|
<NumberField label="开始确认(秒)" value={config.vad.startSecs} min={0.05} max={2} step={0.05} onChange={(startSecs) => updateVad({ startSecs })} />
|
|
<NumberField label="停止确认(秒)" value={config.vad.stopSecs} min={0.05} max={3} step={0.05} onChange={(stopSecs) => updateVad({ stopSecs })} />
|
|
</ConfigSection>
|
|
|
|
<div className="space-y-5">
|
|
<ConfigSection title="抢话检测">
|
|
<label className="block space-y-2">
|
|
<span className="text-sm font-medium">检测方法</span>
|
|
<Select
|
|
value={config.bargeIn.strategy}
|
|
onValueChange={(strategy: "vad" | "transcription") =>
|
|
updateBargeIn({ strategy })
|
|
}
|
|
>
|
|
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="vad">VAD 检测</SelectItem>
|
|
<SelectItem value="transcription">转写检测</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</label>
|
|
</ConfigSection>
|
|
|
|
<ConfigSection title="轮次检测">
|
|
<label className="block space-y-2">
|
|
<span className="text-sm font-medium">检测方式</span>
|
|
<Select
|
|
value={config.turnDetection.strategy}
|
|
onValueChange={(strategy: "silence" | "smart_turn") =>
|
|
updateTurnDetection({ strategy })
|
|
}
|
|
>
|
|
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="silence">静音检测</SelectItem>
|
|
<SelectItem value="smart_turn">Smart Turn</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</label>
|
|
{config.turnDetection.strategy === "silence" && (
|
|
<NumberField
|
|
label="静音等待(秒)"
|
|
value={config.turnDetection.silenceTimeoutSecs}
|
|
min={0.2}
|
|
max={5}
|
|
step={0.1}
|
|
onChange={(silenceTimeoutSecs) =>
|
|
updateTurnDetection({ silenceTimeoutSecs })
|
|
}
|
|
/>
|
|
)}
|
|
</ConfigSection>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
<Switch checked={enabled} onCheckedChange={onEnabledChange} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ConfigSection({ title, children }: { title: string; children: ReactNode }) {
|
|
return (
|
|
<section className="rounded-xl border border-hairline bg-surface-strong/20">
|
|
<div className="border-b border-hairline px-4 py-3 text-sm font-medium">
|
|
{title}
|
|
</div>
|
|
<div className="space-y-4 p-4">
|
|
{children}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function HelpHint({ text }: { text: string }) {
|
|
return (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
type="button"
|
|
aria-label="查看允许用户打断说明"
|
|
onClick={(event) => event.stopPropagation()}
|
|
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
|
|
>
|
|
<HelpCircle size={14} />
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
align="start"
|
|
className="w-72 text-sm leading-6 text-muted-foreground"
|
|
>
|
|
{text}
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|
|
|
|
function NumberField({ label, value, min, max, step, onChange }: { label: string; value: number; min: number; max: number; step: number; onChange: (value: number) => void }) {
|
|
return (
|
|
<label className="block space-y-2">
|
|
<span className="text-sm font-medium">{label}</span>
|
|
<Input
|
|
type="number"
|
|
value={value}
|
|
min={min}
|
|
max={max}
|
|
step={step}
|
|
onChange={(event) => {
|
|
const next = event.currentTarget.valueAsNumber;
|
|
if (Number.isFinite(next)) onChange(next);
|
|
}}
|
|
/>
|
|
</label>
|
|
);
|
|
}
|