diff --git a/web/app/components/rag-pipeline/components/input-field/field-list/field-item.tsx b/web/app/components/rag-pipeline/components/input-field/field-list/field-item.tsx index 8faa5abdd5..080aca33e8 100644 --- a/web/app/components/rag-pipeline/components/input-field/field-list/field-item.tsx +++ b/web/app/components/rag-pipeline/components/input-field/field-list/field-item.tsx @@ -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 (
- - +
) : ( diff --git a/web/app/components/rag-pipeline/components/input-field/field-list/field-list-container.tsx b/web/app/components/rag-pipeline/components/input-field/field-list/field-list-container.tsx new file mode 100644 index 0000000000..c1a082ea98 --- /dev/null +++ b/web/app/components/rag-pipeline/components/input-field/field-list/field-list-container.tsx @@ -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 ( + + className={cn(className)} + list={list} + setList={onListSortChange} + handle='.handle' + ghostClass='opacity-50' + animation={150} + disabled={readonly} + > + {inputFields?.map((item, index) => ( + + ))} + + ) +} + +export default memo(FieldListContainer) diff --git a/web/app/components/rag-pipeline/components/input-field/field-list/hooks.ts b/web/app/components/rag-pipeline/components/input-field/field-list/hooks.ts new file mode 100644 index 0000000000..3a2efdb80b --- /dev/null +++ b/web/app/components/rag-pipeline/components/input-field/field-list/hooks.ts @@ -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(initialInputFields) + const inputFieldsRef = useRef(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() + 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, + } +} diff --git a/web/app/components/rag-pipeline/components/input-field/field-list/index.tsx b/web/app/components/rag-pipeline/components/input-field/field-list/index.tsx index a2cd88c571..dd567818c8 100644 --- a/web/app/components/rag-pipeline/components/input-field/field-list/index.tsx +++ b/web/app/components/rag-pipeline/components/input-field/field-list/index.tsx @@ -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(-1) - const [currentInputField, setCurrentInputField] = useState() - - 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 (
@@ -80,41 +38,27 @@ const FieldList = ({
{LabelRightContent}
- +
- handleListSortChange(list)} - handle='.handle' - ghostClass='opacity-50' - animation={150} - disabled={readonly} - > - {inputFields?.map((item, index) => ( - - ))} - + inputFields={inputFields} + onEditField={handleOpenInputFieldEditor} + onRemoveField={handleRemoveField} + onListSortChange={handleListSortChange} + readonly={readonly} + /> {showInputFieldEditor && ( )} diff --git a/web/app/components/rag-pipeline/components/input-field/field-list/types.ts b/web/app/components/rag-pipeline/components/input-field/field-list/types.ts new file mode 100644 index 0000000000..44e4c20096 --- /dev/null +++ b/web/app/components/rag-pipeline/components/input-field/field-list/types.ts @@ -0,0 +1,4 @@ +export type SortableItem = { + id: string + name: string +} diff --git a/web/app/components/workflow/nodes/data-source/default.ts b/web/app/components/workflow/nodes/data-source/default.ts index 6be0666375..e27ccc155b 100644 --- a/web/app/components/workflow/nodes/data-source/default.ts +++ b/web/app/components/workflow/nodes/data-source/default.ts @@ -11,6 +11,7 @@ const metaData = genNodeMetaData({ const nodeDefault: NodeDefault = { metaData, defaultValue: { + variables: [], datasource_parameters: {}, datasource_configurations: {}, }, diff --git a/web/app/components/workflow/nodes/data-source/hooks/use-config.ts b/web/app/components/workflow/nodes/data-source/hooks/use-config.ts index 66d27dda8c..cec456c8db 100644 --- a/web/app/components/workflow/nodes/data-source/hooks/use-config.ts +++ b/web/app/components/workflow/nodes/data-source/hooks/use-config.ts @@ -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, } } diff --git a/web/app/components/workflow/nodes/data-source/panel.tsx b/web/app/components/workflow/nodes/data-source/panel.tsx index 258f181e4f..c5f738756b 100644 --- a/web/app/components/workflow/nodes/data-source/panel.tsx +++ b/web/app/components/workflow/nodes/data-source/panel.tsx @@ -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> = ({ 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 (
+ { + !isLocalFile && ( + { + e.stopPropagation() + handleOpenInputFieldEditor() + }} + > + + + ), + }, + supportCollapse: true, + }} + > + + + ) + } { isLocalFile && ( > = ({ id, data }) => { ) } + {showInputFieldEditor && ( + + )}
) }