mirror of
https://github.com/langgenius/dify.git
synced 2026-04-21 03:07:39 +08:00
Made-with: Cursor # Conflicts: # .devcontainer/post_create_command.sh # api/commands.py # api/core/agent/cot_agent_runner.py # api/core/agent/fc_agent_runner.py # api/core/app/apps/workflow_app_runner.py # api/core/app/entities/queue_entities.py # api/core/app/entities/task_entities.py # api/core/workflow/workflow_entry.py # api/dify_graph/enums.py # api/dify_graph/graph/graph.py # api/dify_graph/graph_events/node.py # api/dify_graph/model_runtime/entities/message_entities.py # api/dify_graph/node_events/node.py # api/dify_graph/nodes/agent/agent_node.py # api/dify_graph/nodes/base/__init__.py # api/dify_graph/nodes/base/entities.py # api/dify_graph/nodes/base/node.py # api/dify_graph/nodes/llm/entities.py # api/dify_graph/nodes/llm/node.py # api/dify_graph/nodes/tool/tool_node.py # api/pyproject.toml # api/uv.lock # web/app/components/base/avatar/__tests__/index.spec.tsx # web/app/components/base/avatar/index.tsx # web/app/components/base/date-and-time-picker/time-picker/__tests__/index.spec.tsx # web/app/components/base/file-uploader/file-from-link-or-local/index.tsx # web/app/components/base/prompt-editor/index.tsx # web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx # web/app/components/header/account-dropdown/index.spec.tsx # web/app/components/share/text-generation/index.tsx # web/app/components/workflow/block-selector/tool/action-item.tsx # web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx # web/app/components/workflow/hooks/use-edges-interactions.ts # web/app/components/workflow/hooks/use-nodes-interactions.ts # web/app/components/workflow/index.tsx # web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx # web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx # web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-item.tsx # web/app/components/workflow/nodes/loop/use-interactions.ts # web/contract/router.ts # web/env.ts # web/eslint-suppressions.json # web/package.json # web/pnpm-lock.yaml
187 lines
5.4 KiB
TypeScript
187 lines
5.4 KiB
TypeScript
import type { EntityMatch } from '@lexical/text'
|
|
import type {
|
|
Klass,
|
|
LexicalCommand,
|
|
LexicalEditor,
|
|
TextNode,
|
|
} from 'lexical'
|
|
import type { Dispatch, RefObject, SetStateAction } from 'react'
|
|
import type { CustomTextNode } from './plugins/custom-text/node'
|
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
|
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
|
|
import {
|
|
mergeRegister,
|
|
} from '@lexical/utils'
|
|
import {
|
|
$getNodeByKey,
|
|
$getSelection,
|
|
$isDecoratorNode,
|
|
$isNodeSelection,
|
|
COMMAND_PRIORITY_LOW,
|
|
KEY_BACKSPACE_COMMAND,
|
|
KEY_DELETE_COMMAND,
|
|
} from 'lexical'
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from 'react'
|
|
import { DELETE_CONTEXT_BLOCK_COMMAND } from './plugins/context-block'
|
|
import { $isContextBlockNode } from './plugins/context-block/node'
|
|
import { DELETE_HISTORY_BLOCK_COMMAND } from './plugins/history-block'
|
|
import { $isHistoryBlockNode } from './plugins/history-block/node'
|
|
import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block'
|
|
import { $isQueryBlockNode } from './plugins/query-block/node'
|
|
import { registerLexicalTextEntity } from './utils'
|
|
|
|
export type UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement | null>, boolean]
|
|
export const useSelectOrDelete: UseSelectOrDeleteHandler = (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 true
|
|
}
|
|
}
|
|
|
|
return false
|
|
},
|
|
[isSelected, nodeKey, command, editor],
|
|
)
|
|
|
|
const handleSelect = useCallback((e: MouseEvent) => {
|
|
if (!e.metaKey && !e.ctrlKey) {
|
|
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 | null>, 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 function useBasicTypeaheadTriggerMatch(
|
|
trigger: string,
|
|
{ minLength = 1, maxLength = 75 }: { minLength?: number, maxLength?: number },
|
|
): TriggerFn {
|
|
return useCallback(
|
|
(text: string) => {
|
|
const validChars = '[^\\n]'
|
|
const TypeaheadTriggerRegex = new RegExp(
|
|
'(.*)('
|
|
+ `[${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],
|
|
)
|
|
}
|