feat: improve tool configuration UX

This commit is contained in:
Xin Wang
2026-08-14 08:48:03 +08:00
parent 5be21f5ccc
commit 36253de864
4 changed files with 1355 additions and 363 deletions
@@ -5,6 +5,7 @@ import {
AnalysisConfigEditor,
} from "@/components/assistant-editor/analysis-config";
import {
AlertTriangle,
Braces,
Bot,
Brain,
@@ -42,6 +43,14 @@ 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,
@@ -69,6 +78,19 @@ 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",
};
const OPENING_MODE_OPTIONS: Array<{
value: PromptOpeningMode;
label: string;
@@ -177,6 +199,31 @@ export function PromptEditor({
useState<PromptSectionId>("conversation");
const [debugOpen, setDebugOpen] = useState(true);
const [deleting, setDeleting] = useState(false);
const [cleanupDialogOpen, setCleanupDialogOpen] = useState(false);
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(
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;
@@ -719,82 +766,111 @@ export function PromptEditor({
title="Realtime 工具"
description="当前 Realtime 接口尚未开放 Prompt 工具调用"
>
<p className="text-sm text-muted-foreground">
已有配置会被保留,但保存前需要切换到支持工具的模型或清理这些配置。
</p>
{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" && (() => {
const allowedBuiltIns = new Set(
realtimeToolFeatures?.builtIns ?? [],
);
const unsupportedBuiltIns = (
Object.keys(form.builtInTools) as Array<keyof BuiltInTools>
).filter(
(key) =>
Boolean(form.builtInTools[key]) &&
!allowedBuiltIns.has(key),
);
const allowedExternal = new Set(
realtimeToolFeatures?.externalTypes ?? [],
);
const unsupportedToolIds = new Set(
form.toolBindings
.filter((binding) => {
const tool = tools.find((item) => item.id === binding.toolId);
return !tool || !allowedExternal.has(tool.type);
})
.map((binding) => binding.toolId),
);
if (!unsupportedBuiltIns.length && !unsupportedToolIds.size) {
return null;
}
return (
<SectionCard
icon={<Wrench size={15} />}
title="存在不兼容工具配置"
description="这些配置不会被自动删除,后端也会阻止保存"
>
<div className="flex items-center justify-between gap-3">
<p className="text-sm text-muted-foreground">
{unsupportedBuiltIns.length} 个内建工具,
{unsupportedToolIds.size} 个外部工具不受当前接口支持。
</p>
{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={() => {
updateForm(
"builtInTools",
Object.fromEntries(
Object.entries(form.builtInTools).filter(([key]) =>
allowedBuiltIns.has(key as keyof BuiltInTools),
),
) as BuiltInTools,
);
updateForm(
"toolBindings",
form.toolBindings.filter(
(binding) => !unsupportedToolIds.has(binding.toolId),
),
);
}}
onClick={() => setCleanupDialogOpen(true)}
>
清理不兼容配置
查看并移除 {incompatibleToolCount} 项
</Button>
</div>
</SectionCard>
);
})()}
</div>
</SectionCard>
)}
<SectionCard
icon={<Wrench size={15} />}
title="外部工具"
description="选择可复用资源,并配置当前助手的参数来源与结果映射"
description="从资源库添加工具,并配置当前助手的参数来源与结果写入"
>
<ToolBindingEditor
tools={
tools={tools}
selectableTools={
form.runtimeMode === "realtime"
? tools.filter((tool) =>
realtimeToolFeatures?.externalTypes.includes(tool.type),
@@ -899,6 +975,72 @@ export function PromptEditor({
/>
)}
</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>
</>
);
}