Form.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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.textNumber ? 'number' : 'text'}
  162. {...(formSchema.type === FormTypeEnum.textNumber ? { min: (formSchema as CredentialFormSchemaNumberInput).min, max: (formSchema as CredentialFormSchemaNumberInput).max } : {})} />
  163. {fieldMoreInfo?.(formSchema)}
  164. {validating && changeKey === variable && <ValidatingTip />}
  165. </div>
  166. )
  167. }
  168. if (formSchema.type === FormTypeEnum.radio) {
  169. const {
  170. options, variable, label, show_on, required,
  171. } = formSchema as CredentialFormSchemaRadio
  172. if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
  173. return null
  174. const disabled = isEditMode && (variable === '__model_type' || variable === '__model_name')
  175. return (
  176. <div key={variable} className={cn(itemClassName, 'py-3')}>
  177. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  178. {label[language] || label.en_US}
  179. {required && (
  180. <span className='ml-1 text-red-500'>*</span>
  181. )}
  182. {tooltipContent}
  183. </div>
  184. <div className={cn('grid gap-3', `grid-cols-${options?.length}`)}>
  185. {options.filter((option) => {
  186. if (option.show_on.length)
  187. return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
  188. return true
  189. }).map(option => (
  190. <div
  191. className={`
  192. 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
  193. ${value[variable] === option.value && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-sm'}
  194. ${disabled && '!cursor-not-allowed opacity-60'}
  195. `}
  196. onClick={() => handleFormChange(variable, option.value)}
  197. key={`${variable}-${option.value}`}
  198. >
  199. <RadioE isChecked={value[variable] === option.value} />
  200. <div className='system-sm-regular text-text-secondary'>{option.label[language] || option.label.en_US}</div>
  201. </div>
  202. ))}
  203. </div>
  204. {fieldMoreInfo?.(formSchema)}
  205. {validating && changeKey === variable && <ValidatingTip />}
  206. </div>
  207. )
  208. }
  209. if (formSchema.type === FormTypeEnum.select) {
  210. const {
  211. options, variable, label, show_on, required, placeholder,
  212. } = formSchema as CredentialFormSchemaSelect
  213. if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
  214. return null
  215. return (
  216. <div key={variable} className={cn(itemClassName, 'py-3')}>
  217. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  218. {label[language] || label.en_US}
  219. {required && (
  220. <span className='ml-1 text-red-500'>*</span>
  221. )}
  222. {tooltipContent}
  223. </div>
  224. <SimpleSelect
  225. wrapperClassName='h-8'
  226. className={cn(inputClassName)}
  227. disabled={readonly}
  228. defaultValue={(isShowDefaultValue && ((value[variable] as string) === '' || value[variable] === undefined || value[variable] === null)) ? formSchema.default : value[variable]}
  229. items={options.filter((option) => {
  230. if (option.show_on.length)
  231. return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
  232. return true
  233. }).map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
  234. onSelect={item => handleFormChange(variable, item.value as string)}
  235. placeholder={placeholder?.[language] || placeholder?.en_US} />
  236. {fieldMoreInfo?.(formSchema)}
  237. {validating && changeKey === variable && <ValidatingTip />}
  238. </div>
  239. )
  240. }
  241. if (formSchema.type === FormTypeEnum.boolean) {
  242. const {
  243. variable, label, show_on, required,
  244. } = formSchema as CredentialFormSchemaRadio
  245. if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
  246. return null
  247. return (
  248. <div key={variable} className={cn(itemClassName, 'py-3')}>
  249. <div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
  250. <div className='flex items-center space-x-2'>
  251. <span className={cn(fieldLabelClassName, 'system-sm-regular flex items-center py-2 text-text-secondary')}>{label[language] || label.en_US}</span>
  252. {required && (
  253. <span className='ml-1 text-red-500'>*</span>
  254. )}
  255. {tooltipContent}
  256. </div>
  257. <Radio.Group
  258. className='flex items-center'
  259. value={value[variable] === null ? undefined : (value[variable] ? 1 : 0)}
  260. onChange={val => handleFormChange(variable, val === 1)}
  261. >
  262. <Radio value={1} className='!mr-1'>True</Radio>
  263. <Radio value={0}>False</Radio>
  264. </Radio.Group>
  265. </div>
  266. {fieldMoreInfo?.(formSchema)}
  267. </div>
  268. )
  269. }
  270. if (formSchema.type === FormTypeEnum.modelSelector) {
  271. const {
  272. variable, label, required, scope,
  273. } = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
  274. return (
  275. <div key={variable} className={cn(itemClassName, 'py-3')}>
  276. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  277. {label[language] || label.en_US}
  278. {required && (
  279. <span className='ml-1 text-red-500'>*</span>
  280. )}
  281. {tooltipContent}
  282. </div>
  283. <ModelParameterModal
  284. popupClassName='!w-[387px]'
  285. isAdvancedMode
  286. isInWorkflow
  287. isAgentStrategy={isAgentStrategy}
  288. value={value[variable]}
  289. setModel={model => handleModelChanged(variable, model)}
  290. readonly={readonly}
  291. scope={scope} />
  292. {fieldMoreInfo?.(formSchema)}
  293. {validating && changeKey === variable && <ValidatingTip />}
  294. </div>
  295. )
  296. }
  297. if (formSchema.type === FormTypeEnum.toolSelector) {
  298. const {
  299. variable,
  300. label,
  301. required,
  302. scope,
  303. } = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
  304. return (
  305. <div key={variable} className={cn(itemClassName, 'py-3')}>
  306. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  307. {label[language] || label.en_US}
  308. {required && (
  309. <span className='ml-1 text-red-500'>*</span>
  310. )}
  311. {tooltipContent}
  312. </div>
  313. <ToolSelector
  314. scope={scope}
  315. nodeId={nodeId}
  316. nodeOutputVars={nodeOutputVars || []}
  317. availableNodes={availableNodes || []}
  318. disabled={readonly}
  319. value={value[variable]}
  320. // selectedTools={value[variable] ? [value[variable]] : []}
  321. onSelect={item => handleFormChange(variable, item as any)}
  322. onDelete={() => handleFormChange(variable, null as any)}
  323. />
  324. {fieldMoreInfo?.(formSchema)}
  325. {validating && changeKey === variable && <ValidatingTip />}
  326. </div>
  327. )
  328. }
  329. if (formSchema.type === FormTypeEnum.multiToolSelector) {
  330. const {
  331. variable,
  332. label,
  333. tooltip,
  334. required,
  335. scope,
  336. } = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
  337. return (
  338. <div key={variable} className={cn(itemClassName, 'py-3')}>
  339. <MultipleToolSelector
  340. disabled={readonly}
  341. nodeId={nodeId}
  342. nodeOutputVars={nodeOutputVars || []}
  343. availableNodes={availableNodes || []}
  344. scope={scope}
  345. label={label[language] || label.en_US}
  346. required={required}
  347. tooltip={tooltip?.[language] || tooltip?.en_US}
  348. value={value[variable] || []}
  349. onChange={item => handleFormChange(variable, item as any)}
  350. />
  351. {fieldMoreInfo?.(formSchema)}
  352. {validating && changeKey === variable && <ValidatingTip />}
  353. </div>
  354. )
  355. }
  356. if (formSchema.type === FormTypeEnum.appSelector) {
  357. const {
  358. variable, label, required, scope,
  359. } = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
  360. return (
  361. <div key={variable} className={cn(itemClassName, 'py-3')}>
  362. <div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
  363. {label[language] || label.en_US}
  364. {required && (
  365. <span className='ml-1 text-red-500'>*</span>
  366. )}
  367. {tooltipContent}
  368. </div>
  369. <AppSelector
  370. disabled={readonly}
  371. scope={scope}
  372. value={value[variable]}
  373. onSelect={item => handleFormChange(variable, { ...item, type: FormTypeEnum.appSelector } as any)} />
  374. {fieldMoreInfo?.(formSchema)}
  375. {validating && changeKey === variable && <ValidatingTip />}
  376. </div>
  377. )
  378. }
  379. // @ts-expect-error it work
  380. if (!Object.values(FormTypeEnum).includes(formSchema.type))
  381. return customRenderField?.(formSchema as CustomFormSchema, filteredProps)
  382. }
  383. return (
  384. <div className={className}>
  385. {formSchemas.map(formSchema => renderField(formSchema))}
  386. </div>
  387. )
  388. }
  389. export default Form