refactor: enhance OptionCombobox to support grouping and simplify direct tool selection in AIAgentConfigWorkbench
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user