feat: improve tool configuration UX
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user