mirror of
https://github.com/langgenius/dify.git
synced 2026-02-01 16:41:58 +08:00
mail body input
This commit is contained in:
parent
ce8325c83c
commit
82530df38f
@ -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]
|
||||
|
||||
@ -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<PromptEditorProps> = ({
|
||||
onFocus,
|
||||
contextBlock,
|
||||
queryBlock,
|
||||
requestURLBlock,
|
||||
historyBlock,
|
||||
variableBlock,
|
||||
externalToolBlock,
|
||||
@ -125,6 +133,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
ContextBlockNode,
|
||||
HistoryBlockNode,
|
||||
QueryBlockNode,
|
||||
RequestURLBlockNode,
|
||||
WorkflowVariableBlockNode,
|
||||
VariableValueBlockNode,
|
||||
HITLInputNode,
|
||||
@ -184,6 +193,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
requestURLBlock={requestURLBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
@ -194,6 +204,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
requestURLBlock={requestURLBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
@ -248,6 +259,14 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
requestURLBlock?.show && (
|
||||
<>
|
||||
<RequestURLBlock {...requestURLBlock} />
|
||||
<RequestURLBlockReplacementBlock {...requestURLBlock} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<OnChangePlugin onChange={handleEditorChange} />
|
||||
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
|
||||
<UpdateBlock instanceId={instanceId} />
|
||||
|
||||
@ -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 <PromptMenuItem
|
||||
title={t('common.promptEditor.requestURL.item.title')}
|
||||
icon={<RiGlobalLine className='h-4 w-4 text-util-colors-violet-violet-600' />}
|
||||
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(() => {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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<RequestURLBlockComponentProps> = ({
|
||||
nodeKey,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_REQUEST_URL_BLOCK_COMMAND)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border border-components-panel-border-subtle bg-components-badge-white-to-dark px-1 hover:border-[#7839ee]',
|
||||
isSelected && '!border-[#7839ee] hover:!border-[#7839ee]',
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<RiGlobalLine className='mr-0.5 h-3.5 w-3.5 text-util-colors-violet-violet-600' />
|
||||
<div className='system-xs-medium text-util-colors-violet-violet-600'>{t('common.promptEditor.requestURL.item.title')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RequestURLBlockComponent
|
||||
@ -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'
|
||||
@ -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<React.JSX.Element> {
|
||||
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 <RequestURLBlockComponent nodeKey={this.getKey()} />
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -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)
|
||||
@ -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[]
|
||||
|
||||
@ -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<Recipient[]>([])
|
||||
const [subject, setSubject] = useState('')
|
||||
const [body, setBody] = useState('')
|
||||
const [recipients, setRecipients] = useState<Recipient[]>(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 = ({
|
||||
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>
|
||||
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.body`)}
|
||||
</div>
|
||||
<TextArea
|
||||
className="min-h-[200px] w-full"
|
||||
<MailBodyInput
|
||||
value={body}
|
||||
onChange={e => setBody(e.target.value)}
|
||||
placeholder={t('email.configure.enterBody', 'Enter email content')}
|
||||
onChange={setBody}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,15 +5,26 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
import MethodSelector from './method-selector'
|
||||
import MethodItem from './method-item'
|
||||
import type { DeliveryMethod, DeliveryMethodType } from '../../types'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.humanInput'
|
||||
|
||||
type Props = {
|
||||
value: DeliveryMethod[]
|
||||
onchange: (value: DeliveryMethod[]) => void
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
onChange: (value: DeliveryMethod[]) => void
|
||||
}
|
||||
|
||||
const DeliveryMethodForm: React.FC<Props> = ({ value, onchange }) => {
|
||||
const DeliveryMethodForm: React.FC<Props> = ({
|
||||
value,
|
||||
nodesOutputVars,
|
||||
availableNodes,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleMethodChange = (target: DeliveryMethod) => {
|
||||
@ -22,17 +33,17 @@ const DeliveryMethodForm: React.FC<Props> = ({ value, onchange }) => {
|
||||
if (index !== -1)
|
||||
draft[index] = target
|
||||
})
|
||||
onchange(newMethods)
|
||||
onChange(newMethods)
|
||||
}
|
||||
|
||||
const handleMethodAdd = (newMethod: DeliveryMethod) => {
|
||||
const newMethods = [...value, newMethod]
|
||||
onchange(newMethods)
|
||||
onChange(newMethods)
|
||||
}
|
||||
|
||||
const handleMethodDelete = (type: DeliveryMethodType) => {
|
||||
const newMethods = value.filter(method => method.type !== type)
|
||||
onchange(newMethods)
|
||||
onChange(newMethods)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -62,6 +73,8 @@ const DeliveryMethodForm: React.FC<Props> = ({ value, onchange }) => {
|
||||
key={index}
|
||||
onChange={handleMethodChange}
|
||||
onDelete={handleMethodDelete}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import Placeholder from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type MailBodyInputProps = {
|
||||
readOnly?: boolean
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
value?: string
|
||||
onChange?: (text: string) => void
|
||||
}
|
||||
|
||||
const MailBodyInput = ({
|
||||
readOnly = false,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
value = '',
|
||||
onChange,
|
||||
}: MailBodyInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<PromptEditor
|
||||
wrapperClassName={cn(
|
||||
'w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
|
||||
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
|
||||
)}
|
||||
className='caret:text-text-accent min-h-[128px]'
|
||||
editable={!readOnly}
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
requestURLBlock={{
|
||||
show: true,
|
||||
selectable: true,
|
||||
}}
|
||||
placeholder={<Placeholder hideBadge />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailBodyInput
|
||||
@ -13,17 +13,29 @@ import Indicator from '@/app/components/header/indicator'
|
||||
import EmailConfigureModal from './email-configure-modal'
|
||||
import type { DeliveryMethod } from '../../types'
|
||||
import { DeliveryMethodType } from '../../types'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.humanInput'
|
||||
|
||||
type Props = {
|
||||
method: DeliveryMethod
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
onChange: (method: DeliveryMethod) => void
|
||||
onDelete: (type: DeliveryMethodType) => void
|
||||
}
|
||||
|
||||
const DeliveryMethodItem: React.FC<Props> = ({ method, onChange, onDelete }) => {
|
||||
const DeliveryMethodItem: React.FC<Props> = ({
|
||||
method,
|
||||
nodesOutputVars,
|
||||
availableNodes,
|
||||
onChange,
|
||||
onDelete,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isHovering, setIsHovering] = React.useState(false)
|
||||
const [showEmailModal, setShowEmailModal] = React.useState(false)
|
||||
@ -35,6 +47,13 @@ const DeliveryMethodItem: React.FC<Props> = ({ method, onChange, onDelete }) =>
|
||||
})
|
||||
}
|
||||
|
||||
const handleConfigChange = (config: any) => {
|
||||
onChange({
|
||||
...method,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -93,9 +112,12 @@ const DeliveryMethodItem: React.FC<Props> = ({ method, onChange, onDelete }) =>
|
||||
{showEmailModal && (
|
||||
<EmailConfigureModal
|
||||
isShow={showEmailModal}
|
||||
config={method.config}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
onClose={() => setShowEmailModal(false)}
|
||||
onConfirm={(data) => {
|
||||
onChange({ ...method, ...data })
|
||||
handleConfigChange(data)
|
||||
setShowEmailModal(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -47,10 +47,10 @@ const Node: FC<NodeProps<HumanInputNodeType>> = (props) => {
|
||||
<div className='space-y-0.5 py-1'>
|
||||
{userActions.map(userAction => (
|
||||
<div key={userAction.id} className='relative flex flex-row-reverse items-center px-4 py-1'>
|
||||
<span className='system-xs-semibold-uppercase truncate text-text-secondary'>{userAction.name}</span>
|
||||
<span className='system-xs-semibold-uppercase truncate text-text-secondary'>{userAction.id}</span>
|
||||
<NodeSourceHandle
|
||||
{...props}
|
||||
handleId={userAction.name}
|
||||
handleId={userAction.id}
|
||||
handleClassName='!top-1/2 !-right-[9px] !-translate-y-1/2'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -14,6 +14,9 @@ import Divider from '@/app/components/base/divider'
|
||||
import DeliveryMethod from './components/delivery-method'
|
||||
import UserActionItem from './components/user-action'
|
||||
import TimeoutInput from './components/timeout'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import type { Var } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.humanInput'
|
||||
|
||||
@ -30,12 +33,22 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
|
||||
handleUserActionDelete,
|
||||
handleTimeoutChange,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(id, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='py-2'>
|
||||
{/* delivery methods */}
|
||||
<DeliveryMethod
|
||||
value={inputs.delivery_methods || []}
|
||||
onchange={handleDeliveryMethodChange}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
onChange={handleDeliveryMethodChange}
|
||||
/>
|
||||
<div className='px-4 py-2'>
|
||||
<Divider className='!my-0 !h-px !bg-divider-subtle' />
|
||||
|
||||
@ -19,10 +19,22 @@ export enum DeliveryMethodType {
|
||||
Slack = 'slack',
|
||||
}
|
||||
|
||||
export type Recipient = {
|
||||
type: 'member' | 'external'
|
||||
email?: string
|
||||
user_id?: string
|
||||
}
|
||||
|
||||
export type EmailConfig = {
|
||||
recipients: Recipient[]
|
||||
subject: string
|
||||
body: string
|
||||
}
|
||||
|
||||
export type DeliveryMethod = {
|
||||
type: DeliveryMethodType
|
||||
enabled: boolean
|
||||
config?: Record<string, any>
|
||||
config?: EmailConfig
|
||||
}
|
||||
|
||||
export enum UserActionButtonType {
|
||||
|
||||
@ -5,8 +5,13 @@ import { FOCUS_COMMAND } from 'lexical'
|
||||
import { $insertNodes } from 'lexical'
|
||||
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const Placeholder = () => {
|
||||
type Props = {
|
||||
hideBadge?: boolean
|
||||
}
|
||||
|
||||
const Placeholder = ({ hideBadge }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
@ -20,30 +25,37 @@ const Placeholder = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className='pointer-events-auto flex h-full w-full cursor-text items-center px-2'
|
||||
className={cn(
|
||||
'pointer-events-auto flex h-full w-full cursor-text px-2',
|
||||
!hideBadge ? 'items-center' : 'items-start py-1',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleInsert('')
|
||||
}}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
{t('workflow.nodes.tool.insertPlaceholder1')}
|
||||
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
|
||||
<div
|
||||
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
|
||||
onClick={((e) => {
|
||||
e.stopPropagation()
|
||||
handleInsert('/')
|
||||
})}
|
||||
>
|
||||
{t('workflow.nodes.tool.insertPlaceholder2')}
|
||||
<div className={cn('flex grow items-center')}>
|
||||
<div className='flex items-center'>
|
||||
{t('workflow.nodes.tool.insertPlaceholder1')}
|
||||
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
|
||||
<div
|
||||
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
|
||||
onClick={((e) => {
|
||||
e.stopPropagation()
|
||||
handleInsert('/')
|
||||
})}
|
||||
>
|
||||
{t('workflow.nodes.tool.insertPlaceholder2')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className='shrink-0'
|
||||
text='String'
|
||||
uppercase={false}
|
||||
/>
|
||||
{!hideBadge && (
|
||||
<Badge
|
||||
className='shrink-0'
|
||||
text='String'
|
||||
uppercase={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -664,6 +664,12 @@ const translation = {
|
||||
desc: 'Insert user query template',
|
||||
},
|
||||
},
|
||||
requestURL: {
|
||||
item: {
|
||||
title: 'Request URL',
|
||||
desc: 'Insert request URL',
|
||||
},
|
||||
},
|
||||
existed: 'Already exists in the prompt',
|
||||
},
|
||||
imageUploader: {
|
||||
|
||||
@ -664,6 +664,12 @@ const translation = {
|
||||
desc: '插入用户查询模板',
|
||||
},
|
||||
},
|
||||
requestURL: {
|
||||
item: {
|
||||
title: '请求 URL',
|
||||
desc: '插入请求 URL',
|
||||
},
|
||||
},
|
||||
existed: 'Prompt 中已存在',
|
||||
},
|
||||
imageUploader: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user