input field in datasource

This commit is contained in:
zxhlyh 2025-05-27 17:42:02 +08:00
parent 365157c37d
commit 69d1e3ec7d
8 changed files with 255 additions and 97 deletions

View File

@ -1,5 +1,5 @@
'use client'
import React, { useRef } from 'react'
import React, { useCallback, useRef } from 'react'
import { useHover } from 'ahooks'
import { useTranslation } from 'react-i18next'
import {
@ -13,12 +13,13 @@ import cn from '@/utils/classnames'
import Badge from '@/app/components/base/badge'
import type { InputVar } from '@/models/pipeline'
import type { InputVarType } from '@/app/components/workflow/types'
import ActionButton from '@/app/components/base/action-button'
type FieldItemProps = {
readonly?: boolean
payload: InputVar
onClickEdit: () => void
onRemove: () => void
onClickEdit: (id: string) => void
onRemove: (id: string) => void
}
const FieldItem = ({
@ -32,6 +33,16 @@ const FieldItem = ({
const ref = useRef(null)
const isHovering = useHover(ref)
const handleOnClickEdit = useCallback(() => {
if (readonly) return
onClickEdit(payload.variable)
}, [onClickEdit, payload.variable, readonly])
const handleRemove = useCallback(() => {
if (readonly) return
onRemove(payload.variable)
}, [onRemove, payload.variable, readonly])
return (
<div
ref={ref}
@ -67,19 +78,17 @@ const FieldItem = ({
{(isHovering && !readonly)
? (
<div className='flex shrink-0 items-center gap-x-1'>
<button
type='button'
className='cursor-pointer rounded-md p-1 hover:bg-state-base-hover'
onClick={onClickEdit}
<ActionButton
className='mr-1'
onClick={handleOnClickEdit}
>
<RiEditLine className='size-4 text-text-tertiary' />
</button>
<button
onClick={onRemove}
className='group cursor-pointer rounded-md p-1 hover:bg-state-destructive-hover'
</ActionButton>
<ActionButton
onClick={handleRemove}
>
<RiDeleteBinLine className='size-4 text-text-tertiary group-hover:text-text-destructive' />
</button>
</ActionButton>
</div>
)
: (

View File

@ -0,0 +1,59 @@
import {
memo,
useMemo,
} from 'react'
import { ReactSortable } from 'react-sortablejs'
import cn from '@/utils/classnames'
import type { InputVar } from '@/models/pipeline'
import FieldItem from './field-item'
import type { SortableItem } from './types'
type FieldListContainerProps = {
className?: string
inputFields: InputVar[]
onListSortChange: (list: SortableItem[]) => void
onRemoveField: (id: string) => void
onEditField: (id: string) => void
readonly?: boolean
}
const FieldListContainer = ({
className,
inputFields,
onListSortChange,
onRemoveField,
onEditField,
readonly,
}: FieldListContainerProps) => {
const list = useMemo(() => {
return inputFields.map((content) => {
return ({
id: content.variable,
name: content.variable,
})
})
}, [inputFields])
return (
<ReactSortable<SortableItem>
className={cn(className)}
list={list}
setList={onListSortChange}
handle='.handle'
ghostClass='opacity-50'
animation={150}
disabled={readonly}
>
{inputFields?.map((item, index) => (
<FieldItem
key={index}
readonly={readonly}
payload={item}
onRemove={onRemoveField}
onClickEdit={onEditField}
/>
))}
</ReactSortable>
)
}
export default memo(FieldListContainer)

View File

@ -0,0 +1,69 @@
import {
useCallback,
useRef,
useState,
} from 'react'
import { produce } from 'immer'
import type { InputVar } from '@/models/pipeline'
import type { SortableItem } from './types'
export const useFieldList = (
initialInputFields: InputVar[],
onInputFieldsChange: (value: InputVar[]) => void,
) => {
const [inputFields, setInputFields] = useState<InputVar[]>(initialInputFields)
const inputFieldsRef = useRef<InputVar[]>(inputFields)
const handleInputFieldsChange = useCallback((newInputFields: InputVar[]) => {
setInputFields(newInputFields)
inputFieldsRef.current = newInputFields
onInputFieldsChange(newInputFields)
}, [onInputFieldsChange])
const handleListSortChange = useCallback((list: SortableItem[]) => {
const newInputFields = list.map((item) => {
return inputFieldsRef.current.find(field => field.variable === item.name)
})
handleInputFieldsChange(newInputFields as InputVar[])
}, [handleInputFieldsChange])
const [editingField, setEditingField] = useState<InputVar | undefined>()
const [showInputFieldEditor, setShowInputFieldEditor] = useState(false)
const handleOpenInputFieldEditor = useCallback((id?: string) => {
const fieldToEdit = inputFieldsRef.current.find(field => field.variable === id)
setEditingField(fieldToEdit)
setShowInputFieldEditor(true)
}, [])
const handleCancelInputFieldEditor = useCallback(() => {
setShowInputFieldEditor(false)
setEditingField(undefined)
}, [])
const handleRemoveField = useCallback((id: string) => {
const newInputFields = inputFieldsRef.current.filter(field => field.variable !== id)
handleInputFieldsChange(newInputFields)
}, [handleInputFieldsChange])
const handleSubmitField = useCallback((data: InputVar) => {
const newInputFields = produce(inputFieldsRef.current, (draft) => {
const currentIndex = draft.findIndex(field => field.variable === data.variable)
if (currentIndex === -1) {
draft.push(data)
return
}
draft[currentIndex] = data
})
handleInputFieldsChange(newInputFields)
}, [handleInputFieldsChange])
return {
inputFields,
handleInputFieldsChange,
handleListSortChange,
handleRemoveField,
handleSubmitField,
editingField,
showInputFieldEditor,
handleOpenInputFieldEditor,
handleCancelInputFieldEditor,
}
}

View File

@ -1,11 +1,10 @@
import { RiAddLine } from '@remixicon/react'
import FieldItem from './field-item'
import cn from '@/utils/classnames'
import { useCallback, useMemo, useState } from 'react'
import InputFieldEditor from '../editor'
import { ReactSortable } from 'react-sortablejs'
import produce from 'immer'
import type { InputVar } from '@/models/pipeline'
import ActionButton from '@/app/components/base/action-button'
import { useFieldList } from './hooks'
import FieldListContainer from './field-list-container'
type FieldListProps = {
LabelRightContent: React.ReactNode
@ -17,62 +16,21 @@ type FieldListProps = {
const FieldList = ({
LabelRightContent,
inputFields,
handleInputFieldsChange,
inputFields: initialInputFields,
handleInputFieldsChange: onInputFieldsChange,
readonly,
labelClassName,
}: FieldListProps) => {
const [showInputFieldEditor, setShowInputFieldEditor] = useState(false)
const [currentIndex, setCurrentIndex] = useState<number>(-1)
const [currentInputField, setCurrentInputField] = useState<InputVar>()
const optionList = useMemo(() => {
return inputFields.map((content, index) => {
return ({
id: index,
name: content.variable,
})
})
}, [inputFields])
const handleListSortChange = useCallback((list: Array<{ id: number, name: string }>) => {
const newInputFields = list.map((item) => {
return inputFields.find(field => field.variable === item.name)
})
handleInputFieldsChange(newInputFields as InputVar[])
}, [handleInputFieldsChange, inputFields])
const handleRemoveField = useCallback((index: number) => {
const newInputFields = inputFields.filter((_, i) => i !== index)
handleInputFieldsChange(newInputFields)
}, [handleInputFieldsChange, inputFields])
const handleAddField = () => {
setCurrentIndex(-1) // -1 means add new field
setCurrentInputField(undefined)
setShowInputFieldEditor(true)
}
const handleEditField = useCallback((index: number) => {
setCurrentIndex(index)
setCurrentInputField(inputFields[index])
setShowInputFieldEditor(true)
}, [inputFields])
const handleSubmitChange = useCallback((data: InputVar) => {
const newInputFields = produce(inputFields, (draft) => {
if (currentIndex === -1) {
draft.push(data)
return
}
draft[currentIndex] = data
})
handleInputFieldsChange(newInputFields)
}, [currentIndex, handleInputFieldsChange, inputFields])
const handleCloseEditor = useCallback(() => {
setShowInputFieldEditor(false)
}, [])
const {
inputFields,
handleSubmitField,
handleListSortChange,
handleRemoveField,
handleCancelInputFieldEditor,
handleOpenInputFieldEditor,
showInputFieldEditor,
editingField,
} = useFieldList(initialInputFields, onInputFieldsChange)
return (
<div className='flex flex-col'>
@ -80,41 +38,27 @@ const FieldList = ({
<div className='grow'>
{LabelRightContent}
</div>
<button
type='button'
className='h-6 px-2 py-1 disabled:cursor-not-allowed'
onClick={handleAddField}
<ActionButton
onClick={() => handleOpenInputFieldEditor()}
disabled={readonly}
aria-disabled={readonly}
>
<RiAddLine className='h-4 w-4 text-text-tertiary' />
</button>
</ActionButton>
</div>
<ReactSortable
<FieldListContainer
className='flex flex-col gap-y-1 px-4 pb-2'
list={optionList}
setList={list => handleListSortChange(list)}
handle='.handle'
ghostClass='opacity-50'
animation={150}
disabled={readonly}
>
{inputFields?.map((item, index) => (
<FieldItem
key={index}
readonly={readonly}
payload={item}
onRemove={handleRemoveField.bind(null, index)}
onClickEdit={handleEditField.bind(null, index)}
/>
))}
</ReactSortable>
inputFields={inputFields}
onEditField={handleOpenInputFieldEditor}
onRemoveField={handleRemoveField}
onListSortChange={handleListSortChange}
readonly={readonly}
/>
{showInputFieldEditor && (
<InputFieldEditor
show={showInputFieldEditor}
initialData={currentInputField}
onSubmit={handleSubmitChange}
onClose={handleCloseEditor}
initialData={editingField}
onSubmit={handleSubmitField}
onClose={handleCancelInputFieldEditor}
/>
)}
</div>

View File

@ -0,0 +1,4 @@
export type SortableItem = {
id: string
name: string
}

View File

@ -11,6 +11,7 @@ const metaData = genNodeMetaData({
const nodeDefault: NodeDefault<DataSourceNodeType> = {
metaData,
defaultValue: {
variables: [],
datasource_parameters: {},
datasource_configurations: {},
},

View File

@ -1,6 +1,7 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
import type { InputVar } from '@/models/pipeline'
import type { DataSourceNodeType } from '../types'
export const useConfig = (id: string) => {
@ -28,7 +29,16 @@ export const useConfig = (id: string) => {
})
}, [handleNodeDataUpdate, getNodeData])
const handleInputFieldVariablesChange = useCallback((variables: InputVar[]) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
...nodeData?.data,
variables,
})
}, [handleNodeDataUpdate, getNodeData])
return {
handleFileExtensionsChange,
handleInputFieldVariablesChange,
}
}

View File

@ -1,25 +1,79 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import { RiAddLine } from '@remixicon/react'
import type { DataSourceNodeType } from './types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import { BoxGroupField } from '@/app/components/workflow/nodes/_base/components/layout'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import TagInput from '@/app/components/base/tag-input'
import FieldList from '@/app/components/rag-pipeline/components/input-field/field-list/field-list-container'
import { useFieldList } from '@/app/components/rag-pipeline/components/input-field/field-list/hooks'
import InputFieldEditor from '@/app/components/rag-pipeline/components/input-field/editor'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import { useConfig } from './hooks/use-config'
import { OUTPUT_VARIABLES_MAP } from './constants'
import ActionButton from '@/app/components/base/action-button'
const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
const { t } = useTranslation()
const { nodesReadOnly } = useNodesReadOnly()
const {
variables,
provider_type,
fileExtensions = [],
} = data
const { handleFileExtensionsChange } = useConfig(id)
const {
handleInputFieldVariablesChange,
handleFileExtensionsChange,
} = useConfig(id)
const isLocalFile = provider_type === 'local_file'
const {
inputFields,
handleListSortChange,
handleRemoveField,
handleOpenInputFieldEditor,
showInputFieldEditor,
editingField,
handleSubmitField,
handleCancelInputFieldEditor,
} = useFieldList(variables, handleInputFieldVariablesChange)
return (
<div >
{
!isLocalFile && (
<BoxGroupField
boxGroupProps={{
boxProps: { withBorderBottom: true },
}}
fieldProps={{
fieldTitleProps: {
title: t('workflow.nodes.common.inputVars'),
operation: (
<ActionButton
onClick={(e) => {
e.stopPropagation()
handleOpenInputFieldEditor()
}}
>
<RiAddLine className='h-4 w-4' />
</ActionButton>
),
},
supportCollapse: true,
}}
>
<FieldList
inputFields={inputFields}
readonly={nodesReadOnly}
onListSortChange={handleListSortChange}
onRemoveField={handleRemoveField}
onEditField={handleOpenInputFieldEditor}
/>
</BoxGroupField>
)
}
{
isLocalFile && (
<BoxGroupField
@ -64,6 +118,14 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
)
}
</OutputVars>
{showInputFieldEditor && (
<InputFieldEditor
show={showInputFieldEditor}
initialData={editingField}
onSubmit={handleSubmitField}
onClose={handleCancelInputFieldEditor}
/>
)}
</div>
)
}