Files
next-tjwater-frontend/src/features/agent/components/agent-speech-voice-picker.tsx
T
jiang c0e4231b83
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s
feat: expand intelligent investigation workspace
2026-10-30 17:30:19 +08:00

74 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import useSWR from "swr";
import { Check, ChevronDown, SlidersHorizontal } from "lucide-react";
import { fetchSpeechVoiceCatalog } from "../speech-voice-catalog";
import { Button } from "@/shared/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/shared/ui/command";
import { getSpeechVoiceDescription, getSpeechVoiceName, setSelectedSpeechVoice, useSelectedSpeechVoice } from "../speech-voices";
import { AGENT_MESSAGE_ACTION_CLASS_NAME, AGENT_MESSAGE_ACTION_LABEL_CLASS_NAME } from "./agent-chat-message-chrome";
export function AgentSpeechVoicePicker() {
const [open, setOpen] = useState(false);
const selected = useSelectedSpeechVoice();
const { data, error, isLoading, mutate } = useSWR("/api/tts/edge/voices", fetchSpeechVoiceCatalog, {
revalidateOnFocus: false, revalidateOnReconnect: false, dedupingInterval: 60 * 60 * 1_000,
shouldRetryOnError: false
});
const selectedVoice = data?.voices.find((voice) => voice.id === (selected || data.defaultVoice));
const defaultVoice = data?.voices.find((voice) => voice.id === data.defaultVoice);
const selectedName = selectedVoice ? getSpeechVoiceName(selectedVoice) : selected ? "已选音色" : "默认音色";
const choose = (id: string) => { setSelectedSpeechVoice(id); setOpen(false); };
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" size="sm" className={AGENT_MESSAGE_ACTION_CLASS_NAME}
aria-label={`选择朗读音色,当前${selectedName}`} title={`朗读音色:${selectedName}`}>
<SlidersHorizontal size={16} aria-hidden="true" />
<span className={AGENT_MESSAGE_ACTION_LABEL_CLASS_NAME}>音色</span>
<ChevronDown className="size-3" aria-hidden="true" />
</Button>
</PopoverTrigger>
<PopoverContent data-agent-theme="chat" align="end" side="top" collisionPadding={12} className="z-[90] w-80 overflow-hidden p-0" aria-label="选择朗读音色">
<div className="border-b px-3 py-2">
<p className="text-sm font-medium">普通话音色</p>
<p className="text-xs text-muted-foreground">{data ? `${data.voices.length} 种音色,选择后下次播放生效` : "选择后下次播放生效"}</p>
</div>
<Command loop>
<CommandInput placeholder="搜索普通话音色" aria-label="搜索音色" />
<CommandList className="max-h-[min(320px,calc(var(--radix-popover-content-available-height)-180px))] overscroll-contain">
{isLoading ? <p role="status" className="p-3 text-sm text-muted-foreground">正在加载音色…</p> : null}
{error ? <div role="alert" className="p-3 text-sm">
<p>音色列表加载失败</p>
{error instanceof Error ? <p className="mt-1 text-xs text-muted-foreground">{error.message}</p> : null}
<Button variant="link" size="sm" className="px-0" onClick={() => void mutate()}>重新加载音色</Button>
</div> : null}
<CommandEmpty>没有找到匹配的音色</CommandEmpty>
<CommandGroup>
<CommandItem value="default 默认音色" onSelect={() => choose("")}>
<span className="flex-1">默认音色{defaultVoice ? `(${getSpeechVoiceName(defaultVoice)})` : ""}</span>
{!selected ? <Check aria-hidden="true" /> : null}
</CommandItem>
</CommandGroup>
{selected && data && !selectedVoice ? <p role="status" className="px-3 py-2 text-xs text-muted-foreground">原选音色已不可用,请重新选择。</p> : null}
<CommandGroup>
{data?.voices.map((voice) => {
const name = getSpeechVoiceName(voice);
const description = getSpeechVoiceDescription(voice);
return <CommandItem key={voice.id} value={`${voice.id} ${name} ${description} ${voice.name}`} onSelect={() => choose(voice.id)}>
<span className="min-w-0 flex-1">
<span className="block break-words text-sm">{name}</span>
<span className="block break-words text-xs text-muted-foreground">{description}</span>
</span>
{selected === voice.id ? <Check aria-hidden="true" /> : null}
</CommandItem>;
})}
</CommandGroup>
</CommandList>
</Command>
<p className="border-t px-3 py-2 text-xs text-muted-foreground">音色偏好保存在当前浏览器。</p>
</PopoverContent>
</Popover>
);
}