diff --git a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx index cc5184e342..fe382565d5 100644 --- a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx @@ -216,10 +216,13 @@ const AdvancedPromptInput: FC = ({ onAddContext: showSelectDataSet, }} variableBlock={{ + show: true, variables: modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ name: item.name, value: item.key, })), + }} + externalToolBlock={{ externalTools: modelConfig.configs.prompt_variables.filter(item => item.type === 'api').map(item => ({ name: item.name, variableName: item.key, diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index 6ce25b755a..5bc923e040 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -178,10 +178,14 @@ const Prompt: FC = ({ onAddContext: showSelectDataSet, }} variableBlock={{ + show: true, variables: modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ name: item.name, value: item.key, })), + }} + externalToolBlock={{ + show: true, externalTools: modelConfig.configs.prompt_variables.filter(item => item.type === 'api').map(item => ({ name: item.name, variableName: item.key, diff --git a/web/app/components/app/configuration/config/agent/prompt-editor.tsx b/web/app/components/app/configuration/config/agent/prompt-editor.tsx index 6c2610921f..f3fed4ee74 100644 --- a/web/app/components/app/configuration/config/agent/prompt-editor.tsx +++ b/web/app/components/app/configuration/config/agent/prompt-editor.tsx @@ -102,10 +102,14 @@ const Editor: FC = ({ onAddContext: showSelectDataSet, }} variableBlock={{ + show: true, variables: modelConfig.configs.prompt_variables.map(item => ({ name: item.name, value: item.key, })), + }} + externalToolBlock={{ + show: true, externalTools: externalDataToolsConfig.map(item => ({ name: item.label!, variableName: item.variable!, diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx index 362da77d0c..568d1c4ba8 100644 --- a/web/app/components/base/prompt-editor/index.tsx +++ b/web/app/components/base/prompt-editor/index.tsx @@ -16,19 +16,26 @@ import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' // import TreeView from './plugins/tree-view' import Placeholder from './plugins/placeholder' -import ComponentPicker from './plugins/component-picker' -import VariablePicker from './plugins/variable-picker' -import ContextBlock from './plugins/context-block' -import { ContextBlockNode } from './plugins/context-block/node' -import ContextBlockReplacementBlock from './plugins/context-block-replacement-block' -import HistoryBlock from './plugins/history-block' -import { HistoryBlockNode } from './plugins/history-block/node' -import HistoryBlockReplacementBlock from './plugins/history-block-replacement-block' -import QueryBlock from './plugins/query-block' -import { QueryBlockNode } from './plugins/query-block/node' -import WorkflowVariableBlock from './plugins/workflow-variable-block' -import { WorkflowVariableBlockNode } from './plugins/workflow-variable-block/node' -import QueryBlockReplacementBlock from './plugins/query-block-replacement-block' +import ComponentPickerBlock from './plugins/component-picker-block' +import { + ContextBlock, + ContextBlockNode, + ContextBlockReplacementBlock, +} from './plugins/context-block' +import { + QueryBlock, + QueryBlockNode, + QueryBlockReplacementBlock, +} from './plugins/query-block' +import { + HistoryBlock, + HistoryBlockNode, + HistoryBlockReplacementBlock, +} from './plugins/history-block' +import { + WorkflowVariableBlock, + WorkflowVariableBlockNode, +} from './plugins/workflow-variable-block' import VariableBlock from './plugins/variable-block' import VariableValueBlock from './plugins/variable-value-block' import { VariableValueBlockNode } from './plugins/variable-value-block/node' @@ -36,18 +43,19 @@ import { CustomTextNode } from './plugins/custom-text/node' import OnBlurBlock from './plugins/on-blur-or-focus-block' import UpdateBlock from './plugins/update-block' import { textToEditorState } from './utils' -import type { Dataset } from './plugins/context-block' -import type { RoleName } from './plugins/history-block' -import type { ExternalToolOption, Option } from './plugins/variable-picker' +import type { + ContextBlockType, + ExternalToolBlockType, + HistoryBlockType, + QueryBlockType, + VariableBlockType, + WorkflowVariableBlockType, +} from './types' import { UPDATE_DATASETS_EVENT_EMITTER, UPDATE_HISTORY_EVENT_EMITTER, } from './constants' import { useEventEmitterContextContext } from '@/context/event-emitter' -import type { - Node, - NodeOutPutVar, -} from '@/app/components/workflow/types' export type PromptEditorProps = { className?: string @@ -55,47 +63,15 @@ export type PromptEditorProps = { style?: React.CSSProperties value?: string editable?: boolean - outToolDisabled?: boolean - canNotAddContext?: boolean onChange?: (text: string) => void onBlur?: () => void onFocus?: () => void - contextBlock?: { - show?: boolean - selectable?: boolean - datasets: Dataset[] - onInsert?: () => void - onDelete?: () => void - onAddContext: () => void - } - variableBlock?: { - selectable?: boolean - variables: Option[] - externalTools?: ExternalToolOption[] - onAddExternalTool?: () => void - } - historyBlock?: { - show?: boolean - selectable?: boolean - history: RoleName - onInsert?: () => void - onDelete?: () => void - onEditRole: () => void - } - queryBlock?: { - show?: boolean - selectable?: boolean - onInsert?: () => void - onDelete?: () => void - } - workflowVariableBlock?: { - show?: boolean - selectable?: boolean - variables: NodeOutPutVar[] - getWorkflowNode: (nodeId: string) => Node | undefined - onInsert?: () => void - onDelete?: () => void - } + contextBlock?: ContextBlockType + queryBlock?: QueryBlockType + historyBlock?: HistoryBlockType + variableBlock?: VariableBlockType + externalToolBlock?: ExternalToolBlockType + workflowVariableBlock?: WorkflowVariableBlockType } const PromptEditor: FC = ({ @@ -104,47 +80,15 @@ const PromptEditor: FC = ({ style, value, editable = true, - outToolDisabled = false, - canNotAddContext = false, onChange, onBlur, onFocus, - contextBlock = { - show: true, - selectable: true, - datasets: [], - onAddContext: () => { }, - onInsert: () => { }, - onDelete: () => { }, - }, - historyBlock = { - show: true, - selectable: true, - history: { - user: '', - assistant: '', - }, - onEditRole: () => { }, - onInsert: () => { }, - onDelete: () => { }, - }, - variableBlock = { - variables: [], - }, - queryBlock = { - show: true, - selectable: true, - onInsert: () => { }, - onDelete: () => { }, - }, - workflowVariableBlock = { - show: true, - selectable: true, - variables: [], - getWorkflowNode: () => { }, - onInsert: () => { }, - onDelete: () => { }, - }, + contextBlock, + queryBlock, + historyBlock, + variableBlock, + externalToolBlock, + workflowVariableBlock, }) => { const { eventEmitter } = useEventEmitterContextContext() const initialConfig = { @@ -176,15 +120,15 @@ const PromptEditor: FC = ({ useEffect(() => { eventEmitter?.emit({ type: UPDATE_DATASETS_EVENT_EMITTER, - payload: contextBlock.datasets, + payload: contextBlock?.datasets, } as any) - }, [eventEmitter, contextBlock.datasets]) + }, [eventEmitter, contextBlock?.datasets]) useEffect(() => { eventEmitter?.emit({ type: UPDATE_HISTORY_EVENT_EMITTER, - payload: historyBlock.history, + payload: historyBlock?.history, } as any) - }, [eventEmitter, historyBlock.history]) + }, [eventEmitter, historyBlock?.history]) return ( @@ -194,83 +138,63 @@ const PromptEditor: FC = ({ placeholder={} ErrorBoundary={LexicalErrorBoundary} /> - - { - contextBlock.show && ( + contextBlock?.show && ( <> - - - - ) - } - - { - historyBlock.show && ( - <> - - + + ) } { - queryBlock.show && ( + queryBlock?.show && ( <> - + ) } { - workflowVariableBlock.show && ( + historyBlock?.show && ( <> - + + + + ) + } + { + (variableBlock?.show || externalToolBlock?.show) && ( + <> + + + + ) + } + { + workflowVariableBlock?.show && ( + <> + ) } - diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/external-tool-option.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/external-tool-option.tsx new file mode 100644 index 0000000000..ffaf08a0f5 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/external-tool-option.tsx @@ -0,0 +1,85 @@ +import { memo } from 'react' +import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin' + +export class VariableOption extends MenuOption { + title: string + icon?: JSX.Element + extraElement?: JSX.Element + keywords: Array + keyboardShortcut?: string + onSelect: (queryString: string) => void + + constructor( + title: string, + options: { + icon?: JSX.Element + extraElement?: JSX.Element + keywords?: Array + keyboardShortcut?: string + onSelect: (queryString: string) => void + }, + ) { + super(title) + this.title = title + this.keywords = options.keywords || [] + this.icon = options.icon + this.extraElement = options.extraElement + this.keyboardShortcut = options.keyboardShortcut + this.onSelect = options.onSelect.bind(this) + } +} + +type VariableMenuItemProps = { + isSelected: boolean + onClick: () => void + onMouseEnter: () => void + option: VariableOption + queryString: string | null +} +export const VariableMenuItem = memo(({ + isSelected, + onClick, + onMouseEnter, + option, + queryString, +}: VariableMenuItemProps) => { + const title = option.title + let before = title + let middle = '' + let after = '' + + if (queryString) { + const regex = new RegExp(queryString, 'i') + const match = regex.exec(option.title) + + if (match) { + before = title.substring(0, match.index) + middle = match[0] + after = title.substring(match.index + match[0].length) + } + } + + return ( +
+
+ {option.icon} +
+
+ {before} + {middle} + {after} +
+ {option.extraElement} +
+ ) +}) +VariableMenuItem.displayName = 'VariableMenuItem' diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx new file mode 100644 index 0000000000..89c6cb9c0c --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx @@ -0,0 +1,192 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { $insertNodes } from 'lexical' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { + ContextBlockType, + ExternalToolBlockType, + HistoryBlockType, + QueryBlockType, + VariableBlockType, +} from '../../types' +import { INSERT_CONTEXT_BLOCK_COMMAND } from '../context-block' +import { INSERT_HISTORY_BLOCK_COMMAND } from '../history-block' +import { INSERT_QUERY_BLOCK_COMMAND } from '../query-block' +import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block' +import { $createCustomTextNode } from '../custom-text/node' +import { PromptOption } from './prompt-option' +import { VariableOption } from './variable-option' +import { File05 } from '@/app/components/base/icons/src/vender/solid/files' +import { + MessageClockCircle, + Tool03, +} from '@/app/components/base/icons/src/vender/solid/general' +import { BracketsX } from '@/app/components/base/icons/src/vender/line/development' +import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users' +import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows' +import AppIcon from '@/app/components/base/app-icon' + +export const usePromptOptions = ( + contextBlock?: ContextBlockType, + queryBlock?: QueryBlockType, + historyBlock?: HistoryBlockType, +) => { + const { t } = useTranslation() + const [editor] = useLexicalComposerContext() + + return useMemo(() => { + return [ + ...contextBlock?.show + ? [ + new PromptOption(t('common.promptEditor.context.item.title'), { + icon: , + onSelect: () => { + if (contextBlock?.selectable) + return + editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined) + }, + disabled: contextBlock?.selectable, + }), + ] + : [], + ...queryBlock?.show + ? [ + new PromptOption(t('common.promptEditor.query.item.title'), { + icon: , + onSelect: () => { + if (queryBlock?.selectable) + return + editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined) + }, + disabled: queryBlock?.selectable, + }), + ] + : [], + ...historyBlock?.show + ? [ + new PromptOption(t('common.promptEditor.history.item.title'), { + icon: , + onSelect: () => { + if (historyBlock?.selectable) + return + editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined) + }, + disabled: historyBlock?.selectable, + }), + ] + : [], + ] + }, [contextBlock, editor, historyBlock, queryBlock, t]) +} + +export const useVariableOptions = ( + variableBlock?: VariableBlockType, + queryString?: string, +) => { + const { t } = useTranslation() + const [editor] = useLexicalComposerContext() + + const options = useMemo(() => { + const baseOptions = (variableBlock?.variables || []).map((item) => { + return new VariableOption(item.value, { + icon: , + onSelect: () => { + editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`) + }, + }) + }) + if (!queryString) + return baseOptions + + const regex = new RegExp(queryString, 'i') + + return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword))) + }, [editor, queryString, variableBlock]) + + const addOption = useMemo(() => { + return new VariableOption(t('common.promptEditor.variable.modal.add'), { + icon: , + onSelect: () => { + editor.update(() => { + const prefixNode = $createCustomTextNode('{{') + const suffixNode = $createCustomTextNode('}}') + $insertNodes([prefixNode, suffixNode]) + prefixNode.select() + }) + }, + }) + }, [editor, t]) + + return useMemo(() => { + return variableBlock?.show ? [...options, addOption] : [] + }, [options, addOption, variableBlock?.show]) +} + +export const useExternalToolOptions = ( + externalToolBlockType?: ExternalToolBlockType, + queryString?: string, +) => { + const { t } = useTranslation() + const [editor] = useLexicalComposerContext() + + const options = useMemo(() => { + const baseToolOptions = (externalToolBlockType?.externalTools || []).map((item) => { + return new VariableOption(item.name, { + icon: ( + + ), + extraElement:
{item.variableName}
, + onSelect: () => { + editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`) + }, + }) + }) + if (!queryString) + return baseToolOptions + + const regex = new RegExp(queryString, 'i') + + return baseToolOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword))) + }, [editor, queryString, externalToolBlockType]) + + const addOption = useMemo(() => { + return new VariableOption(t('common.promptEditor.variable.modal.addTool'), { + icon: , + extraElement: , + onSelect: () => { + if (externalToolBlockType?.onAddExternalTool) + externalToolBlockType.onAddExternalTool() + }, + }) + }, [externalToolBlockType, t]) + + return useMemo(() => { + return externalToolBlockType?.show ? [...options, addOption] : [] + }, [options, addOption, externalToolBlockType?.show]) +} + +export const useOptions = ( + contextBlock?: ContextBlockType, + queryBlock?: QueryBlockType, + historyBlock?: HistoryBlockType, + variableBlock?: VariableBlockType, + externalToolBlockType?: ExternalToolBlockType, + queryString?: string, +) => { + const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock) + const variableOptions = useVariableOptions(variableBlock, queryString) + const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString) + + return useMemo(() => { + return { + promptOptions, + variableOptions, + externalToolOptions, + allOptions: [...promptOptions, ...variableOptions, ...externalToolOptions], + } + }, [promptOptions, variableOptions, externalToolOptions]) +} 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 new file mode 100644 index 0000000000..a27fb5e44e --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx @@ -0,0 +1,226 @@ +import { + memo, + useCallback, + useState, +} from 'react' +import ReactDOM from 'react-dom' +import type { TextNode } from 'lexical' +import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin' +import type { + ContextBlockType, + ExternalToolBlockType, + HistoryBlockType, + QueryBlockType, + VariableBlockType, + WorkflowVariableBlockType, +} from '../../types' +import { useBasicTypeaheadTriggerMatch } from '../../hooks' +import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block' +import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block' +import type { PromptOption } from './prompt-option' +import PromptMenu from './prompt-menu' +import VariableMenu from './variable-menu' +import type { VariableOption } from './variable-option' +import { useOptions } from './hooks' +import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' +import { useEventEmitterContextContext } from '@/context/event-emitter' + +type ComponentPickerProps = { + triggerString: string + contextBlock?: ContextBlockType + queryBlock?: QueryBlockType + historyBlock?: HistoryBlockType + variableBlock?: VariableBlockType + externalToolBlock?: ExternalToolBlockType + workflowVariableBlock?: WorkflowVariableBlockType +} +const ComponentPicker = ({ + triggerString, + contextBlock, + queryBlock, + historyBlock, + variableBlock, + externalToolBlock, + workflowVariableBlock, +}: ComponentPickerProps) => { + const { eventEmitter } = useEventEmitterContextContext() + const [editor] = useLexicalComposerContext() + const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, { + minLength: 0, + maxLength: 0, + }) + + const [queryString, setQueryString] = useState(null) + + eventEmitter?.useSubscription((v: any) => { + if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND) + editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`) + }) + + const { + allOptions, + promptOptions, + variableOptions, + externalToolOptions, + } = useOptions( + contextBlock, + queryBlock, + historyBlock, + variableBlock, + externalToolBlock, + ) + + const onSelectOption = useCallback( + ( + selectedOption: PromptOption | VariableOption, + nodeToRemove: TextNode | null, + closeMenu: () => void, + matchingString: string, + ) => { + editor.update(() => { + if (nodeToRemove) + nodeToRemove.remove() + + selectedOption.onSelect(matchingString) + closeMenu() + }) + }, + [editor], + ) + + const handleSelectWorkflowVariable = useCallback((variables: string[]) => { + editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables) + }, [editor]) + + const renderMenu = useCallback>(( + anchorElementRef, + { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }, + ) => { + if (anchorElementRef.current && allOptions.length) { + return ReactDOM.createPortal( +
+ { + !!promptOptions.length && ( + <> + { + if (option.disabled) + return + setHighlightedIndex(index) + selectOptionAndCleanUp(option) + }} + onMouseEnter={(index, option) => { + if (option.disabled) + return + setHighlightedIndex(index) + }} + /> + + ) + } + { + !!variableOptions.length && ( + <> + { + !!promptOptions.length && ( +
+ ) + } + { + if (option.disabled) + return + setHighlightedIndex(index) + selectOptionAndCleanUp(option) + }} + onMouseEnter={(index, option) => { + if (option.disabled) + return + setHighlightedIndex(index) + }} + queryString={queryString} + /> + + ) + } + { + !!externalToolOptions.length && ( + <> + { + (!!promptOptions.length || !!variableOptions.length) && ( +
+ ) + } + { + if (option.disabled) + return + setHighlightedIndex(index) + selectOptionAndCleanUp(option) + }} + onMouseEnter={(index, option) => { + if (option.disabled) + return + setHighlightedIndex(index) + }} + queryString={queryString} + /> + + ) + } + { + workflowVariableBlock?.show && !!workflowVariableBlock?.variables?.length && ( + <> + { + (!!promptOptions.length || !!variableOptions.length || !!externalToolOptions.length) && ( +
+ ) + } + + + ) + } +
, + anchorElementRef.current, + ) + } + + return null + }, [ + allOptions, + promptOptions, + variableOptions, + externalToolOptions, + queryString, + workflowVariableBlock, + handleSelectWorkflowVariable, + ]) + + return ( + + ) +} + +export default memo(ComponentPicker) diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-menu.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-menu.tsx new file mode 100644 index 0000000000..6f16fcc2ba --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-menu.tsx @@ -0,0 +1,37 @@ +import { memo } from 'react' +import { PromptMenuItem } from './prompt-option' + +type PromptMenuProps = { + startIndex: number + selectedIndex: number | null + options: any[] + onClick: (index: number, option: any) => void + onMouseEnter: (index: number, option: any) => void +} +const PromptMenu = ({ + startIndex, + selectedIndex, + options, + onClick, + onMouseEnter, +}: PromptMenuProps) => { + return ( +
+ { + options.map((option, index: number) => ( + + )) + } +
+ ) +} + +export default memo(PromptMenu) diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx new file mode 100644 index 0000000000..3d39171a26 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx @@ -0,0 +1,65 @@ +import { memo } from 'react' +import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin' + +export class PromptOption extends MenuOption { + title: string + icon?: JSX.Element + keywords: Array + keyboardShortcut?: string + onSelect: (queryString: string) => void + disabled?: boolean + + constructor( + title: string, + options: { + icon?: JSX.Element + keywords?: Array + keyboardShortcut?: string + onSelect: (queryString: string) => void + disabled?: boolean + }, + ) { + super(title) + this.title = title + this.keywords = options.keywords || [] + this.icon = options.icon + this.keyboardShortcut = options.keyboardShortcut + this.onSelect = options.onSelect.bind(this) + this.disabled = options.disabled + } +} + +type PromptMenuItemMenuItemProps = { + startIndex: number + index: number + isSelected: boolean + onClick: (index: number, option: PromptOption) => void + onMouseEnter: (index: number, option: PromptOption) => void + option: PromptOption +} +export const PromptMenuItem = memo(({ + startIndex, + index, + isSelected, + onClick, + onMouseEnter, + option, +}: PromptMenuItemMenuItemProps) => { + return ( +
onMouseEnter(index + startIndex, option)} + onClick={() => onClick(index + startIndex, option)}> + {option.icon} +
{option.title}
+
+ ) +}) +PromptMenuItem.displayName = 'PromptMenuItem' diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-menu.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-menu.tsx new file mode 100644 index 0000000000..fefd93cb0f --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-menu.tsx @@ -0,0 +1,40 @@ +import { memo } from 'react' +import { VariableMenuItem } from './variable-option' + +type VariableMenuProps = { + startIndex: number + selectedIndex: number | null + options: any[] + onClick: (index: number, option: any) => void + onMouseEnter: (index: number, option: any) => void + queryString: string | null +} +const VariableMenu = ({ + startIndex, + selectedIndex, + options, + onClick, + onMouseEnter, + queryString, +}: VariableMenuProps) => { + return ( +
+ { + options.map((option, index: number) => ( + + )) + } +
+ ) +} + +export default memo(VariableMenu) diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-option.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-option.tsx new file mode 100644 index 0000000000..76f76c8491 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-option.tsx @@ -0,0 +1,89 @@ +import { memo } from 'react' +import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin' + +export class VariableOption extends MenuOption { + title: string + icon?: JSX.Element + extraElement?: JSX.Element + keywords: Array + keyboardShortcut?: string + onSelect: (queryString: string) => void + + constructor( + title: string, + options: { + icon?: JSX.Element + extraElement?: JSX.Element + keywords?: Array + keyboardShortcut?: string + onSelect: (queryString: string) => void + }, + ) { + super(title) + this.title = title + this.keywords = options.keywords || [] + this.icon = options.icon + this.extraElement = options.extraElement + this.keyboardShortcut = options.keyboardShortcut + this.onSelect = options.onSelect.bind(this) + } +} + +type VariableMenuItemProps = { + startIndex: number + index: number + isSelected: boolean + onClick: (index: number, option: VariableOption) => void + onMouseEnter: (index: number, option: VariableOption) => void + option: VariableOption + queryString: string | null +} +export const VariableMenuItem = memo(({ + startIndex, + index, + isSelected, + onClick, + onMouseEnter, + option, + queryString, +}: VariableMenuItemProps) => { + const title = option.title + let before = title + let middle = '' + let after = '' + + if (queryString) { + const regex = new RegExp(queryString, 'i') + const match = regex.exec(option.title) + + if (match) { + before = title.substring(0, match.index) + middle = match[0] + after = title.substring(match.index + match[0].length) + } + } + + return ( +
onMouseEnter(index + startIndex, option)} + onClick={() => onClick(index + startIndex, option)}> +
+ {option.icon} +
+
+ {before} + {middle} + {after} +
+ {option.extraElement} +
+ ) +}) +VariableMenuItem.displayName = 'VariableMenuItem' diff --git a/web/app/components/base/prompt-editor/plugins/component-picker.tsx b/web/app/components/base/prompt-editor/plugins/component-picker.tsx deleted file mode 100644 index 0faa499bd4..0000000000 --- a/web/app/components/base/prompt-editor/plugins/component-picker.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import type { FC } from 'react' -import { useCallback } from 'react' -import ReactDOM from 'react-dom' -import { useTranslation } from 'react-i18next' -import type { TextNode } from 'lexical' -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' -import { - LexicalTypeaheadMenuPlugin, - MenuOption, -} from '@lexical/react/LexicalTypeaheadMenuPlugin' -import { useBasicTypeaheadTriggerMatch } from '../hooks' -import { INSERT_CONTEXT_BLOCK_COMMAND } from './context-block' -import { INSERT_VARIABLE_BLOCK_COMMAND } from './variable-block' -import { INSERT_HISTORY_BLOCK_COMMAND } from './history-block' -import { INSERT_QUERY_BLOCK_COMMAND } from './query-block' -import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from './workflow-variable-block' -import { File05 } from '@/app/components/base/icons/src/vender/solid/files' -import { Variable } from '@/app/components/base/icons/src/vender/line/development' -import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general' -import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users' -import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' -import type { NodeOutPutVar } from '@/app/components/workflow/types' - -class ComponentPickerOption extends MenuOption { - title: string - icon?: JSX.Element - keywords: Array - keyboardShortcut?: string - desc: string - onSelect: (queryString: string) => void - disabled?: boolean - - constructor( - title: string, - options: { - icon?: JSX.Element - keywords?: Array - keyboardShortcut?: string - desc: string - onSelect: (queryString: string) => void - disabled?: boolean - }, - ) { - super(title) - this.title = title - this.keywords = options.keywords || [] - this.icon = options.icon - this.keyboardShortcut = options.keyboardShortcut - this.desc = options.desc - this.onSelect = options.onSelect.bind(this) - this.disabled = options.disabled - } -} - -type ComponentPickerMenuItemProps = { - isSelected: boolean - onClick: () => void - onMouseEnter: () => void - option: ComponentPickerOption -} -const ComponentPickerMenuItem: FC = ({ - isSelected, - onClick, - onMouseEnter, - option, -}) => { - const { t } = useTranslation() - - return ( -
-
- {option.icon} -
-
-
- {option.title} - {option.disabled && t('common.promptEditor.existed')} -
-
{option.desc}
-
-
- ) -} - -type ComponentPickerProps = { - contextDisabled?: boolean - historyDisabled?: boolean - queryDisabled?: boolean - contextShow?: boolean - historyShow?: boolean - queryShow?: boolean - outToolDisabled?: boolean - workflowVariableShow?: boolean - workflowVariables?: NodeOutPutVar[] -} -const ComponentPicker: FC = ({ - contextDisabled, - historyDisabled, - queryDisabled, - contextShow, - historyShow, - queryShow, - outToolDisabled, - workflowVariableShow, - workflowVariables, -}) => { - const { t } = useTranslation() - const [editor] = useLexicalComposerContext() - const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', { - minLength: 0, - maxLength: 0, - }) - - const options = [ - ...contextShow - ? [ - new ComponentPickerOption(t('common.promptEditor.context.item.title'), { - desc: t('common.promptEditor.context.item.desc'), - icon: , - onSelect: () => { - if (contextDisabled) - return - editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined) - }, - disabled: contextDisabled, - }), - ] - : [], - new ComponentPickerOption(t(`common.promptEditor.variable.${!outToolDisabled ? 'item' : 'outputToolDisabledItem'}.title`), { - desc: t(`common.promptEditor.variable.${!outToolDisabled ? 'item' : 'outputToolDisabledItem'}.desc`), - icon: , - onSelect: () => { - editor.dispatchCommand(INSERT_VARIABLE_BLOCK_COMMAND, undefined) - }, - }), - ...historyShow - ? [ - new ComponentPickerOption(t('common.promptEditor.history.item.title'), { - desc: t('common.promptEditor.history.item.desc'), - icon: , - onSelect: () => { - if (historyDisabled) - return - editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined) - }, - disabled: historyDisabled, - }), - ] - : [], - ...queryShow - ? [ - new ComponentPickerOption(t('common.promptEditor.query.item.title'), { - desc: t('common.promptEditor.query.item.desc'), - icon: , - onSelect: () => { - if (queryDisabled) - return - editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined) - }, - disabled: queryDisabled, - }), - ] - : [], - ] - - const onSelectOption = useCallback( - ( - selectedOption: ComponentPickerOption, - nodeToRemove: TextNode | null, - closeMenu: () => void, - matchingString: string, - ) => { - editor.update(() => { - if (nodeToRemove) - nodeToRemove.remove() - - selectedOption.onSelect(matchingString) - closeMenu() - }) - }, - [editor], - ) - - const handleSelectWorkflowVariable = useCallback((variables: string[]) => { - editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables) - }, [editor]) - - return ( - { }} - onSelectOption={onSelectOption} - anchorClassName='z-[999999]' - menuRenderFn={( - anchorElementRef, - { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }, - ) => - (anchorElementRef.current && options.length) - ? ReactDOM.createPortal( -
- {options.map((option, i: number) => ( - { - if (option.disabled) - return - setHighlightedIndex(i) - selectOptionAndCleanUp(option) - }} - onMouseEnter={() => { - if (option.disabled) - return - setHighlightedIndex(i) - }} - key={option.key} - option={option} - /> - ))} - { - workflowVariableShow && !!workflowVariables?.length && ( - - ) - } -
, - anchorElementRef.current, - ) - : null} - triggerFn={checkForTriggerMatch} - /> - ) -} - -export default ComponentPicker diff --git a/web/app/components/base/prompt-editor/plugins/context-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx similarity index 71% rename from web/app/components/base/prompt-editor/plugins/context-block-replacement-block.tsx rename to web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx index b08c08058f..84327193d3 100644 --- a/web/app/components/base/prompt-editor/plugins/context-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx @@ -1,28 +1,28 @@ -import type { FC } from 'react' import { + memo, useCallback, useEffect, } from 'react' import { $applyNodeReplacement } from 'lexical' import { mergeRegister } from '@lexical/utils' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' -import { decoratorTransform } from '../utils' -import { CONTEXT_PLACEHOLDER_TEXT } from '../constants' +import { decoratorTransform } from '../../utils' +import { CONTEXT_PLACEHOLDER_TEXT } from '../../constants' +import type { ContextBlockType } from '../../types' import { $createContextBlockNode, ContextBlockNode, -} from './context-block/node' -import type { ContextBlockProps } from './context-block/index' -import { CustomTextNode } from './custom-text/node' +} from '../context-block/node' +import { CustomTextNode } from '../custom-text/node' const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT) -const ContextBlockReplacementBlock: FC = ({ - datasets, - onAddContext, +const ContextBlockReplacementBlock = ({ + datasets = [], + onAddContext = () => {}, onInsert, canNotAddContext, -}) => { +}: ContextBlockType) => { const [editor] = useLexicalComposerContext() useEffect(() => { @@ -34,7 +34,7 @@ const ContextBlockReplacementBlock: FC = ({ if (onInsert) onInsert() return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext, canNotAddContext)) - }, [datasets, onAddContext, onInsert]) + }, [datasets, onAddContext, onInsert, canNotAddContext]) const getMatch = useCallback((text: string) => { const matchArr = REGEX.exec(text) @@ -54,9 +54,9 @@ const ContextBlockReplacementBlock: FC = ({ return mergeRegister( editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createContextBlockNode)), ) - }, []) + }, [editor, getMatch, createContextBlockNode]) return null } -export default ContextBlockReplacementBlock +export default memo(ContextBlockReplacementBlock) diff --git a/web/app/components/base/prompt-editor/plugins/context-block/index.tsx b/web/app/components/base/prompt-editor/plugins/context-block/index.tsx index 98f825a7ec..5be4f1fad6 100644 --- a/web/app/components/base/prompt-editor/plugins/context-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/context-block/index.tsx @@ -1,5 +1,7 @@ -import type { FC } from 'react' -import { useEffect } from 'react' +import { + memo, + useEffect, +} from 'react' import { $insertNodes, COMMAND_PRIORITY_EDITOR, @@ -7,6 +9,7 @@ import { } from 'lexical' import { mergeRegister } from '@lexical/utils' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { ContextBlockType } from '../../types' import { $createContextBlockNode, ContextBlockNode, @@ -21,20 +24,13 @@ export type Dataset = { type: string } -export type ContextBlockProps = { - datasets: Dataset[] - onAddContext: () => void - onInsert?: () => void - onDelete?: () => void - canNotAddContext?: boolean -} -const ContextBlock: FC = ({ - datasets, - onAddContext, +const ContextBlock = memo(({ + datasets = [], + onAddContext = () => {}, onInsert, onDelete, canNotAddContext, -}) => { +}: ContextBlockType) => { const [editor] = useLexicalComposerContext() useEffect(() => { @@ -67,9 +63,12 @@ const ContextBlock: FC = ({ COMMAND_PRIORITY_EDITOR, ), ) - }, [editor, datasets, onAddContext, onInsert, onDelete]) + }, [editor, datasets, onAddContext, onInsert, onDelete, canNotAddContext]) return null -} +}) +ContextBlock.displayName = 'ContextBlock' -export default ContextBlock +export { ContextBlock } +export { ContextBlockNode } from './node' +export { default as ContextBlockReplacementBlock } from './context-block-replacement-block' diff --git a/web/app/components/base/prompt-editor/plugins/history-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx similarity index 69% rename from web/app/components/base/prompt-editor/plugins/history-block-replacement-block.tsx rename to web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx index d625f9921b..960b9c926f 100644 --- a/web/app/components/base/prompt-editor/plugins/history-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx @@ -1,4 +1,3 @@ -import type { FC } from 'react' import { useCallback, useEffect, @@ -6,22 +5,22 @@ import { import { $applyNodeReplacement } from 'lexical' import { mergeRegister } from '@lexical/utils' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' -import { decoratorTransform } from '../utils' -import { HISTORY_PLACEHOLDER_TEXT } from '../constants' +import { decoratorTransform } from '../../utils' +import { HISTORY_PLACEHOLDER_TEXT } from '../../constants' +import type { HistoryBlockType } from '../../types' import { $createHistoryBlockNode, HistoryBlockNode, -} from './history-block/node' -import type { HistoryBlockProps } from './history-block/index' -import { CustomTextNode } from './custom-text/node' +} from '../history-block/node' +import { CustomTextNode } from '../custom-text/node' const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT) -const HistoryBlockReplacementBlock: FC = ({ - roleName, - onEditRole, +const HistoryBlockReplacementBlock = ({ + history = { user: '', assistant: '' }, + onEditRole = () => {}, onInsert, -}) => { +}: HistoryBlockType) => { const [editor] = useLexicalComposerContext() useEffect(() => { @@ -32,8 +31,8 @@ const HistoryBlockReplacementBlock: FC = ({ const createHistoryBlockNode = useCallback((): HistoryBlockNode => { if (onInsert) onInsert() - return $applyNodeReplacement($createHistoryBlockNode(roleName, onEditRole)) - }, [roleName, onEditRole, onInsert]) + return $applyNodeReplacement($createHistoryBlockNode(history, onEditRole)) + }, [history, onEditRole, onInsert]) const getMatch = useCallback((text: string) => { const matchArr = REGEX.exec(text) @@ -53,7 +52,7 @@ const HistoryBlockReplacementBlock: FC = ({ return mergeRegister( editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHistoryBlockNode)), ) - }, []) + }, [editor, getMatch, createHistoryBlockNode]) return null } diff --git a/web/app/components/base/prompt-editor/plugins/history-block/index.tsx b/web/app/components/base/prompt-editor/plugins/history-block/index.tsx index 22da8cd622..78c7349519 100644 --- a/web/app/components/base/prompt-editor/plugins/history-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/history-block/index.tsx @@ -1,5 +1,7 @@ -import type { FC } from 'react' -import { useEffect } from 'react' +import { + memo, + useEffect, +} from 'react' import { $insertNodes, COMMAND_PRIORITY_EDITOR, @@ -7,6 +9,7 @@ import { } from 'lexical' import { mergeRegister } from '@lexical/utils' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { HistoryBlockType } from '../../types' import { $createHistoryBlockNode, HistoryBlockNode, @@ -27,12 +30,12 @@ export type HistoryBlockProps = { onDelete?: () => void } -const HistoryBlock: FC = ({ - roleName, - onEditRole, +const HistoryBlock = memo(({ + history = { user: '', assistant: '' }, + onEditRole = () => {}, onInsert, onDelete, -}) => { +}: HistoryBlockType) => { const [editor] = useLexicalComposerContext() useEffect(() => { @@ -43,7 +46,7 @@ const HistoryBlock: FC = ({ editor.registerCommand( INSERT_HISTORY_BLOCK_COMMAND, () => { - const historyBlockNode = $createHistoryBlockNode(roleName, onEditRole) + const historyBlockNode = $createHistoryBlockNode(history, onEditRole) $insertNodes([historyBlockNode]) @@ -65,9 +68,12 @@ const HistoryBlock: FC = ({ COMMAND_PRIORITY_EDITOR, ), ) - }, [editor, roleName, onEditRole, onInsert, onDelete]) + }, [editor, history, onEditRole, onInsert, onDelete]) return null -} +}) +HistoryBlock.displayName = 'HistoryBlock' -export default HistoryBlock +export { HistoryBlock } +export { HistoryBlockNode } from './node' +export { default as HistoryBlockReplacementBlock } from './history-block-replacement-block' diff --git a/web/app/components/base/prompt-editor/plugins/on-blur-or-focus-block.tsx b/web/app/components/base/prompt-editor/plugins/on-blur-or-focus-block.tsx index a443bee097..5729e6dd62 100644 --- a/web/app/components/base/prompt-editor/plugins/on-blur-or-focus-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/on-blur-or-focus-block.tsx @@ -40,7 +40,7 @@ const OnBlurBlock: FC = ({ COMMAND_PRIORITY_EDITOR, ), ) - }, [editor, onBlur]) + }, [editor, onBlur, onFocus]) return null } diff --git a/web/app/components/base/prompt-editor/plugins/query-block/index.tsx b/web/app/components/base/prompt-editor/plugins/query-block/index.tsx index 1822a0f71d..0946153130 100644 --- a/web/app/components/base/prompt-editor/plugins/query-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/query-block/index.tsx @@ -1,5 +1,7 @@ -import type { FC } from 'react' -import { useEffect } from 'react' +import { + memo, + useEffect, +} from 'react' import { $insertNodes, COMMAND_PRIORITY_EDITOR, @@ -7,6 +9,7 @@ import { } from 'lexical' import { mergeRegister } from '@lexical/utils' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { QueryBlockType } from '../../types' import { $createQueryBlockNode, QueryBlockNode, @@ -19,10 +22,10 @@ export type QueryBlockProps = { onInsert?: () => void onDelete?: () => void } -const QueryBlock: FC = ({ +const QueryBlock = memo(({ onInsert, onDelete, -}) => { +}: QueryBlockType) => { const [editor] = useLexicalComposerContext() useEffect(() => { @@ -57,6 +60,9 @@ const QueryBlock: FC = ({ }, [editor, onInsert, onDelete]) return null -} +}) +QueryBlock.displayName = 'QueryBlock' -export default QueryBlock +export { QueryBlock } +export { QueryBlockNode } from './node' +export { default as QueryBlockReplacementBlock } from './query-block-replacement-block' diff --git a/web/app/components/base/prompt-editor/plugins/query-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/query-block/query-block-replacement-block.tsx similarity index 75% rename from web/app/components/base/prompt-editor/plugins/query-block-replacement-block.tsx rename to web/app/components/base/prompt-editor/plugins/query-block/query-block-replacement-block.tsx index bc003418ff..1f0decf396 100644 --- a/web/app/components/base/prompt-editor/plugins/query-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/query-block/query-block-replacement-block.tsx @@ -1,25 +1,25 @@ -import type { FC } from 'react' import { + memo, useCallback, useEffect, } from 'react' import { $applyNodeReplacement } from 'lexical' import { mergeRegister } from '@lexical/utils' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' -import { decoratorTransform } from '../utils' -import { QUERY_PLACEHOLDER_TEXT } from '../constants' +import { decoratorTransform } from '../../utils' +import { QUERY_PLACEHOLDER_TEXT } from '../../constants' +import type { QueryBlockType } from '../../types' import { $createQueryBlockNode, QueryBlockNode, -} from './query-block/node' -import type { QueryBlockProps } from './query-block/index' -import { CustomTextNode } from './custom-text/node' +} from '../query-block/node' +import { CustomTextNode } from '../custom-text/node' const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT) -const QueryBlockReplacementBlock: FC = ({ +const QueryBlockReplacementBlock = ({ onInsert, -}) => { +}: QueryBlockType) => { const [editor] = useLexicalComposerContext() useEffect(() => { @@ -51,9 +51,9 @@ const QueryBlockReplacementBlock: FC = ({ return mergeRegister( editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createQueryBlockNode)), ) - }, []) + }, [editor, getMatch, createQueryBlockNode]) return null } -export default QueryBlockReplacementBlock +export default memo(QueryBlockReplacementBlock) diff --git a/web/app/components/base/prompt-editor/plugins/variable-picker.tsx b/web/app/components/base/prompt-editor/plugins/variable-picker.tsx deleted file mode 100644 index 9d418dc229..0000000000 --- a/web/app/components/base/prompt-editor/plugins/variable-picker.tsx +++ /dev/null @@ -1,338 +0,0 @@ -import type { FC } from 'react' -import { useCallback, useMemo, useState } from 'react' -import ReactDOM from 'react-dom' -import { useTranslation } from 'react-i18next' -import { $insertNodes, type TextNode } from 'lexical' -import { - LexicalTypeaheadMenuPlugin, - MenuOption, -} from '@lexical/react/LexicalTypeaheadMenuPlugin' -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' -import { useBasicTypeaheadTriggerMatch } from '../hooks' -import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from './variable-block' -import { $createCustomTextNode } from './custom-text/node' -import { BracketsX } from '@/app/components/base/icons/src/vender/line/development' -import { Tool03 } from '@/app/components/base/icons/src/vender/solid/general' -import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows' -import AppIcon from '@/app/components/base/app-icon' -import { useEventEmitterContextContext } from '@/context/event-emitter' - -class VariablePickerOption extends MenuOption { - title: string - icon?: JSX.Element - extraElement?: JSX.Element - keywords: Array - keyboardShortcut?: string - onSelect: (queryString: string) => void - - constructor( - title: string, - options: { - icon?: JSX.Element - extraElement?: JSX.Element - keywords?: Array - keyboardShortcut?: string - onSelect: (queryString: string) => void - }, - ) { - super(title) - this.title = title - this.keywords = options.keywords || [] - this.icon = options.icon - this.extraElement = options.extraElement - this.keyboardShortcut = options.keyboardShortcut - this.onSelect = options.onSelect.bind(this) - } -} - -type VariablePickerMenuItemProps = { - isSelected: boolean - onClick: () => void - onMouseEnter: () => void - option: VariablePickerOption - queryString: string | null -} -const VariablePickerMenuItem: FC = ({ - isSelected, - onClick, - onMouseEnter, - option, - queryString, -}) => { - const title = option.title - let before = title - let middle = '' - let after = '' - - if (queryString) { - const regex = new RegExp(queryString, 'i') - const match = regex.exec(option.title) - - if (match) { - before = title.substring(0, match.index) - middle = match[0] - after = title.substring(match.index + match[0].length) - } - } - - return ( -
-
- {option.icon} -
-
- {before} - {middle} - {after} -
- {option.extraElement} -
- ) -} - -export type Option = { - value: string - name: string -} - -export type ExternalToolOption = { - name: string - variableName: string - icon?: string - icon_background?: string -} - -type VariablePickerProps = { - items?: Option[] - externalTools?: ExternalToolOption[] - onAddExternalTool?: () => void - outToolDisabled?: boolean -} -const VariablePicker: FC = ({ - items = [], - externalTools = [], - onAddExternalTool, - outToolDisabled, -}) => { - const { t } = useTranslation() - const { eventEmitter } = useEventEmitterContextContext() - const [editor] = useLexicalComposerContext() - const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('{', { - minLength: 0, - maxLength: 6, - }) - const [queryString, setQueryString] = useState(null) - - eventEmitter?.useSubscription((v: any) => { - if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND) - editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`) - }) - - const options = useMemo(() => { - const baseOptions = items.map((item) => { - return new VariablePickerOption(item.value, { - icon: , - onSelect: () => { - editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`) - }, - }) - }) - if (!queryString) - return baseOptions - - const regex = new RegExp(queryString, 'i') - - return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword))) - }, [editor, queryString, items]) - - const toolOptions = useMemo(() => { - const baseToolOptions = externalTools.map((item) => { - return new VariablePickerOption(item.name, { - icon: ( - - ), - extraElement:
{item.variableName}
, - onSelect: () => { - editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`) - }, - }) - }) - if (!queryString) - return baseToolOptions - - const regex = new RegExp(queryString, 'i') - - return baseToolOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword))) - }, [editor, queryString, externalTools]) - - const newOption = new VariablePickerOption(t('common.promptEditor.variable.modal.add'), { - icon: , - onSelect: () => { - editor.update(() => { - const prefixNode = $createCustomTextNode('{{') - const suffixNode = $createCustomTextNode('}}') - $insertNodes([prefixNode, suffixNode]) - prefixNode.select() - }) - }, - }) - - const newToolOption = new VariablePickerOption(t('common.promptEditor.variable.modal.addTool'), { - icon: , - extraElement: , - onSelect: () => { - if (onAddExternalTool) - onAddExternalTool() - }, - }) - - const onSelectOption = useCallback( - ( - selectedOption: VariablePickerOption, - nodeToRemove: TextNode | null, - closeMenu: () => void, - matchingString: string, - ) => { - editor.update(() => { - if (nodeToRemove) - nodeToRemove.remove() - - selectedOption.onSelect(matchingString) - closeMenu() - }) - }, - [editor], - ) - - const mergedOptions = [...options, ...toolOptions, newOption] - if (!outToolDisabled) - mergedOptions.push(newToolOption) - - return ( - - (anchorElementRef.current && mergedOptions.length) - ? ReactDOM.createPortal( -
- { - !!options.length && ( - <> -
- {options.map((option, i: number) => ( - { - setHighlightedIndex(i) - selectOptionAndCleanUp(option) - }} - onMouseEnter={() => { - setHighlightedIndex(i) - }} - key={option.key} - option={option} - queryString={queryString} - /> - ))} -
-
- - ) - } - { - !!toolOptions.length && ( - <> -
- {toolOptions.map((option, i: number) => ( - { - setHighlightedIndex(i + options.length) - selectOptionAndCleanUp(option) - }} - onMouseEnter={() => { - setHighlightedIndex(i + options.length) - }} - key={option.key} - option={option} - queryString={queryString} - /> - ))} -
-
- - ) - } -
-
{ - setHighlightedIndex(options.length + toolOptions.length) - selectOptionAndCleanUp(newOption) - }} - onMouseEnter={() => { - setHighlightedIndex(options.length + toolOptions.length) - }} - key={newOption.key} - > - {newOption.icon} -
{newOption.title}
-
- {!outToolDisabled && ( -
{ - setHighlightedIndex(options.length + toolOptions.length + 1) - selectOptionAndCleanUp(newToolOption) - }} - onMouseEnter={() => { - setHighlightedIndex(options.length + toolOptions.length + 1) - }} - key={newToolOption.key} - > - {newToolOption.icon} -
{newToolOption.title}
- {newToolOption.extraElement} -
- )} - -
-
, - anchorElementRef.current, - ) - : null} - triggerFn={checkForTriggerMatch} - /> - ) -} - -export default VariablePicker diff --git a/web/app/components/base/prompt-editor/plugins/variable-value-block/utils.ts b/web/app/components/base/prompt-editor/plugins/variable-value-block/utils.ts index e983a8509e..f1e5d7d88a 100644 --- a/web/app/components/base/prompt-editor/plugins/variable-value-block/utils.ts +++ b/web/app/components/base/prompt-editor/plugins/variable-value-block/utils.ts @@ -1,236 +1,5 @@ -function getHashtagRegexStringChars(): Readonly<{ - alpha: string - alphanumeric: string - leftChars: string - rightChars: string -}> { - // Latin accented characters - // Excludes 0xd7 from the range - // (the multiplication sign, confusable with "x"). - // Also excludes 0xf7, the division sign - const latinAccents - = '\xC0-\xD6' - + '\xD8-\xF6' - + '\xF8-\xFF' - + '\u0100-\u024F' - + '\u0253-\u0254' - + '\u0256-\u0257' - + '\u0259' - + '\u025B' - + '\u0263' - + '\u0268' - + '\u026F' - + '\u0272' - + '\u0289' - + '\u028B' - + '\u02BB' - + '\u0300-\u036F' - + '\u1E00-\u1EFF' - - // Cyrillic (Russian, Ukrainian, etc.) - const nonLatinChars - = '\u0400-\u04FF' // Cyrillic - + '\u0500-\u0527' // Cyrillic Supplement - + '\u2DE0-\u2DFF' // Cyrillic Extended A - + '\uA640-\uA69F' // Cyrillic Extended B - + '\u0591-\u05BF' // Hebrew - + '\u05C1-\u05C2' - + '\u05C4-\u05C5' - + '\u05C7' - + '\u05D0-\u05EA' - + '\u05F0-\u05F4' - + '\uFB12-\uFB28' // Hebrew Presentation Forms - + '\uFB2A-\uFB36' - + '\uFB38-\uFB3C' - + '\uFB3E' - + '\uFB40-\uFB41' - + '\uFB43-\uFB44' - + '\uFB46-\uFB4F' - + '\u0610-\u061A' // Arabic - + '\u0620-\u065F' - + '\u066E-\u06D3' - + '\u06D5-\u06DC' - + '\u06DE-\u06E8' - + '\u06EA-\u06EF' - + '\u06FA-\u06FC' - + '\u06FF' - + '\u0750-\u077F' // Arabic Supplement - + '\u08A0' // Arabic Extended A - + '\u08A2-\u08AC' - + '\u08E4-\u08FE' - + '\uFB50-\uFBB1' // Arabic Pres. Forms A - + '\uFBD3-\uFD3D' - + '\uFD50-\uFD8F' - + '\uFD92-\uFDC7' - + '\uFDF0-\uFDFB' - + '\uFE70-\uFE74' // Arabic Pres. Forms B - + '\uFE76-\uFEFC' - + '\u200C-\u200C' // Zero-Width Non-Joiner - + '\u0E01-\u0E3A' // Thai - + '\u0E40-\u0E4E' // Hangul (Korean) - + '\u1100-\u11FF' // Hangul Jamo - + '\u3130-\u3185' // Hangul Compatibility Jamo - + '\uA960-\uA97F' // Hangul Jamo Extended-A - + '\uAC00-\uD7AF' // Hangul Syllables - + '\uD7B0-\uD7FF' // Hangul Jamo Extended-B - + '\uFFA1-\uFFDC' // Half-width Hangul - - const charCode = String.fromCharCode - - const cjkChars - = '\u30A1-\u30FA\u30FC-\u30FE' // Katakana (full-width) - + '\uFF66-\uFF9F' // Katakana (half-width) - + '\uFF10-\uFF19\uFF21-\uFF3A' - + '\uFF41-\uFF5A' // Latin (full-width) - + '\u3041-\u3096\u3099-\u309E' // Hiragana - + '\u3400-\u4DBF' // Kanji (CJK Extension A) - + `\u4E00-\u9FFF${// Kanji (Unified) - // Disabled as it breaks the Regex. - // charCode(0x20000) + '-' + charCode(0x2A6DF) + // Kanji (CJK Extension B) - charCode(0x2A700) - }-${ - charCode(0x2B73F) // Kanji (CJK Extension C) - }${charCode(0x2B740) - }-${ - charCode(0x2B81F) // Kanji (CJK Extension D) - }${charCode(0x2F800) - }-${ - charCode(0x2FA1F) - }\u3003\u3005\u303B` // Kanji (CJK supplement) - - const otherChars = latinAccents + nonLatinChars + cjkChars - // equivalent of \p{L} - - const unicodeLetters - = '\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6' - + '\u00F8-\u0241\u0250-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EE\u037A\u0386' - + '\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03F5\u03F7-\u0481' - + '\u048A-\u04CE\u04D0-\u04F9\u0500-\u050F\u0531-\u0556\u0559\u0561-\u0587' - + '\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0640-\u064A\u066E-\u066F' - + '\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710' - + '\u0712-\u072F\u074D-\u076D\u0780-\u07A5\u07B1\u0904-\u0939\u093D\u0950' - + '\u0958-\u0961\u097D\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0' - + '\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1' - + '\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33' - + '\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D' - + '\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD' - + '\u0AD0\u0AE0-\u0AE1\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30' - + '\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83' - + '\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F' - + '\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10' - + '\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60-\u0C61\u0C85-\u0C8C' - + '\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE' - + '\u0CE0-\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39' - + '\u0D60-\u0D61\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6' - + '\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E46\u0E81-\u0E82\u0E84\u0E87-\u0E88' - + '\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7' - + '\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6' - + '\u0EDC-\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6A\u0F88-\u0F8B\u1000-\u1021' - + '\u1023-\u1027\u1029-\u102A\u1050-\u1055\u10A0-\u10C5\u10D0-\u10FA\u10FC' - + '\u1100-\u1159\u115F-\u11A2\u11A8-\u11F9\u1200-\u1248\u124A-\u124D' - + '\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0' - + '\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310' - + '\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C' - + '\u166F-\u1676\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711' - + '\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7' - + '\u17DC\u1820-\u1877\u1880-\u18A8\u1900-\u191C\u1950-\u196D\u1970-\u1974' - + '\u1980-\u19A9\u19C1-\u19C7\u1A00-\u1A16\u1D00-\u1DBF\u1E00-\u1E9B' - + '\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D' - + '\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC' - + '\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC' - + '\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107' - + '\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D' - + '\u212F-\u2131\u2133-\u2139\u213C-\u213F\u2145-\u2149\u2C00-\u2C2E' - + '\u2C30-\u2C5E\u2C80-\u2CE4\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96' - + '\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6' - + '\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3006\u3031-\u3035' - + '\u303B-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF' - + '\u3105-\u312C\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5' - + '\u4E00-\u9FBB\uA000-\uA48C\uA800-\uA801\uA803-\uA805\uA807-\uA80A' - + '\uA80C-\uA822\uAC00-\uD7A3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9' - + '\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C' - + '\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F' - + '\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A' - + '\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7' - + '\uFFDA-\uFFDC' - - // equivalent of \p{Mn}\p{Mc} - const unicodeAccents - = '\u0300-\u036F\u0483-\u0486\u0591-\u05B9\u05BB-\u05BD\u05BF' - + '\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u0615\u064B-\u065E\u0670' - + '\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A' - + '\u07A6-\u07B0\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u0962-\u0963' - + '\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7-\u09C8\u09CB-\u09CD\u09D7' - + '\u09E2-\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D' - + '\u0A70-\u0A71\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD' - + '\u0AE2-\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B43\u0B47-\u0B48\u0B4B-\u0B4D' - + '\u0B56-\u0B57\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7' - + '\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56' - + '\u0C82-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5-\u0CD6' - + '\u0D02-\u0D03\u0D3E-\u0D43\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D82-\u0D83' - + '\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF3\u0E31\u0E34-\u0E3A' - + '\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19' - + '\u0F35\u0F37\u0F39\u0F3E-\u0F3F\u0F71-\u0F84\u0F86-\u0F87\u0F90-\u0F97' - + '\u0F99-\u0FBC\u0FC6\u102C-\u1032\u1036-\u1039\u1056-\u1059\u135F' - + '\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B6-\u17D3\u17DD' - + '\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8-\u19C9' - + '\u1A17-\u1A1B\u1DC0-\u1DC3\u20D0-\u20DC\u20E1\u20E5-\u20EB\u302A-\u302F' - + '\u3099-\u309A\uA802\uA806\uA80B\uA823-\uA827\uFB1E\uFE00-\uFE0F' - + '\uFE20-\uFE23' - - // equivalent of \p{Dn} - const unicodeDigits - = '\u0030-\u0039\u0660-\u0669\u06F0-\u06F9\u0966-\u096F\u09E6-\u09EF' - + '\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F' - + '\u0CE6-\u0CEF\u0D66-\u0D6F\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29' - + '\u1040-\u1049\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9' - + '\uFF10-\uFF19' - - // An alpha char is a unicode chars including unicode marks or - // letter or char in otherChars range - const alpha = unicodeLetters - - // A numeric character is any with the number digit property, or - // underscore. These characters can be included in hashtags, but a hashtag - // cannot have only these characters. - const numeric = `${unicodeDigits}_` - - // Alphanumeric char is any alpha char or a unicode char with decimal - // number property \p{Nd} - const alphanumeric = alpha + numeric - const leftChars = '{' - const rightChars = '}' - - return { - alpha, - alphanumeric, - leftChars, - rightChars, - } -} - export function getHashtagRegexString(): string { - const { alpha, alphanumeric, leftChars, rightChars } = getHashtagRegexStringChars() - - const hashtagAlpha = `[${alpha}]` - const hashtagAlphanumeric = `[${alphanumeric}]` - const hashLeftCharList = `[${leftChars}]` - const hashRightCharList = `[${rightChars}]` - - // A hashtag contains characters, numbers and underscores, - // but not all numbers. - const hashtag - = `(${ - hashLeftCharList - })` - + `(${ - hashLeftCharList - })([a-zA-Z_][a-zA-Z0-9_]{0,29}` - + `)(${ - hashRightCharList - })(${ - hashRightCharList - })` + const hashtag = '(\{)(\{)([a-zA-Z_][a-zA-Z0-9_]{0,29})(\})(\})' return hashtag } 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 94bf6cef81..78d14b6711 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 @@ -10,7 +10,7 @@ import { Line3 } from '@/app/components/base/icons/src/public/common' type WorkflowVariableBlockComponentProps = { nodeKey: string variables: string[] - getWorkflowNode: (nodeId: string) => Node + getWorkflowNode: (nodeId: string) => Node | undefined } const WorkflowVariableBlockComponent: FC = ({ @@ -20,7 +20,7 @@ const WorkflowVariableBlockComponent: FC = }) => { const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND) const node = getWorkflowNode(variables[0]) - const outputVarNode = node.data + const outputVarNode = node?.data const variablesLength = variables.length const lastVariable = variables[variablesLength - 1] diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx index 1cb1130469..e1bc55e20c 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx @@ -1,5 +1,7 @@ -import type { FC } from 'react' -import { useEffect } from 'react' +import { + memo, + useEffect, +} from 'react' import { $insertNodes, COMMAND_PRIORITY_EDITOR, @@ -7,6 +9,7 @@ import { } from 'lexical' import { mergeRegister } from '@lexical/utils' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { WorkflowVariableBlockType } from '../../types' import { $createWorkflowVariableBlockNode, WorkflowVariableBlockNode, @@ -21,11 +24,11 @@ export type WorkflowVariableBlockProps = { onInsert?: () => void onDelete?: () => void } -const WorkflowVariableBlock: FC = ({ - getWorkflowNode, +const WorkflowVariableBlock = memo(({ + getWorkflowNode = () => undefined, onInsert, onDelete, -}) => { +}: WorkflowVariableBlockType) => { const [editor] = useLexicalComposerContext() useEffect(() => { @@ -60,6 +63,8 @@ const WorkflowVariableBlock: FC = ({ }, [editor, onInsert, onDelete, getWorkflowNode]) return null -} +}) +WorkflowVariableBlock.displayName = 'WorkflowVariableBlock' -export default WorkflowVariableBlock +export { WorkflowVariableBlock } +export { WorkflowVariableBlockNode } from './node' diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx index 89fff18b83..6d12d357fd 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx @@ -3,14 +3,15 @@ import { DecoratorNode } from 'lexical' import WorkflowVariableBlockComponent from './component' import type { Node } from '@/app/components/workflow/types' +type GetWorkflowNode = (nodeId: string) => Node | undefined export type SerializedNode = SerializedLexicalNode & { variables: string[] - getWorkflowNode: (nodeId: string) => Node + getWorkflowNode: GetWorkflowNode } export class WorkflowVariableBlockNode extends DecoratorNode { __variables: string[] - __getWorkflowNode: (nodeId: string) => Node + __getWorkflowNode: GetWorkflowNode static getType(): string { return 'workflow-variable-block' @@ -24,7 +25,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode { return true } - constructor(variables: string[], getWorkflowNode: (nodeId: string) => Node, key?: NodeKey) { + constructor(variables: string[], getWorkflowNode: GetWorkflowNode, key?: NodeKey) { super(key) this.__variables = variables @@ -70,7 +71,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode { return `{{#${this.__variables.join('.')}#}}` } } -export function $createWorkflowVariableBlockNode(variables: string[], getWorkflowNodeName: (nodeId: string) => Node): WorkflowVariableBlockNode { +export function $createWorkflowVariableBlockNode(variables: string[], getWorkflowNodeName: (nodeId: string) => Node | undefined): WorkflowVariableBlockNode { return new WorkflowVariableBlockNode(variables, getWorkflowNodeName) } diff --git a/web/app/components/base/prompt-editor/types.ts b/web/app/components/base/prompt-editor/types.ts new file mode 100644 index 0000000000..5c8da1fc39 --- /dev/null +++ b/web/app/components/base/prompt-editor/types.ts @@ -0,0 +1,63 @@ +import type { Dataset } from './plugins/context-block' +import type { RoleName } from './plugins/history-block' +import type { + Node, + NodeOutPutVar, +} from '@/app/components/workflow/types' + +export type Option = { + value: string + name: string +} + +export type ExternalToolOption = { + name: string + variableName: string + icon?: string + icon_background?: string +} + +export type ContextBlockType = { + show?: boolean + selectable?: boolean + datasets?: Dataset[] + canNotAddContext?: boolean + onAddContext?: () => void + onInsert?: () => void + onDelete?: () => void +} + +export type QueryBlockType = { + show?: boolean + selectable?: boolean + onInsert?: () => void + onDelete?: () => void +} + +export type HistoryBlockType = { + show?: boolean + selectable?: boolean + history?: RoleName + onInsert?: () => void + onDelete?: () => void + onEditRole?: () => void +} + +export type VariableBlockType = { + show?: boolean + variables?: Option[] +} + +export type ExternalToolBlockType = { + show?: boolean + externalTools?: ExternalToolOption[] + onAddExternalTool?: () => void +} + +export type WorkflowVariableBlockType = { + show?: boolean + variables?: NodeOutPutVar[] + getWorkflowNode?: (nodeId: string) => Node | undefined + onInsert?: () => void + onDelete?: () => void +} 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 7523cfe901..2d8e96d9eb 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx @@ -38,7 +38,6 @@ type Props = { const Editor: FC = ({ title, value, - variables, onChange, readOnly, showRemove, @@ -122,21 +121,12 @@ const Editor: FC = ({ className={cn('min-h-[84px]')} style={isExpand ? { height: editorExpandHeight - 5 } : {}} value={value} - outToolDisabled - canNotAddContext contextBlock={{ show: justVar ? false : isShowContext, selectable: !hasSetBlockStatus?.context, datasets: [], onAddContext: () => { }, - }} - variableBlock={{ - variables: variables.map(item => ({ - name: item, - value: item, - })), - externalTools: [], - onAddExternalTool: () => { }, + canNotAddContext: true, }} historyBlock={{ show: justVar ? false : isShowHistory, @@ -153,7 +143,6 @@ const Editor: FC = ({ }} workflowVariableBlock={{ show: true, - selectable: true, variables: nodesOutputVars || [], getWorkflowNode: getNode, }}