Files
ai-video-fullstack/frontend/src/components/assistant-editor/prompt-editor.tsx
T

1065 lines
40 KiB
TypeScript

"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<keyof BuiltInTools, string> = {
endConversation: "结束对话",
updateState: "更新状态",
skipTurn: "跳过本轮",
requestHumanHandoff: "转接人工",
};
const EXTERNAL_TOOL_TYPE_LABELS: Record<Tool["type"], string> = {
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<string, DynamicVariableDefinition>;
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<void>;
updateForm: <K extends keyof AssistantForm>(
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<HTMLDivElement>(null);
const sectionRefs = useRef<Record<PromptSectionId, HTMLElement | null>>({
conversation: null,
models: null,
capabilities: null,
interaction: null,
variables: null,
analysis: null,
});
const selectedAnchorRef = useRef<PromptSectionId | null>(null);
const [activeSection, setActiveSection] =
useState<PromptSectionId>("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<keyof BuiltInTools>
).filter(
(key) =>
Boolean(form.builtInTools[key]) && !allowedRealtimeBuiltIns.has(key),
);
const allowedRealtimeExternalTypes = new Set<string>(
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<NonNullable<typeof openingMessage>>,
) {
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 (
<>
<TopbarPortal>
<div className="flex h-full min-w-0 flex-1 items-center justify-between gap-4 sm:-ml-2 lg:-ml-4">
<div className="flex min-w-0 items-center gap-2">
<EditorBackButton onClick={onBack} />
<EditableTitle
value={form.name}
onChange={(value) => updateForm("name", value)}
/>
<AssistantIdentity assistantId={assistantId} />
{onDelete && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon-sm"
className="border-hairline-strong text-muted-foreground hover:text-foreground"
disabled={!assistantId || deleting}
aria-label="更多操作"
>
{deleting ? (
<Loader2 size={15} className="animate-spin" />
) : (
<MoreHorizontal size={15} />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="w-32 min-w-32 rounded-xl border border-hairline bg-popover p-1"
>
<DropdownMenuItem
variant="destructive"
className="rounded-lg"
disabled={deleting}
onSelect={(event) => {
event.preventDefault();
window.setTimeout(() => void handleDeleteSelect(), 0);
}}
>
<Trash2 size={14} />
删除
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<div className="flex shrink-0 items-center gap-2">
{saveError && (
<span
role="alert"
title={saveError}
className="line-clamp-1 max-w-[min(32vw,420px)] text-right text-xs text-destructive"
>
{saveError}
</span>
)}
<Button
variant="outline"
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
disabled={!assistantId}
onClick={() => setDebugOpen(true)}
>
<Bug size={16} />
调试
</Button>
<Button
className="gap-2"
disabled={saving || !dirty || !form.name.trim()}
onClick={() => onSave()}
>
{saving ? (
<Loader2 size={16} className="animate-spin" />
) : (
<Save size={16} />
)}
保存
</Button>
</div>
</div>
</TopbarPortal>
<div
data-app-content="full-bleed"
className="flex h-full min-h-0 overflow-hidden bg-background"
>
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SectionAnchorTabs
ariaLabel="提示词配置分区"
sections={promptSections}
activeSectionId={activeSection}
onSelect={(sectionId) =>
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"
}
/>
<div
ref={scrollContainerRef}
className="scrollbar-subtle min-h-0 min-w-0 flex-1 overflow-y-auto overscroll-none bg-background px-4 pb-6 pt-3 sm:px-6 sm:pb-8 lg:px-8 lg:pb-10"
>
<div
className={
debugOpen ? "space-y-3" : "mx-auto max-w-7xl space-y-3"
}
>
<section
ref={(element) => {
sectionRefs.current.conversation = element;
}}
className="scroll-mt-3 space-y-3"
>
<SectionCard
icon={<MessageSquareText size={15} />}
title="提示词"
description="描述助手的角色、能力和回答要求"
>
<PromptTemplateEditor
value={form.prompt}
onChange={(value) => updateForm("prompt", value)}
variables={promptVariableSuggestions}
placeholder="请输入提示词,描述助手的角色、能力和回答要求"
ariaLabel="提示词"
rows={16}
/>
<DynamicVariableEditorHint
count={Object.keys(dynamicVariableDefinitions).length}
onOpen={() => scrollToSection("variables")}
/>
</SectionCard>
<SectionCard
icon={<Bot size={15} />}
title="开场白"
description="助手与用户首次对话时的开场语"
>
<PromptTemplateEditor
value={form.greeting}
onChange={(value) => updateForm("greeting", value)}
variables={promptVariableSuggestions}
placeholder="请输入助手开场白"
ariaLabel="开场白"
/>
<DynamicVariableEditorHint
count={Object.keys(dynamicVariableDefinitions).length}
onOpen={() => scrollToSection("variables")}
/>
{form.runtimeMode === "pipeline" && (
<div className="mt-4 space-y-3 border-t border-hairline pt-4">
<div>
<div className="mb-1.5 text-sm font-medium text-foreground">
开场白继续方式
</div>
<Select
value={openingMode}
onValueChange={(value: PromptOpeningMode) =>
setOpeningMode(value)
}
>
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
<SelectValue />
</SelectTrigger>
<SelectContent>
{OPENING_MODE_OPTIONS.map((option) => (
<SelectItem
key={option.value}
value={option.value}
>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="mt-1.5 text-xs leading-5 text-muted-foreground">
{openingModeDescription}
</p>
</div>
{openingMode === "confirmation" && openingMessage && (
<div className="space-y-3 rounded-xl border border-hairline bg-canvas-soft p-4">
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-foreground">
弹窗标题
</span>
<Input
value={openingMessage.title}
onChange={(event) =>
updateOpeningMessage({
title: event.target.value,
})
}
placeholder="重要提示"
className="border-hairline-strong bg-background"
/>
</label>
<TextAreaField
label="重要信息"
value={openingMessage.message}
onChange={(message) =>
updateOpeningMessage({ message })
}
placeholder="请输入需要用户确认的重要信息"
rows={4}
/>
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-foreground">
确认按钮
</span>
<Input
value={openingMessage.confirmLabel}
onChange={(event) =>
updateOpeningMessage({
confirmLabel: event.target.value,
})
}
placeholder="确认"
className="border-hairline-strong bg-background"
/>
</label>
<p className="text-xs leading-5 text-muted-foreground">
确认前关闭语音、文字和图片输入;确认后立即停止剩余开场白并进入
Agent。
</p>
</div>
)}
<div className="border-t border-hairline pt-3">
<div className="mb-1.5 text-sm font-medium text-foreground">
Agent 进入行为
</div>
<Select
value={form.startup.entryMode}
onValueChange={(entryMode: PromptEntryMode) =>
updateForm("startup", {
...form.startup,
entryMode,
})
}
>
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
<SelectValue />
</SelectTrigger>
<SelectContent>
{ENTRY_MODE_OPTIONS.map((option) => (
<SelectItem
key={option.value}
value={option.value}
>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="mt-1.5 text-xs leading-5 text-muted-foreground">
该设置仅控制开场阶段正常完成后的 Agent 首轮行为。
</p>
</div>
</div>
)}
</SectionCard>
</section>
<section
ref={(element) => {
sectionRefs.current.models = element;
}}
className="scroll-mt-3 space-y-3"
>
<SectionCard
icon={<Brain size={15} />}
title="模型与语音"
description={
form.runtimeMode === "pipeline"
? "选择运行方式,以及大语言模型、语音识别与语音合成资源"
: "选择运行方式;Realtime 模型内置语音识别与语音合成"
}
>
<RuntimeModeSelector
value={form.runtimeMode}
onChange={(runtimeMode) => {
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" ? (
<>
<ResourceSelectField
label="大语言模型"
value={form.model}
onChange={handlePromptModelChange}
options={llmOptions}
noneLabel="无"
/>
<ResourceSelectField
label="语音识别"
value={form.asr}
onChange={(value) => updateForm("asr", value)}
options={asrOptions}
noneLabel="无"
/>
<ResourceSelectField
label="语音合成"
value={form.voice}
onChange={(value) => updateForm("voice", value)}
options={ttsOptions}
noneLabel="无"
/>
</>
) : (
<ResourceSelectField
label="Realtime 模型"
value={form.realtimeModel}
onChange={(value) => updateForm("realtimeModel", value)}
options={realtimeOptions}
noneLabel="无"
/>
)}
</SectionCard>
</section>
<section
ref={(element) => {
sectionRefs.current.capabilities = element;
}}
className="scroll-mt-3 space-y-3"
>
{form.runtimeMode === "pipeline" && (
<VisionConfigSection
description="配置提示词助手是否可以按需理解用户摄像头画面"
hint="开启后,助手会获得读取当前视频画面的工具。选择「模型自己」时,大语言模型必须支持图片输入。"
enabled={form.visionEnabled}
modelResourceId={form.visionModelResourceId}
promptMode={form.visionPromptMode}
prompt={form.visionPrompt}
mainModelResourceId={form.model}
modelOptions={visionModelOptions}
onEnabledChange={handlePromptVisionEnabledChange}
onModelResourceIdChange={(value) =>
updateForm("visionModelResourceId", value)
}
onPromptModeChange={(value) =>
updateForm("visionPromptMode", value)
}
onPromptChange={(value) =>
updateForm("visionPrompt", value)
}
/>
)}
{form.runtimeMode === "pipeline" && (
<SectionCard
icon={<Database size={15} />}
title="知识库配置"
description="选择助手回答时可检索的业务知识来源"
>
<div className="flex items-center gap-1.5">
<span className="text-sm font-medium text-foreground">
知识库选择
</span>
<KnowledgeRetrievalConfigDialog
disabled={!form.knowledgeBase}
value={form.knowledgeRetrievalConfig}
onChange={(config) =>
updateForm("knowledgeRetrievalConfig", config)
}
/>
</div>
<ResourceSelectField
value={form.knowledgeBase}
onChange={(value) => updateForm("knowledgeBase", value)}
options={kbOptions}
noneLabel="无"
/>
</SectionCard>
)}
{(form.runtimeMode === "pipeline" ||
Boolean(realtimeToolFeatures)) && (
<SectionCard
icon={<Sparkles size={15} />}
title="内建能力"
description="配置平台提供的结束、状态更新与人工接管能力"
>
<BuiltInToolsEditor
value={form.builtInTools}
variables={dynamicVariableDefinitions}
allowedKeys={
form.runtimeMode === "realtime"
? realtimeToolFeatures?.builtIns
: undefined
}
onChange={(builtInTools) =>
updateForm("builtInTools", builtInTools)
}
/>
</SectionCard>
)}
{form.runtimeMode === "realtime" && !realtimeToolFeatures && (
<SectionCard
icon={<Sparkles size={15} />}
title="Realtime 工具"
description="当前 Realtime 接口尚未开放 Prompt 工具调用"
>
{form.realtimeModel ? (
<div className="space-y-2 rounded-xl border border-hairline bg-canvas-soft p-4">
<p className="text-sm font-medium text-foreground">
{selectedRealtimeModel?.label ?? "当前 Realtime 模型"} 不支持工具调用
</p>
<p className="text-xs leading-5 text-muted-foreground">
已有配置会被保留。保存前请选择支持 Prompt 工具的 Realtime 模型,或查看并移除不兼容项。
</p>
</div>
) : (
<div className="rounded-xl border border-dashed border-hairline-strong p-4">
<p className="text-sm font-medium text-foreground">请先选择 Realtime 模型</p>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
选择模型后,这里会显示其支持的内建能力与外部工具类型。
</p>
</div>
)}
</SectionCard>
)}
{form.runtimeMode === "realtime" &&
Boolean(realtimeToolFeatures) && (
<div className="rounded-xl border border-hairline bg-canvas-soft px-4 py-3">
<div className="text-sm font-medium text-foreground">
当前模型支持的工具
</div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
外部工具:
{realtimeToolFeatures?.externalTypes
.map((type) => EXTERNAL_TOOL_TYPE_LABELS[type])
.join("、") || "无"}
;内建能力:
{realtimeToolFeatures?.builtIns
.map((key) => BUILT_IN_TOOL_LABELS[key])
.join("、") || "无"}
。
</p>
</div>
)}
{form.runtimeMode === "realtime" && incompatibleToolCount > 0 && (
<SectionCard
icon={<AlertTriangle size={15} />}
title={`存在 ${incompatibleToolCount} 项不兼容工具配置`}
description="配置不会被自动删除;保存前需要更换模型或手动移除"
>
<div className="space-y-3">
<ul className="space-y-2">
{unsupportedBuiltIns.map((key) => (
<li
key={key}
className="flex items-start justify-between gap-3 rounded-xl border border-hairline bg-canvas-soft px-3 py-2.5"
>
<div>
<div className="text-sm font-medium text-foreground">
{BUILT_IN_TOOL_LABELS[key]}
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
内建能力 · 当前 Realtime 模型不支持
</div>
</div>
</li>
))}
{unsupportedToolBindings.map((binding) => {
const tool = tools.find((item) => item.id === binding.toolId);
return (
<li
key={binding.toolId}
className="flex items-start justify-between gap-3 rounded-xl border border-hairline bg-canvas-soft px-3 py-2.5"
>
<div>
<div className="text-sm font-medium text-foreground">
{tool?.name ?? `已失效工具 ${binding.toolId}`}
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
{tool
? `${EXTERNAL_TOOL_TYPE_LABELS[tool.type]} 工具 · 当前 Realtime 模型不支持此类型`
: "工具资源已不存在"}
</div>
</div>
</li>
);
})}
</ul>
<div className="flex justify-end">
<Button
type="button"
variant="outline"
onClick={() => setCleanupDialogOpen(true)}
>
查看并移除 {incompatibleToolCount} 项
</Button>
</div>
</div>
</SectionCard>
)}
<SectionCard
icon={<Wrench size={15} />}
title="外部工具"
description="从资源库添加工具,并配置当前助手的参数来源与结果写入"
>
<ToolBindingEditor
allowTask={form.runtimeMode === "pipeline"}
tools={tools}
selectableTools={
form.runtimeMode === "realtime"
? tools.filter((tool) =>
realtimeToolFeatures?.externalTypes.some(type => type === tool.type),
)
: tools
}
value={form.toolBindings}
variables={dynamicVariableDefinitions}
onChange={(toolBindings) =>
updateForm("toolBindings", toolBindings)
}
/>
</SectionCard>
</section>
<section
ref={(element) => {
sectionRefs.current.interaction = element;
}}
className="scroll-mt-3 space-y-3"
>
<SectionCard
icon={<Sparkles size={15} />}
title="交互策略"
description="设置实时视频对话时的交互体验"
>
{form.runtimeMode === "pipeline" ? (
<TurnConfigEditor
enabled={form.enableInterrupt}
config={form.turnConfig}
onEnabledChange={(checked) =>
updateForm("enableInterrupt", checked)
}
onConfigChange={(config) =>
updateForm("turnConfig", config)
}
/>
) : (
<p className="text-sm text-muted-foreground">
高级抢话与轮次检测当前仅支持 Pipeline 模式。
</p>
)}
</SectionCard>
</section>
<section
ref={(element) => {
sectionRefs.current.variables = element;
}}
className="scroll-mt-3 space-y-3"
>
<SectionCard
icon={<Braces size={15} />}
title="动态变量"
description="编辑提示词和开场白中引用的自定义变量"
>
<DynamicVariablesSection
definitions={dynamicVariableDefinitions}
onChange={(dynamicVariableDefinitions) =>
updateForm(
"dynamicVariableDefinitions",
dynamicVariableDefinitions,
)
}
/>
</SectionCard>
</section>
<section
ref={(element) => {
sectionRefs.current.analysis = element;
}}
className="scroll-mt-3 space-y-3"
>
<SectionCard
icon={<ChartLine size={15} />}
title="分析"
description="通话结束后自动提取关键信息"
>
<AnalysisConfigEditor
config={form.analysisConfig}
onChange={(analysisConfig) =>
updateForm("analysisConfig", analysisConfig)
}
modelOptions={llmOptions}
/>
</SectionCard>
</section>
</div>
</div>
</div>
{debugOpen && (
<DebugDrawer
overlay
assistantId={assistantId}
onClose={() => setDebugOpen(false)}
hasUnsavedChanges={dirty}
vision={form.visionEnabled}
dynamicVariablesEnabled
dynamicVariableDefinitions={dynamicVariableDefinitions}
promptImprovementEditor={{
prompt: form.prompt,
onChange: (prompt) => updateForm("prompt", prompt),
}}
/>
)}
</div>
<Dialog open={cleanupDialogOpen} onOpenChange={setCleanupDialogOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>移除不兼容工具配置?</DialogTitle>
<DialogDescription>
下面 {incompatibleToolCount} 项配置将从当前助手中移除。工具资源本身不会被删除。
</DialogDescription>
</DialogHeader>
<ul className="max-h-64 space-y-2 overflow-y-auto">
{unsupportedBuiltIns.map((key) => (
<li
key={key}
className="rounded-xl border border-hairline bg-canvas-soft px-3 py-2 text-sm"
>
{BUILT_IN_TOOL_LABELS[key]}
<span className="ml-2 text-xs text-muted-foreground">内建能力</span>
</li>
))}
{unsupportedToolBindings.map((binding) => {
const tool = tools.find((item) => item.id === binding.toolId);
return (
<li
key={binding.toolId}
className="rounded-xl border border-hairline bg-canvas-soft px-3 py-2 text-sm"
>
{tool?.name ?? binding.toolId}
<span className="ml-2 text-xs text-muted-foreground">外部工具</span>
</li>
);
})}
</ul>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setCleanupDialogOpen(false)}
>
取消
</Button>
<Button
type="button"
variant="destructive"
onClick={() => {
updateForm(
"builtInTools",
Object.fromEntries(
Object.entries(form.builtInTools).filter(([key]) =>
allowedRealtimeBuiltIns.has(key as keyof BuiltInTools),
),
) as BuiltInTools,
);
updateForm(
"toolBindings",
form.toolBindings.filter(
(binding) => !unsupportedToolIds.has(binding.toolId),
),
);
setCleanupDialogOpen(false);
}}
>
确认移除
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}