"use client"; import { Bot, Boxes, Brain, Check, Copy, Database, Eye, EyeOff, MessageSquareText, MoreHorizontal, Pencil, Plus, Rocket, Sparkles, Trash2, Workflow, ChevronLeft, ChevronUp, ChevronDown, 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 { NebulaVisualizer } from "@/components/ui/nebula-visualizer"; import { SpectrumVisualizer } from "@/components/ui/spectrum-visualizer"; import { WaveVisualizer } from "@/components/ui/wave-visualizer"; import { WaveformTimelinePanel } from "@/components/ui/waveform-timeline"; import { DataList } from "@/components/ui/data-list"; import { PageHeader } from "@/components/ui/page-header"; import { FilterPills } from "@/components/ui/filter-pills"; import { SearchInput } from "@/components/ui/search-input"; import { ListToolbar } from "@/components/ui/list-toolbar"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { assistantsApi, knowledgeBasesApi, modelResourcesApi, type Assistant, type AssistantType as ApiAssistantType, type AssistantUpsert, type KnowledgeBase, type ModelResource, } from "@/lib/api"; import { useVoicePreview, type ChatMessage, type VoicePreviewStatus, } from "@/hooks/use-voice-preview"; 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", }; const typeFromLabel: Record = { 提示词: "prompt", 工作流: "workflow", Dify: "dify", FastGPT: "fastgpt", OpenCode: "opencode", }; // 后端 type → 编辑器视图(工作流暂为占位页) const typeToView = { prompt: "create", dify: "create-dify", fastgpt: "create-fastgpt", opencode: "create-opencode", workflow: "placeholder", } as const; type View = "list" | "choose" | "loading" | (typeof typeToView)[ApiAssistantType]; // 路由驱动的页面模式: // /assistants → list | /assistants/new → choose(引导,确认即建库) | /assistants/[id] → edit export type AssistantPageProps = | { mode: "list" } | { mode: "choose" } | { mode: "edit"; assistantId: string }; // 各类型的空白表单模板(新建用) function blankPromptForm(name: string): AssistantForm { return { name, greeting: "", prompt: "", runtimeMode: "pipeline", realtimeModel: "", model: "", asr: "", voice: "", knowledgeBase: "", enableInterrupt: true, }; } function blankFastGptForm(name: string): FastGptForm { return { name, appId: "", apiUrl: "", apiKey: "", asr: "", voice: "", enableInterrupt: true, }; } function blankDifyForm(name: string): DifyForm { return { name, apiUrl: "", apiKey: "", asr: "", voice: "", enableInterrupt: true, }; } function blankOpenCodeForm(name: string): OpenCodeForm { return { name, prompt: "", apiUrl: "", apiKey: "", model: "", asr: "", voice: "", enableInterrupt: true, }; } 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; /** 原始 ISO 时间戳,用于按时间排序(updatedAt 为展示用整形字符串) */ updatedAtRaw: string | null | undefined; }; type TypeFilter = "全部" | AssistantType; const typeFilters: TypeFilter[] = ["全部", ...assistantTypes]; // 列表按更新时间排序:newest=最近更新在前(倒叙,默认) / oldest=最早更新在前 type SortOrder = "newest" | "oldest"; export function AssistantPage(props: AssistantPageProps) { const router = useRouter(); // 编辑中的助手 id(来自路由) const editingId = props.mode === "edit" ? props.assistantId : null; const [form, setForm] = useState(() => blankPromptForm("")); const [fastGptForm, setFastGptForm] = useState(() => blankFastGptForm(""), ); const [difyForm, setDifyForm] = useState(() => blankDifyForm("")); const [openCodeForm, setOpenCodeForm] = useState(() => blankOpenCodeForm(""), ); const [assistants, setAssistants] = useState([]); const [listLoading, setListLoading] = useState(true); const [listError, setListError] = useState(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); // 编辑模式:加载指定 id 助手失败时的错误 const [loadError, setLoadError] = useState(null); // 编辑模式:后端返回的打码 API Key(用于编辑页展示"当前密钥") const [storedApiKeyMask, setStoredApiKeyMask] = useState(""); // 下拉数据源:模型资源 + 知识库 const [modelResources, setModelResources] = useState([]); const [knowledgeBases, setKnowledgeBases] = useState([]); // 视图由路由模式决定;仅编辑模式需要先 loading,等拿到助手类型后切换 const [view, setView] = useState(() => { if (props.mode === "choose") return "choose"; if (props.mode === "edit") return "loading"; return "list"; }); const [searchQuery, setSearchQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("全部"); const [sortOrder, setSortOrder] = useState("newest"); const [currentPage, setCurrentPage] = useState(1); // choose 步骤的草稿:名称与已选类型,确认后直接建库并进入编辑页 // (工作流占位页也用它展示名称与类型) const [draftName, setDraftName] = useState(""); const [draftType, setDraftType] = useState(null); // 引导页:创建请求进行中 / 创建失败 const [creating, setCreating] = useState(false); const [createError, setCreateError] = useState(null); // 已保存基线(当前类型表单的 JSON);与表单不一致时保存按钮才可点击 const [savedSnapshot, setSavedSnapshot] = 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(() => { // 列表页挂载时拉取助手列表(与后端同步) if (props.mode !== "list") return; // eslint-disable-next-line react-hooks/set-state-in-effect void loadAssistants(); }, [props.mode, loadAssistants]); // 进入创建/编辑前加载下拉数据源(模型资源 + 知识库) const loadResources = useCallback(async () => { try { const [creds, kbs] = await Promise.all([ modelResourcesApi.list(), knowledgeBasesApi.list(), ]); setModelResources(creds); setKnowledgeBases(kbs); } catch { // 拉取失败时下拉为空,不阻塞表单 } }, []); // 编辑页挂载时加载下拉数据源 useEffect(() => { if (props.mode !== "edit") return; // eslint-disable-next-line react-hooks/set-state-in-effect void loadResources(); }, [props.mode, loadResources]); // 按资源类型生成 {value:id, label:name} 选项 const credOptions = (type: ModelResource["capability"]) => modelResources .filter((c) => c.capability === type) .map((c) => ({ value: c.id, label: c.name })); const kbOptions = knowledgeBases.map((k) => ({ value: k.id, label: k.name })); function startCreate() { router.push("/assistants/new"); } // 把后端 Assistant 回填进提示词表单(model/asr/voice 等存模型资源 id) // 返回回填后的表单,供调用方记录"已保存基线" function fillPromptForm(a: Assistant): AssistantForm { const next: AssistantForm = { name: a.name, greeting: a.greeting, prompt: a.prompt, runtimeMode: a.runtimeMode, realtimeModel: a.modelResourceIds.Realtime ?? "", model: a.modelResourceIds.LLM ?? "", asr: a.modelResourceIds.ASR ?? "", voice: a.modelResourceIds.TTS ?? "", knowledgeBase: a.knowledgeBaseId ?? "", enableInterrupt: a.enableInterrupt, }; setForm(next); return next; } // 编辑:跳转到 /assistants/[id],由编辑页按助手类型回填表单 function handleEdit(assistant: AssistantListItem) { router.push(`/assistants/${assistant.id}`); } // 引导页确认:直接创建到数据库拿到 id,再进入该助手的编辑页 async function confirmType() { if (!draftName.trim() || !draftType || creating) { return; } setCreating(true); setCreateError(null); try { const created = await assistantsApi.create( baseUpsert({ name: draftName.trim(), type: typeFromLabel[draftType], }), ); router.push(`/assistants/${created.id}`); } catch (error) { setCreateError(error instanceof Error ? error.message : "创建失败"); setCreating(false); } } // 复制助手:服务端整行复制(含真 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, modelResourceIds: {}, knowledgeBaseId: null, prompt: "", apiUrl: "", apiKey: "", appId: "", graph: {}, ...over, }; } // 保存:停留在编辑页,刷新密钥掩码并把当前表单记为已保存基线(按钮随之置灰) async function save(payload: AssistantUpsert) { if (!editingId) return; setSaving(true); setSaveError(null); try { const updated = await assistantsApi.update(editingId, payload); setStoredApiKeyMask(updated.apiKey ?? ""); // 密钥输入框清空(空值=保留已存密钥),并以清空后的表单为新基线 if (view === "create") { setSavedSnapshot(JSON.stringify(form)); } else if (view === "create-dify") { const next = { ...difyForm, apiKey: "" }; setDifyForm(next); setSavedSnapshot(JSON.stringify(next)); } else if (view === "create-fastgpt") { const next = { ...fastGptForm, apiKey: "" }; setFastGptForm(next); setSavedSnapshot(JSON.stringify(next)); } else if (view === "create-opencode") { const next = { ...openCodeForm, apiKey: "" }; setOpenCodeForm(next); setSavedSnapshot(JSON.stringify(next)); } } 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, modelResourceIds: { ...(form.model ? { LLM: form.model } : {}), ...(form.asr ? { ASR: form.asr } : {}), ...(form.voice ? { TTS: form.voice } : {}), ...(form.realtimeModel ? { Realtime: form.realtimeModel } : {}), }, knowledgeBaseId: form.knowledgeBase || null, prompt: form.prompt, }), ); } // ---- Dify ---- function fillDifyForm(a: Assistant): DifyForm { const next: DifyForm = { name: a.name, apiUrl: a.apiUrl, // 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key apiKey: "", asr: a.modelResourceIds.ASR ?? "", voice: a.modelResourceIds.TTS ?? "", enableInterrupt: a.enableInterrupt, }; setDifyForm(next); return next; } function handleSaveDify() { void save( baseUpsert({ name: difyForm.name.trim(), type: "dify", enableInterrupt: difyForm.enableInterrupt, modelResourceIds: { ...(difyForm.asr ? { ASR: difyForm.asr } : {}), ...(difyForm.voice ? { TTS: difyForm.voice } : {}), }, apiUrl: difyForm.apiUrl, apiKey: difyForm.apiKey, }), ); } // ---- FastGPT ---- function fillFastGptForm(a: Assistant): FastGptForm { const next: FastGptForm = { name: a.name, appId: a.appId, apiUrl: a.apiUrl, // 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key apiKey: "", asr: a.modelResourceIds.ASR ?? "", voice: a.modelResourceIds.TTS ?? "", enableInterrupt: a.enableInterrupt, }; setFastGptForm(next); return next; } function handleSaveFastGpt() { void save( baseUpsert({ name: fastGptForm.name.trim(), type: "fastgpt", enableInterrupt: fastGptForm.enableInterrupt, modelResourceIds: { ...(fastGptForm.asr ? { ASR: fastGptForm.asr } : {}), ...(fastGptForm.voice ? { TTS: fastGptForm.voice } : {}), }, appId: fastGptForm.appId, apiUrl: fastGptForm.apiUrl, apiKey: fastGptForm.apiKey, }), ); } // ---- OpenCode ---- function fillOpenCodeForm(a: Assistant): OpenCodeForm { const next: OpenCodeForm = { name: a.name, prompt: a.prompt, apiUrl: a.apiUrl, // 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key apiKey: "", model: a.modelResourceIds.LLM ?? "", asr: a.modelResourceIds.ASR ?? "", voice: a.modelResourceIds.TTS ?? "", enableInterrupt: a.enableInterrupt, }; setOpenCodeForm(next); return next; } // 编辑模式:按路由中的 id 拉取助手,回填对应类型的表单后再切换视图 useEffect(() => { if (props.mode !== "edit" || !editingId) return; let cancelled = false; void (async () => { try { const assistant = await assistantsApi.get(editingId); if (cancelled) return; setStoredApiKeyMask(assistant.apiKey ?? ""); if (assistant.type === "prompt") { setSavedSnapshot(JSON.stringify(fillPromptForm(assistant))); } else if (assistant.type === "fastgpt") { setSavedSnapshot(JSON.stringify(fillFastGptForm(assistant))); } else if (assistant.type === "dify") { setSavedSnapshot(JSON.stringify(fillDifyForm(assistant))); } else if (assistant.type === "opencode") { setSavedSnapshot(JSON.stringify(fillOpenCodeForm(assistant))); } else { // 工作流:暂时显示占位页 setDraftName(assistant.name); setDraftType(typeToLabel[assistant.type]); } setView(typeToView[assistant.type]); } catch (error) { if (!cancelled) { setLoadError( error instanceof Error ? error.message : "加载助手失败", ); } } })(); return () => { cancelled = true; }; }, [props.mode, editingId]); function handleSaveOpenCode() { void save( baseUpsert({ name: openCodeForm.name.trim(), type: "opencode", enableInterrupt: openCodeForm.enableInterrupt, modelResourceIds: { ...(openCodeForm.model ? { LLM: openCodeForm.model } : {}), ...(openCodeForm.asr ? { ASR: openCodeForm.asr } : {}), ...(openCodeForm.voice ? { TTS: openCodeForm.voice } : {}), }, prompt: openCodeForm.prompt, apiUrl: openCodeForm.apiUrl, apiKey: openCodeForm.apiKey, }), ); } // 当前编辑器表单相对已保存基线是否有改动(决定保存按钮是否可点) const activeFormJson = view === "create" ? JSON.stringify(form) : view === "create-dify" ? JSON.stringify(difyForm) : view === "create-fastgpt" ? JSON.stringify(fastGptForm) : view === "create-opencode" ? JSON.stringify(openCodeForm) : null; const dirty = activeFormJson !== null && savedSnapshot !== null && activeFormJson !== savedSnapshot; const listItems: AssistantListItem[] = assistants.map((a) => ({ id: a.id, name: a.name, type: typeToLabel[a.type], updatedAt: formatTimestamp(a.updatedAt), updatedAtRaw: a.updatedAt, })); 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); }); // 按更新时间排序(以原始 ISO 时间戳为准);缺失时间戳视为最早,id 作为稳定的次级排序键 const timeValue = (iso: string | null | undefined) => { const t = iso ? new Date(iso).getTime() : NaN; return Number.isNaN(t) ? 0 : t; }; const sortedAssistants = [...filteredAssistants].sort((a, b) => { const diff = timeValue(b.updatedAtRaw) - timeValue(a.updatedAtRaw); if (diff !== 0) return sortOrder === "newest" ? diff : -diff; return a.id.localeCompare(b.id); }); const pageSize = 5; const totalPages = Math.max(1, Math.ceil(sortedAssistants.length / pageSize)); const safeCurrentPage = Math.min(currentPage, totalPages); const pageStart = (safeCurrentPage - 1) * pageSize; const pageEnd = pageStart + pageSize; const paginatedAssistants = sortedAssistants.slice(pageStart, pageEnd); function handleSearchChange(value: string) { setSearchQuery(value); setCurrentPage(1); } function handleFilterChange(filter: TypeFilter) { setTypeFilter(filter); setCurrentPage(1); } function handleSortChange(order: SortOrder) { setSortOrder(order); setCurrentPage(1); } function toggleSortOrder() { handleSortChange(sortOrder === "newest" ? "oldest" : "newest"); } 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 === "loading") { return (
{loadError ? (
加载助手失败
{loadError}
) : (
正在加载助手…
)}
); } if (view === "list") { return (
创建助手 } />
} search={ } /> rows={paginatedAssistants} rowKey={(assistant) => assistant.id} loading={listLoading} loadingText="正在加载助手列表…" error={listError} onRetry={() => void loadAssistants()} empty={{ title: listItems.length === 0 ? "暂无助手" : "未找到匹配的助手", description: listItems.length === 0 ? "点击右上角「创建助手」开始。" : "请调整关键词或筛选条件后再试。", }} pagination={{ page: safeCurrentPage, totalPages, onPageChange: setCurrentPage, summary: filteredAssistants.length === 0 ? "没有数据" : `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredAssistants.length)} / 共 ${filteredAssistants.length} 个助手`, }} columns={[ { key: "name", header: "助手名称", width: "md:w-[360px]", cell: (assistant) => ( <>
{assistant.name}
{assistant.id}
), }, { key: "type", header: "助手类型", width: "md:w-[128px]", cell: (assistant) => ( {assistant.type} ), }, { key: "updatedAt", width: "md:w-[176px]", header: ( ), cellClassName: "whitespace-nowrap tabular-nums text-muted-foreground", cell: (assistant) => assistant.updatedAt, }, { key: "actions", header: "操作", align: "right", cellClassName: "flex justify-end gap-2", cell: (assistant) => ( <> handleDuplicate(assistant)} > 复制 { event.preventDefault(); void handleDelete(assistant.id); }} > 删除 ), }, ]} />
); } if (view === "choose") { return (

创建助手

先为助手取个名字,再选择构建方式。确认后将立即创建助手并进入编辑页。

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

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

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

建设中

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

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

); } if (view === "create-dify") { return (
router.push("/assistants")} /> 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 (
router.push("/assistants")} /> 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 (
router.push("/assistants")} /> 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 (
router.push("/assistants")} /> 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" | "nebula" | "bars" | "wave"; const VIZ_OPTIONS: { style: VizStyle; label: string; icon: React.ReactNode }[] = [ { style: "aura", label: "光环", icon: }, { style: "nebula", label: "星云", icon: }, { style: "bars", label: "频谱", icon: }, { style: "wave", label: "波形", icon: }, ]; // 中央语音可视化(光环/星云/频谱/波形)暂时隐藏:调试面板固定为 // 「上聊天记录 + 下波形监控」布局。置 true 可恢复可视化视图与样式切换。 const SHOW_VOICE_VIZ = false; function SegmentedIconGroup({ children, label, }: { children: React.ReactNode; label: string; }) { return (
{children}
); } function SegmentedIconButton({ selected, label, onClick, children, }: { selected: boolean; label: string; onClick: () => void; children: React.ReactNode; }) { return ( ); } function DebugDrawer({ assistantId }: { assistantId: string | null }) { const [showTranscript, setShowTranscript] = useState(false); const [vizStyle, setVizStyle] = useState("aura"); return ( ); } function DebugVoicePanel({ showTranscript, vizStyle, assistantId, }: { showTranscript: boolean; vizStyle: VizStyle; assistantId: string | null; }) { const { status, error, micWarning, localStream, remoteStream, messages, audioInputs, selectedDeviceId, selectDevice, sendText, connect, disconnect, audioRef, } = useVoicePreview(assistantId); // 连接中或已连通都视作"会话进行中" const recording = status === "connecting" || status === "connected"; const [textDraft, setTextDraft] = useState(""); function handleSendText() { if (sendText(textDraft)) { setTextDraft(""); } } return (
{/* 后端 TTS 音频经 WebRTC 媒体流过来,挂这里播放 */}