diff --git a/web/.env.example b/web/.env.example index 079c3bdeef7..5c89d165a6b 100644 --- a/web/.env.example +++ b/web/.env.example @@ -14,6 +14,8 @@ NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api # When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1. NEXT_PUBLIC_COOKIE_DOMAIN= +# WebSocket server URL. +NEXT_PUBLIC_SOCKET_URL=ws://localhost:5001 # Dev-only Hono proxy targets. # The frontend keeps requesting http://localhost:5001 directly, diff --git a/web/app/components/base/prompt-editor/__tests__/sandbox-placeholder.spec.tsx b/web/app/components/base/prompt-editor/__tests__/sandbox-placeholder.spec.tsx index 20008381206..3c36380d25d 100644 --- a/web/app/components/base/prompt-editor/__tests__/sandbox-placeholder.spec.tsx +++ b/web/app/components/base/prompt-editor/__tests__/sandbox-placeholder.spec.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen } from '@testing-library/react' -import { CLEAR_HIDE_MENU_TIMEOUT } from '../plugins/workflow-variable-block' import SandboxPlaceholder from '../sandbox-placeholder' const mocks = vi.hoisted(() => { @@ -100,7 +99,6 @@ describe('SandboxPlaceholder', () => { expect(mocks.selectEnd).toHaveBeenCalledTimes(1) expect(mocks.createTextNode).toHaveBeenCalledWith('/') expect(mocks.insertNodes).toHaveBeenCalledWith([{ text: '/' }]) - expect(mocks.editor.dispatchCommand).toHaveBeenCalledWith(CLEAR_HIDE_MENU_TIMEOUT, undefined) }) it('should insert at-sign and clear the hide timeout when clicking tools', () => { @@ -113,7 +111,6 @@ describe('SandboxPlaceholder', () => { expect(mocks.selectEnd).toHaveBeenCalledTimes(1) expect(mocks.createTextNode).toHaveBeenCalledWith('@') expect(mocks.insertNodes).toHaveBeenCalledWith([{ text: '@' }]) - expect(mocks.editor.dispatchCommand).toHaveBeenCalledWith(CLEAR_HIDE_MENU_TIMEOUT, undefined) }) it('should not trigger editor insertion when placeholder is not editable', () => { diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx index c19cfb885bc..b4ff8a05e57 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx @@ -39,9 +39,9 @@ import { Fragment, memo, useCallback, + useEffect, useLayoutEffect, useMemo, - useEffect, useRef, useState, } from 'react' diff --git a/web/app/components/base/prompt-editor/sandbox-placeholder.tsx b/web/app/components/base/prompt-editor/sandbox-placeholder.tsx index 21d1ff93744..b46eefc3d3b 100644 --- a/web/app/components/base/prompt-editor/sandbox-placeholder.tsx +++ b/web/app/components/base/prompt-editor/sandbox-placeholder.tsx @@ -5,7 +5,6 @@ import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { cn } from '@/utils/classnames' import { $createCustomTextNode } from './plugins/custom-text/node' -import { CLEAR_HIDE_MENU_TIMEOUT } from './plugins/workflow-variable-block' type SandboxPlaceholderTokenProps = { actionLabel?: string @@ -74,7 +73,6 @@ const SandboxPlaceholder: FC = ({ editor.update(() => { $getRoot().selectEnd() $insertNodes([$createCustomTextNode(trigger)]) - editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined) }) }) }, [editor]) diff --git a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-block.tsx b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-block.tsx index dc07bef1696..5fe30c82f13 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-block.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-block.tsx @@ -14,6 +14,7 @@ import { } from '@/app/components/base/ui/popover' import { FilePickerPanel } from './file-picker-panel' import { $createFileReferenceNode } from './file-reference-block/node' +import { useEditorBlur } from './hooks/use-editor-blur' class FilePickerMenuOption extends MenuOption { constructor() { @@ -30,6 +31,8 @@ const FilePickerBlock = () => { const options = useMemo(() => [new FilePickerMenuOption()], []) + const { blurHidden } = useEditorBlur(editor) + const insertFileReference = useCallback((resourceId: string) => { editor.update(() => { const match = checkForTriggerMatch('/', editor) @@ -46,6 +49,8 @@ const FilePickerBlock = () => { anchorElementRef: React.RefObject, { selectOptionAndCleanUp }: { selectOptionAndCleanUp: (option: MenuOption) => void }, ) => { + if (blurHidden) + return null if (!anchorElementRef.current) return null @@ -75,7 +80,7 @@ const FilePickerBlock = () => { ) - }, [insertFileReference, options]) + }, [blurHidden, insertFileReference, options]) return ( { + const [blurHidden, setBlurHidden] = useState(false) + const blurTimerRef = useRef | null>(null) + + const clearBlurTimer = useCallback(() => { + if (blurTimerRef.current) { + clearTimeout(blurTimerRef.current) + blurTimerRef.current = null + } + }, []) + + useEffect(() => { + const unregister = mergeRegister( + editor.registerCommand( + BLUR_COMMAND, + () => { + clearBlurTimer() + blurTimerRef.current = setTimeout(() => setBlurHidden(true), 200) + return false + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + FOCUS_COMMAND, + () => { + clearBlurTimer() + setBlurHidden(false) + return false + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + + return () => { + if (blurTimerRef.current) + clearTimeout(blurTimerRef.current) + unregister() + } + }, [editor, clearBlurTimer]) + + return { + blurHidden, + } +} diff --git a/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/tool-picker-block.tsx b/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/tool-picker-block.tsx index e04d33ff945..9f0447681b8 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/tool-picker-block.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/tool-picker-block.tsx @@ -21,6 +21,7 @@ import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-for import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' import { START_TAB_ID } from '@/app/components/workflow/skill/constants' import { useWorkflowStore } from '@/app/components/workflow/store' +import { useEditorBlur } from '../hooks/use-editor-blur' import { $createToolBlockNode } from './node' import { useToolBlockContext } from './tool-block-context' import { $createToolGroupBlockNode } from './tool-group-block-node' @@ -52,6 +53,8 @@ const ToolPickerBlock = ({ scope = 'all', enableAutoDefault = false }: ToolPicke const isUsingExternalMetadata = Boolean(onMetadataChange) const [queryString, setQueryString] = useState('') + const { blurHidden } = useEditorBlur(editor) + const canUseAutoByType = useCallback( (type: string) => ![FormTypeEnum.modelSelector, FormTypeEnum.appSelector].includes(type as FormTypeEnum), [], @@ -159,6 +162,8 @@ const ToolPickerBlock = ({ scope = 'all', enableAutoDefault = false }: ToolPicke anchorElementRef: React.RefObject, { selectOptionAndCleanUp }: { selectOptionAndCleanUp: (option: MenuOption) => void }, ) => { + if (blurHidden) + return null if (!anchorElementRef.current) return null @@ -199,7 +204,7 @@ const ToolPickerBlock = ({ scope = 'all', enableAutoDefault = false }: ToolPicke />, anchorElementRef.current, ) - }, [insertTools, options, queryString, scope]) + }, [blurHidden, insertTools, options, queryString, scope]) return (