import { useEffect, useRef, useState } from 'react' import { useDebounceFn } from 'ahooks' import Textarea from '@/app/components/base/textarea' import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor' import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader' import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message' import { checkJsonSchemaDepth, getValidationErrorMessage, validateSchemaAgainstDraft7, } from '@/app/components/workflow/nodes/llm/utils' import { validateJSONSchema, } from '@/app/components/workflow/variable-inspect/utils' import { useFeatures } from '@/app/components/base/features/hooks' import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { JSON_SCHEMA_MAX_DEPTH } from '@/config' import { TransferMethod } from '@/types/app' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import { SupportUploadFileTypes } from '@/app/components/workflow/types' import type { VarInInspect } from '@/types/workflow' import { VarInInspectType } from '@/types/workflow' import cn from '@/utils/classnames' type Props = { currentVar: VarInInspect handleValueChange: (varId: string, value: any) => void } const ValueContent = ({ currentVar, handleValueChange, }: Props) => { const contentContainerRef = useRef(null) const errorMessageRef = useRef(null) const [editorHeight, setEditorHeight] = useState(0) const showTextEditor = currentVar.value_type === 'secret' || currentVar.value_type === 'string' || currentVar.value_type === 'number' 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 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() const [json, setJson] = useState('') const [parseError, setParseError] = useState(null) const [validationError, setValidationError] = useState('') const fileFeature = useFeatures(s => s.features.file) const [fileValue, setFileValue] = useState(formatFileValue(currentVar)) const { run: debounceValueChange } = useDebounceFn(handleValueChange, { wait: 500 }) // update default value when id changed useEffect(() => { if (showTextEditor) { if (currentVar.value_type === 'number') return setValue(JSON.stringify(currentVar.value)) if (!currentVar.value) return setValue('') setValue(currentVar.value) } if (showJSONEditor) setJson(currentVar.value ? JSON.stringify(currentVar.value, null, 2) : '') if (showFileEditor) setFileValue(formatFileValue(currentVar)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentVar.id, currentVar.value]) const handleTextChange = (value: string) => { if (currentVar.value_type === 'string') setValue(value) if (currentVar.value_type === 'number') { if (/^-?\d+(\.)?(\d+)?$/.test(value)) setValue(Number.parseFloat(value)) } const newValue = currentVar.value_type === 'number' ? Number.parseFloat(value) : value debounceValueChange(currentVar.id, newValue) } 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 handleEditorChange = (value: string) => { setJson(value) if (jsonValueValidate(value, currentVar.value_type)) { const parsed = JSON.parse(value) debounceValueChange(currentVar.id, parsed) } } 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)) return if (currentVar.value_type === 'file') debounceValueChange(currentVar.id, value[0]) if (currentVar.value_type === 'array[file]' || isSysFiles) debounceValueChange(currentVar.id, value) } // get editor height useEffect(() => { if (contentContainerRef.current && errorMessageRef.current) { const errorMessageObserver = new ResizeObserver((entries) => { for (const entry of entries) { const { inlineSize } = entry.borderBoxSize[0] const height = (contentContainerRef.current as any).clientHeight - inlineSize setEditorHeight(height) } }) errorMessageObserver.observe(errorMessageRef.current) return () => { errorMessageObserver.disconnect() } } }, [setEditorHeight]) return (
{showTextEditor && (