Form.tsx 17 KB

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