mirror of
https://github.com/langgenius/dify.git
synced 2025-09-03 14:23:48 +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