feat: advanced prompt (#1330)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: Gillian97 <jinling.sunshine@gmail.com>
This commit is contained in:
184
web/app/components/base/prompt-editor/hooks.ts
Normal file
184
web/app/components/base/prompt-editor/hooks.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { Dispatch, RefObject, SetStateAction } from 'react'
|
||||
import type {
|
||||
Klass,
|
||||
LexicalCommand,
|
||||
LexicalEditor,
|
||||
TextNode,
|
||||
} from 'lexical'
|
||||
import {
|
||||
$getNodeByKey,
|
||||
$getSelection,
|
||||
$isDecoratorNode,
|
||||
$isNodeSelection,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
KEY_BACKSPACE_COMMAND,
|
||||
KEY_DELETE_COMMAND,
|
||||
} from 'lexical'
|
||||
import type { EntityMatch } from '@lexical/text'
|
||||
import {
|
||||
mergeRegister,
|
||||
} from '@lexical/utils'
|
||||
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $isContextBlockNode } from './plugins/context-block/node'
|
||||
import { DELETE_CONTEXT_BLOCK_COMMAND } from './plugins/context-block'
|
||||
import { $isHistoryBlockNode } from './plugins/history-block/node'
|
||||
import { DELETE_HISTORY_BLOCK_COMMAND } from './plugins/history-block'
|
||||
import { $isQueryBlockNode } from './plugins/query-block/node'
|
||||
import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block'
|
||||
import type { CustomTextNode } from './plugins/custom-text/node'
|
||||
import { registerLexicalTextEntity } from './utils'
|
||||
|
||||
export type UseSelectOrDeleteHanlder = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement>, boolean]
|
||||
export const useSelectOrDelete: UseSelectOrDeleteHanlder = (nodeKey: string, command?: LexicalCommand<undefined>) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey)
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
const selection = $getSelection()
|
||||
const nodes = selection?.getNodes()
|
||||
if (
|
||||
!isSelected
|
||||
&& nodes?.length === 1
|
||||
&& (
|
||||
($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND)
|
||||
|| ($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND)
|
||||
|| ($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND)
|
||||
)
|
||||
)
|
||||
editor.dispatchCommand(command, undefined)
|
||||
|
||||
if (isSelected && $isNodeSelection(selection)) {
|
||||
event.preventDefault()
|
||||
const node = $getNodeByKey(nodeKey)
|
||||
if ($isDecoratorNode(node)) {
|
||||
if (command)
|
||||
editor.dispatchCommand(command, undefined)
|
||||
|
||||
node.remove()
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
[isSelected, nodeKey, command, editor],
|
||||
)
|
||||
|
||||
const handleSelect = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
clearSelection()
|
||||
setSelected(true)
|
||||
}, [setSelected, clearSelection])
|
||||
|
||||
useEffect(() => {
|
||||
const ele = ref.current
|
||||
|
||||
if (ele)
|
||||
ele.addEventListener('click', handleSelect)
|
||||
|
||||
return () => {
|
||||
if (ele)
|
||||
ele.removeEventListener('click', handleSelect)
|
||||
}
|
||||
}, [handleSelect])
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
KEY_DELETE_COMMAND,
|
||||
handleDelete,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand(
|
||||
KEY_BACKSPACE_COMMAND,
|
||||
handleDelete,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
)
|
||||
}, [editor, clearSelection, handleDelete])
|
||||
|
||||
return [ref, isSelected]
|
||||
}
|
||||
|
||||
export type UseTriggerHandler = () => [RefObject<HTMLDivElement>, boolean, Dispatch<SetStateAction<boolean>>]
|
||||
export const useTrigger: UseTriggerHandler = () => {
|
||||
const triggerRef = useRef<HTMLDivElement>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const handleOpen = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setOpen(v => !v)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const trigger = triggerRef.current
|
||||
if (trigger)
|
||||
trigger.addEventListener('click', handleOpen)
|
||||
|
||||
return () => {
|
||||
if (trigger)
|
||||
trigger.removeEventListener('click', handleOpen)
|
||||
}
|
||||
}, [handleOpen])
|
||||
|
||||
return [triggerRef, open, setOpen]
|
||||
}
|
||||
|
||||
export function useLexicalTextEntity<T extends TextNode>(
|
||||
getMatch: (text: string) => null | EntityMatch,
|
||||
targetNode: Klass<T>,
|
||||
createNode: (textNode: CustomTextNode) => T,
|
||||
) {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(...registerLexicalTextEntity(editor, getMatch, targetNode, createNode))
|
||||
}, [createNode, editor, getMatch, targetNode])
|
||||
}
|
||||
|
||||
export type MenuTextMatch = {
|
||||
leadOffset: number
|
||||
matchingString: string
|
||||
replaceableString: string
|
||||
}
|
||||
export type TriggerFn = (
|
||||
text: string,
|
||||
editor: LexicalEditor,
|
||||
) => MenuTextMatch | null
|
||||
export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'
|
||||
export function useBasicTypeaheadTriggerMatch(
|
||||
trigger: string,
|
||||
{ minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number },
|
||||
): TriggerFn {
|
||||
return useCallback(
|
||||
(text: string) => {
|
||||
const validChars = `[^${trigger}${PUNCTUATION}\\s]`
|
||||
const TypeaheadTriggerRegex = new RegExp(
|
||||
`([^${trigger}]|^)(`
|
||||
+ `[${trigger}]`
|
||||
+ `((?:${validChars}){0,${maxLength}})`
|
||||
+ ')$',
|
||||
)
|
||||
const match = TypeaheadTriggerRegex.exec(text)
|
||||
if (match !== null) {
|
||||
const maybeLeadingWhitespace = match[1]
|
||||
const matchingString = match[3]
|
||||
if (matchingString.length >= minLength) {
|
||||
return {
|
||||
leadOffset: match.index + maybeLeadingWhitespace.length,
|
||||
matchingString,
|
||||
replaceableString: match[2],
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
[maxLength, minLength, trigger],
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user