update data structure

This commit is contained in:
JzoNg 2025-08-06 14:06:33 +08:00
parent 3ed561d943
commit ce8325c83c
11 changed files with 261 additions and 123 deletions

View File

@ -0,0 +1,103 @@
import { memo, useCallback, useState } from 'react'
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 { 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
}
const EmailConfigureModal = ({
isShow,
onClose,
onConfirm,
}: EmailConfigureModalProps) => {
const { t } = useTranslation()
const [recipients, setRecipients] = useState<Recipient[]>([])
const [subject, setSubject] = useState('')
const [body, setBody] = useState('')
const handleConfirm = useCallback(() => {
onConfirm({
recipients: recipients.map(recipient => recipient.value),
subject,
body,
})
}, [recipients, subject, body, onConfirm])
return (
<Modal
isShow={isShow}
onClose={noop}
className='relative !max-w-[720px] !p-0'
>
<div className='absolute right-5 top-5 cursor-pointer p-1.5' onClick={onClose}>
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
</div>
<div className='space-y-1 p-6 pb-3'>
<div className='title-2xl-semi-bold text-text-primary'>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.title`)}</div>
<div className='system-xs-regular text-text-tertiary'>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.description`)}</div>
</div>
<div className='space-y-5 px-6 py-3'>
<div>
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.recipient`)}
</div>
</div>
<div>
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.subject`)}
</div>
<Input
className='w-full'
value={subject}
onChange={e => setSubject(e.target.value)}
placeholder={t(`${i18nPrefix}.deliveryMethod.emailConfigure.subjectPlaceholder`)}
/>
</div>
<div>
<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"
value={body}
onChange={e => setBody(e.target.value)}
placeholder={t('email.configure.enterBody', 'Enter email content')}
/>
</div>
</div>
<div className='flex flex-row-reverse gap-2 p-6 pt-5'>
<Button
variant='primary'
className='w-[72px]'
onClick={onClose}
>
{t('common.operation.save')}
</Button>
<Button
className='w-[72px]'
onClick={handleConfirm}
>
{t('common.operation.cancel')}
</Button>
</div>
</Modal>
)
}
export default memo(EmailConfigureModal)

View File

@ -37,36 +37,36 @@ const DeliveryMethodForm: React.FC<Props> = ({ value, onchange }) => {
return (
<div className='px-4 py-2'>
<div className='mb-1 flex items-center justify-between'>
<div className='flex items-center gap-0.5'>
<div className='system-sm-semibold-uppercase text-text-secondary'>{t(`${i18nPrefix}.deliveryMethod.title`)}</div>
<Tooltip
popupContent={t(`${i18nPrefix}.deliveryMethod.tooltip`)}
/>
</div>
<div className='flex items-center px-1'>
<MethodSelector
data={value}
onAdd={handleMethodAdd}
/>
</div>
<div className='mb-1 flex items-center justify-between'>
<div className='flex items-center gap-0.5'>
<div className='system-sm-semibold-uppercase text-text-secondary'>{t(`${i18nPrefix}.deliveryMethod.title`)}</div>
<Tooltip
popupContent={t(`${i18nPrefix}.deliveryMethod.tooltip`)}
/>
</div>
<div className='flex items-center px-1'>
<MethodSelector
data={value}
onAdd={handleMethodAdd}
/>
</div>
{!value.length && (
<div className='system-xs-regular flex items-center justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary'>{t(`${i18nPrefix}.deliveryMethod.emptyTip`)}</div>
)}
{value.length > 0 && (
<div className='space-y-1'>
{value.map((method, index) => (
<MethodItem
method={method}
key={index}
onChange={handleMethodChange}
onDelete={handleMethodDelete}
/>
))}
</div>
)}
</div>
{!value.length && (
<div className='system-xs-regular flex items-center justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary'>{t(`${i18nPrefix}.deliveryMethod.emptyTip`)}</div>
)}
{value.length > 0 && (
<div className='space-y-1'>
{value.map((method, index) => (
<MethodItem
method={method}
key={index}
onChange={handleMethodChange}
onDelete={handleMethodDelete}
/>
))}
</div>
)}
</div>
)
}

View File

@ -10,6 +10,7 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu
import Button from '@/app/components/base/button'
import Switch from '@/app/components/base/switch'
import Indicator from '@/app/components/header/indicator'
import EmailConfigureModal from './email-configure-modal'
import type { DeliveryMethod } from '../../types'
import { DeliveryMethodType } from '../../types'
import cn from '@/utils/classnames'
@ -25,6 +26,7 @@ type Props = {
const DeliveryMethodItem: React.FC<Props> = ({ method, onChange, onDelete }) => {
const { t } = useTranslation()
const [isHovering, setIsHovering] = React.useState(false)
const [showEmailModal, setShowEmailModal] = React.useState(false)
const handleEnableStatusChange = (enabled: boolean) => {
onChange({
@ -34,59 +36,71 @@ const DeliveryMethodItem: React.FC<Props> = ({ method, onChange, onDelete }) =>
}
return (
<div
className={cn('group flex h-8 items-center justify-between rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-1.5 pr-2 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isHovering && 'border-state-destructive-border bg-state-destructive-hover hover:bg-state-destructive-hover')}
>
<div className='flex items-center gap-1.5'>
{method.type === DeliveryMethodType.WebApp && (
<div className='rounded-[4px] border border-divider-regular bg-components-icon-bg-indigo-solid p-0.5'>
<RiRobot2Fill className='h-3.5 w-3.5 text-text-primary-on-surface' />
</div>
)}
{method.type === DeliveryMethodType.Email && (
<div className='rounded-[4px] border border-divider-regular bg-components-icon-bg-blue-solid p-0.5'>
<RiMailSendFill className='h-3.5 w-3.5 text-text-primary-on-surface' />
</div>
)}
<div className='system-xs-medium capitalize text-text-secondary'>{method.type}</div>
</div>
<div className='flex items-center gap-1'>
<div className='hidden items-end gap-1 group-hover:flex'>
{method.type === DeliveryMethodType.Email && method.configure && (
<ActionButton>
<RiEqualizer2Line className='h-4 w-4' />
</ActionButton>
<>
<div
className={cn('group flex h-8 items-center justify-between rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-1.5 pr-2 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isHovering && 'border-state-destructive-border bg-state-destructive-hover hover:bg-state-destructive-hover')}
>
<div className='flex items-center gap-1.5'>
{method.type === DeliveryMethodType.WebApp && (
<div className='rounded-[4px] border border-divider-regular bg-components-icon-bg-indigo-solid p-0.5'>
<RiRobot2Fill className='h-3.5 w-3.5 text-text-primary-on-surface' />
</div>
)}
{method.type === DeliveryMethodType.Email && (
<div className='rounded-[4px] border border-divider-regular bg-components-icon-bg-blue-solid p-0.5'>
<RiMailSendFill className='h-3.5 w-3.5 text-text-primary-on-surface' />
</div>
)}
<div className='system-xs-medium capitalize text-text-secondary'>{method.type}</div>
</div>
<div className='flex items-center gap-1'>
<div className='hidden items-end gap-1 group-hover:flex'>
{method.type === DeliveryMethodType.Email && method.config && (
<ActionButton onClick={() => setShowEmailModal(true)}>
<RiEqualizer2Line className='h-4 w-4' />
</ActionButton>
)}
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<ActionButton
state={isHovering ? ActionButtonState.Destructive : ActionButtonState.Default}
onClick={() => onDelete(method.type)}
>
<RiDeleteBinLine className='h-4 w-4' />
</ActionButton>
</div>
</div>
{(method.config || method.type === DeliveryMethodType.WebApp) && (
<Switch
defaultValue={method.enabled}
onChange={handleEnableStatusChange}
/>
)}
{method.type === DeliveryMethodType.Email && !method.config && (
<Button
className='-mr-1'
size='small'
onClick={() => setShowEmailModal(true)}
>
{t(`${i18nPrefix}.deliveryMethod.notConfigured`)}
<Indicator color='orange' className='ml-1' />
</Button>
)}
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<ActionButton
state={isHovering ? ActionButtonState.Destructive : ActionButtonState.Default}
onClick={() => onDelete(method.type)}
>
<RiDeleteBinLine className='h-4 w-4' />
</ActionButton>
</div>
</div>
{(method.configure || method.type === DeliveryMethodType.WebApp) && (
<Switch
defaultValue={method.enabled}
onChange={handleEnableStatusChange}
/>
)}
{method.type === DeliveryMethodType.Email && !method.configure && (
<Button
className='-mr-1'
size='small'
onClick={() => onChange({ ...method, enabled: !method.enabled })}
>
{t(`${i18nPrefix}.deliveryMethod.notConfigured`)}
<Indicator color='orange' className='ml-1' />
</Button>
)}
</div>
</div>
{showEmailModal && (
<EmailConfigureModal
isShow={showEmailModal}
onClose={() => setShowEmailModal(false)}
onConfirm={(data) => {
onChange({ ...method, ...data })
setShowEmailModal(false)
}}
/>
)}
</>
)
}

View File

@ -28,22 +28,22 @@ const UserActionItem: FC<Props> = ({
<div className='shrink-0'>
<Input
wrapperClassName='w-[120px]'
value={data.name}
value={data.id}
placeholder={t(`${i18nPrefix}.userActions.actionNamePlaceholder`)}
onChange={e => onChange({ ...data, name: e.target.value })}
onChange={e => onChange({ ...data, id: e.target.value })}
/>
</div>
<div className='grow'>
<Input
value={data.text}
value={data.title}
placeholder={t(`${i18nPrefix}.userActions.buttonTextPlaceholder`)}
onChange={e => onChange({ ...data, text: e.target.value })}
onChange={e => onChange({ ...data, title: e.target.value })}
/>
</div>
<ButtonStyleDropdown
text={data.text}
data={data.type}
onChange={type => onChange({ ...data, type })}
text={data.title}
data={data.button_style}
onChange={type => onChange({ ...data, button_style: type })}
/>
<Button
className='px-2'

View File

@ -5,7 +5,7 @@ import { ALL_CHAT_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault<HumanInputNodeType> = {
defaultValue: {
deliveryMethod: [
delivery_methods: [
{
type: DeliveryMethodType.WebApp,
enabled: true,
@ -15,30 +15,26 @@ const nodeDefault: NodeDefault<HumanInputNodeType> = {
enabled: false,
},
],
userActions: [
user_actions: [
{
id: 'approve-action',
name: 'approve',
text: 'Post to X',
type: UserActionButtonType.Primary,
title: 'Post to X',
button_style: UserActionButtonType.Primary,
},
{
id: 'regenerate-action',
name: 'regenerate',
text: 'regenerate',
type: UserActionButtonType.Default,
title: 'regenerate',
button_style: UserActionButtonType.Default,
},
{
id: 'thinking-action',
name: 'thinking',
text: 'think more',
type: UserActionButtonType.Accent,
title: 'thinking',
button_style: UserActionButtonType.Accent,
},
{
id: 'cancel-action',
name: 'cancel',
text: 'cancel',
type: UserActionButtonType.Ghost,
title: 'cancel',
button_style: UserActionButtonType.Ghost,
},
],
timeout: {

View File

@ -16,8 +16,8 @@ const Node: FC<NodeProps<HumanInputNodeType>> = (props) => {
const { t } = useTranslation()
const { data } = props
const deliveryMethods = data.deliveryMethod
const userActions = data.userActions
const deliveryMethods = data.delivery_methods
const userActions = data.user_actions
return (
<>

View File

@ -14,7 +14,6 @@ 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 { v4 as uuid4 } from 'uuid'
const i18nPrefix = 'workflow.nodes.humanInput'
@ -35,7 +34,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
<div className='py-2'>
{/* delivery methods */}
<DeliveryMethod
value={inputs.deliveryMethod || []}
value={inputs.delivery_methods || []}
onchange={handleDeliveryMethodChange}
/>
<div className='px-4 py-2'>
@ -54,10 +53,9 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
<ActionButton
onClick={() => {
handleUserActionAdd({
id: uuid4(),
name: 'Action',
text: 'Button Text',
type: UserActionButtonType.Default,
id: 'Action',
title: 'Button Text',
button_style: UserActionButtonType.Default,
})
}}
>
@ -65,12 +63,12 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
</ActionButton>
</div>
</div>
{!inputs.userActions.length && (
{!inputs.user_actions.length && (
<div className='system-xs-regular flex items-center justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary'>{t(`${i18nPrefix}.userActions.emptyTip`)}</div>
)}
{inputs.userActions.length > 0 && (
{inputs.user_actions.length > 0 && (
<div className='space-y-2'>
{inputs.userActions.map(action => (
{inputs.user_actions.map(action => (
<UserActionItem
key={action.id}
data={action}

View File

@ -1,9 +1,9 @@
import type { CommonNodeType, Variable } from '@/app/components/workflow/types'
export type HumanInputNodeType = CommonNodeType & {
deliveryMethod: DeliveryMethod[]
formContent: any
userActions: UserAction[]
delivery_methods: DeliveryMethod[]
form_content: any
user_actions: UserAction[]
timeout: Timeout
outputs: Variable[]
}
@ -22,7 +22,7 @@ export enum DeliveryMethodType {
export type DeliveryMethod = {
type: DeliveryMethodType
enabled: boolean
configure?: Record<string, any>
config?: Record<string, any>
}
export enum UserActionButtonType {
@ -34,7 +34,6 @@ export enum UserActionButtonType {
export type UserAction = {
id: string
name: string
text: string
type: UserActionButtonType
title: string
button_style: UserActionButtonType
}

View File

@ -14,34 +14,34 @@ const useConfig = (id: string, payload: HumanInputNodeType) => {
const handleDeliveryMethodChange = (methods: DeliveryMethod[]) => {
setInputs({
...inputs,
deliveryMethod: methods,
delivery_methods: methods,
})
}
const handleUserActionAdd = (newAction: UserAction) => {
setInputs({
...inputs,
userActions: [...inputs.userActions, newAction],
user_actions: [...inputs.user_actions, newAction],
})
}
const handleUserActionChange = (updatedAction: UserAction) => {
const newActions = produce(inputs.userActions, (draft) => {
const newActions = produce(inputs.user_actions, (draft) => {
const index = draft.findIndex(a => a.id === updatedAction.id)
if (index !== -1)
draft[index] = updatedAction
})
setInputs({
...inputs,
userActions: newActions,
user_actions: newActions,
})
}
const handleUserActionDelete = (actionId: string) => {
const newActions = inputs.userActions.filter(action => action.id !== actionId)
const newActions = inputs.user_actions.filter(action => action.id !== actionId)
setInputs({
...inputs,
userActions: newActions,
user_actions: newActions,
})
}

View File

@ -928,6 +928,20 @@ const translation = {
emailConfigure: {
title: 'Email',
description: 'Send request for input via email',
recipient: 'Recipient',
allMembers: 'All members within the {{workspaceName}}',
subject: 'Email Subject',
subjectPlaceholder: 'Enter email subject',
body: 'Email Body',
bodyPlaceholder: 'Enter email body',
requestURLTip: 'The Request URL variable is the triggering entry point for Human Input.',
memberSelector: {
title: 'Add workspace members or external recipients',
trigger: 'Select',
placeholder: 'Emails, comma separated',
add: '+ Add',
added: 'Added',
},
},
},
formContent: 'form content',

View File

@ -929,6 +929,20 @@ const translation = {
emailConfigure: {
title: '电子邮件配置',
description: '通过电子邮件发送输入请求',
recipient: '收件人',
allMembers: '所有成员({{workspaceName}}',
subject: '邮件主题',
subjectPlaceholder: '输入邮件主题',
body: '邮件正文',
bodyPlaceholder: '输入邮件正文',
requestURLTip: '请求 URL 变量是人类输入的触发入口。',
memberSelector: {
title: '添加工作区成员或外部收件人',
trigger: '选择',
placeholder: '电子邮件,以逗号分隔',
add: '+ 添加',
added: '已添加',
},
},
},
formContent: '表单内容',