diff --git a/web/app/components/base/prompt-editor/constants.tsx b/web/app/components/base/prompt-editor/constants.tsx index 48e67a4ee7..6cef41d818 100644 --- a/web/app/components/base/prompt-editor/constants.tsx +++ b/web/app/components/base/prompt-editor/constants.tsx @@ -3,6 +3,7 @@ import { SupportUploadFileTypes, type ValueSelector } from '../../workflow/types export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}' export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}' export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}' +export const REQUEST_URL_PLACEHOLDER_TEXT = '{{#url#}}' export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}' export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets' export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role' @@ -25,6 +26,12 @@ export const checkHasQueryBlock = (text: string) => { return text.includes(QUERY_PLACEHOLDER_TEXT) } +export const checkHasRequestURLBlock = (text: string) => { + if (!text) + return false + return text.includes(REQUEST_URL_PLACEHOLDER_TEXT) +} + /* * {{#1711617514996.name#}} => [1711617514996, name] * {{#1711617514996.sys.query#}} => [sys, query] diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx index 9feace996c..2a571d81cd 100644 --- a/web/app/components/base/prompt-editor/index.tsx +++ b/web/app/components/base/prompt-editor/index.tsx @@ -44,6 +44,11 @@ import { HITLInputBlockReplacementBlock, HITLInputNode, } from './plugins/hitl-input-block' +import { + RequestURLBlock, + RequestURLBlockNode, + RequestURLBlockReplacementBlock, +} from './plugins/request-url-block' import VariableBlock from './plugins/variable-block' import VariableValueBlock from './plugins/variable-value-block' import { VariableValueBlockNode } from './plugins/variable-value-block/node' @@ -57,6 +62,7 @@ import type { HITLInputBlockType, HistoryBlockType, QueryBlockType, + RequestURLBlockType, VariableBlockType, WorkflowVariableBlockType, } from './types' @@ -82,6 +88,7 @@ export type PromptEditorProps = { onFocus?: () => void contextBlock?: ContextBlockType queryBlock?: QueryBlockType + requestURLBlock?: RequestURLBlockType historyBlock?: HistoryBlockType variableBlock?: VariableBlockType externalToolBlock?: ExternalToolBlockType @@ -105,6 +112,7 @@ const PromptEditor: FC = ({ onFocus, contextBlock, queryBlock, + requestURLBlock, historyBlock, variableBlock, externalToolBlock, @@ -125,6 +133,7 @@ const PromptEditor: FC = ({ ContextBlockNode, HistoryBlockNode, QueryBlockNode, + RequestURLBlockNode, WorkflowVariableBlockNode, VariableValueBlockNode, HITLInputNode, @@ -184,6 +193,7 @@ const PromptEditor: FC = ({ contextBlock={contextBlock} historyBlock={historyBlock} queryBlock={queryBlock} + requestURLBlock={requestURLBlock} variableBlock={variableBlock} externalToolBlock={externalToolBlock} workflowVariableBlock={workflowVariableBlock} @@ -194,6 +204,7 @@ const PromptEditor: FC = ({ contextBlock={contextBlock} historyBlock={historyBlock} queryBlock={queryBlock} + requestURLBlock={requestURLBlock} variableBlock={variableBlock} externalToolBlock={externalToolBlock} workflowVariableBlock={workflowVariableBlock} @@ -248,6 +259,14 @@ const PromptEditor: FC = ({ ) } + { + requestURLBlock?.show && ( + <> + + + + ) + } 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 index 7332a0d39b..e50b97ce8c 100644 --- 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 @@ -1,5 +1,6 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { RiGlobalLine } from '@remixicon/react' import { $insertNodes } from 'lexical' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import type { @@ -7,6 +8,7 @@ import type { ExternalToolBlockType, HistoryBlockType, QueryBlockType, + RequestURLBlockType, VariableBlockType, WorkflowVariableBlockType, } from '../../types' @@ -14,6 +16,7 @@ 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 { INSERT_REQUEST_URL_BLOCK_COMMAND } from '../request-url-block' import { $createCustomTextNode } from '../custom-text/node' import { PromptMenuItem } from './prompt-option' import { VariableMenuItem } from './variable-option' @@ -32,6 +35,7 @@ export const usePromptOptions = ( contextBlock?: ContextBlockType, queryBlock?: QueryBlockType, historyBlock?: HistoryBlockType, + requestURLBlock?: RequestURLBlockType, ) => { const { t } = useTranslation() const [editor] = useLexicalComposerContext() @@ -85,6 +89,28 @@ export const usePromptOptions = ( ) } + if (requestURLBlock?.show) { + promptOptions.push(new PickerBlockMenuOption({ + key: t('common.promptEditor.requestURL.item.title'), + group: 'request URL', + render: ({ isSelected, onSelect, onSetHighlight }) => { + return } + disabled={!requestURLBlock.selectable} + isSelected={isSelected} + onClick={onSelect} + onMouseEnter={onSetHighlight} + /> + }, + onSelect: () => { + if (!requestURLBlock?.selectable) + return + editor.dispatchCommand(INSERT_REQUEST_URL_BLOCK_COMMAND, undefined) + }, + })) + } + if (historyBlock?.show) { promptOptions.push( new PickerBlockMenuOption({ @@ -267,9 +293,10 @@ export const useOptions = ( variableBlock?: VariableBlockType, externalToolBlockType?: ExternalToolBlockType, workflowVariableBlockType?: WorkflowVariableBlockType, + requestURLBlock?: RequestURLBlockType, queryString?: string, ) => { - const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock) + const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock, requestURLBlock) const variableOptions = useVariableOptions(variableBlock, queryString) const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString) const workflowVariableOptions = useMemo(() => { 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 bffcdc60d2..304d2d0da2 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 @@ -20,6 +20,7 @@ import type { ExternalToolBlockType, HistoryBlockType, QueryBlockType, + RequestURLBlockType, VariableBlockType, WorkflowVariableBlockType, } from '../../types' @@ -37,6 +38,7 @@ type ComponentPickerProps = { triggerString: string contextBlock?: ContextBlockType queryBlock?: QueryBlockType + requestURLBlock?: RequestURLBlockType historyBlock?: HistoryBlockType variableBlock?: VariableBlockType externalToolBlock?: ExternalToolBlockType @@ -47,6 +49,7 @@ const ComponentPicker = ({ triggerString, contextBlock, queryBlock, + requestURLBlock, historyBlock, variableBlock, externalToolBlock, @@ -87,6 +90,7 @@ const ComponentPicker = ({ variableBlock, externalToolBlock, workflowVariableBlock, + requestURLBlock, ) const onSelectOption = useCallback( diff --git a/web/app/components/base/prompt-editor/plugins/request-url-block/component.tsx b/web/app/components/base/prompt-editor/plugins/request-url-block/component.tsx new file mode 100644 index 0000000000..5253f90cd4 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/request-url-block/component.tsx @@ -0,0 +1,33 @@ +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { RiGlobalLine } from '@remixicon/react' + +import { useSelectOrDelete } from '../../hooks' +import { DELETE_REQUEST_URL_BLOCK_COMMAND } from './index' +import cn from '@/utils/classnames' + +type RequestURLBlockComponentProps = { + nodeKey: string +} + +const RequestURLBlockComponent: FC = ({ + nodeKey, +}) => { + const { t } = useTranslation() + const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_REQUEST_URL_BLOCK_COMMAND) + + return ( +
+ +
{t('common.promptEditor.requestURL.item.title')}
+
+ ) +} + +export default RequestURLBlockComponent diff --git a/web/app/components/base/prompt-editor/plugins/request-url-block/index.tsx b/web/app/components/base/prompt-editor/plugins/request-url-block/index.tsx new file mode 100644 index 0000000000..3a82a4b49e --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/request-url-block/index.tsx @@ -0,0 +1,64 @@ +import { + memo, + useEffect, +} from 'react' +import { + $insertNodes, + COMMAND_PRIORITY_EDITOR, + createCommand, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { RequestURLBlockType } from '../../types' +import { + $createRequestURLBlockNode, + RequestURLBlockNode, +} from './node' + +export const INSERT_REQUEST_URL_BLOCK_COMMAND = createCommand('INSERT_REQUEST_URL_BLOCK_COMMAND') +export const DELETE_REQUEST_URL_BLOCK_COMMAND = createCommand('DELETE_REQUEST_URL_BLOCK_COMMAND') + +const RequestURLBlock = memo(({ + onInsert, + onDelete, +}: RequestURLBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([RequestURLBlockNode])) + throw new Error('RequestURLBlockPlugin: RequestURLBlock not registered on editor') + + return mergeRegister( + editor.registerCommand( + INSERT_REQUEST_URL_BLOCK_COMMAND, + () => { + const contextBlockNode = $createRequestURLBlockNode() + + $insertNodes([contextBlockNode]) + if (onInsert) + onInsert() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + DELETE_REQUEST_URL_BLOCK_COMMAND, + () => { + if (onDelete) + onDelete() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor, onInsert, onDelete]) + + return null +}) +RequestURLBlock.displayName = 'RequestURLBlock' + +export { RequestURLBlock } +export { RequestURLBlockNode } from './node' +export { default as RequestURLBlockReplacementBlock } from './request-url-block-replacement-block' diff --git a/web/app/components/base/prompt-editor/plugins/request-url-block/node.tsx b/web/app/components/base/prompt-editor/plugins/request-url-block/node.tsx new file mode 100644 index 0000000000..b1e74aa3a6 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/request-url-block/node.tsx @@ -0,0 +1,59 @@ +import type { LexicalNode, SerializedLexicalNode } from 'lexical' +import { DecoratorNode } from 'lexical' +import RequestURLBlockComponent from './component' + +export type SerializedNode = SerializedLexicalNode + +export class RequestURLBlockNode extends DecoratorNode { + static getType(): string { + return 'request-url-block' + } + + static clone(node: RequestURLBlockNode): RequestURLBlockNode { + return new RequestURLBlockNode(node.__key) + } + + isInline(): boolean { + return true + } + + createDOM(): HTMLElement { + const div = document.createElement('div') + div.classList.add('inline-flex', 'items-center', 'align-middle') + return div + } + + updateDOM(): false { + return false + } + + decorate(): React.JSX.Element { + return + } + + static importJSON(): RequestURLBlockNode { + const node = $createRequestURLBlockNode() + + return node + } + + exportJSON(): SerializedNode { + return { + type: 'request-url-block', + version: 1, + } + } + + getTextContent(): string { + return '{{#url#}}' + } +} +export function $createRequestURLBlockNode(): RequestURLBlockNode { + return new RequestURLBlockNode() +} + +export function $isRequestURLBlockNode( + node: RequestURLBlockNode | LexicalNode | null | undefined, +): node is RequestURLBlockNode { + return node instanceof RequestURLBlockNode +} diff --git a/web/app/components/base/prompt-editor/plugins/request-url-block/request-url-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/request-url-block/request-url-block-replacement-block.tsx new file mode 100644 index 0000000000..843fb578d5 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/request-url-block/request-url-block-replacement-block.tsx @@ -0,0 +1,60 @@ +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 { REQUEST_URL_PLACEHOLDER_TEXT } from '../../constants' +import type { RequestURLBlockType } from '../../types' +import { + $createRequestURLBlockNode, + RequestURLBlockNode, +} from '../request-url-block/node' +import { CustomTextNode } from '../custom-text/node' + +const REGEX = new RegExp(REQUEST_URL_PLACEHOLDER_TEXT) + +const RequestURLBlockReplacementBlock = ({ + onInsert, +}: RequestURLBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([RequestURLBlockNode])) + throw new Error('RequestURLBlockNodePlugin: RequestURLBlockNode not registered on editor') + }, [editor]) + + const createRequestURLBlockNode = useCallback((): RequestURLBlockNode => { + if (onInsert) + onInsert() + return $applyNodeReplacement($createRequestURLBlockNode()) + }, [onInsert]) + + const getMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text) + + if (matchArr === null) + return null + + const startOffset = matchArr.index + const endOffset = startOffset + REQUEST_URL_PLACEHOLDER_TEXT.length + return { + end: endOffset, + start: startOffset, + } + }, []) + + useEffect(() => { + REGEX.lastIndex = 0 + return mergeRegister( + editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createRequestURLBlockNode)), + ) + }, []) + + return null +} + +export default memo(RequestURLBlockReplacementBlock) diff --git a/web/app/components/base/prompt-editor/types.ts b/web/app/components/base/prompt-editor/types.ts index db182e4a51..f250d449ce 100644 --- a/web/app/components/base/prompt-editor/types.ts +++ b/web/app/components/base/prompt-editor/types.ts @@ -45,6 +45,13 @@ export type HistoryBlockType = { onEditRole?: () => void } +export type RequestURLBlockType = { + show?: boolean + selectable?: boolean + onInsert?: () => void + onDelete?: () => void +} + export type VariableBlockType = { show?: boolean variables?: Option[] diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx index 5731efa969..06d7354520 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx @@ -3,37 +3,43 @@ import { useTranslation } from 'react-i18next' import { RiCloseLine } from '@remixicon/react' import Modal from '@/app/components/base/modal' import Input from '@/app/components/base/input' -import TextArea from '@/app/components/base/textarea' import Button from '@/app/components/base/button' +import MailBodyInput from './mail-body-input' +import type { EmailConfig, Recipient } from '../../types' +import type { + Node, + NodeOutPutVar, +} from '@/app/components/workflow/types' import { noop } from 'lodash-es' const i18nPrefix = 'workflow.nodes.humanInput' -type Recipient = { - value: string - label: string -} - type EmailConfigureModalProps = { isShow: boolean onClose: () => void onConfirm: (data: any) => void + config?: EmailConfig + nodesOutputVars?: NodeOutPutVar[] + availableNodes?: Node[] } const EmailConfigureModal = ({ isShow, onClose, onConfirm, + config, + nodesOutputVars = [], + availableNodes = [], }: EmailConfigureModalProps) => { const { t } = useTranslation() - const [recipients, setRecipients] = useState([]) - const [subject, setSubject] = useState('') - const [body, setBody] = useState('') + const [recipients, setRecipients] = useState(config?.recipients || []) + const [subject, setSubject] = useState(config?.subject || '') + const [body, setBody] = useState(config?.body || '') const handleConfirm = useCallback(() => { onConfirm({ - recipients: recipients.map(recipient => recipient.value), + recipients, subject, body, }) @@ -73,11 +79,11 @@ const EmailConfigureModal = ({
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.body`)}
-