This commit is contained in:
Joel 2025-08-05 18:31:32 +08:00
parent 736ec55f86
commit 177be06d09
4 changed files with 215 additions and 0 deletions

View File

@ -0,0 +1,35 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelectOrDelete } from '../../hooks'
import { DELETE_HITL_INPUT_BLOCK_COMMAND } from './'
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
type QueryBlockComponentProps = {
nodeKey: string
nodeName: string
varName: string
}
const HITLInputComponent: FC<QueryBlockComponentProps> = ({
nodeKey,
nodeName,
varName,
}) => {
const { t } = useTranslation()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HITL_INPUT_BLOCK_COMMAND)
return (
<div
className={`
inline-flex h-6 items-center rounded-[5px] border border-transparent bg-[#FFF6ED] pl-1 pr-0.5 hover:bg-[#FFEAD5]
${isSelected && '!border-[#FD853A]'}
`}
ref={ref}
>
<UserEdit02 className='mr-1 h-[14px] w-[14px] text-[#FD853A]' />
{nodeName}/{varName}
</div>
)
}
export default HITLInputComponent

View File

@ -0,0 +1,62 @@
import {
memo,
useCallback,
useEffect,
} from 'react'
import type { TextNode } from 'lexical'
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 { $createHITLInputNode } from './node'
import {
QueryBlockNode,
} from '../query-block/node'
import { CustomTextNode } from '../custom-text/node'
import { HITL_INPUT_REG } from '@/config'
const REGEX = new RegExp(HITL_INPUT_REG)
const QueryBlockReplacementBlock = ({
onInsert,
}: QueryBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([QueryBlockNode]))
throw new Error('QueryBlockNodePlugin: QueryBlockNode not registered on editor')
}, [editor])
const createHITLInputBlockNode = useCallback((textNode: TextNode): QueryBlockNode => {
if (onInsert)
onInsert()
const varName = textNode.getTextContent().split('.')[1]
return $applyNodeReplacement($createHITLInputNode(varName))
}, [onInsert])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
const startOffset = matchArr.index
const endOffset = startOffset + matchArr[0].length
return {
end: endOffset,
start: startOffset,
}
}, [])
useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHITLInputBlockNode)),
)
}, [])
return null
}
export default memo(QueryBlockReplacementBlock)

View File

@ -0,0 +1,42 @@
import {
memo,
useEffect,
} from 'react'
import {
createCommand,
} from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { QueryBlockType } from '../../types'
import {
HITLInputNode,
} from './node'
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')
export type HITLInputProps = {
onInsert?: () => void
onDelete?: () => void
}
const HITLInputBlock = memo(({
onInsert,
onDelete,
}: QueryBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([HITLInputNode]))
throw new Error('HITLInputBlockPlugin: HITLInputBlock not registered on editor')
}, [editor, onInsert, onDelete])
// TODO
// const createHITLBlockNode = useCallback
return null
})
HITLInputBlock.displayName = 'HITLInputBlock'
export { HITLInputBlock }
export { HITLInputNode } from './node'
export { default as HITLInputBlockReplacementBlock } from './hitl-input-block-replacement-block'

View File

@ -0,0 +1,76 @@
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import HILTInputBlockComponent from './component'
export type SerializedNode = SerializedLexicalNode & {
variableName: string
}
export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
__variableName: string
static getType(): string {
return 'hitl-input-block'
}
getVariableName(): string {
const self = this.getLatest()
return self.__variableName
}
static clone(node: HITLInputNode): HITLInputNode {
return new HITLInputNode(node.__variableName, node.__key)
}
isInline(): boolean {
return true
}
constructor(varName: string, key?: NodeKey) {
super(key)
this.__variableName = varName
}
createDOM(): HTMLElement {
const div = document.createElement('div')
div.classList.add('inline-flex', 'items-center', 'align-middle')
return div
}
updateDOM(): false {
return false
}
decorate(): React.JSX.Element {
return <HILTInputBlockComponent nodeKey={this.getKey()} nodeName='todo' varName={this.getVariableName()} />
}
static importJSON(serializedNode: SerializedNode): HITLInputNode {
const node = $createHITLInputNode(serializedNode.variableName)
return node
}
exportJSON(): SerializedNode {
return {
type: 'hitl-input-block',
version: 1,
variableName: this.getVariableName(),
}
}
getTextContent(): string {
return `{{#$outputs.${this.getVariableName()}#}}`
}
}
export function $createHITLInputNode(variableName: string): HITLInputNode {
return new HITLInputNode(variableName)
}
export function $isHITLInputNode(
node: HITLInputNode | LexicalNode | null | undefined,
): node is HITLInputNode {
return node instanceof HITLInputNode
}