refactor: simplify workflow selection and enhance OptionCombobox component

This commit is contained in:
mlogclub
2026-07-26 12:18:13 +08:00
parent a1e1290472
commit a252cdc8b8
2 changed files with 150 additions and 227 deletions
@@ -2,8 +2,6 @@
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"
import {
BookOpenIcon,
GitBranchIcon,
HistoryIcon,
MessageSquareTextIcon,
PlugIcon,
@@ -155,11 +153,8 @@ export function AIAgentConfigWorkbench({
const [publishedWorkflows, setPublishedWorkflows] = useState<AIWorkflow[]>([])
const [teamToAdd, setTeamToAdd] = useState("")
const [skillToAdd, setSkillToAdd] = useState("")
const [knowledgeBaseToAdd, setKnowledgeBaseToAdd] = useState("")
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("")
const [directToolToAdd, setDirectToolToAdd] = useState("")
const [workflowToAdd, setWorkflowToAdd] = useState("")
useEffect(() => {
setCurrentAgentId(agentId ?? null)
@@ -355,18 +350,12 @@ export function AIAgentConfigWorkbench({
)
const workflowOptions = useMemo(
() =>
publishedWorkflows
.filter(
(workflow) =>
!workflowBindings.some(
(binding) => binding.workflowVersionId === workflow.publishedVersionId,
),
)
.map((workflow) => ({
value: String(workflow.publishedVersionId),
label: `${workflow.name} · 固定版本 #${workflow.publishedVersionId}`,
})),
[publishedWorkflows, workflowBindings],
publishedWorkflows.map((workflow) => ({
value: String(workflow.publishedVersionId),
label: workflow.name,
subtitle: `固定版本 #${workflow.publishedVersionId}`,
})),
[publishedWorkflows],
)
function selectedOptions(ids: number[], options: { value: string; label: string }[]) {
@@ -392,26 +381,36 @@ export function AIAgentConfigWorkbench({
setDirectToolToAdd("")
}
function addWorkflowBinding(value: string) {
const workflow = publishedWorkflows.find(
(item) => item.publishedVersionId === Number(value),
)
if (!workflow) return
if (runtimeMode === "workflow" && workflowBindings.length >= 1) {
toast.error("仅工作流模式只能关联一个已发布工作流")
return
function setWorkflowSelection(values: string[]) {
let selectedVersionIds = values.map(Number).filter((value) => value > 0)
if (runtimeMode === "workflow" && selectedVersionIds.length > 1) {
const currentIds = workflowBindings.map((binding) => binding.workflowVersionId)
const newlySelected = selectedVersionIds.find((id) => !currentIds.includes(id))
selectedVersionIds = [newlySelected ?? selectedVersionIds.at(-1)!]
}
setWorkflowBindings((current) => [
...current,
{
workflowVersionId: workflow.publishedVersionId,
toolName: workflow.name,
triggerInstruction: "",
priority: current.length + 1,
enabled: true,
},
])
setWorkflowToAdd("")
setWorkflowBindings(
selectedVersionIds.flatMap((workflowVersionId, index) => {
const current = workflowBindings.find(
(binding) => binding.workflowVersionId === workflowVersionId,
)
if (current) {
return [{ ...current, priority: index + 1, enabled: true }]
}
const workflow = publishedWorkflows.find(
(item) => item.publishedVersionId === workflowVersionId,
)
if (!workflow) return []
return [
{
workflowVersionId,
toolName: workflow.name,
triggerInstruction: "",
priority: index + 1,
enabled: true,
},
]
}),
)
}
function validateForm() {
@@ -737,55 +736,67 @@ export function AIAgentConfigWorkbench({
{activeSection === "capability" ? (
<div className="space-y-10">
<ResourceSection
<FormSection
title="知识库"
description="Agent 回答时可以检索的知识范围。"
icon={<BookOpenIcon />}
value={knowledgeBaseToAdd}
options={knowledgeBaseOptions.filter(
(option) => !selectedKnowledgeBaseIds.includes(Number(option.value)),
)}
placeholder="选择知识库"
onValueChange={setKnowledgeBaseToAdd}
onAdd={() => {
addSelected(
knowledgeBaseToAdd,
selectedKnowledgeBaseIds,
setSelectedKnowledgeBaseIds,
)
setKnowledgeBaseToAdd("")
}}
items={selectedOptions(selectedKnowledgeBaseIds, knowledgeBaseOptions)}
empty="未配置知识库"
onRemove={(id) =>
setSelectedKnowledgeBaseIds((current) =>
current.filter((item) => item !== id),
)
}
/>
>
<OptionCombobox
multiple
values={selectedKnowledgeBaseIds.map(String)}
options={knowledgeBaseOptions}
placeholder="选择知识库"
emptyText="没有可用知识库"
onValuesChange={(values) =>
setSelectedKnowledgeBaseIds(values.map(Number))
}
/>
</FormSection>
<ResourceSection
<FormSection
title="Skill"
description="Agent 可以调用的标准能力。"
icon={<PlugIcon />}
value={skillToAdd}
options={skillOptions.filter(
(option) => !selectedSkillIds.includes(Number(option.value)),
)}
placeholder="选择 Skill"
onValueChange={setSkillToAdd}
onAdd={() => {
addSelected(skillToAdd, selectedSkillIds, setSelectedSkillIds)
setSkillToAdd("")
}}
items={selectedOptions(selectedSkillIds, skillOptions)}
empty="未配置 Skill"
onRemove={(id) =>
setSelectedSkillIds((current) =>
current.filter((item) => item !== id),
)
>
<OptionCombobox
multiple
values={selectedSkillIds.map(String)}
options={skillOptions}
placeholder="选择 Skill"
emptyText="没有可用 Skill"
onValuesChange={(values) => setSelectedSkillIds(values.map(Number))}
/>
</FormSection>
<FormSection
title="工作流"
description="关联工作流中心中已经发布的固定版本。"
action={
<Button
type="button"
variant="outline"
onClick={() => window.location.assign("/dashboard/ai-workflows")}
>
</Button>
}
/>
>
<div className="rounded-lg border border-blue-200 bg-blue-50 px-3.5 py-3 text-sm text-blue-900 dark:border-blue-900 dark:bg-blue-950/30 dark:text-blue-200">
{runtimeMode === "autonomous"
? "当前为自主接待模式,工作流是可选能力。"
: runtimeMode === "hybrid"
? "当前为 Hybrid 模式,至少关联一个已发布工作流后才能保存。"
: "当前为仅工作流模式,必须且只能关联一个已发布工作流。"}
</div>
<OptionCombobox
multiple
values={workflowBindings.map((binding) =>
String(binding.workflowVersionId),
)}
options={workflowOptions}
placeholder="选择已发布工作流"
emptyText="没有已发布的工作流"
onValuesChange={setWorkflowSelection}
/>
</FormSection>
<FormSection
title="Direct Tool"
@@ -836,78 +847,6 @@ export function AIAgentConfigWorkbench({
)}
</div>
</FormSection>
<FormSection
title="工作流"
description="关联工作流中心中已经发布的固定版本。"
action={
<Button
type="button"
variant="outline"
onClick={() => window.location.assign("/dashboard/ai-workflows")}
>
</Button>
}
>
<div className="rounded-lg border border-blue-200 bg-blue-50 px-3.5 py-3 text-sm text-blue-900 dark:border-blue-900 dark:bg-blue-950/30 dark:text-blue-200">
{runtimeMode === "autonomous"
? "当前为自主接待模式,工作流是可选能力。"
: runtimeMode === "hybrid"
? "当前为 Hybrid 模式,至少关联一个已发布工作流后才能保存。"
: "当前为仅工作流模式,必须且只能关联一个已发布工作流。"}
</div>
<div className="flex gap-2">
<div className="min-w-0 flex-1">
<OptionCombobox
value={workflowToAdd}
options={workflowOptions}
placeholder="选择已发布工作流"
onChange={setWorkflowToAdd}
/>
</div>
<Button
type="button"
variant="outline"
disabled={!workflowToAdd}
onClick={() => addWorkflowBinding(workflowToAdd)}
>
</Button>
</div>
<div className="space-y-2">
{workflowBindings.length === 0 ? (
<EmptyResource></EmptyResource>
) : (
workflowBindings.map((binding) => {
const workflow = publishedWorkflows.find(
(item) =>
item.publishedVersionId === binding.workflowVersionId,
)
return (
<ResourceRow
key={binding.workflowVersionId}
icon={<GitBranchIcon />}
title={
workflow?.name ||
binding.toolName ||
`工作流版本 #${binding.workflowVersionId}`
}
meta={`固定版本 #${binding.workflowVersionId}`}
onRemove={() =>
setWorkflowBindings((current) =>
current.filter(
(item) =>
item.workflowVersionId !== binding.workflowVersionId,
),
)
}
/>
)
})
)}
</div>
</FormSection>
</div>
) : null}
@@ -1087,64 +1026,6 @@ function FieldBlock({
)
}
function ResourceSection({
title,
description,
icon,
value,
options,
placeholder,
onValueChange,
onAdd,
items,
empty,
onRemove,
}: {
title: string
description: string
icon: ReactNode
value: string
options: { value: string; label: string }[]
placeholder: string
onValueChange: (value: string) => void
onAdd: () => void
items: { value: string; label: string }[]
empty: string
onRemove: (id: number) => void
}) {
return (
<FormSection title={title} description={description}>
<div className="flex gap-2">
<div className="min-w-0 flex-1">
<OptionCombobox
value={value}
options={options}
placeholder={placeholder}
onChange={onValueChange}
/>
</div>
<Button type="button" variant="outline" disabled={!value} onClick={onAdd}>
</Button>
</div>
<div className="space-y-2">
{items.length === 0 ? (
<EmptyResource>{empty}</EmptyResource>
) : (
items.map((item) => (
<ResourceRow
key={item.value}
icon={icon}
title={item.label}
onRemove={() => onRemove(Number(item.value))}
/>
))
)}
</div>
</FormSection>
)
}
function ResourceRow({
icon,
title,
+60 -18
View File
@@ -4,6 +4,7 @@ import { useState, type ReactNode } from "react"
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
Command,
CommandEmpty,
@@ -27,8 +28,7 @@ export type ComboboxOption = {
description?: string
}
type OptionComboboxProps = {
value: string
type CommonOptionComboboxProps = {
options: ComboboxOption[]
placeholder: string
searchPlaceholder?: string
@@ -36,12 +36,29 @@ type OptionComboboxProps = {
disabled?: boolean
triggerClassName?: string
preserveExternalSelection?: boolean
onChange: (value: string) => void
renderOptionAction?: (option: ComboboxOption) => ReactNode
}
export function OptionCombobox({
value,
type OptionComboboxProps = CommonOptionComboboxProps &
(
| {
multiple?: false
value: string
onChange: (value: string) => void
values?: never
onValuesChange?: never
}
| {
multiple: true
values: string[]
onValuesChange: (values: string[]) => void
value?: never
onChange?: never
}
)
export function OptionCombobox(props: OptionComboboxProps) {
const {
options,
placeholder,
searchPlaceholder,
@@ -49,13 +66,33 @@ export function OptionCombobox({
disabled = false,
triggerClassName,
preserveExternalSelection = false,
onChange,
renderOptionAction,
}: OptionComboboxProps) {
} = props
const t = useI18n()
const [open, setOpen] = useState(false)
const selectedValues = props.multiple ? props.values : [props.value]
const selectedOptions = options.filter((option) =>
selectedValues.includes(option.value)
)
const selectedLabel =
options.find((option) => option.value === value)?.label ?? placeholder
selectedOptions.length === 0
? placeholder
: selectedOptions.length === 1
? selectedOptions[0].label
: `已选择 ${selectedOptions.length}`
function selectOption(optionValue: string) {
if (props.multiple) {
props.onValuesChange(
props.values.includes(optionValue)
? props.values.filter((value) => value !== optionValue)
: [...props.values, optionValue]
)
return
}
props.onChange(optionValue)
setOpen(false)
}
return (
<Popover open={open} onOpenChange={setOpen}>
@@ -86,19 +123,24 @@ export function OptionCombobox({
<CommandItem
key={option.value}
value={`${option.label} ${option.value} ${option.subtitle ?? ""} ${option.description ?? ""}`}
onSelect={() => {
onChange(option.value)
setOpen(false)
}}
onSelect={() => selectOption(option.value)}
>
<div className="flex min-w-0 flex-1 items-center justify-between gap-2">
<div className="flex min-w-0 items-start">
<CheckIcon
className={cn(
"mr-2 mt-0.5 size-4 shrink-0",
option.value === value ? "opacity-100" : "opacity-0"
)}
/>
{props.multiple ? (
<Checkbox
checked={selectedValues.includes(option.value)}
className="pointer-events-none mr-2 mt-0.5"
tabIndex={-1}
/>
) : (
<CheckIcon
className={cn(
"mr-2 mt-0.5 size-4 shrink-0",
option.value === props.value ? "opacity-100" : "opacity-0"
)}
/>
)}
<span className="min-w-0">
<span className="block truncate">{option.label}</span>
{option.subtitle ? (