mirror of https://github.com/langgenius/dify.git
human input step run
This commit is contained in:
parent
d51db3afb3
commit
be3c6da654
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1030,6 +1030,7 @@ const translation = {
|
|||
singleRun: {
|
||||
label: 'Form variables',
|
||||
button: 'Generate Form',
|
||||
back: 'Back',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1030,6 +1030,7 @@ const translation = {
|
|||
singleRun: {
|
||||
label: '表单变量',
|
||||
button: '生成表单',
|
||||
back: '返回',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue