diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx index 2a571d81cd..901650349a 100644 --- a/web/app/components/base/prompt-editor/index.tsx +++ b/web/app/components/base/prompt-editor/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import type { EditorState, } from 'lexical' @@ -16,7 +16,8 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable' import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' -// import TreeView from './plugins/tree-view' +import DraggableBlockPlugin from './plugins/draggable-plugin' +import TreeView from './plugins/tree-view' import Placeholder from './plugins/placeholder' import ComponentPickerBlock from './plugins/component-picker-block' import { @@ -165,9 +166,16 @@ const PromptEditor: FC = ({ } as any) }, [eventEmitter, historyBlock?.history]) + const [floatingAnchorElem, setFloatingAnchorElem] = useState(null) + + const onRef = (_floatingAnchorElem: any) => { + if (_floatingAnchorElem !== null) + setFloatingAnchorElem(_floatingAnchorElem) + } + return ( -
+
= ({ - {/* */} + {floatingAnchorElem && ( + + )} +
) diff --git a/web/app/components/base/prompt-editor/plugins/draggable-plugin/icons/draggable-block-menu.svg b/web/app/components/base/prompt-editor/plugins/draggable-plugin/icons/draggable-block-menu.svg new file mode 100644 index 0000000000..7086d2990a --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/draggable-plugin/icons/draggable-block-menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/app/components/base/prompt-editor/plugins/draggable-plugin/index.css b/web/app/components/base/prompt-editor/plugins/draggable-plugin/index.css new file mode 100644 index 0000000000..159b3cac44 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/draggable-plugin/index.css @@ -0,0 +1,40 @@ +.draggable-block-menu { + border-radius: 4px; + padding: 2px 1px; + cursor: grab; + opacity: 0; + position: absolute; + left: -20px; + top: 0; + will-change: transform; + display: flex; + gap: 2px; +} + +.draggable-block-menu .icon { + width: 16px; + height: 16px; + opacity: 0.3; + background-image: url(./icons/draggable-block-menu.svg); + background-repeat: no-repeat; +} + +.draggable-block-menu:active { + cursor: grabbing; +} + +.draggable-block-menu .icon:hover { + background-color: #efefef; +} + +.draggable-block-target-line { + pointer-events: none; + background: deepskyblue; + height: 4px; + position: absolute; + left: -21px; + right: 0; + top: 0; + opacity: 0; + will-change: transform; +} diff --git a/web/app/components/base/prompt-editor/plugins/draggable-plugin/index.tsx b/web/app/components/base/prompt-editor/plugins/draggable-plugin/index.tsx new file mode 100644 index 0000000000..3747f0fdf1 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/draggable-plugin/index.tsx @@ -0,0 +1,70 @@ +import type { JSX } from 'react' + +import './index.css' + +import { DraggableBlockPlugin_EXPERIMENTAL } from '@lexical/react/LexicalDraggableBlockPlugin' +import { useEffect, useRef, useState } from 'react' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' + +const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu' + +function isOnMenu(element: HTMLElement): boolean { + return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`) +} + +const SUPPORT_DRAG_CLASS = 'support-drag' +function checkSupportDrag(element: Element | null): boolean { + if (!element) return false + + if (element.classList.contains(SUPPORT_DRAG_CLASS)) return true + + if (element.querySelector(`.${SUPPORT_DRAG_CLASS}`)) return true + + return !!(element.closest(`.${SUPPORT_DRAG_CLASS}`)) +} + +export default function DraggableBlockPlugin({ + anchorElem = document.body, +}: { + anchorElem?: HTMLElement; +}): JSX.Element { + const menuRef = useRef(null) + const targetLineRef = useRef(null) + const [draggableElement, setDraggableElement] = useState( + null, + ) +const [editor] = useLexicalComposerContext() + + const [isSupportDrag, setIsSupportDrag] = useState(false) + + useEffect(() => { + const root = editor.getRootElement() + if (!root) return + + const onMove = (e: MouseEvent) => { + const isSupportDrag = checkSupportDrag(e.target as Element) + setIsSupportDrag(isSupportDrag) + } + + root.addEventListener('mousemove', onMove) + return () => root.removeEventListener('mousemove', onMove) + }, [editor]) + + return ( + +
+
: null + } + targetLineComponent={ +
+ } + isOnMenu={isOnMenu} + onElementChanged={setDraggableElement} + /> + ) +} diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/component.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/component.tsx index 3020ad2194..c7ab5a5307 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/component.tsx @@ -3,6 +3,7 @@ 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' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' type QueryBlockComponentProps = { nodeKey: string @@ -17,13 +18,49 @@ const HITLInputComponent: FC = ({ }) => { const { t } = useTranslation() const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HITL_INPUT_BLOCK_COMMAND) - + const [editor] = useLexicalComposerContext() return (
{ + // e.dataTransfer.setData('application/x-lexical-drag', nodeKey) + // e.dataTransfer.effectAllowed = 'move' + // console.log(`dragging node with key: ${nodeKey}`) + // }} + // onDragOver={(e) => { + // e.preventDefault() + // e.dataTransfer.dropEffect = 'move' + // }} + // onDragEnter={(e) => { + // e.preventDefault() + // e.currentTarget.classList.add('bg-[#FFEAD5]') + // }} + // onDragLeave={(e) => { + // e.currentTarget.classList.remove('bg-[#FFEAD5]') + // }} + // onDrop={(e) => { + // e.preventDefault() + // e.currentTarget.classList.remove('bg-[#FFEAD5]') + + // const draggedNodeKey = e.dataTransfer.getData('application/x-lexical-drag') + // console.log('Drop event triggered with key:', draggedNodeKey) + + // if (draggedNodeKey) { + // editor.update(() => { + // const draggedNode = $getNodeByKey(draggedNodeKey) + // const dropTarget = $getNodeByKey(nodeKey) + + // if (draggedNode && dropTarget && draggedNode !== dropTarget) { + // console.log('Moving node in editor') + // dropTarget.insertAfter(draggedNode) + // } + // }) + // } + // }} ref={ref} > diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/node.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/node.tsx index 925b834b52..b150480e0d 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/node.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/node.tsx @@ -9,6 +9,14 @@ export type SerializedNode = SerializedLexicalNode & { export class HITLInputNode extends DecoratorNode { __variableName: string + isIsolated(): boolean { + return true // This is necessary for drag-and-drop to work correctly + } + + isTopLevel(): boolean { + return true // This is necessary for drag-and-drop to work correctly + } + static getType(): string { return 'hitl-input-block' } @@ -34,7 +42,7 @@ export class HITLInputNode extends DecoratorNode { createDOM(): HTMLElement { const div = document.createElement('div') - div.classList.add('flex', 'items-center', 'align-middle') + div.classList.add('flex', 'items-center', 'align-middle', 'support-drag') return div }