diff --git a/web/app/dashboard/ai-agents/_components/config-workbench.tsx b/web/app/dashboard/ai-agents/_components/config-workbench.tsx index d5bcd90..4d9be36 100644 --- a/web/app/dashboard/ai-agents/_components/config-workbench.tsx +++ b/web/app/dashboard/ai-agents/_components/config-workbench.tsx @@ -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([]) 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" ? (
- } - 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), - ) - } - /> + > + + setSelectedKnowledgeBaseIds(values.map(Number)) + } + /> + - } - 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), - ) + > + setSelectedSkillIds(values.map(Number))} + /> + + + window.location.assign("/dashboard/ai-workflows")} + > + 管理工作流 + } - /> + > +
+ {runtimeMode === "autonomous" + ? "当前为自主接待模式,工作流是可选能力。" + : runtimeMode === "hybrid" + ? "当前为 Hybrid 模式,至少关联一个已发布工作流后才能保存。" + : "当前为仅工作流模式,必须且只能关联一个已发布工作流。"} +
+ + String(binding.workflowVersionId), + )} + options={workflowOptions} + placeholder="选择已发布工作流" + emptyText="没有已发布的工作流" + onValuesChange={setWorkflowSelection} + /> +
- - window.location.assign("/dashboard/ai-workflows")} - > - 管理工作流 - - } - > -
- {runtimeMode === "autonomous" - ? "当前为自主接待模式,工作流是可选能力。" - : runtimeMode === "hybrid" - ? "当前为 Hybrid 模式,至少关联一个已发布工作流后才能保存。" - : "当前为仅工作流模式,必须且只能关联一个已发布工作流。"} -
-
-
- -
- -
-
- {workflowBindings.length === 0 ? ( - 未关联工作流 - ) : ( - workflowBindings.map((binding) => { - const workflow = publishedWorkflows.find( - (item) => - item.publishedVersionId === binding.workflowVersionId, - ) - return ( - } - title={ - workflow?.name || - binding.toolName || - `工作流版本 #${binding.workflowVersionId}` - } - meta={`固定版本 #${binding.workflowVersionId}`} - onRemove={() => - setWorkflowBindings((current) => - current.filter( - (item) => - item.workflowVersionId !== binding.workflowVersionId, - ), - ) - } - /> - ) - }) - )} -
-
) : 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 ( - -
-
- -
- -
-
- {items.length === 0 ? ( - {empty} - ) : ( - items.map((item) => ( - onRemove(Number(item.value))} - /> - )) - )} -
-
- ) -} - function ResourceRow({ icon, title, diff --git a/web/components/option-combobox.tsx b/web/components/option-combobox.tsx index e2fec98..d9a0da5 100644 --- a/web/components/option-combobox.tsx +++ b/web/components/option-combobox.tsx @@ -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 ( @@ -86,19 +123,24 @@ export function OptionCombobox({ { - onChange(option.value) - setOpen(false) - }} + onSelect={() => selectOption(option.value)} >
- + {props.multiple ? ( + + ) : ( + + )} {option.label} {option.subtitle ? (