mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 01:48:04 +08:00
feat(workflow): add selection context menu helpers and integrate with context menu component (#34013)
Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: lif <1835304752@qq.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: tmimmanuel <14046872+tmimmanuel@users.noreply.github.com> Co-authored-by: Desel72 <pedroluiscolmenares722@gmail.com> Co-authored-by: Renzo <170978465+RenzoMXD@users.noreply.github.com> Co-authored-by: Krishna Chaitanya <krishnabkc15@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@ -2,31 +2,23 @@ import type { VarInInspect } from '@/types/workflow'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message'
|
||||
import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
|
||||
import {
|
||||
checkJsonSchemaDepth,
|
||||
getValidationErrorMessage,
|
||||
validateSchemaAgainstDraft7,
|
||||
} from '@/app/components/workflow/nodes/llm/utils'
|
||||
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import {
|
||||
validateJSONSchema,
|
||||
} from '@/app/components/workflow/variable-inspect/utils'
|
||||
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { PreviewMode } from '../../base/features/types'
|
||||
import BoolValue from '../panel/chat-variable-panel/components/bool-value'
|
||||
import DisplayContent from './display-content'
|
||||
import LargeDataAlert from './large-data-alert'
|
||||
import { CHUNK_SCHEMA_TYPES, PreviewType } from './types'
|
||||
import {
|
||||
BoolArraySection,
|
||||
ErrorMessages,
|
||||
FileEditorSection,
|
||||
JsonEditorSection,
|
||||
TextEditorSection,
|
||||
} from './value-content-sections'
|
||||
import {
|
||||
formatInspectFileValue,
|
||||
getValueEditorState,
|
||||
isFileValueUploaded,
|
||||
validateInspectJsonValue,
|
||||
} from './value-content.helpers'
|
||||
|
||||
type Props = {
|
||||
currentVar: VarInInspect
|
||||
@ -42,35 +34,24 @@ const ValueContent = ({
|
||||
const contentContainerRef = useRef<HTMLDivElement>(null)
|
||||
const errorMessageRef = useRef<HTMLDivElement>(null)
|
||||
const [editorHeight, setEditorHeight] = useState(0)
|
||||
const showTextEditor = currentVar.value_type === 'secret' || currentVar.value_type === 'string' || currentVar.value_type === 'number'
|
||||
const showBoolEditor = typeof currentVar.value === 'boolean'
|
||||
const showBoolArrayEditor = Array.isArray(currentVar.value) && currentVar.value.every(v => typeof v === 'boolean')
|
||||
const isSysFiles = currentVar.type === VarInInspectType.system && currentVar.name === 'files'
|
||||
const showJSONEditor = !isSysFiles && (currentVar.value_type === 'object' || currentVar.value_type === 'array[string]' || currentVar.value_type === 'array[number]' || currentVar.value_type === 'array[object]' || currentVar.value_type === 'array[any]')
|
||||
const showFileEditor = isSysFiles || currentVar.value_type === 'file' || currentVar.value_type === 'array[file]'
|
||||
const textEditorDisabled = currentVar.type === VarInInspectType.environment || (currentVar.type === VarInInspectType.system && currentVar.name !== 'query' && currentVar.name !== 'files')
|
||||
const JSONEditorDisabled = currentVar.value_type === 'array[any]'
|
||||
const {
|
||||
showTextEditor,
|
||||
showBoolEditor,
|
||||
showBoolArrayEditor,
|
||||
isSysFiles,
|
||||
showJSONEditor,
|
||||
showFileEditor,
|
||||
textEditorDisabled,
|
||||
JSONEditorDisabled,
|
||||
hasChunks,
|
||||
} = useMemo(() => getValueEditorState(currentVar), [currentVar])
|
||||
const fileUploadConfig = useStore(s => s.fileUploadConfig)
|
||||
|
||||
const hasChunks = useMemo(() => {
|
||||
if (!currentVar.schemaType)
|
||||
return false
|
||||
return CHUNK_SCHEMA_TYPES.includes(currentVar.schemaType)
|
||||
}, [currentVar.schemaType])
|
||||
|
||||
const formatFileValue = (value: VarInInspect) => {
|
||||
if (value.value_type === 'file')
|
||||
return value.value ? getProcessedFilesFromResponse([value.value]) : []
|
||||
if (value.value_type === 'array[file]' || (value.type === VarInInspectType.system && currentVar.name === 'files'))
|
||||
return value.value && value.value.length > 0 ? getProcessedFilesFromResponse(value.value) : []
|
||||
return []
|
||||
}
|
||||
|
||||
const [value, setValue] = useState<any>()
|
||||
const [json, setJson] = useState('')
|
||||
const [parseError, setParseError] = useState<Error | null>(null)
|
||||
const [validationError, setValidationError] = useState<string>('')
|
||||
const [fileValue, setFileValue] = useState<any>(() => formatFileValue(currentVar))
|
||||
const [fileValue, setFileValue] = useState<any>(() => formatInspectFileValue(currentVar))
|
||||
|
||||
const { run: debounceValueChange } = useDebounceFn(handleValueChange, { wait: 500 })
|
||||
|
||||
@ -87,7 +68,7 @@ const ValueContent = ({
|
||||
setJson(currentVar.value != null ? JSON.stringify(currentVar.value, null, 2) : '')
|
||||
|
||||
if (showFileEditor)
|
||||
setFileValue(formatFileValue(currentVar))
|
||||
setFileValue(formatInspectFileValue(currentVar))
|
||||
}, [currentVar.id, currentVar.value])
|
||||
|
||||
const handleTextChange = (value: string) => {
|
||||
@ -105,40 +86,10 @@ const ValueContent = ({
|
||||
}
|
||||
|
||||
const jsonValueValidate = (value: string, type: string) => {
|
||||
try {
|
||||
const newJSONSchema = JSON.parse(value)
|
||||
setParseError(null)
|
||||
const result = validateJSONSchema(newJSONSchema, type)
|
||||
if (!result.success) {
|
||||
setValidationError(result.error.message)
|
||||
return false
|
||||
}
|
||||
if (type === 'object' || type === 'array[object]') {
|
||||
const schemaDepth = checkJsonSchemaDepth(newJSONSchema)
|
||||
if (schemaDepth > JSON_SCHEMA_MAX_DEPTH) {
|
||||
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
|
||||
return false
|
||||
}
|
||||
const validationErrors = validateSchemaAgainstDraft7(newJSONSchema)
|
||||
if (validationErrors.length > 0) {
|
||||
setValidationError(getValidationErrorMessage(validationErrors))
|
||||
return false
|
||||
}
|
||||
}
|
||||
setValidationError('')
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
setValidationError('')
|
||||
if (error instanceof Error) {
|
||||
setParseError(error)
|
||||
return false
|
||||
}
|
||||
else {
|
||||
setParseError(new Error('Invalid JSON'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
const result = validateInspectJsonValue(value, type)
|
||||
setParseError(result.parseError)
|
||||
setValidationError(result.validationError)
|
||||
return result.success
|
||||
}
|
||||
|
||||
const handleEditorChange = (value: string) => {
|
||||
@ -151,13 +102,11 @@ const ValueContent = ({
|
||||
}
|
||||
}
|
||||
|
||||
const fileValueValidate = (fileList: any[]) => fileList.every(file => file.upload_file_id)
|
||||
|
||||
const handleFileChange = (value: any[]) => {
|
||||
setFileValue(value)
|
||||
// check every file upload progress
|
||||
// invoke update api after every file uploaded
|
||||
if (!fileValueValidate(value))
|
||||
if (!isFileValueUploaded(value))
|
||||
return
|
||||
if (currentVar.value_type === 'file')
|
||||
debounceValueChange(currentVar.id, value[0])
|
||||
@ -189,31 +138,13 @@ const ValueContent = ({
|
||||
>
|
||||
<div className={cn('relative grow')} style={{ height: `${editorHeight}px` }}>
|
||||
{showTextEditor && (
|
||||
<>
|
||||
{isTruncated && <LargeDataAlert className="absolute left-3 right-3 top-1" />}
|
||||
{
|
||||
currentVar.value_type === 'string'
|
||||
? (
|
||||
<DisplayContent
|
||||
previewType={PreviewType.Markdown}
|
||||
varType={currentVar.value_type}
|
||||
mdString={value as any}
|
||||
readonly={textEditorDisabled}
|
||||
handleTextChange={handleTextChange}
|
||||
className={cn(isTruncated && 'pt-[36px]')}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Textarea
|
||||
readOnly={textEditorDisabled}
|
||||
disabled={textEditorDisabled || isTruncated}
|
||||
className={cn('h-full', isTruncated && 'pt-[48px]')}
|
||||
value={value as any}
|
||||
onChange={e => handleTextChange(e.target.value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
<TextEditorSection
|
||||
currentVar={currentVar}
|
||||
value={value}
|
||||
textEditorDisabled={textEditorDisabled}
|
||||
isTruncated={isTruncated}
|
||||
onTextChange={handleTextChange}
|
||||
/>
|
||||
)}
|
||||
{showBoolEditor && (
|
||||
<div className="w-[295px]">
|
||||
@ -228,79 +159,41 @@ const ValueContent = ({
|
||||
)}
|
||||
{
|
||||
showBoolArrayEditor && (
|
||||
<div className="w-[295px] space-y-1">
|
||||
{currentVar.value.map((v: boolean, i: number) => (
|
||||
<BoolValue
|
||||
key={i}
|
||||
value={v}
|
||||
onChange={(newValue) => {
|
||||
const newArray = [...(currentVar.value as boolean[])]
|
||||
newArray[i] = newValue
|
||||
setValue(newArray)
|
||||
debounceValueChange(currentVar.id, newArray)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<BoolArraySection
|
||||
values={currentVar.value as boolean[]}
|
||||
onChange={(newArray) => {
|
||||
setValue(newArray)
|
||||
debounceValueChange(currentVar.id, newArray)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{showJSONEditor && (
|
||||
hasChunks
|
||||
? (
|
||||
<DisplayContent
|
||||
previewType={PreviewType.Chunks}
|
||||
varType={currentVar.value_type}
|
||||
schemaType={currentVar.schemaType ?? ''}
|
||||
jsonString={json ?? '{}'}
|
||||
readonly={JSONEditorDisabled}
|
||||
handleEditorChange={handleEditorChange}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<SchemaEditor
|
||||
readonly={JSONEditorDisabled || isTruncated}
|
||||
className="overflow-y-auto"
|
||||
hideTopMenu
|
||||
schema={json}
|
||||
onUpdate={handleEditorChange}
|
||||
isTruncated={isTruncated}
|
||||
/>
|
||||
)
|
||||
<JsonEditorSection
|
||||
hasChunks={hasChunks}
|
||||
schemaType={currentVar.schemaType}
|
||||
valueType={currentVar.value_type}
|
||||
json={json}
|
||||
readonly={JSONEditorDisabled}
|
||||
isTruncated={isTruncated}
|
||||
onChange={handleEditorChange}
|
||||
/>
|
||||
)}
|
||||
{showFileEditor && (
|
||||
<div className="max-w-[460px]">
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={fileValue}
|
||||
onChange={files => handleFileChange(getProcessedFiles(files))}
|
||||
fileConfig={{
|
||||
allowed_file_types: [
|
||||
SupportUploadFileTypes.image,
|
||||
SupportUploadFileTypes.document,
|
||||
SupportUploadFileTypes.audio,
|
||||
SupportUploadFileTypes.video,
|
||||
],
|
||||
allowed_file_extensions: [
|
||||
...FILE_EXTS[SupportUploadFileTypes.image],
|
||||
...FILE_EXTS[SupportUploadFileTypes.document],
|
||||
...FILE_EXTS[SupportUploadFileTypes.audio],
|
||||
...FILE_EXTS[SupportUploadFileTypes.video],
|
||||
],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
number_limits: currentVar.value_type === 'file' ? 1 : fileUploadConfig?.workflow_file_upload_limit || 5,
|
||||
fileUploadConfig,
|
||||
preview_config: {
|
||||
mode: PreviewMode.NewPage,
|
||||
file_type_list: ['application/pdf'],
|
||||
},
|
||||
}}
|
||||
isDisabled={textEditorDisabled}
|
||||
/>
|
||||
</div>
|
||||
<FileEditorSection
|
||||
currentVar={currentVar}
|
||||
fileValue={fileValue}
|
||||
fileUploadConfig={fileUploadConfig}
|
||||
textEditorDisabled={textEditorDisabled}
|
||||
onChange={files => handleFileChange(getProcessedFiles(files))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div ref={errorMessageRef} className="shrink-0">
|
||||
{parseError && <ErrorMessage className="mt-1" message={parseError.message} />}
|
||||
{validationError && <ErrorMessage className="mt-1" message={validationError} />}
|
||||
<ErrorMessages
|
||||
parseError={parseError}
|
||||
validationError={validationError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user