refactor: simplify workflow selection and enhance OptionCombobox component
This commit is contained in:
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"
|
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"
|
||||||
import {
|
import {
|
||||||
BookOpenIcon,
|
|
||||||
GitBranchIcon,
|
|
||||||
HistoryIcon,
|
HistoryIcon,
|
||||||
MessageSquareTextIcon,
|
MessageSquareTextIcon,
|
||||||
PlugIcon,
|
PlugIcon,
|
||||||
@@ -155,11 +153,8 @@ export function AIAgentConfigWorkbench({
|
|||||||
const [publishedWorkflows, setPublishedWorkflows] = useState<AIWorkflow[]>([])
|
const [publishedWorkflows, setPublishedWorkflows] = useState<AIWorkflow[]>([])
|
||||||
|
|
||||||
const [teamToAdd, setTeamToAdd] = useState("")
|
const [teamToAdd, setTeamToAdd] = useState("")
|
||||||
const [skillToAdd, setSkillToAdd] = useState("")
|
|
||||||
const [knowledgeBaseToAdd, setKnowledgeBaseToAdd] = useState("")
|
|
||||||
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("")
|
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("")
|
||||||
const [directToolToAdd, setDirectToolToAdd] = useState("")
|
const [directToolToAdd, setDirectToolToAdd] = useState("")
|
||||||
const [workflowToAdd, setWorkflowToAdd] = useState("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentAgentId(agentId ?? null)
|
setCurrentAgentId(agentId ?? null)
|
||||||
@@ -355,18 +350,12 @@ export function AIAgentConfigWorkbench({
|
|||||||
)
|
)
|
||||||
const workflowOptions = useMemo(
|
const workflowOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
publishedWorkflows
|
publishedWorkflows.map((workflow) => ({
|
||||||
.filter(
|
|
||||||
(workflow) =>
|
|
||||||
!workflowBindings.some(
|
|
||||||
(binding) => binding.workflowVersionId === workflow.publishedVersionId,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map((workflow) => ({
|
|
||||||
value: String(workflow.publishedVersionId),
|
value: String(workflow.publishedVersionId),
|
||||||
label: `${workflow.name} · 固定版本 #${workflow.publishedVersionId}`,
|
label: workflow.name,
|
||||||
|
subtitle: `固定版本 #${workflow.publishedVersionId}`,
|
||||||
})),
|
})),
|
||||||
[publishedWorkflows, workflowBindings],
|
[publishedWorkflows],
|
||||||
)
|
)
|
||||||
|
|
||||||
function selectedOptions(ids: number[], options: { value: string; label: string }[]) {
|
function selectedOptions(ids: number[], options: { value: string; label: string }[]) {
|
||||||
@@ -392,26 +381,36 @@ export function AIAgentConfigWorkbench({
|
|||||||
setDirectToolToAdd("")
|
setDirectToolToAdd("")
|
||||||
}
|
}
|
||||||
|
|
||||||
function addWorkflowBinding(value: string) {
|
function setWorkflowSelection(values: string[]) {
|
||||||
const workflow = publishedWorkflows.find(
|
let selectedVersionIds = values.map(Number).filter((value) => value > 0)
|
||||||
(item) => item.publishedVersionId === Number(value),
|
if (runtimeMode === "workflow" && selectedVersionIds.length > 1) {
|
||||||
)
|
const currentIds = workflowBindings.map((binding) => binding.workflowVersionId)
|
||||||
if (!workflow) return
|
const newlySelected = selectedVersionIds.find((id) => !currentIds.includes(id))
|
||||||
if (runtimeMode === "workflow" && workflowBindings.length >= 1) {
|
selectedVersionIds = [newlySelected ?? selectedVersionIds.at(-1)!]
|
||||||
toast.error("仅工作流模式只能关联一个已发布工作流")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
setWorkflowBindings((current) => [
|
setWorkflowBindings(
|
||||||
...current,
|
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: workflow.publishedVersionId,
|
workflowVersionId,
|
||||||
toolName: workflow.name,
|
toolName: workflow.name,
|
||||||
triggerInstruction: "",
|
triggerInstruction: "",
|
||||||
priority: current.length + 1,
|
priority: index + 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
])
|
]
|
||||||
setWorkflowToAdd("")
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateForm() {
|
function validateForm() {
|
||||||
@@ -737,55 +736,67 @@ export function AIAgentConfigWorkbench({
|
|||||||
|
|
||||||
{activeSection === "capability" ? (
|
{activeSection === "capability" ? (
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
<ResourceSection
|
<FormSection
|
||||||
title="知识库"
|
title="知识库"
|
||||||
description="Agent 回答时可以检索的知识范围。"
|
description="Agent 回答时可以检索的知识范围。"
|
||||||
icon={<BookOpenIcon />}
|
>
|
||||||
value={knowledgeBaseToAdd}
|
<OptionCombobox
|
||||||
options={knowledgeBaseOptions.filter(
|
multiple
|
||||||
(option) => !selectedKnowledgeBaseIds.includes(Number(option.value)),
|
values={selectedKnowledgeBaseIds.map(String)}
|
||||||
)}
|
options={knowledgeBaseOptions}
|
||||||
placeholder="选择知识库"
|
placeholder="选择知识库"
|
||||||
onValueChange={setKnowledgeBaseToAdd}
|
emptyText="没有可用知识库"
|
||||||
onAdd={() => {
|
onValuesChange={(values) =>
|
||||||
addSelected(
|
setSelectedKnowledgeBaseIds(values.map(Number))
|
||||||
knowledgeBaseToAdd,
|
|
||||||
selectedKnowledgeBaseIds,
|
|
||||||
setSelectedKnowledgeBaseIds,
|
|
||||||
)
|
|
||||||
setKnowledgeBaseToAdd("")
|
|
||||||
}}
|
|
||||||
items={selectedOptions(selectedKnowledgeBaseIds, knowledgeBaseOptions)}
|
|
||||||
empty="未配置知识库"
|
|
||||||
onRemove={(id) =>
|
|
||||||
setSelectedKnowledgeBaseIds((current) =>
|
|
||||||
current.filter((item) => item !== id),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
<ResourceSection
|
<FormSection
|
||||||
title="Skill"
|
title="Skill"
|
||||||
description="Agent 可以调用的标准能力。"
|
description="Agent 可以调用的标准能力。"
|
||||||
icon={<PlugIcon />}
|
>
|
||||||
value={skillToAdd}
|
<OptionCombobox
|
||||||
options={skillOptions.filter(
|
multiple
|
||||||
(option) => !selectedSkillIds.includes(Number(option.value)),
|
values={selectedSkillIds.map(String)}
|
||||||
)}
|
options={skillOptions}
|
||||||
placeholder="选择 Skill"
|
placeholder="选择 Skill"
|
||||||
onValueChange={setSkillToAdd}
|
emptyText="没有可用 Skill"
|
||||||
onAdd={() => {
|
onValuesChange={(values) => setSelectedSkillIds(values.map(Number))}
|
||||||
addSelected(skillToAdd, selectedSkillIds, setSelectedSkillIds)
|
|
||||||
setSkillToAdd("")
|
|
||||||
}}
|
|
||||||
items={selectedOptions(selectedSkillIds, skillOptions)}
|
|
||||||
empty="未配置 Skill"
|
|
||||||
onRemove={(id) =>
|
|
||||||
setSelectedSkillIds((current) =>
|
|
||||||
current.filter((item) => item !== id),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
</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
|
<FormSection
|
||||||
title="Direct Tool"
|
title="Direct Tool"
|
||||||
@@ -836,78 +847,6 @@ export function AIAgentConfigWorkbench({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</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>
|
</div>
|
||||||
) : null}
|
) : 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({
|
function ResourceRow({
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState, type ReactNode } from "react"
|
|||||||
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"
|
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -27,8 +28,7 @@ export type ComboboxOption = {
|
|||||||
description?: string
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionComboboxProps = {
|
type CommonOptionComboboxProps = {
|
||||||
value: string
|
|
||||||
options: ComboboxOption[]
|
options: ComboboxOption[]
|
||||||
placeholder: string
|
placeholder: string
|
||||||
searchPlaceholder?: string
|
searchPlaceholder?: string
|
||||||
@@ -36,12 +36,29 @@ type OptionComboboxProps = {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
triggerClassName?: string
|
triggerClassName?: string
|
||||||
preserveExternalSelection?: boolean
|
preserveExternalSelection?: boolean
|
||||||
onChange: (value: string) => void
|
|
||||||
renderOptionAction?: (option: ComboboxOption) => ReactNode
|
renderOptionAction?: (option: ComboboxOption) => ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OptionCombobox({
|
type OptionComboboxProps = CommonOptionComboboxProps &
|
||||||
value,
|
(
|
||||||
|
| {
|
||||||
|
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,
|
options,
|
||||||
placeholder,
|
placeholder,
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
@@ -49,13 +66,33 @@ export function OptionCombobox({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
triggerClassName,
|
triggerClassName,
|
||||||
preserveExternalSelection = false,
|
preserveExternalSelection = false,
|
||||||
onChange,
|
|
||||||
renderOptionAction,
|
renderOptionAction,
|
||||||
}: OptionComboboxProps) {
|
} = props
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const selectedValues = props.multiple ? props.values : [props.value]
|
||||||
|
const selectedOptions = options.filter((option) =>
|
||||||
|
selectedValues.includes(option.value)
|
||||||
|
)
|
||||||
const selectedLabel =
|
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 (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
@@ -86,19 +123,24 @@ export function OptionCombobox({
|
|||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={`${option.label} ${option.value} ${option.subtitle ?? ""} ${option.description ?? ""}`}
|
value={`${option.label} ${option.value} ${option.subtitle ?? ""} ${option.description ?? ""}`}
|
||||||
onSelect={() => {
|
onSelect={() => selectOption(option.value)}
|
||||||
onChange(option.value)
|
|
||||||
setOpen(false)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<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">
|
||||||
<div className="flex min-w-0 items-start">
|
<div className="flex min-w-0 items-start">
|
||||||
|
{props.multiple ? (
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedValues.includes(option.value)}
|
||||||
|
className="pointer-events-none mr-2 mt-0.5"
|
||||||
|
tabIndex={-1}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 mt-0.5 size-4 shrink-0",
|
"mr-2 mt-0.5 size-4 shrink-0",
|
||||||
option.value === value ? "opacity-100" : "opacity-0"
|
option.value === props.value ? "opacity-100" : "opacity-0"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<span className="min-w-0">
|
<span className="min-w-0">
|
||||||
<span className="block truncate">{option.label}</span>
|
<span className="block truncate">{option.label}</span>
|
||||||
{option.subtitle ? (
|
{option.subtitle ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user