mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-10 03:16:51 +08:00
Model Runtime (#1858)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: Garfield Dai <dai.hai@foxmail.com> Co-authored-by: chenhe <guchenhe@gmail.com> Co-authored-by: jyong <jyong@dify.ai> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Yeuoly <admin@srmxy.cn>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
export type FormValue = Record<string, any>
|
||||
|
||||
export type TypeWithI18N<T = string> = {
|
||||
'en_US': T
|
||||
'zh_Hans': T
|
||||
}
|
||||
|
||||
export enum FormTypeEnum {
|
||||
textInput = 'text-input',
|
||||
secretInput = 'secret-input',
|
||||
select = 'select',
|
||||
radio = 'radio',
|
||||
}
|
||||
|
||||
export type FormOption = {
|
||||
label: TypeWithI18N
|
||||
value: string
|
||||
show_on: FormShowOnObject[]
|
||||
}
|
||||
|
||||
export enum ModelTypeEnum {
|
||||
textGeneration = 'llm',
|
||||
textEmbedding = 'text-embedding',
|
||||
rerank = 'rerank',
|
||||
speech2text = 'speech2text',
|
||||
moderation = 'moderation',
|
||||
}
|
||||
|
||||
export const MODEL_TYPE_TEXT = {
|
||||
[ModelTypeEnum.textGeneration]: 'LLM',
|
||||
[ModelTypeEnum.textEmbedding]: 'Text Embedding',
|
||||
[ModelTypeEnum.rerank]: 'Rerank',
|
||||
[ModelTypeEnum.speech2text]: 'Speech2text',
|
||||
[ModelTypeEnum.moderation]: 'Moderation',
|
||||
}
|
||||
|
||||
export enum ConfigurateMethodEnum {
|
||||
predefinedModel = 'predefined-model',
|
||||
customizableModel = 'customizable-model',
|
||||
fetchFromRemote = 'fetch-from-remote',
|
||||
}
|
||||
|
||||
export enum ModelFeatureEnum {
|
||||
toolCall = 'tool-call',
|
||||
multiToolCall = 'multi-tool-call',
|
||||
agentThought = 'agent-thought',
|
||||
vision = 'vision',
|
||||
}
|
||||
|
||||
export enum ModelFeatureTextEnum {
|
||||
toolCall = 'Tool Call',
|
||||
multiToolCall = 'Multi Tool Call',
|
||||
agentThought = 'Agent Thought',
|
||||
vision = 'Vision',
|
||||
}
|
||||
|
||||
export enum ModelStatusEnum {
|
||||
active = 'active',
|
||||
noConfigure = 'no-configure',
|
||||
quotaExceeded = 'quota-exceeded',
|
||||
noPermission = 'no-permission',
|
||||
}
|
||||
|
||||
export const MODEL_STATUS_TEXT = {
|
||||
[ModelStatusEnum.noConfigure]: {
|
||||
en_US: 'No Configure',
|
||||
zh_Hans: '未配置凭据',
|
||||
},
|
||||
[ModelStatusEnum.quotaExceeded]: {
|
||||
en_US: 'Quota Exceeded',
|
||||
zh_Hans: '额度不足',
|
||||
},
|
||||
[ModelStatusEnum.noPermission]: {
|
||||
en_US: 'No Permission',
|
||||
zh_Hans: '无使用权限',
|
||||
},
|
||||
}
|
||||
|
||||
export enum CustomConfigurationStatusEnum {
|
||||
active = 'active',
|
||||
noConfigure = 'no-configure',
|
||||
}
|
||||
|
||||
export type FormShowOnObject = {
|
||||
variable: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type CredentialFormSchemaBase = {
|
||||
variable: string
|
||||
label: TypeWithI18N
|
||||
type: FormTypeEnum
|
||||
required: boolean
|
||||
default?: string
|
||||
show_on: FormShowOnObject[]
|
||||
}
|
||||
|
||||
export type CredentialFormSchemaTextInput = CredentialFormSchemaBase & { max_length?: number; placeholder?: TypeWithI18N }
|
||||
export type CredentialFormSchemaSelect = CredentialFormSchemaBase & { options: FormOption[]; placeholder?: TypeWithI18N }
|
||||
export type CredentialFormSchemaRadio = CredentialFormSchemaBase & { options: FormOption[] }
|
||||
export type CredentialFormSchemaSecretInput = CredentialFormSchemaBase & { placeholder?: TypeWithI18N }
|
||||
export type CredentialFormSchema = CredentialFormSchemaTextInput | CredentialFormSchemaSelect | CredentialFormSchemaRadio | CredentialFormSchemaSecretInput
|
||||
|
||||
export type ModelItem = {
|
||||
model: string
|
||||
label: TypeWithI18N
|
||||
model_type: ModelTypeEnum
|
||||
features?: ModelFeatureEnum[]
|
||||
fetch_from: ConfigurateMethodEnum
|
||||
status: ModelStatusEnum
|
||||
model_properties: Record<string, string | number>
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
export enum PreferredProviderTypeEnum {
|
||||
system = 'system',
|
||||
custom = 'custom',
|
||||
}
|
||||
|
||||
export enum CurrentSystemQuotaTypeEnum {
|
||||
trial = 'trial',
|
||||
free = 'free',
|
||||
paid = 'paid',
|
||||
}
|
||||
|
||||
export enum QuotaUnitEnum {
|
||||
times = 'times',
|
||||
tokens = 'tokens',
|
||||
}
|
||||
|
||||
export type QuotaConfiguration = {
|
||||
quota_type: CurrentSystemQuotaTypeEnum
|
||||
quota_unit: QuotaUnitEnum
|
||||
quota_limit: number
|
||||
quota_used: number
|
||||
last_used: number
|
||||
is_valid: boolean
|
||||
}
|
||||
|
||||
export type ModelProvider = {
|
||||
provider: string
|
||||
label: TypeWithI18N
|
||||
description?: TypeWithI18N
|
||||
help: {
|
||||
title: TypeWithI18N
|
||||
url: TypeWithI18N
|
||||
}
|
||||
icon_small: TypeWithI18N
|
||||
icon_large: TypeWithI18N
|
||||
background?: string
|
||||
supported_model_types: ModelTypeEnum[]
|
||||
configurate_methods: ConfigurateMethodEnum[]
|
||||
provider_credential_schema: {
|
||||
credential_form_schemas: CredentialFormSchema[]
|
||||
}
|
||||
model_credential_schema: {
|
||||
model: {
|
||||
label: TypeWithI18N
|
||||
placeholder: TypeWithI18N
|
||||
}
|
||||
credential_form_schemas: CredentialFormSchema[]
|
||||
}
|
||||
preferred_provider_type: PreferredProviderTypeEnum
|
||||
custom_configuration: {
|
||||
status: CustomConfigurationStatusEnum
|
||||
}
|
||||
system_configuration: {
|
||||
enabled: boolean
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum
|
||||
quota_configurations: QuotaConfiguration[]
|
||||
}
|
||||
}
|
||||
|
||||
export type Model = {
|
||||
provider: string
|
||||
icon_large: TypeWithI18N
|
||||
icon_small: TypeWithI18N
|
||||
label: TypeWithI18N
|
||||
models: ModelItem[]
|
||||
status: ModelStatusEnum
|
||||
}
|
||||
|
||||
export type DefaultModelResponse = {
|
||||
model: string
|
||||
model_type: ModelTypeEnum
|
||||
provider: {
|
||||
provider: string
|
||||
icon_large: TypeWithI18N
|
||||
icon_small: TypeWithI18N
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultModel = {
|
||||
provider: string
|
||||
model: string
|
||||
}
|
||||
|
||||
export type CustomConfigrationModelFixedFields = {
|
||||
__model_name: string
|
||||
__model_type: ModelTypeEnum
|
||||
}
|
||||
|
||||
export type ModelParameterRule = {
|
||||
default?: number | string | boolean | string[]
|
||||
help?: TypeWithI18N
|
||||
label: TypeWithI18N
|
||||
min?: number
|
||||
max?: number
|
||||
name: string
|
||||
precision?: number
|
||||
required: false
|
||||
type: string
|
||||
use_template?: string
|
||||
options?: string[]
|
||||
tagPlaceholder?: TypeWithI18N
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import useSWR, { useSWRConfig } from 'swr'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type {
|
||||
CustomConfigrationModelFixedFields,
|
||||
DefaultModel,
|
||||
DefaultModelResponse,
|
||||
Model,
|
||||
} from './declarations'
|
||||
import {
|
||||
ConfigurateMethodEnum,
|
||||
ModelTypeEnum,
|
||||
} from './declarations'
|
||||
import { languageMaps } from './utils'
|
||||
import I18n from '@/context/i18n'
|
||||
import {
|
||||
fetchDefaultModal,
|
||||
fetchModelList,
|
||||
fetchModelProviderCredentials,
|
||||
fetchModelProviders,
|
||||
getPayUrl,
|
||||
submitFreeQuota,
|
||||
} from '@/service/common'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
type UseDefaultModelAndModelList = (
|
||||
defaultModel: DefaultModelResponse | undefined,
|
||||
modelList: Model[],
|
||||
) => [DefaultModel | undefined, (model: DefaultModel) => void]
|
||||
export const useSystemDefaultModelAndModelList: UseDefaultModelAndModelList = (
|
||||
defaultModel,
|
||||
modelList,
|
||||
) => {
|
||||
const currentDefaultModel = useMemo(() => {
|
||||
const currentProvider = modelList.find(provider => provider.provider === defaultModel?.provider.provider)
|
||||
const currentModel = currentProvider?.models.find(model => model.model === defaultModel?.model)
|
||||
const currentDefaultModel = currentProvider && currentModel && {
|
||||
model: currentModel.model,
|
||||
provider: currentProvider.provider,
|
||||
}
|
||||
|
||||
return currentDefaultModel
|
||||
}, [defaultModel, modelList])
|
||||
const [defaultModelState, setDefaultModelState] = useState<DefaultModel | undefined>(currentDefaultModel)
|
||||
const handleDefaultModelChange = useCallback((model: DefaultModel) => {
|
||||
setDefaultModelState(model)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
setDefaultModelState(currentDefaultModel)
|
||||
}, [currentDefaultModel])
|
||||
|
||||
return [defaultModelState, handleDefaultModelChange]
|
||||
}
|
||||
|
||||
export const useLanguage = () => {
|
||||
const { locale } = useContext(I18n)
|
||||
|
||||
return languageMaps[locale]
|
||||
}
|
||||
|
||||
export const useProviderCrenditialsFormSchemasValue = (
|
||||
provider: string,
|
||||
configurateMethod: ConfigurateMethodEnum,
|
||||
configured?: boolean,
|
||||
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields,
|
||||
) => {
|
||||
const { data: predefinedFormSchemasValue } = useSWR(
|
||||
(configurateMethod === ConfigurateMethodEnum.predefinedModel && configured)
|
||||
? `/workspaces/current/model-providers/${provider}/credentials`
|
||||
: null,
|
||||
fetchModelProviderCredentials,
|
||||
)
|
||||
const { data: customFormSchemasValue } = useSWR(
|
||||
(configurateMethod === ConfigurateMethodEnum.customizableModel && currentCustomConfigrationModelFixedFields)
|
||||
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigrationModelFixedFields?.__model_name}&model_type=${currentCustomConfigrationModelFixedFields?.__model_type}`
|
||||
: null,
|
||||
fetchModelProviderCredentials,
|
||||
)
|
||||
|
||||
const value = useMemo(() => {
|
||||
return configurateMethod === ConfigurateMethodEnum.predefinedModel
|
||||
? predefinedFormSchemasValue?.credentials
|
||||
: customFormSchemasValue?.credentials
|
||||
? {
|
||||
...customFormSchemasValue?.credentials,
|
||||
...currentCustomConfigrationModelFixedFields,
|
||||
}
|
||||
: undefined
|
||||
}, [
|
||||
configurateMethod,
|
||||
currentCustomConfigrationModelFixedFields,
|
||||
customFormSchemasValue?.credentials,
|
||||
predefinedFormSchemasValue?.credentials,
|
||||
])
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export type ModelTypeIndex = 1 | 2 | 3 | 4
|
||||
export const MODEL_TYPE_MAPS = {
|
||||
1: ModelTypeEnum.textGeneration,
|
||||
2: ModelTypeEnum.textEmbedding,
|
||||
3: ModelTypeEnum.rerank,
|
||||
4: ModelTypeEnum.speech2text,
|
||||
}
|
||||
|
||||
export const useModelList = (type: ModelTypeIndex) => {
|
||||
const { data, mutate, isLoading } = useSWR(`/workspaces/current/models/model-types/${MODEL_TYPE_MAPS[type]}`, fetchModelList)
|
||||
|
||||
return {
|
||||
data: data?.data || [],
|
||||
mutate,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
export const useDefaultModel = (type: ModelTypeIndex) => {
|
||||
const { data, mutate, isLoading } = useSWR(`/workspaces/current/default-model?model_type=${MODEL_TYPE_MAPS[type]}`, fetchDefaultModal)
|
||||
|
||||
return {
|
||||
data: data?.data,
|
||||
mutate,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
export const useCurrentProviderAndModel = (modelList: Model[], defaultModel?: DefaultModel) => {
|
||||
const currentProvider = modelList.find(provider => provider.provider === defaultModel?.provider)
|
||||
const currentModel = currentProvider?.models.find(model => model.model === defaultModel?.model)
|
||||
|
||||
return {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
}
|
||||
}
|
||||
|
||||
export const useTextGenerationCurrentProviderAndModelAndModelList = (defaultModel?: DefaultModel) => {
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
} = useCurrentProviderAndModel(textGenerationModelList, defaultModel)
|
||||
|
||||
return {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
textGenerationModelList,
|
||||
}
|
||||
}
|
||||
|
||||
export const useAgentThoughtCurrentProviderAndModelAndModelList = (defaultModel?: DefaultModel) => {
|
||||
const { agentThoughtModelList } = useProviderContext()
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
} = useCurrentProviderAndModel(agentThoughtModelList, defaultModel)
|
||||
|
||||
return {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
agentThoughtModelList,
|
||||
}
|
||||
}
|
||||
|
||||
export const useModelListAndDefaultModel = (type: ModelTypeIndex) => {
|
||||
const { data: modelList } = useModelList(type)
|
||||
const { data: defaultModel } = useDefaultModel(type)
|
||||
|
||||
return {
|
||||
modelList,
|
||||
defaultModel,
|
||||
}
|
||||
}
|
||||
|
||||
export const useModelListAndDefaultModelAndCurrentProviderAndModel = (type: ModelTypeIndex) => {
|
||||
const { modelList, defaultModel } = useModelListAndDefaultModel(type)
|
||||
const { currentProvider, currentModel } = useCurrentProviderAndModel(
|
||||
modelList,
|
||||
{ provider: defaultModel?.provider.provider || '', model: defaultModel?.model || '' },
|
||||
)
|
||||
|
||||
return {
|
||||
modelList,
|
||||
defaultModel,
|
||||
currentProvider,
|
||||
currentModel,
|
||||
}
|
||||
}
|
||||
|
||||
export const useUpdateModelList = () => {
|
||||
const { mutate } = useSWRConfig()
|
||||
|
||||
const updateModelList = useCallback((type: ModelTypeIndex | ModelTypeEnum) => {
|
||||
const modelType = typeof type === 'number' ? MODEL_TYPE_MAPS[type] : type
|
||||
mutate(`/workspaces/current/models/model-types/${modelType}`)
|
||||
}, [mutate])
|
||||
|
||||
return updateModelList
|
||||
}
|
||||
|
||||
export const useAnthropicBuyQuota = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleGetPayUrl = async () => {
|
||||
if (loading)
|
||||
return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getPayUrl('/workspaces/current/model-providers/anthropic/checkout-url')
|
||||
|
||||
window.location.href = res.url
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return handleGetPayUrl
|
||||
}
|
||||
|
||||
export const useFreeQuota = (onSuccess: () => void) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleClick = async (type: string) => {
|
||||
if (loading)
|
||||
return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await submitFreeQuota(`/workspaces/current/model-providers/${type}/free-quota-submit`)
|
||||
|
||||
if (res.type === 'redirect' && res.redirect_url)
|
||||
window.location.href = res.redirect_url
|
||||
else if (res.type === 'submit' && res.result === 'success')
|
||||
onSuccess()
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return handleClick
|
||||
}
|
||||
|
||||
export const useModelProviders = () => {
|
||||
const { data: providersData, mutate, isLoading } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
|
||||
|
||||
return {
|
||||
data: providersData?.data || [],
|
||||
mutate,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
export const useUpdateModelProvidersAndModelList = () => {
|
||||
const { mutate } = useSWRConfig()
|
||||
const updateModelList = useUpdateModelList()
|
||||
|
||||
const updateModelProvidersAndModelList = useCallback(() => {
|
||||
mutate('/workspaces/current/model-providers')
|
||||
updateModelList(1)
|
||||
}, [mutate, updateModelList])
|
||||
|
||||
return updateModelProvidersAndModelList
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SystemModelSelector from './system-model-selector'
|
||||
import ProviderAddedCard, { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card'
|
||||
import ProviderCard from './provider-card'
|
||||
import type {
|
||||
ConfigurateMethodEnum,
|
||||
CustomConfigrationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from './declarations'
|
||||
import { CustomConfigurationStatusEnum } from './declarations'
|
||||
import {
|
||||
useDefaultModel,
|
||||
useUpdateModelProvidersAndModelList,
|
||||
} from './hooks'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
const ModelProviderPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const updateModelProvidersAndModelList = useUpdateModelProvidersAndModelList()
|
||||
const { data: textGenerationDefaultModel } = useDefaultModel(1)
|
||||
const { data: embeddingsDefaultModel } = useDefaultModel(2)
|
||||
const { data: rerankDefaultModel } = useDefaultModel(3)
|
||||
const { data: speech2textDefaultModel } = useDefaultModel(4)
|
||||
const { modelProviders: providers } = useProviderContext()
|
||||
const { setShowModelModal } = useModalContext()
|
||||
const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel
|
||||
const [configedProviders, notConfigedProviders] = useMemo(() => {
|
||||
const configedProviders: ModelProvider[] = []
|
||||
const notConfigedProviders: ModelProvider[] = []
|
||||
|
||||
providers.forEach((provider) => {
|
||||
if (provider.custom_configuration.status === CustomConfigurationStatusEnum.active || provider.system_configuration.enabled === true)
|
||||
configedProviders.push(provider)
|
||||
else
|
||||
notConfigedProviders.push(provider)
|
||||
})
|
||||
|
||||
return [configedProviders, notConfigedProviders]
|
||||
}, [providers])
|
||||
|
||||
const handleOpenModal = (
|
||||
provider: ModelProvider,
|
||||
configurateMethod: ConfigurateMethodEnum,
|
||||
customConfigrationModelFixedFields?: CustomConfigrationModelFixedFields,
|
||||
) => {
|
||||
setShowModelModal({
|
||||
payload: {
|
||||
currentProvider: provider,
|
||||
currentConfigurateMethod: configurateMethod,
|
||||
currentCustomConfigrationModelFixedFields: customConfigrationModelFixedFields,
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
updateModelProvidersAndModelList()
|
||||
|
||||
if (customConfigrationModelFixedFields && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
|
||||
eventEmitter?.emit({
|
||||
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
|
||||
payload: provider.provider,
|
||||
} as any)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative pt-1 -mt-2'>
|
||||
<div className={`flex items-center justify-between mb-2 h-8 ${defaultModelNotConfigured && 'px-3 bg-[#FFFAEB] rounded-lg border border-[#FEF0C7]'}`}>
|
||||
{
|
||||
defaultModelNotConfigured
|
||||
? (
|
||||
<div className='flex items-center text-xs font-medium text-gray-700'>
|
||||
<AlertTriangle className='mr-1 w-3 h-3 text-[#F79009]' />
|
||||
{t('common.modelProvider.notConfigured')}
|
||||
</div>
|
||||
)
|
||||
: <div className='text-sm font-medium text-gray-800'>{t('common.modelProvider.models')}</div>
|
||||
}
|
||||
<SystemModelSelector
|
||||
textGenerationDefaultModel={textGenerationDefaultModel}
|
||||
embeddingsDefaultModel={embeddingsDefaultModel}
|
||||
rerankDefaultModel={rerankDefaultModel}
|
||||
speech2textDefaultModel={speech2textDefaultModel}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
!!configedProviders?.length && (
|
||||
<div className='pb-3'>
|
||||
{
|
||||
configedProviders?.map(provider => (
|
||||
<ProviderAddedCard
|
||||
key={provider.provider}
|
||||
provider={provider}
|
||||
onOpenModal={(configurateMethod: ConfigurateMethodEnum, currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => handleOpenModal(provider, configurateMethod, currentCustomConfigrationModelFixedFields)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!notConfigedProviders?.length && (
|
||||
<>
|
||||
<div className='flex items-center mb-2 text-xs font-semibold text-gray-500'>
|
||||
+ {t('common.modelProvider.addMoreModelProvider')}
|
||||
<span className='grow ml-3 h-[1px] bg-gradient-to-r from-[#f3f4f6]' />
|
||||
</div>
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
{
|
||||
notConfigedProviders?.map(provider => (
|
||||
<ProviderCard
|
||||
key={provider.provider}
|
||||
provider={provider}
|
||||
onOpenModal={(configurateMethod: ConfigurateMethodEnum) => handleOpenModal(provider, configurateMethod)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelProviderPage
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
|
||||
type ModelBadgeProps = {
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
const ModelBadge: FC<ModelBadgeProps> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`
|
||||
flex items-center px-1 h-[18px] rounded-[5px] border border-black/[0.08] bg-white/[0.48]
|
||||
text-[10px] font-medium text-gray-500
|
||||
${className}
|
||||
`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelBadge
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
Model,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
|
||||
import { OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
|
||||
|
||||
type ModelIconProps = {
|
||||
provider?: Model | ModelProvider
|
||||
modelName?: string
|
||||
className?: string
|
||||
}
|
||||
const ModelIcon: FC<ModelIconProps> = ({
|
||||
provider,
|
||||
className,
|
||||
modelName,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
|
||||
if (provider?.provider === 'openai' && modelName?.startsWith('gpt-4'))
|
||||
return <OpenaiViolet className={`w-4 h-4 ${className}`}/>
|
||||
|
||||
if (provider?.icon_small) {
|
||||
return (
|
||||
<img
|
||||
alt='model-icon'
|
||||
src={`${provider.icon_small[language]}?_token=${localStorage.getItem('console_token')}`}
|
||||
className={`w-4 h-4 ${className}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
flex items-center justify-center w-6 h-6 rounded border-[0.5px] border-black/5 bg-gray-50
|
||||
${className}
|
||||
`}>
|
||||
<CubeOutline className='w-4 h-4 text-gray-400' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelIcon
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { ValidatingTip } from '../../key-validator/ValidateStatus'
|
||||
import type {
|
||||
CredentialFormSchema,
|
||||
CredentialFormSchemaRadio,
|
||||
CredentialFormSchemaSecretInput,
|
||||
CredentialFormSchemaSelect,
|
||||
CredentialFormSchemaTextInput,
|
||||
FormValue,
|
||||
} from '../declarations'
|
||||
import { FormTypeEnum } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import Input from './Input'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
|
||||
type FormProps = {
|
||||
value: FormValue
|
||||
onChange: (val: FormValue) => void
|
||||
formSchemas: CredentialFormSchema[]
|
||||
validating: boolean
|
||||
validatedSuccess?: boolean
|
||||
showOnVariableMap: Record<string, string[]>
|
||||
}
|
||||
|
||||
const Form: FC<FormProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
formSchemas,
|
||||
validating,
|
||||
validatedSuccess,
|
||||
showOnVariableMap,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const [changeKey, setChangeKey] = useState('')
|
||||
|
||||
const handleFormChange = (key: string, val: string) => {
|
||||
setChangeKey(key)
|
||||
const shouldClearVariable: Record<string, string | undefined> = {}
|
||||
if (showOnVariableMap[key]?.length) {
|
||||
showOnVariableMap[key].forEach((clearVariable) => {
|
||||
shouldClearVariable[clearVariable] = undefined
|
||||
})
|
||||
}
|
||||
onChange({ ...value, [key]: val, ...shouldClearVariable })
|
||||
}
|
||||
|
||||
const renderField = (formSchema: CredentialFormSchema) => {
|
||||
if (formSchema.type === FormTypeEnum.textInput || formSchema.type === FormTypeEnum.secretInput) {
|
||||
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
|
||||
|
||||
return (
|
||||
<div key={variable} className='py-3'>
|
||||
<div className='py-2 text-sm text-gray-900'>
|
||||
{label[language]}
|
||||
{
|
||||
required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Input
|
||||
value={value[variable] as string}
|
||||
onChange={val => handleFormChange(variable, val)}
|
||||
validated={validatedSuccess}
|
||||
placeholder={placeholder?.[language]}
|
||||
/>
|
||||
{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
|
||||
|
||||
return (
|
||||
<div key={variable} className='py-3'>
|
||||
<div className='py-2 text-sm text-gray-900'>
|
||||
{label[language]}
|
||||
{
|
||||
required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className={`grid grid-cols-${options?.length} gap-3`}>
|
||||
{
|
||||
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 items-center px-3 py-2 rounded-lg border border-gray-100 bg-gray-25 cursor-pointer
|
||||
${value[variable] === option.value && 'bg-white border-[1.5px] border-primary-400 shadow-sm'}
|
||||
`}
|
||||
onClick={() => handleFormChange(variable, option.value)}
|
||||
key={`${variable}-${option.value}`}
|
||||
>
|
||||
<div className={`
|
||||
flex justify-center items-center mr-2 w-4 h-4 border border-gray-300 rounded-full
|
||||
${value[variable] === option.value && 'border-[5px] border-primary-600'}
|
||||
`} />
|
||||
<div className='text-sm text-gray-900'>{option.label[language]}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === '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='py-3'>
|
||||
<div className='py-2 text-sm text-gray-900'>
|
||||
{label[language]}
|
||||
{
|
||||
required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<SimpleSelect
|
||||
defaultValue={value[variable] as string}
|
||||
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] }))}
|
||||
onSelect={item => handleFormChange(variable, item.value as string)}
|
||||
placeholder={placeholder?.[language]}
|
||||
/>
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
formSchemas.map(formSchema => renderField(formSchema))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@@ -0,0 +1,46 @@
|
||||
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
|
||||
}
|
||||
const Input: FC<InputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
placeholder,
|
||||
validated,
|
||||
}) => {
|
||||
return (
|
||||
<div className='relative'>
|
||||
<input
|
||||
tabIndex={-1}
|
||||
className={`
|
||||
block px-3 w-full h-9 bg-gray-100 text-sm rounded-lg border border-transparent
|
||||
appearance-none outline-none caret-primary-600
|
||||
hover:border-[rgba(0,0,0,0.08)] hover:bg-gray-50
|
||||
focus:bg-white focus:border-gray-300 focus:shadow-xs
|
||||
placeholder:text-sm placeholder:text-gray-400
|
||||
${validated && 'pr-[30px]'}
|
||||
`}
|
||||
placeholder={placeholder || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onFocus={onFocus}
|
||||
value={value || ''}
|
||||
/>
|
||||
{
|
||||
validated && (
|
||||
<div className='absolute top-2.5 right-2.5'>
|
||||
<CheckCircle className='w-4 h-4 text-[#039855]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
||||
@@ -0,0 +1,330 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
CredentialFormSchema,
|
||||
CredentialFormSchemaRadio,
|
||||
CredentialFormSchemaSelect,
|
||||
CustomConfigrationModelFixedFields,
|
||||
FormValue,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurateMethodEnum,
|
||||
CustomConfigurationStatusEnum,
|
||||
FormTypeEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
genModelNameFormSchema,
|
||||
genModelTypeFormSchema,
|
||||
removeCredentials,
|
||||
saveCredentials,
|
||||
} from '../utils'
|
||||
import {
|
||||
useLanguage,
|
||||
useProviderCrenditialsFormSchemasValue,
|
||||
} from '../hooks'
|
||||
import ProviderIcon from '../provider-icon'
|
||||
import { useValidate } from '../../key-validator/hooks'
|
||||
import { ValidatedStatus } from '../../key-validator/declarations'
|
||||
import Form from './Form'
|
||||
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 { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import ConfirmCommon from '@/app/components/base/confirm/common'
|
||||
|
||||
type ModelModalProps = {
|
||||
provider: ModelProvider
|
||||
configurateMethod: ConfigurateMethodEnum
|
||||
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields
|
||||
onCancel: () => void
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
const ModelModal: FC<ModelModalProps> = ({
|
||||
provider,
|
||||
configurateMethod,
|
||||
currentCustomConfigrationModelFixedFields,
|
||||
onCancel,
|
||||
onSave,
|
||||
}) => {
|
||||
const providerFormSchemaPredefined = configurateMethod === ConfigurateMethodEnum.predefinedModel
|
||||
const formSchemasValue = useProviderCrenditialsFormSchemasValue(
|
||||
provider.provider,
|
||||
configurateMethod,
|
||||
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
|
||||
currentCustomConfigrationModelFixedFields,
|
||||
)
|
||||
const isEditMode = !!formSchemasValue
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const language = useLanguage()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const formSchemas = useMemo(() => {
|
||||
return providerFormSchemaPredefined
|
||||
? provider.provider_credential_schema.credential_form_schemas
|
||||
: [
|
||||
genModelTypeFormSchema(provider.supported_model_types),
|
||||
genModelNameFormSchema(provider.model_credential_schema?.model),
|
||||
...provider.model_credential_schema.credential_form_schemas,
|
||||
]
|
||||
}, [
|
||||
providerFormSchemaPredefined,
|
||||
provider.provider_credential_schema?.credential_form_schemas,
|
||||
provider.supported_model_types,
|
||||
provider.model_credential_schema?.credential_form_schemas,
|
||||
provider.model_credential_schema?.model,
|
||||
])
|
||||
const [
|
||||
requiredFormSchemas,
|
||||
secretFormSchemas,
|
||||
defaultFormSchemaValue,
|
||||
showOnVariableMap,
|
||||
] = useMemo(() => {
|
||||
const requiredFormSchemas: CredentialFormSchema[] = []
|
||||
const secretFormSchemas: CredentialFormSchema[] = []
|
||||
const defaultFormSchemaValue: Record<string, string | number> = {}
|
||||
const showOnVariableMap: Record<string, string[]> = {}
|
||||
|
||||
formSchemas.forEach((formSchema) => {
|
||||
if (formSchema.required)
|
||||
requiredFormSchemas.push(formSchema)
|
||||
|
||||
if (formSchema.type === FormTypeEnum.secretInput)
|
||||
secretFormSchemas.push(formSchema)
|
||||
|
||||
if (formSchema.default)
|
||||
defaultFormSchemaValue[formSchema.variable] = formSchema.default
|
||||
|
||||
if (formSchema.show_on.length) {
|
||||
formSchema.show_on.forEach((showOnItem) => {
|
||||
if (!showOnVariableMap[showOnItem.variable])
|
||||
showOnVariableMap[showOnItem.variable] = []
|
||||
|
||||
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
|
||||
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
|
||||
})
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.select || formSchema.type === FormTypeEnum.radio) {
|
||||
(formSchema as (CredentialFormSchemaRadio | CredentialFormSchemaSelect)).options.forEach((option) => {
|
||||
if (option.show_on.length) {
|
||||
option.show_on.forEach((showOnItem) => {
|
||||
if (!showOnVariableMap[showOnItem.variable])
|
||||
showOnVariableMap[showOnItem.variable] = []
|
||||
|
||||
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
|
||||
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return [
|
||||
requiredFormSchemas,
|
||||
secretFormSchemas,
|
||||
defaultFormSchemaValue,
|
||||
showOnVariableMap,
|
||||
]
|
||||
}, [formSchemas])
|
||||
const initialFormSchemasValue = useMemo(() => {
|
||||
return {
|
||||
...defaultFormSchemaValue,
|
||||
...formSchemasValue,
|
||||
}
|
||||
}, [formSchemasValue, defaultFormSchemaValue])
|
||||
const [value, setValue] = useState(initialFormSchemasValue)
|
||||
useEffect(() => {
|
||||
setValue(initialFormSchemasValue)
|
||||
}, [initialFormSchemasValue])
|
||||
const [validate, validating, validatedStatusState] = useValidate(value)
|
||||
const filteredRequiredFormSchemas = requiredFormSchemas.filter((requiredFormSchema) => {
|
||||
if (requiredFormSchema.show_on.length && requiredFormSchema.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return true
|
||||
|
||||
if (!requiredFormSchema.show_on.length)
|
||||
return true
|
||||
|
||||
return false
|
||||
})
|
||||
const getSecretValues = useCallback((v: FormValue) => {
|
||||
return secretFormSchemas.reduce((prev, next) => {
|
||||
if (v[next.variable] === initialFormSchemasValue[next.variable])
|
||||
prev[next.variable] = '[__HIDDEN__]'
|
||||
|
||||
return prev
|
||||
}, {} as Record<string, string>)
|
||||
}, [initialFormSchemasValue, secretFormSchemas])
|
||||
|
||||
const handleValueChange = (v: FormValue) => {
|
||||
setValue(v)
|
||||
}
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const res = await saveCredentials(
|
||||
providerFormSchemaPredefined,
|
||||
provider.provider,
|
||||
{
|
||||
...value,
|
||||
...getSecretValues(value),
|
||||
},
|
||||
)
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onSave()
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const res = await removeCredentials(
|
||||
providerFormSchemaPredefined,
|
||||
provider.provider,
|
||||
value,
|
||||
)
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onSave()
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const renderTitlePrefix = () => {
|
||||
const prefix = configurateMethod === ConfigurateMethodEnum.customizableModel ? t('common.operation.add') : t('common.operation.setup')
|
||||
|
||||
return `${prefix} ${provider.label[language]}`
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem open>
|
||||
<PortalToFollowElemContent className='w-full h-full z-[60]'>
|
||||
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
|
||||
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
|
||||
<div className='px-8 pt-8'>
|
||||
<div className='flex justify-between items-center mb-2'>
|
||||
<div className='text-xl font-semibold text-gray-900'>{renderTitlePrefix()}</div>
|
||||
<ProviderIcon provider={provider} />
|
||||
</div>
|
||||
<Form
|
||||
value={value}
|
||||
onChange={handleValueChange}
|
||||
formSchemas={formSchemas}
|
||||
validating={validating}
|
||||
validatedSuccess={validatedStatusState.status === ValidatedStatus.Success}
|
||||
showOnVariableMap={showOnVariableMap}
|
||||
/>
|
||||
<div className='sticky bottom-0 flex justify-between items-center py-6 flex-wrap gap-y-2 bg-white'>
|
||||
{
|
||||
(provider.help && (provider.help.title || provider.help.url))
|
||||
? (
|
||||
<a
|
||||
href={provider.help?.url[language]}
|
||||
target='_blank'
|
||||
className='inline-flex items-center text-xs text-primary-600'
|
||||
onClick={e => !provider.help.url && e.preventDefault()}
|
||||
>
|
||||
{provider.help.title?.[language] || provider.help.url[language]}
|
||||
<LinkExternal02 className='ml-1 w-3 h-3' />
|
||||
</a>
|
||||
)
|
||||
: <div />
|
||||
}
|
||||
<div>
|
||||
{
|
||||
isEditMode && (
|
||||
<Button
|
||||
className='mr-2 h-9 text-sm font-medium text-[#D92D20]'
|
||||
onClick={() => setShowConfirm(true)}
|
||||
>
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
className='mr-2 h-9 text-sm font-medium text-gray-700'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className='h-9 text-sm font-medium'
|
||||
type='primary'
|
||||
onClick={handleSave}
|
||||
disabled={loading || filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-t-black/5'>
|
||||
{
|
||||
(validatedStatusState.status === ValidatedStatus.Error && validatedStatusState.message)
|
||||
? (
|
||||
<div className='flex px-[10px] py-3 bg-[#FEF3F2] text-xs text-[#D92D20]'>
|
||||
<AlertCircle className='mt-[1px] mr-2 w-[14px] h-[14px]' />
|
||||
{validatedStatusState.message}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
|
||||
<Lock01 className='mr-1 w-3 h-3 text-gray-500' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='text-primary-600 mx-1'
|
||||
target={'_blank'}
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showConfirm && (
|
||||
<ConfirmCommon
|
||||
title='Are you sure?'
|
||||
isShow={showConfirm}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
onConfirm={handleRemove}
|
||||
confirmWrapperClassName='z-[70]'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ModelModal)
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
modelTypeFormat,
|
||||
sizeFormat,
|
||||
} from '../utils'
|
||||
import { useLanguage } from '../hooks'
|
||||
import type { ModelItem } from '../declarations'
|
||||
import ModelBadge from '../model-badge'
|
||||
import FeatureIcon from '../model-selector/feature-icon'
|
||||
|
||||
type ModelNameProps = {
|
||||
modelItem: ModelItem
|
||||
className?: string
|
||||
showModelType?: boolean
|
||||
modelTypeClassName?: string
|
||||
showMode?: boolean
|
||||
modeClassName?: string
|
||||
showFeatures?: boolean
|
||||
featuresClassName?: string
|
||||
showContextSize?: boolean
|
||||
}
|
||||
const ModelName: FC<ModelNameProps> = ({
|
||||
modelItem,
|
||||
className,
|
||||
showModelType,
|
||||
modelTypeClassName,
|
||||
showMode,
|
||||
modeClassName,
|
||||
showFeatures,
|
||||
featuresClassName,
|
||||
showContextSize,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
|
||||
if (!modelItem)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex items-center truncate text-[13px] font-medium text-gray-800
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className='mr-2 truncate'
|
||||
title={modelItem.label[language]}
|
||||
>
|
||||
{modelItem.label[language]}
|
||||
</div>
|
||||
{
|
||||
showModelType && (
|
||||
<ModelBadge className={`mr-0.5 ${modelTypeClassName}`}>
|
||||
{modelTypeFormat(modelItem.model_type)}
|
||||
</ModelBadge>
|
||||
)
|
||||
}
|
||||
{
|
||||
modelItem.model_properties.mode && showMode && (
|
||||
<ModelBadge className={`mr-0.5 ${modeClassName}`}>
|
||||
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
|
||||
</ModelBadge>
|
||||
)
|
||||
}
|
||||
{
|
||||
showFeatures && modelItem.features?.map(feature => (
|
||||
<FeatureIcon
|
||||
key={feature}
|
||||
feature={feature}
|
||||
className={featuresClassName}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
showContextSize && modelItem.model_properties.context_size && (
|
||||
<ModelBadge>
|
||||
{sizeFormat(modelItem.model_properties.context_size as number)}
|
||||
</ModelBadge>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelName
|
||||
@@ -0,0 +1,223 @@
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
DefaultModel,
|
||||
FormValue,
|
||||
ModelParameterRule,
|
||||
} from '../declarations'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import ModelSelector from '../model-selector'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '../hooks'
|
||||
import ParameterItem from './parameter-item'
|
||||
import type { ParameterValue } from './parameter-item'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
|
||||
import { fetchModelParameterRules } from '@/service/common'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
||||
type ModelParameterModalProps = {
|
||||
isAdvancedMode: boolean
|
||||
mode: string
|
||||
modelId: string
|
||||
provider: string
|
||||
setModel: (model: { modelId: string; provider: string; mode?: string; features: string[] }) => void
|
||||
completionParams: FormValue
|
||||
onCompletionParamsChange: (newParams: FormValue) => void
|
||||
disabled: boolean
|
||||
}
|
||||
const stopParameerRule: ModelParameterRule = {
|
||||
default: [],
|
||||
help: {
|
||||
en_US: 'Up to four sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.',
|
||||
zh_Hans: '最多四个序列,API 将停止生成更多的 token。返回的文本将不包含停止序列。',
|
||||
},
|
||||
label: {
|
||||
en_US: 'Stop sequences',
|
||||
zh_Hans: '停止序列 stop_sequences',
|
||||
},
|
||||
name: 'stop',
|
||||
required: false,
|
||||
type: 'tag',
|
||||
tagPlaceholder: {
|
||||
en_US: 'Enter sequence and press Tab',
|
||||
zh_Hans: '输入序列并按 Tab 键',
|
||||
},
|
||||
}
|
||||
const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
isAdvancedMode,
|
||||
modelId,
|
||||
provider,
|
||||
setModel,
|
||||
completionParams,
|
||||
onCompletionParamsChange,
|
||||
disabled,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { data: parameterRulesData, isLoading } = useSWR(`/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}`, fetchModelParameterRules)
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
textGenerationModelList,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{ provider, model: modelId },
|
||||
)
|
||||
|
||||
const parameterRules = parameterRulesData?.data || []
|
||||
|
||||
const handleParamChange = (key: string, value: ParameterValue) => {
|
||||
onCompletionParamsChange({
|
||||
...completionParams,
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleChangeModel = ({ provider, model }: DefaultModel) => {
|
||||
const targetProvider = textGenerationModelList.find(modelItem => modelItem.provider === provider)
|
||||
const targetModelItem = targetProvider?.models.find(modelItem => modelItem.model === model)
|
||||
setModel({
|
||||
modelId: model,
|
||||
provider,
|
||||
mode: targetModelItem?.model_properties.mode as string,
|
||||
features: targetModelItem?.features || [],
|
||||
})
|
||||
}
|
||||
|
||||
const handleChangeParams = () => {
|
||||
const newCompletionParams = parameterRules.reduce((acc, parameter) => {
|
||||
if (parameter.default !== undefined)
|
||||
acc[parameter.name] = parameter.default
|
||||
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
|
||||
onCompletionParamsChange(newCompletionParams)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleChangeParams()
|
||||
}, [parameterRules])
|
||||
|
||||
const handleSwitch = (key: string, value: boolean, assignValue: ParameterValue) => {
|
||||
if (!value) {
|
||||
const newCompletionParams = { ...completionParams }
|
||||
delete newCompletionParams[key]
|
||||
|
||||
onCompletionParamsChange(newCompletionParams)
|
||||
}
|
||||
if (value) {
|
||||
onCompletionParamsChange({
|
||||
...completionParams,
|
||||
[key]: assignValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className='block'
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-2 h-8 rounded-lg border cursor-pointer hover:border-[1.5px]
|
||||
${disabled ? 'border-[#F79009] bg-[#FFFAEB]' : 'border-[#444CE7] bg-primary-50'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
currentProvider && (
|
||||
<ModelIcon
|
||||
className='mr-1.5 !w-5 !h-5'
|
||||
provider={currentProvider}
|
||||
modelName={currentModel?.model}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
currentModel && (
|
||||
<ModelName
|
||||
className='mr-1.5 text-gray-900'
|
||||
modelItem={currentModel}
|
||||
showMode={isAdvancedMode}
|
||||
modeClassName='!text-[#444CE7] !border-[#A4BCFD]'
|
||||
showFeatures={isAdvancedMode}
|
||||
featuresClassName='!text-[#444CE7] !border-[#A4BCFD]'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
disabled
|
||||
? (
|
||||
<AlertTriangle className='w-4 h-4 text-[#F79009]' />
|
||||
)
|
||||
: (
|
||||
<SlidersH className='w-4 h-4 text-indigo-600' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<div className='w-[496px] rounded-xl border border-gray-100 bg-white shadow-xl'>
|
||||
<div className='flex items-center px-4 h-12 rounded-t-xl border-b border-gray-100 bg-gray-50 text-md font-medium text-gray-900'>
|
||||
<CubeOutline className='mr-2 w-4 h-4 text-primary-600' />
|
||||
{t('common.modelProvider.modelAndParameters')}
|
||||
</div>
|
||||
<div className='px-10 pt-4 pb-8'>
|
||||
<div className='flex items-center justify-between h-8'>
|
||||
<div className='text-sm font-medium text-gray-900'>
|
||||
{t('common.modelProvider.model')}
|
||||
</div>
|
||||
<ModelSelector
|
||||
defaultModel={{ provider, model: modelId }}
|
||||
modelList={textGenerationModelList}
|
||||
onSelect={handleChangeModel}
|
||||
/>
|
||||
</div>
|
||||
<div className='my-5 h-[1px] bg-gray-100' />
|
||||
{
|
||||
isLoading && (
|
||||
<Loading />
|
||||
)
|
||||
}
|
||||
{
|
||||
!isLoading && (
|
||||
[
|
||||
...parameterRules,
|
||||
...(isAdvancedMode ? [stopParameerRule] : []),
|
||||
].map(parameter => (
|
||||
<ParameterItem
|
||||
key={parameter.name}
|
||||
className='mb-4'
|
||||
parameterRule={parameter}
|
||||
value={completionParams[parameter.name]}
|
||||
onChange={v => handleParamChange(parameter.name, v)}
|
||||
onSwitch={(checked, assignValue) => handleSwitch(parameter.name, checked, assignValue)}
|
||||
/>
|
||||
))
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelParameterModal
|
||||
@@ -0,0 +1,223 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import type { ModelParameterRule } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import { isNullOrUndefined } from '../utils'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import TagInput from '@/app/components/base/tag-input'
|
||||
|
||||
export type ParameterValue = number | string | string[] | boolean | undefined
|
||||
type ParameterItemProps = {
|
||||
parameterRule: ModelParameterRule
|
||||
value?: ParameterValue
|
||||
onChange?: (value: ParameterValue) => void
|
||||
className?: string
|
||||
onSwitch?: (checked: boolean, assignValue: ParameterValue) => void
|
||||
}
|
||||
const ParameterItem: FC<ParameterItemProps> = ({
|
||||
parameterRule,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
onSwitch,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const [localValue, setLocalValue] = useState(value)
|
||||
const mergedValue = isNullOrUndefined(value) ? localValue : value
|
||||
const renderValue = mergedValue === undefined ? parameterRule.default : mergedValue
|
||||
|
||||
const handleChange = (v: ParameterValue) => {
|
||||
setLocalValue(v)
|
||||
if (!isNullOrUndefined(value) && onChange)
|
||||
onChange(v)
|
||||
}
|
||||
|
||||
const handleNumberInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let num = +e.target.value
|
||||
|
||||
if (!isNullOrUndefined(parameterRule.max) && num > parameterRule.max!)
|
||||
num = parameterRule.max as number
|
||||
|
||||
if (!isNullOrUndefined(parameterRule.min) && num < parameterRule.min!)
|
||||
num = parameterRule.min as number
|
||||
|
||||
handleChange(num)
|
||||
}
|
||||
|
||||
const handleSlideChange = (num: number) => {
|
||||
handleChange(num)
|
||||
}
|
||||
|
||||
const handleRadioChange = (v: number) => {
|
||||
handleChange(v === 1)
|
||||
}
|
||||
|
||||
const handleStringInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleChange(e.target.value)
|
||||
}
|
||||
|
||||
const handleSelect = (option: { value: string | number; name: string }) => {
|
||||
handleChange(option.value)
|
||||
}
|
||||
|
||||
const handleTagChange = (newSequences: string[]) => {
|
||||
handleChange(newSequences)
|
||||
}
|
||||
|
||||
const handleSwitch = (checked: boolean) => {
|
||||
if (onSwitch) {
|
||||
let assignValue: ParameterValue = localValue
|
||||
|
||||
if (isNullOrUndefined(localValue)) {
|
||||
if (parameterRule.type === 'int' || parameterRule.type === 'float')
|
||||
assignValue = !isNullOrUndefined(parameterRule.default) ? parameterRule.default : 0
|
||||
|
||||
if (parameterRule.type === 'string' && !parameterRule.options?.length)
|
||||
assignValue = parameterRule.default || ''
|
||||
|
||||
if (parameterRule.type === 'string' && parameterRule.options?.length)
|
||||
assignValue = parameterRule.options[0]
|
||||
|
||||
if (parameterRule.type === 'boolean')
|
||||
assignValue = !isNullOrUndefined(parameterRule.default) ? parameterRule.default : false
|
||||
|
||||
if (parameterRule.type === 'tag')
|
||||
assignValue = !isNullOrUndefined(parameterRule.default) ? parameterRule.default : []
|
||||
}
|
||||
|
||||
onSwitch(checked, assignValue)
|
||||
}
|
||||
}
|
||||
|
||||
const numberInputWithSlide = (parameterRule.type === 'int' || parameterRule.type === 'float')
|
||||
&& !isNullOrUndefined(parameterRule.min)
|
||||
&& !isNullOrUndefined(parameterRule.max)
|
||||
const numberInput = (parameterRule.type === 'int' || parameterRule.type === 'float')
|
||||
&& (isNullOrUndefined(parameterRule.min) || isNullOrUndefined(parameterRule.max))
|
||||
|
||||
return (
|
||||
<div className={`flex items-center justify-between ${className}`}>
|
||||
<div>
|
||||
<div className='shrink-0 flex items-center w-[200px]'>
|
||||
<div
|
||||
className='mr-0.5 text-[13px] font-medium text-gray-700 truncate'
|
||||
title={parameterRule.label[language]}
|
||||
>
|
||||
{parameterRule.label[language]}
|
||||
</div>
|
||||
{
|
||||
parameterRule.help && (
|
||||
<Tooltip
|
||||
selector={`model-parameter-rule-${parameterRule.name}`}
|
||||
htmlContent={(
|
||||
<div className='w-[200px] whitespace-pre-wrap'>{parameterRule.help[language]}</div>
|
||||
)}
|
||||
>
|
||||
<HelpCircle className='mr-1.5 w-3.5 h-3.5 text-gray-400' />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
{
|
||||
!parameterRule.required && parameterRule.name !== 'stop' && (
|
||||
<Switch
|
||||
defaultValue={!isNullOrUndefined(value)}
|
||||
onChange={handleSwitch}
|
||||
size='md'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
parameterRule.type === 'tag' && (
|
||||
<div className='w-[200px] text-gray-400 text-xs font-normal'>
|
||||
{parameterRule?.tagPlaceholder?.[language]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
numberInputWithSlide && (
|
||||
<div className='flex items-center'>
|
||||
<Slider
|
||||
className='w-[120px]'
|
||||
value={isNullOrUndefined(renderValue) ? 0 : +renderValue!}
|
||||
min={parameterRule.min}
|
||||
max={parameterRule.max}
|
||||
step={+`0.${parameterRule.precision || 0}`}
|
||||
onChange={handleSlideChange}
|
||||
/>
|
||||
<input
|
||||
className='shrink-0 block ml-4 pl-3 w-16 h-8 appearance-none outline-none rounded-lg bg-gray-100 text-[13px] text-gra-900'
|
||||
type='number'
|
||||
max={parameterRule.max}
|
||||
min={parameterRule.min}
|
||||
step={+`0.${parameterRule.precision || 0}`}
|
||||
value={isNullOrUndefined(renderValue) ? 0 : +renderValue!}
|
||||
onChange={handleNumberInputChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
parameterRule.type === 'boolean' && (
|
||||
<Radio.Group
|
||||
className='w-[200px] flex items-center'
|
||||
value={isNullOrUndefined(renderValue) ? 1 : 0}
|
||||
onChange={handleRadioChange}
|
||||
>
|
||||
<Radio value={1} className='!mr-1 w-[94px]'>True</Radio>
|
||||
<Radio value={0} className='w-[94px]'>False</Radio>
|
||||
</Radio.Group>
|
||||
)
|
||||
}
|
||||
{
|
||||
numberInput && (
|
||||
<input
|
||||
type='number'
|
||||
className='flex items-center px-3 w-[200px] h-8 appearance-none outline-none rounded-lg bg-gray-100 text-[13px] text-gra-900'
|
||||
value={(isNullOrUndefined(renderValue) ? '' : renderValue) as string}
|
||||
onChange={handleNumberInputChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
parameterRule.type === 'string' && !parameterRule.options?.length && (
|
||||
<input
|
||||
className='flex items-center px-3 w-[200px] h-8 appearance-none outline-none rounded-lg bg-gray-100 text-[13px] text-gra-900'
|
||||
value={(isNullOrUndefined(renderValue) ? '' : renderValue) as string}
|
||||
onChange={handleStringInputChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
parameterRule.type === 'string' && parameterRule?.options?.length && (
|
||||
<SimpleSelect
|
||||
className='!py-0'
|
||||
wrapperClassName='!w-[200px] !h-8'
|
||||
defaultValue={renderValue as string}
|
||||
onSelect={handleSelect}
|
||||
items={parameterRule.options.map(option => ({ value: option, name: option }))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
parameterRule.type === 'tag' && (
|
||||
<div className='w-[200px]'>
|
||||
<TagInput
|
||||
items={isNullOrUndefined(renderValue) ? [] : (renderValue as string[])}
|
||||
onChange={handleTagChange}
|
||||
customizedConfirmKey='Tab'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ParameterItem
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { FC } from 'react'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
|
||||
|
||||
type ModelTriggerProps = {
|
||||
open: boolean
|
||||
className?: string
|
||||
}
|
||||
const ModelTrigger: FC<ModelTriggerProps> = ({
|
||||
open,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-2 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 cursor-pointer
|
||||
${className}
|
||||
${open && '!bg-gray-200'}
|
||||
`}
|
||||
>
|
||||
<div className='grow flex items-center'>
|
||||
<div className='mr-1.5 flex items-center justify-center w-4 h-4 rounded-[5px] border border-dashed border-black/5'>
|
||||
<CubeOutline className='w-3 h-3 text-gray-400' />
|
||||
</div>
|
||||
<div
|
||||
className='text-[13px] text-gray-500 truncate'
|
||||
title='Select model'
|
||||
>
|
||||
Select model
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0 flex items-center justify-center w-4 h-4'>
|
||||
<ChevronDown className='w-3.5 h-3.5 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelTrigger
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelBadge from '../model-badge'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
ModelFeatureTextEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
MagicBox,
|
||||
MagicEyes,
|
||||
MagicWand,
|
||||
Robot,
|
||||
} from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
|
||||
type FeatureIconProps = {
|
||||
feature: ModelFeatureEnum
|
||||
className?: string
|
||||
}
|
||||
const FeatureIcon: FC<FeatureIconProps> = ({
|
||||
className,
|
||||
feature,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (feature === ModelFeatureEnum.agentThought) {
|
||||
return (
|
||||
<TooltipPlus
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.agentThought })}
|
||||
>
|
||||
<ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
|
||||
<Robot className='w-3 h-3' />
|
||||
</ModelBadge>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
|
||||
if (feature === ModelFeatureEnum.toolCall) {
|
||||
return (
|
||||
<TooltipPlus
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.toolCall })}
|
||||
>
|
||||
<ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
|
||||
<MagicWand className='w-3 h-3' />
|
||||
</ModelBadge>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
|
||||
if (feature === ModelFeatureEnum.multiToolCall) {
|
||||
return (
|
||||
<TooltipPlus
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.multiToolCall })}
|
||||
>
|
||||
<ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
|
||||
<MagicBox className='w-3 h-3' />
|
||||
</ModelBadge>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
|
||||
if (feature === ModelFeatureEnum.vision) {
|
||||
return (
|
||||
<TooltipPlus
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.vision })}
|
||||
>
|
||||
<ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
|
||||
<MagicEyes className='w-3 h-3' />
|
||||
</ModelBadge>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default FeatureIcon
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import type {
|
||||
DefaultModel,
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import { useCurrentProviderAndModel } from '../hooks'
|
||||
import ModelTrigger from './model-trigger'
|
||||
import EmptyTrigger from './empty-trigger'
|
||||
import Popup from './popup'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
|
||||
type ModelSelectorProps = {
|
||||
defaultModel?: DefaultModel
|
||||
modelList: Model[]
|
||||
triggerClassName?: string
|
||||
popupClassName?: string
|
||||
onSelect?: (model: DefaultModel) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
const ModelSelector: FC<ModelSelectorProps> = ({
|
||||
defaultModel,
|
||||
modelList,
|
||||
triggerClassName,
|
||||
popupClassName,
|
||||
onSelect,
|
||||
readonly,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
} = useCurrentProviderAndModel(
|
||||
modelList,
|
||||
defaultModel,
|
||||
)
|
||||
|
||||
const handleSelect = (provider: string, model: ModelItem) => {
|
||||
setOpen(false)
|
||||
|
||||
if (onSelect)
|
||||
onSelect({ provider, model: model.model })
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
if (readonly)
|
||||
return
|
||||
|
||||
setOpen(v => !v)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={handleToggle}
|
||||
className='block'
|
||||
>
|
||||
{
|
||||
currentModel && currentProvider && (
|
||||
<ModelTrigger
|
||||
open={open}
|
||||
provider={currentProvider}
|
||||
model={currentModel}
|
||||
className={triggerClassName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentModel && (
|
||||
<EmptyTrigger
|
||||
open={open}
|
||||
className={triggerClassName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className={`z-[60] ${popupClassName}`}>
|
||||
<Popup
|
||||
defaultModel={defaultModel}
|
||||
modelList={modelList}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelSelector
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
// import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
type ModelTriggerProps = {
|
||||
open: boolean
|
||||
provider: Model
|
||||
model: ModelItem
|
||||
className?: string
|
||||
}
|
||||
const ModelTrigger: FC<ModelTriggerProps> = ({
|
||||
open,
|
||||
provider,
|
||||
model,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
group flex items-center px-2 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 cursor-pointer
|
||||
${className}
|
||||
${open && '!bg-gray-200'}
|
||||
`}
|
||||
>
|
||||
<ModelIcon
|
||||
className='shrink-0 mr-1.5'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
<ModelName
|
||||
className='grow'
|
||||
modelItem={model}
|
||||
showMode
|
||||
showFeatures
|
||||
/>
|
||||
<div className='shrink-0 flex items-center justify-center w-4 h-4'>
|
||||
<ChevronDown
|
||||
className='w-3.5 h-3.5 text-gray-500'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelTrigger
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
DefaultModel,
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import {
|
||||
useLanguage,
|
||||
useUpdateModelList,
|
||||
useUpdateModelProvidersAndModelList,
|
||||
} from '../hooks'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import {
|
||||
ConfigurateMethodEnum,
|
||||
MODEL_STATUS_TEXT,
|
||||
ModelStatusEnum,
|
||||
ModelTypeEnum,
|
||||
} from '../declarations'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type PopupItemProps = {
|
||||
defaultModel?: DefaultModel
|
||||
model: Model
|
||||
onSelect: (provider: string, model: ModelItem) => void
|
||||
}
|
||||
const PopupItem: FC<PopupItemProps> = ({
|
||||
defaultModel,
|
||||
model,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const { setShowModelModal } = useModalContext()
|
||||
const { modelProviders } = useProviderContext()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const updateModelProvidersAndModelList = useUpdateModelProvidersAndModelList()
|
||||
const currentProvider = modelProviders.find(provider => provider.provider === model.provider)!
|
||||
const handleSelect = (provider: string, modelItem: ModelItem) => {
|
||||
if (modelItem.status !== ModelStatusEnum.active)
|
||||
return
|
||||
|
||||
onSelect(provider, modelItem)
|
||||
}
|
||||
const handleOpenModelModal = () => {
|
||||
setShowModelModal({
|
||||
payload: {
|
||||
currentProvider,
|
||||
currentConfigurateMethod: ConfigurateMethodEnum.predefinedModel,
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
updateModelProvidersAndModelList()
|
||||
|
||||
const modelType = model.models[0].model_type
|
||||
|
||||
if (modelType !== ModelTypeEnum.textGeneration)
|
||||
updateModelList(modelType)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mb-1'>
|
||||
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>
|
||||
{model.label[language]}
|
||||
</div>
|
||||
{
|
||||
model.models.map(modelItem => (
|
||||
<Tooltip
|
||||
selector={`${modelItem.model}-${modelItem.status}`}
|
||||
key={modelItem.model}
|
||||
content={modelItem.status !== ModelStatusEnum.active ? MODEL_STATUS_TEXT[modelItem.status][language] : undefined}
|
||||
position='right'
|
||||
>
|
||||
<div
|
||||
key={modelItem.model}
|
||||
className={`
|
||||
group relative flex items-center px-3 py-1.5 h-8 rounded-lg
|
||||
${modelItem.status === ModelStatusEnum.active ? 'cursor-pointer hover:bg-gray-50' : 'cursor-not-allowed hover:bg-gray-50/60'}
|
||||
`}
|
||||
onClick={() => handleSelect(model.provider, modelItem)}
|
||||
>
|
||||
<ModelIcon
|
||||
className={`
|
||||
shrink-0 mr-2 w-4 h-4
|
||||
${modelItem.status !== ModelStatusEnum.active && 'opacity-60'}
|
||||
`}
|
||||
provider={model}
|
||||
modelName={modelItem.model}
|
||||
/>
|
||||
<ModelName
|
||||
className={`
|
||||
grow text-sm font-normal text-gray-900
|
||||
${modelItem.status !== ModelStatusEnum.active && 'opacity-60'}
|
||||
`}
|
||||
modelItem={modelItem}
|
||||
showMode
|
||||
showFeatures
|
||||
/>
|
||||
{
|
||||
defaultModel?.model === modelItem.model && (
|
||||
<Check className='shrink-0 w-4 h-4 text-primary-600' />
|
||||
)
|
||||
}
|
||||
{
|
||||
modelItem.status === ModelStatusEnum.noConfigure && (
|
||||
<div
|
||||
className='hidden group-hover:block text-xs font-medium text-primary-600 cursor-pointer'
|
||||
onClick={handleOpenModelModal}
|
||||
>
|
||||
{t('common.operation.add').toLocaleUpperCase()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PopupItem
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import type {
|
||||
DefaultModel,
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import PopupItem from './popup-item'
|
||||
import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
|
||||
type PopupProps = {
|
||||
defaultModel?: DefaultModel
|
||||
modelList: Model[]
|
||||
onSelect: (provider: string, model: ModelItem) => void
|
||||
}
|
||||
const Popup: FC<PopupProps> = ({
|
||||
defaultModel,
|
||||
modelList,
|
||||
onSelect,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
const filteredModelList = modelList.filter(model => model.models.filter(modelItem => modelItem.label[language].includes(searchText)).length)
|
||||
|
||||
return (
|
||||
<div className='w-[320px] max-h-[480px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg overflow-y-auto'>
|
||||
<div className='sticky top-0 pl-3 pt-3 pr-2 pb-1 bg-white z-10'>
|
||||
<div className={`
|
||||
flex items-center pl-[9px] pr-[10px] h-8 rounded-lg border
|
||||
${searchText ? 'bg-white border-gray-300 shadow-xs' : 'bg-gray-100 border-transparent'}
|
||||
`}>
|
||||
<SearchLg
|
||||
className={`
|
||||
shrink-0 mr-[7px] w-[14px] h-[14px]
|
||||
${searchText ? 'text-gray-500' : 'text-gray-400'}
|
||||
`}
|
||||
/>
|
||||
<input
|
||||
className='block grow h-[18px] text-[13px] appearance-none outline-none bg-transparent'
|
||||
placeholder='Search model'
|
||||
value={searchText}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
/>
|
||||
{
|
||||
searchText && (
|
||||
<XCircle
|
||||
className='shrink-0 ml-1.5 w-[14px] h-[14px] text-gray-400 cursor-pointer'
|
||||
onClick={() => setSearchText('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
{
|
||||
filteredModelList.map(model => (
|
||||
<PopupItem
|
||||
key={model.provider}
|
||||
defaultModel={defaultModel}
|
||||
model={model}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
!filteredModelList.length && (
|
||||
<div className='px-3 py-1.5 leading-[18px] text-center text-xs text-gray-500 break-all'>
|
||||
{`No model found for “${searchText}”`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Popup
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
|
||||
import { LinkExternal01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
const ModelTrigger = () => {
|
||||
return (
|
||||
<div className='flex items-center px-2 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 cursor-pointer'>
|
||||
<div className='grow flex items-center'>
|
||||
<div className='mr-1.5 flex items-center justify-center w-4 h-4 rounded-[5px] border-dashed border-black/5'>
|
||||
<CubeOutline className='w-[11px] h-[11px] text-gray-400' />
|
||||
</div>
|
||||
<div
|
||||
className='text-[13px] text-gray-500 truncate'
|
||||
title='Select model'
|
||||
>
|
||||
Please setup the Rerank model
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0 flex items-center justify-center w-4 h-4'>
|
||||
<LinkExternal01 className='w-3.5 h-3.5 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelTrigger
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
|
||||
type AddModelButtonProps = {
|
||||
className?: string
|
||||
onClick: () => void
|
||||
}
|
||||
const AddModelButton: FC<AddModelButtonProps> = ({
|
||||
className,
|
||||
onClick,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`
|
||||
shrink-0 flex items-center px-1.5 h-6 text-xs font-medium text-gray-500 cursor-pointer
|
||||
hover:bg-primary-50 hover:text-primary-600 rounded-md ${className}
|
||||
`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusCircle className='mr-1 w-3 h-3' />
|
||||
{t('common.modelProvider.addModel')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddModelButton
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import {
|
||||
CustomConfigurationStatusEnum,
|
||||
PreferredProviderTypeEnum,
|
||||
} from '../declarations'
|
||||
import { useUpdateModelList } from '../hooks'
|
||||
import PrioritySelector from './priority-selector'
|
||||
import PriorityUseTip from './priority-use-tip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { changeModelProviderPriority } from '@/service/common'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
|
||||
type CredentialPanelProps = {
|
||||
provider: ModelProvider
|
||||
onSetup: () => void
|
||||
}
|
||||
const CredentialPanel: FC<CredentialPanelProps> = ({
|
||||
provider,
|
||||
onSetup,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { mutate } = useSWRConfig()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const customConfig = provider.custom_configuration
|
||||
const systemConfig = provider.system_configuration
|
||||
const priorityUseType = provider.preferred_provider_type
|
||||
const customConfiged = customConfig.status === CustomConfigurationStatusEnum.active
|
||||
|
||||
const handleChangePriority = async (key: PreferredProviderTypeEnum) => {
|
||||
const res = await changeModelProviderPriority({
|
||||
url: `/workspaces/current/model-providers/${provider.provider}/preferred-provider-type`,
|
||||
body: {
|
||||
preferred_provider_type: key,
|
||||
},
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
mutate('/workspaces/current/model-providers')
|
||||
updateModelList(1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='shrink-0 relative ml-1 p-1 w-[112px] rounded-lg bg-white/[0.3] border-[0.5px] border-black/5'>
|
||||
<div className='flex items-center justify-between mb-1 pt-1 pl-2 pr-[7px] h-5 text-xs font-medium text-gray-500'>
|
||||
API-KEY
|
||||
<Indicator color={customConfiged ? 'green' : 'gray'} />
|
||||
</div>
|
||||
<div className='flex items-center gap-0.5'>
|
||||
<Button
|
||||
className='grow px-0 h-6 bg-white text-xs font-medium rounded-md'
|
||||
onClick={onSetup}
|
||||
>
|
||||
<Settings01 className='mr-1 w-3 h-3' />
|
||||
{t('common.operation.setup')}
|
||||
</Button>
|
||||
{
|
||||
systemConfig.enabled && customConfiged && (
|
||||
<PrioritySelector
|
||||
value={priorityUseType}
|
||||
onSelect={handleChangePriority}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
priorityUseType === PreferredProviderTypeEnum.custom && systemConfig.enabled && (
|
||||
<PriorityUseTip />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CredentialPanel
|
||||
@@ -0,0 +1,161 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
CustomConfigrationModelFixedFields,
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { ConfigurateMethodEnum } from '../declarations'
|
||||
import {
|
||||
DEFAULT_BACKGROUND_COLOR,
|
||||
modelTypeFormat,
|
||||
} from '../utils'
|
||||
import ProviderIcon from '../provider-icon'
|
||||
import ModelBadge from '../model-badge'
|
||||
import CredentialPanel from './credential-panel'
|
||||
import QuotaPanel from './quota-panel'
|
||||
import ModelList from './model-list'
|
||||
import AddModelButton from './add-model-button'
|
||||
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { fetchModelProviderModelList } from '@/service/common'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
|
||||
type ProviderAddedCardProps = {
|
||||
provider: ModelProvider
|
||||
onOpenModal: (configurateMethod: ConfigurateMethodEnum, currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void
|
||||
}
|
||||
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
provider,
|
||||
onOpenModal,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [fetched, setFetched] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
const [modelList, setModelList] = useState<ModelItem[]>([])
|
||||
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
|
||||
const systemConfig = provider.system_configuration
|
||||
const hasModelList = fetched && !!modelList.length
|
||||
const showQuota = systemConfig.enabled || ['minimax', 'spark', 'zhipuai', 'anthropic'].includes(provider.provider)
|
||||
|
||||
const getModelList = async (providerName: string) => {
|
||||
if (loading)
|
||||
return
|
||||
try {
|
||||
setLoading(true)
|
||||
const modelsData = await fetchModelProviderModelList(`/workspaces/current/model-providers/${providerName}/models`)
|
||||
setModelList(modelsData.data)
|
||||
setCollapsed(false)
|
||||
setFetched(true)
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
const handleOpenModelList = () => {
|
||||
if (fetched) {
|
||||
setCollapsed(false)
|
||||
return
|
||||
}
|
||||
|
||||
getModelList(provider.provider)
|
||||
}
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v?.type === UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST && v.payload === provider.provider)
|
||||
getModelList(v.payload)
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className='mb-2 rounded-xl border-[0.5px] border-black/5 shadow-xs'
|
||||
style={{ background: provider.background || DEFAULT_BACKGROUND_COLOR }}
|
||||
>
|
||||
<div className='flex pl-3 py-2 pr-2 rounded-t-xl'>
|
||||
<div className='grow px-1 pt-1 pb-0.5'>
|
||||
<ProviderIcon
|
||||
className='mb-2'
|
||||
provider={provider}
|
||||
/>
|
||||
<div className='flex gap-0.5'>
|
||||
{
|
||||
provider.supported_model_types.map(modelType => (
|
||||
<ModelBadge key={modelType}>
|
||||
{modelTypeFormat(modelType)}
|
||||
</ModelBadge>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showQuota && (
|
||||
<QuotaPanel
|
||||
provider={provider}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
configurateMethods.includes(ConfigurateMethodEnum.predefinedModel) && (
|
||||
<CredentialPanel
|
||||
onSetup={() => onOpenModal(ConfigurateMethodEnum.predefinedModel)}
|
||||
provider={provider}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
collapsed && (
|
||||
<div className='group flex items-center justify-between pl-2 py-1.5 pr-[11px] border-t border-t-black/5 bg-white/30 text-xs font-medium text-gray-500'>
|
||||
<div className='group-hover:hidden pl-1 pr-1.5 h-6 leading-6'>
|
||||
{
|
||||
hasModelList
|
||||
? t('common.modelProvider.modelsNum', { num: modelList.length })
|
||||
: t('common.modelProvider.showModels')
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className='hidden group-hover:flex items-center pl-1 pr-1.5 h-6 rounded-lg hover:bg-white cursor-pointer'
|
||||
onClick={handleOpenModelList}
|
||||
>
|
||||
<ChevronDownDouble className='mr-0.5 w-3 h-3' />
|
||||
{
|
||||
hasModelList
|
||||
? t('common.modelProvider.showModelsNum', { num: modelList.length })
|
||||
: t('common.modelProvider.showModels')
|
||||
}
|
||||
{
|
||||
loading && (
|
||||
<Loading02 className='ml-0.5 animate-spin w-3 h-3' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
configurateMethods.includes(ConfigurateMethodEnum.customizableModel) && (
|
||||
<AddModelButton
|
||||
onClick={() => onOpenModal(ConfigurateMethodEnum.customizableModel)}
|
||||
className='hidden group-hover:flex group-hover:text-primary-600'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!collapsed && (
|
||||
<ModelList
|
||||
provider={provider}
|
||||
models={modelList}
|
||||
onCollapse={() => setCollapsed(true)}
|
||||
onConfig={currentCustomConfigrationModelFixedFields => onOpenModal(ConfigurateMethodEnum.customizableModel, currentCustomConfigrationModelFixedFields)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderAddedCard
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
CustomConfigrationModelFixedFields,
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurateMethodEnum,
|
||||
ModelStatusEnum,
|
||||
} from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
// import Tab from './tab'
|
||||
import AddModelButton from './add-model-button'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type ModelListProps = {
|
||||
provider: ModelProvider
|
||||
models: ModelItem[]
|
||||
onCollapse: () => void
|
||||
onConfig: (currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void
|
||||
}
|
||||
const ModelList: FC<ModelListProps> = ({
|
||||
provider,
|
||||
models,
|
||||
onCollapse,
|
||||
onConfig,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
|
||||
const canCustomConfig = configurateMethods.includes(ConfigurateMethodEnum.customizableModel)
|
||||
// const canSystemConfig = configurateMethods.includes(ConfigurateMethodEnum.predefinedModel)
|
||||
|
||||
return (
|
||||
<div className='px-2 pb-2 rounded-b-xl'>
|
||||
<div className='py-1 bg-white rounded-lg'>
|
||||
<div className='flex items-center pl-1 pr-[3px]'>
|
||||
<span className='group shrink-0 flex items-center mr-2'>
|
||||
<span className='group-hover:hidden pl-1 pr-1.5 h-6 leading-6 text-xs font-medium text-gray-500'>
|
||||
{t('common.modelProvider.modelsNum', { num: models.length })}
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
hidden group-hover:inline-flex items-center pl-1 pr-1.5 h-6 bg-gray-50
|
||||
text-xs font-medium text-gray-500 cursor-pointer rounded-lg
|
||||
`}
|
||||
onClick={() => onCollapse()}
|
||||
>
|
||||
<ChevronDownDouble className='mr-0.5 w-3 h-3 rotate-180' />
|
||||
{t('common.modelProvider.collapse')}
|
||||
</span>
|
||||
</span>
|
||||
{/* {
|
||||
canCustomConfig && canSystemConfig && (
|
||||
<span className='flex items-center'>
|
||||
<Tab active='all' onSelect={() => {}} />
|
||||
</span>
|
||||
)
|
||||
} */}
|
||||
{
|
||||
canCustomConfig && (
|
||||
<div className='grow flex justify-end'>
|
||||
<AddModelButton onClick={() => onConfig()} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
models.map(model => (
|
||||
<div
|
||||
key={model.model}
|
||||
className={`
|
||||
group flex items-center pl-2 pr-2.5 h-8 rounded-lg
|
||||
${canCustomConfig && 'hover:bg-gray-50'}
|
||||
`}
|
||||
>
|
||||
<div className='shrink-0 mr-2' style={{ background: provider.icon_small[language] }} />
|
||||
<ModelIcon
|
||||
className='shrink-0 mr-2'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
<ModelName
|
||||
className='grow text-sm font-normal text-gray-900'
|
||||
modelItem={model}
|
||||
showModelType
|
||||
showMode
|
||||
showContextSize
|
||||
/>
|
||||
<div className='shrink-0 flex items-center'>
|
||||
{
|
||||
model.fetch_from === ConfigurateMethodEnum.customizableModel && (
|
||||
<Button
|
||||
className='hidden group-hover:flex py-0 h-7 text-xs font-medium text-gray-700'
|
||||
onClick={() => onConfig({ __model_name: model.model, __model_type: model.model_type })}
|
||||
>
|
||||
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
|
||||
{t('common.modelProvider.config')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Indicator
|
||||
className='ml-2.5'
|
||||
color={model.status === ModelStatusEnum.active ? 'green' : 'gray'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelList
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Fragment } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PreferredProviderTypeEnum } from '../declarations'
|
||||
import { Check, DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type SelectorProps = {
|
||||
value?: string
|
||||
onSelect: (key: PreferredProviderTypeEnum) => void
|
||||
}
|
||||
const Selector: FC<SelectorProps> = ({
|
||||
value,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const options = [
|
||||
{
|
||||
key: PreferredProviderTypeEnum.custom,
|
||||
text: 'API',
|
||||
},
|
||||
{
|
||||
key: PreferredProviderTypeEnum.system,
|
||||
text: t('common.modelProvider.quota'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Popover className='relative'>
|
||||
<Popover.Button>
|
||||
{
|
||||
({ open }) => (
|
||||
<Button className={`
|
||||
px-0 w-6 h-6 bg-white rounded-md
|
||||
${open && '!bg-gray-100'}
|
||||
`}>
|
||||
<DotsHorizontal className='w-3 h-3 text-gray-700' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave='transition ease-in duration-100'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<Popover.Panel className='absolute top-7 right-0 w-[144px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg z-10'>
|
||||
<div className='p-1'>
|
||||
<div className='px-3 pt-2 pb-1 text-sm font-medium text-gray-700'>{t('common.modelProvider.card.priorityUse')}</div>
|
||||
{
|
||||
options.map(option => (
|
||||
<Popover.Button as={Fragment} key={option.key}>
|
||||
<div
|
||||
className='flex items-center justify-between px-3 h-9 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
|
||||
onClick={() => onSelect(option.key)}
|
||||
>
|
||||
<div className='grow'>{option.text}</div>
|
||||
{value === option.key && <Check className='w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
</Popover.Button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default Selector
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
const PriorityUseTip = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
selector='provider-quota-credential-priority-using'
|
||||
content={t('common.modelProvider.priorityUsing') || ''}
|
||||
>
|
||||
<div className='absolute -right-[5px] -top-[5px] bg-indigo-50 rounded-[5px] border-[0.5px] border-indigo-100 cursor-pointer'>
|
||||
<ChevronDownDouble className='rotate-180 w-3 h-3 text-indigo-600' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriorityUseTip
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { FC } from 'react'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import {
|
||||
CustomConfigurationStatusEnum,
|
||||
PreferredProviderTypeEnum,
|
||||
QuotaUnitEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
useAnthropicBuyQuota,
|
||||
useFreeQuota,
|
||||
} from '../hooks'
|
||||
import PriorityUseTip from './priority-use-tip'
|
||||
import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type QuotaPanelProps = {
|
||||
provider: ModelProvider
|
||||
}
|
||||
const QuotaPanel: FC<QuotaPanelProps> = ({
|
||||
provider,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { mutate } = useSWRConfig()
|
||||
const handlePay = useAnthropicBuyQuota()
|
||||
const handleFreeQuotaSuccess = () => {
|
||||
mutate('/workspaces/current/model-providers')
|
||||
}
|
||||
const handleFreeQuota = useFreeQuota(handleFreeQuotaSuccess)
|
||||
const customConfig = provider.custom_configuration
|
||||
const priorityUseType = provider.preferred_provider_type
|
||||
const systemConfig = provider.system_configuration
|
||||
const currentQuota = systemConfig.enabled && systemConfig.quota_configurations.find(item => item.quota_type === systemConfig.current_quota_type)
|
||||
|
||||
return (
|
||||
<div className='group relative shrink-0 min-w-[112px] px-3 py-2 rounded-lg bg-white/[0.3] border-[0.5px] border-black/5'>
|
||||
<div className='flex items-center mb-2 h-4 text-xs font-medium text-gray-500'>
|
||||
{t('common.modelProvider.quota')}
|
||||
<InfoCircle className='ml-0.5 w-3 h-3 text-gray-400' />
|
||||
</div>
|
||||
{
|
||||
currentQuota && (
|
||||
<div className='flex items-center h-4 text-xs text-gray-500'>
|
||||
<span className='mr-0.5 text-sm font-semibold text-gray-700'>{(currentQuota?.quota_limit || 0) - (currentQuota?.quota_used || 0)}</span>
|
||||
{
|
||||
currentQuota?.quota_unit === QuotaUnitEnum.tokens && 'Tokens'
|
||||
}
|
||||
{
|
||||
currentQuota?.quota_unit === QuotaUnitEnum.times && t('common.modelProvider.callTimes')
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentQuota && provider.provider === 'anthropic' && (
|
||||
<Button
|
||||
className='h-6 bg-white text-xs font-medium rounded-md'
|
||||
onClick={handlePay}
|
||||
>
|
||||
{t('common.modelProvider.buyQuota')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentQuota && ['minimax', 'spark', 'zhipuai'].includes(provider.provider) && (
|
||||
<Button
|
||||
className='h-6 bg-white text-xs font-medium rounded-md'
|
||||
onClick={() => handleFreeQuota(provider.provider)}
|
||||
>
|
||||
{t('common.modelProvider.getFreeTokens')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{
|
||||
provider.provider === 'anthropic' && systemConfig.enabled && (
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 bottom-0 hidden group-hover:flex items-center justify-center
|
||||
w-full h-[30px] backdrop-blur-[2px] bg-gradient-to-r from-[rgba(238,244,255,0.80)] to-[rgba(237,237,240,0.70)]
|
||||
text-xs font-medium text-primary-600 cursor-pointer rounded-b-lg
|
||||
`}
|
||||
onClick={handlePay}
|
||||
>
|
||||
{t('common.modelProvider.buyQuota')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
priorityUseType === PreferredProviderTypeEnum.system && customConfig.status === CustomConfigurationStatusEnum.active && (
|
||||
<PriorityUseTip />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuotaPanel
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { FC } from 'react'
|
||||
|
||||
type TabProps = {
|
||||
active: string
|
||||
onSelect: (active: string) => void
|
||||
}
|
||||
const Tab: FC<TabProps> = ({
|
||||
active,
|
||||
onSelect,
|
||||
}) => {
|
||||
const tabs = [
|
||||
{
|
||||
key: 'all',
|
||||
text: 'All',
|
||||
},
|
||||
{
|
||||
key: 'added',
|
||||
text: 'Added',
|
||||
},
|
||||
{
|
||||
key: 'build-in',
|
||||
text: 'Build-in',
|
||||
},
|
||||
]
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={`
|
||||
flex items-center mr-1 px-[5px] h-[18px] rounded-md text-xs cursor-pointer
|
||||
${active === tab.key ? 'bg-gray-200 font-medium text-gray-900' : 'text-gray-500 font-normal'}
|
||||
`}
|
||||
onClick={() => onSelect(tab.key)}
|
||||
>
|
||||
{tab.text}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tab
|
||||
@@ -0,0 +1,4 @@
|
||||
.vender {
|
||||
background: linear-gradient(131deg, #2250F2 0%, #0EBCF3 100%);
|
||||
background-clip: text;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { FC } from 'react'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
ModelProvider,
|
||||
TypeWithI18N,
|
||||
} from '../declarations'
|
||||
import { ConfigurateMethodEnum } from '../declarations'
|
||||
import {
|
||||
DEFAULT_BACKGROUND_COLOR,
|
||||
modelTypeFormat,
|
||||
} from '../utils'
|
||||
import {
|
||||
useAnthropicBuyQuota,
|
||||
useFreeQuota,
|
||||
useLanguage,
|
||||
} from '../hooks'
|
||||
import ModelBadge from '../model-badge'
|
||||
import ProviderIcon from '../provider-icon'
|
||||
import s from './index.module.css'
|
||||
import { Plus, Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { CoinsStacked01 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type ProviderCardProps = {
|
||||
provider: ModelProvider
|
||||
onOpenModal: (configurateMethod: ConfigurateMethodEnum) => void
|
||||
}
|
||||
|
||||
const TIP_MAP: { [k: string]: TypeWithI18N } = {
|
||||
minimax: {
|
||||
en_US: 'Earn 1 million tokens for free',
|
||||
zh_Hans: '免费获取 100 万个 token',
|
||||
},
|
||||
spark: {
|
||||
en_US: 'Earn 3 million tokens (v3.0) for free',
|
||||
zh_Hans: '免费获取 300 万个 token (v3.0)',
|
||||
},
|
||||
zhipuai: {
|
||||
en_US: 'Earn 10 million tokens for free',
|
||||
zh_Hans: '免费获取 1000 万个 token',
|
||||
},
|
||||
}
|
||||
const ProviderCard: FC<ProviderCardProps> = ({
|
||||
provider,
|
||||
onOpenModal,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const { mutate } = useSWRConfig()
|
||||
const handlePay = useAnthropicBuyQuota()
|
||||
const handleFreeQuotaSuccess = () => {
|
||||
mutate('/workspaces/current/model-providers')
|
||||
}
|
||||
const handleFreeQuota = useFreeQuota(handleFreeQuotaSuccess)
|
||||
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
|
||||
const canGetFreeQuota = ['mininmax', 'spark', 'zhipuai'].includes(provider.provider)
|
||||
|
||||
return (
|
||||
<div
|
||||
className='group relative flex flex-col justify-between px-4 py-3 h-[148px] border-[0.5px] border-black/5 rounded-xl shadow-xs hover:shadow-lg'
|
||||
style={{ background: provider.background || DEFAULT_BACKGROUND_COLOR }}
|
||||
>
|
||||
<div>
|
||||
<div className='py-0.5'>
|
||||
<ProviderIcon provider={provider} />
|
||||
</div>
|
||||
{
|
||||
provider.description && (
|
||||
<div className='mt-1 leading-4 text-xs text-black/[48]'>{provider.description[language]}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<div className={`flex flex-wrap group-hover:hidden gap-0.5 ${canGetFreeQuota && 'pb-[18px]'}`}>
|
||||
{
|
||||
provider.supported_model_types.map(modelType => (
|
||||
<ModelBadge key={modelType}>
|
||||
{modelTypeFormat(modelType)}
|
||||
</ModelBadge>
|
||||
))
|
||||
}
|
||||
{
|
||||
canGetFreeQuota && (
|
||||
<div className='absolute left-0 right-0 bottom-0 flex items-center h-[26px] px-4 bg-white/50 rounded-b-xl'>
|
||||
📣
|
||||
<div
|
||||
className={`${s.vender} text-xs font-medium text-transparent truncate`}
|
||||
title={TIP_MAP[provider.provider][language]}
|
||||
>
|
||||
{TIP_MAP[provider.provider][language]}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
canGetFreeQuota && (
|
||||
<div className='hidden group-hover:block'>
|
||||
<Button
|
||||
className='mb-1 w-full h-7 text-xs'
|
||||
type='primary'
|
||||
onClick={() => handleFreeQuota(provider.provider)}
|
||||
>
|
||||
{t('common.modelProvider.getFreeTokens')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={`hidden group-hover:grid grid-cols-${configurateMethods.length} gap-1`}>
|
||||
{
|
||||
configurateMethods.map((method) => {
|
||||
if (method === ConfigurateMethodEnum.predefinedModel) {
|
||||
return (
|
||||
<Button
|
||||
key={method}
|
||||
className='h-7 bg-white text-xs text-gray-700'
|
||||
onClick={() => onOpenModal(method)}
|
||||
>
|
||||
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
|
||||
{t('common.operation.setup')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={method}
|
||||
className='px-0 h-7 bg-white text-xs text-gray-700'
|
||||
onClick={() => onOpenModal(method)}
|
||||
>
|
||||
<Plus className='mr-[5px] w-3.5 h-3.5' />
|
||||
{t('common.modelProvider.addModel')}
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
}
|
||||
{
|
||||
provider.provider === 'anthropic' && (
|
||||
<Button
|
||||
className='h-7 text-xs text-gray-700'
|
||||
onClick={handlePay}
|
||||
>
|
||||
<CoinsStacked01 className='mr-[5px] w-3.5 h-3.5' />
|
||||
{t('common.modelProvider.buyQuota')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderCard
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
|
||||
type ProviderIconProps = {
|
||||
provider: ModelProvider
|
||||
className?: string
|
||||
}
|
||||
const ProviderIcon: FC<ProviderIconProps> = ({
|
||||
provider,
|
||||
className,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
|
||||
if (provider.icon_large) {
|
||||
return (
|
||||
<img
|
||||
alt='provider-icon'
|
||||
src={`${provider.icon_large[language]}?_token=${localStorage.getItem('console_token')}`}
|
||||
className={`w-auto h-6 ${className}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-center ${className}`}>
|
||||
<div className='text-xs font-semibold text-black'>
|
||||
{provider.label[language]}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderIcon
|
||||
@@ -0,0 +1,231 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelSelector from '../model-selector'
|
||||
import {
|
||||
useModelList,
|
||||
useSystemDefaultModelAndModelList,
|
||||
useUpdateModelList,
|
||||
} from '../hooks'
|
||||
import type {
|
||||
DefaultModel,
|
||||
DefaultModelResponse,
|
||||
} from '../declarations'
|
||||
import { ModelTypeEnum } from '../declarations'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { HelpCircle, Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { updateDefaultModel } from '@/service/common'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
|
||||
type SystemModelSelectorProps = {
|
||||
textGenerationDefaultModel: DefaultModelResponse | undefined
|
||||
embeddingsDefaultModel: DefaultModelResponse | undefined
|
||||
rerankDefaultModel: DefaultModelResponse | undefined
|
||||
speech2textDefaultModel: DefaultModelResponse | undefined
|
||||
}
|
||||
const SystemModel: FC<SystemModelSelectorProps> = ({
|
||||
textGenerationDefaultModel,
|
||||
embeddingsDefaultModel,
|
||||
rerankDefaultModel,
|
||||
speech2textDefaultModel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const { data: embeddingModelList } = useModelList(2)
|
||||
const { data: rerankModelList } = useModelList(3)
|
||||
const { data: speech2textModelList } = useModelList(4)
|
||||
const [changedModelTypes, setChangedModelTypes] = useState<ModelTypeEnum[]>([])
|
||||
const [currentTextGenerationDefaultModel, changeCurrentTextGenerationDefaultModel] = useSystemDefaultModelAndModelList(textGenerationDefaultModel, textGenerationModelList)
|
||||
const [currentEmbeddingsDefaultModel, changeCurrentEmbeddingsDefaultModel] = useSystemDefaultModelAndModelList(embeddingsDefaultModel, embeddingModelList)
|
||||
const [currentRerankDefaultModel, changeCurrentRerankDefaultModel] = useSystemDefaultModelAndModelList(rerankDefaultModel, rerankModelList)
|
||||
const [currentSpeech2textDefaultModel, changeCurrentSpeech2textDefaultModel] = useSystemDefaultModelAndModelList(speech2textDefaultModel, speech2textModelList)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const getCurrentDefaultModelByModelType = (modelType: ModelTypeEnum) => {
|
||||
if (modelType === ModelTypeEnum.textGeneration)
|
||||
return currentTextGenerationDefaultModel
|
||||
else if (modelType === ModelTypeEnum.textEmbedding)
|
||||
return currentEmbeddingsDefaultModel
|
||||
else if (modelType === ModelTypeEnum.rerank)
|
||||
return currentRerankDefaultModel
|
||||
else if (modelType === ModelTypeEnum.speech2text)
|
||||
return currentSpeech2textDefaultModel
|
||||
|
||||
return undefined
|
||||
}
|
||||
const handleChangeDefaultModel = (modelType: ModelTypeEnum, model: DefaultModel) => {
|
||||
if (modelType === ModelTypeEnum.textGeneration)
|
||||
changeCurrentTextGenerationDefaultModel(model)
|
||||
else if (modelType === ModelTypeEnum.textEmbedding)
|
||||
changeCurrentEmbeddingsDefaultModel(model)
|
||||
else if (modelType === ModelTypeEnum.rerank)
|
||||
changeCurrentRerankDefaultModel(model)
|
||||
else if (modelType === ModelTypeEnum.speech2text)
|
||||
changeCurrentSpeech2textDefaultModel(model)
|
||||
|
||||
if (!changedModelTypes.includes(modelType))
|
||||
setChangedModelTypes([...changedModelTypes, modelType])
|
||||
}
|
||||
const handleSave = async () => {
|
||||
const res = await updateDefaultModel({
|
||||
url: '/workspaces/current/default-model',
|
||||
body: {
|
||||
model_settings: [ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank, ModelTypeEnum.speech2text].map((modelType) => {
|
||||
return {
|
||||
model_type: modelType,
|
||||
provider: getCurrentDefaultModelByModelType(modelType)?.provider,
|
||||
model: getCurrentDefaultModelByModelType(modelType)?.model,
|
||||
}
|
||||
}),
|
||||
},
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
setOpen(false)
|
||||
|
||||
changedModelTypes.forEach((modelType) => {
|
||||
if (modelType === ModelTypeEnum.textGeneration)
|
||||
updateModelList(modelType)
|
||||
else if (modelType === ModelTypeEnum.textEmbedding)
|
||||
updateModelList(modelType)
|
||||
else if (modelType === ModelTypeEnum.rerank)
|
||||
updateModelList(modelType)
|
||||
else if (modelType === ModelTypeEnum.speech2text)
|
||||
updateModelList(modelType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 8,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<div className={`
|
||||
flex items-center px-2 h-6 text-xs text-gray-700 cursor-pointer bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs
|
||||
hover:bg-gray-100 hover:shadow-none
|
||||
${open && 'bg-gray-100 shadow-none'}
|
||||
`}>
|
||||
<Settings01 className='mr-1 w-3 h-3 text-gray-500' />
|
||||
{t('common.modelProvider.systemModelSettings')}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-50'>
|
||||
<div className='pt-4 w-[360px] rounded-xl border-[0.5px] border-black/5 bg-white shadow-xl'>
|
||||
<div className='px-6 py-1'>
|
||||
<div className='flex items-center h-8 text-[13px] font-medium text-gray-900'>
|
||||
{t('common.modelProvider.systemReasoningModel.key')}
|
||||
<Tooltip
|
||||
selector='model-page-system-reasoning-model-tip'
|
||||
htmlContent={
|
||||
<div className='w-[261px] text-gray-500'>{t('common.modelProvider.systemReasoningModel.tip')}</div>
|
||||
}
|
||||
>
|
||||
<HelpCircle className='ml-0.5 w-[14px] h-[14px] text-gray-400' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
defaultModel={currentTextGenerationDefaultModel}
|
||||
modelList={textGenerationModelList}
|
||||
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.textGeneration, model)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 py-1'>
|
||||
<div className='flex items-center h-8 text-[13px] font-medium text-gray-900'>
|
||||
{t('common.modelProvider.embeddingModel.key')}
|
||||
<Tooltip
|
||||
selector='model-page-system-embedding-model-tip'
|
||||
htmlContent={
|
||||
<div className='w-[261px] text-gray-500'>{t('common.modelProvider.embeddingModel.tip')}</div>
|
||||
}
|
||||
>
|
||||
<HelpCircle className='ml-0.5 w-[14px] h-[14px] text-gray-400' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
defaultModel={currentEmbeddingsDefaultModel}
|
||||
modelList={embeddingModelList}
|
||||
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.textEmbedding, model)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 py-1'>
|
||||
<div className='flex items-center h-8 text-[13px] font-medium text-gray-900'>
|
||||
{t('common.modelProvider.rerankModel.key')}
|
||||
<Tooltip
|
||||
selector='model-page-system-rerankModel-model-tip'
|
||||
htmlContent={
|
||||
<div className='w-[261px] text-gray-500'>{t('common.modelProvider.rerankModel.tip')}</div>
|
||||
}
|
||||
>
|
||||
<HelpCircle className='ml-0.5 w-[14px] h-[14px] text-gray-400' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
defaultModel={currentRerankDefaultModel}
|
||||
modelList={rerankModelList}
|
||||
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.rerank, model)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 py-1'>
|
||||
<div className='flex items-center h-8 text-[13px] font-medium text-gray-900'>
|
||||
{t('common.modelProvider.speechToTextModel.key')}
|
||||
<Tooltip
|
||||
selector='model-page-system-speechToText-model-tip'
|
||||
htmlContent={
|
||||
<div className='w-[261px] text-gray-500'>{t('common.modelProvider.speechToTextModel.tip')}</div>
|
||||
}
|
||||
>
|
||||
<HelpCircle className='ml-0.5 w-[14px] h-[14px] text-gray-400' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
defaultModel={currentSpeech2textDefaultModel}
|
||||
modelList={speech2textModelList}
|
||||
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.speech2text, model)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-end px-6 py-4'>
|
||||
<Button
|
||||
className='mr-2 !h-8 !text-[13px]'
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
className='!h-8 !text-[13px]'
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default SystemModel
|
||||
@@ -0,0 +1,159 @@
|
||||
import { ValidatedStatus } from '../key-validator/declarations'
|
||||
import type {
|
||||
CredentialFormSchemaRadio,
|
||||
CredentialFormSchemaTextInput,
|
||||
FormValue,
|
||||
} from './declarations'
|
||||
import {
|
||||
FormTypeEnum,
|
||||
MODEL_TYPE_TEXT,
|
||||
ModelTypeEnum,
|
||||
} from './declarations'
|
||||
import {
|
||||
deleteModelProvider,
|
||||
setModelProvider,
|
||||
validateModelProvider,
|
||||
} from '@/service/common'
|
||||
|
||||
export const languageMaps = {
|
||||
'en': 'en_US',
|
||||
'zh-Hans': 'zh_Hans',
|
||||
} as {
|
||||
'en': 'en_US'
|
||||
'zh-Hans': 'zh_Hans'
|
||||
}
|
||||
|
||||
export const DEFAULT_BACKGROUND_COLOR = '#F3F4F6'
|
||||
|
||||
export const isNullOrUndefined = (value: any) => {
|
||||
return value === undefined || value === null
|
||||
}
|
||||
|
||||
export const validateCredentials = async (predefined: boolean, provider: string, v: FormValue) => {
|
||||
let body, url
|
||||
|
||||
if (predefined) {
|
||||
body = {
|
||||
credentials: v,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${provider}/credentials/validate`
|
||||
}
|
||||
else {
|
||||
const { __model_name, __model_type, ...credentials } = v
|
||||
body = {
|
||||
model: __model_name,
|
||||
model_type: __model_type,
|
||||
credentials,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${provider}/models/credentials/validate`
|
||||
}
|
||||
try {
|
||||
const res = await validateModelProvider({ url, body })
|
||||
if (res.result === 'success')
|
||||
return Promise.resolve({ status: ValidatedStatus.Success })
|
||||
else
|
||||
return Promise.resolve({ status: ValidatedStatus.Error, message: res.error || 'error' })
|
||||
}
|
||||
catch (e: any) {
|
||||
return Promise.resolve({ status: ValidatedStatus.Error, message: e.message })
|
||||
}
|
||||
}
|
||||
|
||||
export const saveCredentials = async (predefined: boolean, provider: string, v: FormValue) => {
|
||||
let body, url
|
||||
|
||||
if (predefined) {
|
||||
body = {
|
||||
credentials: v,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${provider}`
|
||||
}
|
||||
else {
|
||||
const { __model_name, __model_type, ...credentials } = v
|
||||
body = {
|
||||
model: __model_name,
|
||||
model_type: __model_type,
|
||||
credentials,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${provider}/models`
|
||||
}
|
||||
|
||||
return setModelProvider({ url, body })
|
||||
}
|
||||
|
||||
export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue) => {
|
||||
let url = ''
|
||||
let body
|
||||
|
||||
if (predefined) {
|
||||
url = `/workspaces/current/model-providers/${provider}`
|
||||
}
|
||||
else {
|
||||
if (v) {
|
||||
const { __model_name, __model_type } = v
|
||||
body = {
|
||||
model: __model_name,
|
||||
model_type: __model_type,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${provider}/models`
|
||||
}
|
||||
}
|
||||
|
||||
return deleteModelProvider({ url, body })
|
||||
}
|
||||
|
||||
export const sizeFormat = (size: number) => {
|
||||
const remainder = Math.floor(size / 1000)
|
||||
if (remainder < 1)
|
||||
return `${size}`
|
||||
else
|
||||
return `${remainder}K`
|
||||
}
|
||||
|
||||
export const modelTypeFormat = (modelType: ModelTypeEnum) => {
|
||||
if (modelType === ModelTypeEnum.textEmbedding)
|
||||
return 'TEXT EMBEDDING'
|
||||
|
||||
return modelType.toLocaleUpperCase()
|
||||
}
|
||||
|
||||
export const genModelTypeFormSchema = (modelTypes: ModelTypeEnum[]) => {
|
||||
return {
|
||||
type: FormTypeEnum.radio,
|
||||
label: {
|
||||
zh_Hans: '模型类型',
|
||||
en_US: 'Model Type',
|
||||
},
|
||||
variable: '__model_type',
|
||||
default: modelTypes[0],
|
||||
required: true,
|
||||
show_on: [],
|
||||
options: modelTypes.map((modelType: ModelTypeEnum) => {
|
||||
return {
|
||||
value: modelType,
|
||||
label: {
|
||||
zh_Hans: MODEL_TYPE_TEXT[modelType],
|
||||
en_US: MODEL_TYPE_TEXT[modelType],
|
||||
},
|
||||
show_on: [],
|
||||
}
|
||||
}),
|
||||
} as CredentialFormSchemaRadio
|
||||
}
|
||||
|
||||
export const genModelNameFormSchema = (model?: Pick<CredentialFormSchemaTextInput, 'label' | 'placeholder'>) => {
|
||||
return {
|
||||
type: FormTypeEnum.textInput,
|
||||
label: model?.label || {
|
||||
zh_Hans: '模型名称',
|
||||
en_US: 'Model Name',
|
||||
},
|
||||
variable: '__model_name',
|
||||
required: true,
|
||||
show_on: [],
|
||||
placeholder: model?.placeholder || {
|
||||
zh_Hans: '请输入模型名称',
|
||||
en_US: 'Please enter model name',
|
||||
},
|
||||
} as CredentialFormSchemaTextInput
|
||||
}
|
||||
Reference in New Issue
Block a user