"use client"; import { Bot, Boxes, Brain, Check, Copy, Database, Eye, EyeOff, MessageSquareText, MoreHorizontal, Pencil, Plus, Rocket, Search, Sparkles, Trash2, Workflow, ChevronLeft, ChevronRight, Save, Mic, Send, HelpCircle, Waypoints, AudioLines, Terminal, Loader2, PhoneOff, Orbit, Waves, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { AuraVisualizer } from "@/components/ui/aura-visualizer"; import { SpectrumVisualizer } from "@/components/ui/spectrum-visualizer"; import { WaveVisualizer } from "@/components/ui/wave-visualizer"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { useCallback, useEffect, useRef, useState } from "react"; import { assistantsApi, credentialsApi, knowledgeBasesApi, type Assistant, type AssistantType as ApiAssistantType, type AssistantUpsert, type Credential, type KnowledgeBase, } from "@/lib/api"; type RuntimeMode = "pipeline" | "realtime"; type AssistantForm = { name: string; greeting: string; prompt: string; runtimeMode: RuntimeMode; realtimeModel: string; model: string; asr: string; voice: string; knowledgeBase: string; enableInterrupt: boolean; }; type FastGptForm = { name: string; appId: string; apiUrl: string; apiKey: string; asr: string; voice: string; enableInterrupt: boolean; }; type DifyForm = { name: string; apiUrl: string; apiKey: string; asr: string; voice: string; enableInterrupt: boolean; }; type OpenCodeForm = { name: string; prompt: string; apiUrl: string; apiKey: string; model: string; asr: string; voice: string; enableInterrupt: boolean; }; type AssistantType = "提示词" | "工作流" | "Dify" | "FastGPT" | "OpenCode"; const assistantTypes: AssistantType[] = [ "提示词", "工作流", "Dify", "FastGPT", "OpenCode", ]; // 后端 type(英文) ↔ 列表展示标签(中文) const typeToLabel: Record = { prompt: "提示词", workflow: "工作流", dify: "Dify", fastgpt: "FastGPT", opencode: "OpenCode", }; function formatTimestamp(iso?: string | null): string { if (!iso) return ""; const d = new Date(iso); if (Number.isNaN(d.getTime())) return ""; const pad = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad( d.getHours(), )}:${pad(d.getMinutes())}`; } type AssistantTypeOption = { type: AssistantType; label: string; description: string; icon: React.ReactNode; /** 提示词、Dify、FastGPT 类型已落地,工作流暂时显示占位页 */ available: boolean; }; const assistantTypeOptions: AssistantTypeOption[] = [ { type: "提示词", label: "使用提示词构建", description: "通过提示词、模型与语音快速搭建对话助手,适合大多数场景。", icon: , available: true, }, { type: "工作流", label: "使用工作流构建", description: "用可视化编排串联多个节点,适合多步骤、带分支的复杂流程。", icon: , available: false, }, { type: "Dify", label: "使用 Dify 构建", description: "对接 Dify 应用,复用其编排能力与知识库配置。", icon: , available: true, }, { type: "FastGPT", label: "使用 FastGPT 构建", description: "对接 FastGPT 应用,复用其知识库问答与工作流能力。", icon: , available: true, }, { type: "OpenCode", label: "使用 OpenCode 构建", description: "对接 OpenCode 服务,通过提示词驱动代码助手并支持实时语音对话。", icon: , available: true, }, ]; type AssistantListItem = { id: string; name: string; type: AssistantType; updatedAt: string; }; type TypeFilter = "全部" | AssistantType; const typeFilters: TypeFilter[] = ["全部", ...assistantTypes]; export function AssistantPage() { const [form, setForm] = useState({ name: "政务视频咨询助手", greeting: "您好,我是 AI 视频助手,请问有什么可以帮您?", prompt: "你是一名专业的政务视频咨询助手,负责为市民提供政策解读、办事指南和常见问题解答。\n\n请遵循以下原则:\n1. 回答准确、简洁,使用通俗易懂的语言\n2. 不确定的信息应明确告知,不编造政策内容\n3. 涉及具体办事流程时,引导用户前往官方渠道核实", runtimeMode: "pipeline", realtimeModel: "", model: "", asr: "", voice: "", knowledgeBase: "", enableInterrupt: true, }); const [fastGptForm, setFastGptForm] = useState({ name: "FastGPT 售后咨询助手", appId: "", apiUrl: "https://api.fastgpt.in/api/v1/chat/completions", apiKey: "", asr: "", voice: "", enableInterrupt: true, }); const [difyForm, setDifyForm] = useState({ name: "Dify 知识库问答助手", apiUrl: "https://api.dify.ai/v1/chat-messages", apiKey: "", asr: "", voice: "", enableInterrupt: true, }); const [openCodeForm, setOpenCodeForm] = useState({ name: "OpenCode 代码助手", prompt: "你是一个代码助手的语音交互界面,请用简洁、口语化的方式回答用户关于代码与工程的问题。", apiUrl: "http://localhost:4096", apiKey: "", model: "", asr: "", voice: "", enableInterrupt: true, }); const [assistants, setAssistants] = useState([]); const [listLoading, setListLoading] = useState(true); const [listError, setListError] = useState(null); // 编辑中的助手 id;null = 新建 const [editingId, setEditingId] = useState(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); // 下拉数据源:模型凭证 + 知识库 const [credentials, setCredentials] = useState([]); const [knowledgeBases, setKnowledgeBases] = useState([]); const [view, setView] = useState< | "list" | "choose" | "create" | "create-dify" | "create-fastgpt" | "create-opencode" | "placeholder" >("list"); const [searchQuery, setSearchQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("全部"); const [currentPage, setCurrentPage] = useState(1); // choose 步骤的草稿:名称与已选类型,确认后才决定进入哪个构建页 const [draftName, setDraftName] = useState(""); const [draftType, setDraftType] = useState(null); const loadAssistants = useCallback(async () => { setListLoading(true); setListError(null); try { setAssistants(await assistantsApi.list()); } catch (error) { setListError(error instanceof Error ? error.message : "加载失败"); } finally { setListLoading(false); } }, []); useEffect(() => { // 挂载时拉取助手列表(与后端同步) // eslint-disable-next-line react-hooks/set-state-in-effect void loadAssistants(); }, [loadAssistants]); // 进入创建/编辑前加载下拉数据源(模型凭证 + 知识库) const loadResources = useCallback(async () => { try { const [creds, kbs] = await Promise.all([ credentialsApi.list(), knowledgeBasesApi.list(), ]); setCredentials(creds); setKnowledgeBases(kbs); } catch { // 拉取失败时下拉为空,不阻塞表单 } }, []); // 按资源类型生成 {value:id, label:name} 选项 const credOptions = (type: Credential["type"]) => credentials .filter((c) => c.type === type) .map((c) => ({ value: c.id, label: c.name })); const kbOptions = knowledgeBases.map((k) => ({ value: k.id, label: k.name })); function startCreate() { setDraftName(""); setDraftType(null); setView("choose"); } // 提示词类型的空白模板(新建用) function blankPromptForm(name: string): AssistantForm { return { name, greeting: "", prompt: "", runtimeMode: "pipeline", realtimeModel: "", model: "", asr: "", voice: "", knowledgeBase: "", enableInterrupt: true, }; } // 把后端 Assistant 回填进提示词表单(注意:model/asr/voice 等存的是凭证 id) function fillPromptForm(a: Assistant) { setForm({ name: a.name, greeting: a.greeting, prompt: a.prompt, runtimeMode: a.runtimeMode, realtimeModel: a.realtimeCredentialId ?? "", model: a.llmCredentialId ?? "", asr: a.asrCredentialId ?? "", voice: a.ttsCredentialId ?? "", knowledgeBase: a.knowledgeBaseId ?? "", enableInterrupt: a.enableInterrupt, }); } // 编辑:根据助手类型进入对应的构建/编辑页 async function handleEdit(assistant: AssistantListItem) { if (assistant.type === "提示词") { void loadResources(); setSaveError(null); setEditingId(assistant.id); try { fillPromptForm(await assistantsApi.get(assistant.id)); setView("create"); } catch (error) { setListError(error instanceof Error ? error.message : "加载助手失败"); } } else if (assistant.type === "FastGPT") { void loadResources(); setSaveError(null); setEditingId(assistant.id); try { fillFastGptForm(await assistantsApi.get(assistant.id)); setView("create-fastgpt"); } catch (error) { setListError(error instanceof Error ? error.message : "加载助手失败"); } } else if (assistant.type === "Dify") { void loadResources(); setSaveError(null); setEditingId(assistant.id); try { fillDifyForm(await assistantsApi.get(assistant.id)); setView("create-dify"); } catch (error) { setListError(error instanceof Error ? error.message : "加载助手失败"); } } else if (assistant.type === "OpenCode") { void loadResources(); setSaveError(null); setEditingId(assistant.id); try { fillOpenCodeForm(await assistantsApi.get(assistant.id)); setView("create-opencode"); } catch (error) { setListError(error instanceof Error ? error.message : "加载助手失败"); } } else { // 工作流:暂时显示占位页 setDraftName(assistant.name); setDraftType(assistant.type); setView("placeholder"); } } function confirmType() { if (!draftName.trim() || !draftType) { return; } if (draftType === "提示词") { // 提示词类型:新建,空白模板 + 带入名称 void loadResources(); setEditingId(null); setSaveError(null); setForm(blankPromptForm(draftName.trim())); setView("create"); } else if (draftType === "FastGPT") { // FastGPT 类型:新建,清空表单 + 带入名称 void loadResources(); setEditingId(null); setSaveError(null); setFastGptForm({ name: draftName.trim(), appId: "", apiUrl: "", apiKey: "", model: "", asr: "", voice: "", enableInterrupt: true, }); setView("create-fastgpt"); } else if (draftType === "Dify") { // Dify 类型:新建,清空表单 + 带入名称 void loadResources(); setEditingId(null); setSaveError(null); setDifyForm({ name: draftName.trim(), apiUrl: "", apiKey: "", asr: "", voice: "", enableInterrupt: true, }); setView("create-dify"); } else if (draftType === "OpenCode") { // OpenCode 类型:新建,清空表单 + 带入名称 void loadResources(); setEditingId(null); setSaveError(null); setOpenCodeForm({ name: draftName.trim(), prompt: "", apiUrl: "", apiKey: "", asr: "", voice: "", enableInterrupt: true, }); setView("create-opencode"); } else { // 工作流:暂时显示占位页 setView("placeholder"); } } // 复制助手:服务端整行复制(含真 key,密钥不经浏览器) async function handleDuplicate(assistant: AssistantListItem) { try { await assistantsApi.duplicate(assistant.id); await loadAssistants(); } catch (error) { setListError(error instanceof Error ? error.message : "复制失败"); } } // 删除助手 async function handleDelete(id: string) { try { await assistantsApi.remove(id); await loadAssistants(); } catch (error) { setListError(error instanceof Error ? error.message : "删除失败"); } } // 平铺字段的空白 upsert,各类型按需覆盖(后端会按 type 清掉无关字段) function baseUpsert(over: Partial): AssistantUpsert { return { name: "", type: "prompt", runtimeMode: "pipeline", greeting: "", enableInterrupt: true, llmCredentialId: null, asrCredentialId: null, ttsCredentialId: null, realtimeCredentialId: null, knowledgeBaseId: null, prompt: "", apiUrl: "", apiKey: "", appId: "", graph: {}, ...over, }; } // 统一的保存:新建 POST / 编辑 PUT async function save(payload: AssistantUpsert) { setSaving(true); setSaveError(null); try { if (editingId) await assistantsApi.update(editingId, payload); else await assistantsApi.create(payload); await loadAssistants(); setView("list"); } catch (error) { setSaveError(error instanceof Error ? error.message : "保存失败"); } finally { setSaving(false); } } function handleSavePrompt() { void save( baseUpsert({ name: form.name.trim(), type: "prompt", runtimeMode: form.runtimeMode, greeting: form.greeting, enableInterrupt: form.enableInterrupt, llmCredentialId: form.model || null, asrCredentialId: form.asr || null, ttsCredentialId: form.voice || null, realtimeCredentialId: form.realtimeModel || null, knowledgeBaseId: form.knowledgeBase || null, prompt: form.prompt, }), ); } // ---- Dify ---- function fillDifyForm(a: Assistant) { setDifyForm({ name: a.name, apiUrl: a.apiUrl, // 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key apiKey: "", model: a.llmCredentialId ?? "", asr: a.asrCredentialId ?? "", voice: a.ttsCredentialId ?? "", enableInterrupt: a.enableInterrupt, }); } function handleSaveDify() { void save( baseUpsert({ name: difyForm.name.trim(), type: "dify", enableInterrupt: difyForm.enableInterrupt, asrCredentialId: difyForm.asr || null, ttsCredentialId: difyForm.voice || null, apiUrl: difyForm.apiUrl, apiKey: difyForm.apiKey, }), ); } // ---- FastGPT ---- function fillFastGptForm(a: Assistant) { setFastGptForm({ name: a.name, appId: a.appId, apiUrl: a.apiUrl, // 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key apiKey: "", asr: a.asrCredentialId ?? "", voice: a.ttsCredentialId ?? "", enableInterrupt: a.enableInterrupt, }); } function handleSaveFastGpt() { void save( baseUpsert({ name: fastGptForm.name.trim(), type: "fastgpt", enableInterrupt: fastGptForm.enableInterrupt, asrCredentialId: fastGptForm.asr || null, ttsCredentialId: fastGptForm.voice || null, appId: fastGptForm.appId, apiUrl: fastGptForm.apiUrl, apiKey: fastGptForm.apiKey, }), ); } // ---- OpenCode ---- function fillOpenCodeForm(a: Assistant) { setOpenCodeForm({ name: a.name, prompt: a.prompt, apiUrl: a.apiUrl, // 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key apiKey: "", asr: a.asrCredentialId ?? "", voice: a.ttsCredentialId ?? "", enableInterrupt: a.enableInterrupt, }); } function handleSaveOpenCode() { void save( baseUpsert({ name: openCodeForm.name.trim(), type: "opencode", enableInterrupt: openCodeForm.enableInterrupt, llmCredentialId: openCodeForm.model || null, asrCredentialId: openCodeForm.asr || null, ttsCredentialId: openCodeForm.voice || null, prompt: openCodeForm.prompt, apiUrl: openCodeForm.apiUrl, apiKey: openCodeForm.apiKey, }), ); } const listItems: AssistantListItem[] = assistants.map((a) => ({ id: a.id, name: a.name, type: typeToLabel[a.type], updatedAt: formatTimestamp(a.updatedAt), })); const storedApiKeyMask = (editingId && assistants.find((assistant) => assistant.id === editingId)?.apiKey) || ""; const filteredAssistants = listItems.filter((assistant) => { if (typeFilter !== "全部" && assistant.type !== typeFilter) { return false; } const keyword = searchQuery.trim().toLowerCase(); if (!keyword) { return true; } return [assistant.name, assistant.id, assistant.type, assistant.updatedAt] .join(" ") .toLowerCase() .includes(keyword); }); const pageSize = 5; const totalPages = Math.max(1, Math.ceil(filteredAssistants.length / pageSize)); const safeCurrentPage = Math.min(currentPage, totalPages); const pageStart = (safeCurrentPage - 1) * pageSize; const pageEnd = pageStart + pageSize; const paginatedAssistants = filteredAssistants.slice(pageStart, pageEnd); function handleSearchChange(value: string) { setSearchQuery(value); setCurrentPage(1); } function handleFilterChange(filter: TypeFilter) { setTypeFilter(filter); setCurrentPage(1); } function updateForm( key: K, value: AssistantForm[K], ) { setForm((prev) => ({ ...prev, [key]: value, })); } function updateFastGptForm( key: K, value: FastGptForm[K], ) { setFastGptForm((prev) => ({ ...prev, [key]: value, })); } function updateDifyForm( key: K, value: DifyForm[K], ) { setDifyForm((prev) => ({ ...prev, [key]: value, })); } function updateOpenCodeForm( key: K, value: OpenCodeForm[K], ) { setOpenCodeForm((prev) => ({ ...prev, [key]: value, })); } if (view === "list") { return (

助手列表

管理已有的视频助手,支持提示词、工作流、Dify 和 FastGPT 类型。

{typeFilters.map((filter) => ( ))}
handleSearchChange(event.target.value)} className="h-10 border-hairline-strong bg-background pl-9 text-sm text-foreground placeholder:text-muted-soft" placeholder="搜索助手名称、类型或 ID..." />
助手名称
助手类型
更新时间
操作
{paginatedAssistants.map((assistant) => (
{assistant.name}
{assistant.id}
{assistant.type}
{assistant.updatedAt}
handleDuplicate(assistant)} > 复制 { event.preventDefault(); void handleDelete(assistant.id); }} > 删除
))} {listLoading && (
正在加载助手列表…
)} {!listLoading && listError && (
加载失败
{listError}
)} {!listLoading && !listError && filteredAssistants.length === 0 && (
{listItems.length === 0 ? "暂无助手" : "未找到匹配的助手"}
{listItems.length === 0 ? "点击右上角「创建助手」开始。" : "请调整关键词或筛选条件后再试。"}
)}
{filteredAssistants.length === 0 ? "没有数据" : `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredAssistants.length)} / 共 ${filteredAssistants.length} 个助手`}
{Array.from({ length: totalPages }, (_, index) => index + 1).map( (page) => ( ), )}
); } if (view === "choose") { const selectedOption = assistantTypeOptions.find( (option) => option.type === draftType, ); return (

创建助手

先为助手取个名字,再选择构建方式。不同方式将进入不同的构建流程。

构建方式
{assistantTypeOptions.map((option) => { const selected = draftType === option.type; return ( ); })}
); } if (view === "placeholder") { return (

{draftName.trim() || "创建助手"}

{draftType} 构建方式正在开发中,敬请期待。

建设中

{draftType} 构建页面正在编写中

页面骨架与设计语言已就绪,后续将填充具体构建流程与数据。

); } if (view === "create-dify") { return (
setView("list")} /> updateDifyForm("name", value)} />
{saveError && ( {saveError} )}
} title="Dify 应用配置" description="填写 API URL 与 API Key 以对接 Dify 应用。开场白、知识库、提示词等对话编排请在 Dify 平台配置,本页不重复设置。" > updateDifyForm("apiUrl", value)} placeholder="https://api.dify.ai/v1/chat-messages" /> updateDifyForm("apiKey", value)} placeholder="请输入 Dify API Key" storedValueMask={storedApiKeyMask} /> } title="语音配置" description="从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 Dify 应用提供,请前往 Dify 平台配置。" > updateDifyForm("asr", value)} options={credOptions("ASR")} noneLabel="无" /> updateDifyForm("voice", value)} options={credOptions("TTS")} noneLabel="无" /> } title="交互策略" description="设置实时视频对话时的交互体验" > updateDifyForm("enableInterrupt", checked) } />
); } if (view === "create-fastgpt") { return (
setView("list")} /> updateFastGptForm("name", value)} />
{saveError && ( {saveError} )}
} title="FastGPT 应用配置" description="填写 App ID、API URL 与 API Key 以对接 FastGPT 应用。开场白、知识库、提示词等对话编排请在 FastGPT 平台配置,本页不重复设置。" > updateFastGptForm("appId", value)} placeholder="请输入 FastGPT 应用 ID" /> updateFastGptForm("apiUrl", value)} placeholder="https://api.fastgpt.in/api/v1/chat/completions" /> updateFastGptForm("apiKey", value)} placeholder="请输入 FastGPT API Key" storedValueMask={storedApiKeyMask} /> } title="语音配置" description="从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 FastGPT 应用提供,请前往 FastGPT 平台配置。" > updateFastGptForm("asr", value)} options={credOptions("ASR")} noneLabel="无" /> updateFastGptForm("voice", value)} options={credOptions("TTS")} noneLabel="无" /> } title="交互策略" description="设置实时视频对话时的交互体验" > updateFastGptForm("enableInterrupt", checked) } />
); } if (view === "create-opencode") { return (
setView("list")} /> updateOpenCodeForm("name", value)} />
{saveError && ( {saveError} )}
} title="OpenCode 服务配置" description="填写 OpenCode 服务地址与 API Key 以对接代码助手。" > updateOpenCodeForm("apiUrl", value)} placeholder="http://localhost:4096" /> updateOpenCodeForm("apiKey", value)} placeholder="请输入 OpenCode API Key" storedValueMask={storedApiKeyMask} /> } title="提示词" description="描述助手的角色、能力和回答要求" > updateOpenCodeForm("prompt", value)} placeholder="请输入提示词,描述助手的角色、能力和回答要求" rows={8} /> } title="模型与语音配置" description="配置 OpenCode 使用的大语言模型、语音识别与语音合成资源。" > updateOpenCodeForm("model", value)} options={credOptions("LLM")} noneLabel="无" /> updateOpenCodeForm("asr", value)} options={credOptions("ASR")} noneLabel="无" /> updateOpenCodeForm("voice", value)} options={credOptions("TTS")} noneLabel="无" /> } title="交互策略" description="设置实时视频对话时的交互体验" > updateOpenCodeForm("enableInterrupt", checked) } />
); } return (
setView("list")} /> updateForm("name", value)} />
{saveError && ( {saveError} )}
updateForm("runtimeMode", "pipeline")} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); updateForm("runtimeMode", "pipeline"); } }} className={[ "cursor-pointer rounded-2xl border p-5 text-left transition-colors", form.runtimeMode === "pipeline" ? "border-primary bg-primary/5 ring-1 ring-primary" : "border-hairline bg-canvas-soft hover:border-hairline-strong", ].join(" ")} >
Pipeline 模式
{form.runtimeMode === "pipeline" && ( )}
updateForm("runtimeMode", "realtime")} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); updateForm("runtimeMode", "realtime"); } }} className={[ "cursor-pointer rounded-2xl border p-5 text-left transition-colors", form.runtimeMode === "realtime" ? "border-primary bg-primary/5 ring-1 ring-primary" : "border-hairline bg-canvas-soft hover:border-hairline-strong", ].join(" ")} >
Realtime 模式
{form.runtimeMode === "realtime" && ( )}
} title="提示词" description="描述助手的角色、能力和回答要求" > updateForm("prompt", value)} placeholder="请输入提示词,描述助手的角色、能力和回答要求" rows={8} /> {form.runtimeMode === "pipeline" ? ( } title="模型配置" description="从「模型资源」中选择大语言模型、语音识别与语音合成" > updateForm("model", value)} options={credOptions("LLM")} noneLabel="无" /> updateForm("asr", value)} options={credOptions("ASR")} noneLabel="无" /> updateForm("voice", value)} options={credOptions("TTS")} noneLabel="无" /> ) : ( } title="模型配置" description="当前模式下 ASR 与 TTS 由 Realtime 模型内置完成" > updateForm("realtimeModel", value)} options={credOptions("Realtime")} noneLabel="无" /> )} } title="开场白" description="助手与用户首次对话时的开场语" > updateForm("greeting", value)} placeholder="请输入助手开场白" /> } title="知识库配置" description="选择助手回答时可检索的业务知识来源" > updateForm("knowledgeBase", value)} options={kbOptions} noneLabel="无" /> } title="交互策略" description="设置实时视频对话时的交互体验" > updateForm("enableInterrupt", checked)} />
); } type VizStyle = "aura" | "bars" | "wave"; const VIZ_ORDER: VizStyle[] = ["aura", "bars", "wave"]; const VIZ_LABEL: Record = { aura: "光环", bars: "频谱", wave: "波形", }; function DebugDrawer() { const [showTranscript, setShowTranscript] = useState(false); const [vizStyle, setVizStyle] = useState("wave"); return ( ); } function DebugVoicePanel({ showTranscript, vizStyle, }: { showTranscript: boolean; vizStyle: VizStyle; }) { const [recording, setRecording] = useState(false); const [micError, setMicError] = useState(false); return (
{showTranscript ? ( ) : (
{recording ? "会话进行中" : "准备开始"}
{(() => { const onVizError = () => { setMicError(true); setRecording(false); }; const shared = { active: recording, className: "relative shrink-0", onError: onVizError, } as const; if (vizStyle === "aura") return ; if (vizStyle === "bars") return ( ); return ; })()}
{recording ? "我在聆听" : "开始一次语音对话"}

{micError ? "无法访问麦克风,请检查浏览器权限后重试。" : recording ? "直接说话即可。助手会在您停顿后自然回应。" : "测试语音识别、响应速度与助手的播报效果。"}

)}