mirror of
				https://github.com/langgenius/dify.git
				synced 2025-10-22 22:48:40 +00:00 
			
		
		
		
	feat: implement input field dialog and related components for rag pipeline
This commit is contained in:
		
							parent
							
								
									e04ae927b6
								
							
						
					
					
						commit
						5b8c43052e
					
				| @ -1,5 +1,6 @@ | ||||
| import cn from '@/utils/classnames' | ||||
| import { useFieldContext } from '../..' | ||||
| import type { PureSelectProps } from '../../../select/pure' | ||||
| import PureSelect from '../../../select/pure' | ||||
| import Label from '../label' | ||||
| import { useCallback } from 'react' | ||||
| @ -18,7 +19,7 @@ type SelectFieldProps = { | ||||
|   tooltip?: string | ||||
|   className?: string | ||||
|   labelClassName?: string | ||||
| } | ||||
| } & Omit<PureSelectProps, 'options' | 'value' | 'onChange'> | ||||
| 
 | ||||
| const SelectField = ({ | ||||
|   label, | ||||
| @ -29,6 +30,7 @@ const SelectField = ({ | ||||
|   tooltip, | ||||
|   className, | ||||
|   labelClassName, | ||||
|   ...selectProps | ||||
| }: SelectFieldProps) => { | ||||
|   const field = useFieldContext<string>() | ||||
| 
 | ||||
| @ -51,6 +53,7 @@ const SelectField = ({ | ||||
|         value={field.state.value} | ||||
|         options={options} | ||||
|         onChange={handleChange} | ||||
|         {...selectProps} | ||||
|       /> | ||||
|     </div> | ||||
|   ) | ||||
|  | ||||
| @ -103,6 +103,9 @@ const InputFieldForm = ({ | ||||
|               label={t('appDebug.variableConfig.fieldType')} | ||||
|               options={inputTypes} | ||||
|               onChange={handleTypeChange} | ||||
|               popupProps={{ | ||||
|                 wrapperClassName: 'z-40', | ||||
|               }} | ||||
|             /> | ||||
|           )} | ||||
|         /> | ||||
|  | ||||
| @ -22,7 +22,7 @@ type Option = { | ||||
|   value: string | ||||
| } | ||||
| 
 | ||||
| type PureSelectProps = { | ||||
| export type PureSelectProps = { | ||||
|   options: Option[] | ||||
|   value?: string | ||||
|   onChange?: (value: string) => void | ||||
|  | ||||
| @ -0,0 +1,55 @@ | ||||
| import { Fragment, useCallback } from 'react' | ||||
| import type { ReactNode } from 'react' | ||||
| import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react' | ||||
| import cn from '@/utils/classnames' | ||||
| 
 | ||||
| type DialogWrapperProps = { | ||||
|   className?: string | ||||
|   panelWrapperClassName?: string | ||||
|   children: ReactNode | ||||
|   show: boolean | ||||
|   onClose?: () => void | ||||
| } | ||||
| 
 | ||||
| const DialogWrapper = ({ | ||||
|   className, | ||||
|   panelWrapperClassName, | ||||
|   children, | ||||
|   show, | ||||
|   onClose, | ||||
| }: DialogWrapperProps) => { | ||||
|   const close = useCallback(() => onClose?.(), [onClose]) | ||||
|   return ( | ||||
|     <Transition appear show={show} as={Fragment}> | ||||
|       <Dialog as='div' className='relative z-40' onClose={close}> | ||||
|         <TransitionChild> | ||||
|           <div className={cn( | ||||
|             'fixed inset-0 bg-black/25', | ||||
|             'data-[closed]:opacity-0', | ||||
|             'data-[enter]:opacity-100 data-[enter]:duration-300 data-[enter]:ease-out', | ||||
|             'data-[leave]:opacity-0 data-[leave]:duration-200 data-[leave]:ease-in', | ||||
|           )} /> | ||||
|         </TransitionChild> | ||||
| 
 | ||||
|         <div className='fixed inset-0'> | ||||
|           <div className={cn('flex min-h-full flex-col items-end justify-center pb-1 pt-[116px]', panelWrapperClassName)}> | ||||
|             <TransitionChild> | ||||
|               <DialogPanel className={cn( | ||||
|                 'relative flex w-[420px] grow flex-col overflow-hidden border-components-panel-border bg-components-panel-bg-alt p-0 shadow-xl shadow-shadow-shadow-5 transition-all', | ||||
|                 'rounded-l-2xl border-y-[0.5px] border-l-[0.5px]', | ||||
|                 'data-[closed]:scale-95  data-[closed]:opacity-0', | ||||
|                 'data-[enter]:scale-100 data-[enter]:opacity-100 data-[enter]:duration-300 data-[enter]:ease-out', | ||||
|                 'data-[leave]:scale-95 data-[leave]:opacity-0 data-[leave]:duration-200 data-[leave]:ease-in', | ||||
|                 className, | ||||
|               )}> | ||||
|                 {children} | ||||
|               </DialogPanel> | ||||
|             </TransitionChild> | ||||
|           </div> | ||||
|         </div> | ||||
|       </Dialog> | ||||
|     </Transition > | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export default DialogWrapper | ||||
| @ -0,0 +1,49 @@ | ||||
| import InputFieldForm from '@/app/components/base/form/form-scenarios/input-field' | ||||
| import { RiCloseLine } from '@remixicon/react' | ||||
| import DialogWrapper from './dialog-wrapper' | ||||
| import type { InputVar } from '@/app/components/workflow/types' | ||||
| 
 | ||||
| type InputFieldEditorProps = { | ||||
|   show: boolean | ||||
|   onClose: () => void | ||||
|   initialData?: InputVar | ||||
| } | ||||
| 
 | ||||
| const InputFieldEditor = ({ | ||||
|   show, | ||||
|   onClose, | ||||
|   initialData, | ||||
| }: InputFieldEditorProps) => { | ||||
|   return ( | ||||
|     <DialogWrapper | ||||
|       show={show} | ||||
|       onClose={onClose} | ||||
|       panelWrapperClassName='pr-[424px] justify-start' | ||||
|       className='w-[400px] grow-0 rounded-2xl border-[0.5px] bg-components-panel-bg shadow-shadow-shadow-9' | ||||
|     > | ||||
|       <div className='relative flex h-fit flex-col'> | ||||
|         <div className='system-xl-semibold flex items-center pb-1 pl-4 pr-11 pt-3.5 text-text-primary'> | ||||
|           Add Input Field | ||||
|         </div> | ||||
|         <button | ||||
|           type='button' | ||||
|           className='absolute right-2.5 top-2.5 flex size-8 items-center justify-center' | ||||
|           onClick={onClose} | ||||
|         > | ||||
|           <RiCloseLine className='size-4 text-text-tertiary' /> | ||||
|         </button> | ||||
|         <InputFieldForm | ||||
|           initialData={initialData} | ||||
|           supportFile | ||||
|           onCancel={onClose} | ||||
|           onSubmit={(value) => { | ||||
|             console.log('submit', value) | ||||
|             onClose() | ||||
|           }} | ||||
|         /> | ||||
|       </div> | ||||
|     </DialogWrapper> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export default InputFieldEditor | ||||
| @ -1,14 +1,13 @@ | ||||
| 'use client' | ||||
| import React, { useCallback, useRef } from 'react' | ||||
| import React, { useRef } from 'react' | ||||
| import { useHover } from 'ahooks' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { | ||||
|   RiDeleteBinLine, | ||||
|   RiDraggable, | ||||
|   RiEditLine, | ||||
| } from '@remixicon/react' | ||||
| import type { InputVar } from '@/app/components/workflow/types' | ||||
| import { noop } from 'lodash-es' | ||||
| import { useStore } from '@/app/components/workflow/store' | ||||
| import { InputField } from '@/app/components/base/icons/src/public/pipeline' | ||||
| import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon' | ||||
| import cn from '@/utils/classnames' | ||||
| @ -17,23 +16,20 @@ import Badge from '@/app/components/base/badge' | ||||
| type FieldItemProps = { | ||||
|   readonly?: boolean | ||||
|   payload: InputVar | ||||
|   onRemove?: () => void | ||||
|   onClickEdit: () => void | ||||
|   onRemove: () => void | ||||
| } | ||||
| 
 | ||||
| const FieldItem = ({ | ||||
|   readonly, | ||||
|   payload, | ||||
|   onRemove = noop, | ||||
|   onClickEdit, | ||||
|   onRemove, | ||||
| }: FieldItemProps) => { | ||||
|   const { t } = useTranslation() | ||||
| 
 | ||||
|   const ref = useRef(null) | ||||
|   const isHovering = useHover(ref) | ||||
|   const setShowInputFieldEditor = useStore(state => state.setShowInputFieldEditor) | ||||
| 
 | ||||
|   const showInputFieldEditor = useCallback(() => { | ||||
|     setShowInputFieldEditor?.(true) | ||||
|   }, [setShowInputFieldEditor]) | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
| @ -44,7 +40,11 @@ const FieldItem = ({ | ||||
|       )} | ||||
|     > | ||||
|       <div className='flex grow basis-0 items-center gap-x-1'> | ||||
|         <InputField className='size-4 text-text-accent' /> | ||||
|         { | ||||
|           isHovering | ||||
|             ? <RiDraggable className='handle h-4 w-4 cursor-all-scroll text-text-quaternary' /> | ||||
|             : <InputField className='size-4 text-text-accent' /> | ||||
|         } | ||||
|         <div | ||||
|           title={payload.variable} | ||||
|           className='system-sm-medium max-w-[130px] shrink-0 truncate text-text-secondary' | ||||
| @ -77,7 +77,7 @@ const FieldItem = ({ | ||||
|             <button | ||||
|               type='button' | ||||
|               className='cursor-pointer rounded-md p-1 hover:bg-state-base-hover' | ||||
|               onClick={showInputFieldEditor} | ||||
|               onClick={onClickEdit} | ||||
|             > | ||||
|               <RiEditLine className='size-4 text-text-tertiary' /> | ||||
|             </button> | ||||
| @ -1,8 +1,9 @@ | ||||
| import { useStore } from '@/app/components/workflow/store' | ||||
| import type { InputVar } from '@/app/components/workflow/types' | ||||
| import { RiAddLine } from '@remixicon/react' | ||||
| import FieldItem from './field-item' | ||||
| import cn from '@/utils/classnames' | ||||
| import { useState } from 'react' | ||||
| import InputFieldEditor from '../editor' | ||||
| 
 | ||||
| type FieldListProps = { | ||||
|   LabelRightContent: React.ReactNode | ||||
| @ -17,13 +18,18 @@ const FieldList = ({ | ||||
|   readonly, | ||||
|   labelClassName, | ||||
| }: FieldListProps) => { | ||||
|   const showInputFieldEditor = useStore(state => state.showInputFieldEditor) | ||||
|   const setShowInputFieldEditor = useStore(state => state.setShowInputFieldEditor) | ||||
| 
 | ||||
|   const isReadonly = readonly || showInputFieldEditor | ||||
|   const [showInputFieldEditor, setShowInputFieldEditor] = useState(false) | ||||
| 
 | ||||
|   const handleAddField = () => { | ||||
|     setShowInputFieldEditor?.(true) | ||||
|     setShowInputFieldEditor(true) | ||||
|   } | ||||
| 
 | ||||
|   const handleEditField = (index: number) => { | ||||
|     setShowInputFieldEditor(true) | ||||
|   } | ||||
| 
 | ||||
|   const handleCloseEditor = () => { | ||||
|     setShowInputFieldEditor(false) | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
| @ -36,8 +42,8 @@ const FieldList = ({ | ||||
|           type='button' | ||||
|           className='h-6 px-2 py-1 disabled:cursor-not-allowed' | ||||
|           onClick={handleAddField} | ||||
|           disabled={isReadonly} | ||||
|           aria-disabled={isReadonly} | ||||
|           disabled={readonly} | ||||
|           aria-disabled={readonly} | ||||
|         > | ||||
|           <RiAddLine className='h-4 w-4 text-text-tertiary' /> | ||||
|         </button> | ||||
| @ -46,14 +52,21 @@ const FieldList = ({ | ||||
|         {inputFields?.map((item, index) => ( | ||||
|           <FieldItem | ||||
|             key={index} | ||||
|             readonly={isReadonly} | ||||
|             readonly={readonly} | ||||
|             payload={item} | ||||
|             onRemove={() => { | ||||
|               // Handle remove action
 | ||||
|             }} | ||||
|             onClickEdit={handleEditField.bind(null, index)} | ||||
|           /> | ||||
|         ))} | ||||
|       </div> | ||||
|       {showInputFieldEditor && ( | ||||
|         <InputFieldEditor | ||||
|           show={showInputFieldEditor} | ||||
|           onClose={handleCloseEditor} | ||||
|         /> | ||||
|       )} | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| @ -4,31 +4,35 @@ import { | ||||
| } from 'react' | ||||
| import { useStore } from '@/app/components/workflow/store' | ||||
| import { RiCloseLine } from '@remixicon/react' | ||||
| import FieldList from './field-list' | ||||
| import { Jina } from '@/app/components/base/icons/src/public/llm' | ||||
| import { InputVarType } from '@/app/components/workflow/types' | ||||
| import Tooltip from '@/app/components/base/tooltip' | ||||
| import DialogWrapper from './dialog-wrapper' | ||||
| import FieldList from './field-list' | ||||
| import FooterTip from './footer-tip' | ||||
| import InputFieldEditor from './editor' | ||||
| 
 | ||||
| type InputFieldPanelProps = { | ||||
| type InputFieldDialogProps = { | ||||
|   readonly?: boolean | ||||
| } | ||||
| 
 | ||||
| const InputFieldPanel = ({ | ||||
| const InputFieldDialog = ({ | ||||
|   readonly = false, | ||||
| }: InputFieldPanelProps) => { | ||||
|   const showInputFieldEditor = useStore(state => state.showInputFieldEditor) | ||||
|   const setShowInputFieldPanel = useStore(state => state.setShowInputFieldPanel) | ||||
| }: InputFieldDialogProps) => { | ||||
|   const showInputFieldDialog = useStore(state => state.showInputFieldDialog) | ||||
|   const setShowInputFieldDialog = useStore(state => state.setShowInputFieldDialog) | ||||
| 
 | ||||
|   const closePanel = useCallback(() => { | ||||
|     setShowInputFieldPanel?.(false) | ||||
|   }, [setShowInputFieldPanel]) | ||||
|     setShowInputFieldDialog?.(false) | ||||
|   }, [setShowInputFieldDialog]) | ||||
| 
 | ||||
|   return ( | ||||
|     <div className='flex h-full flex-row-reverse gap-x-1'> | ||||
|       <div className='flex h-full w-[420px] flex-col rounded-l-2xl border-y border-l border-components-panel-border bg-components-panel-bg-alt shadow-xl shadow-shadow-shadow-5'> | ||||
|     <DialogWrapper | ||||
|       show={!!showInputFieldDialog} | ||||
|       onClose={closePanel} | ||||
|     > | ||||
|       <div className='flex grow flex-col'> | ||||
|         <div className='flex items-center p-4 pb-0'> | ||||
|           {/* // TODO: i18n */} | ||||
|           <div className='system-xl-semibold grow'> | ||||
|             User input fields | ||||
|           </div> | ||||
| @ -60,6 +64,11 @@ const InputFieldPanel = ({ | ||||
|               type: InputVarType.textInput, | ||||
|               required: true, | ||||
|               max_length: 12, | ||||
|             }, { | ||||
|               variable: 'num', | ||||
|               label: 'num', | ||||
|               type: InputVarType.number, | ||||
|               required: true, | ||||
|             }]} | ||||
|             readonly={readonly} | ||||
|             labelClassName='pt-2 pb-1' | ||||
| @ -108,9 +117,8 @@ const InputFieldPanel = ({ | ||||
|         </div> | ||||
|         <FooterTip /> | ||||
|       </div> | ||||
|       {showInputFieldEditor && <InputFieldEditor />} | ||||
|     </div> | ||||
|     </DialogWrapper> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export default memo(InputFieldPanel) | ||||
| export default memo(InputFieldDialog) | ||||
| @ -1,19 +1,10 @@ | ||||
| import { useStore } from '@/app/components/workflow/store' | ||||
| import InputField from './input-field' | ||||
| import { useMemo } from 'react' | ||||
| import type { PanelProps } from '@/app/components/workflow/panel' | ||||
| import Panel from '@/app/components/workflow/panel' | ||||
| 
 | ||||
| const RagPipelinePanelOnRight = () => { | ||||
|   const showInputField = useStore(s => s.showInputFieldPanel) | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       { | ||||
|         showInputField && ( | ||||
|           <InputField /> | ||||
|         ) | ||||
|       } | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -1,38 +0,0 @@ | ||||
| import { useStore } from '@/app/components/workflow/store' | ||||
| import InputFieldForm from '@/app/components/base/form/form-scenarios/input-field' | ||||
| import { useCallback } from 'react' | ||||
| import { RiCloseLine } from '@remixicon/react' | ||||
| 
 | ||||
| const InputFieldEditor = () => { | ||||
|   const setShowInputFieldEditor = useStore(state => state.setShowInputFieldEditor) | ||||
| 
 | ||||
|   const closeEditor = useCallback(() => { | ||||
|     setShowInputFieldEditor?.(false) | ||||
|   }, [setShowInputFieldEditor]) | ||||
| 
 | ||||
|   return ( | ||||
|     <div className='relative flex h-fit w-[400px] flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-2xl shadow-shadow-shadow-9'> | ||||
|       <div className='system-xl-semibold flex items-center pb-1 pl-4 pr-11 pt-3.5 text-text-primary'> | ||||
|         Add Input Field | ||||
|       </div> | ||||
|       <button | ||||
|         type='button' | ||||
|         className='absolute right-2.5 top-2.5 flex size-8 items-center justify-center' | ||||
|         onClick={closeEditor} | ||||
|       > | ||||
|         <RiCloseLine className='size-4 text-text-tertiary' /> | ||||
|       </button> | ||||
|       <InputFieldForm | ||||
|         initialData={undefined} | ||||
|         supportFile | ||||
|         onCancel={closeEditor} | ||||
|         onSubmit={(value) => { | ||||
|           console.log('submit', value) | ||||
|           closeEditor() | ||||
|         }} | ||||
|       /> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export default InputFieldEditor | ||||
| @ -0,0 +1,20 @@ | ||||
| import { useStore } from '../../workflow/store' | ||||
| import InputField from './input-field' | ||||
| import RagPipelinePanel from './panel' | ||||
| import RagPipelineHeader from './rag-pipeline-header' | ||||
| 
 | ||||
| const RagPipelineChildren = () => { | ||||
|   const showInputFieldDialog = useStore(state => state.showInputFieldDialog) | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <RagPipelineHeader /> | ||||
|       <RagPipelinePanel /> | ||||
|       { | ||||
|         showInputFieldDialog && (<InputField />) | ||||
|       } | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export default RagPipelineChildren | ||||
| @ -3,12 +3,11 @@ import { InputField } from '@/app/components/base/icons/src/public/pipeline' | ||||
| import { useStore } from '@/app/components/workflow/store' | ||||
| import { useCallback } from 'react' | ||||
| 
 | ||||
| // TODO: i18n
 | ||||
| const InputFieldButton = () => { | ||||
|   const setShowInputFieldPanel = useStore(state => state.setShowInputFieldPanel) | ||||
|   const setShowInputFieldDialog = useStore(state => state.setShowInputFieldDialog) | ||||
|   const handleClick = useCallback(() => { | ||||
|     setShowInputFieldPanel?.(true) | ||||
|   }, [setShowInputFieldPanel]) | ||||
|     setShowInputFieldDialog?.(true) | ||||
|   }, [setShowInputFieldDialog]) | ||||
| 
 | ||||
|   return ( | ||||
|     <Button | ||||
| @ -17,6 +16,7 @@ const InputFieldButton = () => { | ||||
|       onClick={handleClick} | ||||
|     > | ||||
|       <InputField className='h-4 w-4' /> | ||||
|       {/* // TODO: i18n */} | ||||
|       <span className='px-0.5'>Input Field</span> | ||||
|     </Button> | ||||
|   ) | ||||
|  | ||||
| @ -1,13 +1,12 @@ | ||||
| import WorkflowWithDefaultContext, { | ||||
|   WorkflowWithInnerContext, | ||||
| } from '@/app/components/workflow' | ||||
| import RagPipelinePanel from './components/panel' | ||||
| import { | ||||
|   WorkflowContextProvider, | ||||
| } from '@/app/components/workflow/context' | ||||
| import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store' | ||||
| import RagPipelineHeader from './components/rag-pipeline-header' | ||||
| import { createRagPipelineSliceSlice } from './store' | ||||
| import RagPipelineChildren from './components/rag-pipeline-children' | ||||
| 
 | ||||
| const RagPipeline = () => { | ||||
|   return ( | ||||
| @ -22,8 +21,7 @@ const RagPipeline = () => { | ||||
|           nodes={[]} | ||||
|           edges={[]} | ||||
|         > | ||||
|           <RagPipelineHeader /> | ||||
|           <RagPipelinePanel /> | ||||
|           <RagPipelineChildren /> | ||||
|         </WorkflowWithInnerContext> | ||||
|       </WorkflowWithDefaultContext> | ||||
|     </WorkflowContextProvider> | ||||
|  | ||||
| @ -1,20 +1,16 @@ | ||||
| import type { StateCreator } from 'zustand' | ||||
| 
 | ||||
| export type RagPipelineSliceShape = { | ||||
|   showInputFieldEditor: boolean | ||||
|   setShowInputFieldEditor: (showInputFieldDialog: boolean) => void | ||||
|   showInputFieldPanel: boolean | ||||
|   setShowInputFieldPanel: (showInputFieldPanel: boolean) => void | ||||
|   showInputFieldDialog: boolean | ||||
|   setShowInputFieldDialog: (showInputFieldPanel: boolean) => void | ||||
|   nodesDefaultConfigs: Record<string, any> | ||||
|   setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void | ||||
| } | ||||
| 
 | ||||
| export type CreateRagPipelineSliceSlice = StateCreator<RagPipelineSliceShape> | ||||
| export const createRagPipelineSliceSlice: StateCreator<RagPipelineSliceShape> = set => ({ | ||||
|   showInputFieldEditor: false, | ||||
|   setShowInputFieldEditor: showInputFieldEditor => set(() => ({ showInputFieldEditor })), | ||||
|   showInputFieldPanel: false, | ||||
|   setShowInputFieldPanel: showInputFieldPanel => set(() => ({ showInputFieldPanel })), | ||||
|   showInputFieldDialog: false, | ||||
|   setShowInputFieldDialog: showInputFieldDialog => set(() => ({ showInputFieldDialog })), | ||||
|   nodesDefaultConfigs: {}, | ||||
|   setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })), | ||||
| }) | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 twwu
						twwu