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.

Form.tsx 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. import { useCallback, useState } from 'react'
  2. import type { ReactNode } from 'react'
  3. import { ValidatingTip } from '../../key-validator/ValidateStatus'
  4. import type {
  5. CredentialFormSchema,
  6. CredentialFormSchemaNumberInput,
  7. CredentialFormSchemaRadio,
  8. CredentialFormSchemaSecretInput,
  9. CredentialFormSchemaSelect,
  10. CredentialFormSchemaTextInput,
  11. FormValue,
  12. } from '../declarations'
  13. import { FormTypeEnum } from '../declarations'
  14. import { useLanguage } from '../hooks'
  15. import Input from './Input'
  16. import cn from '@/utils/classnames'
  17. import { SimpleSelect } from '@/app/components/base/select'
  18. import Tooltip from '@/app/components/base/tooltip'
  19. import Radio from '@/app/components/base/radio'
  20. import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
  21. import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
  22. import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
  23. import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
  24. import RadioE from '@/app/components/base/radio/ui'
  25. import type {
  26. NodeOutPutVar,
  27. } from '@/app/components/workflow/types'
  28. import type { Node } from 'reactflow'
  29. type FormProps<
  30. CustomFormSchema extends Omit<CredentialFormSchema, 'type'> & { type: string } = never,
  31. > = {
  32. className?: string
  33. itemClassName?: string
  34. fieldLabelClassName?: string
  35. value: FormValue
  36. onChange: (val: FormValue) => void
  37. formSchemas: Array<CredentialFormSchema | CustomFormSchema>
  38. validating: boolean
  39. validatedSuccess?: boolean
  40. showOnVariableMap: Record<string, string[]>
  41. isEditMode: boolean
  42. isAgentStrategy?: boolean
  43. readonly?: boolean
  44. inputClassName?: string
  45. isShowDefaultValue?: boolean
  46. fieldMoreInfo?: (payload: CredentialFormSchema | CustomFormSchema) => ReactNode
  47. customRenderField?: (
  48. formSchema: CustomFormSchema,
  49. props: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'>
  50. ) => ReactNode
  51. // If return falsy value, this field will fallback to default render
  52. override?: [Array<FormTypeEnum>, (formSchema: CredentialFormSchema, props: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'>) => ReactNode]
  53. nodeId?: string
  54. nodeOutputVars?: NodeOutPutVar[],
  55. availableNodes?: Node[],
  56. }
  57. function Form<
  58. CustomFormSchema extends Omit<CredentialFormSchema, 'type'> & { type: string } = never,
  59. >({
  60. className,
  61. itemClassName,
  62. fieldLabelClassName,
  63. value,
  64. onChange,
  65. formSchemas,
  66. validating,
  67. validatedSuccess,
  68. showOnVariableMap,
  69. isEditMode,
  70. isAgentStrategy = false,
  71. readonly,
  72. inputClassName,
  73. isShowDefaultValue = false,
  74. fieldMoreInfo,
  75. customRenderField,
  76. override,
  77. nodeId,
  78. nodeOutputVars,
  79. availableNodes,
  80. }: FormProps<CustomFormSchema>) {
  81. const language = useLanguage()
  82. const [changeKey, setChangeKey] = useState('')
  83. const filteredProps: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'> = {
  84. className,
  85. itemClassName,
  86. fieldLabelClassName,
  87. value,
  88. onChange,
  89. formSchemas,
  90. validating,
  91. validatedSuccess,
  92. showOnVariableMap,
  93. isEditMode,
  94. readonly,
  95. inputClassName,
  96. isShowDefaultValue,
  97. fieldMoreInfo,
  98. }
  99. const handleFormChange = (key: string, val: string | boolean) => {
  100. if (isEditMode && (key === '__model_type' || key === '__model_name'))
  101. return
  102. setChangeKey(key)
  103. const shouldClearVariable: Record<string, string | undefined> = {}
  104. if (showOnVariableMap[key]?.length) {
  105. showOnVariableMap[key].forEach((clearVariable) => {
  106. const schema = formSchemas.find(it => it.variable === clearVariable)
  107. shouldClearVariable[clearVariable] = schema ? schema.default : undefined
  108. })
  109. }
  110. onChange({ ...value, [key]: val, ...shouldClearVariable })
  111. }
  112. const handleModelChanged = useCallback((key: string, model: any) => {
  113. const newValue = {
  114. ...value[key],
  115. ...model,
  116. type: FormTypeEnum.modelSelector,
  117. }
  118. onChange({ ...value, [key]: newValue })
  119. }, [onChange, value])
  120. const renderField = (formSchema: CredentialFormSchema | CustomFormSchema) => {
  121. const tooltip = formSchema.tooltip
  122. const tooltipContent = (tooltip && (
  123. <Tooltip
  124. popupContent={<div className='w-[200px]'>
  125. {tooltip[language] || tooltip.en_US}
  126. </div>}
  127. triggerClassName='ml-1 w-4 h-4'
  128. asChild={false} />
  129. ))
  130. if (override) {
  131. const [overrideTypes, overrideRender] = override
  132. if (overrideTypes.includes(formSchema.type as FormTypeEnum)) {
  133. const node = overrideRender(formSchema as CredentialFormSchema, filteredProps)
  134. if (node)
  135. return node
  136. }
  137. }
  138. if (formSchema.type === FormTypeEnum.textInput || formSchema.type === FormTypeEnum.secretInput || formSchema.type === FormTypeEnum.textNumber) {
  139. const {
  140. variable, label, placeholder, required, show_on,
  141. } = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
  142. if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
  143. return null
  144. const disabled = readonly || (isEditMode && (variable === '__model_type' || variable === '__model_name'))
  145. return (
  146. <div key={variable} className={cn(itemClassName, 'py-3')}>
  147. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  148. {label[language] || label.en_US}
  149. {required && (
  150. <span className='ml-1 text-red-500'>*</span>
  151. )}
  152. {tooltipContent}
  153. </div>
  154. <Input
  155. className={cn(inputClassName, `${disabled && 'cursor-not-allowed opacity-60'}`)}
  156. value={(isShowDefaultValue && ((value[variable] as string) === '' || value[variable] === undefined || value[variable] === null)) ? formSchema.default : value[variable]}
  157. onChange={val => handleFormChange(variable, val)}
  158. validated={validatedSuccess}
  159. placeholder={placeholder?.[language] || placeholder?.en_US}
  160. disabled={disabled}
  161. type={formSchema.type === FormTypeEnum.secretInput ? 'password'
  162. : formSchema.type === FormTypeEnum.textNumber ? 'number'
  163. : 'text'}
  164. {...(formSchema.type === FormTypeEnum.textNumber ? { min: (formSchema as CredentialFormSchemaNumberInput).min, max: (formSchema as CredentialFormSchemaNumberInput).max } : {})} />
  165. {fieldMoreInfo?.(formSchema)}
  166. {validating && changeKey === variable && <ValidatingTip />}
  167. </div>
  168. )
  169. }
  170. if (formSchema.type === FormTypeEnum.radio) {
  171. const {
  172. options, variable, label, show_on, required,
  173. } = formSchema as CredentialFormSchemaRadio
  174. if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
  175. return null
  176. const disabled = isEditMode && (variable === '__model_type' || variable === '__model_name')
  177. return (
  178. <div key={variable} className={cn(itemClassName, 'py-3')}>
  179. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  180. {label[language] || label.en_US}
  181. {required && (
  182. <span className='ml-1 text-red-500'>*</span>
  183. )}
  184. {tooltipContent}
  185. </div>
  186. <div className={cn('grid gap-3', `grid-cols-${options?.length}`)}>
  187. {options.filter((option) => {
  188. if (option.show_on.length)
  189. return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
  190. return true
  191. }).map(option => (
  192. <div
  193. className={`
  194. flex cursor-pointer items-center gap-2 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg px-3 py-2
  195. ${value[variable] === option.value && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-sm'}
  196. ${disabled && '!cursor-not-allowed opacity-60'}
  197. `}
  198. onClick={() => handleFormChange(variable, option.value)}
  199. key={`${variable}-${option.value}`}
  200. >
  201. <RadioE isChecked={value[variable] === option.value} />
  202. <div className='system-sm-regular text-text-secondary'>{option.label[language] || option.label.en_US}</div>
  203. </div>
  204. ))}
  205. </div>
  206. {fieldMoreInfo?.(formSchema)}
  207. {validating && changeKey === variable && <ValidatingTip />}
  208. </div>
  209. )
  210. }
  211. if (formSchema.type === FormTypeEnum.select) {
  212. const {
  213. options, variable, label, show_on, required, placeholder,
  214. } = formSchema as CredentialFormSchemaSelect
  215. if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
  216. return null
  217. return (
  218. <div key={variable} className={cn(itemClassName, 'py-3')}>
  219. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  220. {label[language] || label.en_US}
  221. {required && (
  222. <span className='ml-1 text-red-500'>*</span>
  223. )}
  224. {tooltipContent}
  225. </div>
  226. <SimpleSelect
  227. wrapperClassName='h-8'
  228. className={cn(inputClassName)}
  229. disabled={readonly}
  230. defaultValue={(isShowDefaultValue && ((value[variable] as string) === '' || value[variable] === undefined || value[variable] === null)) ? formSchema.default : value[variable]}
  231. items={options.filter((option) => {
  232. if (option.show_on.length)
  233. return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
  234. return true
  235. }).map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
  236. onSelect={item => handleFormChange(variable, item.value as string)}
  237. placeholder={placeholder?.[language] || placeholder?.en_US} />
  238. {fieldMoreInfo?.(formSchema)}
  239. {validating && changeKey === variable && <ValidatingTip />}
  240. </div>
  241. )
  242. }
  243. if (formSchema.type === FormTypeEnum.boolean) {
  244. const {
  245. variable, label, show_on, required,
  246. } = formSchema as CredentialFormSchemaRadio
  247. if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
  248. return null
  249. return (
  250. <div key={variable} className={cn(itemClassName, 'py-3')}>
  251. <div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
  252. <div className='flex items-center space-x-2'>
  253. <span className={cn(fieldLabelClassName, 'system-sm-regular flex items-center py-2 text-text-secondary')}>{label[language] || label.en_US}</span>
  254. {required && (
  255. <span className='ml-1 text-red-500'>*</span>
  256. )}
  257. {tooltipContent}
  258. </div>
  259. <Radio.Group
  260. className='flex items-center'
  261. value={value[variable] === null ? undefined : (value[variable] ? 1 : 0)}
  262. onChange={val => handleFormChange(variable, val === 1)}
  263. >
  264. <Radio value={1} className='!mr-1'>True</Radio>
  265. <Radio value={0}>False</Radio>
  266. </Radio.Group>
  267. </div>
  268. {fieldMoreInfo?.(formSchema)}
  269. </div>
  270. )
  271. }
  272. if (formSchema.type === FormTypeEnum.modelSelector) {
  273. const {
  274. variable, label, required, scope,
  275. } = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
  276. return (
  277. <div key={variable} className={cn(itemClassName, 'py-3')}>
  278. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  279. {label[language] || label.en_US}
  280. {required && (
  281. <span className='ml-1 text-red-500'>*</span>
  282. )}
  283. {tooltipContent}
  284. </div>
  285. <ModelParameterModal
  286. popupClassName='!w-[387px]'
  287. isAdvancedMode
  288. isInWorkflow
  289. isAgentStrategy={isAgentStrategy}
  290. value={value[variable]}
  291. setModel={model => handleModelChanged(variable, model)}
  292. readonly={readonly}
  293. scope={scope} />
  294. {fieldMoreInfo?.(formSchema)}
  295. {validating && changeKey === variable && <ValidatingTip />}
  296. </div>
  297. )
  298. }
  299. if (formSchema.type === FormTypeEnum.toolSelector) {
  300. const {
  301. variable,
  302. label,
  303. required,
  304. scope,
  305. } = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
  306. return (
  307. <div key={variable} className={cn(itemClassName, 'py-3')}>
  308. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  309. {label[language] || label.en_US}
  310. {required && (
  311. <span className='ml-1 text-red-500'>*</span>
  312. )}
  313. {tooltipContent}
  314. </div>
  315. <ToolSelector
  316. scope={scope}
  317. nodeId={nodeId}
  318. nodeOutputVars={nodeOutputVars || []}
  319. availableNodes={availableNodes || []}
  320. disabled={readonly}
  321. value={value[variable]}
  322. // selectedTools={value[variable] ? [value[variable]] : []}
  323. onSelect={item => handleFormChange(variable, item as any)}
  324. onDelete={() => handleFormChange(variable, null as any)}
  325. />
  326. {fieldMoreInfo?.(formSchema)}
  327. {validating && changeKey === variable && <ValidatingTip />}
  328. </div>
  329. )
  330. }
  331. if (formSchema.type === FormTypeEnum.multiToolSelector) {
  332. const {
  333. variable,
  334. label,
  335. tooltip,
  336. required,
  337. scope,
  338. } = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
  339. return (
  340. <div key={variable} className={cn(itemClassName, 'py-3')}>
  341. <MultipleToolSelector
  342. disabled={readonly}
  343. nodeId={nodeId}
  344. nodeOutputVars={nodeOutputVars || []}
  345. availableNodes={availableNodes || []}
  346. scope={scope}
  347. label={label[language] || label.en_US}
  348. required={required}
  349. tooltip={tooltip?.[language] || tooltip?.en_US}
  350. value={value[variable] || []}
  351. onChange={item => handleFormChange(variable, item as any)}
  352. supportCollapse
  353. />
  354. {fieldMoreInfo?.(formSchema)}
  355. {validating && changeKey === variable && <ValidatingTip />}
  356. </div>
  357. )
  358. }
  359. if (formSchema.type === FormTypeEnum.appSelector) {
  360. const {
  361. variable, label, required, scope,
  362. } = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
  363. return (
  364. <div key={variable} className={cn(itemClassName, 'py-3')}>
  365. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  366. {label[language] || label.en_US}
  367. {required && (
  368. <span className='ml-1 text-red-500'>*</span>
  369. )}
  370. {tooltipContent}
  371. </div>
  372. <AppSelector
  373. disabled={readonly}
  374. scope={scope}
  375. value={value[variable]}
  376. onSelect={item => handleFormChange(variable, { ...item, type: FormTypeEnum.appSelector } as any)} />
  377. {fieldMoreInfo?.(formSchema)}
  378. {validating && changeKey === variable && <ValidatingTip />}
  379. </div>
  380. )
  381. }
  382. // @ts-expect-error it work
  383. if (!Object.values(FormTypeEnum).includes(formSchema.type))
  384. return customRenderField?.(formSchema as CustomFormSchema, filteredProps)
  385. }
  386. return (
  387. <div className={className}>
  388. {formSchemas.map(formSchema => renderField(formSchema))}
  389. </div>
  390. )
  391. }
  392. export default Form