From 0b1445aed53dcbe262fa2f5cb5e5fa7ec2fa2681 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Mon, 22 Sep 2025 18:24:36 +0800 Subject: [PATCH] memory popup --- .../components/base/prompt-editor/index.tsx | 7 + .../plugins/memory-popup-plugin/index.tsx | 177 ++++++++++++++++++ .../prompt-editor/plugins/update-block.tsx | 17 +- .../workflow-variable-block/component.tsx | 4 +- .../components/prompt/add-memory-button.tsx | 30 +++ .../nodes/_base/components/prompt/editor.tsx | 18 +- .../components/workflow/nodes/llm/panel.tsx | 1 + 7 files changed, 241 insertions(+), 13 deletions(-) create mode 100644 web/app/components/base/prompt-editor/plugins/memory-popup-plugin/index.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/prompt/add-memory-button.tsx diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx index 50fdc1f920..73262cb6a6 100644 --- a/web/app/components/base/prompt-editor/index.tsx +++ b/web/app/components/base/prompt-editor/index.tsx @@ -61,6 +61,8 @@ import { VariableValueBlockNode } from './plugins/variable-value-block/node' import { CustomTextNode } from './plugins/custom-text/node' import OnBlurBlock from './plugins/on-blur-or-focus-block' import UpdateBlock from './plugins/update-block' +import MemoryPopupPlugin from './plugins/memory-popup-plugin' + import { textToEditorState } from './utils' import type { ContextBlockType, @@ -103,6 +105,7 @@ export type PromptEditorProps = { errorMessageBlock?: ErrorMessageBlockType lastRunBlock?: LastRunBlockType isSupportFileVar?: boolean + isMemorySupported?: boolean } const PromptEditor: FC = ({ @@ -128,6 +131,7 @@ const PromptEditor: FC = ({ errorMessageBlock, lastRunBlock, isSupportFileVar, + isMemorySupported, }) => { const { eventEmitter } = useEventEmitterContextContext() const initialConfig = { @@ -198,6 +202,9 @@ const PromptEditor: FC = ({ } ErrorBoundary={LexicalErrorBoundary} /> + {isMemorySupported && ( + + )} (null) + const lastSelectionRef = useRef(null) + + const containerEl = useMemo(() => container ?? (typeof document !== 'undefined' ? document.body : null), [container]) + + const useContainer = !!containerEl && containerEl !== document.body + + const { refs, floatingStyles, isPositioned } = useFloating({ + placement: 'bottom-start', + middleware: [ + offset(0), // fix hide cursor + shift({ + padding: 8, + altBoundary: true, + }), + flip(), + size({ + apply({ availableWidth, availableHeight, elements }) { + Object.assign(elements.floating.style, { + maxWidth: `${Math.min(400, availableWidth)}px`, + maxHeight: `${Math.min(300, availableHeight)}px`, + overflow: 'auto', + }) + }, + padding: 8, + }), + ], + whileElementsMounted: autoUpdate, + }) + + const openPortal = useCallback(() => { + const domSelection = window.getSelection() + let range: Range | null = null + if (domSelection && domSelection.rangeCount > 0) + range = domSelection.getRangeAt(0).cloneRange() + else + range = lastSelectionRef.current + + if (range) { + const rects = range.getClientRects() + let rect: DOMRect | null = null + + if (rects && rects.length) + rect = rects[rects.length - 1] + + else + rect = range.getBoundingClientRect() + + if (rect.width === 0 && rect.height === 0) { + const root = editor.getRootElement() + if (root) { + const sc = range.startContainer + const node = sc.nodeType === Node.ELEMENT_NODE + ? sc as Element + : (sc.parentElement || root) + + rect = node.getBoundingClientRect() + + if (rect.width === 0 && rect.height === 0) + rect = root.getBoundingClientRect() + } + } + + if (rect && !(rect.top === 0 && rect.left === 0 && rect.width === 0 && rect.height === 0)) { + const virtualEl = { + getBoundingClientRect() { + return rect! + }, + } + refs.setReference(virtualEl as Element) + } + } + + setOpen(true) + }, [setOpen]) + + const closePortal = useCallback(() => { + setOpen(false) + }, [setOpen]) + + eventEmitter?.useSubscription((v: any) => { + if (v.type === MEMORY_POPUP_SHOW_BY_EVENT_EMITTER && v.instanceId === instanceId) + openPortal() + }) + + useEffect(() => { + return editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + const selection = $getSelection() + if ($isRangeSelection(selection)) { + const domSelection = window.getSelection() + if (domSelection && domSelection.rangeCount > 0) + lastSelectionRef.current = domSelection.getRangeAt(0).cloneRange() + } + }) + }) + }, [editor]) + + useEffect(() => { + if (!open) + return + + const onMouseDown = (e: MouseEvent) => { + if (!portalRef.current) + return + if (!portalRef.current.contains(e.target as Node)) + closePortal() + } + document.addEventListener('mousedown', onMouseDown, false) + return () => document.removeEventListener('mousedown', onMouseDown, false) + }, [open, closePortal]) + + if (!open || !containerEl) + return null + + return createPortal( +
{ + portalRef.current = node + refs.setFloating(node) + }} + className={cn( + useContainer ? '' : 'z-[999999]', + 'absolute rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', + className, + )} + style={{ + ...floatingStyles, + visibility: isPositioned ? 'visible' : 'hidden', + }} + > + Memory Popup +
, + containerEl, + ) +} diff --git a/web/app/components/base/prompt-editor/plugins/update-block.tsx b/web/app/components/base/prompt-editor/plugins/update-block.tsx index 89c93748fb..201de93469 100644 --- a/web/app/components/base/prompt-editor/plugins/update-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/update-block.tsx @@ -1,8 +1,11 @@ -import { $insertNodes } from 'lexical' +import { + $insertNodes, +} from 'lexical' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { textToEditorState } from '../utils' import { CustomTextNode } from './custom-text/node' import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block' +import { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER } from '../../../workflow/nodes/_base/components/prompt/add-memory-button' import { useEventEmitterContextContext } from '@/context/event-emitter' export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER' @@ -36,6 +39,18 @@ const UpdateBlock = ({ } }) + eventEmitter?.useSubscription((v: any) => { + if (v.type === MEMORY_POPUP_SHOW_BY_EVENT_EMITTER && v.instanceId === instanceId) { + editor.focus() + editor.update(() => { + const textNode = new CustomTextNode('') + $insertNodes([textNode]) + + editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined) + }) + } + }) + return null } diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx index 3caf6efa3b..72eb990dd3 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx @@ -66,7 +66,7 @@ const WorkflowVariableBlockComponent = ({ const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState(workflowNodesMap) const node = localWorkflowNodesMap![variables[isRagVar ? 1 : 0]] const isEnv = isENV(variables) - const isChatVar = isConversationVar(variables) && conversationVariables?.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}` && v.type !== 'memory') + // const isChatVar = isConversationVar(variables) && conversationVariables?.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}` && v.type !== 'memory') const isMemoryVar = isConversationVar(variables) && conversationVariables?.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}` && v.type === 'memory') const isException = isExceptionVariable(varName, node?.type) let variableValid = true @@ -74,7 +74,7 @@ const WorkflowVariableBlockComponent = ({ if (environmentVariables) variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`) } - else if (isChatVar) { + else if (isConversationVar(variables)) { if (conversationVariables) variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`) } diff --git a/web/app/components/workflow/nodes/_base/components/prompt/add-memory-button.tsx b/web/app/components/workflow/nodes/_base/components/prompt/add-memory-button.tsx new file mode 100644 index 0000000000..fa0594fd82 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/prompt/add-memory-button.tsx @@ -0,0 +1,30 @@ +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import { Memory } from '@/app/components/base/icons/src/vender/line/others' + +export const MEMORY_POPUP_SHOW_BY_EVENT_EMITTER = 'MEMORY_POPUP_SHOW_BY_EVENT_EMITTER' +export const PROMPT_EDITOR_INSERT_QUICKLY = 'PROMPT_EDITOR_INSERT_QUICKLY' + +type Props = { + onAddMemory: () => void +} + +const AddMemoryButton = ({ onAddMemory }: Props) => { + const { t } = useTranslation() + + return ( +
+ +
+ ) +} + +export default AddMemoryButton diff --git a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx index 3462a95b1d..e9c89d0cb8 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx @@ -36,8 +36,7 @@ import Switch from '@/app/components/base/switch' import { Jinja } from '@/app/components/base/icons/src/vender/workflow' import { useStore } from '@/app/components/workflow/store' import { useWorkflowVariableType } from '@/app/components/workflow/hooks' -import Button from '@/app/components/base/button' -import { Memory } from '@/app/components/base/icons/src/vender/line/others' +import AddMemoryButton, { MEMORY_POPUP_SHOW_BY_EVENT_EMITTER } from './add-memory-button' type Props = { className?: string @@ -157,6 +156,11 @@ const Editor: FC = ({ const pipelineId = useStore(s => s.pipelineId) const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel) + const handleAddMemory = () => { + setFocus() + eventEmitter?.emit({ type: MEMORY_POPUP_SHOW_BY_EVENT_EMITTER, instanceId } as any) + } + return (
@@ -296,18 +300,12 @@ const Editor: FC = ({ onFocus={setFocus} editable={!readOnly} isSupportFileVar={isSupportFileVar} + isMemorySupported /> {/* to patch Editor not support dynamic change editable status */} {readOnly &&
}
- {isMemorySupported && isFocus && ( -
- -
- )} + {isMemorySupported && } ) : ( diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index 117328897d..4d09b0e21d 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -211,6 +211,7 @@ const Panel: FC> = ({ nodesOutputVars={availableVars} availableNodes={availableNodesWithParent} isSupportFileVar + instanceId={`${id}-chat-workflow-llm-prompt-editor-user`} /> {inputs.memory.query_prompt_template && !inputs.memory.query_prompt_template.includes('{{#sys.query#}}') && (