Implement audio visualizers and refactor AssistantPage

- Introduce three new audio visualizer components: AuraVisualizer, SpectrumVisualizer, and WaveVisualizer, enhancing the audio interaction experience.
- Replace the deprecated VoiceVisualizer with the new visualizers, ensuring a cohesive visual language across components.
- Update the AssistantPage to support dynamic visualization style switching, improving user engagement during audio interactions.
- Refactor DebugVoicePanel to accommodate the new visualizer props and enhance the overall debugging interface.
This commit is contained in:
Xin Wang
2026-06-09 16:28:45 +08:00
parent 4f0f639e8f
commit b3fbfac5df
7 changed files with 813 additions and 397 deletions

View File

@@ -29,6 +29,8 @@ import {
Terminal,
Loader2,
PhoneOff,
Orbit,
Waves,
} from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -54,7 +56,9 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { VoiceVisualizer } from "@/components/ui/voice-visualizer";
import { AuraVisualizer } from "@/components/ui/aura-visualizer";
import { SpectrumVisualizer } from "@/components/ui/spectrum-visualizer";
import { WaveVisualizer } from "@/components/ui/wave-visualizer";
import {
Card,
CardContent,
@@ -1662,32 +1666,76 @@ export function AssistantPage() {
);
}
type VizStyle = "aura" | "bars" | "wave";
const VIZ_ORDER: VizStyle[] = ["aura", "bars", "wave"];
const VIZ_LABEL: Record<VizStyle, string> = {
aura: "光环",
bars: "频谱",
wave: "波形",
};
function DebugDrawer() {
const [showTranscript, setShowTranscript] = useState(false);
const [vizStyle, setVizStyle] = useState<VizStyle>("wave");
return (
<aside className="hidden min-w-0 flex-1 flex-col overflow-hidden rounded-2xl border border-hairline bg-card shadow-sm lg:flex">
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-hairline px-5 py-4">
<div className="text-sm font-medium text-foreground"></div>
<Button
type="button"
variant={showTranscript ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-full text-xs font-medium"
onClick={() => setShowTranscript((value) => !value)}
aria-label={showTranscript ? "显示音频可视化" : "显示文字聊天记录"}
aria-pressed={showTranscript}
>
</Button>
<div className="flex items-center gap-2">
{!showTranscript && (
<Button
type="button"
variant="outline"
size="icon"
className="h-8 w-8 rounded-full"
onClick={() =>
setVizStyle(
(value) =>
VIZ_ORDER[
(VIZ_ORDER.indexOf(value) + 1) % VIZ_ORDER.length
],
)
}
aria-label={`切换可视化样式(当前:${VIZ_LABEL[vizStyle]}`}
title={`可视化:${VIZ_LABEL[vizStyle]}`}
>
{vizStyle === "aura" ? (
<Orbit size={16} />
) : vizStyle === "bars" ? (
<AudioLines size={16} />
) : (
<Waves size={16} />
)}
</Button>
)}
<Button
type="button"
variant={showTranscript ? "default" : "outline"}
size="icon"
className="h-8 w-8 rounded-full text-xs font-medium"
onClick={() => setShowTranscript((value) => !value)}
aria-label={showTranscript ? "显示音频可视化" : "显示文字聊天记录"}
aria-pressed={showTranscript}
>
</Button>
</div>
</div>
<DebugVoicePanel showTranscript={showTranscript} />
<DebugVoicePanel showTranscript={showTranscript} vizStyle={vizStyle} />
</aside>
);
}
function DebugVoicePanel({ showTranscript }: { showTranscript: boolean }) {
function DebugVoicePanel({
showTranscript,
vizStyle,
}: {
showTranscript: boolean;
vizStyle: VizStyle;
}) {
const [recording, setRecording] = useState(false);
const [micError, setMicError] = useState(false);
@@ -1696,7 +1744,7 @@ function DebugVoicePanel({ showTranscript }: { showTranscript: boolean }) {
{showTranscript ? (
<DebugTranscriptPanel />
) : (
<div className="relative flex min-h-0 flex-1 flex-col items-center justify-center overflow-y-auto px-5 py-3 text-center">
<div className="relative flex min-h-0 flex-1 flex-col items-center justify-center gap-3 overflow-y-auto px-6 py-3 text-center">
<div
className="pointer-events-none absolute left-1/2 top-2 h-72 w-72 -translate-x-1/2 rounded-full opacity-50 blur-3xl"
style={{
@@ -1718,22 +1766,32 @@ function DebugVoicePanel({ showTranscript }: { showTranscript: boolean }) {
{recording ? "会话进行中" : "准备开始"}
</Badge>
<VoiceVisualizer
active={recording}
size={220}
barCount={96}
className="relative -my-2 shrink-0"
onError={() => {
setMicError(true);
setRecording(false);
}}
/>
<div className="relative flex h-[200px] w-[240px] shrink-0 items-center justify-center">
{(() => {
const onVizError = () => {
setMicError(true);
setRecording(false);
};
const shared = {
active: recording,
className: "relative shrink-0",
onError: onVizError,
} as const;
if (vizStyle === "aura")
return <AuraVisualizer {...shared} size={200} />;
if (vizStyle === "bars")
return (
<SpectrumVisualizer {...shared} size={200} barCount={64} />
);
return <WaveVisualizer {...shared} size={200} />;
})()}
</div>
<div className="relative -mt-1">
<div className="font-display text-xl text-foreground">
<div className="relative max-w-xs space-y-1.5">
<div className="font-display display-sm text-foreground">
{recording ? "我在聆听" : "开始一次语音对话"}
</div>
<p className="mx-auto mt-1 max-w-xs text-xs leading-5 text-muted-foreground">
<p className="mx-auto text-xs leading-5 text-muted-foreground">
{micError
? "无法访问麦克风,请检查浏览器权限后重试。"
: recording
@@ -1742,24 +1800,22 @@ function DebugVoicePanel({ showTranscript }: { showTranscript: boolean }) {
</p>
</div>
<div className="relative mt-3 flex items-center gap-3">
<Button
onClick={() => {
setMicError(false);
setRecording((value) => !value);
}}
size="icon"
className={[
"h-12 w-12 rounded-full shadow-md transition-transform hover:scale-105",
recording
? "bg-destructive text-white hover:bg-destructive/90"
: "",
].join(" ")}
aria-label={recording ? "结束语音测试" : "开始语音测试"}
>
{recording ? <PhoneOff size={20} /> : <Mic size={21} />}
</Button>
</div>
<Button
onClick={() => {
setMicError(false);
setRecording((value) => !value);
}}
className={[
"relative h-11 gap-2 rounded-full px-6 text-sm font-medium shadow-sm transition-transform hover:scale-[1.03]",
recording
? "bg-destructive text-white hover:bg-destructive/90"
: "",
].join(" ")}
aria-label={recording ? "结束语音测试" : "开始语音测试"}
>
{recording ? <PhoneOff size={18} /> : <Mic size={18} />}
{recording ? "结束对话" : "开始对话"}
</Button>
</div>
)}