mirror of https://github.com/langgenius/dify.git
single run form data
This commit is contained in:
parent
22683fba3f
commit
d51db3afb3
|
|
@ -11,10 +11,13 @@ import { InputVarType } from '@/app/components/workflow/types'
|
|||
import Toast from '@/app/components/base/toast'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
|
||||
import type { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { SpecialResultPanelProps } from '@/app/components/workflow/run/special-result-panel'
|
||||
import PanelWrap from './panel-wrap'
|
||||
import SingleRunForm from '@/app/components/workflow/nodes/human-input/components/single-run-form'
|
||||
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
export type BeforeRunFormProps = {
|
||||
|
|
@ -29,6 +32,8 @@ export type BeforeRunFormProps = {
|
|||
showSpecialResultPanel?: boolean
|
||||
existVarValuesInForms: Record<string, any>[]
|
||||
filteredExistVarForms: FormProps[]
|
||||
generatedFormContentData?: Record<string, any>
|
||||
setSubmittedData?: (data: Record<string, any>) => void
|
||||
} & Partial<SpecialResultPanelProps>
|
||||
|
||||
function formatValue(value: string | any, type: InputVarType) {
|
||||
|
|
@ -58,14 +63,19 @@ function formatValue(value: string | any, type: InputVarType) {
|
|||
}
|
||||
const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
||||
nodeName,
|
||||
nodeType,
|
||||
onHide,
|
||||
onRun,
|
||||
forms,
|
||||
filteredExistVarForms,
|
||||
existVarValuesInForms,
|
||||
generatedFormContentData,
|
||||
setSubmittedData,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isHumanInput = nodeType === BlockEnum.HumanInput
|
||||
|
||||
const isFileLoaded = (() => {
|
||||
if (!forms || forms.length === 0)
|
||||
return true
|
||||
|
|
@ -80,7 +90,8 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
|||
|
||||
return true
|
||||
})()
|
||||
const handleRun = () => {
|
||||
|
||||
const handleRunOrGenerateForm = () => {
|
||||
let errMsg = ''
|
||||
forms.forEach((form, i) => {
|
||||
const existVarValuesInForm = existVarValuesInForms[i]
|
||||
|
|
@ -131,8 +142,12 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
|||
return
|
||||
}
|
||||
|
||||
onRun(submitData)
|
||||
if (isHumanInput)
|
||||
setSubmittedData?.(submitData)
|
||||
else
|
||||
onRun(submitData)
|
||||
}
|
||||
|
||||
const hasRun = useRef(false)
|
||||
useEffect(() => {
|
||||
// React 18 run twice in dev mode
|
||||
|
|
@ -152,22 +167,34 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
|||
onHide={onHide}
|
||||
>
|
||||
<div className='h-0 grow overflow-y-auto pb-4'>
|
||||
<div className='mt-3 space-y-4 px-4'>
|
||||
{filteredExistVarForms.map((form, index) => (
|
||||
<div key={index}>
|
||||
<Form
|
||||
key={index}
|
||||
className={cn(index < forms.length - 1 && 'mb-4')}
|
||||
{...form}
|
||||
/>
|
||||
{index < forms.length - 1 && <Split />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!generatedFormContentData && (
|
||||
<div className='mt-3 space-y-4 px-4'>
|
||||
{filteredExistVarForms.map((form, index) => (
|
||||
<div key={index}>
|
||||
<Form
|
||||
key={index}
|
||||
className={cn(index < forms.length - 1 && 'mb-4')}
|
||||
{...form}
|
||||
/>
|
||||
{index < forms.length - 1 && <Split />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{generatedFormContentData && (
|
||||
<SingleRunForm />
|
||||
)}
|
||||
<div className='mt-4 flex justify-between space-x-2 px-4' >
|
||||
<Button disabled={!isFileLoaded} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
|
||||
<div>{t(`${i18nPrefix}.startRun`)}</div>
|
||||
</Button>
|
||||
{!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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,265 @@
|
|||
'use client'
|
||||
import React, { useEffect, 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 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'
|
||||
|
||||
export type FormData = {
|
||||
site: any
|
||||
form_content: string
|
||||
inputs: GeneratedFormInputItem[]
|
||||
user_actions: UserAction[]
|
||||
timeout: number
|
||||
timeout_unit: 'hour' | 'day'
|
||||
}
|
||||
|
||||
const FormContent = () => {
|
||||
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)
|
||||
return parts.filter(part => part.length > 0)
|
||||
}
|
||||
|
||||
const initializeInputs = (formInputs: GeneratedFormInputItem[]) => {
|
||||
const initialInputs: Record<string, any> = {}
|
||||
formInputs.forEach((item) => {
|
||||
if (item.type === 'text-input' || item.type === 'paragraph')
|
||||
initialInputs[item.output_variable_name] = ''
|
||||
else
|
||||
initialInputs[item.output_variable_name] = undefined
|
||||
})
|
||||
setInputs(initialInputs)
|
||||
}
|
||||
|
||||
const initializeContentList = (formContent: string) => {
|
||||
const parts = splitByOutputVar(formContent)
|
||||
setContentList(parts)
|
||||
}
|
||||
|
||||
// use immer
|
||||
const handleInputsChange = (name: string, value: any) => {
|
||||
setInputs(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FormContent)
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import type { HumanInputNodeType } from './types'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { useMemo } from 'react'
|
||||
import { isOutput } from './utils'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.humanInput'
|
||||
|
|
@ -24,7 +24,7 @@ const useSingleRunFormParams = ({
|
|||
}: Params) => {
|
||||
const { t } = useTranslation()
|
||||
const { inputs } = useNodeCrud<HumanInputNodeType>(id, payload)
|
||||
|
||||
const [submittedData, setSubmittedData] = useState<Record<string, any> | null>(null)
|
||||
const generatedInputs = useMemo(() => {
|
||||
if (!inputs.form_content)
|
||||
return []
|
||||
|
|
@ -41,6 +41,21 @@ const useSingleRunFormParams = ({
|
|||
return forms
|
||||
}, [runInputData, setRunInputData, generatedInputs])
|
||||
|
||||
const formContentOutputFields = useMemo(() => {
|
||||
const res = (inputs.inputs || [])
|
||||
.filter((item) => {
|
||||
return inputs.form_content.includes(`{{#$output.${item.output_variable_name}#}}`)
|
||||
})
|
||||
.map((item) => {
|
||||
return {
|
||||
type: item.type,
|
||||
output_variable_name: item.output_variable_name,
|
||||
placeholder: item.placeholder?.type === 'const' ? item.placeholder.value : '',
|
||||
}
|
||||
})
|
||||
return res
|
||||
}, [inputs.form_content, inputs.inputs])
|
||||
|
||||
const getDependentVars = () => {
|
||||
return generatedInputs.map((item) => {
|
||||
// Guard against null/undefined variable to prevent app crash
|
||||
|
|
@ -51,9 +66,33 @@ const useSingleRunFormParams = ({
|
|||
}).filter(arr => arr.length > 0)
|
||||
}
|
||||
|
||||
const generatedFormContentData = useMemo(() => {
|
||||
if (!inputs.form_content)
|
||||
return null
|
||||
if (!generatedInputs.length) {
|
||||
return {
|
||||
content: inputs.form_content,
|
||||
inputFields: formContentOutputFields,
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!submittedData)
|
||||
return null
|
||||
const newContent = inputs.form_content.replace(/{{#(.*?)#}}/g, (originStr, varName) => {
|
||||
return submittedData[varName] || isOutput(varName.split('.')) ? originStr : ''
|
||||
})
|
||||
return {
|
||||
content: newContent,
|
||||
inputFields: formContentOutputFields,
|
||||
}
|
||||
}
|
||||
}, [inputs.form_content, submittedData, formContentOutputFields])
|
||||
|
||||
return {
|
||||
forms,
|
||||
getDependentVars,
|
||||
generatedFormContentData,
|
||||
setSubmittedData,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1029,6 +1029,7 @@ const translation = {
|
|||
},
|
||||
singleRun: {
|
||||
label: 'Form variables',
|
||||
button: 'Generate Form',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1029,6 +1029,7 @@ const translation = {
|
|||
},
|
||||
singleRun: {
|
||||
label: '表单变量',
|
||||
button: '生成表单',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue