Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

Form.tsx 16KB

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