You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

value-content.tsx 9.9KB

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