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.

index.tsx 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import type { ChangeEvent, FC, FormEvent } from 'react'
  2. import { useEffect, useState } from 'react'
  3. import React, { useCallback } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import {
  6. RiPlayLargeLine,
  7. } from '@remixicon/react'
  8. import Select from '@/app/components/base/select'
  9. import type { SiteInfo } from '@/models/share'
  10. import type { PromptConfig } from '@/models/debug'
  11. import Button from '@/app/components/base/button'
  12. import Textarea from '@/app/components/base/textarea'
  13. import Input from '@/app/components/base/input'
  14. import { DEFAULT_VALUE_MAX_LEN } from '@/config'
  15. import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
  16. import type { VisionFile, VisionSettings } from '@/types/app'
  17. import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
  18. import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
  19. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  20. import cn from '@/utils/classnames'
  21. import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
  22. import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
  23. import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
  24. export type IRunOnceProps = {
  25. siteInfo: SiteInfo
  26. promptConfig: PromptConfig
  27. inputs: Record<string, any>
  28. inputsRef: React.MutableRefObject<Record<string, any>>
  29. onInputsChange: (inputs: Record<string, any>) => void
  30. onSend: () => void
  31. visionConfig: VisionSettings
  32. onVisionFilesChange: (files: VisionFile[]) => void
  33. }
  34. const RunOnce: FC<IRunOnceProps> = ({
  35. promptConfig,
  36. inputs,
  37. inputsRef,
  38. onInputsChange,
  39. onSend,
  40. visionConfig,
  41. onVisionFilesChange,
  42. }) => {
  43. const { t } = useTranslation()
  44. const media = useBreakpoints()
  45. const isPC = media === MediaType.pc
  46. const [isInitialized, setIsInitialized] = useState(false)
  47. const onClear = () => {
  48. const newInputs: Record<string, any> = {}
  49. promptConfig.prompt_variables.forEach((item) => {
  50. if (item.type === 'string' || item.type === 'paragraph')
  51. newInputs[item.key] = ''
  52. else
  53. newInputs[item.key] = undefined
  54. })
  55. onInputsChange(newInputs)
  56. }
  57. const onSubmit = (e: FormEvent<HTMLFormElement>) => {
  58. e.preventDefault()
  59. onSend()
  60. }
  61. const handleInputsChange = useCallback((newInputs: Record<string, any>) => {
  62. onInputsChange(newInputs)
  63. inputsRef.current = newInputs
  64. }, [onInputsChange, inputsRef])
  65. useEffect(() => {
  66. if (isInitialized) return
  67. const newInputs: Record<string, any> = {}
  68. promptConfig.prompt_variables.forEach((item) => {
  69. if (item.type === 'select')
  70. newInputs[item.key] = item.default
  71. else if (item.type === 'string' || item.type === 'paragraph')
  72. newInputs[item.key] = item.default || ''
  73. else if (item.type === 'number')
  74. newInputs[item.key] = item.default
  75. else if (item.type === 'file')
  76. newInputs[item.key] = item.default
  77. else if (item.type === 'file-list')
  78. newInputs[item.key] = item.default || []
  79. else
  80. newInputs[item.key] = undefined
  81. })
  82. onInputsChange(newInputs)
  83. setIsInitialized(true)
  84. }, [promptConfig.prompt_variables, onInputsChange])
  85. return (
  86. <div className="">
  87. <section>
  88. {/* input form */}
  89. <form onSubmit={onSubmit}>
  90. {(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized ? null
  91. : promptConfig.prompt_variables.map(item => (
  92. <div className='mt-4 w-full' key={item.key}>
  93. {item.type !== 'boolean' && (
  94. <label className='system-md-semibold flex h-6 items-center text-text-secondary'>{item.name}</label>
  95. )}
  96. <div className='mt-1'>
  97. {item.type === 'select' && (
  98. <Select
  99. className='w-full'
  100. defaultValue={inputs[item.key]}
  101. onSelect={(i) => { handleInputsChange({ ...inputsRef.current, [item.key]: i.value }) }}
  102. items={(item.options || []).map(i => ({ name: i, value: i }))}
  103. allowSearch={false}
  104. />
  105. )}
  106. {item.type === 'string' && (
  107. <Input
  108. type="text"
  109. placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  110. value={inputs[item.key]}
  111. onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
  112. maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
  113. />
  114. )}
  115. {item.type === 'paragraph' && (
  116. <Textarea
  117. className='h-[104px] sm:text-xs'
  118. placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  119. value={inputs[item.key]}
  120. onChange={(e: ChangeEvent<HTMLTextAreaElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
  121. />
  122. )}
  123. {item.type === 'number' && (
  124. <Input
  125. type="number"
  126. placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  127. value={inputs[item.key]}
  128. onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
  129. />
  130. )}
  131. {item.type === 'boolean' && (
  132. <BoolInput
  133. name={item.name || item.key}
  134. value={!!inputs[item.key]}
  135. required={item.required}
  136. onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
  137. />
  138. )}
  139. {item.type === 'file' && (
  140. <FileUploaderInAttachmentWrapper
  141. value={inputs[item.key] ? [inputs[item.key]] : []}
  142. onChange={(files) => { handleInputsChange({ ...inputsRef.current, [item.key]: getProcessedFiles(files)[0] }) }}
  143. fileConfig={{
  144. ...item.config,
  145. fileUploadConfig: (visionConfig as any).fileUploadConfig,
  146. }}
  147. />
  148. )}
  149. {item.type === 'file-list' && (
  150. <FileUploaderInAttachmentWrapper
  151. value={inputs[item.key]}
  152. onChange={(files) => { handleInputsChange({ ...inputsRef.current, [item.key]: getProcessedFiles(files) }) }}
  153. fileConfig={{
  154. ...item.config,
  155. fileUploadConfig: (visionConfig as any).fileUploadConfig,
  156. }}
  157. />
  158. )}
  159. {item.type === 'json_object' && (
  160. <CodeEditor
  161. language={CodeLanguage.json}
  162. value={inputs[item.key]}
  163. onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
  164. noWrapper
  165. className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
  166. placeholder={
  167. <div className='whitespace-pre'>{item.json_schema}</div>
  168. }
  169. />
  170. )}
  171. </div>
  172. </div>
  173. ))}
  174. {
  175. visionConfig?.enabled && (
  176. <div className="mt-4 w-full">
  177. <div className="system-md-semibold flex h-6 items-center text-text-secondary">{t('common.imageUploader.imageUpload')}</div>
  178. <div className='mt-1'>
  179. <TextGenerationImageUploader
  180. settings={visionConfig}
  181. onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
  182. type: 'image',
  183. transfer_method: fileItem.type,
  184. url: fileItem.url,
  185. upload_file_id: fileItem.fileId,
  186. })))}
  187. />
  188. </div>
  189. </div>
  190. )
  191. }
  192. <div className='mb-3 mt-6 w-full'>
  193. <div className="flex items-center justify-between gap-2">
  194. <Button
  195. onClick={onClear}
  196. disabled={false}
  197. >
  198. <span className='text-[13px]'>{t('common.operation.clear')}</span>
  199. </Button>
  200. <Button
  201. className={cn(!isPC && 'grow')}
  202. type='submit'
  203. variant="primary"
  204. disabled={false}
  205. >
  206. <RiPlayLargeLine className="mr-1 h-4 w-4 shrink-0" aria-hidden="true" />
  207. <span className='text-[13px]'>{t('share.generation.run')}</span>
  208. </Button>
  209. </div>
  210. </div>
  211. </form>
  212. </section>
  213. </div>
  214. )
  215. }
  216. export default React.memo(RunOnce)