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 CreateAIAgentPayload,
type KnowledgeBase, type KnowledgeBase,
type MCPToolCatalogItem, type MCPToolCatalogItem,
type MCPToolSourceType,
type SkillDefinition, type SkillDefinition,
} from "@/lib/api/admin" } from "@/lib/api/admin"
import { import {
@@ -69,9 +68,10 @@ type DirectToolItem = CreateAIAgentPayload["directTools"][number]
type DirectToolOption = { type DirectToolOption = {
value: string value: string
label: string label: string
group: string
subtitle: string
description?: string
meta: DirectToolItem meta: DirectToolItem
sourceType: MCPToolSourceType
groupLabel: string
} }
const runtimeModes: { const runtimeModes: {
@@ -153,8 +153,6 @@ export function AIAgentConfigWorkbench({
const [publishedWorkflows, setPublishedWorkflows] = useState<AIWorkflow[]>([]) const [publishedWorkflows, setPublishedWorkflows] = useState<AIWorkflow[]>([])
const [teamToAdd, setTeamToAdd] = useState("") const [teamToAdd, setTeamToAdd] = useState("")
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("")
const [directToolToAdd, setDirectToolToAdd] = useState("")
useEffect(() => { useEffect(() => {
setCurrentAgentId(agentId ?? null) setCurrentAgentId(agentId ?? null)
@@ -313,9 +311,10 @@ export function AIAgentConfigWorkbench({
) )
.map((tool) => ({ .map((tool) => ({
value: tool.toolCode, value: tool.toolCode,
label: `${tool.title || tool.toolName} · ${tool.toolCode}`, label: tool.title || tool.toolName,
sourceType: tool.sourceType, group: tool.sourceType === "builtin" ? "内置工具" : tool.serverCode,
groupLabel: tool.sourceType === "builtin" ? "内置工具" : tool.serverCode, subtitle: tool.toolCode,
description: tool.description || undefined,
meta: { meta: {
toolCode: tool.toolCode, toolCode: tool.toolCode,
serverCode: tool.serverCode, serverCode: tool.serverCode,
@@ -327,27 +326,6 @@ export function AIAgentConfigWorkbench({
})), })),
[toolCatalog], [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( const workflowOptions = useMemo(
() => () =>
publishedWorkflows.map((workflow) => ({ publishedWorkflows.map((workflow) => ({
@@ -370,15 +348,13 @@ export function AIAgentConfigWorkbench({
setNext([...current, id]) setNext([...current, id])
} }
function addDirectTool(value: string) { function setDirectToolSelection(values: string[]) {
const option = directToolOptions.find((item) => item.value === value) setDirectTools(
if (!option) return values.flatMap((value) => {
setDirectTools((current) => const option = directToolOptions.find((item) => item.value === value)
current.some((tool) => tool.toolCode === option.meta.toolCode) return option ? [option.meta] : []
? current }),
: [...current, option.meta],
) )
setDirectToolToAdd("")
} }
function setWorkflowSelection(values: string[]) { function setWorkflowSelection(values: string[]) {
@@ -802,50 +778,14 @@ export function AIAgentConfigWorkbench({
title="Direct Tool" title="Direct Tool"
description="从工具分组中选择 Agent 可以直接调用的工具。" description="从工具分组中选择 Agent 可以直接调用的工具。"
> >
<div className="grid gap-2 sm:grid-cols-[180px_minmax(0,1fr)_auto]"> <OptionCombobox
<OptionCombobox multiple
value={directToolGroupToAdd} values={directTools.map((tool) => tool.toolCode)}
options={directToolGroupOptions} options={directToolOptions}
placeholder="选择工具分组" placeholder="选择 Direct Tool"
onChange={(value) => { emptyText="没有可用 Direct Tool"
setDirectToolGroupToAdd(value) onValuesChange={setDirectToolSelection}
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>
</FormSection> </FormSection>
</div> </div>
) : null} ) : 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({ function BadgeList({
empty, empty,
items, items,
+23 -13
View File
@@ -24,6 +24,7 @@ import { useI18n } from "@/i18n/provider"
export type ComboboxOption = { export type ComboboxOption = {
value: string value: string
label: string label: string
group?: string
subtitle?: string subtitle?: string
description?: string description?: string
} }
@@ -59,14 +60,14 @@ type OptionComboboxProps = CommonOptionComboboxProps &
export function OptionCombobox(props: OptionComboboxProps) { export function OptionCombobox(props: OptionComboboxProps) {
const { const {
options, options,
placeholder, placeholder,
searchPlaceholder, searchPlaceholder,
emptyText, emptyText,
disabled = false, disabled = false,
triggerClassName, triggerClassName,
preserveExternalSelection = false, preserveExternalSelection = false,
renderOptionAction, renderOptionAction,
} = props } = props
const t = useI18n() const t = useI18n()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@@ -80,6 +81,13 @@ export function OptionCombobox(props: OptionComboboxProps) {
: selectedOptions.length === 1 : selectedOptions.length === 1
? selectedOptions[0].label ? selectedOptions[0].label
: `已选择 ${selectedOptions.length}` : `已选择 ${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) { function selectOption(optionValue: string) {
if (props.multiple) { if (props.multiple) {
@@ -118,11 +126,12 @@ export function OptionCombobox(props: OptionComboboxProps) {
<CommandInput placeholder={searchPlaceholder ?? t("common.searchKeyword")} /> <CommandInput placeholder={searchPlaceholder ?? t("common.searchKeyword")} />
<CommandList> <CommandList>
<CommandEmpty>{emptyText ?? t("common.emptyOptions")}</CommandEmpty> <CommandEmpty>{emptyText ?? t("common.emptyOptions")}</CommandEmpty>
<CommandGroup> {optionGroups.map(([group, groupOptions]) => (
{options.map((option) => ( <CommandGroup key={group || "__default"} heading={group || undefined}>
{groupOptions.map((option) => (
<CommandItem <CommandItem
key={option.value} 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)} onSelect={() => selectOption(option.value)}
> >
<div className="flex min-w-0 flex-1 items-center justify-between gap-2"> <div className="flex min-w-0 flex-1 items-center justify-between gap-2">
@@ -166,8 +175,9 @@ export function OptionCombobox(props: OptionComboboxProps) {
) : null} ) : null}
</div> </div>
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
))}
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>