feat: pass current value to form input

This commit is contained in:
Joel 2025-08-08 14:22:01 +08:00
parent c28720529e
commit a18bcf3957
14 changed files with 240 additions and 45 deletions

View File

@ -26,6 +26,10 @@
@apply p-0.5 w-6 h-6 rounded-lg
}
.action-btn-s {
@apply w-5 h-5 rounded-[6px]
}
.action-btn-xs {
@apply p-0 w-4 h-4 rounded
}

View File

@ -17,6 +17,7 @@ const actionButtonVariants = cva(
variants: {
size: {
xs: 'action-btn-xs',
s: 'action-btn-s',
m: 'action-btn-m',
l: 'action-btn-l',
xl: 'action-btn-xl',

View File

@ -262,7 +262,11 @@ const PromptEditor: FC<PromptEditorProps> = ({
hitlInputBlock?.show && (
<>
<HITLInputBlock />
<HITLInputBlockReplacementBlock />
<HITLInputBlockReplacementBlock
nodeTitle={hitlInputBlock.nodeTitle}
formInputs={hitlInputBlock.formInputs}
onFormInputsChange={hitlInputBlock.onFormInputsChange}
/>
</>
)
}

View File

@ -55,7 +55,7 @@ export default function DraggableBlockPlugin({
menuRef={menuRef as any}
targetLineRef={targetLineRef as any}
menuComponent={
isSupportDrag ? <div ref={menuRef} className={cn(DRAGGABLE_BLOCK_MENU_CLASSNAME, 'absolute right-[10px] top-[18px] cursor-grab opacity-0 will-change-transform active:cursor-move')}>
isSupportDrag ? <div ref={menuRef} className={cn(DRAGGABLE_BLOCK_MENU_CLASSNAME, 'absolute right-[12px] top-[16px] cursor-grab opacity-0 will-change-transform active:cursor-move')}>
<RiDraggable className='size-3.5 text-text-tertiary' />
</div> : null
}

View File

@ -1,24 +1,63 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import React, { useCallback, useEffect, useRef } from 'react'
import { VariableX } from '../../../icons/src/vender/workflow'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { BlockEnum } from '@/app/components/workflow/types'
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
import { Variable02 } from '../../../icons/src/vender/solid/development'
import { useTranslation } from 'react-i18next'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import ActionButton from '../../../action-button'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import InputField from './input-field'
import { useBoolean } from 'ahooks'
import Modal from '../../../modal'
type Props = {
nodeName: string
nodeTitle: string
varName: string
isSelected: boolean
formInput?: FormInputItem
onChange: (input: FormInputItem) => void
}
const ComponentUI: FC<Props> = ({
nodeName,
nodeTitle,
varName,
// isSelected,
formInput = {
type: InputVarType.textInput,
output_variable_name: varName,
placeholder: {
type: 'const',
selector: [],
value: '',
},
},
onChange,
}) => {
const { t } = useTranslation()
const [isShowEditModal, {
setTrue: showEditModal,
setFalse: hideEditModal,
}] = useBoolean(false)
const editBtnRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const editBtn = editBtnRef.current
if (editBtn)
editBtn.addEventListener('click', showEditModal)
return () => {
if (editBtn)
editBtn.removeEventListener('click', showEditModal)
}
}, [])
const handleChange = useCallback((newPayload: FormInputItem) => {
onChange(newPayload)
hideEditModal()
}, [onChange])
return (
<div
@ -32,17 +71,50 @@ const ComponentUI: FC<Props> = ({
</div>
</div>
<div className='flex h-[18px] items-center rounded-[5px] border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark px-1 shadow-xs'>
<div className='flex items-center space-x-0.5 text-text-secondary'>
<VarBlockIcon type={BlockEnum.Start} />
<div className='system-xs-medium'>{nodeName}</div>
<div className='flex w-full items-center justify-between'>
{/* Node info */}
<div className='flex h-[18px] items-center rounded-[5px] border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark px-1 shadow-xs'>
<div className='flex items-center space-x-0.5 text-text-secondary'>
<VarBlockIcon type={BlockEnum.HumanInput} />
<div className='system-xs-medium'>{nodeTitle}</div>
</div>
<div className='system-xs-regular mx-px text-divider-deep'>/</div>
<div className='flex items-center space-x-0.5 text-text-accent'>
<Variable02 className='size-3.5' />
<div className='system-xs-medium'>{varName}</div>
</div>
</div>
<div className='system-xs-regular mx-px text-divider-deep'>/</div>
<div className='flex items-center space-x-0.5 text-text-accent'>
<Variable02 className='size-3.5' />
<div className='system-xs-medium'>{varName}</div>
{/* Actions */}
<div className='flex h-full items-center space-x-1 pr-[24px]'>
<div className='flex h-full items-center' ref={editBtnRef}>
<ActionButton size='s'>
<RiEditLine className='size-4 text-text-tertiary' />
</ActionButton>
</div>
<div className='flex h-full items-center' >
<ActionButton size='s'>
<RiDeleteBinLine className='size-4 text-text-tertiary' />
</ActionButton>
</div>
</div>
</div>
{isShowEditModal && (
<Modal
isShow
onClose={hideEditModal}
wrapperClassName='z-[999]'
className='max-w-[372px] !p-0'
>
<InputField
payload={formInput}
onChange={handleChange}
onCancel={hideEditModal}
/>
</Modal>
)}
</div>
)
}

View File

@ -1,29 +1,51 @@
import type { FC } from 'react'
import { type FC, useCallback } from 'react'
import { useSelectOrDelete } from '../../hooks'
import { DELETE_HITL_INPUT_BLOCK_COMMAND } from './'
import ComponentUi from './component-ui'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import produce from 'immer'
type QueryBlockComponentProps = {
nodeKey: string
nodeName: string
nodeTitle: string
varName: string
formInputs?: FormInputItem[]
onChange: (inputs: FormInputItem[]) => void
}
const HITLInputComponent: FC<QueryBlockComponentProps> = ({
nodeKey,
nodeName,
nodeTitle,
varName,
formInputs = [],
onChange,
}) => {
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HITL_INPUT_BLOCK_COMMAND)
const payload = formInputs.find(item => item.output_variable_name === varName)
const handleChange = useCallback((newPayload: FormInputItem) => {
if(!payload) {
onChange([...formInputs, newPayload])
return
}
if(payload?.output_variable_name !== newPayload.output_variable_name) {
onChange(produce(formInputs, (draft) => {
draft.splice(draft.findIndex(item => item.output_variable_name === payload?.output_variable_name), 1, newPayload)
}))
return
}
onChange(formInputs.map(item => item.output_variable_name === varName ? newPayload : item))
}, [onChange])
return (
<div
ref={ref}
className='w-full pb-1 pt-3'
>
<ComponentUi
nodeName={nodeName}
nodeTitle={nodeTitle}
varName={varName}
isSelected={isSelected}
formInput={payload}
onChange={handleChange}
/>
</div>
)

View File

@ -8,7 +8,7 @@ import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils'
import type { QueryBlockType } from '../../types'
import type { HITLInputBlockType } from '../../types'
import { $createHITLInputNode } from './node'
import {
QueryBlockNode,
@ -19,8 +19,11 @@ import { HITL_INPUT_REG } from '@/config'
const REGEX = new RegExp(HITL_INPUT_REG)
const HITLInputReplacementBlock = ({
onInsert,
}: QueryBlockType) => {
// onInsert,
nodeTitle,
formInputs,
onFormInputsChange,
}: HITLInputBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@ -29,11 +32,9 @@ const HITLInputReplacementBlock = ({
}, [editor])
const createHITLInputBlockNode = useCallback((textNode: TextNode): QueryBlockNode => {
if (onInsert)
onInsert()
const varName = textNode.getTextContent().split('.')[1].replace(/#}}$/, '')
return $applyNodeReplacement($createHITLInputNode(varName))
}, [onInsert])
return $applyNodeReplacement($createHITLInputNode(varName, nodeTitle, formInputs || [], onFormInputsChange!))
}, [nodeTitle, formInputs, onFormInputsChange])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)

View File

@ -1,14 +1,29 @@
import React from 'react'
import React, { useCallback, useState } from 'react'
import Input from '@/app/components/base/input'
import PromptEditor from '@/app/components/base/prompt-editor'
import TagLabel from './tag-label'
import Button from '../../../button'
import { useTranslation } from 'react-i18next'
import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
const i18nPrefix = 'workflow.nodes.humanInput.insertInputField'
const InputField: React.FC = () => {
type Props = {
payload: FormInputItem
onChange: (newPayload: FormInputItem) => void
onCancel: () => void
}
const InputField: React.FC<Props> = ({
payload,
onChange,
onCancel,
}) => {
const { t } = useTranslation()
const [tempPayload, setTempPayload] = useState(payload)
const handleSave = useCallback(() => {
onChange(tempPayload)
}, [tempPayload])
return (
<div className="w-[372px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-3 shadow-lg backdrop-blur-[5px]">
<div className='system-md-semibold text-text-primary'>{t(`${i18nPrefix}.title`)}</div>
@ -19,6 +34,10 @@ const InputField: React.FC = () => {
<Input
className="mt-1.5"
placeholder={t(`${i18nPrefix}.saveResponseAsPlaceholder`)}
value={tempPayload.output_variable_name}
onChange={(e) => {
setTempPayload(prev => ({ ...prev, output_variable_name: e.target.value }))
}}
/>
</div>
<div className='mt-4'>
@ -37,14 +56,21 @@ const InputField: React.FC = () => {
<span>{t(`${i18nPrefix}.users`)}</span>
</div>
<div className="flex h-5 items-center">{t(`${i18nPrefix}.prePopulateFieldPlaceholderEnd`)}</div>
</div>}
</div>
}
onChange={
(newValue) => {
setTempPayload(prev => ({ ...prev, prePopulateField: newValue }))
}
}
/>
</div>
<div className='mt-4 flex justify-end space-x-2'>
<Button >{t('common.operation.cancel')}</Button>
<Button onClick={onCancel}>{t('common.operation.cancel')}</Button>
<Button
className='flex'
variant='primary'
onClick={handleSave}
>
<span className='mr-1'>{t(`${i18nPrefix}.insert`)}</span>
<span className='system-kbd mr-0.5 flex h-4 items-center rounded-[4px] bg-components-kbd-bg-white px-1'>{getKeyboardKeyNameBySystem('ctrl')}</span>

View File

@ -1,13 +1,20 @@
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import HILTInputBlockComponent from './component'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
export type SerializedNode = SerializedLexicalNode & {
variableName: string
nodeTitle: string
formInputs: FormInputItem[]
onFormInputsChange: (inputs: FormInputItem[]) => void
}
export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
__variableName: string
__nodeTitle: string
__formInputs?: FormInputItem[]
__onFormInputsChange: (inputs: FormInputItem[]) => void
isIsolated(): boolean {
return true // This is necessary for drag-and-drop to work correctly
@ -26,18 +33,36 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
return self.__variableName
}
getNodeTitle(): string {
const self = this.getLatest()
return self.__nodeTitle
}
getFormInputs(): FormInputItem[] {
const self = this.getLatest()
return self.__formInputs || []
}
getOnFormInputsChange(): (inputs: FormInputItem[]) => void {
const self = this.getLatest()
return self.__onFormInputsChange
}
static clone(node: HITLInputNode): HITLInputNode {
return new HITLInputNode(node.__variableName, node.__key)
return new HITLInputNode(node.__variableName, node.__nodeTitle, node.__formInputs || [], node.__onFormInputsChange, node.__key)
}
isInline(): boolean {
return true
}
constructor(varName: string, key?: NodeKey) {
constructor(varName: string, nodeTitle: string, formInputs: FormInputItem[], onFormInputsChange: (inputs: FormInputItem[]) => void, key?: NodeKey) {
super(key)
this.__variableName = varName
this.__nodeTitle = nodeTitle
this.__formInputs = formInputs
this.__onFormInputsChange = onFormInputsChange
}
createDOM(): HTMLElement {
@ -51,11 +76,17 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
}
decorate(): React.JSX.Element {
return <HILTInputBlockComponent nodeKey={this.getKey()} nodeName='todo' varName={this.getVariableName()} />
return <HILTInputBlockComponent
nodeKey={this.getKey()}
varName={this.getVariableName()}
nodeTitle={this.getNodeTitle()}
formInputs={this.getFormInputs()}
onChange={this.getOnFormInputsChange()}
/>
}
static importJSON(serializedNode: SerializedNode): HITLInputNode {
const node = $createHITLInputNode(serializedNode.variableName)
const node = $createHITLInputNode(serializedNode.variableName, serializedNode.nodeTitle, serializedNode.formInputs, serializedNode.onFormInputsChange)
return node
}
@ -65,6 +96,9 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
type: 'hitl-input-block',
version: 1,
variableName: this.getVariableName(),
nodeTitle: this.getNodeTitle(),
formInputs: this.getFormInputs(),
onFormInputsChange: this.getOnFormInputsChange(),
}
}
@ -73,8 +107,8 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
}
}
export function $createHITLInputNode(variableName: string): HITLInputNode {
return new HITLInputNode(variableName)
export function $createHITLInputNode(variableName: string, nodeTitle: string, formInputs: FormInputItem[], onFormInputsChange: (inputs: FormInputItem[]) => void): HITLInputNode {
return new HITLInputNode(variableName, nodeTitle, formInputs, onFormInputsChange)
}
export function $isHITLInputNode(

View File

@ -1,3 +1,4 @@
import type { FormInputItem } from '../../workflow/nodes/human-input/types'
import type { Type } from '../../workflow/nodes/llm/types'
import type { Dataset } from './plugins/context-block'
import type { RoleName } from './plugins/history-block'
@ -79,6 +80,9 @@ export type WorkflowVariableBlockType = {
export type HITLInputBlockType = {
show?: boolean
nodeTitle: string
formInputs?: FormInputItem[]
onFormInputsChange?: (inputs: FormInputItem[]) => void
}
export type MenuTextMatch = {

View File

@ -6,17 +6,24 @@ import useAvailableVarList from '../../_base/hooks/use-available-var-list'
import { BlockEnum } from '../../../types'
import { useWorkflowVariableType } from '../../../hooks'
import { useTranslation } from 'react-i18next'
import type { FormInputItem } from '../types'
type Props = {
nodeId: string
value: string
onChange: (value: string) => void
formInputs: FormInputItem[]
onFormInputsChange: (payload: FormInputItem[]) => void
nodeTitle: string
}
const FormContent: FC<Props> = ({
nodeId,
value,
onChange,
formInputs,
onFormInputsChange,
nodeTitle,
}) => {
const { t } = useTranslation()
const filterVar = () => true
@ -38,6 +45,9 @@ const FormContent: FC<Props> = ({
className='min-h-[80px]'
hitlInputBlock={{
show: true,
formInputs,
nodeTitle,
onFormInputsChange,
}}
workflowVariableBlock={{
show: true,

View File

@ -35,6 +35,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
handleUserActionDelete,
handleTimeoutChange,
handleFormContentChange,
handleFormInputsChange,
} = useConfig(id, data)
const { availableVars, availableNodesWithParent } = useAvailableVarList(id, {
@ -70,6 +71,9 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
nodeId={id}
value={inputs.form_content}
onChange={handleFormContentChange}
nodeTitle={inputs.title}
formInputs={inputs.inputs}
onFormInputsChange={handleFormInputsChange}
/>
</div>
{/* user actions */}

View File

@ -3,15 +3,7 @@ import type { CommonNodeType, InputVarType, ValueSelector, Variable } from '@/ap
export type HumanInputNodeType = CommonNodeType & {
delivery_methods: DeliveryMethod[]
form_content: string
form_input: {
type: InputVarType
output_variable_name: string
placeholder?: { // only text-input and paragraph support placeholder
type: 'variable' | 'const',
selector: ValueSelector
value: string
}
}[]
inputs: FormInputItem[]
user_actions: UserAction[]
timeout: number
timeout_unit: 'hour' | 'day'
@ -47,6 +39,19 @@ export type DeliveryMethod = {
config?: EmailConfig
}
export type FormInputItemPlaceholder = {
type: 'variable' | 'const',
selector: ValueSelector
value: string
}
export type FormInputItem = {
type: InputVarType
output_variable_name: string
// only text-input and paragraph support placeholder
placeholder?: FormInputItemPlaceholder
}
export enum UserActionButtonType {
Primary = 'primary',
Default = 'default',

View File

@ -1,5 +1,5 @@
import useNodeCrud from '../_base/hooks/use-node-crud'
import type { HumanInputNodeType } from './types'
import type { FormInputItem, HumanInputNodeType } from './types'
const useFormContent = (id: string, payload: HumanInputNodeType) => {
const { inputs, setInputs } = useNodeCrud<HumanInputNodeType>(id, payload)
@ -9,8 +9,16 @@ const useFormContent = (id: string, payload: HumanInputNodeType) => {
form_content: value,
})
}
const handleFormInputsChange = (formInputs: FormInputItem[]) => {
setInputs({
...inputs,
inputs: formInputs,
})
}
return {
handleFormContentChange,
handleFormInputsChange,
}
}