"use client"; import { useEffect, useRef, useState } from "react"; import { Braces, Bot, Brain, Database, Loader2, MessageSquareText, Save, Sparkles, Wrench, } from "lucide-react"; import { DebugDrawer } from "@/components/assistant-editor/debug-preview"; import { DynamicVariableEditorHint, DynamicVariablesSection, } from "@/components/assistant-editor/dynamic-variables"; import { AssistantIdentity, EditableTitle, EditorBackButton, KnowledgeRetrievalConfigDialog, ResourceSelectField, RuntimeModeSelector, TextAreaField, ToolPicker, } from "@/components/assistant-editor/editor-controls"; import type { AssistantForm } from "@/components/assistant-editor/types"; import { SectionCard } from "@/components/editor/section-card"; import { VisionConfigSection } from "@/components/editor/vision-config-section"; import { TurnConfigEditor } from "@/components/turn-config-editor"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import type { DynamicVariableDefinition, StartupConfig, Tool, } from "@/lib/api"; type ResourceOption = { value: string; label: string }; type PromptOpeningMode = StartupConfig["openingMode"]; type PromptEntryMode = StartupConfig["entryMode"]; 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: "动态变量" }, ] 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[]; onBack: () => void; onSave: () => void; 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, onBack, onSave, 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, }); const selectedAnchorRef = useRef(null); const [activeSection, setActiveSection] = useState("conversation"); 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 }, }); } return (
updateForm("name", value)} />
{saveError && ( {saveError} )}
{ sectionRefs.current.conversation = element; }} className="scroll-mt-3 space-y-3" > } title="提示词" description="描述助手的角色、能力和回答要求" > updateForm("prompt", value)} placeholder="请输入提示词,描述助手的角色、能力和回答要求" rows={8} /> scrollToSection("variables")} /> } title="开场白" description="助手与用户首次对话时的开场语" > updateForm("greeting", value)} placeholder="请输入助手开场白" /> 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) } /> )} {form.runtimeMode === "pipeline" && ( } title="知识库配置" description="选择助手回答时可检索的业务知识来源" >
知识库选择 updateForm("knowledgeRetrievalConfig", config) } />
updateForm("knowledgeBase", value)} options={kbOptions} noneLabel="无" />
)} } title="工具" description="配置该提示词助手可以调用的工具" > tool.status === "active")} selectedIds={form.toolIds} onChange={(toolIds) => updateForm("toolIds", toolIds)} />
{ 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, ) } />
); }