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 12KB

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