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:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user