dify
This commit is contained in:
@@ -0,0 +1,460 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { ValidatingTip } from '../../key-validator/ValidateStatus'
|
||||
import type {
|
||||
CredentialFormSchema,
|
||||
CredentialFormSchemaNumberInput,
|
||||
CredentialFormSchemaRadio,
|
||||
CredentialFormSchemaSecretInput,
|
||||
CredentialFormSchemaSelect,
|
||||
CredentialFormSchemaTextInput,
|
||||
FormValue,
|
||||
} from '../declarations'
|
||||
import { FormTypeEnum } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import Input from './Input'
|
||||
import cn from '@/utils/classnames'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
|
||||
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
|
||||
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
|
||||
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import RadioE from '@/app/components/base/radio/ui'
|
||||
import type {
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { Node } from 'reactflow'
|
||||
|
||||
type FormProps<
|
||||
CustomFormSchema extends Omit<CredentialFormSchema, 'type'> & { type: string } = never,
|
||||
> = {
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
fieldLabelClassName?: string
|
||||
value: FormValue
|
||||
onChange: (val: FormValue) => void
|
||||
formSchemas: Array<CredentialFormSchema | CustomFormSchema>
|
||||
validating: boolean
|
||||
validatedSuccess?: boolean
|
||||
showOnVariableMap: Record<string, string[]>
|
||||
isEditMode: boolean
|
||||
isAgentStrategy?: boolean
|
||||
readonly?: boolean
|
||||
inputClassName?: string
|
||||
isShowDefaultValue?: boolean
|
||||
fieldMoreInfo?: (payload: CredentialFormSchema | CustomFormSchema) => ReactNode
|
||||
customRenderField?: (
|
||||
formSchema: CustomFormSchema,
|
||||
props: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'>,
|
||||
) => ReactNode,
|
||||
// If return falsy value, this field will fallback to default render
|
||||
override?: [Array<FormTypeEnum>, (formSchema: CredentialFormSchema, props: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'>) => ReactNode]
|
||||
nodeId?: string
|
||||
nodeOutputVars?: NodeOutPutVar[],
|
||||
availableNodes?: Node[],
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
function Form<
|
||||
CustomFormSchema extends Omit<CredentialFormSchema, 'type'> & { type: string } = never,
|
||||
>({
|
||||
className,
|
||||
itemClassName,
|
||||
fieldLabelClassName,
|
||||
value,
|
||||
onChange,
|
||||
formSchemas,
|
||||
validating,
|
||||
validatedSuccess,
|
||||
showOnVariableMap,
|
||||
isEditMode,
|
||||
isAgentStrategy = false,
|
||||
readonly,
|
||||
inputClassName,
|
||||
isShowDefaultValue = false,
|
||||
fieldMoreInfo,
|
||||
customRenderField,
|
||||
override,
|
||||
nodeId,
|
||||
nodeOutputVars,
|
||||
availableNodes,
|
||||
canChooseMCPTool,
|
||||
}: FormProps<CustomFormSchema>) {
|
||||
const language = useLanguage()
|
||||
const [changeKey, setChangeKey] = useState('')
|
||||
const filteredProps: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'> = {
|
||||
className,
|
||||
itemClassName,
|
||||
fieldLabelClassName,
|
||||
value,
|
||||
onChange,
|
||||
formSchemas,
|
||||
validating,
|
||||
validatedSuccess,
|
||||
showOnVariableMap,
|
||||
isEditMode,
|
||||
readonly,
|
||||
inputClassName,
|
||||
isShowDefaultValue,
|
||||
fieldMoreInfo,
|
||||
}
|
||||
|
||||
const handleFormChange = (key: string, val: string | boolean) => {
|
||||
if (isEditMode && (key === '__model_type' || key === '__model_name'))
|
||||
return
|
||||
|
||||
setChangeKey(key)
|
||||
const shouldClearVariable: Record<string, string | undefined> = {}
|
||||
if (showOnVariableMap[key]?.length) {
|
||||
showOnVariableMap[key].forEach((clearVariable) => {
|
||||
const schema = formSchemas.find(it => it.variable === clearVariable)
|
||||
shouldClearVariable[clearVariable] = schema ? schema.default : undefined
|
||||
})
|
||||
}
|
||||
onChange({ ...value, [key]: val, ...shouldClearVariable })
|
||||
}
|
||||
|
||||
const handleModelChanged = useCallback((key: string, model: any) => {
|
||||
const newValue = {
|
||||
...value[key],
|
||||
...model,
|
||||
type: FormTypeEnum.modelSelector,
|
||||
}
|
||||
onChange({ ...value, [key]: newValue })
|
||||
}, [onChange, value])
|
||||
|
||||
const renderField = (formSchema: CredentialFormSchema | CustomFormSchema) => {
|
||||
const tooltip = formSchema.tooltip
|
||||
const tooltipContent = (tooltip && (
|
||||
<Tooltip
|
||||
popupContent={<div className='w-[200px]'>
|
||||
{tooltip[language] || tooltip.en_US}
|
||||
</div>}
|
||||
triggerClassName='ml-1 w-4 h-4'
|
||||
asChild={false} />
|
||||
))
|
||||
if (override) {
|
||||
const [overrideTypes, overrideRender] = override
|
||||
if (overrideTypes.includes(formSchema.type as FormTypeEnum)) {
|
||||
const node = overrideRender(formSchema as CredentialFormSchema, filteredProps)
|
||||
if (node)
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.textInput || formSchema.type === FormTypeEnum.secretInput || formSchema.type === FormTypeEnum.textNumber) {
|
||||
const {
|
||||
variable, label, placeholder, required, show_on,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
|
||||
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return null
|
||||
|
||||
const disabled = readonly || (isEditMode && (variable === '__model_type' || variable === '__model_name'))
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<Input
|
||||
className={cn(inputClassName, `${disabled && 'cursor-not-allowed opacity-60'}`)}
|
||||
value={(isShowDefaultValue && ((value[variable] as string) === '' || value[variable] === undefined || value[variable] === null)) ? formSchema.default : value[variable]}
|
||||
onChange={val => handleFormChange(variable, val)}
|
||||
validated={validatedSuccess}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
disabled={disabled}
|
||||
type={formSchema.type === FormTypeEnum.secretInput ? 'password'
|
||||
: formSchema.type === FormTypeEnum.textNumber ? 'number'
|
||||
: 'text'}
|
||||
{...(formSchema.type === FormTypeEnum.textNumber ? { min: (formSchema as CredentialFormSchemaNumberInput).min, max: (formSchema as CredentialFormSchemaNumberInput).max } : {})} />
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.radio) {
|
||||
const {
|
||||
options, variable, label, show_on, required,
|
||||
} = formSchema as CredentialFormSchemaRadio
|
||||
|
||||
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return null
|
||||
|
||||
const disabled = isEditMode && (variable === '__model_type' || variable === '__model_name')
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<div className={cn('grid gap-3', `grid-cols-${options?.length}`)}>
|
||||
{options.filter((option) => {
|
||||
if (option.show_on.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map(option => (
|
||||
<div
|
||||
className={`
|
||||
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
|
||||
${value[variable] === option.value && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-sm'}
|
||||
${disabled && '!cursor-not-allowed opacity-60'}
|
||||
`}
|
||||
onClick={() => handleFormChange(variable, option.value)}
|
||||
key={`${variable}-${option.value}`}
|
||||
>
|
||||
<RadioE isChecked={value[variable] === option.value} />
|
||||
|
||||
<div className='system-sm-regular text-text-secondary'>{option.label[language] || option.label.en_US}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.select) {
|
||||
const {
|
||||
options, variable, label, show_on, required, placeholder,
|
||||
} = formSchema as CredentialFormSchemaSelect
|
||||
|
||||
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return null
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<SimpleSelect
|
||||
wrapperClassName='h-8'
|
||||
className={cn(inputClassName)}
|
||||
disabled={readonly}
|
||||
defaultValue={(isShowDefaultValue && ((value[variable] as string) === '' || value[variable] === undefined || value[variable] === null)) ? formSchema.default : value[variable]}
|
||||
items={options.filter((option) => {
|
||||
if (option.show_on.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
|
||||
onSelect={item => handleFormChange(variable, item.value as string)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US} />
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.checkbox) {
|
||||
const {
|
||||
variable, label, show_on, required,
|
||||
} = formSchema as CredentialFormSchemaRadio
|
||||
|
||||
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return null
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<span className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>{label[language] || label.en_US}</span>
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<Radio.Group
|
||||
className='flex items-center'
|
||||
value={value[variable]}
|
||||
onChange={val => handleFormChange(variable, val)}
|
||||
>
|
||||
<Radio value={true} className='!mr-1'>True</Radio>
|
||||
<Radio value={false}>False</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.modelSelector) {
|
||||
const {
|
||||
variable, label, required, scope,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<ModelParameterModal
|
||||
popupClassName='!w-[387px]'
|
||||
isAdvancedMode
|
||||
isInWorkflow
|
||||
isAgentStrategy={isAgentStrategy}
|
||||
value={value[variable]}
|
||||
setModel={model => handleModelChanged(variable, model)}
|
||||
readonly={readonly}
|
||||
scope={scope} />
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.toolSelector) {
|
||||
const {
|
||||
variable,
|
||||
label,
|
||||
required,
|
||||
scope,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<ToolSelector
|
||||
scope={scope}
|
||||
nodeId={nodeId}
|
||||
nodeOutputVars={nodeOutputVars || []}
|
||||
availableNodes={availableNodes || []}
|
||||
disabled={readonly}
|
||||
value={value[variable]}
|
||||
// selectedTools={value[variable] ? [value[variable]] : []}
|
||||
onSelect={item => handleFormChange(variable, item as any)}
|
||||
onDelete={() => handleFormChange(variable, null as any)}
|
||||
/>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.multiToolSelector) {
|
||||
const {
|
||||
variable,
|
||||
label,
|
||||
tooltip,
|
||||
required,
|
||||
scope,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<MultipleToolSelector
|
||||
disabled={readonly}
|
||||
nodeId={nodeId}
|
||||
nodeOutputVars={nodeOutputVars || []}
|
||||
availableNodes={availableNodes || []}
|
||||
scope={scope}
|
||||
label={label[language] || label.en_US}
|
||||
required={required}
|
||||
tooltip={tooltip?.[language] || tooltip?.en_US}
|
||||
value={value[variable] || []}
|
||||
onChange={item => handleFormChange(variable, item as any)}
|
||||
supportCollapse
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.appSelector) {
|
||||
const {
|
||||
variable, label, required, scope,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<AppSelector
|
||||
disabled={readonly}
|
||||
scope={scope}
|
||||
value={value[variable]}
|
||||
onSelect={item => handleFormChange(variable, { ...item, type: FormTypeEnum.appSelector } as any)} />
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.any) {
|
||||
const {
|
||||
variable, label, required, scope,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<VarReferencePicker
|
||||
zIndex={1001}
|
||||
readonly={false}
|
||||
isShowNodeName
|
||||
nodeId={nodeId || ''}
|
||||
value={value[variable] || []}
|
||||
onChange={item => handleFormChange(variable, item as any)}
|
||||
filterVar={(varPayload) => {
|
||||
if (!scope) return true
|
||||
return scope.split('&').includes(varPayload.type)
|
||||
}}
|
||||
/>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error it work
|
||||
if (!Object.values(FormTypeEnum).includes(formSchema.type))
|
||||
return customRenderField?.(formSchema as CustomFormSchema, filteredProps)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{formSchemas.map(formSchema => renderField(formSchema))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@@ -0,0 +1,16 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import Input from './Input'
|
||||
|
||||
test('Input renders correctly as password type with no autocomplete', () => {
|
||||
const { asFragment, getByPlaceholderText } = render(
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="API Key"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
const input = getByPlaceholderText('API Key')
|
||||
expect(input).toHaveAttribute('type', 'password')
|
||||
expect(input).not.toHaveAttribute('autocomplete')
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { FC } from 'react'
|
||||
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
|
||||
type InputProps = {
|
||||
value?: string
|
||||
onChange: (v: string) => void
|
||||
onFocus?: () => void
|
||||
placeholder?: string
|
||||
validated?: boolean
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
type?: string
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
|
||||
const Input: FC<InputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
placeholder,
|
||||
validated,
|
||||
className,
|
||||
disabled,
|
||||
type = 'text',
|
||||
min,
|
||||
max,
|
||||
}) => {
|
||||
const toLimit = (v: string) => {
|
||||
const minNum = Number.parseFloat(`${min}`)
|
||||
const maxNum = Number.parseFloat(`${max}`)
|
||||
if (!isNaN(minNum) && Number.parseFloat(v) < minNum) {
|
||||
onChange(`${min}`)
|
||||
return
|
||||
}
|
||||
if (!isNaN(maxNum) && Number.parseFloat(v) > maxNum)
|
||||
onChange(`${max}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<input
|
||||
tabIndex={0}
|
||||
// Do not set autoComplete for security - prevents browser from storing sensitive API keys
|
||||
className={`
|
||||
block h-8 w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-sm
|
||||
text-components-input-text-filled caret-primary-600 outline-none
|
||||
placeholder:text-sm placeholder:text-text-tertiary
|
||||
hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active
|
||||
focus:bg-components-input-bg-active focus:shadow-xs
|
||||
${validated ? 'pr-[30px]' : ''}
|
||||
${className || ''}
|
||||
`}
|
||||
placeholder={placeholder || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onBlur={e => toLimit(e.target.value)}
|
||||
onFocus={onFocus}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
{validated && (
|
||||
<div className='absolute right-2.5 top-2.5'>
|
||||
<CheckCircle className='h-4 w-4 text-[#039855]' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
||||
@@ -0,0 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Input renders correctly as password type with no autocomplete 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="relative"
|
||||
>
|
||||
<input
|
||||
class="
|
||||
block h-8 w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-sm
|
||||
text-components-input-text-filled caret-primary-600 outline-none
|
||||
placeholder:text-sm placeholder:text-text-tertiary
|
||||
hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active
|
||||
focus:bg-components-input-bg-active focus:shadow-xs
|
||||
|
||||
|
||||
"
|
||||
placeholder="API Key"
|
||||
tabindex="0"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -0,0 +1,449 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
FormTypeEnum,
|
||||
ModelModalModeEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
useLanguage,
|
||||
} from '../hooks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
|
||||
import type {
|
||||
FormRefObject,
|
||||
FormSchema,
|
||||
} from '@/app/components/base/form/types'
|
||||
import { useModelFormSchemas } from '../model-auth/hooks'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModel,
|
||||
} from '../declarations'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import {
|
||||
useAuth,
|
||||
useCredentialData,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { CredentialSelector } from '../model-auth'
|
||||
|
||||
type ModelModalProps = {
|
||||
provider: ModelProvider
|
||||
configurateMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
onCancel: () => void
|
||||
onSave: (formValues?: Record<string, any>) => void
|
||||
onRemove: (formValues?: Record<string, any>) => void
|
||||
model?: CustomModel
|
||||
credential?: Credential
|
||||
isModelCredential?: boolean
|
||||
mode?: ModelModalModeEnum
|
||||
}
|
||||
|
||||
const ModelModal: FC<ModelModalProps> = ({
|
||||
provider,
|
||||
configurateMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
onCancel,
|
||||
onSave,
|
||||
model,
|
||||
credential,
|
||||
isModelCredential,
|
||||
mode = ModelModalModeEnum.configProviderCredential,
|
||||
}) => {
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
|
||||
const {
|
||||
isLoading,
|
||||
credentialData,
|
||||
} = useCredentialData(provider, providerFormSchemaPredefined, isModelCredential, credential, model)
|
||||
const {
|
||||
handleSaveCredential,
|
||||
handleConfirmDelete,
|
||||
deleteCredentialId,
|
||||
closeConfirmDelete,
|
||||
openConfirmDelete,
|
||||
doingAction,
|
||||
handleActiveCredential,
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurateMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential,
|
||||
mode,
|
||||
},
|
||||
)
|
||||
const {
|
||||
credentials: formSchemasValue,
|
||||
available_credentials,
|
||||
} = credentialData as any
|
||||
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const {
|
||||
formSchemas,
|
||||
formValues,
|
||||
modelNameAndTypeFormSchemas,
|
||||
modelNameAndTypeFormValues,
|
||||
} = useModelFormSchemas(provider, providerFormSchemaPredefined, formSchemasValue, credential, model)
|
||||
const formRef1 = useRef<FormRefObject>(null)
|
||||
const [selectedCredential, setSelectedCredential] = useState<Credential & { addNewCredential?: boolean } | undefined>()
|
||||
const formRef2 = useRef<FormRefObject>(null)
|
||||
const isEditMode = !!credential && !!Object.keys(formSchemasValue || {}).filter((key) => {
|
||||
return key !== '__model_name' && key !== '__model_type' && !!formValues[key]
|
||||
}).length && isCurrentWorkspaceManager
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList && selectedCredential && !selectedCredential?.addNewCredential) {
|
||||
handleActiveCredential(selectedCredential, model)
|
||||
onCancel()
|
||||
return
|
||||
}
|
||||
|
||||
let modelNameAndTypeIsCheckValidated = true
|
||||
let modelNameAndTypeValues: Record<string, any> = {}
|
||||
|
||||
if (mode === ModelModalModeEnum.configCustomModel) {
|
||||
const formResult = formRef1.current?.getFormValues({
|
||||
needCheckValidatedValues: true,
|
||||
}) || { isCheckValidated: false, values: {} }
|
||||
modelNameAndTypeIsCheckValidated = formResult.isCheckValidated
|
||||
modelNameAndTypeValues = formResult.values
|
||||
}
|
||||
|
||||
if (mode === ModelModalModeEnum.configModelCredential && model) {
|
||||
modelNameAndTypeValues = {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList && selectedCredential?.addNewCredential && model) {
|
||||
modelNameAndTypeValues = {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
}
|
||||
}
|
||||
const {
|
||||
isCheckValidated,
|
||||
values,
|
||||
} = formRef2.current?.getFormValues({
|
||||
needCheckValidatedValues: true,
|
||||
needTransformWhenSecretFieldIsPristine: true,
|
||||
}) || { isCheckValidated: false, values: {} }
|
||||
if (!isCheckValidated || !modelNameAndTypeIsCheckValidated)
|
||||
return
|
||||
|
||||
const {
|
||||
__model_name,
|
||||
__model_type,
|
||||
} = modelNameAndTypeValues
|
||||
const {
|
||||
__authorization_name__,
|
||||
...rest
|
||||
} = values
|
||||
if (__model_name && __model_type) {
|
||||
await handleSaveCredential({
|
||||
credential_id: credential?.credential_id,
|
||||
credentials: rest,
|
||||
name: __authorization_name__,
|
||||
model: __model_name,
|
||||
model_type: __model_type,
|
||||
})
|
||||
}
|
||||
else {
|
||||
await handleSaveCredential({
|
||||
credential_id: credential?.credential_id,
|
||||
credentials: rest,
|
||||
name: __authorization_name__,
|
||||
})
|
||||
}
|
||||
onSave(values)
|
||||
}, [handleSaveCredential, credential?.credential_id, model, onSave, mode, selectedCredential, handleActiveCredential])
|
||||
|
||||
const modalTitle = useMemo(() => {
|
||||
let label = t('common.modelProvider.auth.apiKeyModal.title')
|
||||
|
||||
if (mode === ModelModalModeEnum.configCustomModel || mode === ModelModalModeEnum.addCustomModelToModelList)
|
||||
label = t('common.modelProvider.auth.addModel')
|
||||
if (mode === ModelModalModeEnum.configModelCredential) {
|
||||
if (credential)
|
||||
label = t('common.modelProvider.auth.editModelCredential')
|
||||
else
|
||||
label = t('common.modelProvider.auth.addModelCredential')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='title-2xl-semi-bold text-text-primary'>
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
}, [t, mode, credential])
|
||||
|
||||
const modalDesc = useMemo(() => {
|
||||
if (providerFormSchemaPredefined) {
|
||||
return (
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>
|
||||
{t('common.modelProvider.auth.apiKeyModal.desc')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [providerFormSchemaPredefined, t])
|
||||
|
||||
const modalModel = useMemo(() => {
|
||||
if (mode === ModelModalModeEnum.configCustomModel) {
|
||||
return (
|
||||
<div className='mt-2 flex items-center'>
|
||||
<ModelIcon
|
||||
className='mr-2 h-4 w-4 shrink-0'
|
||||
provider={provider}
|
||||
/>
|
||||
<div className='system-md-regular mr-1 text-text-secondary'>{renderI18nObject(provider.label)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (model && (mode === ModelModalModeEnum.configModelCredential || mode === ModelModalModeEnum.addCustomModelToModelList)) {
|
||||
return (
|
||||
<div className='mt-2 flex items-center'>
|
||||
<ModelIcon
|
||||
className='mr-2 h-4 w-4 shrink-0'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
<div className='system-md-regular mr-1 text-text-secondary'>{model.model}</div>
|
||||
<Badge>{model.model_type}</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [model, provider, mode, renderI18nObject])
|
||||
|
||||
const showCredentialLabel = useMemo(() => {
|
||||
if (mode === ModelModalModeEnum.configCustomModel)
|
||||
return true
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList)
|
||||
return selectedCredential?.addNewCredential
|
||||
}, [mode, selectedCredential])
|
||||
const showCredentialForm = useMemo(() => {
|
||||
if (mode !== ModelModalModeEnum.addCustomModelToModelList)
|
||||
return true
|
||||
return selectedCredential?.addNewCredential
|
||||
}, [mode, selectedCredential])
|
||||
const saveButtonText = useMemo(() => {
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList || mode === ModelModalModeEnum.configCustomModel)
|
||||
return t('common.operation.add')
|
||||
return t('common.operation.save')
|
||||
}, [mode, t])
|
||||
|
||||
const handleDeleteCredential = useCallback(() => {
|
||||
handleConfirmDelete()
|
||||
onCancel()
|
||||
}, [handleConfirmDelete])
|
||||
|
||||
const handleModelNameAndTypeChange = useCallback((field: string, value: any) => {
|
||||
const {
|
||||
getForm,
|
||||
} = formRef2.current as FormRefObject || {}
|
||||
if (getForm())
|
||||
getForm()?.setFieldValue(field, value)
|
||||
}, [])
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.stopPropagation()
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown, true)
|
||||
}
|
||||
}, [onCancel])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem open>
|
||||
<PortalToFollowElemContent className='z-[60] h-full w-full'>
|
||||
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
|
||||
<div className='relative w-[640px] rounded-2xl bg-components-panel-bg shadow-xl'>
|
||||
<div
|
||||
className='absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center'
|
||||
onClick={onCancel}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='p-6 pb-3'>
|
||||
{modalTitle}
|
||||
{modalDesc}
|
||||
{modalModel}
|
||||
</div>
|
||||
<div className='max-h-[calc(100vh-320px)] overflow-y-auto px-6 py-3'>
|
||||
{
|
||||
mode === ModelModalModeEnum.configCustomModel && (
|
||||
<AuthForm
|
||||
formSchemas={modelNameAndTypeFormSchemas.map((formSchema) => {
|
||||
return {
|
||||
...formSchema,
|
||||
name: formSchema.variable,
|
||||
}
|
||||
}) as FormSchema[]}
|
||||
defaultValues={modelNameAndTypeFormValues}
|
||||
inputClassName='justify-start'
|
||||
ref={formRef1}
|
||||
onChange={handleModelNameAndTypeChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
mode === ModelModalModeEnum.addCustomModelToModelList && (
|
||||
<CredentialSelector
|
||||
credentials={available_credentials || []}
|
||||
onSelect={setSelectedCredential}
|
||||
selectedCredential={selectedCredential}
|
||||
disabled={isLoading}
|
||||
notAllowAddNewCredential={notAllowCustomCredential}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showCredentialLabel && (
|
||||
<div className='system-xs-medium-uppercase mb-3 mt-6 flex items-center text-text-tertiary'>
|
||||
{t('common.modelProvider.auth.modelCredential')}
|
||||
<div className='ml-2 h-px grow bg-gradient-to-r from-divider-regular to-background-gradient-mask-transparent' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
isLoading && (
|
||||
<div className='mt-3 flex items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isLoading
|
||||
&& showCredentialForm
|
||||
&& (
|
||||
<AuthForm
|
||||
formSchemas={formSchemas.map((formSchema) => {
|
||||
return {
|
||||
...formSchema,
|
||||
name: formSchema.variable,
|
||||
showRadioUI: formSchema.type === FormTypeEnum.radio,
|
||||
}
|
||||
}) as FormSchema[]}
|
||||
defaultValues={formValues}
|
||||
inputClassName='justify-start'
|
||||
ref={formRef2}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='flex justify-between p-6 pt-5'>
|
||||
{
|
||||
(provider.help && (provider.help.title || provider.help.url))
|
||||
? (
|
||||
<a
|
||||
href={provider.help?.url[language] || provider.help?.url.en_US}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='system-xs-regular mt-2 inline-block align-middle text-text-accent'
|
||||
onClick={e => !provider.help.url && e.preventDefault()}
|
||||
>
|
||||
{provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US}
|
||||
<LinkExternal02 className='ml-1 mt-[-2px] inline-block h-3 w-3' />
|
||||
</a>
|
||||
)
|
||||
: <div />
|
||||
}
|
||||
<div className='ml-2 flex items-center justify-end space-x-2'>
|
||||
{
|
||||
isEditMode && (
|
||||
<Button
|
||||
variant='warning'
|
||||
onClick={() => openConfirmDelete(credential, model)}
|
||||
>
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || doingAction}
|
||||
>
|
||||
{saveButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
(mode === ModelModalModeEnum.configCustomModel || mode === ModelModalModeEnum.configProviderCredential) && (
|
||||
<div className='border-t-[0.5px] border-t-divider-regular'>
|
||||
<div className='flex items-center justify-center rounded-b-2xl bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
deleteCredentialId && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('common.modelProvider.confirmDelete')}
|
||||
isDisabled={doingAction}
|
||||
onCancel={closeConfirmDelete}
|
||||
onConfirm={handleDeleteCredential}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ModelModal)
|
||||
Reference in New Issue
Block a user