mirror of https://github.com/langgenius/dify.git
form content
This commit is contained in:
parent
36acd0b9dd
commit
bdf1e9ed3b
|
|
@ -0,0 +1,104 @@
|
|||
import React from 'react'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import Select from '@/app/components/base/select'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
|
||||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
import type { GeneratedFormInputItem } from '@/app/components/workflow/nodes/human-input/types'
|
||||
|
||||
type Props = {
|
||||
content: string
|
||||
formInputFields: GeneratedFormInputItem[]
|
||||
inputs: Record<string, any>
|
||||
onInputChange: (name: string, value: any) => void
|
||||
}
|
||||
|
||||
const ContentItem = ({ content, formInputFields, inputs, onInputChange }: Props) => {
|
||||
const isInputField = (field: string) => {
|
||||
const outputVarRegex = /{{#\$output\.[^#]+#}}/
|
||||
return outputVarRegex.test(field)
|
||||
}
|
||||
|
||||
const extractFieldName = (str: string) => {
|
||||
const outputVarRegex = /{{#\$output\.([^#]+)#}}/
|
||||
const match = str.match(outputVarRegex)
|
||||
return match ? match[1] : ''
|
||||
}
|
||||
|
||||
if (!isInputField(content)) {
|
||||
return (
|
||||
<Markdown content={content} />
|
||||
)
|
||||
}
|
||||
|
||||
const fieldName = extractFieldName(content)
|
||||
const formInputField = formInputFields.find(field => field.output_variable_name === fieldName)
|
||||
|
||||
if (!formInputField) return null
|
||||
|
||||
return (
|
||||
<div className='py-3'>
|
||||
{formInputField.type === 'select' && (
|
||||
<Select
|
||||
className='w-full'
|
||||
defaultValue={inputs[fieldName]}
|
||||
onSelect={i => onInputChange(fieldName, i.value)}
|
||||
items={(formInputField.options || []).map(i => ({ name: i, value: i }))}
|
||||
allowSearch={false}
|
||||
/>
|
||||
)}
|
||||
{formInputField.type === 'text-input' && (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={formInputField.placeholder}
|
||||
value={inputs[fieldName]}
|
||||
onChange={(e) => { onInputChange(fieldName, e.target.value) }}
|
||||
maxLength={formInputField.max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
/>
|
||||
)}
|
||||
{formInputField.type === 'paragraph' && (
|
||||
<Textarea
|
||||
className='h-[104px] sm:text-xs'
|
||||
placeholder={formInputField.placeholder}
|
||||
value={inputs[fieldName]}
|
||||
onChange={(e) => { onInputChange(fieldName, e.target.value) }}
|
||||
/>
|
||||
)}
|
||||
{formInputField.type === 'number' && (
|
||||
<Input
|
||||
type="number"
|
||||
value={inputs[fieldName]}
|
||||
onChange={(e) => { onInputChange(fieldName, e.target.value) }}
|
||||
/>
|
||||
)}
|
||||
{formInputField.type === 'file' && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
onChange={(files) => { onInputChange(fieldName, getProcessedFiles(files)[0]) }}
|
||||
fileConfig={{
|
||||
number_limits: 1,
|
||||
allowed_file_extensions: formInputField.allowed_file_extensions,
|
||||
allowed_file_types: formInputField.allowed_file_types,
|
||||
allowed_file_upload_methods: formInputField.allowed_file_upload_methods as any,
|
||||
// fileUploadConfig: (visionConfig as any).fileUploadConfig,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{formInputField.type === 'file-list' && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
onChange={(files) => { onInputChange(fieldName, getProcessedFiles(files)) }}
|
||||
fileConfig={{
|
||||
number_limits: formInputField.max_length,
|
||||
allowed_file_extensions: formInputField.allowed_file_extensions,
|
||||
allowed_file_types: formInputField.allowed_file_types,
|
||||
allowed_file_upload_methods: formInputField.allowed_file_upload_methods as any,
|
||||
// fileUploadConfig: (visionConfig as any).fileUploadConfig,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContentItem
|
||||
|
|
@ -9,18 +9,19 @@ import {
|
|||
} from '@remixicon/react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import Button from '@/app/components/base/button'
|
||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||
import ContentItem from './content-item'
|
||||
import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { FormInputItem, UserAction } 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: FormInputItem[]
|
||||
inputs: GeneratedFormInputItem[]
|
||||
user_actions: UserAction[]
|
||||
timeout: number
|
||||
timeout_unit: 'hour' | 'day'
|
||||
|
|
@ -32,9 +33,10 @@ const FormContent = () => {
|
|||
const { token } = useParams<{ token: string }>()
|
||||
useDocumentTitle('')
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
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)
|
||||
|
|
@ -53,10 +55,42 @@ const FormContent = () => {
|
|||
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) {
|
||||
|
|
@ -70,10 +104,15 @@ const FormContent = () => {
|
|||
await submitHumanInputForm(token, { inputs, action: actionID })
|
||||
setSuccess(true)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
setExpired(true)
|
||||
setSubmitted(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)
|
||||
|
|
@ -185,7 +224,15 @@ const FormContent = () => {
|
|||
</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'>
|
||||
<Markdown content={formData.form_content || ''} />
|
||||
{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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
|
||||
export const MOCK_DATA = {
|
||||
site: {
|
||||
|
|
@ -52,7 +53,32 @@ export const MOCK_DATA = {
|
|||
{{#$output.content#}}
|
||||
`,
|
||||
// 对每一个字段的描述,参考上方 FormInput 的定义。
|
||||
inputs: ['<FormInput>'],
|
||||
inputs: [
|
||||
{
|
||||
output_variable_name: 'content',
|
||||
type: InputVarType.paragraph,
|
||||
label: 'Name',
|
||||
options: [],
|
||||
max_length: 4096,
|
||||
placeholder: 'Enter your name',
|
||||
},
|
||||
{
|
||||
output_variable_name: 'location',
|
||||
type: InputVarType.textInput,
|
||||
label: 'Location',
|
||||
// placeholder: 'Enter your location',
|
||||
options: [],
|
||||
max_length: 4096,
|
||||
},
|
||||
{
|
||||
output_variable_name: 'season',
|
||||
type: InputVarType.textInput,
|
||||
label: 'Favorite Season',
|
||||
// placeholder: 'Enter your favorite season',
|
||||
options: [],
|
||||
max_length: 4096,
|
||||
},
|
||||
],
|
||||
// 用户对这个表单可采取的操作,参考上方 UserAction 的定义。
|
||||
user_actions: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ export type FormInputItem = {
|
|||
output_variable_name: string
|
||||
// only text-input and paragraph support placeholder
|
||||
placeholder?: FormInputItemPlaceholder
|
||||
options: any[]
|
||||
max_length: number
|
||||
allowed_file_extensions?: string[]
|
||||
allowed_file_types?: string[]
|
||||
allowed_file_upload_methods?: string[]
|
||||
}
|
||||
|
||||
export enum UserActionButtonType {
|
||||
|
|
@ -64,3 +69,15 @@ export type UserAction = {
|
|||
title: string
|
||||
button_style: UserActionButtonType
|
||||
}
|
||||
|
||||
export type GeneratedFormInputItem = {
|
||||
type: InputVarType
|
||||
output_variable_name: string
|
||||
// only text-input and paragraph support placeholder
|
||||
placeholder?: string
|
||||
options: any[]
|
||||
max_length: number
|
||||
allowed_file_extensions?: string[]
|
||||
allowed_file_types?: string[]
|
||||
allowed_file_upload_methods?: string[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import type {
|
|||
} from '@/models/share'
|
||||
import type { ChatConfig } from '@/app/components/base/chat/types'
|
||||
import type { AccessMode } from '@/models/access-control'
|
||||
import type { FormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { GeneratedFormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
|
||||
|
||||
function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) {
|
||||
switch (action) {
|
||||
|
|
@ -312,7 +312,7 @@ export const getHumanInputForm = (token: string) => {
|
|||
return get<{
|
||||
site: any
|
||||
form_content: string
|
||||
inputs: FormInputItem[]
|
||||
inputs: GeneratedFormInputItem[]
|
||||
user_actions: UserAction[]
|
||||
timeout: number
|
||||
timeout_unit: 'hour' | 'day'
|
||||
|
|
|
|||
Loading…
Reference in New Issue