feat: add system tools and state updates

This commit is contained in:
Xin Wang
2026-08-04 15:27:39 +08:00
parent 5bf5987fe4
commit 731a372df9
33 changed files with 1787 additions and 124 deletions
@@ -12,6 +12,7 @@ import {
Plus,
ServerCog,
Settings2,
Sparkles,
Waypoints,
Wrench,
X,
@@ -37,8 +38,19 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import type { KnowledgeRetrievalConfig, Tool } from "@/lib/api";
import type {
KnowledgeRetrievalConfig,
SystemToolKind,
Tool,
} from "@/lib/api";
import { SYSTEM_TOOL_OPTIONS } from "@/lib/system-tools";
import type { RuntimeMode } from "./types";
@@ -475,31 +487,90 @@ export function ToolPicker({
tools,
selectedIds,
onChange,
selectedSystemTools,
onSystemToolsChange,
showBuiltInSystemTools,
}: {
tools: Tool[];
selectedIds: string[];
onChange: (ids: string[]) => void;
selectedSystemTools: SystemToolKind[];
onSystemToolsChange: (tools: SystemToolKind[]) => void;
showBuiltInSystemTools: boolean;
}) {
const [open, setOpen] = useState(false);
const [activeTab, setActiveTab] = useState<Tool["type"]>("system");
const [draftIds, setDraftIds] = useState<string[]>(selectedIds);
const [draftSystemTools, setDraftSystemTools] =
useState<SystemToolKind[]>(selectedSystemTools);
const selectedTools = selectedIds
.map((id) => tools.find((tool) => tool.id === id))
.filter((tool): tool is Tool => Boolean(tool));
const selectedBuiltInTools = selectedSystemTools
.map((kind) => SYSTEM_TOOL_OPTIONS.find((option) => option.value === kind))
.filter((option): option is (typeof SYSTEM_TOOL_OPTIONS)[number] =>
Boolean(option),
);
function openPicker() {
setDraftIds(selectedIds);
setDraftSystemTools(selectedSystemTools);
setOpen(true);
}
function toggleResourceTool(toolId: string) {
setDraftIds((current) =>
current.includes(toolId)
? current.filter((id) => id !== toolId)
: [...current, toolId],
);
}
function toggleBuiltInTool(kind: SystemToolKind) {
setDraftSystemTools((current) =>
current.includes(kind)
? current.filter((item) => item !== kind)
: [...current, kind],
);
}
const tabs: Array<{ value: Tool["type"]; label: string }> = [
{ value: "system", label: "System" },
{ value: "http", label: "HTTP" },
{ value: "client", label: "Client" },
{ value: "mcp", label: "MCP" },
];
return (
<>
<div className="flex min-h-9 flex-wrap items-center gap-2">
{selectedBuiltInTools.map((tool) => (
<div
key={`built-in-${tool.value}`}
className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
>
<Sparkles size={14} />
<span className="max-w-48 truncate">{tool.label}</span>
<button
type="button"
onClick={() =>
onSystemToolsChange(
selectedSystemTools.filter((kind) => kind !== tool.value),
)
}
className="text-muted-soft transition-colors hover:text-foreground"
aria-label={`移除系统工具 ${tool.label}`}
>
<X size={13} />
</button>
</div>
))}
{selectedTools.map((tool) => (
<div
key={tool.id}
className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
>
{tool.type === "end_call" ? (
{tool.type === "system" ? (
<PhoneOff size={14} />
) : tool.type === "mcp" ? (
<ServerCog size={14} />
@@ -534,58 +605,108 @@ export function ToolPicker({
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>选择工具</DialogTitle>
<DialogDescription>选择已经在工具资源中启用的工具。</DialogDescription>
<DialogDescription>
按类型选择内置系统工具或已经启用的工具资源。
</DialogDescription>
</DialogHeader>
{tools.length === 0 ? (
<div className="rounded-xl border border-dashed border-hairline-strong px-4 py-10 text-center text-sm text-muted-foreground">
暂无可用工具
</div>
) : (
<div className="max-h-80 divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
{tools.map((tool) => {
const checked = draftIds.includes(tool.id);
return (
<label
key={tool.id}
className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-surface-strong/40"
>
<input
type="checkbox"
checked={checked}
onChange={() =>
setDraftIds((current) =>
checked
? current.filter((id) => id !== tool.id)
: [...current, tool.id],
)
}
className="size-4 accent-primary"
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium text-foreground">
{tool.name}
</span>
<Badge variant="secondary">
{tool.type === "end_call"
? "End Call"
: tool.type === "mcp"
? "MCP"
: tool.type === "client"
? "Client"
: "HTTP"}
</Badge>
</div>
<div className="mt-0.5 truncate font-mono text-xs text-muted-foreground">
{tool.functionName}
</div>
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as Tool["type"])}
>
<TabsList
variant="line"
className="w-full justify-start border-b border-hairline px-1"
>
{tabs.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="flex-none px-4"
>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab) => {
const resources = tools.filter((tool) => tool.type === tab.value);
const hasBuiltIns =
tab.value === "system" && showBuiltInSystemTools;
const isEmpty = resources.length === 0 && !hasBuiltIns;
return (
<TabsContent key={tab.value} value={tab.value} className="pt-3">
{isEmpty ? (
<div className="rounded-xl border border-dashed border-hairline-strong px-4 py-10 text-center text-sm text-muted-foreground">
暂无可用的 {tab.label} 工具
</div>
</label>
);
})}
</div>
)}
) : (
<div className="max-h-[280px] divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
{hasBuiltIns &&
SYSTEM_TOOL_OPTIONS.map((option) => {
const checked = draftSystemTools.includes(option.value);
return (
<label
key={`built-in-${option.value}`}
className="flex h-14 cursor-pointer items-center gap-3 px-4 transition-colors hover:bg-surface-strong/40"
>
<input
type="checkbox"
checked={checked}
onChange={() => toggleBuiltInTool(option.value)}
className="size-4 accent-primary"
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium text-foreground">
{option.label}
</span>
<Badge variant="secondary">内置</Badge>
</div>
<div className="mt-0.5 truncate text-xs text-muted-foreground">
{option.description}
</div>
</div>
</label>
);
})}
{resources.map((tool) => {
const checked = draftIds.includes(tool.id);
return (
<label
key={tool.id}
className="flex h-14 cursor-pointer items-center gap-3 px-4 transition-colors hover:bg-surface-strong/40"
>
<input
type="checkbox"
checked={checked}
onChange={() => toggleResourceTool(tool.id)}
className="size-4 accent-primary"
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium text-foreground">
{tool.name}
</span>
{tool.type === "system" && (
<Badge variant="secondary">资源</Badge>
)}
</div>
<div className="mt-0.5 truncate font-mono text-xs text-muted-foreground">
{tool.functionName}
</div>
</div>
</label>
);
})}
</div>
)}
</TabsContent>
);
})}
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
@@ -594,6 +715,7 @@ export function ToolPicker({
<Button
onClick={() => {
onChange(draftIds);
onSystemToolsChange(draftSystemTools);
setOpen(false);
}}
>