mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-09 10:56:52 +08:00
feat: frontend multi models support (#804)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
@@ -71,7 +71,7 @@ export default function AppSelector() {
|
||||
className="
|
||||
absolute right-0 mt-1.5 w-60 max-w-80
|
||||
divide-y divide-gray-100 origin-top-right rounded-lg bg-white
|
||||
shadow-[0_10px_15px_-3px_rgba(0,0,0,0.1),0_4px_6px_rgba(0,0,0,0.05)]
|
||||
shadow-lg
|
||||
"
|
||||
>
|
||||
<Menu.Item>
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function Operate({
|
||||
className="
|
||||
absolute right-0 top-9 w-60 max-w-80
|
||||
divide-y divide-gray-100 origin-top-right rounded-lg bg-white
|
||||
shadow-[0_10px_15px_-3px_rgba(0,0,0,0.1),0_4px_6px_rgba(0,0,0,0.05)]
|
||||
shadow-lg
|
||||
"
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.modal {
|
||||
max-width: 720px !important;
|
||||
max-width: 1024px !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 0 !important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -2,19 +2,22 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { AtSymbolIcon, CubeTransparentIcon, GlobeAltIcon, UserIcon, UsersIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { GlobeAltIcon as GlobalAltIconSolid, UserIcon as UserIconSolid, UsersIcon as UsersIconSolid } from '@heroicons/react/24/solid'
|
||||
import AccountPage from './account-page'
|
||||
import MembersPage from './members-page'
|
||||
import IntegrationsPage from './Integrations-page'
|
||||
import LanguagePage from './language-page'
|
||||
import ProviderPage from './provider-page'
|
||||
import PluginPage from './plugin-page'
|
||||
import DataSourcePage from './data-source-page'
|
||||
import ModelPage from './model-page'
|
||||
import s from './index.module.css'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { Database03, PuzzlePiece01 } from '@/app/components/base/icons/src/vender/line/development'
|
||||
import { Database03 as Database03Solid, PuzzlePiece01 as PuzzlePiece01Solid } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { User01, Users01 } from '@/app/components/base/icons/src/vender/line/users'
|
||||
import { User01 as User01Solid, Users01 as Users01Solid } from '@/app/components/base/icons/src/vender/solid/users'
|
||||
import { Globe01 } from '@/app/components/base/icons/src/vender/line/mapsAndTravel'
|
||||
import { AtSign, XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
|
||||
|
||||
const iconClassName = `
|
||||
w-4 h-4 ml-3 mr-2
|
||||
@@ -35,30 +38,6 @@ export default function AccountSetting({
|
||||
const [activeMenu, setActiveMenu] = useState(activeTab)
|
||||
const { t } = useTranslation()
|
||||
const menuItems = [
|
||||
{
|
||||
key: 'account-group',
|
||||
name: t('common.settings.accountGroup'),
|
||||
items: [
|
||||
{
|
||||
key: 'account',
|
||||
name: t('common.settings.account'),
|
||||
icon: <UserIcon className={iconClassName} />,
|
||||
activeIcon: <UserIconSolid className={iconClassName} />,
|
||||
},
|
||||
{
|
||||
key: 'integrations',
|
||||
name: t('common.settings.integrations'),
|
||||
icon: <AtSymbolIcon className={iconClassName} />,
|
||||
activeIcon: <AtSymbolIcon className={iconClassName} />,
|
||||
},
|
||||
{
|
||||
key: 'language',
|
||||
name: t('common.settings.language'),
|
||||
icon: <GlobeAltIcon className={iconClassName} />,
|
||||
activeIcon: <GlobalAltIconSolid className={iconClassName} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'workspace-group',
|
||||
name: t('common.settings.workplaceGroup'),
|
||||
@@ -66,14 +45,14 @@ export default function AccountSetting({
|
||||
{
|
||||
key: 'members',
|
||||
name: t('common.settings.members'),
|
||||
icon: <UsersIcon className={iconClassName} />,
|
||||
activeIcon: <UsersIconSolid className={iconClassName} />,
|
||||
icon: <Users01 className={iconClassName} />,
|
||||
activeIcon: <Users01Solid className={iconClassName} />,
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
name: t('common.settings.provider'),
|
||||
icon: <CubeTransparentIcon className={iconClassName} />,
|
||||
activeIcon: <CubeTransparentIcon className={iconClassName} />,
|
||||
icon: <CubeOutline className={iconClassName} />,
|
||||
activeIcon: <CubeOutline className={iconClassName} />,
|
||||
},
|
||||
{
|
||||
key: 'data-source',
|
||||
@@ -89,6 +68,30 @@ export default function AccountSetting({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'account-group',
|
||||
name: t('common.settings.accountGroup'),
|
||||
items: [
|
||||
{
|
||||
key: 'account',
|
||||
name: t('common.settings.account'),
|
||||
icon: <User01 className={iconClassName} />,
|
||||
activeIcon: <User01Solid className={iconClassName} />,
|
||||
},
|
||||
{
|
||||
key: 'integrations',
|
||||
name: t('common.settings.integrations'),
|
||||
icon: <AtSign className={iconClassName} />,
|
||||
activeIcon: <AtSign className={iconClassName} />,
|
||||
},
|
||||
{
|
||||
key: 'language',
|
||||
name: t('common.settings.language'),
|
||||
icon: <Globe01 className={iconClassName} />,
|
||||
activeIcon: <Globe01 className={iconClassName} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
@@ -101,6 +104,7 @@ export default function AccountSetting({
|
||||
}
|
||||
useEffect(() => {
|
||||
const targetElement = scrollRef.current
|
||||
|
||||
targetElement?.addEventListener('scroll', scrollHandle)
|
||||
return () => {
|
||||
targetElement?.removeEventListener('scroll', scrollHandle)
|
||||
@@ -143,17 +147,19 @@ export default function AccountSetting({
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div ref={scrollRef} className='relative w-[520px] h-[580px] pb-4 overflow-y-auto'>
|
||||
<div ref={scrollRef} className='relative w-[824px] h-[720px] pb-4 overflow-y-auto'>
|
||||
<div className={cn('sticky top-0 px-6 py-4 flex items-center justify-between h-14 mb-4 bg-white text-base font-medium text-gray-900 z-20', scrolled && scrolledClassName)}>
|
||||
{[...menuItems[0].items, ...menuItems[1].items].find(item => item.key === activeMenu)?.name}
|
||||
<XMarkIcon className='w-4 h-4 cursor-pointer' onClick={onCancel} />
|
||||
<div className='flex items-center justify-center -mr-4 w-6 h-6 cursor-pointer' onClick={onCancel}>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6'>
|
||||
<div className='px-8 pt-2'>
|
||||
{activeMenu === 'account' && <AccountPage />}
|
||||
{activeMenu === 'members' && <MembersPage />}
|
||||
{activeMenu === 'integrations' && <IntegrationsPage />}
|
||||
{activeMenu === 'language' && <LanguagePage />}
|
||||
{activeMenu === 'provider' && <ProviderPage />}
|
||||
{activeMenu === 'provider' && <ModelPage />}
|
||||
{activeMenu === 'data-source' && <DataSourcePage />}
|
||||
{activeMenu === 'plugin' && <PluginPage />}
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@ const KeyInput = ({
|
||||
<div className="mb-2 text-[13px] font-medium text-gray-800">{name}</div>
|
||||
<div className='
|
||||
flex items-center px-3 bg-white rounded-lg
|
||||
shadow-[0_1px_2px_rgba(16,24,40,0.05)]
|
||||
shadow-xs
|
||||
'>
|
||||
<input
|
||||
className='
|
||||
|
||||
@@ -13,7 +13,7 @@ export type ValidatedStatusState = {
|
||||
|
||||
export type Status = 'add' | 'fail' | 'success'
|
||||
|
||||
export type ValidateValue = Record<string, string | undefined>
|
||||
export type ValidateValue = Record<string, string | undefined | boolean>
|
||||
|
||||
export type ValidateCallback = {
|
||||
before: (v?: ValidateValue) => boolean | undefined
|
||||
|
||||
@@ -14,7 +14,6 @@ export const useValidate: (value: ValidateValue) => [DebouncedFunc<(validateCall
|
||||
setValidatedStatus({})
|
||||
return
|
||||
}
|
||||
|
||||
setValidating(true)
|
||||
|
||||
if (validateCallback.run) {
|
||||
@@ -26,7 +25,7 @@ export const useValidate: (value: ValidateValue) => [DebouncedFunc<(validateCall
|
||||
|
||||
setValidating(false)
|
||||
}
|
||||
}, { wait: 500 })
|
||||
}, { wait: 1000 })
|
||||
|
||||
return [run, validating, validatedStatus]
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const MembersPage = () => {
|
||||
<div className='
|
||||
shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200
|
||||
text-[13px] font-medium text-primary-600 bg-white
|
||||
shadow-[0_1px_2px_rgba(16,24,40,0.05)] rounded-lg cursor-pointer
|
||||
shadow-xs rounded-lg cursor-pointer
|
||||
' onClick={() => setInviteModalVisible(true)}>
|
||||
<UserPlusIcon className='w-4 h-4 mr-2 ' />
|
||||
{t('common.members.invite')}
|
||||
|
||||
@@ -24,7 +24,7 @@ const InvitedModal = ({
|
||||
<div className='
|
||||
w-12 h-12 flex items-center justify-center rounded-xl
|
||||
bg-white border-[0.5px] border-gray-100
|
||||
shadow-[0px_20px_24px_-4px_rgba(16,24,40,0.08),0px_8px_8px_-4px_rgba(16,24,40,0.03)]
|
||||
shadow-xl
|
||||
'>
|
||||
<CheckCircleIcon className='w-[22px] h-[22px] text-[#039855]' />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ProviderEnum } from '../declarations'
|
||||
import type { ProviderConfig } from '../declarations'
|
||||
import { Anthropic, AnthropicText } from '@/app/components/base/icons/src/public/llm'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
const config: ProviderConfig = {
|
||||
selector: {
|
||||
name: {
|
||||
'en': 'Anthropic',
|
||||
'zh-Hans': 'Anthropic',
|
||||
},
|
||||
icon: <Anthropic className='w-full h-full' />,
|
||||
},
|
||||
item: {
|
||||
key: ProviderEnum.anthropic,
|
||||
titleIcon: {
|
||||
'en': <AnthropicText className='h-5' />,
|
||||
'zh-Hans': <AnthropicText className='h-5' />,
|
||||
},
|
||||
subTitleIcon: <Anthropic className='h-6' />,
|
||||
desc: {
|
||||
'en': 'Anthropic’s powerful models, such as Claude 2 and Claude Instant.',
|
||||
'zh-Hans': 'Anthropic 的强大模型,例如 Claude 2 和 Claude Instant。',
|
||||
},
|
||||
bgColor: 'bg-[#F0F0EB]',
|
||||
},
|
||||
modal: {
|
||||
key: ProviderEnum.anthropic,
|
||||
title: {
|
||||
'en': 'Anthropic',
|
||||
'zh-Hans': 'Anthropic',
|
||||
},
|
||||
icon: <Anthropic className='h-6' />,
|
||||
link: {
|
||||
href: 'https://console.anthropic.com/account/keys',
|
||||
label: {
|
||||
'en': 'Get your API key from Anthropic',
|
||||
'zh-Hans': '从 Anthropic 获取 API Key',
|
||||
},
|
||||
},
|
||||
validateKeys: ['anthropic_api_key'],
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
key: 'anthropic_api_key',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Key',
|
||||
'zh-Hans': 'API Key',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API key here',
|
||||
'zh-Hans': '在此输入您的 API Key',
|
||||
},
|
||||
},
|
||||
...(
|
||||
IS_CE_EDITION
|
||||
? [{
|
||||
type: 'text',
|
||||
key: 'anthropic_api_url',
|
||||
required: false,
|
||||
label: {
|
||||
'en': 'Custom API Domain',
|
||||
'zh-Hans': '自定义 API 域名',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API domain, eg: https://example.com/xxx(Optional)',
|
||||
'zh-Hans': '在此输入您的 API 域名,如:https://example.com/xxx(选填)',
|
||||
},
|
||||
help: {
|
||||
'en': 'Configurable custom Anthropic API server url.',
|
||||
'zh-Hans': '可配置自定义 Anthropic API 服务器地址。',
|
||||
},
|
||||
}]
|
||||
: []
|
||||
),
|
||||
],
|
||||
},
|
||||
}
|
||||
export default config
|
||||
@@ -0,0 +1,175 @@
|
||||
import { ProviderEnum } from '../declarations'
|
||||
import type { ProviderConfig } from '../declarations'
|
||||
import { AzureOpenaiService, AzureOpenaiServiceText, OpenaiBlue } from '@/app/components/base/icons/src/public/llm'
|
||||
|
||||
const config: ProviderConfig = {
|
||||
selector: {
|
||||
name: {
|
||||
'en': 'Azure OpenAI Service',
|
||||
'zh-Hans': 'Azure OpenAI Service',
|
||||
},
|
||||
icon: <OpenaiBlue className='w-full h-full' />,
|
||||
},
|
||||
item: {
|
||||
key: ProviderEnum.azure_openai,
|
||||
titleIcon: {
|
||||
'en': <AzureOpenaiServiceText className='h-6' />,
|
||||
'zh-Hans': <AzureOpenaiServiceText className='h-6' />,
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
key: ProviderEnum.azure_openai,
|
||||
title: {
|
||||
'en': 'Azure OpenAI Service Model',
|
||||
'zh-Hans': 'Azure OpenAI Service Model',
|
||||
},
|
||||
icon: <AzureOpenaiService className='h-6' />,
|
||||
link: {
|
||||
href: 'https://azure.microsoft.com/en-us/products/ai-services/openai-service',
|
||||
label: {
|
||||
'en': 'Get your API key from Azure',
|
||||
'zh-Hans': '从 Azure 获取 API Key',
|
||||
},
|
||||
},
|
||||
defaultValue: {
|
||||
model_type: 'text-generation',
|
||||
},
|
||||
validateKeys: [
|
||||
'model_name',
|
||||
'model_type',
|
||||
'openai_api_base',
|
||||
'openai_api_key',
|
||||
'base_model_name',
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
key: 'model_name',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'Deployment Name',
|
||||
'zh-Hans': '部署名称',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your Deployment Name here',
|
||||
'zh-Hans': '在此输入您的部署名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
key: 'model_type',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'Model Type',
|
||||
'zh-Hans': '模型类型',
|
||||
},
|
||||
options: [
|
||||
{
|
||||
key: 'text-generation',
|
||||
label: {
|
||||
'en': 'Text Generation',
|
||||
'zh-Hans': '文本生成',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'embeddings',
|
||||
label: {
|
||||
'en': 'Embeddings',
|
||||
'zh-Hans': 'Embeddings',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'openai_api_base',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Endpoint URL',
|
||||
'zh-Hans': 'API 域名',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API Endpoint, eg: https://example.com/xxx',
|
||||
'zh-Hans': '在此输入您的 API 域名,如:https://example.com/xxx',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'openai_api_key',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Key',
|
||||
'zh-Hans': 'API Key',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API key here',
|
||||
'zh-Hans': '在此输入您的 API Key',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
key: 'base_model_name',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'Base Model',
|
||||
'zh-Hans': '基础模型',
|
||||
},
|
||||
options: (v) => {
|
||||
if (v.model_type === 'text-generation') {
|
||||
return [
|
||||
{
|
||||
key: 'gpt-35-turbo',
|
||||
label: {
|
||||
'en': 'gpt-35-turbo',
|
||||
'zh-Hans': 'gpt-35-turbo',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'gpt-35-turbo-16k',
|
||||
label: {
|
||||
'en': 'gpt-35-turbo-16k',
|
||||
'zh-Hans': 'gpt-35-turbo-16k',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'gpt-4',
|
||||
label: {
|
||||
'en': 'gpt-4',
|
||||
'zh-Hans': 'gpt-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'gpt-4-32k',
|
||||
label: {
|
||||
'en': 'gpt-4-32k',
|
||||
'zh-Hans': 'gpt-4-32k',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'text-davinci-003',
|
||||
label: {
|
||||
'en': 'text-davinci-003',
|
||||
'zh-Hans': 'text-davinci-003',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
if (v.model_type === 'embeddings') {
|
||||
return [
|
||||
{
|
||||
key: 'text-embedding-ada-002',
|
||||
label: {
|
||||
'en': 'text-embedding-ada-002',
|
||||
'zh-Hans': 'text-embedding-ada-002',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ProviderEnum } from '../declarations'
|
||||
import type { ProviderConfig } from '../declarations'
|
||||
import { Chatglm, ChatglmText } from '@/app/components/base/icons/src/public/llm'
|
||||
|
||||
const config: ProviderConfig = {
|
||||
selector: {
|
||||
name: {
|
||||
'en': 'ChatGLM',
|
||||
'zh-Hans': 'ChatGLM',
|
||||
},
|
||||
icon: <Chatglm className='w-full h-full' />,
|
||||
},
|
||||
item: {
|
||||
key: ProviderEnum.chatglm,
|
||||
titleIcon: {
|
||||
'en': <ChatglmText className='h-6' />,
|
||||
'zh-Hans': <ChatglmText className='h-6' />,
|
||||
},
|
||||
disable: {
|
||||
tip: {
|
||||
'en': 'Only supports the ',
|
||||
'zh-Hans': '仅支持',
|
||||
},
|
||||
link: {
|
||||
href: {
|
||||
'en': 'https://docs.dify.ai/getting-started/install-self-hosted',
|
||||
'zh-Hans': 'https://docs.dify.ai/v/zh-hans/getting-started/install-self-hosted',
|
||||
},
|
||||
label: {
|
||||
'en': 'community open-source version',
|
||||
'zh-Hans': '社区开源版本',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
key: ProviderEnum.chatglm,
|
||||
title: {
|
||||
'en': 'ChatGLM',
|
||||
'zh-Hans': 'ChatGLM',
|
||||
},
|
||||
icon: <Chatglm className='h-6' />,
|
||||
link: {
|
||||
href: 'https://github.com/THUDM/ChatGLM-6B#api%E9%83%A8%E7%BD%B2',
|
||||
label: {
|
||||
'en': 'How to deploy ChatGLM',
|
||||
'zh-Hans': '如何部署 ChatGLM',
|
||||
},
|
||||
},
|
||||
validateKeys: ['api_base'],
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
key: 'api_base',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'Custom API Domain',
|
||||
'zh-Hans': '自定义 API 域名',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API domain, eg: https://example.com/xxx',
|
||||
'zh-Hans': '在此输入您的 API 域名,如:https://example.com/xxx',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,127 @@
|
||||
import { ProviderEnum } from '../declarations'
|
||||
import type { FormValue, ProviderConfig } from '../declarations'
|
||||
import { Huggingface, HuggingfaceText } from '@/app/components/base/icons/src/public/llm'
|
||||
|
||||
const config: ProviderConfig = {
|
||||
selector: {
|
||||
name: {
|
||||
'en': 'Hugging Face',
|
||||
'zh-Hans': 'Hugging Face',
|
||||
},
|
||||
icon: <Huggingface className='w-full h-full' />,
|
||||
},
|
||||
item: {
|
||||
key: ProviderEnum.huggingface_hub,
|
||||
titleIcon: {
|
||||
'en': <HuggingfaceText className='h-6' />,
|
||||
'zh-Hans': <HuggingfaceText className='h-6' />,
|
||||
},
|
||||
hit: {
|
||||
'en': '🐑 Llama 2 Supported',
|
||||
'zh-Hans': '🐑 Llama 2 已支持',
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
key: ProviderEnum.huggingface_hub,
|
||||
title: {
|
||||
'en': 'Hugging Face Model',
|
||||
'zh-Hans': 'Hugging Face Model',
|
||||
},
|
||||
icon: <Huggingface className='h-6' />,
|
||||
link: {
|
||||
href: 'https://huggingface.co/settings/tokens',
|
||||
label: {
|
||||
'en': 'Get your API key from Hugging Face Hub',
|
||||
'zh-Hans': '从 Hugging Face Hub 获取 API Key',
|
||||
},
|
||||
},
|
||||
defaultValue: {
|
||||
model_type: 'text-generation',
|
||||
huggingfacehub_api_type: 'hosted_inference_api',
|
||||
},
|
||||
validateKeys: (v?: FormValue) => {
|
||||
if (v?.huggingfacehub_api_type === 'hosted_inference_api') {
|
||||
return [
|
||||
'huggingfacehub_api_token',
|
||||
'model_name',
|
||||
]
|
||||
}
|
||||
if (v?.huggingfacehub_api_type === 'inference_endpoints') {
|
||||
return [
|
||||
'huggingfacehub_api_token',
|
||||
'model_name',
|
||||
'huggingfacehub_endpoint_url',
|
||||
]
|
||||
}
|
||||
return []
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'radio',
|
||||
key: 'huggingfacehub_api_type',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'Endpoint Type',
|
||||
'zh-Hans': '端点类型',
|
||||
},
|
||||
options: [
|
||||
{
|
||||
key: 'hosted_inference_api',
|
||||
label: {
|
||||
'en': 'Hosted Inference API',
|
||||
'zh-Hans': 'Hosted Inference API',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'inference_endpoints',
|
||||
label: {
|
||||
'en': 'Inference Endpoints',
|
||||
'zh-Hans': 'Inference Endpoints',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'huggingfacehub_api_token',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Token',
|
||||
'zh-Hans': 'API Token',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your Hugging Face Hub API Token here',
|
||||
'zh-Hans': '在此输入您的 Hugging Face Hub API Token',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'model_name',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'Model Name',
|
||||
'zh-Hans': '模型名称',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your Model Name here',
|
||||
'zh-Hans': '在此输入您的模型名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
hidden: (value?: FormValue) => value?.huggingfacehub_api_type === 'hosted_inference_api',
|
||||
type: 'text',
|
||||
key: 'huggingfacehub_endpoint_url',
|
||||
label: {
|
||||
'en': 'Endpoint URL',
|
||||
'zh-Hans': '端点 URL',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your Endpoint URL here',
|
||||
'zh-Hans': '在此输入您的端点 URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,23 @@
|
||||
import openai from './openai'
|
||||
import anthropic from './anthropic'
|
||||
import azure_openai from './azure_openai'
|
||||
import replicate from './replicate'
|
||||
import huggingface_hub from './huggingface_hub'
|
||||
import wenxin from './wenxin'
|
||||
import tongyi from './tongyi'
|
||||
import spark from './spark'
|
||||
import minimax from './minimax'
|
||||
import chatglm from './chatglm'
|
||||
|
||||
export default {
|
||||
openai,
|
||||
anthropic,
|
||||
azure_openai,
|
||||
replicate,
|
||||
huggingface_hub,
|
||||
wenxin,
|
||||
tongyi,
|
||||
spark,
|
||||
minimax,
|
||||
chatglm,
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ProviderEnum } from '../declarations'
|
||||
import type { ProviderConfig } from '../declarations'
|
||||
import { Minimax, MinimaxText } from '@/app/components/base/icons/src/image/llm'
|
||||
|
||||
const config: ProviderConfig = {
|
||||
selector: {
|
||||
name: {
|
||||
'en': 'MINIMAX',
|
||||
'zh-Hans': 'MINIMAX',
|
||||
},
|
||||
icon: <Minimax className='w-full h-full' />,
|
||||
},
|
||||
item: {
|
||||
key: ProviderEnum.minimax,
|
||||
titleIcon: {
|
||||
'en': <MinimaxText className='w-[84px] h-6' />,
|
||||
'zh-Hans': <MinimaxText className='w-[84px] h-6' />,
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
key: ProviderEnum.minimax,
|
||||
title: {
|
||||
'en': 'MiniMax',
|
||||
'zh-Hans': 'MiniMax',
|
||||
},
|
||||
icon: <Minimax className='w-6 h-6' />,
|
||||
link: {
|
||||
href: 'https://api.minimax.chat/user-center/basic-information/interface-key',
|
||||
label: {
|
||||
'en': 'Get your API key from MiniMax',
|
||||
'zh-Hans': '从 MiniMax 获取 API Key',
|
||||
},
|
||||
},
|
||||
validateKeys: [
|
||||
'minimax_api_key',
|
||||
'minimax_group_id',
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
key: 'minimax_api_key',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Key',
|
||||
'zh-Hans': 'API Key',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API key here',
|
||||
'zh-Hans': '在此输入您的 API Key',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'minimax_group_id',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'Group ID',
|
||||
'zh-Hans': 'Group ID',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your Group ID here',
|
||||
'zh-Hans': '在此输入您的 Group ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,93 @@
|
||||
import { ProviderEnum } from '../declarations'
|
||||
import type { ProviderConfig } from '../declarations'
|
||||
import { OpenaiBlack, OpenaiText, OpenaiTransparent } from '@/app/components/base/icons/src/public/llm'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
const config: ProviderConfig = {
|
||||
selector: {
|
||||
name: {
|
||||
'en': 'OpenAI',
|
||||
'zh-Hans': 'OpenAI',
|
||||
},
|
||||
icon: <OpenaiBlack className='w-full h-full' />,
|
||||
},
|
||||
item: {
|
||||
key: ProviderEnum.openai,
|
||||
titleIcon: {
|
||||
'en': <OpenaiText className='h-5' />,
|
||||
'zh-Hans': <OpenaiText className='h-5' />,
|
||||
},
|
||||
subTitleIcon: <OpenaiBlack className='w-6 h-6' />,
|
||||
desc: {
|
||||
'en': 'Models provided by OpenAI, such as GPT-3.5-Turbo and GPT-4.',
|
||||
'zh-Hans': 'OpenAI 提供的模型,例如 GPT-3.5-Turbo 和 GPT-4。',
|
||||
},
|
||||
bgColor: 'bg-gray-200',
|
||||
},
|
||||
modal: {
|
||||
key: ProviderEnum.openai,
|
||||
title: {
|
||||
'en': 'OpenAI',
|
||||
'zh-Hans': 'OpenAI',
|
||||
},
|
||||
icon: <OpenaiTransparent className='w-6 h-6' />,
|
||||
link: {
|
||||
href: 'https://platform.openai.com/account/api-keys',
|
||||
label: {
|
||||
'en': 'Get your API key from OpenAI',
|
||||
'zh-Hans': '从 OpenAI 获取 API Key',
|
||||
},
|
||||
},
|
||||
validateKeys: ['openai_api_key'],
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
key: 'openai_api_key',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Key',
|
||||
'zh-Hans': 'API Key',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API key here',
|
||||
'zh-Hans': '在此输入您的 API Key',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'openai_organization',
|
||||
required: false,
|
||||
label: {
|
||||
'en': 'Organization ID',
|
||||
'zh-Hans': '组织 ID',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your Organization ID(Optional)',
|
||||
'zh-Hans': '在此输入您的组织 ID(选填)',
|
||||
},
|
||||
},
|
||||
...(
|
||||
IS_CE_EDITION
|
||||
? [{
|
||||
type: 'text',
|
||||
key: 'openai_api_base',
|
||||
required: false,
|
||||
label: {
|
||||
'en': 'Custom API Domain',
|
||||
'zh-Hans': '自定义 API 域名',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API domain, eg: https://example.com/xxx(Optional)',
|
||||
'zh-Hans': '在此输入您的 API 域名,如:https://example.com/xxx(选填)',
|
||||
},
|
||||
help: {
|
||||
'en': 'You can configure your server compatible with the OpenAI API specification, or proxy mirror address',
|
||||
'zh-Hans': '可配置您的兼容 OpenAI API 规范的服务器,或者代理镜像地址',
|
||||
},
|
||||
}]
|
||||
: []
|
||||
),
|
||||
],
|
||||
},
|
||||
}
|
||||
export default config
|
||||
@@ -0,0 +1,115 @@
|
||||
import { ProviderEnum } from '../declarations'
|
||||
import type { ProviderConfig } from '../declarations'
|
||||
import { Replicate, ReplicateText } from '@/app/components/base/icons/src/public/llm'
|
||||
|
||||
const config: ProviderConfig = {
|
||||
selector: {
|
||||
name: {
|
||||
'en': 'Replicate',
|
||||
'zh-Hans': 'Replicate',
|
||||
},
|
||||
icon: <Replicate className='w-full h-full' />,
|
||||
},
|
||||
item: {
|
||||
key: ProviderEnum.replicate,
|
||||
titleIcon: {
|
||||
'en': <ReplicateText className='h-6' />,
|
||||
'zh-Hans': <ReplicateText className='h-6' />,
|
||||
},
|
||||
hit: {
|
||||
'en': '🐑 Llama 2 Supported',
|
||||
'zh-Hans': '🐑 Llama 2 已支持',
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
key: ProviderEnum.replicate,
|
||||
title: {
|
||||
'en': 'Replicate Model',
|
||||
'zh-Hans': 'Replicate Model',
|
||||
},
|
||||
icon: <Replicate className='h-6' />,
|
||||
link: {
|
||||
href: 'https://replicate.com/account/api-tokens',
|
||||
label: {
|
||||
'en': 'Get your API key from Replicate',
|
||||
'zh-Hans': '从 Replicate 获取 API Key',
|
||||
},
|
||||
},
|
||||
defaultValue: {
|
||||
model_type: 'text-generation',
|
||||
},
|
||||
validateKeys: [
|
||||
'model_type',
|
||||
'replicate_api_token',
|
||||
'model_name',
|
||||
'model_version',
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
type: 'radio',
|
||||
key: 'model_type',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'Model Type',
|
||||
'zh-Hans': '模型类型',
|
||||
},
|
||||
options: [
|
||||
{
|
||||
key: 'text-generation',
|
||||
label: {
|
||||
'en': 'Text Generation',
|
||||
'zh-Hans': '文本生成',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'embeddings',
|
||||
label: {
|
||||
'en': 'Embeddings',
|
||||
'zh-Hans': 'Embeddings',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'replicate_api_token',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Key',
|
||||
'zh-Hans': 'API Key',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your Replicate API key here',
|
||||
'zh-Hans': '在此输入您的 Replicate API Key',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'model_name',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'Model Name',
|
||||
'zh-Hans': '模型名称',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your Model Name here',
|
||||
'zh-Hans': '在此输入您的模型名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'model_version',
|
||||
label: {
|
||||
'en': 'Model Version',
|
||||
'zh-Hans': '模型版本',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your Model Version here',
|
||||
'zh-Hans': '在此输入您的模型版本',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,83 @@
|
||||
import { ProviderEnum } from '../declarations'
|
||||
import type { ProviderConfig } from '../declarations'
|
||||
import { IflytekSpark, IflytekSparkText, IflytekSparkTextCn } from '@/app/components/base/icons/src/public/llm'
|
||||
|
||||
const config: ProviderConfig = {
|
||||
selector: {
|
||||
name: {
|
||||
'en': 'iFLYTEK SPARK',
|
||||
'zh-Hans': '讯飞星火',
|
||||
},
|
||||
icon: <IflytekSpark className='w-full h-full' />,
|
||||
},
|
||||
item: {
|
||||
key: ProviderEnum.spark,
|
||||
titleIcon: {
|
||||
'en': <IflytekSparkText className='h-6' />,
|
||||
'zh-Hans': <IflytekSparkTextCn className='h-6' />,
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
key: ProviderEnum.spark,
|
||||
title: {
|
||||
'en': 'iFLYTEK SPARK',
|
||||
'zh-Hans': '讯飞星火',
|
||||
},
|
||||
icon: <IflytekSpark className='w-6 h-6' />,
|
||||
link: {
|
||||
href: 'https://www.xfyun.cn/solutions/xinghuoAPI',
|
||||
label: {
|
||||
'en': 'Get your API key from AliCloud',
|
||||
'zh-Hans': '从阿里云获取 API Key',
|
||||
},
|
||||
},
|
||||
validateKeys: [
|
||||
'app_id',
|
||||
'api_key',
|
||||
'api_secret',
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
key: 'app_id',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API ID',
|
||||
'zh-Hans': 'API ID',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API ID here',
|
||||
'zh-Hans': '在此输入您的 API ID',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'api_key',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Key',
|
||||
'zh-Hans': 'API Key',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API key here',
|
||||
'zh-Hans': '在此输入您的 API Key',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'api_secret',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Secret',
|
||||
'zh-Hans': 'API Secret',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API Secret here',
|
||||
'zh-Hans': '在此输入您的 API Secret',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ProviderEnum } from '../declarations'
|
||||
import type { ProviderConfig } from '../declarations'
|
||||
import { Tongyi, TongyiText, TongyiTextCn } from '@/app/components/base/icons/src/image/llm'
|
||||
|
||||
const config: ProviderConfig = {
|
||||
selector: {
|
||||
name: {
|
||||
'en': 'TONGYI QIANWEN',
|
||||
'zh-Hans': '通义千问',
|
||||
},
|
||||
icon: <Tongyi className='w-full h-full' />,
|
||||
},
|
||||
item: {
|
||||
key: ProviderEnum.tongyi,
|
||||
titleIcon: {
|
||||
'en': <TongyiText className='w-[88px] h-6' />,
|
||||
'zh-Hans': <TongyiTextCn className='w-[100px] h-6' />,
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
key: ProviderEnum.tongyi,
|
||||
title: {
|
||||
'en': 'Tongyi',
|
||||
'zh-Hans': '通义千问',
|
||||
},
|
||||
icon: <Tongyi className='w-6 h-6' />,
|
||||
link: {
|
||||
href: 'https://dashscope.console.aliyun.com/api-key_management',
|
||||
label: {
|
||||
'en': 'Get your API key from AliCloud',
|
||||
'zh-Hans': '从阿里云获取 API Key',
|
||||
},
|
||||
},
|
||||
validateKeys: ['dashscope_api_key'],
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
key: 'dashscope_api_key',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Key',
|
||||
'zh-Hans': 'API Key',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API key here',
|
||||
'zh-Hans': '在此输入您的 API Key',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ProviderEnum } from '../declarations'
|
||||
import type { ProviderConfig } from '../declarations'
|
||||
import { Wxyy, WxyyText, WxyyTextCn } from '@/app/components/base/icons/src/image/llm'
|
||||
|
||||
const config: ProviderConfig = {
|
||||
selector: {
|
||||
name: {
|
||||
'en': 'WENXIN YIYAN',
|
||||
'zh-Hans': '文心一言',
|
||||
},
|
||||
icon: <Wxyy className='w-full h-full' />,
|
||||
},
|
||||
item: {
|
||||
key: ProviderEnum.wenxin,
|
||||
titleIcon: {
|
||||
'en': <WxyyText className='w-[124px] h-6' />,
|
||||
'zh-Hans': <WxyyTextCn className='w-[100px] h-6' />,
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
key: ProviderEnum.wenxin,
|
||||
title: {
|
||||
'en': 'WENXINYIYAN',
|
||||
'zh-Hans': '文心一言',
|
||||
},
|
||||
icon: <Wxyy className='w-6 h-6' />,
|
||||
link: {
|
||||
href: 'https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application',
|
||||
label: {
|
||||
'en': 'Get your API key from Baidu',
|
||||
'zh-Hans': '从百度获取 API Key',
|
||||
},
|
||||
},
|
||||
validateKeys: ['api_key', 'secret_key'],
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
key: 'api_key',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'API Key',
|
||||
'zh-Hans': 'API Key',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your API key here',
|
||||
'zh-Hans': '在此输入您的 API Key',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'secret_key',
|
||||
required: true,
|
||||
label: {
|
||||
'en': 'Secret Key',
|
||||
'zh-Hans': 'Secret Key',
|
||||
},
|
||||
placeholder: {
|
||||
'en': 'Enter your Secret key here',
|
||||
'zh-Hans': '在此输入您的 Secret Key',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { ReactElement } from 'react'
|
||||
|
||||
export type FormValue = Record<string, string>
|
||||
|
||||
export type TypeWithI18N<T = string> = {
|
||||
'en': T
|
||||
'zh-Hans': T
|
||||
}
|
||||
|
||||
export type Option = {
|
||||
key: string
|
||||
label: TypeWithI18N
|
||||
}
|
||||
|
||||
export type ProviderSelector = {
|
||||
name: TypeWithI18N
|
||||
icon: ReactElement
|
||||
}
|
||||
|
||||
export type Field = {
|
||||
hidden?: (v?: FormValue) => boolean
|
||||
type: string
|
||||
key: string
|
||||
required?: boolean
|
||||
label: TypeWithI18N
|
||||
options?: Option[] | ((v: FormValue) => Option[])
|
||||
placeholder?: TypeWithI18N
|
||||
help?: TypeWithI18N
|
||||
}
|
||||
|
||||
export enum ProviderEnum {
|
||||
'openai' = 'openai',
|
||||
'anthropic' = 'anthropic',
|
||||
'replicate' = 'replicate',
|
||||
'azure_openai' = 'azure_openai',
|
||||
'huggingface_hub' = 'huggingface_hub',
|
||||
'tongyi' = 'tongyi',
|
||||
'wenxin' = 'wenxin',
|
||||
'spark' = 'spark',
|
||||
'minimax' = 'minimax',
|
||||
'chatglm' = 'chatglm',
|
||||
}
|
||||
|
||||
export type ProviderConfigItem = {
|
||||
key: ProviderEnum
|
||||
titleIcon: TypeWithI18N<ReactElement>
|
||||
subTitleIcon?: ReactElement
|
||||
desc?: TypeWithI18N
|
||||
bgColor?: string
|
||||
hit?: TypeWithI18N
|
||||
disable?: {
|
||||
tip: TypeWithI18N
|
||||
link: {
|
||||
href: TypeWithI18N
|
||||
label: TypeWithI18N
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum ModelType {
|
||||
textGeneration = 'text-generation',
|
||||
embeddings = 'embeddings',
|
||||
speech2text = 'speech2text',
|
||||
}
|
||||
|
||||
export enum ModelFeature {
|
||||
agentThought = 'agent_thought',
|
||||
}
|
||||
|
||||
// backend defined model struct: /console/api/workspaces/current/models/model-type/:model_type
|
||||
export type BackendModel = {
|
||||
model_name: string
|
||||
model_type: ModelType
|
||||
model_provider: {
|
||||
provider_name: ProviderEnum
|
||||
provider_type: PreferredProviderTypeEnum
|
||||
}
|
||||
features: ModelFeature[]
|
||||
}
|
||||
|
||||
export type ProviderConfigModal = {
|
||||
key: ProviderEnum
|
||||
title: TypeWithI18N
|
||||
icon: ReactElement
|
||||
defaultValue?: FormValue
|
||||
validateKeys?: string[] | ((v?: FormValue) => string[])
|
||||
fields: Field[]
|
||||
link: {
|
||||
href: string
|
||||
label: TypeWithI18N
|
||||
}
|
||||
}
|
||||
|
||||
export type ProviderConfig = {
|
||||
selector: ProviderSelector
|
||||
item: ProviderConfigItem
|
||||
modal: ProviderConfigModal
|
||||
}
|
||||
|
||||
export enum PreferredProviderTypeEnum {
|
||||
'system' = 'system',
|
||||
'custom' = 'custom',
|
||||
}
|
||||
export enum ModelFlexibilityEnum {
|
||||
'fixed' = 'fixed',
|
||||
'configurable' = 'configurable',
|
||||
}
|
||||
|
||||
export type ProviderCommon = {
|
||||
provider_name: ProviderEnum
|
||||
provider_type: PreferredProviderTypeEnum
|
||||
is_valid: boolean
|
||||
last_used: number
|
||||
}
|
||||
|
||||
export type ProviderWithQuota = {
|
||||
quota_type: string
|
||||
quota_unit: string
|
||||
quota_limit: number
|
||||
quota_used: number
|
||||
} & ProviderCommon
|
||||
|
||||
export type ProviderWithConfig = {
|
||||
config: Record<string, string>
|
||||
} & ProviderCommon
|
||||
|
||||
export type Model = {
|
||||
model_name: string
|
||||
model_type: string
|
||||
config: Record<string, string>
|
||||
}
|
||||
|
||||
export type ProviderWithModels = {
|
||||
models: Model[]
|
||||
} & ProviderCommon
|
||||
|
||||
export type ProviderInstance = ProviderWithQuota | ProviderWithConfig | ProviderWithModels
|
||||
|
||||
export type Provider = {
|
||||
preferred_provider_type: PreferredProviderTypeEnum
|
||||
model_flexibility: ModelFlexibilityEnum
|
||||
providers: ProviderInstance[]
|
||||
}
|
||||
export type ProviderMap = {
|
||||
[k in ProviderEnum]: Provider
|
||||
}
|
||||
298
web/app/components/header/account-setting/model-page/index.tsx
Normal file
298
web/app/components/header/account-setting/model-page/index.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import { useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
BackendModel,
|
||||
FormValue,
|
||||
ProviderConfigModal,
|
||||
ProviderEnum,
|
||||
} from './declarations'
|
||||
import ModelSelector from './model-selector'
|
||||
import ModelCard from './model-card'
|
||||
import ModelItem from './model-item'
|
||||
import ModelModal from './model-modal'
|
||||
import config from './configs'
|
||||
import { ConfigurableProviders } from './utils'
|
||||
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
// import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
changeModelProviderPriority,
|
||||
deleteModelProvider,
|
||||
deleteModelProviderModel,
|
||||
fetchDefaultModal,
|
||||
fetchModelProviders,
|
||||
setModelProvider,
|
||||
updateDefaultModel,
|
||||
} from '@/service/common'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import Confirm from '@/app/components/base/confirm/common'
|
||||
import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
const MODEL_CARD_LIST = [
|
||||
config.openai,
|
||||
config.anthropic,
|
||||
]
|
||||
|
||||
const MODEL_LIST = [
|
||||
config.azure_openai,
|
||||
config.replicate,
|
||||
config.huggingface_hub,
|
||||
config.minimax,
|
||||
config.spark,
|
||||
config.tongyi,
|
||||
config.wenxin,
|
||||
config.chatglm,
|
||||
]
|
||||
|
||||
const titleClassName = `
|
||||
flex items-center h-9 text-sm font-medium text-gray-900
|
||||
`
|
||||
const tipClassName = `
|
||||
ml-0.5 w-[14px] h-[14px] text-gray-400
|
||||
`
|
||||
|
||||
type DeleteModel = {
|
||||
model_name: string
|
||||
model_type: string
|
||||
}
|
||||
|
||||
const ModelPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const { updateModelList } = useProviderContext()
|
||||
const { data: providers, mutate: mutateProviders } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
|
||||
const { data: textGenerationDefaultModel, mutate: mutateTextGenerationDefaultModel } = useSWR('/workspaces/current/default-model?model_type=text-generation', fetchDefaultModal)
|
||||
const { data: embeddingsDefaultModel, mutate: mutateEmbeddingsDefaultModel } = useSWR('/workspaces/current/default-model?model_type=embeddings', fetchDefaultModal)
|
||||
const { data: speech2textDefaultModel, mutate: mutateSpeech2textDefaultModel } = useSWR('/workspaces/current/default-model?model_type=speech2text', fetchDefaultModal)
|
||||
const [showMoreModel, setShowMoreModel] = useState(false)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { notify } = useToastContext()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [modelModalConfig, setModelModalConfig] = useState<ProviderConfigModal | undefined>(undefined)
|
||||
const [confirmShow, setConfirmShow] = useState(false)
|
||||
const [deleteModel, setDeleteModel] = useState<DeleteModel & { providerKey: ProviderEnum }>()
|
||||
const [modalMode, setModalMode] = useState('add')
|
||||
|
||||
const handleOpenModal = (newModelModalConfig: ProviderConfigModal | undefined, editValue?: FormValue) => {
|
||||
if (newModelModalConfig) {
|
||||
setShowModal(true)
|
||||
const defaultValue = editValue ? { ...newModelModalConfig.defaultValue, ...editValue } : newModelModalConfig.defaultValue
|
||||
setModelModalConfig({
|
||||
...newModelModalConfig,
|
||||
defaultValue,
|
||||
})
|
||||
if (editValue)
|
||||
setModalMode('edit')
|
||||
else
|
||||
setModalMode('add')
|
||||
}
|
||||
}
|
||||
const handleCancelModal = () => {
|
||||
setShowModal(false)
|
||||
}
|
||||
const handleUpdateProvidersAndModelList = () => {
|
||||
updateModelList(ModelType.textGeneration)
|
||||
updateModelList(ModelType.embeddings)
|
||||
mutateProviders()
|
||||
}
|
||||
const handleSave = async (v?: FormValue) => {
|
||||
if (v && modelModalConfig) {
|
||||
let body, url
|
||||
if (ConfigurableProviders.includes(modelModalConfig.key)) {
|
||||
const { model_name, model_type, ...config } = v
|
||||
body = {
|
||||
model_name,
|
||||
model_type,
|
||||
config,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${modelModalConfig.key}/models`
|
||||
}
|
||||
else {
|
||||
body = {
|
||||
config: v,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${modelModalConfig.key}`
|
||||
}
|
||||
|
||||
try {
|
||||
eventEmitter?.emit('provider-save')
|
||||
const res = await setModelProvider({ url, body })
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
handleUpdateProvidersAndModelList()
|
||||
handleCancelModal()
|
||||
}
|
||||
eventEmitter?.emit('')
|
||||
}
|
||||
catch (e) {
|
||||
eventEmitter?.emit('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = (deleteModel: DeleteModel, providerKey: ProviderEnum) => {
|
||||
setDeleteModel({ ...deleteModel, providerKey })
|
||||
setConfirmShow(true)
|
||||
}
|
||||
|
||||
const handleOperate = async ({ type, value }: Record<string, any>, provierKey: ProviderEnum) => {
|
||||
if (type === 'delete') {
|
||||
if (!value) {
|
||||
const res = await deleteModelProvider({ url: `/workspaces/current/model-providers/${provierKey}` })
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
handleUpdateProvidersAndModelList()
|
||||
}
|
||||
}
|
||||
else {
|
||||
handleConfirm(value, provierKey)
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'priority') {
|
||||
const res = await changeModelProviderPriority({
|
||||
url: `/workspaces/current/model-providers/${provierKey}/preferred-provider-type`,
|
||||
body: {
|
||||
preferred_provider_type: value,
|
||||
},
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
mutateProviders()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteModel = async () => {
|
||||
const { model_name, model_type, providerKey } = deleteModel || {}
|
||||
const res = await deleteModelProviderModel({
|
||||
url: `/workspaces/current/model-providers/${providerKey}/models?model_name=${model_name}&model_type=${model_type}`,
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
setConfirmShow(false)
|
||||
handleUpdateProvidersAndModelList()
|
||||
}
|
||||
}
|
||||
|
||||
const mutateDefaultModel = (type: ModelType) => {
|
||||
if (type === ModelType.textGeneration)
|
||||
mutateTextGenerationDefaultModel()
|
||||
if (type === ModelType.embeddings)
|
||||
mutateEmbeddingsDefaultModel()
|
||||
if (type === ModelType.speech2text)
|
||||
mutateSpeech2textDefaultModel()
|
||||
}
|
||||
const handleChangeDefaultModel = async (type: ModelType, v: BackendModel) => {
|
||||
const res = await updateDefaultModel({
|
||||
url: '/workspaces/current/default-model',
|
||||
body: {
|
||||
model_type: type,
|
||||
provider_name: v.model_provider.provider_name,
|
||||
model_name: v.model_name,
|
||||
},
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
mutateDefaultModel(type)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative pt-1 -mt-2'>
|
||||
<div className='grid grid-cols-3 gap-4 mb-5'>
|
||||
<div className='w-full'>
|
||||
<div className={titleClassName}>
|
||||
{t('common.modelProvider.systemReasoningModel.key')}
|
||||
{/* <HelpCircle className={tipClassName} /> */}
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
value={textGenerationDefaultModel && { providerName: textGenerationDefaultModel.model_provider.provider_name, modelName: textGenerationDefaultModel.model_name }}
|
||||
modelType={ModelType.textGeneration}
|
||||
onChange={v => handleChangeDefaultModel(ModelType.textGeneration, v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<div className={titleClassName}>
|
||||
{t('common.modelProvider.embeddingModel.key')}
|
||||
{/* <HelpCircle className={tipClassName} /> */}
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
value={embeddingsDefaultModel && { providerName: embeddingsDefaultModel.model_provider.provider_name, modelName: embeddingsDefaultModel.model_name }}
|
||||
modelType={ModelType.embeddings}
|
||||
onChange={v => handleChangeDefaultModel(ModelType.embeddings, v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<div className={titleClassName}>
|
||||
{t('common.modelProvider.speechToTextModel.key')}
|
||||
{/* <HelpCircle className={tipClassName} /> */}
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
value={speech2textDefaultModel && { providerName: speech2textDefaultModel.model_provider.provider_name, modelName: speech2textDefaultModel.model_name }}
|
||||
modelType={ModelType.speech2text}
|
||||
onChange={v => handleChangeDefaultModel(ModelType.speech2text, v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-5 h-[0.5px] bg-gray-100' />
|
||||
<div className='mb-3 text-sm font-medium text-gray-800'>{t('common.modelProvider.models')}</div>
|
||||
<div className='grid grid-cols-2 gap-4 mb-6'>
|
||||
{
|
||||
MODEL_CARD_LIST.map((model, index) => (
|
||||
<ModelCard
|
||||
key={index}
|
||||
modelItem={model.item}
|
||||
currentProvider={providers?.[model.item.key]}
|
||||
onOpenModal={editValue => handleOpenModal(model.modal, editValue)}
|
||||
onOperate={v => handleOperate(v, model.item.key)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
MODEL_LIST.slice(0, showMoreModel ? MODEL_LIST.length : 3).map((model, index) => (
|
||||
<ModelItem
|
||||
key={index}
|
||||
modelItem={model.item}
|
||||
currentProvider={providers?.[model.item.key]}
|
||||
onOpenModal={editValue => handleOpenModal(model.modal, editValue)}
|
||||
onOperate={v => handleOperate(v, model.item.key)}
|
||||
onUpdate={mutateProviders}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
!showMoreModel && (
|
||||
<div className='inline-flex items-center px-1 h-[26px] cursor-pointer' onClick={() => setShowMoreModel(true)}>
|
||||
<ChevronDownDouble className='mr-1 w-3 h-3 text-gray-500' />
|
||||
<div className='text-xs font-medium text-gray-500'>{t('common.modelProvider.showMoreModelProvider')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ModelModal
|
||||
isShow={showModal}
|
||||
modelModal={modelModalConfig}
|
||||
onCancel={handleCancelModal}
|
||||
onSave={handleSave}
|
||||
mode={modalMode}
|
||||
/>
|
||||
<Confirm
|
||||
isShow={confirmShow}
|
||||
onCancel={() => setConfirmShow(false)}
|
||||
title={deleteModel?.model_name || ''}
|
||||
desc={t('common.modelProvider.item.deleteDesc', { modelName: deleteModel?.model_name }) || ''}
|
||||
onConfirm={handleDeleteModel}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelPage
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Provider, ProviderWithQuota } from '../declarations'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { getPayUrl } from '@/service/common'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type QuotaProps = {
|
||||
currentProvider: Provider
|
||||
}
|
||||
const Quota: FC<QuotaProps> = ({
|
||||
currentProvider,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const systemTrial = currentProvider.providers.find(p => p.provider_type === 'system' && (p as ProviderWithQuota)?.quota_type === 'trial') as ProviderWithQuota
|
||||
const systemPaid = currentProvider.providers.find(p => p.provider_type === 'system' && (p as ProviderWithQuota)?.quota_type === 'paid') as ProviderWithQuota
|
||||
const QUOTA_UNIT_MAP: Record<string, string> = {
|
||||
times: t('common.modelProvider.card.callTimes'),
|
||||
tokens: 'Tokens',
|
||||
}
|
||||
|
||||
const renderStatus = () => {
|
||||
const totalQuota = (systemPaid?.is_valid ? systemPaid.quota_limit : 0) + systemTrial.quota_limit
|
||||
const totalUsed = (systemPaid?.is_valid ? systemPaid.quota_used : 0) + systemTrial.quota_used
|
||||
|
||||
if (totalQuota === totalUsed) {
|
||||
return (
|
||||
<div className='px-1.5 bg-[#FEF3F2] rounded-md text-xs font-semibold text-[#D92D20]'>
|
||||
{t('common.modelProvider.card.quotaExhausted')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (systemPaid?.is_valid) {
|
||||
return (
|
||||
<div className='px-1.5 bg-[#FFF6ED] rounded-md text-xs font-semibold text-[#EC4A0A]'>
|
||||
{t('common.modelProvider.card.paid')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='px-1.5 bg-primary-50 rounded-md text-xs font-semibold text-primary-600'>
|
||||
{t('common.modelProvider.card.onTrial')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderQuota = () => {
|
||||
if (systemPaid?.is_valid)
|
||||
return systemPaid.quota_limit - systemPaid.quota_used
|
||||
|
||||
if (systemTrial.is_valid)
|
||||
return systemTrial.quota_limit - systemTrial.quota_used
|
||||
}
|
||||
const renderUnit = () => {
|
||||
if (systemPaid?.is_valid)
|
||||
return QUOTA_UNIT_MAP[systemPaid.quota_unit]
|
||||
|
||||
if (systemTrial.is_valid)
|
||||
return QUOTA_UNIT_MAP[systemTrial.quota_unit]
|
||||
}
|
||||
const handleGetPayUrl = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getPayUrl(`/workspaces/current/model-providers/${systemPaid.provider_name}/checkout-url`)
|
||||
|
||||
window.location.href = res.url
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex justify-between px-4 py-3 border-b-[0.5px] border-b-[rgba(0, 0, 0, 0.5)]'>
|
||||
<div>
|
||||
<div className='flex items-center mb-1 h-5'>
|
||||
<div className='mr-1 text-xs font-medium text-gray-500'>
|
||||
{t('common.modelProvider.card.quota')}
|
||||
</div>
|
||||
{renderStatus()}
|
||||
</div>
|
||||
<div className='flex items-center text-gray-700'>
|
||||
<div className='mr-1 text-sm font-medium'>{renderQuota()}</div>
|
||||
<div className='mr-1 text-sm'>
|
||||
{renderUnit()}
|
||||
</div>
|
||||
<Tooltip
|
||||
selector='setting-model-card'
|
||||
htmlContent={
|
||||
<div className='w-[261px] text-gray-500'>{t('common.modelProvider.card.tip')}</div>
|
||||
}
|
||||
>
|
||||
<InfoCircle className='w-3 h-3 text-gray-400 hover:text-gray-700' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
systemPaid && (
|
||||
<Button
|
||||
type='primary'
|
||||
className='mt-1.5 !px-3 !h-8 !text-[13px] font-medium !rounded-lg'
|
||||
onClick={handleGetPayUrl}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('common.modelProvider.card.buyQuota')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Quota
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type {
|
||||
FormValue,
|
||||
Provider,
|
||||
ProviderConfigItem,
|
||||
ProviderWithConfig,
|
||||
} from '../declarations'
|
||||
import Indicator from '../../../indicator'
|
||||
import Selector from '../selector'
|
||||
import Quota from './Quota'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import I18n from '@/context/i18n'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type ModelCardProps = {
|
||||
currentProvider?: Provider
|
||||
modelItem: ProviderConfigItem
|
||||
onOpenModal: (v?: FormValue) => void
|
||||
onOperate: (v: Record<string, any>) => void
|
||||
}
|
||||
|
||||
const ModelCard: FC<ModelCardProps> = ({
|
||||
currentProvider,
|
||||
modelItem,
|
||||
onOpenModal,
|
||||
onOperate,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const { t } = useTranslation()
|
||||
const custom = currentProvider?.providers.find(p => p.provider_type === 'custom') as ProviderWithConfig
|
||||
|
||||
return (
|
||||
<div className='rounded-xl border-[0.5px] border-gray-200 shadow-xs'>
|
||||
<div className={`flex px-4 pt-4 pb-3 rounded-t-lg ${modelItem.bgColor}`}>
|
||||
<div className='grow mr-3'>
|
||||
<div className='mb-1'>
|
||||
{modelItem.titleIcon[locale]}
|
||||
</div>
|
||||
<div className='h-9 text-xs text-black opacity-60'>{modelItem.desc?.[locale]}</div>
|
||||
</div>
|
||||
{modelItem.subTitleIcon}
|
||||
</div>
|
||||
{
|
||||
!IS_CE_EDITION && currentProvider && <Quota currentProvider={currentProvider} />
|
||||
}
|
||||
{
|
||||
custom?.is_valid
|
||||
? (
|
||||
<div className='flex items-center px-4 h-12'>
|
||||
<Indicator color='green' className='mr-2' />
|
||||
<div className='grow text-[13px] font-medium text-gray-700'>API key</div>
|
||||
<div
|
||||
className='mr-1 px-2 leading-6 rounded-md text-xs font-medium text-gray-500 hover:bg-gray-50 cursor-pointer'
|
||||
onClick={() => onOpenModal(custom?.config)}
|
||||
>
|
||||
{t('common.operation.edit')}
|
||||
</div>
|
||||
<Selector
|
||||
onOperate={onOperate}
|
||||
value={currentProvider?.preferred_provider_type}
|
||||
hiddenOptions={IS_CE_EDITION}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div
|
||||
className='inline-flex items-center px-4 h-12 text-gray-500 cursor-pointer hover:text-primary-600'
|
||||
onClick={() => onOpenModal()}
|
||||
>
|
||||
<Plus className='mr-1.5 w-4 h-4'/>
|
||||
<div className='text-xs font-medium'>{t('common.modelProvider.addApiKey')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelCard
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '../../../indicator'
|
||||
import Selector from '../selector'
|
||||
import type { Model, ProviderEnum } from '../declarations'
|
||||
import { ProviderEnum as ProviderEnumValue } from '../declarations'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type CardProps = {
|
||||
providerType: ProviderEnum
|
||||
models: any[]
|
||||
onOpenModal: (v: any) => void
|
||||
onOperate: (v: Record<string, any>) => void
|
||||
}
|
||||
|
||||
const Card: FC<CardProps> = ({
|
||||
providerType,
|
||||
models,
|
||||
onOpenModal,
|
||||
onOperate,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const renderDesc = (model: Model) => {
|
||||
if (providerType === ProviderEnumValue.azure_openai)
|
||||
return model.config.openai_api_base
|
||||
if (providerType === ProviderEnumValue.replicate)
|
||||
return `version: ${model.config.model_version}`
|
||||
if (providerType === ProviderEnumValue.huggingface_hub)
|
||||
return model.config.huggingfacehub_endpoint_url
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='px-3 pb-3'>
|
||||
{
|
||||
models.map((model: Model) => (
|
||||
<div key={`${model.model_name}-${model.model_type}`} className='flex mb-1 px-3 py-2 bg-white rounded-lg shadow-xs last:mb-0'>
|
||||
<div className='grow'>
|
||||
<div className='flex items-center mb-0.5 h-[18px] text-[13px] font-medium text-gray-700'>
|
||||
{model.model_name}
|
||||
<div className='ml-2 px-1.5 rounded-md border border-[rgba(0,0,0,0.08)] text-xs text-gray-600'>{model.model_type}</div>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>
|
||||
{renderDesc(model)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<Indicator className='mr-3' />
|
||||
<Button
|
||||
className='mr-1 !px-3 !h-7 rounded-md bg-white !text-xs font-medium text-gray-700'
|
||||
onClick={() => onOpenModal({ model_name: model.model_name, model_type: model.model_type, ...model.config })}
|
||||
>
|
||||
{t('common.operation.edit')}
|
||||
</Button>
|
||||
<Selector
|
||||
hiddenOptions
|
||||
onOperate={v => onOperate({ ...v, value: model })}
|
||||
className={open => `${open && '!bg-gray-100 shadow-none'} flex justify-center items-center w-7 h-7 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer hover:bg-gray-100`}
|
||||
deleteText={t('common.operation.remove') || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type QuotaCardProps = {
|
||||
remainTokens: number
|
||||
}
|
||||
|
||||
const QuotaCard: FC<QuotaCardProps> = ({
|
||||
remainTokens,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='px-3 pb-3'>
|
||||
<div className='px-3 py-2 bg-white rounded-lg shadow-xs last:mb-0'>
|
||||
<div className='flex items-center h-[18px] text-xs font-medium text-gray-500'>
|
||||
{t('common.modelProvider.item.freeQuota')}
|
||||
</div>
|
||||
<div className='flex items-center h-5 text-sm font-medium text-gray-700'>
|
||||
{remainTokens}
|
||||
<div className='ml-1 font-normal'>Tokens</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuotaCard
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type { FormValue, Provider, ProviderConfigItem, ProviderWithConfig, ProviderWithQuota } from '../declarations'
|
||||
import Indicator from '../../../indicator'
|
||||
import Selector from '../selector'
|
||||
import I18n from '@/context/i18n'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
type SettingProps = {
|
||||
currentProvider?: Provider
|
||||
modelItem: ProviderConfigItem
|
||||
onOpenModal: (v?: FormValue) => void
|
||||
onOperate: (v: Record<string, any>) => void
|
||||
}
|
||||
|
||||
const Setting: FC<SettingProps> = ({
|
||||
currentProvider,
|
||||
modelItem,
|
||||
onOpenModal,
|
||||
onOperate,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const { t } = useTranslation()
|
||||
const configurable = currentProvider?.model_flexibility === 'configurable'
|
||||
const systemFree = currentProvider?.providers.find(p => p.provider_type === 'system' && (p as ProviderWithQuota).quota_type === 'free') as ProviderWithQuota
|
||||
const custom = currentProvider?.providers.find(p => p.provider_type === 'custom') as ProviderWithConfig
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
modelItem.disable && !IS_CE_EDITION && (
|
||||
<div className='flex items-center text-xs text-gray-500'>
|
||||
{modelItem.disable.tip[locale]}
|
||||
<a
|
||||
className={`${locale === 'en' && 'ml-1'} text-primary-600 cursor-pointer`}
|
||||
href={modelItem.disable.link.href[locale]}
|
||||
target='_blank'
|
||||
>
|
||||
{modelItem.disable.link.label[locale]}
|
||||
</a>
|
||||
<div className='mx-2 w-[1px] h-4 bg-black/5' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
configurable && (
|
||||
<Button
|
||||
className={`!px-3 !h-7 rounded-md bg-white !text-xs font-medium text-gray-700 ${!!modelItem.disable && '!text-gray-300'}`}
|
||||
onClick={() => onOpenModal()}
|
||||
>
|
||||
{t('common.operation.add')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{
|
||||
!configurable && custom?.config && (
|
||||
<div className='flex items-center'>
|
||||
<Indicator className='mr-3' />
|
||||
<Button
|
||||
className='mr-1 !px-3 !h-7 rounded-md bg-white !text-xs font-medium text-gray-700'
|
||||
onClick={() => onOpenModal(custom.config)}
|
||||
>
|
||||
{t('common.operation.edit')}
|
||||
</Button>
|
||||
<Selector
|
||||
hiddenOptions={!systemFree?.is_valid || IS_CE_EDITION}
|
||||
value={currentProvider?.preferred_provider_type}
|
||||
onOperate={onOperate}
|
||||
className={open => `${open && '!bg-gray-100 shadow-none'} flex justify-center items-center w-7 h-7 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer hover:bg-gray-100`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!configurable && !custom?.config && (
|
||||
<Button
|
||||
className={`!px-3 !h-7 rounded-md bg-white !text-xs font-medium text-gray-700 ${!!modelItem.disable && !IS_CE_EDITION && '!text-gray-300'}`}
|
||||
onClick={() => onOpenModal()}
|
||||
disabled={!!modelItem.disable && !IS_CE_EDITION}
|
||||
>
|
||||
{t('common.operation.setup')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Setting
|
||||
@@ -0,0 +1,4 @@
|
||||
.vender {
|
||||
background: linear-gradient(131deg, #2250F2 0%, #0EBCF3 100%);
|
||||
background-clip: text;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { FC } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type {
|
||||
FormValue,
|
||||
Provider,
|
||||
ProviderConfigItem,
|
||||
ProviderWithModels,
|
||||
ProviderWithQuota,
|
||||
} from '../declarations'
|
||||
import Setting from './Setting'
|
||||
import Card from './Card'
|
||||
import QuotaCard from './QuotaCard'
|
||||
import I18n from '@/context/i18n'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
type ModelItemProps = {
|
||||
currentProvider?: Provider
|
||||
modelItem: ProviderConfigItem
|
||||
onOpenModal: (v?: FormValue) => void
|
||||
onOperate: (v: Record<string, any>) => void
|
||||
onUpdate: () => void
|
||||
}
|
||||
|
||||
const ModelItem: FC<ModelItemProps> = ({
|
||||
currentProvider,
|
||||
modelItem,
|
||||
onOpenModal,
|
||||
onOperate,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const custom = currentProvider?.providers.find(p => p.provider_type === 'custom') as ProviderWithModels
|
||||
const systemFree = currentProvider?.providers.find(p => p.provider_type === 'system' && (p as ProviderWithQuota).quota_type === 'free') as ProviderWithQuota
|
||||
|
||||
return (
|
||||
<div className='mb-2 bg-gray-50 rounded-xl'>
|
||||
<div className='flex justify-between items-center px-4 h-14'>
|
||||
<div className='flex items-center'>
|
||||
{modelItem.titleIcon[locale]}
|
||||
{
|
||||
modelItem.hit && (
|
||||
<div className='ml-2 text-xs text-gray-500'>{modelItem.hit[locale]}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Setting
|
||||
currentProvider={currentProvider}
|
||||
modelItem={modelItem}
|
||||
onOpenModal={onOpenModal}
|
||||
onOperate={onOperate}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
!!custom?.models?.length && (
|
||||
<Card
|
||||
providerType={modelItem.key}
|
||||
models={custom?.models}
|
||||
onOpenModal={onOpenModal}
|
||||
onOperate={onOperate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
systemFree?.is_valid && !IS_CE_EDITION && (
|
||||
<QuotaCard remainTokens={systemFree.quota_limit - systemFree.quota_used}/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelItem
|
||||
@@ -0,0 +1,177 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type { Field, FormValue, ProviderConfigModal } from '../declarations'
|
||||
import { useValidate } from '../../key-validator/hooks'
|
||||
import { ValidatingTip } from '../../key-validator/ValidateStatus'
|
||||
import { validateModelProviderFn } from '../utils'
|
||||
import Input from './Input'
|
||||
import I18n from '@/context/i18n'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
|
||||
type FormProps = {
|
||||
modelModal?: ProviderConfigModal
|
||||
initValue?: FormValue
|
||||
fields: Field[]
|
||||
onChange: (v: FormValue) => void
|
||||
onValidatedError: (v: string) => void
|
||||
mode: string
|
||||
cleared: boolean
|
||||
onClearedChange: Dispatch<SetStateAction<boolean>>
|
||||
onValidating: (validating: boolean) => void
|
||||
}
|
||||
|
||||
const nameClassName = `
|
||||
py-2 text-sm text-gray-900
|
||||
`
|
||||
|
||||
const Form: FC<FormProps> = ({
|
||||
modelModal,
|
||||
initValue = {},
|
||||
fields,
|
||||
onChange,
|
||||
onValidatedError,
|
||||
mode,
|
||||
cleared,
|
||||
onClearedChange,
|
||||
onValidating,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const [value, setValue] = useState(initValue)
|
||||
const [validate, validating, validatedStatusState] = useValidate(value)
|
||||
const [changeKey, setChangeKey] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
onValidatedError(validatedStatusState.message || '')
|
||||
}, [validatedStatusState, onValidatedError])
|
||||
useEffect(() => {
|
||||
onValidating(validating)
|
||||
}, [validating, onValidating])
|
||||
|
||||
const updateValue = (v: FormValue) => {
|
||||
setValue(v)
|
||||
onChange(v)
|
||||
}
|
||||
|
||||
const handleMultiFormChange = (v: FormValue, newChangeKey: string) => {
|
||||
updateValue(v)
|
||||
setChangeKey(newChangeKey)
|
||||
|
||||
const validateKeys = (typeof modelModal?.validateKeys === 'function' ? modelModal?.validateKeys(v) : modelModal?.validateKeys) || []
|
||||
if (validateKeys.length) {
|
||||
validate({
|
||||
before: () => {
|
||||
for (let i = 0; i < validateKeys.length; i++) {
|
||||
if (!v[validateKeys[i]])
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
run: () => {
|
||||
return validateModelProviderFn(modelModal!.key, v)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = (saveValue?: FormValue) => {
|
||||
const needClearFields = modelModal?.fields.filter(field => field.type !== 'radio')
|
||||
const newValue: Record<string, string> = {}
|
||||
needClearFields?.forEach((field) => {
|
||||
newValue[field.key] = ''
|
||||
})
|
||||
updateValue({ ...value, ...newValue, ...saveValue })
|
||||
onClearedChange(true)
|
||||
}
|
||||
|
||||
const handleFormChange = (k: string, v: string) => {
|
||||
if (mode === 'edit' && !cleared)
|
||||
handleClear({ [k]: v })
|
||||
else
|
||||
handleMultiFormChange({ ...value, [k]: v }, k)
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
if (mode === 'edit' && !cleared)
|
||||
handleClear()
|
||||
}
|
||||
|
||||
const renderField = (field: Field) => {
|
||||
const hidden = typeof field.hidden === 'function' ? field.hidden(value) : field.hidden
|
||||
|
||||
if (hidden)
|
||||
return null
|
||||
|
||||
if (field.type === 'text') {
|
||||
return (
|
||||
<div key={field.key} className='py-3'>
|
||||
<div className={nameClassName}>{field.label[locale]}</div>
|
||||
<Input
|
||||
field={field}
|
||||
value={value}
|
||||
onChange={v => handleMultiFormChange(v, field.key)}
|
||||
onFocus={handleFocus}
|
||||
validatedStatusState={validatedStatusState}
|
||||
/>
|
||||
{validating && changeKey === field.key && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'radio') {
|
||||
const options = typeof field.options === 'function' ? field.options(value) : field.options
|
||||
return (
|
||||
<div key={field.key} className='py-3'>
|
||||
<div className={nameClassName}>{field.label[locale]}</div>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
{
|
||||
options?.map(option => (
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-3 h-9 rounded-lg border border-gray-100 bg-gray-25 cursor-pointer
|
||||
${value?.[field.key] === option.key && 'bg-white border-[1.5px] border-primary-400 shadow-sm'}
|
||||
`}
|
||||
onClick={() => handleFormChange(field.key, option.key)}
|
||||
key={`${field.key}-${option.key}`}
|
||||
>
|
||||
<div className={`
|
||||
flex justify-center items-center mr-2 w-4 h-4 border border-gray-300 rounded-full
|
||||
${value?.[field.key] === option.key && 'border-[5px] border-primary-600'}
|
||||
`} />
|
||||
<div className='text-sm text-gray-900'>{option.label[locale]}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{validating && changeKey === field.key && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'select') {
|
||||
const options = typeof field.options === 'function' ? field.options(value) : field.options
|
||||
|
||||
return (
|
||||
<div key={field.key} className='py-3'>
|
||||
<div className={nameClassName}>{field.label[locale]}</div>
|
||||
<SimpleSelect
|
||||
defaultValue={value[field.key]}
|
||||
items={options!.map(option => ({ value: option.key, name: option.label[locale] }))}
|
||||
onSelect={item => handleFormChange(field.key, item.value as string)}
|
||||
/>
|
||||
{validating && changeKey === field.key && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
fields.map(field => renderField(field))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { FC } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type { Field, FormValue } from '../declarations'
|
||||
import { ValidatedSuccessIcon } from '../../key-validator/ValidateStatus'
|
||||
import { ValidatedStatus } from '../../key-validator/declarations'
|
||||
import type { ValidatedStatusState } from '../../key-validator/declarations'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
type InputProps = {
|
||||
field: Field
|
||||
value: FormValue
|
||||
onChange: (v: FormValue) => void
|
||||
onFocus: () => void
|
||||
validatedStatusState: ValidatedStatusState
|
||||
}
|
||||
const Input: FC<InputProps> = ({
|
||||
field,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
validatedStatusState,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const showValidatedIcon = validatedStatusState.status === ValidatedStatus.Success && value[field.key]
|
||||
|
||||
const getValidatedIcon = () => {
|
||||
if (showValidatedIcon)
|
||||
return <div className='absolute top-2.5 right-2.5'><ValidatedSuccessIcon /></div>
|
||||
}
|
||||
|
||||
const handleChange = (v: string) => {
|
||||
const newFormValue = { ...value, [field.key]: v }
|
||||
onChange(newFormValue)
|
||||
}
|
||||
|
||||
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
|
||||
${showValidatedIcon && 'pr-[30px]'}
|
||||
`}
|
||||
placeholder={field?.placeholder?.[locale] || ''}
|
||||
onChange={e => handleChange(e.target.value)}
|
||||
onFocus={onFocus}
|
||||
value={value[field.key] || ''}
|
||||
/>
|
||||
{getValidatedIcon()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
||||
@@ -0,0 +1,165 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Portal } from '@headlessui/react'
|
||||
import type { FormValue, ProviderConfigModal } from '../declarations'
|
||||
import { ConfigurableProviders } from '../utils'
|
||||
import Form from './Form'
|
||||
import I18n from '@/context/i18n'
|
||||
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 { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
type ModelModalProps = {
|
||||
isShow: boolean
|
||||
onCancel: () => void
|
||||
modelModal?: ProviderConfigModal
|
||||
onSave: (v?: FormValue) => void
|
||||
mode: string
|
||||
}
|
||||
|
||||
const ModelModal: FC<ModelModalProps> = ({
|
||||
isShow,
|
||||
onCancel,
|
||||
modelModal,
|
||||
onSave,
|
||||
mode,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [value, setValue] = useState<FormValue | undefined>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [cleared, setCleared] = useState(false)
|
||||
const [prevIsShow, setPrevIsShow] = useState(isShow)
|
||||
const [validating, setValidating] = useState(false)
|
||||
|
||||
if (prevIsShow !== isShow) {
|
||||
setCleared(false)
|
||||
setPrevIsShow(isShow)
|
||||
}
|
||||
|
||||
eventEmitter?.useSubscription((v) => {
|
||||
if (v === 'provider-save')
|
||||
setLoading(true)
|
||||
else
|
||||
setLoading(false)
|
||||
})
|
||||
const handleValidatedError = useCallback((newErrorMessage: string) => {
|
||||
setErrorMessage(newErrorMessage)
|
||||
}, [])
|
||||
const handleValidating = useCallback((newValidating: boolean) => {
|
||||
setValidating(newValidating)
|
||||
}, [])
|
||||
const validateRequiredValue = () => {
|
||||
const validateValue = value || modelModal?.defaultValue
|
||||
if (modelModal) {
|
||||
const { fields } = modelModal
|
||||
const requiredFields = fields.filter(field => !(typeof field.hidden === 'function' ? field.hidden(validateValue) : field.hidden) && field.required)
|
||||
|
||||
for (let i = 0; i < requiredFields.length; i++) {
|
||||
const currentField = requiredFields[i]
|
||||
if (!validateValue?.[currentField.key]) {
|
||||
setErrorMessage(t('appDebug.errorMessage.valueOfVarRequired', { key: currentField.label[locale] }) || '')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
const handleSave = () => {
|
||||
if (validateRequiredValue())
|
||||
onSave(value || modelModal?.defaultValue)
|
||||
}
|
||||
|
||||
const renderTitlePrefix = () => {
|
||||
let prefix
|
||||
if (mode === 'edit')
|
||||
prefix = t('common.operation.edit')
|
||||
else
|
||||
prefix = ConfigurableProviders.includes(modelModal!.key) ? t('common.operation.create') : t('common.operation.setup')
|
||||
|
||||
return `${prefix} ${modelModal?.title[locale]}`
|
||||
}
|
||||
|
||||
if (!isShow)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
|
||||
<div className='w-[640px] max-h-screen 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>
|
||||
{modelModal?.icon}
|
||||
</div>
|
||||
<Form
|
||||
modelModal={modelModal}
|
||||
fields={modelModal?.fields || []}
|
||||
initValue={modelModal?.defaultValue}
|
||||
onChange={newValue => setValue(newValue)}
|
||||
onValidatedError={handleValidatedError}
|
||||
mode={mode}
|
||||
cleared={cleared}
|
||||
onClearedChange={setCleared}
|
||||
onValidating={handleValidating}
|
||||
/>
|
||||
<div className='flex justify-between items-center py-6'>
|
||||
<a
|
||||
href={modelModal?.link.href}
|
||||
target='_blank'
|
||||
className='inline-flex items-center text-xs text-primary-600'
|
||||
>
|
||||
{modelModal?.link.label[locale]}
|
||||
<LinkExternal02 className='ml-1 w-3 h-3' />
|
||||
</a>
|
||||
<div>
|
||||
<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 || (mode === 'edit' && !cleared) || validating}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-t-[rgba(0,0,0,0.05)]'>
|
||||
{
|
||||
errorMessage
|
||||
? (
|
||||
<div className='flex px-[10px] py-3 bg-[#FEF3F2] text-xs text-[#D92D20]'>
|
||||
<AlertCircle className='mt-[1px] mr-2 w-[14px] h-[14px]' />
|
||||
{errorMessage}
|
||||
</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>
|
||||
</div>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelModal
|
||||
@@ -0,0 +1,216 @@
|
||||
import type { FC } from 'react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import _ from 'lodash-es'
|
||||
import cn from 'classnames'
|
||||
import type { BackendModel, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
|
||||
import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Check, SearchLg } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { AlertCircle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
|
||||
import ModelName, { supportI18nModelName } from '@/app/components/app/configuration/config-model/model-name'
|
||||
import ProviderName from '@/app/components/app/configuration/config-model/provider-name'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
type Props = {
|
||||
value: {
|
||||
providerName: ProviderEnum
|
||||
modelName: string
|
||||
} | undefined
|
||||
modelType: ModelType
|
||||
supportAgentThought?: boolean
|
||||
onChange: (value: BackendModel) => void
|
||||
popClassName?: string
|
||||
readonly?: boolean
|
||||
triggerIconSmall?: boolean
|
||||
}
|
||||
|
||||
const ModelSelector: FC<Props> = ({
|
||||
value,
|
||||
modelType,
|
||||
supportAgentThought,
|
||||
onChange,
|
||||
popClassName,
|
||||
readonly,
|
||||
triggerIconSmall,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { textGenerationModelList, embeddingsModelList, speech2textModelList, agentThoughtModelList } = useProviderContext()
|
||||
const [search, setSearch] = useState('')
|
||||
const modelList = supportAgentThought
|
||||
? agentThoughtModelList
|
||||
: ({
|
||||
[ModelType.textGeneration]: textGenerationModelList,
|
||||
[ModelType.embeddings]: embeddingsModelList,
|
||||
[ModelType.speech2text]: speech2textModelList,
|
||||
})[modelType]
|
||||
const allModelNames = (() => {
|
||||
if (!search)
|
||||
return {}
|
||||
|
||||
const res: Record<string, string> = {}
|
||||
modelList.forEach(({ model_name }) => {
|
||||
res[model_name] = supportI18nModelName.includes(model_name) ? t(`common.modelName.${model_name}`) : model_name
|
||||
})
|
||||
return res
|
||||
})()
|
||||
const filteredModelList = search
|
||||
? modelList.filter(({ model_name }) => {
|
||||
if (allModelNames[model_name].includes(search))
|
||||
return true
|
||||
|
||||
return false
|
||||
})
|
||||
: modelList
|
||||
|
||||
const hasRemoved = value && !modelList.find(({ model_name }) => model_name === value.modelName)
|
||||
|
||||
const modelOptions: any[] = (() => {
|
||||
const providers = _.uniq(filteredModelList.map(item => item.model_provider.provider_name))
|
||||
const res: any[] = []
|
||||
providers.forEach((providerName) => {
|
||||
res.push({
|
||||
type: 'provider',
|
||||
value: providerName,
|
||||
})
|
||||
const models = filteredModelList.filter(m => m.model_provider.provider_name === providerName)
|
||||
models.forEach(({ model_name }) => {
|
||||
res.push({
|
||||
type: 'model',
|
||||
providerName,
|
||||
value: model_name,
|
||||
})
|
||||
})
|
||||
})
|
||||
return res
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className=''>
|
||||
<Popover className='relative'>
|
||||
<Popover.Button className={cn('flex items-center px-2.5 w-full h-9 rounded-lg', readonly ? '!cursor-auto' : 'bg-gray-100', hasRemoved && '!bg-[#FEF3F2]')}>
|
||||
{
|
||||
({ open }) => (
|
||||
<>
|
||||
{
|
||||
value
|
||||
? (
|
||||
<>
|
||||
<ModelIcon
|
||||
className={cn('mr-1.5', !triggerIconSmall && 'w-5 h-5')}
|
||||
modelId={value.modelName}
|
||||
providerName={value.providerName}
|
||||
/>
|
||||
<div className='mr-1.5 grow text-left text-sm text-gray-900 truncate'><ModelName modelId={value.modelName} /></div>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<div className='grow text-left text-sm text-gray-800 opacity-60'>{t('common.modelProvider.selectModel')}</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasRemoved && (
|
||||
<Tooltip
|
||||
selector='model-selector-remove-tip'
|
||||
htmlContent={
|
||||
<div className='w-[261px] text-gray-500'>{t('common.modelProvider.selector.tip')}</div>
|
||||
}
|
||||
>
|
||||
<AlertCircle className='mr-1 w-4 h-4 text-[#F04438]' />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
{!readonly && <ChevronDown className={`w-4 h-4 text-gray-700 ${open ? 'opacity-100' : 'opacity-60'}`} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Popover.Button>
|
||||
{!readonly && (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave='transition ease-in duration-100'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<Popover.Panel className={cn(popClassName, 'absolute top-10 p-1 min-w-[232px] max-w-[260px] max-h-[366px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg overflow-auto z-10')}>
|
||||
<div className='px-2 pt-2 pb-1'>
|
||||
<div className='flex items-center px-2 h-8 bg-gray-100 rounded-lg'>
|
||||
<div className='mr-1.5 p-[1px]'><SearchLg className='w-[14px] h-[14px] text-gray-400' /></div>
|
||||
<div className='grow px-0.5'>
|
||||
<input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className={`
|
||||
block w-full h-8 bg-transparent text-[13px] text-gray-700
|
||||
outline-none appearance-none border-none
|
||||
`}
|
||||
placeholder={t('common.modelProvider.searchModel') || ''}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
search && (
|
||||
<div className='ml-1 p-0.5 cursor-pointer' onClick={() => setSearch('')}>
|
||||
<XCircle className='w-3 h-3 text-gray-400' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
modelOptions.map((model: any) => {
|
||||
if (model.type === 'provider') {
|
||||
return (
|
||||
<div
|
||||
className='px-3 pt-2 pb-1 text-xs font-medium text-gray-500'
|
||||
key={`${model.type}-${model.value}`}
|
||||
>
|
||||
<ProviderName provideName={model.value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (model.type === 'model') {
|
||||
return (
|
||||
<Popover.Button
|
||||
key={`${model.providerName}-${model.value}`}
|
||||
className={`
|
||||
flex items-center px-3 w-full h-8 rounded-lg hover:bg-gray-50
|
||||
${!readonly ? 'cursor-pointer' : 'cursor-auto'}
|
||||
${(value?.providerName === model.providerName && value?.modelName === model.value) && 'bg-gray-50'}
|
||||
`}
|
||||
onClick={() => {
|
||||
const selectedModel = modelList.find((item) => {
|
||||
return item.model_name === model.value && item.model_provider.provider_name === model.providerName
|
||||
})
|
||||
onChange(selectedModel as BackendModel)
|
||||
}}
|
||||
>
|
||||
<ModelIcon
|
||||
className='mr-2 shrink-0'
|
||||
modelId={model.value}
|
||||
providerName={model.providerName}
|
||||
/>
|
||||
<div className='grow text-left text-sm text-gray-900 truncate'><ModelName modelId={model.value} /></div>
|
||||
{ (value?.providerName === model.providerName && value?.modelName === model.value) && <Check className='shrink-0 w-4 h-4 text-primary-600' /> }
|
||||
</Popover.Button>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
}
|
||||
{(search && filteredModelList.length === 0) && (
|
||||
<div className='px-3 pt-1.5 h-[30px] text-center text-xs text-gray-500'>{t('common.modelProvider.noModelFound', { model: search })}</div>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelSelector
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Fragment } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Check, DotsHorizontal, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
const itemClassName = `
|
||||
flex items-center px-3 h-9 text-sm text-gray-700 rounded-lg cursor-pointer
|
||||
`
|
||||
|
||||
type SelectorProps = {
|
||||
value?: string
|
||||
onOperate: (v: Record<string, string>) => void
|
||||
hiddenOptions?: boolean
|
||||
className?: (v: boolean) => string
|
||||
deleteText?: string
|
||||
}
|
||||
const Selector: FC<SelectorProps> = ({
|
||||
value,
|
||||
onOperate,
|
||||
hiddenOptions,
|
||||
className,
|
||||
deleteText,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const options = [
|
||||
{
|
||||
key: 'custom',
|
||||
text: 'API',
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
text: t('common.modelProvider.quota'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Popover className='relative'>
|
||||
<Popover.Button>
|
||||
{
|
||||
({ open }) => (
|
||||
<div className={`
|
||||
flex justify-center items-center w-6 h-6 rounded-md hover:bg-gray-50 cursor-pointer
|
||||
${open && 'bg-gray-50'}
|
||||
${className && className(open)}
|
||||
`}>
|
||||
<DotsHorizontal className='w-3 h-3 text-gray-700' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</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-[192px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg z-10'>
|
||||
{
|
||||
!hiddenOptions && (
|
||||
<>
|
||||
<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={`${itemClassName} hover:bg-gray-50`}
|
||||
onClick={() => onOperate({ type: 'priority', value: option.key })}>
|
||||
<div className='grow'>{option.text}</div>
|
||||
{value === option.key && <Check className='w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
</Popover.Button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='h-[1px] bg-gray-100' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className='p-1'>
|
||||
<Popover.Button as={Fragment}>
|
||||
<div
|
||||
className={`group ${itemClassName} hover:bg-[#FEF3F2] hover:text-[#D92D20]`}
|
||||
onClick={() => onOperate({ type: 'delete' })}>
|
||||
<Trash03 className='mr-2 w-4 h-4 text-gray-500 group-hover:text-[#D92D20]' />
|
||||
{deleteText || t('common.modelProvider.card.removeKey')}
|
||||
</div>
|
||||
</Popover.Button>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default Selector
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ValidatedStatus } from '../key-validator/declarations'
|
||||
import { ProviderEnum } from './declarations'
|
||||
import { validateModelProvider } from '@/service/common'
|
||||
|
||||
export const ConfigurableProviders = [ProviderEnum.azure_openai, ProviderEnum.replicate, ProviderEnum.huggingface_hub]
|
||||
|
||||
export const validateModelProviderFn = async (providerName: ProviderEnum, v: any) => {
|
||||
let body, url
|
||||
|
||||
if (ConfigurableProviders.includes(providerName)) {
|
||||
const { model_name, model_type, ...config } = v
|
||||
body = {
|
||||
model_name,
|
||||
model_type,
|
||||
config,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${providerName}/models/validate`
|
||||
}
|
||||
else {
|
||||
body = {
|
||||
config: v,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${providerName}/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 })
|
||||
}
|
||||
catch (e: any) {
|
||||
return Promise.resolve({ status: ValidatedStatus.Error, message: e.message })
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
background: url(../../../assets/anthropic.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.bar {
|
||||
background: linear-gradient(90deg, rgba(41, 112, 255, 0.9) 0%, rgba(21, 94, 239, 0.9) 100%);
|
||||
}
|
||||
|
||||
.bar-error {
|
||||
background: linear-gradient(90deg, rgba(240, 68, 56, 0.72) 0%, rgba(217, 45, 32, 0.9) 100%);
|
||||
}
|
||||
|
||||
.bar-item {
|
||||
width: 10%;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.bar-item:last-of-type {
|
||||
border-right: 0;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import s from './index.module.css'
|
||||
import type { ProviderHosted } from '@/models/common'
|
||||
|
||||
type AnthropicHostedProviderProps = {
|
||||
provider: ProviderHosted
|
||||
}
|
||||
const AnthropicHostedProvider = ({
|
||||
provider,
|
||||
}: AnthropicHostedProviderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const exhausted = provider.quota_used > provider.quota_limit
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
border-[0.5px] border-gray-200 rounded-xl
|
||||
${exhausted ? 'bg-[#FFFBFA]' : 'bg-gray-50'}
|
||||
`}>
|
||||
<div className='pt-4 px-4 pb-3'>
|
||||
<div className='flex items-center mb-3'>
|
||||
<div className={s.icon} />
|
||||
<div className='grow text-sm font-medium text-gray-800'>
|
||||
{t('common.provider.anthropicHosted.anthropicHosted')}
|
||||
</div>
|
||||
<div className={`
|
||||
px-2 h-[22px] flex items-center rounded-md border
|
||||
text-xs font-semibold
|
||||
${exhausted ? 'border-[#D92D20] text-[#D92D20]' : 'border-primary-600 text-primary-600'}
|
||||
`}>
|
||||
{exhausted ? t('common.provider.anthropicHosted.exhausted') : t('common.provider.anthropicHosted.onTrial')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-[13px] text-gray-500'>{t('common.provider.anthropicHosted.desc')}</div>
|
||||
</div>
|
||||
<div className='flex items-center h-[42px] px-4 border-t-[0.5px] border-t-[rgba(0, 0, 0, 0.05)]'>
|
||||
<div className='text-[13px] text-gray-700'>{t('common.provider.anthropicHosted.callTimes')}</div>
|
||||
<div className='relative grow h-2 flex bg-gray-200 rounded-md mx-2 overflow-hidden'>
|
||||
<div
|
||||
className={cn(s.bar, exhausted && s['bar-error'], 'absolute top-0 left-0 right-0 bottom-0')}
|
||||
style={{ width: `${(provider.quota_used / provider.quota_limit * 100).toFixed(2)}%` }}
|
||||
/>
|
||||
{Array(10).fill(0).map((i, k) => (
|
||||
<div key={k} className={s['bar-item']} />
|
||||
))}
|
||||
</div>
|
||||
<div className={`
|
||||
text-[13px] font-medium ${exhausted ? 'text-[#D92D20]' : 'text-gray-700'}
|
||||
`}>{provider.quota_used}/{provider.quota_limit}</div>
|
||||
</div>
|
||||
{
|
||||
exhausted && (
|
||||
<div className='
|
||||
px-4 py-3 leading-[18px] flex items-center text-[13px] text-gray-700 font-medium
|
||||
bg-[#FFFAEB] border-t border-t-[rgba(0, 0, 0, 0.05)] rounded-b-xl
|
||||
'>
|
||||
{t('common.provider.anthropicHosted.usedUp')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnthropicHostedProvider
|
||||
@@ -1,90 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
|
||||
import ProviderInput from '../provider-input'
|
||||
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
|
||||
import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
|
||||
import {
|
||||
ValidatedErrorIcon,
|
||||
ValidatedErrorOnOpenaiTip,
|
||||
ValidatedSuccessIcon,
|
||||
ValidatingTip,
|
||||
} from '../provider-input/Validate'
|
||||
import type { Provider, ProviderAnthropicToken } from '@/models/common'
|
||||
|
||||
type AnthropicProviderProps = {
|
||||
provider: Provider
|
||||
onValidatedStatus: (status?: ValidatedStatusState) => void
|
||||
onTokenChange: (token: ProviderAnthropicToken) => void
|
||||
}
|
||||
|
||||
const AnthropicProvider = ({
|
||||
provider,
|
||||
onValidatedStatus,
|
||||
onTokenChange,
|
||||
}: AnthropicProviderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [token, setToken] = useState<ProviderAnthropicToken>((provider.token as ProviderAnthropicToken) || { anthropic_api_key: '' })
|
||||
const [validating, validatedStatus, setValidatedStatus, validate] = useValidateToken(provider.provider_name)
|
||||
const handleFocus = () => {
|
||||
if (token.anthropic_api_key === (provider.token as ProviderAnthropicToken).anthropic_api_key) {
|
||||
setToken({ anthropic_api_key: '' })
|
||||
onTokenChange({ anthropic_api_key: '' })
|
||||
setValidatedStatus({})
|
||||
}
|
||||
}
|
||||
const handleChange = (v: string) => {
|
||||
const apiKey = { anthropic_api_key: v }
|
||||
setToken(apiKey)
|
||||
onTokenChange(apiKey)
|
||||
validate(apiKey, {
|
||||
beforeValidating: () => {
|
||||
if (!v) {
|
||||
setValidatedStatus({})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
if (typeof onValidatedStatus === 'function')
|
||||
onValidatedStatus(validatedStatus)
|
||||
}, [validatedStatus])
|
||||
|
||||
const getValidatedIcon = () => {
|
||||
if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed)
|
||||
return <ValidatedErrorIcon />
|
||||
|
||||
if (validatedStatus.status === ValidatedStatus.Success)
|
||||
return <ValidatedSuccessIcon />
|
||||
}
|
||||
const getValidatedTip = () => {
|
||||
if (validating)
|
||||
return <ValidatingTip />
|
||||
|
||||
if (validatedStatus?.status === ValidatedStatus.Error)
|
||||
return <ValidatedErrorOnOpenaiTip errorMessage={validatedStatus.message ?? ''} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='px-4 pt-3 pb-4'>
|
||||
<ProviderInput
|
||||
value={token.anthropic_api_key}
|
||||
name={t('common.provider.apiKey')}
|
||||
placeholder={t('common.provider.enterYourKey')}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
validatedIcon={getValidatedIcon()}
|
||||
validatedTip={getValidatedTip()}
|
||||
/>
|
||||
<Link className="inline-flex items-center mt-3 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://docs.anthropic.com/claude/reference/getting-started-with-the-api" target={'_blank'}>
|
||||
{t('common.provider.anthropic.keyFrom')}
|
||||
<ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnthropicProvider
|
||||
@@ -1,100 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ProviderInput from '../provider-input'
|
||||
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
|
||||
import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
|
||||
import {
|
||||
ValidatedErrorIcon,
|
||||
ValidatedErrorOnAzureOpenaiTip,
|
||||
ValidatedSuccessIcon,
|
||||
ValidatingTip,
|
||||
} from '../provider-input/Validate'
|
||||
import { ProviderName } from '@/models/common'
|
||||
import type { Provider, ProviderAzureToken } from '@/models/common'
|
||||
|
||||
type IAzureProviderProps = {
|
||||
provider: Provider
|
||||
onValidatedStatus: (status?: ValidatedStatusState) => void
|
||||
onTokenChange: (token: ProviderAzureToken) => void
|
||||
}
|
||||
const AzureProvider = ({
|
||||
provider,
|
||||
onTokenChange,
|
||||
onValidatedStatus,
|
||||
}: IAzureProviderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [token, setToken] = useState<ProviderAzureToken>(provider.provider_name === ProviderName.AZURE_OPENAI ? { ...provider.token } : {})
|
||||
const [validating, validatedStatus, setValidatedStatus, validate] = useValidateToken(provider.provider_name)
|
||||
const handleFocus = (type: keyof ProviderAzureToken) => {
|
||||
if (token[type] === (provider?.token as ProviderAzureToken)[type]) {
|
||||
token[type] = ''
|
||||
setToken({ ...token })
|
||||
onTokenChange({ ...token })
|
||||
setValidatedStatus({})
|
||||
}
|
||||
}
|
||||
const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => {
|
||||
token[type] = v
|
||||
setToken({ ...token })
|
||||
onTokenChange({ ...token })
|
||||
validate({ ...token }, {
|
||||
beforeValidating: () => {
|
||||
if (!token.openai_api_base || !token.openai_api_key) {
|
||||
setValidatedStatus({})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
}
|
||||
const getValidatedIcon = () => {
|
||||
if (validatedStatus.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed)
|
||||
return <ValidatedErrorIcon />
|
||||
|
||||
if (validatedStatus.status === ValidatedStatus.Success)
|
||||
return <ValidatedSuccessIcon />
|
||||
}
|
||||
const getValidatedTip = () => {
|
||||
if (validating)
|
||||
return <ValidatingTip />
|
||||
|
||||
if (validatedStatus.status === ValidatedStatus.Error)
|
||||
return <ValidatedErrorOnAzureOpenaiTip errorMessage={validatedStatus.message ?? ''} />
|
||||
}
|
||||
useEffect(() => {
|
||||
if (typeof onValidatedStatus === 'function')
|
||||
onValidatedStatus(validatedStatus)
|
||||
}, [validatedStatus])
|
||||
|
||||
return (
|
||||
<div className='px-4 py-3'>
|
||||
<ProviderInput
|
||||
className='mb-4'
|
||||
name={t('common.provider.azure.apiBase')}
|
||||
placeholder={t('common.provider.azure.apiBasePlaceholder')}
|
||||
value={token.openai_api_base}
|
||||
onChange={v => handleChange('openai_api_base', v, validate)}
|
||||
onFocus={() => handleFocus('openai_api_base')}
|
||||
validatedIcon={getValidatedIcon()}
|
||||
/>
|
||||
<ProviderInput
|
||||
className='mb-4'
|
||||
name={t('common.provider.azure.apiKey')}
|
||||
placeholder={t('common.provider.azure.apiKeyPlaceholder')}
|
||||
value={token.openai_api_key}
|
||||
onChange={v => handleChange('openai_api_key', v, validate)}
|
||||
onFocus={() => handleFocus('openai_api_key')}
|
||||
validatedIcon={getValidatedIcon()}
|
||||
validatedTip={getValidatedTip()}
|
||||
/>
|
||||
<Link className="flex items-center text-xs cursor-pointer text-primary-600" href="https://platform.openai.com/account/api-keys" target={'_blank'}>
|
||||
{t('common.provider.azure.helpTip')}
|
||||
<ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AzureProvider
|
||||
@@ -1,17 +0,0 @@
|
||||
.wrapper .button {
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.gpt-icon {
|
||||
margin-right: 12px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url(../../assets/gpt.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.input {
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { LockClosedIcon } from '@heroicons/react/24/solid'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import ProviderItem from './provider-item'
|
||||
import OpenaiHostedProvider from './openai-hosted-provider'
|
||||
import AnthropicHostedProvider from './anthropic-hosted-provider'
|
||||
import type { ProviderHosted } from '@/models/common'
|
||||
import { fetchProviders } from '@/service/common'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
const providersMap: { [k: string]: any } = {
|
||||
'openai-custom': {
|
||||
icon: 'openai',
|
||||
name: 'OpenAI',
|
||||
},
|
||||
'azure_openai-custom': {
|
||||
icon: 'azure',
|
||||
name: 'Azure OpenAI Service',
|
||||
},
|
||||
'anthropic-custom': {
|
||||
icon: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
},
|
||||
}
|
||||
|
||||
// const providersList = [
|
||||
// {
|
||||
// id: 'openai',
|
||||
// name: 'OpenAI',
|
||||
// providerKey: '1',
|
||||
// status: '',
|
||||
// child: <OpenaiProvider />
|
||||
// },
|
||||
// {
|
||||
// id: 'azure',
|
||||
// name: 'Azure OpenAI Service',
|
||||
// providerKey: '1',
|
||||
// status: 'error',
|
||||
// child: <AzureProvider />
|
||||
// },
|
||||
// {
|
||||
// id: 'anthropic',
|
||||
// name: 'Anthropic',
|
||||
// providerKey: '',
|
||||
// status: '',
|
||||
// child: <div>placeholder</div>
|
||||
// },
|
||||
// {
|
||||
// id: 'hugging-face',
|
||||
// name: 'Hugging Face Hub',
|
||||
// providerKey: '',
|
||||
// comingSoon: true,
|
||||
// status: '',
|
||||
// child: <div>placeholder</div>
|
||||
// }
|
||||
// ]
|
||||
|
||||
const ProviderPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const [activeProviderId, setActiveProviderId] = useState('')
|
||||
const { data, mutate } = useSWR({ url: '/workspaces/current/providers' }, fetchProviders)
|
||||
const providers = data?.filter(provider => providersMap[`${provider.provider_name}-${provider.provider_type}`])?.map((provider) => {
|
||||
const providerKey = `${provider.provider_name}-${provider.provider_type}`
|
||||
return {
|
||||
provider,
|
||||
icon: providersMap[providerKey].icon,
|
||||
name: providersMap[providerKey].name,
|
||||
}
|
||||
})
|
||||
const providerHosted = data?.filter(provider => provider.provider_name === 'openai' && provider.provider_type === 'system')?.[0]
|
||||
const anthropicHosted = data?.filter(provider => provider.provider_name === 'anthropic' && provider.provider_type === 'system')?.[0]
|
||||
const providedOpenaiProvider = data?.find(provider => provider.is_enabled && (provider.provider_name === 'openai' || provider.provider_name === 'azure_openai'))
|
||||
|
||||
return (
|
||||
<div className='pb-7'>
|
||||
{
|
||||
providerHosted && !IS_CE_EDITION && (
|
||||
<>
|
||||
<div>
|
||||
<OpenaiHostedProvider provider={providerHosted as ProviderHosted} />
|
||||
</div>
|
||||
<div className='my-5 w-full h-0 border-[0.5px] border-gray-100' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
anthropicHosted && !IS_CE_EDITION && (
|
||||
<>
|
||||
<div>
|
||||
<AnthropicHostedProvider provider={anthropicHosted as ProviderHosted} />
|
||||
</div>
|
||||
<div className='my-5 w-full h-0 border-[0.5px] border-gray-100' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
{
|
||||
providers?.map(providerItem => (
|
||||
<ProviderItem
|
||||
key={`${providerItem.provider.provider_name}-${providerItem.provider.provider_type}`}
|
||||
icon={providerItem.icon}
|
||||
name={providerItem.name}
|
||||
provider={providerItem.provider}
|
||||
activeId={activeProviderId}
|
||||
onActive={aid => setActiveProviderId(aid)}
|
||||
onSave={() => mutate()}
|
||||
providedOpenaiProvider={providedOpenaiProvider}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='fixed bottom-0 w-[472px] h-[42px] flex items-center bg-white text-xs text-gray-500'>
|
||||
<LockClosedIcon className='w-3 h-3 mr-1' />
|
||||
{t('common.provider.encrypted.front')}
|
||||
<Link
|
||||
className='text-primary-600 mx-1'
|
||||
target={'_blank'}
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</Link>
|
||||
{t('common.provider.encrypted.back')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderPage
|
||||
@@ -1,24 +0,0 @@
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
background: url(../../../assets/gpt.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.bar {
|
||||
background: linear-gradient(90deg, rgba(41, 112, 255, 0.9) 0%, rgba(21, 94, 239, 0.9) 100%);
|
||||
}
|
||||
|
||||
.bar-error {
|
||||
background: linear-gradient(90deg, rgba(240, 68, 56, 0.72) 0%, rgba(217, 45, 32, 0.9) 100%);
|
||||
}
|
||||
|
||||
.bar-item {
|
||||
width: 10%;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.bar-item:last-of-type {
|
||||
border-right: 0;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import s from './index.module.css'
|
||||
import cn from 'classnames'
|
||||
import type { ProviderHosted } from '@/models/common'
|
||||
|
||||
interface IOpenaiHostedProviderProps {
|
||||
provider: ProviderHosted
|
||||
}
|
||||
const OpenaiHostedProvider = ({
|
||||
provider
|
||||
}: IOpenaiHostedProviderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const exhausted = provider.quota_used > provider.quota_limit
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
border-[0.5px] border-gray-200 rounded-xl
|
||||
${exhausted ? 'bg-[#FFFBFA]' : 'bg-gray-50'}
|
||||
`}>
|
||||
<div className='pt-4 px-4 pb-3'>
|
||||
<div className='flex items-center mb-3'>
|
||||
<div className={s.icon} />
|
||||
<div className='grow text-sm font-medium text-gray-800'>
|
||||
{t('common.provider.openaiHosted.openaiHosted')}
|
||||
</div>
|
||||
<div className={`
|
||||
px-2 h-[22px] flex items-center rounded-md border
|
||||
text-xs font-semibold
|
||||
${exhausted ? 'border-[#D92D20] text-[#D92D20]' : 'border-primary-600 text-primary-600'}
|
||||
`}>
|
||||
{exhausted ? t('common.provider.openaiHosted.exhausted') : t('common.provider.openaiHosted.onTrial')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-[13px] text-gray-500'>{t('common.provider.openaiHosted.desc')}</div>
|
||||
</div>
|
||||
<div className='flex items-center h-[42px] px-4 border-t-[0.5px] border-t-[rgba(0, 0, 0, 0.05)]'>
|
||||
<div className='text-[13px] text-gray-700'>{t('common.provider.openaiHosted.callTimes')}</div>
|
||||
<div className='relative grow h-2 flex bg-gray-200 rounded-md mx-2 overflow-hidden'>
|
||||
<div
|
||||
className={cn(s.bar, exhausted && s['bar-error'], 'absolute top-0 left-0 right-0 bottom-0')}
|
||||
style={{ width: `${(provider.quota_used / provider.quota_limit * 100).toFixed(2)}%` }}
|
||||
/>
|
||||
{Array(10).fill(0).map((i, k) => (
|
||||
<div key={k} className={s['bar-item']} />
|
||||
))}
|
||||
</div>
|
||||
<div className={`
|
||||
text-[13px] font-medium ${exhausted ? 'text-[#D92D20]' : 'text-gray-700'}
|
||||
`}>{provider.quota_used}/{provider.quota_limit}</div>
|
||||
</div>
|
||||
{
|
||||
exhausted && (
|
||||
<div className='
|
||||
px-4 py-3 leading-[18px] flex items-center text-[13px] text-gray-700 font-medium
|
||||
bg-[#FFFAEB] border-t border-t-[rgba(0, 0, 0, 0.05)] rounded-b-xl
|
||||
'>
|
||||
{t('common.provider.openaiHosted.usedUp')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OpenaiHostedProvider
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
|
||||
import ProviderInput from '../provider-input'
|
||||
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
|
||||
import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
|
||||
import {
|
||||
ValidatedErrorIcon,
|
||||
ValidatedErrorOnOpenaiTip,
|
||||
ValidatedSuccessIcon,
|
||||
ValidatingTip,
|
||||
} from '../provider-input/Validate'
|
||||
import type { Provider } from '@/models/common'
|
||||
|
||||
type IOpenaiProviderProps = {
|
||||
provider: Provider
|
||||
onValidatedStatus: (status?: ValidatedStatusState) => void
|
||||
onTokenChange: (token: string) => void
|
||||
}
|
||||
|
||||
const OpenaiProvider = ({
|
||||
provider,
|
||||
onValidatedStatus,
|
||||
onTokenChange,
|
||||
}: IOpenaiProviderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [token, setToken] = useState(provider.token as string || '')
|
||||
const [validating, validatedStatus, setValidatedStatus, validate] = useValidateToken(provider.provider_name)
|
||||
const handleFocus = () => {
|
||||
if (token === provider.token) {
|
||||
setToken('')
|
||||
onTokenChange('')
|
||||
setValidatedStatus({})
|
||||
}
|
||||
}
|
||||
const handleChange = (v: string) => {
|
||||
setToken(v)
|
||||
onTokenChange(v)
|
||||
validate(v, {
|
||||
beforeValidating: () => {
|
||||
if (!v) {
|
||||
setValidatedStatus({})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
if (typeof onValidatedStatus === 'function')
|
||||
onValidatedStatus(validatedStatus)
|
||||
}, [validatedStatus])
|
||||
|
||||
const getValidatedIcon = () => {
|
||||
if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed)
|
||||
return <ValidatedErrorIcon />
|
||||
|
||||
if (validatedStatus.status === ValidatedStatus.Success)
|
||||
return <ValidatedSuccessIcon />
|
||||
}
|
||||
const getValidatedTip = () => {
|
||||
if (validating)
|
||||
return <ValidatingTip />
|
||||
|
||||
if (validatedStatus?.status === ValidatedStatus.Error)
|
||||
return <ValidatedErrorOnOpenaiTip errorMessage={validatedStatus.message ?? ''} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='px-4 pt-3 pb-4'>
|
||||
<ProviderInput
|
||||
value={token}
|
||||
name={t('common.provider.apiKey')}
|
||||
placeholder={t('common.provider.enterYourKey')}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
validatedIcon={getValidatedIcon()}
|
||||
validatedTip={getValidatedTip()}
|
||||
/>
|
||||
<Link className="inline-flex items-center mt-3 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://platform.openai.com/account/api-keys" target={'_blank'}>
|
||||
{t('appOverview.welcome.getKeyTip')}
|
||||
<ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OpenaiProvider
|
||||
@@ -1,59 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
export const ValidatedErrorIcon = () => {
|
||||
return <ExclamationCircleIcon className='w-4 h-4 text-[#D92D20]' />
|
||||
}
|
||||
|
||||
export const ValidatedSuccessIcon = () => {
|
||||
return <CheckCircleIcon className='w-4 h-4 text-[#039855]' />
|
||||
}
|
||||
|
||||
export const ValidatingTip = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={'mt-2 text-primary-600 text-xs font-normal'}>
|
||||
{t('common.provider.validating')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ValidatedExceedOnOpenaiTip = () => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
|
||||
return (
|
||||
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
|
||||
{t('common.provider.apiKeyExceedBill')}
|
||||
<Link
|
||||
className='underline'
|
||||
href="https://platform.openai.com/account/api-keys"
|
||||
target={'_blank'}>
|
||||
{locale === 'en' ? 'this link' : '这篇文档'}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ValidatedErrorOnOpenaiTip = ({ errorMessage }: { errorMessage: string }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
|
||||
{t('common.provider.validatedError')}{errorMessage}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ValidatedErrorOnAzureOpenaiTip = ({ errorMessage }: { errorMessage: string }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
|
||||
{t('common.provider.validatedError')}{errorMessage}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { ChangeEvent } from 'react'
|
||||
import type { ReactElement } from 'react-markdown/lib/react-markdown'
|
||||
|
||||
type IProviderInputProps = {
|
||||
value?: string
|
||||
name: string
|
||||
placeholder: string
|
||||
className?: string
|
||||
onChange: (v: string) => void
|
||||
onFocus?: () => void
|
||||
validatedIcon?: ReactElement
|
||||
validatedTip?: ReactElement
|
||||
}
|
||||
|
||||
const ProviderInput = ({
|
||||
value,
|
||||
name,
|
||||
placeholder,
|
||||
className,
|
||||
onChange,
|
||||
onFocus,
|
||||
validatedIcon,
|
||||
validatedTip,
|
||||
}: IProviderInputProps) => {
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = e.target.value
|
||||
onChange(inputValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="mb-2 text-[13px] font-medium text-gray-800">{name}</div>
|
||||
<div className='
|
||||
flex items-center px-3 bg-white rounded-lg
|
||||
shadow-[0_1px_2px_rgba(16,24,40,0.05)]
|
||||
'>
|
||||
<input
|
||||
className='
|
||||
w-full py-[9px]
|
||||
text-xs font-medium text-gray-700 leading-[18px]
|
||||
appearance-none outline-none bg-transparent
|
||||
'
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
{validatedIcon}
|
||||
</div>
|
||||
{validatedTip}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderInput
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import debounce from 'lodash-es/debounce'
|
||||
import type { DebouncedFunc } from 'lodash-es'
|
||||
import { validateProviderKey } from '@/service/common'
|
||||
|
||||
export enum ValidatedStatus {
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
Exceed = 'exceed',
|
||||
}
|
||||
export type ValidatedStatusState = {
|
||||
status?: ValidatedStatus
|
||||
message?: string
|
||||
}
|
||||
// export type ValidatedStatusState = ValidatedStatus | undefined | ValidatedError
|
||||
export type SetValidatedStatus = Dispatch<SetStateAction<ValidatedStatusState>>
|
||||
export type ValidateFn = DebouncedFunc<(token: any, config: ValidateFnConfig) => void>
|
||||
type ValidateTokenReturn = [
|
||||
boolean,
|
||||
ValidatedStatusState,
|
||||
SetValidatedStatus,
|
||||
ValidateFn,
|
||||
]
|
||||
export type ValidateFnConfig = {
|
||||
beforeValidating: (token: any) => boolean
|
||||
}
|
||||
|
||||
const useValidateToken = (providerName: string): ValidateTokenReturn => {
|
||||
const [validating, setValidating] = useState(false)
|
||||
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({})
|
||||
const validate = useCallback(debounce(async (token: string, config: ValidateFnConfig) => {
|
||||
if (!config.beforeValidating(token))
|
||||
return false
|
||||
|
||||
setValidating(true)
|
||||
try {
|
||||
const res = await validateProviderKey({ url: `/workspaces/current/providers/${providerName}/token-validate`, body: { token } })
|
||||
setValidatedStatus(
|
||||
res.result === 'success'
|
||||
? { status: ValidatedStatus.Success }
|
||||
: { status: ValidatedStatus.Error, message: res.error })
|
||||
}
|
||||
catch (e: any) {
|
||||
setValidatedStatus({ status: ValidatedStatus.Error, message: e.message })
|
||||
}
|
||||
finally {
|
||||
setValidating(false)
|
||||
}
|
||||
}, 500), [])
|
||||
|
||||
return [
|
||||
validating,
|
||||
validatedStatus,
|
||||
setValidatedStatus,
|
||||
validate,
|
||||
]
|
||||
}
|
||||
|
||||
export default useValidateToken
|
||||
@@ -1,19 +0,0 @@
|
||||
.icon-openai {
|
||||
background: url(../../../assets/gpt.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.icon-azure {
|
||||
background: url(../../../assets/azure.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.icon-anthropic {
|
||||
background: url(../../../assets/anthropic.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.icon-hugging-face {
|
||||
background: url(../../../assets/hugging-face.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '../../../indicator'
|
||||
import OpenaiProvider from '../openai-provider'
|
||||
import AzureProvider from '../azure-provider'
|
||||
import AnthropicProvider from '../anthropic-provider'
|
||||
import type { ValidatedStatusState } from '../provider-input/useValidateToken'
|
||||
import { ValidatedStatus } from '../provider-input/useValidateToken'
|
||||
import s from './index.module.css'
|
||||
import type { Provider, ProviderAnthropicToken, ProviderAzureToken } from '@/models/common'
|
||||
import { ProviderName } from '@/models/common'
|
||||
import { updateProviderAIKey } from '@/service/common'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
const providerNameMap: Record<string, string> = {
|
||||
openai: 'OpenAI',
|
||||
azure_openai: 'Azure OpenAI Service',
|
||||
}
|
||||
type IProviderItemProps = {
|
||||
icon: string
|
||||
name: string
|
||||
provider: Provider
|
||||
activeId: string
|
||||
onActive: (v: string) => void
|
||||
onSave: () => void
|
||||
providedOpenaiProvider?: Provider
|
||||
}
|
||||
const ProviderItem = ({
|
||||
activeId,
|
||||
icon,
|
||||
name,
|
||||
provider,
|
||||
onActive,
|
||||
onSave,
|
||||
providedOpenaiProvider,
|
||||
}: IProviderItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [token, setToken] = useState<ProviderAzureToken | string | ProviderAnthropicToken>(
|
||||
provider.provider_name === 'azure_openai'
|
||||
? { openai_api_base: '', openai_api_key: '' }
|
||||
: provider.provider_name === 'anthropic'
|
||||
? { anthropic_api_key: '' }
|
||||
: '',
|
||||
)
|
||||
const id = `${provider.provider_name}-${provider.provider_type}`
|
||||
const isOpen = id === activeId
|
||||
const comingSoon = false
|
||||
const isValid = provider.is_valid
|
||||
|
||||
const providerTokenHasSetted = () => {
|
||||
if (provider.provider_name === ProviderName.AZURE_OPENAI) {
|
||||
return (provider.token && provider.token.openai_api_base && provider.token.openai_api_key)
|
||||
? {
|
||||
openai_api_base: provider.token.openai_api_base,
|
||||
openai_api_key: provider.token.openai_api_key,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
if (provider.provider_name === ProviderName.OPENAI)
|
||||
return provider.token
|
||||
if (provider.provider_name === ProviderName.ANTHROPIC)
|
||||
return provider.token?.anthropic_api_key
|
||||
}
|
||||
const handleUpdateToken = async () => {
|
||||
if (loading)
|
||||
return
|
||||
if (validatedStatus?.status === ValidatedStatus.Success) {
|
||||
try {
|
||||
setLoading(true)
|
||||
await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onActive('')
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('common.provider.saveFailed') })
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
onSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mb-2 border-[0.5px] border-gray-200 bg-gray-50 rounded-md'>
|
||||
<div className='flex items-center px-4 h-[52px] cursor-pointer border-b-[0.5px] border-b-gray-200'>
|
||||
<div className={cn(s[`icon-${icon}`], 'mr-3 w-6 h-6 rounded-md')} />
|
||||
<div className='grow text-sm font-medium text-gray-800'>{name}</div>
|
||||
{
|
||||
providerTokenHasSetted() && !comingSoon && !isOpen && provider.provider_name !== ProviderName.ANTHROPIC && (
|
||||
<div className='flex items-center mr-4'>
|
||||
{!isValid && <div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>}
|
||||
<Indicator color={!isValid ? 'red' : 'green'} className='ml-2' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(providerTokenHasSetted() && !comingSoon && !isOpen && provider.provider_name === ProviderName.ANTHROPIC) && (
|
||||
<div className='flex items-center mr-4'>
|
||||
{
|
||||
providedOpenaiProvider?.is_valid
|
||||
? !isValid
|
||||
? <div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>
|
||||
: null
|
||||
: <div className='text-xs text-[#DC6803]'>{t('common.provider.anthropic.notEnabled')}</div>
|
||||
}
|
||||
<Indicator color={
|
||||
providedOpenaiProvider?.is_valid
|
||||
? isValid
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'yellow'
|
||||
} className='ml-2' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!comingSoon && !isOpen && provider.provider_name !== ProviderName.ANTHROPIC && (
|
||||
<div className='
|
||||
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
|
||||
text-xs font-medium text-gray-700 flex items-center
|
||||
' onClick={() => onActive(id)}>
|
||||
{providerTokenHasSetted() ? t('common.provider.editKey') : t('common.provider.addKey')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(!comingSoon && !isOpen && provider.provider_name === ProviderName.ANTHROPIC)
|
||||
? providedOpenaiProvider?.is_enabled
|
||||
? (
|
||||
<div className='
|
||||
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
|
||||
text-xs font-medium text-gray-700 flex items-center
|
||||
' onClick={() => providedOpenaiProvider.is_valid && onActive(id)}>
|
||||
{providerTokenHasSetted() ? t('common.provider.editKey') : t('common.provider.addKey')}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<Tooltip
|
||||
htmlContent={<div className='w-[320px]'>
|
||||
{t('common.provider.anthropic.enableTip')}
|
||||
</div>}
|
||||
position='bottom'
|
||||
selector='anthropic-provider-enable-top-tooltip'>
|
||||
<div className='
|
||||
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-not-allowed
|
||||
text-xs font-medium text-gray-700 flex items-center opacity-50
|
||||
'>
|
||||
{t('common.provider.addKey')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
: null
|
||||
}
|
||||
{
|
||||
comingSoon && !isOpen && (
|
||||
<div className='
|
||||
flex items-center px-2 h-[22px] border border-[#444CE7] rounded-md
|
||||
text-xs font-medium text-[#444CE7]
|
||||
'>
|
||||
{t('common.provider.comingSoon')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
isOpen && (
|
||||
<div className='flex items-center'>
|
||||
<div className='
|
||||
flex items-center
|
||||
mr-[5px] px-3 h-7 rounded-md cursor-pointer
|
||||
text-xs font-medium text-gray-700
|
||||
' onClick={() => onActive('')} >
|
||||
{t('common.operation.cancel')}
|
||||
</div>
|
||||
<div className='
|
||||
flex items-center
|
||||
px-3 h-7 rounded-md cursor-pointer bg-primary-700
|
||||
text-xs font-medium text-white
|
||||
' onClick={handleUpdateToken}>
|
||||
{t('common.operation.save')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
provider.provider_name === ProviderName.OPENAI && isOpen && (
|
||||
<OpenaiProvider
|
||||
provider={provider}
|
||||
onValidatedStatus={v => setValidatedStatus(v)}
|
||||
onTokenChange={v => setToken(v)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
provider.provider_name === ProviderName.AZURE_OPENAI && isOpen && (
|
||||
<AzureProvider
|
||||
provider={provider}
|
||||
onValidatedStatus={v => setValidatedStatus(v)}
|
||||
onTokenChange={v => setToken(v)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
provider.provider_name === ProviderName.ANTHROPIC && isOpen && (
|
||||
<AnthropicProvider
|
||||
provider={provider}
|
||||
onValidatedStatus={v => setValidatedStatus(v)}
|
||||
onTokenChange={v => setToken(v)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
provider.provider_name === ProviderName.ANTHROPIC && !isOpen && providerTokenHasSetted() && providedOpenaiProvider?.is_valid && (
|
||||
<div className='px-4 py-3 text-[13px] font-medium text-gray-700'>
|
||||
{t('common.provider.anthropic.using')} {providerNameMap[providedOpenaiProvider.provider_name as string]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
provider.provider_name === ProviderName.ANTHROPIC && !isOpen && providerTokenHasSetted() && !providedOpenaiProvider?.is_valid && (
|
||||
<div className='px-4 py-3 bg-[#FFFAEB] text-[13px] font-medium text-gray-700'>
|
||||
{t('common.provider.anthropic.enableTip')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderItem
|
||||
@@ -55,7 +55,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
|
||||
className="
|
||||
absolute -left-11 right-0 mt-1.5 w-60 max-w-80
|
||||
divide-y divide-gray-100 origin-top-right rounded-lg bg-white
|
||||
shadow-[0_10px_15px_-3px_rgba(0,0,0,0.1),0_4px_6px_rgba(0,0,0,0.05)]
|
||||
shadow-lg
|
||||
"
|
||||
>
|
||||
{!!appItems.length && (<div className="px-1 py-1 overflow-auto" style={{ maxHeight: '50vh' }}>
|
||||
|
||||
@@ -21,7 +21,7 @@ const ExploreNav = ({
|
||||
return (
|
||||
<Link href="/explore/apps" className={classNames(
|
||||
className, 'group',
|
||||
actived && 'bg-white shadow-[0_2px_5px_-1px_rgba(0,0,0,0.05),0_2px_4px_-2px_rgba(0,0,0,0.05)]',
|
||||
actived && 'bg-white shadow-md',
|
||||
actived ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200',
|
||||
)}>
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@ const Nav = ({
|
||||
return (
|
||||
<div className={`
|
||||
flex items-center h-8 mr-3 px-0.5 rounded-xl text-sm shrink-0 font-medium
|
||||
${isActived && 'bg-white shadow-[0_2px_5px_-1px_rgba(0,0,0,0.05),0_2px_4px_-2px_rgba(0,0,0,0.05)] font-semibold'}
|
||||
${isActived && 'bg-white shadow-md font-semibold'}
|
||||
${!curNav && !isActived && 'hover:bg-gray-200'}
|
||||
`}>
|
||||
<Link href={link}>
|
||||
|
||||
@@ -61,7 +61,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSel
|
||||
className="
|
||||
absolute -left-11 right-0 mt-1.5 w-60 max-w-80
|
||||
divide-y divide-gray-100 origin-top-right rounded-lg bg-white
|
||||
shadow-[0_10px_15px_-3px_rgba(0,0,0,0.1),0_4px_6px_rgba(0,0,0,0.05)]
|
||||
shadow-lg
|
||||
"
|
||||
>
|
||||
<div className="px-1 py-1 overflow-auto" style={{ maxHeight: '50vh' }} onScroll={handleScroll}>
|
||||
|
||||
@@ -21,7 +21,7 @@ const PluginNav = ({
|
||||
return (
|
||||
<Link href="/plugins-coming-soon" className={classNames(
|
||||
className, 'group',
|
||||
isPluginsComingSoon && 'bg-white shadow-[0_2px_5px_-1px_rgba(0,0,0,0.05),0_2px_4px_-2px_rgba(0,0,0,0.05)]',
|
||||
isPluginsComingSoon && 'bg-white shadow-md',
|
||||
isPluginsComingSoon ? 'text-primary-600' : 'text-gray-500 hover:bg-gray-200',
|
||||
)}>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user