human input step run

This commit is contained in:
JzoNg 2025-09-05 19:59:33 +08:00
parent d51db3afb3
commit be3c6da654
5 changed files with 116 additions and 240 deletions

View File

@ -33,7 +33,9 @@ export type BeforeRunFormProps = {
existVarValuesInForms: Record<string, any>[]
filteredExistVarForms: FormProps[]
generatedFormContentData?: Record<string, any>
setSubmittedData?: (data: Record<string, any>) => void
showGeneratedForm?: boolean
handleShowGeneratedForm?: (data: Record<string, any>) => void
handleHideGeneratedForm?: () => void
} & Partial<SpecialResultPanelProps>
function formatValue(value: string | any, type: InputVarType) {
@ -70,7 +72,9 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
filteredExistVarForms,
existVarValuesInForms,
generatedFormContentData,
setSubmittedData,
showGeneratedForm = false,
handleShowGeneratedForm,
handleHideGeneratedForm,
}) => {
const { t } = useTranslation()
@ -143,7 +147,7 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
}
if (isHumanInput)
setSubmittedData?.(submitData)
handleShowGeneratedForm?.(submitData)
else
onRun(submitData)
}
@ -167,7 +171,7 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
onHide={onHide}
>
<div className='h-0 grow overflow-y-auto pb-4'>
{!generatedFormContentData && (
{!showGeneratedForm && (
<div className='mt-3 space-y-4 px-4'>
{filteredExistVarForms.map((form, index) => (
<div key={index}>
@ -181,21 +185,29 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
))}
</div>
)}
{generatedFormContentData && (
<SingleRunForm />
{showGeneratedForm && generatedFormContentData && (
<SingleRunForm
nodeName={nodeName}
handleBack={handleHideGeneratedForm}
formContent={generatedFormContentData.formContent}
inputFields={generatedFormContentData.inputFields}
userActions={generatedFormContentData.userActions}
/>
)}
{!showGeneratedForm && (
<div className='mt-4 flex justify-between space-x-2 px-4' >
{!isHumanInput && (
<Button disabled={!isFileLoaded} variant='primary' className='w-0 grow space-x-2' onClick={handleRunOrGenerateForm}>
<div>{t(`${i18nPrefix}.startRun`)}</div>
</Button>
)}
{isHumanInput && (
<Button disabled={!isFileLoaded} variant='primary' className='w-0 grow space-x-2' onClick={handleRunOrGenerateForm}>
<div>{t('workflow.nodes.humanInput.singleRun.button')}</div>
</Button>
)}
</div>
)}
<div className='mt-4 flex justify-between space-x-2 px-4' >
{!isHumanInput && (
<Button disabled={!isFileLoaded} variant='primary' className='w-0 grow space-x-2' onClick={handleRunOrGenerateForm}>
<div>{t(`${i18nPrefix}.startRun`)}</div>
</Button>
)}
{isHumanInput && (
<Button disabled={!isFileLoaded} variant='primary' className='w-0 grow space-x-2' onClick={handleRunOrGenerateForm}>
<div>{t('workflow.nodes.humanInput.singleRun.button')}</div>
</Button>
)}
</div>
</div>
</PanelWrap>
)

View File

@ -1,60 +1,31 @@
'use client'
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import useDocumentTitle from '@/hooks/use-document-title'
import { useParams } from 'next/navigation'
import {
RiCheckboxCircleFill,
RiInformation2Fill,
} from '@remixicon/react'
import Loading from '@/app/components/base/loading'
import AppIcon from '@/app/components/base/app-icon'
import { RiArrowLeftLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import ContentItem from '@/app/components/base/chat/chat/answer/human-input-content/content-item'
import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types'
import type { GeneratedFormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
import { getHumanInputForm, submitHumanInputForm } from '@/service/share'
import { asyncRunSafe } from '@/utils'
import cn from '@/utils/classnames'
// import cn from '@/utils/classnames'
export type FormData = {
site: any
form_content: string
inputs: GeneratedFormInputItem[]
user_actions: UserAction[]
timeout: number
timeout_unit: 'hour' | 'day'
type Props = {
nodeName: string
formContent: string
inputFields: GeneratedFormInputItem[]
userActions: UserAction[]
handleBack?: () => void
}
const FormContent = () => {
const FormContent = ({
nodeName,
formContent,
inputFields,
userActions,
handleBack,
}: Props) => {
const { t } = useTranslation()
const { token } = useParams<{ token: string }>()
useDocumentTitle('')
const [isLoading, setIsLoading] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [formData, setFormData] = useState<FormData>()
const [contentList, setContentList] = useState<string[]>([])
const [inputs, setInputs] = useState({})
const [success, setSuccess] = useState(false)
const [expired, setExpired] = useState(false)
const [submitted, setSubmitted] = useState(false)
const site = formData?.site.site
const getButtonStyle = (style: UserActionButtonType) => {
if (style === UserActionButtonType.Primary)
return 'primary'
if (style === UserActionButtonType.Default)
return 'secondary'
if (style === UserActionButtonType.Accent)
return 'secondary-accent'
if (style === UserActionButtonType.Ghost)
return 'ghost'
}
const splitByOutputVar = (content: string): string[] => {
const outputVarRegex = /({{#\$output\.[^#]+#}})/g
const parts = content.split(outputVarRegex)
@ -69,12 +40,22 @@ const FormContent = () => {
else
initialInputs[item.output_variable_name] = undefined
})
setInputs(initialInputs)
return initialInputs
}
const initializeContentList = (formContent: string) => {
const parts = splitByOutputVar(formContent)
setContentList(parts)
const contentList = splitByOutputVar(formContent)
const defaultInputValues = initializeInputs(inputFields)
const [inputs, setInputs] = useState(defaultInputValues)
const getButtonStyle = (style: UserActionButtonType) => {
if (style === UserActionButtonType.Primary)
return 'primary'
if (style === UserActionButtonType.Default)
return 'secondary'
if (style === UserActionButtonType.Accent)
return 'secondary-accent'
if (style === UserActionButtonType.Ghost)
return 'ghost'
}
// use immer
@ -85,180 +66,45 @@ const FormContent = () => {
}))
}
const getForm = async (token: string) => {
try {
const data = await getHumanInputForm(token)
setFormData(data)
initializeInputs(data.inputs)
initializeContentList(data.form_content)
setIsLoading(false)
}
catch (error) {
console.error(error)
}
}
const submit = async (actionID: string) => {
setIsSubmitting(true)
try {
await submitHumanInputForm(token, { inputs, action: actionID })
setSuccess(true)
}
catch (e: any) {
if (e.status === 400) {
const [, errRespData] = await asyncRunSafe<{ error_code: string }>(e.json())
const { error_code } = errRespData || {}
if (error_code === 'human_input_form_expired')
setExpired(true)
if (error_code === 'human_input_form_submitted')
setSubmitted(true)
}
}
finally {
setIsSubmitting(false)
}
}
useEffect(() => {
getForm(token)
}, [token])
if (isLoading || !formData) {
return (
<Loading type='app' />
)
}
if (success) {
return (
<div className={cn('flex h-full w-full flex-col items-center justify-center')}>
<div className='min-w-[480px] max-w-[640px]'>
<div className='border-components-divider-subtle flex h-[320px] flex-col gap-4 rounded-[20px] border bg-chat-bubble-bg p-10 pb-9 shadow-lg backdrop-blur-sm'>
<div className='h-[56px] w-[56px] shrink-0 rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge p-3'>
<RiCheckboxCircleFill className='h-8 w-8 text-text-success' />
</div>
<div className='grow'>
<div className='title-4xl-semi-bold text-text-primary'>{t('share.humanInput.thanks')}</div>
<div className='title-4xl-semi-bold text-text-primary'>{t('share.humanInput.recorded')}</div>
</div>
<div className='system-2xs-regular-uppercase shrink-0 text-text-tertiary'>{t('share.humanInput.submissionID', { id: token })}</div>
</div>
<div className='flex flex-row-reverse px-2 py-3'>
<div className={cn(
'flex shrink-0 items-center gap-1.5 px-1',
)}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
<DifyLogo size='small' />
</div>
</div>
</div>
</div>
)
}
if (expired) {
return (
<div className={cn('flex h-full w-full flex-col items-center justify-center')}>
<div className='min-w-[480px] max-w-[640px]'>
<div className='border-components-divider-subtle flex h-[320px] flex-col gap-4 rounded-[20px] border bg-chat-bubble-bg p-10 pb-9 shadow-lg backdrop-blur-sm'>
<div className='h-[56px] w-[56px] shrink-0 rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge p-3'>
<RiInformation2Fill className='h-8 w-8 text-text-accent' />
</div>
<div className='grow'>
<div className='title-4xl-semi-bold text-text-primary'>{t('share.humanInput.sorry')}</div>
<div className='title-4xl-semi-bold text-text-primary'>{t('share.humanInput.expired')}</div>
</div>
<div className='system-2xs-regular-uppercase shrink-0 text-text-tertiary'>{t('share.humanInput.submissionID', { id: token })}</div>
</div>
<div className='flex flex-row-reverse px-2 py-3'>
<div className={cn(
'flex shrink-0 items-center gap-1.5 px-1',
)}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
<DifyLogo size='small' />
</div>
</div>
</div>
</div>
)
}
if (submitted) {
return (
<div className={cn('flex h-full w-full flex-col items-center justify-center')}>
<div className='min-w-[480px] max-w-[640px]'>
<div className='border-components-divider-subtle flex h-[320px] flex-col gap-4 rounded-[20px] border bg-chat-bubble-bg p-10 pb-9 shadow-lg backdrop-blur-sm'>
<div className='h-[56px] w-[56px] shrink-0 rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge p-3'>
<RiInformation2Fill className='h-8 w-8 text-text-accent' />
</div>
<div className='grow'>
<div className='title-4xl-semi-bold text-text-primary'>{t('share.humanInput.sorry')}</div>
<div className='title-4xl-semi-bold text-text-primary'>{t('share.humanInput.completed')}</div>
</div>
<div className='system-2xs-regular-uppercase shrink-0 text-text-tertiary'>{t('share.humanInput.submissionID', { id: token })}</div>
</div>
<div className='flex flex-row-reverse px-2 py-3'>
<div className={cn(
'flex shrink-0 items-center gap-1.5 px-1',
)}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
<DifyLogo size='small' />
</div>
</div>
</div>
</div>
)
// TODO
}
return (
<div className={cn('mx-auto flex h-full w-full max-w-[720px] flex-col items-center')}>
<div className='mt-4 flex w-full shrink-0 items-center gap-3 py-3'>
<AppIcon
size='large'
iconType={site.icon_type as any}
icon={site.icon}
background={site.icon_background}
imageUrl={site.icon_url}
/>
<div className='system-xl-semibold grow text-text-primary'>{site.title}</div>
</div>
<div className='h-0 w-full grow overflow-y-auto'>
<div className='border-components-divider-subtle rounded-[20px] border bg-chat-bubble-bg p-4 shadow-lg backdrop-blur-sm'>
{contentList.map((content, index) => (
<ContentItem
key={index}
content={content}
formInputFields={formData.inputs}
inputs={inputs}
onInputChange={handleInputsChange}
/>
<>
{inputFields.length > 0 && (
<div className='flex items-center p-4 pb-1'>
<div className='system-sm-semibold-uppercase flex cursor-pointer items-center text-text-accent' onClick={handleBack}>
<RiArrowLeftLine className='mr-1 h-4 w-4' />
{t('workflow.nodes.humanInput.singleRun.back')}
</div>
<div className='system-xs-regular mx-1 text-divider-deep'>/</div>
<div className='system-sm-semibold-uppercase text-text-secondary'>{nodeName}</div>
</div>
)}
<div className='px-4 py-3'>
{contentList.map((content, index) => (
<ContentItem
key={index}
content={content}
formInputFields={inputFields}
inputs={inputs}
onInputChange={handleInputsChange}
/>
))}
<div className='flex flex-wrap gap-1 py-1'>
{userActions.map((action: any) => (
<Button
key={action.id}
variant={getButtonStyle(action.button_style) as any}
onClick={() => submit(action.id)}
>
{action.title}
</Button>
))}
<div className='flex flex-wrap gap-1 py-1'>
{formData.user_actions.map((action: any) => (
<Button
key={action.id}
disabled={isSubmitting}
variant={getButtonStyle(action.button_style) as any}
onClick={() => submit(action.id)}
>
{action.title}
</Button>
))}
</div>
<div className='system-xs-regular mt-1 text-text-tertiary'>
{formData.timeout_unit === 'day' ? t('share.humanInput.timeoutDay', { count: formData.timeout }) : t('share.humanInput.timeoutHour', { count: formData.timeout })}
</div>
</div>
<div className='flex flex-row-reverse px-2 py-3'>
<div className={cn(
'flex shrink-0 items-center gap-1.5 px-1',
)}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
<DifyLogo size='small' />
</div>
</div>
</div>
</div>
</>
)
}

View File

@ -25,6 +25,7 @@ const useSingleRunFormParams = ({
const { t } = useTranslation()
const { inputs } = useNodeCrud<HumanInputNodeType>(id, payload)
const [submittedData, setSubmittedData] = useState<Record<string, any> | null>(null)
const [showGeneratedForm, setShowGeneratedForm] = useState(false)
const generatedInputs = useMemo(() => {
if (!inputs.form_content)
return []
@ -71,28 +72,43 @@ const useSingleRunFormParams = ({
return null
if (!generatedInputs.length) {
return {
content: inputs.form_content,
formContent: inputs.form_content,
inputFields: formContentOutputFields,
userActions: inputs.user_actions,
}
}
else {
if (!submittedData)
return null
const newContent = inputs.form_content.replace(/{{#(.*?)#}}/g, (originStr, varName) => {
return submittedData[varName] || isOutput(varName.split('.')) ? originStr : ''
if (isOutput(varName.split('.')))
return originStr
return submittedData[`#${varName}#`] ?? ''
})
return {
content: newContent,
formContent: newContent,
inputFields: formContentOutputFields,
userActions: inputs.user_actions,
}
}
}, [inputs.form_content, submittedData, formContentOutputFields])
}, [inputs.form_content, inputs.user_actions, submittedData, formContentOutputFields])
const handleShowGeneratedForm = (formValue: Record<string, any>) => {
setSubmittedData(formValue)
setShowGeneratedForm(true)
}
const handleHideGeneratedForm = () => {
setShowGeneratedForm(false)
}
return {
forms,
getDependentVars,
generatedFormContentData,
setSubmittedData,
showGeneratedForm,
handleShowGeneratedForm,
handleHideGeneratedForm,
}
}

View File

@ -1030,6 +1030,7 @@ const translation = {
singleRun: {
label: 'Form variables',
button: 'Generate Form',
back: 'Back',
},
},
},

View File

@ -1030,6 +1030,7 @@ const translation = {
singleRun: {
label: '表单变量',
button: '生成表单',
back: '返回',
},
},
},