feat(agent): similar to the start node of workflow, agent variables also support drag-and-drop (#26899)

This commit is contained in:
yangzheli 2025-10-15 13:07:51 +08:00 committed by GitHub
parent 4d8b8f9210
commit cff5de626b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 69 additions and 43 deletions

View File

@ -1,10 +1,11 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useState } from 'react' import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import produce from 'immer' import produce from 'immer'
import { ReactSortable } from 'react-sortablejs'
import Panel from '../base/feature-panel' import Panel from '../base/feature-panel'
import EditModal from './config-modal' import EditModal from './config-modal'
import VarItem from './var-item' import VarItem from './var-item'
@ -22,6 +23,7 @@ import { useModalContext } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import type { InputVar } from '@/app/components/workflow/types' import type { InputVar } from '@/app/components/workflow/types'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
export const ADD_EXTERNAL_DATA_TOOL = 'ADD_EXTERNAL_DATA_TOOL' export const ADD_EXTERNAL_DATA_TOOL = 'ADD_EXTERNAL_DATA_TOOL'
@ -218,6 +220,16 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
showEditModal() showEditModal()
} }
const promptVariablesWithIds = useMemo(() => promptVariables.map((item) => {
return {
id: item.key,
variable: { ...item },
}
}), [promptVariables])
const canDrag = !readonly && promptVariables.length > 1
return ( return (
<Panel <Panel
className="mt-2" className="mt-2"
@ -245,18 +257,32 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
)} )}
{hasVar && ( {hasVar && (
<div className='mt-1 px-3 pb-3'> <div className='mt-1 px-3 pb-3'>
{promptVariables.map(({ key, name, type, required, config, icon, icon_background }, index) => ( <ReactSortable
<VarItem className='space-y-1'
key={index} list={promptVariablesWithIds}
readonly={readonly} setList={(list) => { onPromptVariablesChange?.(list.map(item => item.variable)) }}
name={key} handle='.handle'
label={name} ghostClass='opacity-50'
required={!!required} animation={150}
type={type} >
onEdit={() => handleConfig({ type, key, index, name, config, icon, icon_background })} {promptVariablesWithIds.map((item, index) => {
onRemove={() => handleRemoveVar(index)} const { key, name, type, required, config, icon, icon_background } = item.variable
/> return (
))} <VarItem
className={cn(canDrag && 'handle')}
key={key}
readonly={readonly}
name={key}
label={name}
required={!!required}
type={type}
onEdit={() => handleConfig({ type, key, index, name, config, icon, icon_background })}
onRemove={() => handleRemoveVar(index)}
canDrag={canDrag}
/>
)
})}
</ReactSortable>
</div> </div>
)} )}

View File

@ -3,6 +3,7 @@ import type { FC } from 'react'
import React, { useState } from 'react' import React, { useState } from 'react'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
RiDraggable,
RiEditLine, RiEditLine,
} from '@remixicon/react' } from '@remixicon/react'
import type { IInputTypeIconProps } from './input-type-icon' import type { IInputTypeIconProps } from './input-type-icon'
@ -12,6 +13,7 @@ import Badge from '@/app/components/base/badge'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type ItemProps = { type ItemProps = {
className?: string
readonly?: boolean readonly?: boolean
name: string name: string
label: string label: string
@ -19,9 +21,11 @@ type ItemProps = {
type: string type: string
onEdit: () => void onEdit: () => void
onRemove: () => void onRemove: () => void
canDrag?: boolean
} }
const VarItem: FC<ItemProps> = ({ const VarItem: FC<ItemProps> = ({
className,
readonly, readonly,
name, name,
label, label,
@ -29,12 +33,16 @@ const VarItem: FC<ItemProps> = ({
type, type,
onEdit, onEdit,
onRemove, onRemove,
canDrag,
}) => { }) => {
const [isDeleting, setIsDeleting] = useState(false) const [isDeleting, setIsDeleting] = useState(false)
return ( return (
<div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed opacity-30')}> <div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed opacity-30', className)}>
<VarIcon className='mr-1 h-4 w-4 shrink-0 text-text-accent' /> <VarIcon className={cn('mr-1 h-4 w-4 shrink-0 text-text-accent', canDrag && 'group-hover:opacity-0')} />
{canDrag && (
<RiDraggable className='absolute left-3 top-3 hidden h-3 w-3 cursor-pointer text-text-tertiary group-hover:block' />
)}
<div className='flex w-0 grow items-center'> <div className='flex w-0 grow items-center'>
<div className='truncate' title={`${name} · ${label}`}> <div className='truncate' title={`${name} · ${label}`}>
<span className='system-sm-medium text-text-secondary'>{name}</span> <span className='system-sm-medium text-text-secondary'>{name}</span>

View File

@ -61,7 +61,6 @@ const VarList: FC<Props> = ({
return return
} }
if (list.some(item => item.variable?.trim() === newKey.trim())) { if (list.some(item => item.variable?.trim() === newKey.trim())) {
console.log('new key', newKey.trim())
setToastHandle(Toast.notify({ setToastHandle(Toast.notify({
type: 'error', type: 'error',
message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }), message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }),

View File

@ -5,7 +5,6 @@ import produce from 'immer'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import VarItem from './var-item' import VarItem from './var-item'
import { ChangeType, type InputVar, type MoreInfo } from '@/app/components/workflow/types' import { ChangeType, type InputVar, type MoreInfo } from '@/app/components/workflow/types'
import { v4 as uuid4 } from 'uuid'
import { ReactSortable } from 'react-sortablejs' import { ReactSortable } from 'react-sortablejs'
import { RiDraggable } from '@remixicon/react' import { RiDraggable } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -71,9 +70,8 @@ const VarList: FC<Props> = ({
}, [list, onChange]) }, [list, onChange])
const listWithIds = useMemo(() => list.map((item) => { const listWithIds = useMemo(() => list.map((item) => {
const id = uuid4()
return { return {
id, id: item.variable,
variable: { ...item }, variable: { ...item },
} }
}), [list]) }), [list])
@ -88,6 +86,8 @@ const VarList: FC<Props> = ({
) )
} }
const canDrag = !readonly && varCount > 1
return ( return (
<ReactSortable <ReactSortable
className='space-y-1' className='space-y-1'
@ -97,30 +97,23 @@ const VarList: FC<Props> = ({
ghostClass='opacity-50' ghostClass='opacity-50'
animation={150} animation={150}
> >
{list.map((item, index) => { {listWithIds.map((itemWithId, index) => (
const canDrag = (() => { <div key={itemWithId.id} className='group relative'>
if (readonly) <VarItem
return false className={cn(canDrag && 'handle')}
return varCount > 1 readonly={readonly}
})() payload={itemWithId.variable}
return ( onChange={handleVarChange(index)}
<div key={index} className='group relative'> onRemove={handleVarRemove(index)}
<VarItem varKeys={list.map(item => item.variable)}
className={cn(canDrag && 'handle')} canDrag={canDrag}
readonly={readonly} />
payload={item} {canDrag && <RiDraggable className={cn(
onChange={handleVarChange(index)} 'handle absolute left-3 top-2.5 hidden h-3 w-3 cursor-pointer text-text-tertiary',
onRemove={handleVarRemove(index)} 'group-hover:block',
varKeys={list.map(item => item.variable)} )} />}
canDrag={canDrag} </div>
/> ))}
{canDrag && <RiDraggable className={cn(
'handle absolute left-3 top-2.5 hidden h-3 w-3 cursor-pointer text-text-tertiary',
'group-hover:block',
)} />}
</div>
)
})}
</ReactSortable> </ReactSortable>
) )
} }