"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { AnalysisConfigEditor, } from "@/components/assistant-editor/analysis-config"; import { AlertTriangle, Braces, Bot, Brain, Bug, ChartLine, Database, Loader2, MessageSquareText, MoreHorizontal, Save, Sparkles, Trash2, Wrench, } from "lucide-react"; import { DebugDrawer } from "@/components/assistant-editor/debug-preview"; import { DynamicVariableEditorHint, DynamicVariablesSection, } from "@/components/assistant-editor/dynamic-variables"; import { PromptTemplateEditor } from "@/components/assistant-editor/prompt-template-editor"; import { AssistantIdentity, EditableTitle, EditorBackButton, KnowledgeRetrievalConfigDialog, ResourceSelectField, RuntimeModeSelector, TextAreaField, } from "@/components/assistant-editor/editor-controls"; import type { AssistantForm } from "@/components/assistant-editor/types"; import { SectionAnchorTabs } from "@/components/editor/section-anchor-tabs"; import { SectionCard } from "@/components/editor/section-card"; import { VisionConfigSection } from "@/components/editor/vision-config-section"; import { BuiltInToolsEditor } from "@/components/assistant-editor/built-in-tools"; import { ToolBindingEditor } from "@/components/tools/tool-binding-editor"; import { TurnConfigEditor } from "@/components/turn-config-editor"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { TopbarPortal } from "@/components/layout/topbar-portal"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import type { BuiltInTools, DynamicVariableDefinition, RealtimePromptToolFeatures, StartupConfig, Tool, } from "@/lib/api"; type ResourceOption = { value: string; label: string }; type PromptOpeningMode = StartupConfig["openingMode"]; type PromptEntryMode = StartupConfig["entryMode"]; const BUILT_IN_TOOL_LABELS: Record = { endConversation: "结束对话", updateState: "更新状态", skipTurn: "跳过本轮", requestHumanHandoff: "转接人工", }; const EXTERNAL_TOOL_TYPE_LABELS: Record = { http: "HTTP", mcp: "MCP", client: "Client", task: "后台任务", }; const OPENING_MODE_OPTIONS: Array<{ value: PromptOpeningMode; label: string; description: string; }> = [ { value: "interruptible", label: "可打断", description: "用户说话、发文字或图片时立即结束开场白,并保留这次输入。", }, { value: "playback", label: "播完继续", description: "开场白播放期间关闭输入,实际播放完成后进入 Agent。", }, { value: "confirmation", label: "需要弹窗确认继续", description: "关闭对话输入并显示弹窗,用户确认后立即进入 Agent。", }, ]; const ENTRY_MODE_OPTIONS: Array<{ value: PromptEntryMode; label: string; }> = [ { value: "wait_user", label: "等待下一轮用户输入" }, { value: "generate", label: "进入后立即回复" }, ]; const promptSections = [ { id: "conversation", label: "对话内容" }, { id: "models", label: "模型与语音" }, { id: "capabilities", label: "知识与工具" }, { id: "interaction", label: "交互策略" }, { id: "variables", label: "动态变量" }, { id: "analysis", label: "分析" }, ] as const; type PromptSectionId = (typeof promptSections)[number]["id"]; type PromptEditorProps = { assistantId: string | null; form: AssistantForm; dynamicVariableDefinitions: Record; saving: boolean; dirty: boolean; saveError: string | null; llmOptions: ResourceOption[]; asrOptions: ResourceOption[]; ttsOptions: ResourceOption[]; realtimeOptions: ResourceOption[]; visionModelOptions: ResourceOption[]; knowledgeOptions: ResourceOption[]; tools: Tool[]; realtimeToolFeatures?: RealtimePromptToolFeatures; onBack: () => void; onSave: () => void; onDelete?: () => void | Promise; updateForm: ( key: K, value: AssistantForm[K], ) => void; handlePromptVisionEnabledChange: (enabled: boolean) => void; handlePromptModelChange: (value: string) => void; }; export function PromptEditor({ assistantId, form, dynamicVariableDefinitions, saving, dirty, saveError, llmOptions, asrOptions, ttsOptions, realtimeOptions, visionModelOptions, knowledgeOptions: kbOptions, tools, realtimeToolFeatures, onBack, onSave, onDelete, updateForm, handlePromptVisionEnabledChange, handlePromptModelChange, }: PromptEditorProps) { const openingMessage = form.startup.openingMessage; const openingMode = form.startup.openingMode; const openingModeDescription = OPENING_MODE_OPTIONS.find( (option) => option.value === openingMode, )?.description; const scrollContainerRef = useRef(null); const sectionRefs = useRef>({ conversation: null, models: null, capabilities: null, interaction: null, variables: null, analysis: null, }); const selectedAnchorRef = useRef(null); const [activeSection, setActiveSection] = useState("conversation"); const [debugOpen, setDebugOpen] = useState(true); const [deleting, setDeleting] = useState(false); const [cleanupDialogOpen, setCleanupDialogOpen] = useState(false); const promptVariableSuggestions = useMemo( () => ({ ...form.dynamicVariableDefinitions, ...dynamicVariableDefinitions, }), [form.dynamicVariableDefinitions, dynamicVariableDefinitions], ); const selectedRealtimeModel = realtimeOptions.find( (option) => option.value === form.realtimeModel, ); const allowedRealtimeBuiltIns = new Set( realtimeToolFeatures?.builtIns ?? [], ); const unsupportedBuiltIns = ( Object.keys(form.builtInTools) as Array ).filter( (key) => Boolean(form.builtInTools[key]) && !allowedRealtimeBuiltIns.has(key), ); const allowedRealtimeExternalTypes = new Set( realtimeToolFeatures?.externalTypes ?? [], ); const unsupportedToolBindings = form.toolBindings.filter((binding) => { const tool = tools.find((item) => item.id === binding.toolId); return !tool || !allowedRealtimeExternalTypes.has(tool.type); }); const unsupportedToolIds = new Set( unsupportedToolBindings.map((binding) => binding.toolId), ); const incompatibleToolCount = unsupportedBuiltIns.length + unsupportedToolBindings.length; useEffect(() => { const container = scrollContainerRef.current; if (!container) return; const scrollContainer: HTMLDivElement = container; let animationFrame = 0; function updateActiveSection() { if (selectedAnchorRef.current) { setActiveSection(selectedAnchorRef.current); return; } const containerTop = scrollContainer.getBoundingClientRect().top; const activationLine = containerTop + 24; let nextSection: PromptSectionId = promptSections[0].id; for (const section of promptSections) { const element = sectionRefs.current[section.id]; if (element && element.getBoundingClientRect().top <= activationLine) { nextSection = section.id; } } const reachedBottom = scrollContainer.scrollHeight > scrollContainer.clientHeight + 8 && scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight < 8; if (reachedBottom) { nextSection = promptSections[promptSections.length - 1].id; } setActiveSection((current) => current === nextSection ? current : nextSection, ); } function scheduleUpdate() { window.cancelAnimationFrame(animationFrame); animationFrame = window.requestAnimationFrame(updateActiveSection); } function releaseSelectedAnchor() { selectedAnchorRef.current = null; scheduleUpdate(); } scheduleUpdate(); scrollContainer.addEventListener("scroll", scheduleUpdate, { passive: true, }); scrollContainer.addEventListener("wheel", releaseSelectedAnchor, { passive: true, }); scrollContainer.addEventListener("touchstart", releaseSelectedAnchor, { passive: true, }); window.addEventListener("resize", scheduleUpdate); return () => { window.cancelAnimationFrame(animationFrame); scrollContainer.removeEventListener("scroll", scheduleUpdate); scrollContainer.removeEventListener("wheel", releaseSelectedAnchor); scrollContainer.removeEventListener("touchstart", releaseSelectedAnchor); window.removeEventListener("resize", scheduleUpdate); }; }, [form.runtimeMode]); function scrollToSection(sectionId: PromptSectionId) { const container = scrollContainerRef.current; const section = sectionRefs.current[sectionId]; if (!container || !section) return; const containerTop = container.getBoundingClientRect().top; const sectionTop = section.getBoundingClientRect().top; selectedAnchorRef.current = sectionId; setActiveSection(sectionId); container.scrollTo({ top: container.scrollTop + sectionTop - containerTop, behavior: "smooth", }); } function setOpeningMode(nextMode: PromptOpeningMode) { updateForm("startup", { ...form.startup, openingMode: nextMode, openingMessage: nextMode === "confirmation" ? { title: openingMessage?.title ?? "重要提示", message: openingMessage?.message ?? "请确认已阅读以上信息。", confirmLabel: openingMessage?.confirmLabel ?? "确认", } : null, }); } function updateOpeningMessage( patch: Partial>, ) { if (!openingMessage) return; updateForm("startup", { ...form.startup, openingMessage: { ...openingMessage, ...patch }, }); } async function handleDeleteSelect() { if (!onDelete || deleting) return; setDeleting(true); try { await onDelete(); } finally { setDeleting(false); } } return ( <>
updateForm("name", value)} /> {onDelete && ( { event.preventDefault(); window.setTimeout(() => void handleDeleteSelect(), 0); }} > 删除 )}
{saveError && ( {saveError} )}
scrollToSection(sectionId as PromptSectionId) } className="bg-background px-4 pt-3 sm:px-6 sm:pt-4 lg:px-8" contentClassName={ debugOpen ? "w-full" : "mx-auto w-full max-w-7xl" } />
{ sectionRefs.current.conversation = element; }} className="scroll-mt-3 space-y-3" > } title="提示词" description="描述助手的角色、能力和回答要求" > updateForm("prompt", value)} variables={promptVariableSuggestions} placeholder="请输入提示词,描述助手的角色、能力和回答要求" ariaLabel="提示词" rows={16} /> scrollToSection("variables")} /> } title="开场白" description="助手与用户首次对话时的开场语" > updateForm("greeting", value)} variables={promptVariableSuggestions} placeholder="请输入助手开场白" ariaLabel="开场白" /> scrollToSection("variables")} /> {form.runtimeMode === "pipeline" && (
开场白继续方式

{openingModeDescription}

{openingMode === "confirmation" && openingMessage && (
updateOpeningMessage({ message }) } placeholder="请输入需要用户确认的重要信息" rows={4} />

确认前关闭语音、文字和图片输入;确认后立即停止剩余开场白并进入 Agent。

)}
Agent 进入行为

该设置仅控制开场阶段正常完成后的 Agent 首轮行为。

)}
{ sectionRefs.current.models = element; }} className="scroll-mt-3 space-y-3" > } title="模型与语音" description={ form.runtimeMode === "pipeline" ? "选择运行方式,以及大语言模型、语音识别与语音合成资源" : "选择运行方式;Realtime 模型内置语音识别与语音合成" } > { updateForm("runtimeMode", runtimeMode); if ( runtimeMode === "realtime" && (form.startup.actions.length || form.startup.openingMode !== "interruptible" || form.startup.entryMode !== "wait_user" || form.startup.openingMessage) ) { updateForm("startup", { executionMode: "sequential", openingMode: "interruptible", entryMode: "wait_user", actions: [], openingMessage: null, }); } }} /> {form.runtimeMode === "pipeline" ? ( <> updateForm("asr", value)} options={asrOptions} noneLabel="无" /> updateForm("voice", value)} options={ttsOptions} noneLabel="无" /> ) : ( updateForm("realtimeModel", value)} options={realtimeOptions} noneLabel="无" /> )}
{ sectionRefs.current.capabilities = element; }} className="scroll-mt-3 space-y-3" > {form.runtimeMode === "pipeline" && ( updateForm("visionModelResourceId", value) } onPromptModeChange={(value) => updateForm("visionPromptMode", value) } onPromptChange={(value) => updateForm("visionPrompt", value) } /> )} {form.runtimeMode === "pipeline" && ( } title="知识库配置" description="选择助手回答时可检索的业务知识来源" >
知识库选择 updateForm("knowledgeRetrievalConfig", config) } />
updateForm("knowledgeBase", value)} options={kbOptions} noneLabel="无" />
)} {(form.runtimeMode === "pipeline" || Boolean(realtimeToolFeatures)) && ( } title="内建能力" description="配置平台提供的结束、状态更新与人工接管能力" > updateForm("builtInTools", builtInTools) } /> )} {form.runtimeMode === "realtime" && !realtimeToolFeatures && ( } title="Realtime 工具" description="当前 Realtime 接口尚未开放 Prompt 工具调用" > {form.realtimeModel ? (

{selectedRealtimeModel?.label ?? "当前 Realtime 模型"} 不支持工具调用

已有配置会被保留。保存前请选择支持 Prompt 工具的 Realtime 模型,或查看并移除不兼容项。

) : (

请先选择 Realtime 模型

选择模型后,这里会显示其支持的内建能力与外部工具类型。

)}
)} {form.runtimeMode === "realtime" && Boolean(realtimeToolFeatures) && (
当前模型支持的工具

外部工具: {realtimeToolFeatures?.externalTypes .map((type) => EXTERNAL_TOOL_TYPE_LABELS[type]) .join("、") || "无"} ;内建能力: {realtimeToolFeatures?.builtIns .map((key) => BUILT_IN_TOOL_LABELS[key]) .join("、") || "无"} 。

)} {form.runtimeMode === "realtime" && incompatibleToolCount > 0 && ( } title={`存在 ${incompatibleToolCount} 项不兼容工具配置`} description="配置不会被自动删除;保存前需要更换模型或手动移除" >
    {unsupportedBuiltIns.map((key) => (
  • {BUILT_IN_TOOL_LABELS[key]}
    内建能力 · 当前 Realtime 模型不支持
  • ))} {unsupportedToolBindings.map((binding) => { const tool = tools.find((item) => item.id === binding.toolId); return (
  • {tool?.name ?? `已失效工具 ${binding.toolId}`}
    {tool ? `${EXTERNAL_TOOL_TYPE_LABELS[tool.type]} 工具 · 当前 Realtime 模型不支持此类型` : "工具资源已不存在"}
  • ); })}
)} } title="外部工具" description="从资源库添加工具,并配置当前助手的参数来源与结果写入" > realtimeToolFeatures?.externalTypes.some(type => type === tool.type), ) : tools } value={form.toolBindings} variables={dynamicVariableDefinitions} onChange={(toolBindings) => updateForm("toolBindings", toolBindings) } />
{ sectionRefs.current.interaction = element; }} className="scroll-mt-3 space-y-3" > } title="交互策略" description="设置实时视频对话时的交互体验" > {form.runtimeMode === "pipeline" ? ( updateForm("enableInterrupt", checked) } onConfigChange={(config) => updateForm("turnConfig", config) } /> ) : (

高级抢话与轮次检测当前仅支持 Pipeline 模式。

)}
{ sectionRefs.current.variables = element; }} className="scroll-mt-3 space-y-3" > } title="动态变量" description="编辑提示词和开场白中引用的自定义变量" > updateForm( "dynamicVariableDefinitions", dynamicVariableDefinitions, ) } />
{ sectionRefs.current.analysis = element; }} className="scroll-mt-3 space-y-3" > } title="分析" description="通话结束后自动提取关键信息" > updateForm("analysisConfig", analysisConfig) } modelOptions={llmOptions} />
{debugOpen && ( setDebugOpen(false)} hasUnsavedChanges={dirty} vision={form.visionEnabled} dynamicVariablesEnabled dynamicVariableDefinitions={dynamicVariableDefinitions} promptImprovementEditor={{ prompt: form.prompt, onChange: (prompt) => updateForm("prompt", prompt), }} /> )}
移除不兼容工具配置? 下面 {incompatibleToolCount} 项配置将从当前助手中移除。工具资源本身不会被删除。
    {unsupportedBuiltIns.map((key) => (
  • {BUILT_IN_TOOL_LABELS[key]} 内建能力
  • ))} {unsupportedToolBindings.map((binding) => { const tool = tools.find((item) => item.id === binding.toolId); return (
  • {tool?.name ?? binding.toolId} 外部工具
  • ); })}
); }