Files
ai-video-fullstack/frontend/src/components/turn-config-editor.tsx
Xin Wang 01c563a3e7 Add turn configuration support for assistants
- 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.
2026-07-12 11:08:19 +08:00

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>
);
}