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 { 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>
)

View File

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

View File

@ -22,7 +22,7 @@ type Option = {
value: string
}
type PureSelectProps = {
export type PureSelectProps = {
options: Option[]
value?: string
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'
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>

View File

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

View File

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

View File

@ -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 />
)
}
</>
)
}

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 { 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>
)

View File

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

View File

@ -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 })),
})