feat: can insert hitl node by /

This commit is contained in:
Joel 2025-08-26 15:50:53 +08:00
parent 6b11973151
commit 71a511a470
7 changed files with 129 additions and 9 deletions

View File

@ -4,6 +4,7 @@ import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import type {
EditorState,
LexicalCommand,
} from 'lexical'
import {
$getRoot,
@ -119,7 +120,7 @@ export type PromptEditorProps = {
errorMessageBlock?: ErrorMessageBlockType
lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean
shortcutPopups?: Array<{ hotkey: Hotkey; Popup: React.ComponentType<{ onClose: () => void }> }>
shortcutPopups?: Array<{ hotkey: Hotkey; Popup: React.ComponentType<{ onClose: () => void, onInsert: (command: LexicalCommand<unknown>, params: any[]) => void }> }>
}
const PromptEditor: FC<PromptEditorProps> = ({
@ -229,7 +230,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
/>
{shortcutPopups?.map(({ hotkey, Popup }, idx) => (
<ShortcutsPopupPlugin key={idx} hotkey={hotkey} >
{closePortal => <Popup onClose={closePortal} />}
{(closePortal, onInsert) => <Popup onClose={closePortal} onInsert={onInsert} />}
</ShortcutsPopupPlugin>
))}
<ComponentPickerBlock

View File

@ -3,13 +3,20 @@ import {
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { QueryBlockType } from '../../types'
import type {
HITLNodeProps,
} from './node'
import {
$createHITLInputNode,
HITLInputNode,
} from './node'
import { mergeRegister } from '@lexical/utils'
export const INSERT_HITL_INPUT_BLOCK_COMMAND = createCommand('INSERT_HITL_INPUT_BLOCK_COMMAND')
export const DELETE_HITL_INPUT_BLOCK_COMMAND = createCommand('DELETE_HITL_INPUT_BLOCK_COMMAND')
@ -27,6 +34,49 @@ const HITLInputBlock = memo(({
useEffect(() => {
if (!editor.hasNodes([HITLInputNode]))
throw new Error('HITLInputBlockPlugin: HITLInputBlock not registered on editor')
return mergeRegister(
editor.registerCommand(
INSERT_HITL_INPUT_BLOCK_COMMAND,
(nodeProps: HITLNodeProps) => {
const {
variableName,
nodeId,
nodeTitle,
formInputs,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
} = nodeProps
const currentHITLNode = $createHITLInputNode(
variableName,
nodeId,
nodeTitle,
formInputs,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
)
$insertNodes([currentHITLNode])
if (onInsert)
onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_HITL_INPUT_BLOCK_COMMAND,
() => {
if (onDelete)
onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, onInsert, onDelete])
return null

View File

@ -8,16 +8,22 @@ import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
import type { FormInputItem, FormInputItemPlaceholder } from '@/app/components/workflow/nodes/human-input/types'
import PrePopulate from './pre-populate'
import produce from 'immer'
import { InputVarType } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.humanInput.insertInputField'
type Props = {
nodeId: string
isEdit: boolean
payload: FormInputItem
payload?: FormInputItem
onChange: (newPayload: FormInputItem) => void
onCancel: () => void
}
const defaultPayload: FormInputItem = {
type: InputVarType.paragraph,
output_variable_name: '',
placeholder: { type: 'const', selector: [], value: '' },
}
const InputField: React.FC<Props> = ({
nodeId,
isEdit,
@ -26,7 +32,7 @@ const InputField: React.FC<Props> = ({
onCancel,
}) => {
const { t } = useTranslation()
const [tempPayload, setTempPayload] = useState(payload)
const [tempPayload, setTempPayload] = useState(payload || defaultPayload)
const handleSave = useCallback(() => {
onChange(tempPayload)
}, [tempPayload])

View File

@ -3,7 +3,7 @@ import { DecoratorNode } from 'lexical'
import HILTInputBlockComponent from './component'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
export type SerializedNode = SerializedLexicalNode & {
export type HITLNodeProps = {
variableName: string
nodeId: string
nodeTitle: string
@ -13,6 +13,8 @@ export type SerializedNode = SerializedLexicalNode & {
onFormInputItemRemove: (varName: string) => void
}
export type SerializedNode = SerializedLexicalNode & HITLNodeProps
export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
__variableName: string
__nodeId: string

View File

@ -7,6 +7,7 @@ import {
useState,
} from 'react'
import { createPortal } from 'react-dom'
import type { LexicalCommand } from 'lexical'
import {
$getSelection,
$isRangeSelection,
@ -24,7 +25,7 @@ export type Hotkey = string | string[] | string[][] | ((e: KeyboardEvent) => boo
type ShortcutPopupPluginProps = {
hotkey?: Hotkey
children?: React.ReactNode | ((close: () => void) => React.ReactNode)
children?: React.ReactNode | ((close: () => void, onInsert: (command: LexicalCommand<unknown>, params: any[]) => void) => React.ReactNode)
className?: string
style?: React.CSSProperties
container?: Element | null
@ -261,6 +262,11 @@ export default function ShortcutsPopupPlugin({
return () => document.removeEventListener('mousedown', onMouseDown, true)
}, [open, closePortal])
const handleInsert = useCallback((command: LexicalCommand<unknown>, params: any) => {
editor.dispatchCommand(command, params)
closePortal()
}, [editor, closePortal])
if (!open || !containerEl)
return null
@ -274,7 +280,7 @@ export default function ShortcutsPopupPlugin({
)}
style={{ top: `${position.top}px`, left: `${position.left}px`, ...style }}
>
{typeof children === 'function' ? children(closePortal) : (children ?? SHORTCUTS_EMPTY_CONTENT)}
{typeof children === 'function' ? children(closePortal, handleInsert) : (children ?? SHORTCUTS_EMPTY_CONTENT)}
</div>,
containerEl,
)

View File

@ -0,0 +1,27 @@
'use client'
import InputField from '@/app/components/base/prompt-editor/plugins/hitl-input-block/input-field'
import type { FC } from 'react'
import React from 'react'
import type { FormInputItem } from '../types'
type Props = {
nodeId: string
onSave: (newPayload: FormInputItem) => void
onCancel: () => void
}
const AddInputField: FC<Props> = ({
nodeId,
onSave,
onCancel,
}) => {
return (
<InputField
nodeId={nodeId}
isEdit={false}
onChange={onSave}
onCancel={onCancel}
/>
)
}
export default React.memo(AddInputField)

View File

@ -7,28 +7,31 @@ import { BlockEnum } from '../../../types'
import { useWorkflowVariableType } from '../../../hooks'
import { useTranslation } from 'react-i18next'
import type { FormInputItem } from '../types'
import AddInputField from './add-input-field'
import { INSERT_HITL_INPUT_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/hitl-input-block'
import type { LexicalCommand } from 'lexical'
type Props = {
nodeId: string
nodeTitle: string
value: string
onChange: (value: string) => void
formInputs: FormInputItem[]
onFormInputsChange: (payload: FormInputItem[]) => void
onFormInputItemRename: (payload: FormInputItem, oldName: string) => void
onFormInputItemRemove: (varName: string) => void
nodeTitle: string
editorKey: number
}
const FormContent: FC<Props> = ({
nodeId,
nodeTitle,
value,
onChange,
formInputs,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
nodeTitle,
editorKey,
}) => {
const { t } = useTranslation()
@ -43,6 +46,21 @@ const FormContent: FC<Props> = ({
const getVarType = useWorkflowVariableType()
const handleInsertHITLNode = (onInsert: (command: LexicalCommand<unknown>, params: any) => void) => {
return (payload: FormInputItem) => {
// todo insert into form inputs
onInsert(INSERT_HITL_INPUT_BLOCK_COMMAND, {
variableName: payload.output_variable_name,
nodeId,
nodeTitle,
formInputs,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
})
}
}
return (
<div>
<PromptEditor
@ -81,6 +99,16 @@ const FormContent: FC<Props> = ({
}, {}),
}}
editable
shortcutPopups={[{
hotkey: ['mod', '/'],
Popup: ({ onClose, onInsert }) => (
<AddInputField
nodeId={nodeId}
onSave={handleInsertHITLNode(onInsert!)}
onCancel={onClose}
/>
),
}]}
/>
</div>
)