mirror of
https://github.com/langgenius/dify.git
synced 2026-02-19 17:34:41 +08:00
refactor: update human input form handling with new hooks and improve error management
This commit is contained in:
parent
8f780cad4c
commit
ac46cf499f
@ -1,12 +1,15 @@
|
||||
'use client'
|
||||
import type { FormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { HumanInputFormError } from '@/service/use-share'
|
||||
import {
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiInformation2Fill,
|
||||
} from '@remixicon/react'
|
||||
import { produce } from 'immer'
|
||||
import { useParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
@ -16,8 +19,7 @@ import { getButtonStyle } from '@/app/components/base/chat/chat/answer/human-inp
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { getHumanInputForm, submitHumanInputForm } from '@/service/share'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { useGetHumanInputForm, useSubmitHumanInputForm } from '@/service/use-share'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type FormData = {
|
||||
@ -35,14 +37,15 @@ const FormContent = () => {
|
||||
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 [inputs, setInputs] = useState<Record<string, string>>({})
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [expired, setExpired] = useState(false)
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
|
||||
const { mutate: submitForm, isPending: isSubmitting } = useSubmitHumanInputForm()
|
||||
|
||||
const { data: formData, isLoading, error } = useGetHumanInputForm(token)
|
||||
|
||||
const expired = (error as HumanInputFormError | null)?.code === 'human_input_form_expired'
|
||||
const submitted = (error as HumanInputFormError | null)?.code === 'human_input_form_submitted'
|
||||
|
||||
const site = formData?.site.site
|
||||
|
||||
@ -52,69 +55,42 @@ const FormContent = () => {
|
||||
return parts.filter(part => part.length > 0)
|
||||
}
|
||||
|
||||
const initializeInputs = (formInputs: FormInputItem[]) => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
const contentList = useMemo(() => {
|
||||
if (!formData?.form_content)
|
||||
return []
|
||||
return splitByOutputVar(formData.form_content)
|
||||
}, [formData?.form_content])
|
||||
|
||||
useEffect(() => {
|
||||
getForm(token)
|
||||
}, [token])
|
||||
if (!formData?.inputs)
|
||||
return
|
||||
const initialInputs: Record<string, string> = {}
|
||||
formData.inputs.forEach((item) => {
|
||||
initialInputs[item.output_variable_name] = ''
|
||||
})
|
||||
setInputs(initialInputs)
|
||||
}, [formData?.inputs])
|
||||
|
||||
if (isLoading || !formData) {
|
||||
// use immer
|
||||
const handleInputsChange = (name: string, value: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft[name] = value
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
|
||||
const submit = (actionID: string) => {
|
||||
submitForm(
|
||||
{ token, data: { inputs, action: actionID } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSuccess(true)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Loading type="app" />
|
||||
)
|
||||
@ -153,7 +129,7 @@ const FormContent = () => {
|
||||
<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">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center 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">
|
||||
@ -181,7 +157,7 @@ const FormContent = () => {
|
||||
<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">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center 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">
|
||||
@ -204,6 +180,32 @@ const FormContent = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (!formData) {
|
||||
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="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge p-3">
|
||||
<RiErrorWarningFill className="h-8 w-8 text-text-destructive" />
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div className="title-4xl-semi-bold text-text-primary">{t('humanInput.formNotFound', { ns: 'share' })}</div>
|
||||
</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('chat.poweredBy', { ns: 'share' })}</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">
|
||||
|
||||
@ -100,8 +100,11 @@
|
||||
}
|
||||
},
|
||||
"app/(humanInputLayout)/form/[token]/form.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 7
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"app/(shareLayout)/components/splash.tsx": {
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
"humanInput.completed": "Seems like this request was dealt with elsewhere.",
|
||||
"humanInput.expirationTime": "This action will expire {{relativeTime}}.",
|
||||
"humanInput.expired": "Seems like this request has expired.",
|
||||
"humanInput.formNotFound": "Form not found.",
|
||||
"humanInput.recorded": "Your input has been recorded.",
|
||||
"humanInput.sorry": "Sorry!",
|
||||
"humanInput.submissionID": "submission_id: {{id}}",
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
"humanInput.completed": "此请求似乎在其他地方得到了处理。",
|
||||
"humanInput.expirationTime": "此操作将在 {{relativeTime}}过期。",
|
||||
"humanInput.expired": "此请求似乎已过期。",
|
||||
"humanInput.formNotFound": "表单不存在。",
|
||||
"humanInput.recorded": "您的输入已被记录。",
|
||||
"humanInput.sorry": "抱歉!",
|
||||
"humanInput.thanks": "谢谢!",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { FormData as HumanInputFormData } from '@/app/(humanInputLayout)/form/[token]/form'
|
||||
import type { AppConversationData, ConversationItem } from '@/models/share'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
fetchAppInfo,
|
||||
fetchAppMeta,
|
||||
@ -8,6 +9,8 @@ import {
|
||||
fetchConversations,
|
||||
generationConversationName,
|
||||
getAppAccessModeByAppCode,
|
||||
getHumanInputForm,
|
||||
submitHumanInputForm,
|
||||
} from './share'
|
||||
import { useInvalid } from './use-base'
|
||||
|
||||
@ -48,6 +51,7 @@ export const shareQueryKeys = {
|
||||
conversationList: (params: ShareConversationsParams) => [NAME_SPACE, 'conversations', params] as const,
|
||||
chatList: (params: ShareChatListParams) => [NAME_SPACE, 'chatList', params] as const,
|
||||
conversationName: (params: ShareConversationNameParams) => [NAME_SPACE, 'conversationName', params] as const,
|
||||
humanInputForm: (token: string) => [NAME_SPACE, 'humanInputForm', token] as const,
|
||||
}
|
||||
|
||||
export const useGetWebAppAccessModeByCode = (code: string | null) => {
|
||||
@ -148,3 +152,60 @@ export const useShareConversationName = (params: ShareConversationNameParams, op
|
||||
export const useInvalidateShareConversations = () => {
|
||||
return useInvalid(shareQueryKeys.conversations)
|
||||
}
|
||||
|
||||
export class HumanInputFormError extends Error {
|
||||
code: string
|
||||
status: number
|
||||
|
||||
constructor(code: string, message: string, status: number) {
|
||||
super(message)
|
||||
this.name = 'HumanInputFormError'
|
||||
this.code = code
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
export const useGetHumanInputForm = (token: string, options: ShareQueryOptions = {}) => {
|
||||
const {
|
||||
enabled = true,
|
||||
refetchOnReconnect,
|
||||
refetchOnWindowFocus,
|
||||
} = options
|
||||
return useQuery<HumanInputFormData, HumanInputFormError>({
|
||||
queryKey: shareQueryKeys.humanInputForm(token),
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await getHumanInputForm(token)
|
||||
}
|
||||
catch (error) {
|
||||
const response = error as Response
|
||||
if (response.status && response.json) {
|
||||
const errorData = await response.json() as { code: string, message: string }
|
||||
throw new HumanInputFormError(errorData.code, errorData.message, response.status)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
enabled: enabled && !!token,
|
||||
refetchOnReconnect,
|
||||
refetchOnWindowFocus,
|
||||
retry: false,
|
||||
})
|
||||
}
|
||||
|
||||
export type SubmitHumanInputFormParams = {
|
||||
token: string
|
||||
data: {
|
||||
inputs: Record<string, unknown>
|
||||
action: string
|
||||
}
|
||||
}
|
||||
|
||||
export const useSubmitHumanInputForm = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'submit-human-input-form'],
|
||||
mutationFn: ({ token, data }: SubmitHumanInputFormParams) => {
|
||||
return submitHumanInputForm(token, data)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user