refactor: enhance OptionCombobox to support grouping and simplify direct tool selection in AIAgentConfigWorkbench

This commit is contained in:
mlogclub
2026-07-26 12:21:58 +08:00
parent a252cdc8b8
commit b9af4549a6
2 changed files with 44 additions and 130 deletions
@@ -50,7 +50,6 @@ import {
type CreateAIAgentPayload,
type KnowledgeBase,
type MCPToolCatalogItem,
type MCPToolSourceType,
type SkillDefinition,
} from "@/lib/api/admin"
import {
@@ -69,9 +68,10 @@ type DirectToolItem = CreateAIAgentPayload["directTools"][number]
type DirectToolOption = {
value: string
label: string
group: string
subtitle: string
description?: string
meta: DirectToolItem
sourceType: MCPToolSourceType
groupLabel: string
}
const runtimeModes: {
@@ -153,8 +153,6 @@ export function AIAgentConfigWorkbench({
const [publishedWorkflows, setPublishedWorkflows] = useState<AIWorkflow[]>([])
const [teamToAdd, setTeamToAdd] = useState("")
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("")
const [directToolToAdd, setDirectToolToAdd] = useState("")
useEffect(() => {
setCurrentAgentId(agentId ?? null)
@@ -313,9 +311,10 @@ export function AIAgentConfigWorkbench({
)
.map((tool) => ({
value: tool.toolCode,
label: `${tool.title || tool.toolName} · ${tool.toolCode}`,
sourceType: tool.sourceType,
groupLabel: tool.sourceType === "builtin" ? "内置工具" : tool.serverCode,
label: tool.title || tool.toolName,
group: tool.sourceType === "builtin" ? "内置工具" : tool.serverCode,
subtitle: tool.toolCode,
description: tool.description || undefined,
meta: {
toolCode: tool.toolCode,
serverCode: tool.serverCode,
@@ -327,27 +326,6 @@ export function AIAgentConfigWorkbench({
})),
[toolCatalog],
)
const directToolGroupOptions = useMemo(
() =>
Array.from(
new Map(
directToolOptions.map((option) => [
option.groupLabel,
{ value: option.groupLabel, label: option.groupLabel },
]),
).values(),
),
[directToolOptions],
)
const addableDirectToolOptions = useMemo(
() =>
directToolOptions.filter(
(option) =>
option.groupLabel === directToolGroupToAdd &&
!directTools.some((tool) => tool.toolCode === option.value),
),
[directToolGroupToAdd, directToolOptions, directTools],
)
const workflowOptions = useMemo(
() =>
publishedWorkflows.map((workflow) => ({
@@ -370,15 +348,13 @@ export function AIAgentConfigWorkbench({
setNext([...current, id])
}
function addDirectTool(value: string) {
const option = directToolOptions.find((item) => item.value === value)
if (!option) return
setDirectTools((current) =>
current.some((tool) => tool.toolCode === option.meta.toolCode)
? current
: [...current, option.meta],
function setDirectToolSelection(values: string[]) {
setDirectTools(
values.flatMap((value) => {
const option = directToolOptions.find((item) => item.value === value)
return option ? [option.meta] : []
}),
)
setDirectToolToAdd("")
}
function setWorkflowSelection(values: string[]) {
@@ -802,50 +778,14 @@ export function AIAgentConfigWorkbench({
title="Direct Tool"
description="从工具分组中选择 Agent 可以直接调用的工具。"
>
<div className="grid gap-2 sm:grid-cols-[180px_minmax(0,1fr)_auto]">
<OptionCombobox
value={directToolGroupToAdd}
options={directToolGroupOptions}
placeholder="选择工具分组"
onChange={(value) => {
setDirectToolGroupToAdd(value)
setDirectToolToAdd("")
}}
/>
<OptionCombobox
value={directToolToAdd}
options={addableDirectToolOptions}
placeholder="选择 Direct Tool"
onChange={setDirectToolToAdd}
/>
<Button
type="button"
variant="outline"
disabled={!directToolToAdd}
onClick={() => addDirectTool(directToolToAdd)}
>
</Button>
</div>
<div className="space-y-2">
{directTools.length === 0 ? (
<EmptyResource> Direct Tool</EmptyResource>
) : (
directTools.map((tool) => (
<ResourceRow
key={tool.toolCode}
icon={<PlugIcon />}
title={tool.title || tool.toolCode}
meta={tool.serverCode || "内置工具"}
onRemove={() =>
setDirectTools((current) =>
current.filter((item) => item.toolCode !== tool.toolCode),
)
}
/>
))
)}
</div>
<OptionCombobox
multiple
values={directTools.map((tool) => tool.toolCode)}
options={directToolOptions}
placeholder="选择 Direct Tool"
emptyText="没有可用 Direct Tool"
onValuesChange={setDirectToolSelection}
/>
</FormSection>
</div>
) : null}
@@ -1026,42 +966,6 @@ function FieldBlock({
)
}
function ResourceRow({
icon,
title,
meta,
onRemove,
}: {
icon: ReactNode
title: string
meta?: string
onRemove: () => void
}) {
return (
<div className="flex items-center gap-3 rounded-lg border px-3 py-2.5">
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground [&>svg]:size-4">
{icon}
</span>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">{title}</div>
{meta ? <div className="text-xs text-muted-foreground">{meta}</div> : null}
</div>
<Button type="button" variant="ghost" size="sm" onClick={onRemove}>
<Trash2Icon />
</Button>
</div>
)
}
function EmptyResource({ children }: { children: ReactNode }) {
return (
<div className="rounded-lg border border-dashed p-5 text-center text-sm text-muted-foreground">
{children}
</div>
)
}
function BadgeList({
empty,
items,
+23 -13
View File
@@ -24,6 +24,7 @@ import { useI18n } from "@/i18n/provider"
export type ComboboxOption = {
value: string
label: string
group?: string
subtitle?: string
description?: string
}
@@ -59,14 +60,14 @@ type OptionComboboxProps = CommonOptionComboboxProps &
export function OptionCombobox(props: OptionComboboxProps) {
const {
options,
placeholder,
searchPlaceholder,
emptyText,
disabled = false,
triggerClassName,
preserveExternalSelection = false,
renderOptionAction,
options,
placeholder,
searchPlaceholder,
emptyText,
disabled = false,
triggerClassName,
preserveExternalSelection = false,
renderOptionAction,
} = props
const t = useI18n()
const [open, setOpen] = useState(false)
@@ -80,6 +81,13 @@ export function OptionCombobox(props: OptionComboboxProps) {
: selectedOptions.length === 1
? selectedOptions[0].label
: `已选择 ${selectedOptions.length}`
const optionGroups = Array.from(
options.reduce((groups, option) => {
const group = option.group ?? ""
groups.set(group, [...(groups.get(group) ?? []), option])
return groups
}, new Map<string, ComboboxOption[]>()),
)
function selectOption(optionValue: string) {
if (props.multiple) {
@@ -118,11 +126,12 @@ export function OptionCombobox(props: OptionComboboxProps) {
<CommandInput placeholder={searchPlaceholder ?? t("common.searchKeyword")} />
<CommandList>
<CommandEmpty>{emptyText ?? t("common.emptyOptions")}</CommandEmpty>
<CommandGroup>
{options.map((option) => (
{optionGroups.map(([group, groupOptions]) => (
<CommandGroup key={group || "__default"} heading={group || undefined}>
{groupOptions.map((option) => (
<CommandItem
key={option.value}
value={`${option.label} ${option.value} ${option.subtitle ?? ""} ${option.description ?? ""}`}
value={`${option.group ?? ""} ${option.label} ${option.value} ${option.subtitle ?? ""} ${option.description ?? ""}`}
onSelect={() => selectOption(option.value)}
>
<div className="flex min-w-0 flex-1 items-center justify-between gap-2">
@@ -166,8 +175,9 @@ export function OptionCombobox(props: OptionComboboxProps) {
) : null}
</div>
</CommandItem>
))}
</CommandGroup>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>