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 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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user