feat: add InputField component and integrate into RagPipeline panel

This commit is contained in:
twwu 2025-04-21 16:58:22 +08:00
parent 51165408ed
commit 47af1a9c42
13 changed files with 481 additions and 7 deletions

View File

@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon L">
<g id="Subtract">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3333 1.66667C11.3333 1.29848 11.0348 1 10.6666 1C10.2984 1 9.99992 1.29848 9.99992 1.66667V14.3333C9.99992 14.7015 10.2984 15 10.6666 15C11.0348 15 11.3333 14.7015 11.3333 14.3333V13.3333H12.6666C13.7712 13.3333 14.6666 12.4379 14.6666 11.3333V4.66667C14.6666 3.5621 13.7712 2.66667 12.6666 2.66667H11.3333V1.66667ZM12.6666 12H11.3333V4H12.6666C13.0348 4 13.3333 4.29847 13.3333 4.66667V11.3333C13.3333 11.7015 13.0348 12 12.6666 12Z" fill="#354052"/>
<path d="M8.66658 13.3333V12H3.33325C2.96506 12 2.66659 11.7015 2.66659 11.3333V4.66667C2.66659 4.29848 2.96506 4 3.33325 4H8.66658V2.66667H3.33325C2.22868 2.66667 1.33325 3.5621 1.33325 4.66667V11.3333C1.33325 12.4379 2.22869 13.3333 3.33325 13.3333H8.66658Z" fill="#354052"/>
<path d="M8.66658 5.24892C8.63219 5.24484 8.59703 5.24339 8.56132 5.24478L8.51461 5.24659C7.98481 5.26717 7.48663 5.51351 7.15247 5.92673L6.57985 6.63483L6.44192 6.22222C6.26445 5.69137 5.75558 5.35376 5.20721 5.37506L4.4811 5.40327C4.11319 5.41756 3.82653 5.7274 3.84082 6.09531C3.85511 6.46322 4.16494 6.74989 4.53286 6.7356L5.19902 6.70972L5.58518 7.86484L4.47727 9.23487C4.38815 9.34509 4.25098 9.41522 4.10014 9.42108L4.05343 9.4229C3.68552 9.43719 3.39885 9.74702 3.41314 10.1149C3.42743 10.4828 3.73727 10.7695 4.10518 10.7552L4.15189 10.7534C4.68169 10.7328 5.17987 10.4865 5.51403 10.0733L6.08671 9.3651L6.22466 9.77778C6.40213 10.3086 6.911 10.6462 7.45937 10.6249C7.46692 10.6246 7.47448 10.6242 7.48202 10.6237L8.66658 10.5372V9.20033L7.46676 9.2879L7.08138 8.13509L8.18923 6.76513C8.27836 6.65491 8.41553 6.58478 8.56637 6.57892L8.61307 6.5771C8.63111 6.5764 8.64896 6.57499 8.66658 6.5729V5.24892Z" fill="#354052"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,64 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Icon L"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Subtract"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M11.3333 1.66667C11.3333 1.29848 11.0348 1 10.6666 1C10.2984 1 9.99992 1.29848 9.99992 1.66667V14.3333C9.99992 14.7015 10.2984 15 10.6666 15C11.0348 15 11.3333 14.7015 11.3333 14.3333V13.3333H12.6666C13.7712 13.3333 14.6666 12.4379 14.6666 11.3333V4.66667C14.6666 3.5621 13.7712 2.66667 12.6666 2.66667H11.3333V1.66667ZM12.6666 12H11.3333V4H12.6666C13.0348 4 13.3333 4.29847 13.3333 4.66667V11.3333C13.3333 11.7015 13.0348 12 12.6666 12Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M8.66658 13.3333V12H3.33325C2.96506 12 2.66659 11.7015 2.66659 11.3333V4.66667C2.66659 4.29848 2.96506 4 3.33325 4H8.66658V2.66667H3.33325C2.22868 2.66667 1.33325 3.5621 1.33325 4.66667V11.3333C1.33325 12.4379 2.22869 13.3333 3.33325 13.3333H8.66658Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M8.66658 5.24892C8.63219 5.24484 8.59703 5.24339 8.56132 5.24478L8.51461 5.24659C7.98481 5.26717 7.48663 5.51351 7.15247 5.92673L6.57985 6.63483L6.44192 6.22222C6.26445 5.69137 5.75558 5.35376 5.20721 5.37506L4.4811 5.40327C4.11319 5.41756 3.82653 5.7274 3.84082 6.09531C3.85511 6.46322 4.16494 6.74989 4.53286 6.7356L5.19902 6.70972L5.58518 7.86484L4.47727 9.23487C4.38815 9.34509 4.25098 9.41522 4.10014 9.42108L4.05343 9.4229C3.68552 9.43719 3.39885 9.74702 3.41314 10.1149C3.42743 10.4828 3.73727 10.7695 4.10518 10.7552L4.15189 10.7534C4.68169 10.7328 5.17987 10.4865 5.51403 10.0733L6.08671 9.3651L6.22466 9.77778C6.40213 10.3086 6.911 10.6462 7.45937 10.6249C7.46692 10.6246 7.47448 10.6242 7.48202 10.6237L8.66658 10.5372V9.20033L7.46676 9.2879L7.08138 8.13509L8.18923 6.76513C8.27836 6.65491 8.41553 6.58478 8.56637 6.57892L8.61307 6.5771C8.63111 6.5764 8.64896 6.57499 8.66658 6.5729V5.24892Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "InputField"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './InputField.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'InputField'
export default Icon

View File

@ -0,0 +1 @@
export { default as InputField } from './InputField'

View File

@ -0,0 +1,36 @@
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 />
)
}
</>
)
}
const RagPipelinePanel = () => {
const panelProps: PanelProps = useMemo(() => {
return {
components: {
left: null,
right: <RagPipelinePanelOnRight />,
},
}
}, [])
return (
<Panel {...panelProps} />
)
}
export default RagPipelinePanel

View File

@ -0,0 +1,38 @@
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,96 @@
'use client'
import React, { useCallback, useRef } from 'react'
import { useHover } from 'ahooks'
import { useTranslation } from 'react-i18next'
import {
RiDeleteBinLine,
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'
import Badge from '@/app/components/base/badge'
type FieldItemProps = {
readonly?: boolean
payload: InputVar
onRemove?: () => void
}
const FieldItem = ({
readonly,
payload,
onRemove = noop,
}: 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
ref={ref}
className={cn(
'flex h-8 cursor-pointer items-center justify-between gap-x-1 rounded-lg border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg py-1 pl-2 shadow-xs hover:shadow-sm',
(!isHovering || readonly) ? 'pr-2.5' : !readonly && 'pr-1',
)}
>
<div className='flex grow basis-0 items-center gap-x-1'>
<InputField className='size-4 text-text-accent' />
<div
title={payload.variable}
className='system-sm-medium max-w-[130px] shrink-0 truncate text-text-secondary'
>
{payload.variable}
</div>
{payload.label && (
<>
<div className='system-xs-regular shrink-0 text-text-quaternary'>·</div>
<div
title={payload.label as string}
className='system-xs-medium max-w-[130px] truncate text-text-tertiary'
>
{payload.label as string}
</div>
</>
)}
</div>
{(!isHovering || readonly)
? (
<div className='flex shrink-0 items-center gap-x-2'>
{payload.required && (
<Badge>{t('workflow.nodes.start.required')}</Badge>
)}
<InputVarTypeIcon type={payload.type} className='h-3 w-3 text-text-tertiary' />
</div>
)
: (!readonly && (
<div className='flex shrink-0 items-center gap-x-1'>
<button
type='button'
className='cursor-pointer rounded-md p-1 hover:bg-state-base-hover'
onClick={showInputFieldEditor}
>
<RiEditLine className='size-4 text-text-tertiary' />
</button>
<button
onClick={onRemove}
className='group cursor-pointer rounded-md p-1 hover:bg-state-destructive-hover'
>
<RiDeleteBinLine className='size-4 text-text-tertiary group-hover:text-text-destructive' />
</button>
</div>
))
}
</div>
)
}
export default React.memo(FieldItem)

View File

@ -0,0 +1,61 @@
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'
type FieldListProps = {
LabelRightContent: React.ReactNode
inputFields?: InputVar[]
readonly?: boolean
labelClassName?: string
}
const FieldList = ({
LabelRightContent,
inputFields,
readonly,
labelClassName,
}: FieldListProps) => {
const showInputFieldEditor = useStore(state => state.showInputFieldEditor)
const setShowInputFieldEditor = useStore(state => state.setShowInputFieldEditor)
const isReadonly = readonly || showInputFieldEditor
const handleAddField = () => {
setShowInputFieldEditor?.(true)
}
return (
<div className='flex flex-col'>
<div className={cn('flex items-center gap-x-2 px-4', labelClassName)}>
<div className='grow'>
{LabelRightContent}
</div>
<button
type='button'
className='h-6 px-2 py-1 disabled:cursor-not-allowed'
onClick={handleAddField}
disabled={isReadonly}
aria-disabled={isReadonly}
>
<RiAddLine className='h-4 w-4 text-text-tertiary' />
</button>
</div>
<div className='flex flex-col gap-y-1 px-4 pb-2'>
{inputFields?.map((item, index) => (
<FieldItem
key={index}
readonly={isReadonly}
payload={item}
onRemove={() => {
// Handle remove action
}}
/>
))}
</div>
</div>
)
}
export default FieldList

View File

@ -0,0 +1,13 @@
import React from 'react'
import { RiDragDropLine } from '@remixicon/react'
const FooterTip = () => {
return (
<div className='flex items-center justify-center gap-x-2 py-4 text-text-quaternary'>
<RiDragDropLine className='size-4' />
<span className='system-xs-regular'>Drag to adjust grouping</span>
</div>
)
}
export default React.memo(FooterTip)

View File

@ -0,0 +1,116 @@
import {
memo,
useCallback,
} 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 FooterTip from './footer-tip'
import InputFieldEditor from './editor'
type InputFieldPanelProps = {
readonly?: boolean
}
const InputFieldPanel = ({
readonly = false,
}: InputFieldPanelProps) => {
const showInputFieldEditor = useStore(state => state.showInputFieldEditor)
const setShowInputFieldPanel = useStore(state => state.setShowInputFieldPanel)
const closePanel = useCallback(() => {
setShowInputFieldPanel?.(false)
}, [setShowInputFieldPanel])
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'>
<div className='flex items-center p-4 pb-0'>
<div className='system-xl-semibold grow'>
User input fields
</div>
<button
type='button'
className='flex size-6 shrink-0 items-center justify-center p-0.5'
onClick={closePanel}
>
<RiCloseLine className='size-4 text-text-tertiary' />
</button>
</div>
<div className='system-sm-regular px-4 py-1 text-text-tertiary'>
User input fields are used to define and collect variables required during the pipeline execution process. Users can customize the field type and flexibly configure the input value to meet the needs of different data sources or document processing steps.
</div>
<div className='flex grow flex-col overflow-y-auto'>
{/* Jina Reader Field List */}
<FieldList
LabelRightContent={(
<div className='flex items-center gap-x-1.5'>
<div className='flex size-5 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default'>
<Jina className='size-3.5' />
</div>
<span className='system-sm-medium text-text-secondary'>Jina Reader</span>
</div>
)}
inputFields={[{
variable: 'name',
label: 'name',
type: InputVarType.textInput,
required: true,
max_length: 12,
}]}
readonly={readonly}
labelClassName='pt-2 pb-1'
/>
{/* Firecrawl Field List */}
<FieldList
LabelRightContent={(
<div className='flex items-center gap-x-1.5'>
<div className='flex size-5 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default'>
<span className='text-[14px] leading-[14px]'>🔥</span>
</div>
<span className='system-sm-medium text-text-secondary'>Firecrawl</span>
</div>
)}
inputFields={[{
variable: 'name',
label: 'name',
type: InputVarType.textInput,
required: true,
max_length: 12,
}]}
readonly={readonly}
labelClassName='pt-2 pb-1'
/>
{/* Shared Inputs */}
<FieldList
LabelRightContent={(
<div className='flex items-center gap-x-1'>
<span className='system-sm-medium text-text-secondary'>SHARED INPUTS</span>
<Tooltip
popupContent='Shared Inputs are available to all downstream nodes across data sources. For example, variables like delimiter and maximum chunk length can be uniformly applied when processing documents from multiple sources.'
popupClassName='!w-[300px]'
/>
</div>
)}
inputFields={[{
variable: 'name',
label: 'name',
type: InputVarType.textInput,
required: true,
max_length: 12,
}]}
readonly={readonly}
labelClassName='pt-1 pb-2'
/>
</div>
<FooterTip />
</div>
{showInputFieldEditor && <InputFieldEditor />}
</div>
)
}
export default memo(InputFieldPanel)

View File

@ -1,8 +1,24 @@
import Button from '@/app/components/base/button'
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 handleClick = useCallback(() => {
setShowInputFieldPanel?.(true)
}, [setShowInputFieldPanel])
return (
<Button>Input Field</Button>
<Button
variant='secondary'
className='flex gap-x-0.5'
onClick={handleClick}
>
<InputField className='h-4 w-4' />
<span className='px-0.5'>Input Field</span>
</Button>
)
}

View File

@ -1,7 +1,7 @@
import WorkflowWithDefaultContext, {
WorkflowWithInnerContext,
} from '@/app/components/workflow'
import Panel from '@/app/components/workflow/panel'
import RagPipelinePanel from './components/panel'
import {
WorkflowContextProvider,
} from '@/app/components/workflow/context'
@ -23,7 +23,7 @@ const RagPipeline = () => {
edges={[]}
>
<RagPipelineHeader />
<Panel />
<RagPipelinePanel />
</WorkflowWithInnerContext>
</WorkflowWithDefaultContext>
</WorkflowContextProvider>

View File

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