feat: implement input field dialog and related components for rag pipeline

This commit is contained in:
twwu 2025-04-22 11:29:03 +08:00
parent e04ae927b6
commit 5b8c43052e
15 changed files with 198 additions and 100 deletions

View File

@ -1,5 +1,6 @@
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useFieldContext } from '../..' import { useFieldContext } from '../..'
import type { PureSelectProps } from '../../../select/pure'
import PureSelect from '../../../select/pure' import PureSelect from '../../../select/pure'
import Label from '../label' import Label from '../label'
import { useCallback } from 'react' import { useCallback } from 'react'
@ -18,7 +19,7 @@ type SelectFieldProps = {
tooltip?: string tooltip?: string
className?: string className?: string
labelClassName?: string labelClassName?: string
} } & Omit<PureSelectProps, 'options' | 'value' | 'onChange'>
const SelectField = ({ const SelectField = ({
label, label,
@ -29,6 +30,7 @@ const SelectField = ({
tooltip, tooltip,
className, className,
labelClassName, labelClassName,
...selectProps
}: SelectFieldProps) => { }: SelectFieldProps) => {
const field = useFieldContext<string>() const field = useFieldContext<string>()
@ -51,6 +53,7 @@ const SelectField = ({
value={field.state.value} value={field.state.value}
options={options} options={options}
onChange={handleChange} onChange={handleChange}
{...selectProps}
/> />
</div> </div>
) )

View File

@ -103,6 +103,9 @@ const InputFieldForm = ({
label={t('appDebug.variableConfig.fieldType')} label={t('appDebug.variableConfig.fieldType')}
options={inputTypes} options={inputTypes}
onChange={handleTypeChange} onChange={handleTypeChange}
popupProps={{
wrapperClassName: 'z-40',
}}
/> />
)} )}
/> />

View File

@ -22,7 +22,7 @@ type Option = {
value: string value: string
} }
type PureSelectProps = { export type PureSelectProps = {
options: Option[] options: Option[]
value?: string value?: string
onChange?: (value: string) => void onChange?: (value: string) => void

View File

@ -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

View File

@ -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

View File

@ -1,14 +1,13 @@
'use client' 'use client'
import React, { useCallback, useRef } from 'react' import React, { useRef } from 'react'
import { useHover } from 'ahooks' import { useHover } from 'ahooks'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
RiDraggable,
RiEditLine, RiEditLine,
} from '@remixicon/react' } from '@remixicon/react'
import type { InputVar } from '@/app/components/workflow/types' 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 { InputField } from '@/app/components/base/icons/src/public/pipeline'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon' import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -17,23 +16,20 @@ import Badge from '@/app/components/base/badge'
type FieldItemProps = { type FieldItemProps = {
readonly?: boolean readonly?: boolean
payload: InputVar payload: InputVar
onRemove?: () => void onClickEdit: () => void
onRemove: () => void
} }
const FieldItem = ({ const FieldItem = ({
readonly, readonly,
payload, payload,
onRemove = noop, onClickEdit,
onRemove,
}: FieldItemProps) => { }: FieldItemProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const ref = useRef(null) const ref = useRef(null)
const isHovering = useHover(ref) const isHovering = useHover(ref)
const setShowInputFieldEditor = useStore(state => state.setShowInputFieldEditor)
const showInputFieldEditor = useCallback(() => {
setShowInputFieldEditor?.(true)
}, [setShowInputFieldEditor])
return ( return (
<div <div
@ -44,7 +40,11 @@ const FieldItem = ({
)} )}
> >
<div className='flex grow basis-0 items-center gap-x-1'> <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 <div
title={payload.variable} title={payload.variable}
className='system-sm-medium max-w-[130px] shrink-0 truncate text-text-secondary' className='system-sm-medium max-w-[130px] shrink-0 truncate text-text-secondary'
@ -77,7 +77,7 @@ const FieldItem = ({
<button <button
type='button' type='button'
className='cursor-pointer rounded-md p-1 hover:bg-state-base-hover' className='cursor-pointer rounded-md p-1 hover:bg-state-base-hover'
onClick={showInputFieldEditor} onClick={onClickEdit}
> >
<RiEditLine className='size-4 text-text-tertiary' /> <RiEditLine className='size-4 text-text-tertiary' />
</button> </button>

View File

@ -1,8 +1,9 @@
import { useStore } from '@/app/components/workflow/store'
import type { InputVar } from '@/app/components/workflow/types' import type { InputVar } from '@/app/components/workflow/types'
import { RiAddLine } from '@remixicon/react' import { RiAddLine } from '@remixicon/react'
import FieldItem from './field-item' import FieldItem from './field-item'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useState } from 'react'
import InputFieldEditor from '../editor'
type FieldListProps = { type FieldListProps = {
LabelRightContent: React.ReactNode LabelRightContent: React.ReactNode
@ -17,13 +18,18 @@ const FieldList = ({
readonly, readonly,
labelClassName, labelClassName,
}: FieldListProps) => { }: FieldListProps) => {
const showInputFieldEditor = useStore(state => state.showInputFieldEditor) const [showInputFieldEditor, setShowInputFieldEditor] = useState(false)
const setShowInputFieldEditor = useStore(state => state.setShowInputFieldEditor)
const isReadonly = readonly || showInputFieldEditor
const handleAddField = () => { const handleAddField = () => {
setShowInputFieldEditor?.(true) setShowInputFieldEditor(true)
}
const handleEditField = (index: number) => {
setShowInputFieldEditor(true)
}
const handleCloseEditor = () => {
setShowInputFieldEditor(false)
} }
return ( return (
@ -36,8 +42,8 @@ const FieldList = ({
type='button' type='button'
className='h-6 px-2 py-1 disabled:cursor-not-allowed' className='h-6 px-2 py-1 disabled:cursor-not-allowed'
onClick={handleAddField} onClick={handleAddField}
disabled={isReadonly} disabled={readonly}
aria-disabled={isReadonly} aria-disabled={readonly}
> >
<RiAddLine className='h-4 w-4 text-text-tertiary' /> <RiAddLine className='h-4 w-4 text-text-tertiary' />
</button> </button>
@ -46,14 +52,21 @@ const FieldList = ({
{inputFields?.map((item, index) => ( {inputFields?.map((item, index) => (
<FieldItem <FieldItem
key={index} key={index}
readonly={isReadonly} readonly={readonly}
payload={item} payload={item}
onRemove={() => { onRemove={() => {
// Handle remove action // Handle remove action
}} }}
onClickEdit={handleEditField.bind(null, index)}
/> />
))} ))}
</div> </div>
{showInputFieldEditor && (
<InputFieldEditor
show={showInputFieldEditor}
onClose={handleCloseEditor}
/>
)}
</div> </div>
) )
} }

View File

@ -4,31 +4,35 @@ import {
} from 'react' } from 'react'
import { useStore } from '@/app/components/workflow/store' import { useStore } from '@/app/components/workflow/store'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import FieldList from './field-list'
import { Jina } from '@/app/components/base/icons/src/public/llm' import { Jina } from '@/app/components/base/icons/src/public/llm'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import DialogWrapper from './dialog-wrapper'
import FieldList from './field-list'
import FooterTip from './footer-tip' import FooterTip from './footer-tip'
import InputFieldEditor from './editor'
type InputFieldPanelProps = { type InputFieldDialogProps = {
readonly?: boolean readonly?: boolean
} }
const InputFieldPanel = ({ const InputFieldDialog = ({
readonly = false, readonly = false,
}: InputFieldPanelProps) => { }: InputFieldDialogProps) => {
const showInputFieldEditor = useStore(state => state.showInputFieldEditor) const showInputFieldDialog = useStore(state => state.showInputFieldDialog)
const setShowInputFieldPanel = useStore(state => state.setShowInputFieldPanel) const setShowInputFieldDialog = useStore(state => state.setShowInputFieldDialog)
const closePanel = useCallback(() => { const closePanel = useCallback(() => {
setShowInputFieldPanel?.(false) setShowInputFieldDialog?.(false)
}, [setShowInputFieldPanel]) }, [setShowInputFieldDialog])
return ( return (
<div className='flex h-full flex-row-reverse gap-x-1'> <DialogWrapper
<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'> show={!!showInputFieldDialog}
onClose={closePanel}
>
<div className='flex grow flex-col'>
<div className='flex items-center p-4 pb-0'> <div className='flex items-center p-4 pb-0'>
{/* // TODO i18n */}
<div className='system-xl-semibold grow'> <div className='system-xl-semibold grow'>
User input fields User input fields
</div> </div>
@ -60,6 +64,11 @@ const InputFieldPanel = ({
type: InputVarType.textInput, type: InputVarType.textInput,
required: true, required: true,
max_length: 12, max_length: 12,
}, {
variable: 'num',
label: 'num',
type: InputVarType.number,
required: true,
}]} }]}
readonly={readonly} readonly={readonly}
labelClassName='pt-2 pb-1' labelClassName='pt-2 pb-1'
@ -108,9 +117,8 @@ const InputFieldPanel = ({
</div> </div>
<FooterTip /> <FooterTip />
</div> </div>
{showInputFieldEditor && <InputFieldEditor />} </DialogWrapper>
</div>
) )
} }
export default memo(InputFieldPanel) export default memo(InputFieldDialog)

View File

@ -1,19 +1,10 @@
import { useStore } from '@/app/components/workflow/store'
import InputField from './input-field'
import { useMemo } from 'react' import { useMemo } from 'react'
import type { PanelProps } from '@/app/components/workflow/panel' import type { PanelProps } from '@/app/components/workflow/panel'
import Panel from '@/app/components/workflow/panel' import Panel from '@/app/components/workflow/panel'
const RagPipelinePanelOnRight = () => { const RagPipelinePanelOnRight = () => {
const showInputField = useStore(s => s.showInputFieldPanel)
return ( return (
<> <>
{
showInputField && (
<InputField />
)
}
</> </>
) )
} }

View File

@ -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

View File

@ -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

View File

@ -3,12 +3,11 @@ import { InputField } from '@/app/components/base/icons/src/public/pipeline'
import { useStore } from '@/app/components/workflow/store' import { useStore } from '@/app/components/workflow/store'
import { useCallback } from 'react' import { useCallback } from 'react'
// TODO: i18n
const InputFieldButton = () => { const InputFieldButton = () => {
const setShowInputFieldPanel = useStore(state => state.setShowInputFieldPanel) const setShowInputFieldDialog = useStore(state => state.setShowInputFieldDialog)
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setShowInputFieldPanel?.(true) setShowInputFieldDialog?.(true)
}, [setShowInputFieldPanel]) }, [setShowInputFieldDialog])
return ( return (
<Button <Button
@ -17,6 +16,7 @@ const InputFieldButton = () => {
onClick={handleClick} onClick={handleClick}
> >
<InputField className='h-4 w-4' /> <InputField className='h-4 w-4' />
{/* // TODO: i18n */}
<span className='px-0.5'>Input Field</span> <span className='px-0.5'>Input Field</span>
</Button> </Button>
) )

View File

@ -1,13 +1,12 @@
import WorkflowWithDefaultContext, { import WorkflowWithDefaultContext, {
WorkflowWithInnerContext, WorkflowWithInnerContext,
} from '@/app/components/workflow' } from '@/app/components/workflow'
import RagPipelinePanel from './components/panel'
import { import {
WorkflowContextProvider, WorkflowContextProvider,
} from '@/app/components/workflow/context' } from '@/app/components/workflow/context'
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store' import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
import RagPipelineHeader from './components/rag-pipeline-header'
import { createRagPipelineSliceSlice } from './store' import { createRagPipelineSliceSlice } from './store'
import RagPipelineChildren from './components/rag-pipeline-children'
const RagPipeline = () => { const RagPipeline = () => {
return ( return (
@ -22,8 +21,7 @@ const RagPipeline = () => {
nodes={[]} nodes={[]}
edges={[]} edges={[]}
> >
<RagPipelineHeader /> <RagPipelineChildren />
<RagPipelinePanel />
</WorkflowWithInnerContext> </WorkflowWithInnerContext>
</WorkflowWithDefaultContext> </WorkflowWithDefaultContext>
</WorkflowContextProvider> </WorkflowContextProvider>

View File

@ -1,20 +1,16 @@
import type { StateCreator } from 'zustand' import type { StateCreator } from 'zustand'
export type RagPipelineSliceShape = { export type RagPipelineSliceShape = {
showInputFieldEditor: boolean showInputFieldDialog: boolean
setShowInputFieldEditor: (showInputFieldDialog: boolean) => void setShowInputFieldDialog: (showInputFieldPanel: boolean) => void
showInputFieldPanel: boolean
setShowInputFieldPanel: (showInputFieldPanel: boolean) => void
nodesDefaultConfigs: Record<string, any> nodesDefaultConfigs: Record<string, any>
setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void
} }
export type CreateRagPipelineSliceSlice = StateCreator<RagPipelineSliceShape> export type CreateRagPipelineSliceSlice = StateCreator<RagPipelineSliceShape>
export const createRagPipelineSliceSlice: StateCreator<RagPipelineSliceShape> = set => ({ export const createRagPipelineSliceSlice: StateCreator<RagPipelineSliceShape> = set => ({
showInputFieldEditor: false, showInputFieldDialog: false,
setShowInputFieldEditor: showInputFieldEditor => set(() => ({ showInputFieldEditor })), setShowInputFieldDialog: showInputFieldDialog => set(() => ({ showInputFieldDialog })),
showInputFieldPanel: false,
setShowInputFieldPanel: showInputFieldPanel => set(() => ({ showInputFieldPanel })),
nodesDefaultConfigs: {}, nodesDefaultConfigs: {},
setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })), setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })),
}) })