Refactor AssistantPage and DebugDrawer components

- Remove debug mode state management from AssistantPage, simplifying the component structure.
- Update DebugDrawer to eliminate mode selection, focusing on voice interaction features.
- Enhance the VoiceVisualizer component with improved visual effects and responsiveness to audio input.
- Adjust styles and layout for better user experience in the debugging interface.
This commit is contained in:
Xin Wang
2026-06-09 15:28:24 +08:00
parent c64b7dcf99
commit 4f0f639e8f
2 changed files with 216 additions and 130 deletions

View File

@@ -23,13 +23,12 @@ import {
Save,
Mic,
Send,
MessageCircle,
HelpCircle,
Waypoints,
AudioLines,
Square,
Terminal,
Loader2,
PhoneOff,
} from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -201,8 +200,6 @@ type AssistantListItem = {
updatedAt: string;
};
type DebugMode = "text" | "voice";
type TypeFilter = "全部" | AssistantType;
const typeFilters: TypeFilter[] = ["全部", ...assistantTypes];
@@ -271,8 +268,6 @@ export function AssistantPage() {
const [searchQuery, setSearchQuery] = useState("");
const [typeFilter, setTypeFilter] = useState<TypeFilter>("全部");
const [currentPage, setCurrentPage] = useState(1);
// 右侧调试 drawer 的当前模式
const [debugMode, setDebugMode] = useState<DebugMode>("text");
// choose 步骤的草稿:名称与已选类型,确认后才决定进入哪个构建页
const [draftName, setDraftName] = useState("");
const [draftType, setDraftType] = useState<AssistantType | null>(null);
@@ -1229,7 +1224,7 @@ export function AssistantPage() {
</SectionCard>
</div>
<DebugDrawer mode={debugMode} onModeChange={setDebugMode} />
<DebugDrawer />
</div>
</div>
);
@@ -1333,7 +1328,7 @@ export function AssistantPage() {
</SectionCard>
</div>
<DebugDrawer mode={debugMode} onModeChange={setDebugMode} />
<DebugDrawer />
</div>
</div>
);
@@ -1451,7 +1446,7 @@ export function AssistantPage() {
</SectionCard>
</div>
<DebugDrawer mode={debugMode} onModeChange={setDebugMode} />
<DebugDrawer />
</div>
</div>
);
@@ -1661,88 +1656,121 @@ export function AssistantPage() {
</SectionCard>
</div>
<DebugDrawer mode={debugMode} onModeChange={setDebugMode} />
<DebugDrawer />
</div>
</div>
);
}
function DebugDrawer({
mode,
onModeChange,
}: {
mode: DebugMode;
onModeChange: (mode: DebugMode) => void;
}) {
const modeTabs: { key: DebugMode; label: string; icon: React.ReactNode }[] = [
{ key: "text", label: "文字测试", icon: <MessageCircle size={15} /> },
{ key: "voice", label: "语音测试", icon: <Mic size={15} /> },
];
function DebugDrawer() {
const [showTranscript, setShowTranscript] = useState(false);
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="shrink-0 border-b border-hairline p-4">
<div className="mb-3 text-sm font-medium text-foreground"></div>
<div className="flex gap-1 rounded-full bg-surface-strong p-1">
{modeTabs.map((tab) => (
<button
key={tab.key}
type="button"
onClick={() => onModeChange(tab.key)}
className={[
"flex flex-1 items-center justify-center gap-1.5 rounded-full px-3 py-1.5 text-sm transition-colors",
mode === tab.key
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
].join(" ")}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
<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>
{mode === "text" ? <DebugTextPanel /> : <DebugVoicePanel />}
<DebugVoicePanel showTranscript={showTranscript} />
</aside>
);
}
function DebugTextPanel() {
function DebugVoicePanel({ showTranscript }: { showTranscript: boolean }) {
const [recording, setRecording] = useState(false);
const [micError, setMicError] = useState(false);
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4">
<div className="flex max-w-[85%] flex-col gap-1">
<div className="rounded-2xl rounded-tl-sm bg-surface-strong px-4 py-2.5 text-sm leading-6 text-foreground">
AI
</div>
<span className="px-1 text-xs text-muted-soft"></span>
</div>
{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="pointer-events-none absolute left-1/2 top-2 h-72 w-72 -translate-x-1/2 rounded-full opacity-50 blur-3xl"
style={{
background:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-sky) 42%, transparent), color-mix(in srgb, var(--gradient-lavender) 18%, transparent) 48%, transparent 74%)",
}}
/>
<div className="flex max-w-[85%] flex-col items-end gap-1 self-end">
<div className="rounded-2xl rounded-tr-sm bg-primary px-4 py-2.5 text-sm leading-6 text-primary-foreground">
</div>
<span className="px-1 text-xs text-muted-soft"></span>
</div>
<Badge
variant="secondary"
className="relative gap-1.5 rounded-full border border-hairline bg-canvas-soft px-3 py-1 text-[11px] font-medium text-muted-foreground shadow-none"
>
<span
className={[
"h-1.5 w-1.5 rounded-full",
recording ? "animate-pulse bg-success" : "bg-muted-soft",
].join(" ")}
/>
{recording ? "会话进行中" : "准备开始"}
</Badge>
<div className="flex max-w-[85%] flex-col gap-1">
<div className="rounded-2xl rounded-tl-sm bg-surface-strong px-4 py-2.5 text-sm leading-6 text-foreground">
线线线 App
线
</div>
<span className="px-1 text-xs text-muted-soft"></span>
</div>
</div>
<VoiceVisualizer
active={recording}
size={220}
barCount={96}
className="relative -my-2 shrink-0"
onError={() => {
setMicError(true);
setRecording(false);
}}
/>
<div className="shrink-0 border-t border-hairline p-3">
<div className="relative -mt-1">
<div className="font-display text-xl text-foreground">
{recording ? "我在聆听" : "开始一次语音对话"}
</div>
<p className="mx-auto mt-1 max-w-xs text-xs leading-5 text-muted-foreground">
{micError
? "无法访问麦克风,请检查浏览器权限后重试。"
: recording
? "直接说话即可。助手会在您停顿后自然回应。"
: "测试语音识别、响应速度与助手的播报效果。"}
</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>
</div>
)}
<div className="shrink-0 border-t border-hairline bg-card p-3">
<div className="flex items-end gap-2">
<Textarea
rows={1}
placeholder="输入测试消息…"
className="max-h-28 min-h-10 flex-1 resize-none border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
placeholder="输入文字以模拟用户消息…"
className="max-h-24 min-h-10 flex-1 resize-none border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
<Button size="icon" className="shrink-0" aria-label="发送">
<Button size="icon" className="shrink-0" aria-label="发送调试消息">
<Send size={16} />
</Button>
</div>
@@ -1751,54 +1779,31 @@ function DebugTextPanel() {
);
}
function DebugVoicePanel() {
const [recording, setRecording] = useState(false);
const [micError, setMicError] = useState(false);
function DebugTranscriptPanel() {
return (
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-6 p-6 text-center">
<VoiceVisualizer
active={recording}
size={200}
onError={() => {
setMicError(true);
setRecording(false);
}}
/>
<div>
<div className="text-sm font-medium text-foreground">
{recording ? "正在聆听…" : "点击开始语音测试"}
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-4">
<div className="flex flex-col gap-4">
<div className="flex max-w-[88%] flex-col gap-1 self-start">
<span className="px-1 text-[11px] text-muted-soft"> · 10:24</span>
<div className="rounded-2xl rounded-tl-sm bg-surface-strong px-4 py-2.5 text-sm leading-6 text-foreground">
AI
</div>
</div>
<p className="mt-1.5 text-sm leading-6 text-muted-foreground">
{micError
? "无法访问麦克风,请检查浏览器权限后重试。"
: "点击下方按钮开始录音,实时识别结果将显示在下方。"}
</p>
</div>
<Button
onClick={() => {
setMicError(false);
setRecording((value) => !value);
}}
variant={recording ? "outline" : "default"}
className={[
"gap-2",
recording
? "border-hairline-strong text-muted-foreground hover:text-foreground"
: "",
].join(" ")}
>
{recording ? <Square size={16} /> : <Mic size={16} />}
{recording ? "结束录音" : "开始语音测试"}
</Button>
<div className="flex max-w-[88%] flex-col items-end gap-1 self-end">
<span className="px-1 text-[11px] text-muted-soft"> · 10:25</span>
<div className="rounded-2xl rounded-tr-sm bg-primary px-4 py-2.5 text-sm leading-6 text-primary-foreground">
</div>
</div>
<div className="w-full rounded-xl border border-hairline bg-canvas-soft p-4 text-left">
<div className="caption-label mb-2 text-muted-soft"></div>
<p className="text-sm leading-6 text-muted-soft">
{recording ? "聆听中,请开始说话…" : "等待语音输入…"}
</p>
<div className="flex max-w-[88%] flex-col gap-1 self-start">
<span className="px-1 text-[11px] text-muted-soft"> · 10:25</span>
<div className="rounded-2xl rounded-tl-sm bg-surface-strong px-4 py-2.5 text-sm leading-6 text-foreground">
线线线 App
线
</div>
</div>
</div>
</div>
);