Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

value-content.tsx 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import { useEffect, useRef, useState } from 'react'
  2. import { useDebounceFn } from 'ahooks'
  3. import Textarea from '@/app/components/base/textarea'
  4. import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
  5. import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
  6. import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message'
  7. import {
  8. checkJsonSchemaDepth,
  9. getValidationErrorMessage,
  10. validateSchemaAgainstDraft7,
  11. } from '@/app/components/workflow/nodes/llm/utils'
  12. import {
  13. validateJSONSchema,
  14. } from '@/app/components/workflow/variable-inspect/utils'
  15. import { useFeatures } from '@/app/components/base/features/hooks'
  16. import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
  17. import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
  18. import { TransferMethod } from '@/types/app'
  19. import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
  20. import { SupportUploadFileTypes } from '@/app/components/workflow/types'
  21. import type { VarInInspect } from '@/types/workflow'
  22. import { VarInInspectType } from '@/types/workflow'
  23. import cn from '@/utils/classnames'
  24. type Props = {
  25. currentVar: VarInInspect
  26. handleValueChange: (varId: string, value: any) => void
  27. }
  28. const ValueContent = ({
  29. currentVar,
  30. handleValueChange,
  31. }: Props) => {
  32. const contentContainerRef = useRef<HTMLDivElement>(null)
  33. const errorMessageRef = useRef<HTMLDivElement>(null)
  34. const [editorHeight, setEditorHeight] = useState(0)
  35. const showTextEditor = currentVar.value_type === 'secret' || currentVar.value_type === 'string' || currentVar.value_type === 'number'
  36. const isSysFiles = currentVar.type === VarInInspectType.system && currentVar.name === 'files'
  37. 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]')
  38. const showFileEditor = isSysFiles || currentVar.value_type === 'file' || currentVar.value_type === 'array[file]'
  39. const textEditorDisabled = currentVar.type === VarInInspectType.environment || (currentVar.type === VarInInspectType.system && currentVar.name !== 'query' && currentVar.name !== 'files')
  40. const JSONEditorDisabled = currentVar.value_type === 'array[any]'
  41. const formatFileValue = (value: VarInInspect) => {
  42. if (value.value_type === 'file')
  43. return value.value ? getProcessedFilesFromResponse([value.value]) : []
  44. if (value.value_type === 'array[file]' || (value.type === VarInInspectType.system && currentVar.name === 'files'))
  45. return value.value && value.value.length > 0 ? getProcessedFilesFromResponse(value.value) : []
  46. return []
  47. }
  48. const [value, setValue] = useState<any>()
  49. const [json, setJson] = useState('')
  50. const [parseError, setParseError] = useState<Error | null>(null)
  51. const [validationError, setValidationError] = useState<string>('')
  52. const fileFeature = useFeatures(s => s.features.file)
  53. const [fileValue, setFileValue] = useState<any>(formatFileValue(currentVar))
  54. const { run: debounceValueChange } = useDebounceFn(handleValueChange, { wait: 500 })
  55. // update default value when id changed
  56. useEffect(() => {
  57. if (showTextEditor) {
  58. if (currentVar.value_type === 'number')
  59. return setValue(JSON.stringify(currentVar.value))
  60. if (!currentVar.value)
  61. return setValue('')
  62. setValue(currentVar.value)
  63. }
  64. if (showJSONEditor)
  65. setJson(currentVar.value ? JSON.stringify(currentVar.value, null, 2) : '')
  66. if (showFileEditor)
  67. setFileValue(formatFileValue(currentVar))
  68. // eslint-disable-next-line react-hooks/exhaustive-deps
  69. }, [currentVar.id, currentVar.value])
  70. const handleTextChange = (value: string) => {
  71. if (currentVar.value_type === 'string')
  72. setValue(value)
  73. if (currentVar.value_type === 'number') {
  74. if (/^-?\d+(\.)?(\d+)?$/.test(value))
  75. setValue(Number.parseFloat(value))
  76. }
  77. const newValue = currentVar.value_type === 'number' ? Number.parseFloat(value) : value
  78. debounceValueChange(currentVar.id, newValue)
  79. }
  80. const jsonValueValidate = (value: string, type: string) => {
  81. try {
  82. const newJSONSchema = JSON.parse(value)
  83. setParseError(null)
  84. const result = validateJSONSchema(newJSONSchema, type)
  85. if (!result.success) {
  86. setValidationError(result.error.message)
  87. return false
  88. }
  89. if (type === 'object' || type === 'array[object]') {
  90. const schemaDepth = checkJsonSchemaDepth(newJSONSchema)
  91. if (schemaDepth > JSON_SCHEMA_MAX_DEPTH) {
  92. setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
  93. return false
  94. }
  95. const validationErrors = validateSchemaAgainstDraft7(newJSONSchema)
  96. if (validationErrors.length > 0) {
  97. setValidationError(getValidationErrorMessage(validationErrors))
  98. return false
  99. }
  100. }
  101. setValidationError('')
  102. return true
  103. }
  104. catch (error) {
  105. setValidationError('')
  106. if (error instanceof Error) {
  107. setParseError(error)
  108. return false
  109. }
  110. else {
  111. setParseError(new Error('Invalid JSON'))
  112. return false
  113. }
  114. }
  115. }
  116. const handleEditorChange = (value: string) => {
  117. setJson(value)
  118. if (jsonValueValidate(value, currentVar.value_type)) {
  119. const parsed = JSON.parse(value)
  120. debounceValueChange(currentVar.id, parsed)
  121. }
  122. }
  123. const fileValueValidate = (fileList: any[]) => fileList.every(file => file.upload_file_id)
  124. const handleFileChange = (value: any[]) => {
  125. setFileValue(value)
  126. // check every file upload progress
  127. // invoke update api after every file uploaded
  128. if (!fileValueValidate(value))
  129. return
  130. if (currentVar.value_type === 'file')
  131. debounceValueChange(currentVar.id, value[0])
  132. if (currentVar.value_type === 'array[file]' || isSysFiles)
  133. debounceValueChange(currentVar.id, value)
  134. }
  135. // get editor height
  136. useEffect(() => {
  137. if (contentContainerRef.current && errorMessageRef.current) {
  138. const errorMessageObserver = new ResizeObserver((entries) => {
  139. for (const entry of entries) {
  140. const { inlineSize } = entry.borderBoxSize[0]
  141. const height = (contentContainerRef.current as any).clientHeight - inlineSize
  142. setEditorHeight(height)
  143. }
  144. })
  145. errorMessageObserver.observe(errorMessageRef.current)
  146. return () => {
  147. errorMessageObserver.disconnect()
  148. }
  149. }
  150. }, [setEditorHeight])
  151. return (
  152. <div
  153. ref={contentContainerRef}
  154. className='flex h-full flex-col'
  155. >
  156. <div className={cn('grow')} style={{ height: `${editorHeight}px` }}>
  157. {showTextEditor && (
  158. <Textarea
  159. readOnly={textEditorDisabled}
  160. disabled={textEditorDisabled}
  161. className='h-full'
  162. value={value as any}
  163. onChange={e => handleTextChange(e.target.value)}
  164. />
  165. )}
  166. {showJSONEditor && (
  167. <SchemaEditor
  168. readonly={JSONEditorDisabled}
  169. className='overflow-y-auto'
  170. hideTopMenu
  171. schema={json}
  172. onUpdate={handleEditorChange}
  173. />
  174. )}
  175. {showFileEditor && (
  176. <div className='max-w-[460px]'>
  177. <FileUploaderInAttachmentWrapper
  178. value={fileValue}
  179. onChange={files => handleFileChange(getProcessedFiles(files))}
  180. fileConfig={{
  181. allowed_file_types: [
  182. SupportUploadFileTypes.image,
  183. SupportUploadFileTypes.document,
  184. SupportUploadFileTypes.audio,
  185. SupportUploadFileTypes.video,
  186. ],
  187. allowed_file_extensions: [
  188. ...FILE_EXTS[SupportUploadFileTypes.image],
  189. ...FILE_EXTS[SupportUploadFileTypes.document],
  190. ...FILE_EXTS[SupportUploadFileTypes.audio],
  191. ...FILE_EXTS[SupportUploadFileTypes.video],
  192. ],
  193. allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
  194. number_limits: currentVar.value_type === 'file' ? 1 : (fileFeature as any).fileUploadConfig?.workflow_file_upload_limit || 5,
  195. fileUploadConfig: (fileFeature as any).fileUploadConfig,
  196. }}
  197. isDisabled={textEditorDisabled}
  198. />
  199. </div>
  200. )}
  201. </div>
  202. <div ref={errorMessageRef} className='shrink-0'>
  203. {parseError && <ErrorMessage className='mt-1' message={parseError.message} />}
  204. {validationError && <ErrorMessage className='mt-1' message={validationError} />}
  205. </div>
  206. </div>
  207. )
  208. }
  209. export default ValueContent