mail body input

This commit is contained in:
JzoNg 2025-08-06 17:01:53 +08:00
parent ce8325c83c
commit 82530df38f
19 changed files with 479 additions and 44 deletions

View File

@ -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]

View File

@ -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} />

View File

@ -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(() => {

View File

@ -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(

View File

@ -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

View File

@ -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'

View File

@ -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
}

View File

@ -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)

View File

@ -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[]

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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)
}}
/>

View File

@ -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>

View File

@ -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' />

View File

@ -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 {

View File

@ -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>
)
}

View File

@ -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: {

View File

@ -664,6 +664,12 @@ const translation = {
desc: '插入用户查询模板',
},
},
requestURL: {
item: {
title: '请求 URL',
desc: '插入请求 URL',
},
},
existed: 'Prompt 中已存在',
},
imageUploader: {