From ac46cf499fb1c323df389206666436e529026e71 Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 22 Jan 2026 14:52:06 +0800 Subject: [PATCH] refactor: update human input form handling with new hooks and improve error management --- .../(humanInputLayout)/form/[token]/form.tsx | 146 +++++++++--------- web/eslint-suppressions.json | 5 +- web/i18n/en-US/share.json | 1 + web/i18n/zh-Hans/share.json | 1 + web/service/use-share.ts | 63 +++++++- 5 files changed, 142 insertions(+), 74 deletions(-) diff --git a/web/app/(humanInputLayout)/form/[token]/form.tsx b/web/app/(humanInputLayout)/form/[token]/form.tsx index 3a37a5cc6b..033168e7a7 100644 --- a/web/app/(humanInputLayout)/form/[token]/form.tsx +++ b/web/app/(humanInputLayout)/form/[token]/form.tsx @@ -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() - const [contentList, setContentList] = useState([]) - const [inputs, setInputs] = useState({}) + const [inputs, setInputs] = useState>({}) 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 = {} - 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 = {} + 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 ( ) @@ -153,7 +129,7 @@ const FormContent = () => {
-
+
@@ -181,7 +157,7 @@ const FormContent = () => {
-
+
@@ -204,6 +180,32 @@ const FormContent = () => { ) } + if (!formData) { + return ( +
+
+
+
+ +
+
+
{t('humanInput.formNotFound', { ns: 'share' })}
+
+
+
+
+
{t('chat.poweredBy', { ns: 'share' })}
+ +
+
+
+
+ ) + } + return (
diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index d0f2e47f85..850b19980b 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -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": { diff --git a/web/i18n/en-US/share.json b/web/i18n/en-US/share.json index 98c8149e7e..729602571e 100644 --- a/web/i18n/en-US/share.json +++ b/web/i18n/en-US/share.json @@ -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}}", diff --git a/web/i18n/zh-Hans/share.json b/web/i18n/zh-Hans/share.json index add0341658..e87abacca4 100644 --- a/web/i18n/zh-Hans/share.json +++ b/web/i18n/zh-Hans/share.json @@ -61,6 +61,7 @@ "humanInput.completed": "此请求似乎在其他地方得到了处理。", "humanInput.expirationTime": "此操作将在 {{relativeTime}}过期。", "humanInput.expired": "此请求似乎已过期。", + "humanInput.formNotFound": "表单不存在。", "humanInput.recorded": "您的输入已被记录。", "humanInput.sorry": "抱歉!", "humanInput.thanks": "谢谢!", diff --git a/web/service/use-share.ts b/web/service/use-share.ts index eef61ccc29..56dd0941f6 100644 --- a/web/service/use-share.ts +++ b/web/service/use-share.ts @@ -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({ + 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 + action: string + } +} + +export const useSubmitHumanInputForm = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'submit-human-input-form'], + mutationFn: ({ token, data }: SubmitHumanInputFormParams) => { + return submitHumanInputForm(token, data) + }, + }) +}