Model Runtime (#1858)

Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
Co-authored-by: Garfield Dai <dai.hai@foxmail.com>
Co-authored-by: chenhe <guchenhe@gmail.com>
Co-authored-by: jyong <jyong@dify.ai>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Yeuoly <admin@srmxy.cn>
This commit is contained in:
takatost 2024-01-02 23:42:00 +08:00 • committed by GitHub
commit d069c668f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
807 changed files with 171310 additions and 23806 deletions

View file

@ -11,7 +11,7 @@ import LanguagePage from './language-page'
import PluginPage from './plugin-page'
import ApiBasedExtensionPage from './api-based-extension-page'
import DataSourcePage from './data-source-page'
import ModelPage from './model-page'
import ModelProviderPage from './model-provider-page'
import s from './index.module.css'
import BillingPage from '@/app/components/billing/billing-page'
import CustomPage from '@/app/components/custom/custom-page'
@ -211,7 +211,7 @@ export default function AccountSetting({
{activeMenu === 'billing' && <BillingPage />}
{activeMenu === 'integrations' && <IntegrationsPage />}
{activeMenu === 'language' && <LanguagePage />}
{activeMenu === 'provider' && <ModelPage />}
{activeMenu === 'provider' && <ModelProviderPage />}
{activeMenu === 'data-source' && <DataSourcePage />}
{activeMenu === 'plugin' && <PluginPage />}
{activeMenu === 'api-based-extension' && <ApiBasedExtensionPage /> }

View file

@ -13,7 +13,7 @@ export type ValidatedStatusState = {
export type Status = 'add' | 'fail' | 'success'
export type ValidateValue = Record<string, string | undefined | boolean>
export type ValidateValue = Record<string, any>
export type ValidateCallback = {
before: (v?: ValidateValue) => boolean | undefined

View file

@ -1,80 +0,0 @@
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

View file

@ -1,189 +0,0 @@
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, matching the Azure deployment name.',
'zh-Hans': '在此输入您的部署名称,需要与 Azure 的部署名称匹配',
},
},
{
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: 'gpt-4-1106-preview',
label: {
'en': 'gpt-4-1106-preview',
'zh-Hans': 'gpt-4-1106-preview',
},
},
{
key: 'gpt-4-vision-preview',
label: {
'en': 'gpt-4-vision-preview',
'zh-Hans': 'gpt-4-vision-preview',
},
},
{
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

View file

@ -1,70 +0,0 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { BaichuanTextCn } from '@/app/components/base/icons/src/image/llm'
import {
Baichuan,
BaichuanText,
} from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'BAICHUAN AI',
'zh-Hans': '百川智能',
},
icon: <Baichuan className='w-full h-full' />,
},
item: {
key: ProviderEnum.baichuan,
titleIcon: {
'en': <BaichuanText className='w-[124px] h-6' />,
'zh-Hans': <BaichuanTextCn className='w-[100px] h-6' />,
},
},
modal: {
key: ProviderEnum.baichuan,
title: {
'en': 'BAICHUAN AI',
'zh-Hans': '百川智能',
},
icon: <Baichuan className='w-6 h-6' />,
link: {
href: 'https://platform.baichuan-ai.com/console/apikey',
label: {
'en': 'Get your API key from BAICHUAN AI',
'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

View file

@ -1,69 +0,0 @@
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

View file

@ -1,57 +0,0 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { Cohere, CohereText } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'cohere',
'zh-Hans': 'cohere',
},
icon: <Cohere className='w-full h-full' />,
},
item: {
key: ProviderEnum.cohere,
titleIcon: {
'en': <CohereText className='w-[120px] h-6' />,
'zh-Hans': <CohereText className='w-[120px] h-6' />,
},
hit: {
'en': 'Rerank Model Supported',
'zh-Hans': '支持 Rerank 模型',
},
},
modal: {
key: ProviderEnum.cohere,
title: {
'en': 'Rerank Model',
'zh-Hans': 'Rerank 模型',
},
icon: <Cohere className='w-6 h-6' />,
link: {
href: 'https://dashboard.cohere.com/api-keys',
label: {
'en': 'Get your API key from cohere',
'zh-Hans': '从 cohere 获取 API Key',
},
},
validateKeys: ['api_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',
},
},
],
},
}
export default config

View file

@ -1,278 +0,0 @@
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',
task_type: 'text-generation',
},
validateKeys: (v?: FormValue) => {
if (v?.huggingfacehub_api_type === 'hosted_inference_api') {
return [
'huggingfacehub_api_token',
'model_name',
]
}
if (v?.huggingfacehub_api_type === 'inference_endpoints') {
if (v.model_type === 'embeddings') {
return [
'huggingfacehub_api_token',
'huggingface_namespace',
'model_name',
'huggingfacehub_endpoint_url',
'task_type',
]
}
return [
'huggingfacehub_api_token',
'model_name',
'huggingfacehub_endpoint_url',
'task_type',
]
}
return []
},
filterValue: (v?: FormValue) => {
let filteredKeys: string[] = []
if (v?.huggingfacehub_api_type === 'hosted_inference_api') {
filteredKeys = [
'huggingfacehub_api_type',
'huggingfacehub_api_token',
'model_name',
'model_type',
]
}
if (v?.huggingfacehub_api_type === 'inference_endpoints') {
if (v.model_type === 'embeddings') {
filteredKeys = [
'huggingfacehub_api_type',
'huggingfacehub_api_token',
'huggingface_namespace',
'model_name',
'huggingfacehub_endpoint_url',
'task_type',
'model_type',
]
}
else {
filteredKeys = [
'huggingfacehub_api_type',
'huggingfacehub_api_token',
'model_name',
'huggingfacehub_endpoint_url',
'task_type',
'model_type',
]
}
}
return filteredKeys.reduce((prev: FormValue, next: string) => {
prev[next] = v?.[next] || ''
return prev
}, {})
},
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: 'chat',
// label: {
// 'en': 'Chat',
// 'zh-Hans': '聊天',
// },
// },
{
key: 'embeddings',
label: {
'en': 'Embeddings',
'zh-Hans': 'Embeddings',
},
},
],
},
{
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',
},
},
{
hidden: (value?: FormValue) => !(value?.huggingfacehub_api_type === 'inference_endpoints' && value?.model_type === 'embeddings'),
type: 'text',
key: 'huggingface_namespace',
required: true,
label: {
'en': 'User Name / Organization Name',
'zh-Hans': '用户名 / 组织名称',
},
placeholder: {
'en': 'Enter your User Name / Organization Name here',
'zh-Hans': '在此输入您的用户名 / 组织名称',
},
},
{
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',
},
},
{
hidden: (value?: FormValue) => value?.huggingfacehub_api_type === 'hosted_inference_api' || value?.model_type === 'embeddings',
type: 'radio',
key: 'task_type',
required: true,
label: {
'en': 'Task',
'zh-Hans': 'Task',
},
options: (value?: FormValue) => {
if (value?.model_type === 'chat') {
return [{
key: 'question-answer',
label: {
'en': '问答',
'zh-Hans': 'Question Answer',
},
}]
}
return [
{
key: 'text2text-generation',
label: {
'en': 'Text-to-Text Generation',
'zh-Hans': 'Text-to-Text Generation',
},
},
{
key: 'text-generation',
label: {
'en': 'Text Generation',
'zh-Hans': 'Text Generation',
},
},
]
},
},
{
hidden: (value?: FormValue) => !(value?.huggingfacehub_api_type === 'inference_endpoints' && value?.model_type === 'embeddings'),
type: 'radio',
key: 'task_type',
required: true,
label: {
'en': 'Task',
'zh-Hans': 'Task',
},
options: [
{
key: 'feature-extraction',
label: {
'en': 'Feature Extraction',
'zh-Hans': 'Feature Extraction',
},
},
],
},
],
},
}
export default config

View file

@ -1,37 +0,0 @@
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'
import xinference from './xinference'
import openllm from './openllm'
import localai from './localai'
import zhipuai from './zhipuai'
import baichuan from './baichuan'
import cohere from './cohere'
import jina from './jina'
export default {
openai,
anthropic,
azure_openai,
replicate,
huggingface_hub,
wenxin,
tongyi,
spark,
minimax,
chatglm,
xinference,
openllm,
localai,
zhipuai,
baichuan,
cohere,
jina,
}

View file

@ -1,57 +0,0 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { Jina, JinaText } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'Jina AI',
'zh-Hans': 'Jina AI',
},
icon: <Jina className='w-full h-full' />,
},
item: {
key: ProviderEnum.jina,
titleIcon: {
'en': <JinaText className='w-[58px] h-6' />,
'zh-Hans': <JinaText className='w-[58px] h-6' />,
},
hit: {
'en': 'Embedding Model Supported',
'zh-Hans': '支持 Embedding 模型',
},
},
modal: {
key: ProviderEnum.jina,
title: {
'en': 'Embedding Model',
'zh-Hans': 'Embedding 模型',
},
icon: <JinaText className='w-[58px] h-6' />,
link: {
href: 'https://jina.ai/embeddings/',
label: {
'en': 'Get your API key from Jina AI',
'zh-Hans': '从 Jina AI 获取 API Key',
},
},
validateKeys: ['api_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',
},
},
],
},
}
export default config

View file

@ -1,176 +0,0 @@
import { ProviderEnum } from '../declarations'
import type { FormValue, ProviderConfig } from '../declarations'
import { Localai, LocalaiText } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'LocalAI',
'zh-Hans': 'LocalAI',
},
icon: <Localai className='w-full h-full' />,
},
item: {
key: ProviderEnum.localai,
titleIcon: {
'en': <LocalaiText className='h-6' />,
'zh-Hans': <LocalaiText 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.localai,
title: {
'en': 'LocalAI',
'zh-Hans': 'LocalAI',
},
icon: <Localai className='h-6' />,
link: {
href: 'https://github.com/go-skynet/LocalAI',
label: {
'en': 'How to deploy LocalAI',
'zh-Hans': '如何部署 LocalAI',
},
},
defaultValue: {
model_type: 'text-generation',
completion_type: 'completion',
},
validateKeys: (v?: FormValue) => {
if (v?.model_type === 'text-generation') {
return [
'model_type',
'model_name',
'server_url',
'completion_type',
]
}
if (v?.model_type === 'embeddings') {
return [
'model_type',
'model_name',
'server_url',
]
}
return []
},
filterValue: (v?: FormValue) => {
let filteredKeys: string[] = []
if (v?.model_type === 'text-generation') {
filteredKeys = [
'model_type',
'model_name',
'server_url',
'completion_type',
]
}
if (v?.model_type === 'embeddings') {
filteredKeys = [
'model_type',
'model_name',
'server_url',
]
}
return filteredKeys.reduce((prev: FormValue, next: string) => {
prev[next] = v?.[next] || ''
return prev
}, {})
},
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: 'model_name',
required: true,
label: {
'en': 'Model Name',
'zh-Hans': '模型名称',
},
placeholder: {
'en': 'Enter your Model Name here',
'zh-Hans': '在此输入您的模型名称',
},
},
{
hidden: (value?: FormValue) => value?.model_type === 'embeddings',
type: 'radio',
key: 'completion_type',
required: true,
label: {
'en': 'Completion Type',
'zh-Hans': 'Completion Type',
},
options: [
{
key: 'completion',
label: {
'en': 'Completion',
'zh-Hans': 'Completion',
},
},
{
key: 'chat_completion',
label: {
'en': 'Chat Completion',
'zh-Hans': 'Chat Completion',
},
},
],
},
{
type: 'text',
key: 'server_url',
required: true,
label: {
'en': 'Server url',
'zh-Hans': 'Server url',
},
placeholder: {
'en': 'Enter your Server Url, eg: https://example.com/xxx',
'zh-Hans': '在此输入您的 Server Url,如:https://example.com/xxx',
},
},
],
},
}
export default config

View file

@ -1,69 +0,0 @@
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

View file

@ -1,93 +0,0 @@
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

View file

@ -1,114 +0,0 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { Openllm, OpenllmText } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'OpenLLM',
'zh-Hans': 'OpenLLM',
},
icon: <Openllm className='w-full h-full' />,
},
item: {
key: ProviderEnum.openllm,
titleIcon: {
'en': <OpenllmText className='h-6' />,
'zh-Hans': <OpenllmText 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.openllm,
title: {
'en': 'OpenLLM',
'zh-Hans': 'OpenLLM',
},
icon: <Openllm className='h-6' />,
link: {
href: 'https://github.com/bentoml/OpenLLM',
label: {
'en': 'How to deploy OpenLLM',
'zh-Hans': '如何部署 OpenLLM',
},
},
defaultValue: {
model_type: 'text-generation',
},
validateKeys: [
'model_type',
'model_name',
'server_url',
],
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: 'model_name',
required: true,
label: {
'en': 'Model Name',
'zh-Hans': '模型名称',
},
placeholder: {
'en': 'Enter your Model Name here',
'zh-Hans': '在此输入您的模型名称',
},
},
{
type: 'text',
key: 'server_url',
required: true,
label: {
'en': 'Server url',
'zh-Hans': 'Server url',
},
placeholder: {
'en': 'Enter your Server Url, eg: https://example.com/xxx',
'zh-Hans': '在此输入您的 Server Url,如:https://example.com/xxx',
},
},
],
},
}
export default config

View file

@ -1,122 +0,0 @@
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: 'chat',
// label: {
// 'en': 'Chat',
// '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

View file

@ -1,83 +0,0 @@
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 iFLYTEK SPARK',
'zh-Hans': '从讯飞星火获取 API Key',
},
},
validateKeys: [
'app_id',
'api_key',
'api_secret',
],
fields: [
{
type: 'text',
key: 'app_id',
required: true,
label: {
'en': 'APPID',
'zh-Hans': 'APPID',
},
placeholder: {
'en': 'Enter your APPID here',
'zh-Hans': '在此输入您的 APPID',
},
},
{
type: 'text',
key: 'api_secret',
required: true,
label: {
'en': 'APISecret',
'zh-Hans': 'APISecret',
},
placeholder: {
'en': 'Enter your APISecret here',
'zh-Hans': '在此输入您的 APISecret',
},
},
{
type: 'text',
key: 'api_key',
required: true,
label: {
'en': 'APIKey',
'zh-Hans': 'APIKey',
},
placeholder: {
'en': 'Enter your APIKey here',
'zh-Hans': '在此输入您的 APIKey',
},
},
],
},
}
export default config

View file

@ -1,53 +0,0 @@
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

View file

@ -1,66 +0,0 @@
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

View file

@ -1,135 +0,0 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { XorbitsInference, XorbitsInferenceText } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'Xinference',
'zh-Hans': 'Xinference',
},
icon: <XorbitsInference className='w-full h-full' />,
},
item: {
key: ProviderEnum.xinference,
titleIcon: {
'en': <XorbitsInferenceText className='h-6' />,
'zh-Hans': <XorbitsInferenceText 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.xinference,
title: {
'en': 'Xinference',
'zh-Hans': 'Xinference',
},
icon: <XorbitsInference className='h-6' />,
link: {
href: 'https://github.com/xorbitsai/inference',
label: {
'en': 'How to deploy Xinference',
'zh-Hans': '如何部署 Xinference',
},
},
defaultValue: {
model_type: 'text-generation',
},
validateKeys: [
'model_type',
'model_name',
'server_url',
'model_uid',
],
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',
},
},
{
key: 'reranking',
label: {
'en': 'Rerank',
'zh-Hans': 'Rerank',
},
},
],
},
{
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: 'server_url',
required: true,
label: {
'en': 'Server Url',
'zh-Hans': 'Server Url',
},
placeholder: {
'en': 'Enter your Server url, eg: https://example.com/xxx',
'zh-Hans': '在此输入您的 Server url,如:https://example.com/xxx',
},
},
{
type: 'text',
key: 'model_uid',
required: true,
label: {
'en': 'Model UID',
'zh-Hans': 'Model UID',
},
placeholder: {
'en': 'Enter your Model UID',
'zh-Hans': '在此输入您的 Model UID',
},
},
],
},
}
export default config

View file

@ -1,55 +0,0 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { Zhipuai, ZhipuaiText, ZhipuaiTextCn } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'ZHIPU AI',
'zh-Hans': '智谱 AI',
},
icon: <Zhipuai className='w-full h-full' />,
},
item: {
key: ProviderEnum.zhipuai,
titleIcon: {
'en': <ZhipuaiText className='-ml-1 h-7' />,
'zh-Hans': <ZhipuaiTextCn className='h-8' />,
},
},
modal: {
key: ProviderEnum.zhipuai,
title: {
'en': 'ZHIPU AI',
'zh-Hans': '智谱 AI',
},
icon: <Zhipuai className='w-6 h-6' />,
link: {
href: 'https://open.bigmodel.cn/usercenter/apikeys',
label: {
'en': 'Get your API key from ZHIPU AI',
'zh-Hans': '从智谱 AI 获取 API Key',
},
},
validateKeys: [
'api_key',
],
fields: [
{
type: 'text',
key: 'api_key',
required: true,
label: {
'en': 'APIKey',
'zh-Hans': 'APIKey',
},
placeholder: {
'en': 'Enter your APIKey here',
'zh-Hans': '在此输入您的 APIKey',
},
},
],
},
}
export default config

View file

@ -1,163 +0,0 @@
import type { ReactElement } from 'react'
import type { ModelModeType } from '@/types/app'
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',
'xinference' = 'xinference',
'openllm' = 'openllm',
'localai' = 'localai',
'zhipuai' = 'zhipuai',
'baichuan' = 'baichuan',
'cohere' = 'cohere',
'jina' = 'jina',
}
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',
reranking = 'reranking',
}
export enum ModelFeature {
agentThought = 'agent_thought',
vision = 'vision',
}
// backend defined model struct: /console/api/workspaces/current/models/model-type/:model_type
export type BackendModel = {
model_name: string
model_display_name: string // not always exist
model_type: ModelType
model_mode: ModelModeType
model_provider: {
provider_name: ProviderEnum
provider_type: PreferredProviderTypeEnum
quota_type: 'trial' | 'paid'
quota_unit: 'times' | 'tokens'
quota_used: number
quota_limit: number
}
features: ModelFeature[]
}
export type ProviderConfigModal = {
key: ProviderEnum
title: TypeWithI18N
icon: ReactElement
defaultValue?: FormValue
validateKeys?: string[] | ((v?: FormValue) => string[])
filterValue?: (v?: FormValue) => FormValue
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
}

View file

@ -1,267 +0,0 @@
import { useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import type {
FormValue,
ProviderConfigModal,
ProviderEnum,
} from './declarations'
import ModelCard from './model-card'
import ModelItem from './model-item'
import ModelModal from './model-modal'
import SystemModel from './system-model'
import config from './configs'
import { ConfigurableProviders } from './utils'
import {
changeModelProviderPriority,
deleteModelProvider,
deleteModelProviderModel,
fetchModelProviders,
setModelProvider,
} 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'
import I18n from '@/context/i18n'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
const MODEL_CARD_LIST = [
config.openai,
config.anthropic,
]
type DeleteModel = {
model_name: string
model_type: string
}
const ModelPage = () => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const {
updateModelList,
textGenerationDefaultModel,
embeddingsDefaultModel,
speech2textDefaultModel,
rerankDefaultModel,
} = useProviderContext()
const { data: providers, mutate: mutateProviders } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
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')
let modelList = []
if (locale === 'en') {
modelList = [
config.azure_openai,
config.replicate,
config.huggingface_hub,
config.cohere,
config.zhipuai,
config.baichuan,
config.spark,
config.minimax,
config.tongyi,
config.wenxin,
config.jina,
config.chatglm,
config.xinference,
config.openllm,
config.localai,
]
}
else {
modelList = [
config.huggingface_hub,
config.cohere,
config.zhipuai,
config.spark,
config.baichuan,
config.minimax,
config.azure_openai,
config.replicate,
config.tongyi,
config.wenxin,
config.jina,
config.chatglm,
config.xinference,
config.openllm,
config.localai,
]
}
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)
updateModelList(ModelType.speech2text)
updateModelList(ModelType.reranking)
mutateProviders()
}
const handleSave = async (originValue?: FormValue) => {
if (originValue && modelModalConfig) {
const v = modelModalConfig.filterValue ? modelModalConfig.filterValue(originValue) : originValue
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 defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel
return (
<div className='relative pt-1 -mt-2'>
<div className={`flex items-center justify-between mb-2 h-8 ${defaultModelNotConfigured && 'px-3 bg-[#FFFAEB] rounded-lg border border-[#FEF0C7]'}`}>
{
defaultModelNotConfigured
? (
<div className='flex items-center text-xs font-medium text-gray-700'>
<AlertTriangle className='mr-1 w-3 h-3 text-[#F79009]' />
{t('common.modelProvider.notConfigured')}
</div>
)
: <div className='text-sm font-medium text-gray-800'>{t('common.modelProvider.models')}</div>
}
<SystemModel onUpdate={() => mutateProviders()} />
</div>
<div className='grid grid-cols-1 lg: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>
{
modelList.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}
/>
))
}
<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}
confirmWrapperClassName='!z-30'
/>
</div>
)
}
export default ModelPage

View file

@ -1,117 +0,0 @@
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'
import { formatNumber } from '@/utils/format'
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 formatNumber(systemPaid.quota_limit - systemPaid.quota_used)
if (systemTrial.is_valid)
return formatNumber(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

View file

@ -1,81 +0,0 @@
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

View file

@ -1,69 +0,0 @@
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: Model[]
onOpenModal: (v: Omit<Model, 'config'> & Model['config']) => 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 => (
<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

View file

@ -1,79 +0,0 @@
import { useState } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import type { ProviderConfigItem, TypeWithI18N } from '../declarations'
import { ProviderEnum as ProviderEnumValue } from '../declarations'
import s from './index.module.css'
import I18n from '@/context/i18n'
import Button from '@/app/components/base/button'
import { submitFreeQuota } from '@/service/common'
import { LinkExternal01 } from '@/app/components/base/icons/src/vender/line/general'
const TIP_MAP: { [k: string]: TypeWithI18N } = {
[ProviderEnumValue.minimax]: {
'en': 'Earn 1 million tokens for free',
'zh-Hans': '免费获取 100 万个 token',
},
[ProviderEnumValue.spark]: {
'en': 'Earn 3 million tokens (v3.0) for free',
'zh-Hans': '免费获取 300 万个 token (v3.0)',
},
[ProviderEnumValue.zhipuai]: {
'en': 'Earn 10 million tokens for free',
'zh-Hans': '免费获取 1000 万个 token',
},
}
type FreeQuotaProps = {
modelItem: ProviderConfigItem
onUpdate: () => void
}
const FreeQuota: FC<FreeQuotaProps> = ({
modelItem,
onUpdate,
}) => {
const { locale } = useContext(I18n)
const { t } = useTranslation()
const [loading, setLoading] = useState(false)
const handleClick = async () => {
try {
setLoading(true)
const res = await submitFreeQuota(`/workspaces/current/model-providers/${modelItem.key}/free-quota-submit`)
if (res.type === 'redirect' && res.redirect_url)
window.location.href = res.redirect_url
else if (res.type === 'submit' && res.result === 'success')
onUpdate()
}
finally {
setLoading(false)
}
}
return (
<div className='flex items-center'>
📣
<div className={`${s.vender} ml-1 text-xs font-medium text-transparent`}>{TIP_MAP[modelItem.key][locale]}</div>
<div className='mx-1 text-xs font-medium text-gray-400'>·</div>
<a
href='https://docs.dify.ai/v/zh-hans/getting-started/faq/llms-use-faq#8.-ru-he-mian-fei-shen-ling-xun-fei-xing-huo-minimax-mo-xing-de-ti-yanedu'
target='_blank'
className='flex items-center text-xs font-medium text-[#155EEF]'>
{t('common.modelProvider.freeQuota.howToEarn')}
<LinkExternal01 className='ml-0.5 w-3 h-3' />
</a>
<Button
type='primary'
className='ml-3 !px-3 !h-7 !rounded-md !text-xs !font-medium'
onClick={handleClick}
disabled={loading}
>
{t('common.operation.getForFree')}
</Button>
<div className='mx-2 w-[1px] h-4 bg-black/5' />
</div>
)
}
export default FreeQuota

View file

@ -1,29 +0,0 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { formatNumber } from '@/utils/format'
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'>
{formatNumber(remainTokens)}
<div className='ml-1 font-normal'>Tokens</div>
</div>
</div>
</div>
)
}
export default QuotaCard

View file

@ -1,104 +0,0 @@
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 { ProviderEnum } from '../declarations'
import Indicator from '../../../indicator'
import Selector from '../selector'
import FreeQuota from './FreeQuota'
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
onUpdate: () => void
}
const Setting: FC<SettingProps> = ({
currentProvider,
modelItem,
onOpenModal,
onOperate,
onUpdate,
}) => {
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.key === ProviderEnum.minimax || modelItem.key === ProviderEnum.spark || modelItem.key === ProviderEnum.zhipuai) && systemFree && !systemFree?.is_valid && !IS_CE_EDITION && locale === 'zh-Hans' && (
<FreeQuota
modelItem={modelItem}
onUpdate={onUpdate}
/>
)
}
{
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 && !IS_CE_EDITION && '!text-gray-300'}`}
onClick={() => onOpenModal()}
disabled={!!modelItem.disable && !IS_CE_EDITION}
>
{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

View file

@ -1,73 +0,0 @@
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,
onUpdate,
}) => {
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 p-4 min-h-[56px] flex-wrap gap-y-1'>
<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}
onUpdate={onUpdate}
/>
</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

View file

@ -1,208 +0,0 @@
import { useEffect, useState } from 'react'
import type { Dispatch, FC, SetStateAction } from 'react'
import { useContext } from 'use-context-selector'
import { type Field, type FormValue, type ProviderConfigModal, ProviderEnum } 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, modelModal?.filterValue ? modelModal?.filterValue(v) : 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 {
const extraValue: Record<string, string> = {}
if (
(
(k === 'model_type' && v === 'embeddings' && value.huggingfacehub_api_type === 'inference_endpoints')
|| (k === 'huggingfacehub_api_type' && v === 'inference_endpoints' && value.model_type === 'embeddings')
)
&& modelModal?.key === ProviderEnum.huggingface_hub
)
extraValue.task_type = 'feature-extraction'
if (
(
(k === 'model_type' && v === 'text-generation' && value.huggingfacehub_api_type === 'inference_endpoints')
|| (k === 'huggingfacehub_api_type' && v === 'inference_endpoints' && value.model_type === 'text-generation')
)
&& modelModal?.key === ProviderEnum.huggingface_hub
)
extraValue.task_type = 'text-generation'
if (
(
(k === 'model_type' && v === 'chat' && value.huggingfacehub_api_type === 'inference_endpoints')
|| (k === 'huggingfacehub_api_type' && v === 'inference_endpoints' && value.model_type === 'chat')
)
&& modelModal?.key === ProviderEnum.huggingface_hub
)
extraValue.task_type = 'question-answer'
handleMultiFormChange({ ...value, [k]: v, ...extraValue }, 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-${options?.length} gap-3`}>
{
options?.map(option => (
<div
className={`
flex items-center px-3 py-2 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

View file

@ -1,58 +0,0 @@
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

View file

@ -1,170 +0,0 @@
import { useCallback, useState } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
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'
import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
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 (
<PortalToFollowElem open>
<PortalToFollowElemContent className='w-full h-full z-[60]'>
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
<div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-2'>
<div className='text-xl font-semibold text-gray-900'>{renderTitlePrefix()}</div>
{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 flex-wrap gap-y-2'>
<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>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default ModelModal

View file

@ -1,347 +0,0 @@
import type { FC } from 'react'
import React, { Fragment, useEffect, useState } from 'react'
import useSWR from 'swr'
import { Popover, Transition } from '@headlessui/react'
import { useTranslation } from 'react-i18next'
import _ from 'lodash-es'
import cn from 'classnames'
import ModelModal from '../model-modal'
import cohereConfig from '../configs/cohere'
import s from './style.module.css'
import type { BackendModel, FormValue, 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, LinkExternal01, 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 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'
import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
import type { ModelModeType } from '@/types/app'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import { useModalContext } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { fetchDefaultModal, setModelProvider } from '@/service/common'
import { useToastContext } from '@/app/components/base/toast'
type Props = {
value: {
providerName: ProviderEnum
modelName: string
} | undefined
modelType: ModelType
isShowModelModeType?: boolean
isShowAddModel?: boolean
supportAgentThought?: boolean
onChange: (value: BackendModel) => void
popClassName?: string
readonly?: boolean
triggerIconSmall?: boolean
whenEmptyGoToSetting?: boolean
onUpdate?: () => void
}
type ModelOption = {
type: 'model'
value: string
providerName: ProviderEnum
modelDisplayName: string
model_mode: ModelModeType
} | {
type: 'provider'
value: ProviderEnum
}
const ModelSelector: FC<Props> = ({
value,
modelType,
isShowModelModeType,
isShowAddModel,
supportAgentThought,
onChange,
popClassName,
readonly,
triggerIconSmall,
whenEmptyGoToSetting,
onUpdate,
}) => {
const { t } = useTranslation()
const { setShowAccountSettingModal } = useModalContext()
const {
textGenerationModelList,
embeddingsModelList,
speech2textModelList,
rerankModelList,
agentThoughtModelList,
updateModelList,
} = useProviderContext()
const [search, setSearch] = useState('')
const modelList = supportAgentThought
? agentThoughtModelList
: ({
[ModelType.textGeneration]: textGenerationModelList,
[ModelType.embeddings]: embeddingsModelList,
[ModelType.speech2text]: speech2textModelList,
[ModelType.reranking]: rerankModelList,
})[modelType]
const currModel = modelList.find(item => item.model_name === value?.modelName && item.model_provider.provider_name === value.providerName)
const allModelNames = (() => {
if (!search)
return {}
const res: Record<string, string> = {}
modelList.forEach(({ model_name, model_display_name }) => {
res[model_name] = model_display_name
})
return res
})()
const filteredModelList = search
? modelList.filter(({ model_name }) => {
if (allModelNames[model_name].includes(search))
return true
return false
})
: modelList
const hasRemoved = (value && value.modelName && value.providerName) && !modelList.find(({ model_name, model_provider }) => model_name === value.modelName && model_provider.provider_name === value.providerName)
const modelOptions: ModelOption[] = (() => {
const providers = _.uniq(filteredModelList.map(item => item.model_provider.provider_name))
const res: ModelOption[] = []
providers.forEach((providerName) => {
res.push({
type: 'provider',
value: providerName,
})
const models = filteredModelList.filter(m => m.model_provider.provider_name === providerName)
models.forEach(({ model_name, model_display_name, model_mode }) => {
res.push({
type: 'model',
providerName,
value: model_name,
modelDisplayName: model_display_name,
model_mode,
})
})
})
return res
})()
const { eventEmitter } = useEventEmitterContextContext()
const [showRerankModal, setShowRerankModal] = useState(false)
const [shouldFetchRerankDefaultModel, setShouldFetchRerankDefaultModel] = useState(false)
const { notify } = useToastContext()
const { data: rerankDefaultModel } = useSWR(shouldFetchRerankDefaultModel ? '/workspaces/current/default-model?model_type=reranking' : null, fetchDefaultModal)
const handleOpenRerankModal = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
setShowRerankModal(true)
}
const handleRerankModalSave = async (originValue?: FormValue) => {
if (originValue) {
try {
eventEmitter?.emit('provider-save')
const res = await setModelProvider({
url: `/workspaces/current/model-providers/${cohereConfig.modal.key}`,
body: {
config: originValue,
},
})
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
updateModelList(ModelType.reranking)
setShowRerankModal(false)
setShouldFetchRerankDefaultModel(true)
if (onUpdate)
onUpdate()
}
eventEmitter?.emit('')
}
catch (e) {
eventEmitter?.emit('')
}
}
}
useEffect(() => {
if (rerankDefaultModel && whenEmptyGoToSetting)
onChange(rerankDefaultModel)
}, [rerankDefaultModel])
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 opacity-50' : 'bg-gray-100', hasRemoved && '!bg-[#FEF3F2]')}>
{
({ open }) => (
<>
{
(value && value.modelName && value.providerName)
? (
<>
<ModelIcon
className={cn('mr-1.5', !triggerIconSmall && 'w-5 h-5')}
modelId={value.modelName}
providerName={value.providerName}
/>
<div className='mr-1.5 grow flex items-center text-left text-sm text-gray-900 truncate'>
<ModelName modelId={value.modelName} modelDisplayName={currModel?.model_display_name || value.modelName} />
{isShowModelModeType && (
<ModelModeTypeLabel className='ml-2' type={currModel?.model_mode as ModelModeType} />
)}
</div>
</>
)
: whenEmptyGoToSetting
? (
<div className='grow flex items-center h-9 justify-between' onClick={handleOpenRerankModal}>
<div className='flex items-center text-[13px] font-medium text-primary-500'>
<CubeOutline className='mr-1.5 w-4 h-4' />
{t('common.modelProvider.selector.rerankTip')}
</div>
<LinkExternal01 className='w-3 h-3 text-gray-500' />
</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 && !whenEmptyGoToSetting && (
<ChevronDown className={`w-4 h-4 text-gray-700 ${open ? 'opacity-100' : 'opacity-60'}`} />
)
}
{
whenEmptyGoToSetting && (value && value.modelName && value.providerName) && (
<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, isShowModelModeType ? 'max-w-[312px]' : 'max-w-[260px]', 'absolute top-10 p-1 min-w-[232px] 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) => {
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={`${s.optionItem}
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='mr-2 grow flex items-center text-left text-sm text-gray-900 truncate'>
<ModelName modelId={model.value} modelDisplayName={model.modelDisplayName} />
{isShowModelModeType && (
<ModelModeTypeLabel className={`${s.modelModeLabel} ml-2`} type={model.model_mode} />
)}
</div>
{ (value?.providerName === model.providerName && value?.modelName === model.value) && <Check className='shrink-0 w-4 h-4 text-primary-600' /> }
</Popover.Button>
)
}
return null
})
}
{modelList.length !== 0 && (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>
)}
{isShowAddModel && (
<div
className='border-t flex items-center h-9 pl-3 text-xs text-[#155EEF] cursor-pointer'
style={{
borderColor: 'rgba(0, 0, 0, 0.05)',
}}
onClick={() => setShowAccountSettingModal({ payload: 'provider' })}
>
<CubeOutline className='w-4 h-4 mr-2' />
<div>{t('common.model.addMoreModel')}</div>
</div>
)}
</Popover.Panel>
</Transition>
)}
</Popover>
<ModelModal
isShow={showRerankModal}
modelModal={cohereConfig.modal}
onCancel={() => setShowRerankModal(false)}
onSave={handleRerankModalSave}
mode={'add'}
/>
</div>
)
}
export default ModelSelector

View file

@ -1,358 +0,0 @@
import type { FC } from 'react'
import React, { Fragment, useEffect, useRef, useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import _ from 'lodash-es'
import cn from 'classnames'
import ModelModal from '../model-modal'
import cohereConfig from '../configs/cohere'
import s from './style.module.css'
import type { BackendModel, FormValue, 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, LinkExternal01, 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 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'
import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
import type { ModelModeType } from '@/types/app'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import { useModalContext } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { fetchDefaultModal, setModelProvider } from '@/service/common'
import { useToastContext } from '@/app/components/base/toast'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
type Props = {
value: {
providerName: ProviderEnum
modelName: string
} | undefined
modelType: ModelType
isShowModelModeType?: boolean
isShowAddModel?: boolean
supportAgentThought?: boolean
onChange: (value: BackendModel) => void
popClassName?: string
readonly?: boolean
triggerIconSmall?: boolean
whenEmptyGoToSetting?: boolean
onUpdate?: () => void
widthSameToTrigger?: boolean
}
type ModelOption = {
type: 'model'
value: string
providerName: ProviderEnum
modelDisplayName: string
model_mode: ModelModeType
} | {
type: 'provider'
value: ProviderEnum
}
const ModelSelector: FC<Props> = ({
value,
modelType,
isShowModelModeType,
isShowAddModel,
supportAgentThought,
onChange,
popClassName,
readonly,
triggerIconSmall,
whenEmptyGoToSetting,
onUpdate,
widthSameToTrigger,
}) => {
const { t } = useTranslation()
const { setShowAccountSettingModal } = useModalContext()
const {
textGenerationModelList,
embeddingsModelList,
speech2textModelList,
rerankModelList,
agentThoughtModelList,
updateModelList,
} = useProviderContext()
const [search, setSearch] = useState('')
const modelList = supportAgentThought
? agentThoughtModelList
: ({
[ModelType.textGeneration]: textGenerationModelList,
[ModelType.embeddings]: embeddingsModelList,
[ModelType.speech2text]: speech2textModelList,
[ModelType.reranking]: rerankModelList,
})[modelType]
const currModel = modelList.find(item => item.model_name === value?.modelName && item.model_provider.provider_name === value.providerName)
const allModelNames = (() => {
if (!search)
return {}
const res: Record<string, string> = {}
modelList.forEach(({ model_name, model_display_name }) => {
res[model_name] = model_display_name
})
return res
})()
const filteredModelList = search
? modelList.filter(({ model_name }) => {
if (allModelNames[model_name].includes(search))
return true
return false
})
: modelList
const hasRemoved = (value && value.modelName && value.providerName) && !modelList.find(({ model_name, model_provider }) => model_name === value.modelName && model_provider.provider_name === value.providerName)
const modelOptions: ModelOption[] = (() => {
const providers = _.uniq(filteredModelList.map(item => item.model_provider.provider_name))
const res: ModelOption[] = []
providers.forEach((providerName) => {
res.push({
type: 'provider',
value: providerName,
})
const models = filteredModelList.filter(m => m.model_provider.provider_name === providerName)
models.forEach(({ model_name, model_display_name, model_mode }) => {
res.push({
type: 'model',
providerName,
value: model_name,
modelDisplayName: model_display_name,
model_mode,
})
})
})
return res
})()
const { eventEmitter } = useEventEmitterContextContext()
const [showRerankModal, setShowRerankModal] = useState(false)
const [shouldFetchRerankDefaultModel, setShouldFetchRerankDefaultModel] = useState(false)
const { notify } = useToastContext()
const { data: rerankDefaultModel } = useSWR(shouldFetchRerankDefaultModel ? '/workspaces/current/default-model?model_type=reranking' : null, fetchDefaultModal)
const handleOpenRerankModal = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
setShowRerankModal(true)
}
const handleRerankModalSave = async (originValue?: FormValue) => {
if (originValue) {
try {
eventEmitter?.emit('provider-save')
const res = await setModelProvider({
url: `/workspaces/current/model-providers/${cohereConfig.modal.key}`,
body: {
config: originValue,
},
})
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
updateModelList(ModelType.reranking)
setShowRerankModal(false)
setShouldFetchRerankDefaultModel(true)
if (onUpdate)
onUpdate()
}
eventEmitter?.emit('')
}
catch (e) {
eventEmitter?.emit('')
}
}
}
const [open, setOpen] = useState(false)
const triggerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (rerankDefaultModel && whenEmptyGoToSetting)
onChange(rerankDefaultModel)
}, [rerankDefaultModel])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
>
<div className='relative'>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)} className={cn('flex items-center px-2.5 w-full h-9 rounded-lg', readonly ? '!cursor-auto bg-gray-100 opacity-50' : 'bg-gray-100', hasRemoved && '!bg-[#FEF3F2]')}>
{
<div ref={triggerRef} className='flex items-center w-full cursor-pointer'>
{
(value && value.modelName && value.providerName)
? (
<>
<ModelIcon
className={cn('mr-1.5', !triggerIconSmall && 'w-5 h-5')}
modelId={value.modelName}
providerName={value.providerName}
/>
<div className='mr-1.5 grow flex items-center text-left text-sm text-gray-900 truncate'>
<ModelName modelId={value.modelName} modelDisplayName={currModel?.model_display_name || value.modelName} />
{isShowModelModeType && (
<ModelModeTypeLabel className='ml-2' type={currModel?.model_mode as ModelModeType} />
)}
</div>
</>
)
: whenEmptyGoToSetting
? (
<div className='grow flex items-center h-9 justify-between' onClick={handleOpenRerankModal}>
<div className='flex items-center text-[13px] font-medium text-primary-500'>
<CubeOutline className='mr-1.5 w-4 h-4' />
{t('common.modelProvider.selector.rerankTip')}
</div>
<LinkExternal01 className='w-3 h-3 text-gray-500' />
</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 && !whenEmptyGoToSetting && (
<ChevronDown className={`w-4 h-4 text-gray-700 ${open ? 'opacity-100' : 'opacity-60'}`} />
)
}
{
whenEmptyGoToSetting && (value && value.modelName && value.providerName) && (
<ChevronDown className={`w-4 h-4 text-gray-700 ${open ? 'opacity-100' : 'opacity-60'}`} />
)
}
</div>
}
</PortalToFollowElemTrigger>
{!readonly && (
<PortalToFollowElemContent
className={cn(popClassName, !widthSameToTrigger && (isShowModelModeType ? 'max-w-[312px]' : 'max-w-[260px]'), 'absolute top-10 p-1 min-w-[232px] max-h-[366px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg overflow-auto z-[999]')}
style={{
width: (widthSameToTrigger && triggerRef.current?.offsetWidth) ? `${triggerRef.current?.offsetWidth}px` : 'auto',
}}
>
<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) => {
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 (
<div
key={`${model.providerName}-${model.value}`}
className={`${s.optionItem}
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)
setOpen(false)
}}
>
<ModelIcon
className='mr-2 shrink-0'
modelId={model.value}
providerName={model.providerName}
/>
<div className='mr-2 grow flex items-center text-left text-sm text-gray-900 truncate'>
<ModelName modelId={model.value} modelDisplayName={model.modelDisplayName} />
{isShowModelModeType && (
<ModelModeTypeLabel className={`${s.modelModeLabel} ml-2`} type={model.model_mode} />
)}
</div>
{(value?.providerName === model.providerName && value?.modelName === model.value) && <Check className='shrink-0 w-4 h-4 text-primary-600' />}
</div>
)
}
return null
})
}
{modelList.length !== 0 && (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>
)}
{isShowAddModel && (
<div
className='border-t flex items-center h-9 pl-3 text-xs text-[#155EEF] cursor-pointer'
style={{
borderColor: 'rgba(0, 0, 0, 0.05)',
}}
onClick={() => setShowAccountSettingModal({ payload: 'provider' })}
>
<CubeOutline className='w-4 h-4 mr-2' />
<div>{t('common.model.addMoreModel')}</div>
</div>
)}
</PortalToFollowElemContent>
)}
</div>
<ModelModal
isShow={showRerankModal}
modelModal={cohereConfig.modal}
onCancel={() => setShowRerankModal(false)}
onSave={handleRerankModalSave}
mode={'add'}
/>
</PortalToFollowElem>
)
}
export default ModelSelector

View file

@ -1,7 +0,0 @@
.modelModeLabel {
visibility: hidden;
}
.optionItem:hover .modelModeLabel {
visibility: visible;
}

View file

@ -1,97 +0,0 @@
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

View file

@ -1,35 +0,0 @@
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, ProviderEnum.xinference, ProviderEnum.openllm, ProviderEnum.localai]
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 })
}
}

View file

@ -0,0 +1,216 @@
export type FormValue = Record<string, any>
export type TypeWithI18N<T = string> = {
'en_US': T
'zh_Hans': T
}
export enum FormTypeEnum {
textInput = 'text-input',
secretInput = 'secret-input',
select = 'select',
radio = 'radio',
}
export type FormOption = {
label: TypeWithI18N
value: string
show_on: FormShowOnObject[]
}
export enum ModelTypeEnum {
textGeneration = 'llm',
textEmbedding = 'text-embedding',
rerank = 'rerank',
speech2text = 'speech2text',
moderation = 'moderation',
}
export const MODEL_TYPE_TEXT = {
[ModelTypeEnum.textGeneration]: 'LLM',
[ModelTypeEnum.textEmbedding]: 'Text Embedding',
[ModelTypeEnum.rerank]: 'Rerank',
[ModelTypeEnum.speech2text]: 'Speech2text',
[ModelTypeEnum.moderation]: 'Moderation',
}
export enum ConfigurateMethodEnum {
predefinedModel = 'predefined-model',
customizableModel = 'customizable-model',
fetchFromRemote = 'fetch-from-remote',
}
export enum ModelFeatureEnum {
toolCall = 'tool-call',
multiToolCall = 'multi-tool-call',
agentThought = 'agent-thought',
vision = 'vision',
}
export enum ModelFeatureTextEnum {
toolCall = 'Tool Call',
multiToolCall = 'Multi Tool Call',
agentThought = 'Agent Thought',
vision = 'Vision',
}
export enum ModelStatusEnum {
active = 'active',
noConfigure = 'no-configure',
quotaExceeded = 'quota-exceeded',
noPermission = 'no-permission',
}
export const MODEL_STATUS_TEXT = {
[ModelStatusEnum.noConfigure]: {
en_US: 'No Configure',
zh_Hans: '未配置凭据',
},
[ModelStatusEnum.quotaExceeded]: {
en_US: 'Quota Exceeded',
zh_Hans: '额度不足',
},
[ModelStatusEnum.noPermission]: {
en_US: 'No Permission',
zh_Hans: '无使用权限',
},
}
export enum CustomConfigurationStatusEnum {
active = 'active',
noConfigure = 'no-configure',
}
export type FormShowOnObject = {
variable: string
value: string
}
export type CredentialFormSchemaBase = {
variable: string
label: TypeWithI18N
type: FormTypeEnum
required: boolean
default?: string
show_on: FormShowOnObject[]
}
export type CredentialFormSchemaTextInput = CredentialFormSchemaBase & { max_length?: number; placeholder?: TypeWithI18N }
export type CredentialFormSchemaSelect = CredentialFormSchemaBase & { options: FormOption[]; placeholder?: TypeWithI18N }
export type CredentialFormSchemaRadio = CredentialFormSchemaBase & { options: FormOption[] }
export type CredentialFormSchemaSecretInput = CredentialFormSchemaBase & { placeholder?: TypeWithI18N }
export type CredentialFormSchema = CredentialFormSchemaTextInput | CredentialFormSchemaSelect | CredentialFormSchemaRadio | CredentialFormSchemaSecretInput
export type ModelItem = {
model: string
label: TypeWithI18N
model_type: ModelTypeEnum
features?: ModelFeatureEnum[]
fetch_from: ConfigurateMethodEnum
status: ModelStatusEnum
model_properties: Record<string, string | number>
deprecated?: boolean
}
export enum PreferredProviderTypeEnum {
system = 'system',
custom = 'custom',
}
export enum CurrentSystemQuotaTypeEnum {
trial = 'trial',
free = 'free',
paid = 'paid',
}
export enum QuotaUnitEnum {
times = 'times',
tokens = 'tokens',
}
export type QuotaConfiguration = {
quota_type: CurrentSystemQuotaTypeEnum
quota_unit: QuotaUnitEnum
quota_limit: number
quota_used: number
last_used: number
is_valid: boolean
}
export type ModelProvider = {
provider: string
label: TypeWithI18N
description?: TypeWithI18N
help: {
title: TypeWithI18N
url: TypeWithI18N
}
icon_small: TypeWithI18N
icon_large: TypeWithI18N
background?: string
supported_model_types: ModelTypeEnum[]
configurate_methods: ConfigurateMethodEnum[]
provider_credential_schema: {
credential_form_schemas: CredentialFormSchema[]
}
model_credential_schema: {
model: {
label: TypeWithI18N
placeholder: TypeWithI18N
}
credential_form_schemas: CredentialFormSchema[]
}
preferred_provider_type: PreferredProviderTypeEnum
custom_configuration: {
status: CustomConfigurationStatusEnum
}
system_configuration: {
enabled: boolean
current_quota_type: CurrentSystemQuotaTypeEnum
quota_configurations: QuotaConfiguration[]
}
}
export type Model = {
provider: string
icon_large: TypeWithI18N
icon_small: TypeWithI18N
label: TypeWithI18N
models: ModelItem[]
status: ModelStatusEnum
}
export type DefaultModelResponse = {
model: string
model_type: ModelTypeEnum
provider: {
provider: string
icon_large: TypeWithI18N
icon_small: TypeWithI18N
}
}
export type DefaultModel = {
provider: string
model: string
}
export type CustomConfigrationModelFixedFields = {
__model_name: string
__model_type: ModelTypeEnum
}
export type ModelParameterRule = {
default?: number | string | boolean | string[]
help?: TypeWithI18N
label: TypeWithI18N
min?: number
max?: number
name: string
precision?: number
required: false
type: string
use_template?: string
options?: string[]
tagPlaceholder?: TypeWithI18N
}

View file

@ -0,0 +1,271 @@
import {
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import useSWR, { useSWRConfig } from 'swr'
import { useContext } from 'use-context-selector'
import type {
CustomConfigrationModelFixedFields,
DefaultModel,
DefaultModelResponse,
Model,
} from './declarations'
import {
ConfigurateMethodEnum,
ModelTypeEnum,
} from './declarations'
import { languageMaps } from './utils'
import I18n from '@/context/i18n'
import {
fetchDefaultModal,
fetchModelList,
fetchModelProviderCredentials,
fetchModelProviders,
getPayUrl,
submitFreeQuota,
} from '@/service/common'
import { useProviderContext } from '@/context/provider-context'
type UseDefaultModelAndModelList = (
defaultModel: DefaultModelResponse | undefined,
modelList: Model[],
) => [DefaultModel | undefined, (model: DefaultModel) => void]
export const useSystemDefaultModelAndModelList: UseDefaultModelAndModelList = (
defaultModel,
modelList,
) => {
const currentDefaultModel = useMemo(() => {
const currentProvider = modelList.find(provider => provider.provider === defaultModel?.provider.provider)
const currentModel = currentProvider?.models.find(model => model.model === defaultModel?.model)
const currentDefaultModel = currentProvider && currentModel && {
model: currentModel.model,
provider: currentProvider.provider,
}
return currentDefaultModel
}, [defaultModel, modelList])
const [defaultModelState, setDefaultModelState] = useState<DefaultModel | undefined>(currentDefaultModel)
const handleDefaultModelChange = useCallback((model: DefaultModel) => {
setDefaultModelState(model)
}, [])
useEffect(() => {
setDefaultModelState(currentDefaultModel)
}, [currentDefaultModel])
return [defaultModelState, handleDefaultModelChange]
}
export const useLanguage = () => {
const { locale } = useContext(I18n)
return languageMaps[locale]
}
export const useProviderCrenditialsFormSchemasValue = (
provider: string,
configurateMethod: ConfigurateMethodEnum,
configured?: boolean,
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields,
) => {
const { data: predefinedFormSchemasValue } = useSWR(
(configurateMethod === ConfigurateMethodEnum.predefinedModel && configured)
? `/workspaces/current/model-providers/${provider}/credentials`
: null,
fetchModelProviderCredentials,
)
const { data: customFormSchemasValue } = useSWR(
(configurateMethod === ConfigurateMethodEnum.customizableModel && currentCustomConfigrationModelFixedFields)
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigrationModelFixedFields?.__model_name}&model_type=${currentCustomConfigrationModelFixedFields?.__model_type}`
: null,
fetchModelProviderCredentials,
)
const value = useMemo(() => {
return configurateMethod === ConfigurateMethodEnum.predefinedModel
? predefinedFormSchemasValue?.credentials
: customFormSchemasValue?.credentials
? {
...customFormSchemasValue?.credentials,
...currentCustomConfigrationModelFixedFields,
}
: undefined
}, [
configurateMethod,
currentCustomConfigrationModelFixedFields,
customFormSchemasValue?.credentials,
predefinedFormSchemasValue?.credentials,
])
return value
}
export type ModelTypeIndex = 1 | 2 | 3 | 4
export const MODEL_TYPE_MAPS = {
1: ModelTypeEnum.textGeneration,
2: ModelTypeEnum.textEmbedding,
3: ModelTypeEnum.rerank,
4: ModelTypeEnum.speech2text,
}
export const useModelList = (type: ModelTypeIndex) => {
const { data, mutate, isLoading } = useSWR(`/workspaces/current/models/model-types/${MODEL_TYPE_MAPS[type]}`, fetchModelList)
return {
data: data?.data || [],
mutate,
isLoading,
}
}
export const useDefaultModel = (type: ModelTypeIndex) => {
const { data, mutate, isLoading } = useSWR(`/workspaces/current/default-model?model_type=${MODEL_TYPE_MAPS[type]}`, fetchDefaultModal)
return {
data: data?.data,
mutate,
isLoading,
}
}
export const useCurrentProviderAndModel = (modelList: Model[], defaultModel?: DefaultModel) => {
const currentProvider = modelList.find(provider => provider.provider === defaultModel?.provider)
const currentModel = currentProvider?.models.find(model => model.model === defaultModel?.model)
return {
currentProvider,
currentModel,
}
}
export const useTextGenerationCurrentProviderAndModelAndModelList = (defaultModel?: DefaultModel) => {
const { textGenerationModelList } = useProviderContext()
const {
currentProvider,
currentModel,
} = useCurrentProviderAndModel(textGenerationModelList, defaultModel)
return {
currentProvider,
currentModel,
textGenerationModelList,
}
}
export const useAgentThoughtCurrentProviderAndModelAndModelList = (defaultModel?: DefaultModel) => {
const { agentThoughtModelList } = useProviderContext()
const {
currentProvider,
currentModel,
} = useCurrentProviderAndModel(agentThoughtModelList, defaultModel)
return {
currentProvider,
currentModel,
agentThoughtModelList,
}
}
export const useModelListAndDefaultModel = (type: ModelTypeIndex) => {
const { data: modelList } = useModelList(type)
const { data: defaultModel } = useDefaultModel(type)
return {
modelList,
defaultModel,
}
}
export const useModelListAndDefaultModelAndCurrentProviderAndModel = (type: ModelTypeIndex) => {
const { modelList, defaultModel } = useModelListAndDefaultModel(type)
const { currentProvider, currentModel } = useCurrentProviderAndModel(
modelList,
{ provider: defaultModel?.provider.provider || '', model: defaultModel?.model || '' },
)
return {
modelList,
defaultModel,
currentProvider,
currentModel,
}
}
export const useUpdateModelList = () => {
const { mutate } = useSWRConfig()
const updateModelList = useCallback((type: ModelTypeIndex | ModelTypeEnum) => {
const modelType = typeof type === 'number' ? MODEL_TYPE_MAPS[type] : type
mutate(`/workspaces/current/models/model-types/${modelType}`)
}, [mutate])
return updateModelList
}
export const useAnthropicBuyQuota = () => {
const [loading, setLoading] = useState(false)
const handleGetPayUrl = async () => {
if (loading)
return
setLoading(true)
try {
const res = await getPayUrl('/workspaces/current/model-providers/anthropic/checkout-url')
window.location.href = res.url
}
finally {
setLoading(false)
}
}
return handleGetPayUrl
}
export const useFreeQuota = (onSuccess: () => void) => {
const [loading, setLoading] = useState(false)
const handleClick = async (type: string) => {
if (loading)
return
try {
setLoading(true)
const res = await submitFreeQuota(`/workspaces/current/model-providers/${type}/free-quota-submit`)
if (res.type === 'redirect' && res.redirect_url)
window.location.href = res.redirect_url
else if (res.type === 'submit' && res.result === 'success')
onSuccess()
}
finally {
setLoading(false)
}
}
return handleClick
}
export const useModelProviders = () => {
const { data: providersData, mutate, isLoading } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
return {
data: providersData?.data || [],
mutate,
isLoading,
}
}
export const useUpdateModelProvidersAndModelList = () => {
const { mutate } = useSWRConfig()
const updateModelList = useUpdateModelList()
const updateModelProvidersAndModelList = useCallback(() => {
mutate('/workspaces/current/model-providers')
updateModelList(1)
}, [mutate, updateModelList])
return updateModelProvidersAndModelList
}

View file

@ -0,0 +1,130 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import SystemModelSelector from './system-model-selector'
import ProviderAddedCard, { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card'
import ProviderCard from './provider-card'
import type {
ConfigurateMethodEnum,
CustomConfigrationModelFixedFields,
ModelProvider,
} from './declarations'
import { CustomConfigurationStatusEnum } from './declarations'
import {
useDefaultModel,
useUpdateModelProvidersAndModelList,
} from './hooks'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter'
const ModelProviderPage = () => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
const updateModelProvidersAndModelList = useUpdateModelProvidersAndModelList()
const { data: textGenerationDefaultModel } = useDefaultModel(1)
const { data: embeddingsDefaultModel } = useDefaultModel(2)
const { data: rerankDefaultModel } = useDefaultModel(3)
const { data: speech2textDefaultModel } = useDefaultModel(4)
const { modelProviders: providers } = useProviderContext()
const { setShowModelModal } = useModalContext()
const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel
const [configedProviders, notConfigedProviders] = useMemo(() => {
const configedProviders: ModelProvider[] = []
const notConfigedProviders: ModelProvider[] = []
providers.forEach((provider) => {
if (provider.custom_configuration.status === CustomConfigurationStatusEnum.active || provider.system_configuration.enabled === true)
configedProviders.push(provider)
else
notConfigedProviders.push(provider)
})
return [configedProviders, notConfigedProviders]
}, [providers])
const handleOpenModal = (
provider: ModelProvider,
configurateMethod: ConfigurateMethodEnum,
customConfigrationModelFixedFields?: CustomConfigrationModelFixedFields,
) => {
setShowModelModal({
payload: {
currentProvider: provider,
currentConfigurateMethod: configurateMethod,
currentCustomConfigrationModelFixedFields: customConfigrationModelFixedFields,
},
onSaveCallback: () => {
updateModelProvidersAndModelList()
if (customConfigrationModelFixedFields && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
eventEmitter?.emit({
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
payload: provider.provider,
} as any)
}
},
})
}
return (
<div className='relative pt-1 -mt-2'>
<div className={`flex items-center justify-between mb-2 h-8 ${defaultModelNotConfigured && 'px-3 bg-[#FFFAEB] rounded-lg border border-[#FEF0C7]'}`}>
{
defaultModelNotConfigured
? (
<div className='flex items-center text-xs font-medium text-gray-700'>
<AlertTriangle className='mr-1 w-3 h-3 text-[#F79009]' />
{t('common.modelProvider.notConfigured')}
</div>
)
: <div className='text-sm font-medium text-gray-800'>{t('common.modelProvider.models')}</div>
}
<SystemModelSelector
textGenerationDefaultModel={textGenerationDefaultModel}
embeddingsDefaultModel={embeddingsDefaultModel}
rerankDefaultModel={rerankDefaultModel}
speech2textDefaultModel={speech2textDefaultModel}
/>
</div>
{
!!configedProviders?.length && (
<div className='pb-3'>
{
configedProviders?.map(provider => (
<ProviderAddedCard
key={provider.provider}
provider={provider}
onOpenModal={(configurateMethod: ConfigurateMethodEnum, currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => handleOpenModal(provider, configurateMethod, currentCustomConfigrationModelFixedFields)}
/>
))
}
</div>
)
}
{
!!notConfigedProviders?.length && (
<>
<div className='flex items-center mb-2 text-xs font-semibold text-gray-500'>
+ {t('common.modelProvider.addMoreModelProvider')}
<span className='grow ml-3 h-[1px] bg-gradient-to-r from-[#f3f4f6]' />
</div>
<div className='grid grid-cols-3 gap-2'>
{
notConfigedProviders?.map(provider => (
<ProviderCard
key={provider.provider}
provider={provider}
onOpenModal={(configurateMethod: ConfigurateMethodEnum) => handleOpenModal(provider, configurateMethod)}
/>
))
}
</div>
</>
)
}
</div>
)
}
export default ModelProviderPage

View file

@ -0,0 +1,22 @@
import type { FC, ReactNode } from 'react'
type ModelBadgeProps = {
className?: string
children?: ReactNode
}
const ModelBadge: FC<ModelBadgeProps> = ({
className,
children,
}) => {
return (
<div className={`
flex items-center px-1 h-[18px] rounded-[5px] border border-black/[0.08] bg-white/[0.48]
text-[10px] font-medium text-gray-500
${className}
`}>
{children}
</div>
)
}
export default ModelBadge

View file

@ -0,0 +1,45 @@
import type { FC } from 'react'
import type {
Model,
ModelProvider,
} from '../declarations'
import { useLanguage } from '../hooks'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import { OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
type ModelIconProps = {
provider?: Model | ModelProvider
modelName?: string
className?: string
}
const ModelIcon: FC<ModelIconProps> = ({
provider,
className,
modelName,
}) => {
const language = useLanguage()
if (provider?.provider === 'openai' && modelName?.startsWith('gpt-4'))
return <OpenaiViolet className={`w-4 h-4 ${className}`}/>
if (provider?.icon_small) {
return (
<img
alt='model-icon'
src={`${provider.icon_small[language]}?_token=${localStorage.getItem('console_token')}`}
className={`w-4 h-4 ${className}`}
/>
)
}
return (
<div className={`
flex items-center justify-center w-6 h-6 rounded border-[0.5px] border-black/5 bg-gray-50
${className}
`}>
<CubeOutline className='w-4 h-4 text-gray-400' />
</div>
)
}
export default ModelIcon

View file

@ -0,0 +1,183 @@
import { useState } from 'react'
import type { FC } from 'react'
import { ValidatingTip } from '../../key-validator/ValidateStatus'
import type {
CredentialFormSchema,
CredentialFormSchemaRadio,
CredentialFormSchemaSecretInput,
CredentialFormSchemaSelect,
CredentialFormSchemaTextInput,
FormValue,
} from '../declarations'
import { FormTypeEnum } from '../declarations'
import { useLanguage } from '../hooks'
import Input from './Input'
import { SimpleSelect } from '@/app/components/base/select'
type FormProps = {
value: FormValue
onChange: (val: FormValue) => void
formSchemas: CredentialFormSchema[]
validating: boolean
validatedSuccess?: boolean
showOnVariableMap: Record<string, string[]>
}
const Form: FC<FormProps> = ({
value,
onChange,
formSchemas,
validating,
validatedSuccess,
showOnVariableMap,
}) => {
const language = useLanguage()
const [changeKey, setChangeKey] = useState('')
const handleFormChange = (key: string, val: string) => {
setChangeKey(key)
const shouldClearVariable: Record<string, string | undefined> = {}
if (showOnVariableMap[key]?.length) {
showOnVariableMap[key].forEach((clearVariable) => {
shouldClearVariable[clearVariable] = undefined
})
}
onChange({ ...value, [key]: val, ...shouldClearVariable })
}
const renderField = (formSchema: CredentialFormSchema) => {
if (formSchema.type === FormTypeEnum.textInput || formSchema.type === FormTypeEnum.secretInput) {
const {
variable,
label,
placeholder,
required,
show_on,
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
return null
return (
<div key={variable} className='py-3'>
<div className='py-2 text-sm text-gray-900'>
{label[language]}
{
required && (
<span className='ml-1 text-red-500'>*</span>
)
}
</div>
<Input
value={value[variable] as string}
onChange={val => handleFormChange(variable, val)}
validated={validatedSuccess}
placeholder={placeholder?.[language]}
/>
{validating && changeKey === variable && <ValidatingTip />}
</div>
)
}
if (formSchema.type === FormTypeEnum.radio) {
const {
options,
variable,
label,
show_on,
required,
} = formSchema as CredentialFormSchemaRadio
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
return null
return (
<div key={variable} className='py-3'>
<div className='py-2 text-sm text-gray-900'>
{label[language]}
{
required && (
<span className='ml-1 text-red-500'>*</span>
)
}
</div>
<div className={`grid grid-cols-${options?.length} gap-3`}>
{
options.filter((option) => {
if (option.show_on.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map(option => (
<div
className={`
flex items-center px-3 py-2 rounded-lg border border-gray-100 bg-gray-25 cursor-pointer
${value[variable] === option.value && 'bg-white border-[1.5px] border-primary-400 shadow-sm'}
`}
onClick={() => handleFormChange(variable, option.value)}
key={`${variable}-${option.value}`}
>
<div className={`
flex justify-center items-center mr-2 w-4 h-4 border border-gray-300 rounded-full
${value[variable] === option.value && 'border-[5px] border-primary-600'}
`} />
<div className='text-sm text-gray-900'>{option.label[language]}</div>
</div>
))
}
</div>
{validating && changeKey === variable && <ValidatingTip />}
</div>
)
}
if (formSchema.type === 'select') {
const {
options,
variable,
label,
show_on,
required,
placeholder,
} = formSchema as CredentialFormSchemaSelect
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
return null
return (
<div key={variable} className='py-3'>
<div className='py-2 text-sm text-gray-900'>
{label[language]}
{
required && (
<span className='ml-1 text-red-500'>*</span>
)
}
</div>
<SimpleSelect
defaultValue={value[variable] as string}
items={options.filter((option) => {
if (option.show_on.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map(option => ({ value: option.value, name: option.label[language] }))}
onSelect={item => handleFormChange(variable, item.value as string)}
placeholder={placeholder?.[language]}
/>
{validating && changeKey === variable && <ValidatingTip />}
</div>
)
}
}
return (
<div>
{
formSchemas.map(formSchema => renderField(formSchema))
}
</div>
)
}
export default Form

View file

@ -0,0 +1,46 @@
import type { FC } from 'react'
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
type InputProps = {
value?: string
onChange: (v: string) => void
onFocus?: () => void
placeholder?: string
validated?: boolean
}
const Input: FC<InputProps> = ({
value,
onChange,
onFocus,
placeholder,
validated,
}) => {
return (
<div className='relative'>
<input
tabIndex={-1}
className={`
block px-3 w-full h-9 bg-gray-100 text-sm rounded-lg border border-transparent
appearance-none outline-none caret-primary-600
hover:border-[rgba(0,0,0,0.08)] hover:bg-gray-50
focus:bg-white focus:border-gray-300 focus:shadow-xs
placeholder:text-sm placeholder:text-gray-400
${validated && 'pr-[30px]'}
`}
placeholder={placeholder || ''}
onChange={e => onChange(e.target.value)}
onFocus={onFocus}
value={value || ''}
/>
{
validated && (
<div className='absolute top-2.5 right-2.5'>
<CheckCircle className='w-4 h-4 text-[#039855]' />
</div>
)
}
</div>
)
}
export default Input

View file

@ -0,0 +1,330 @@
import type { FC } from 'react'
import {
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import type {
CredentialFormSchema,
CredentialFormSchemaRadio,
CredentialFormSchemaSelect,
CustomConfigrationModelFixedFields,
FormValue,
ModelProvider,
} from '../declarations'
import {
ConfigurateMethodEnum,
CustomConfigurationStatusEnum,
FormTypeEnum,
} from '../declarations'
import {
genModelNameFormSchema,
genModelTypeFormSchema,
removeCredentials,
saveCredentials,
} from '../utils'
import {
useLanguage,
useProviderCrenditialsFormSchemasValue,
} from '../hooks'
import ProviderIcon from '../provider-icon'
import { useValidate } from '../../key-validator/hooks'
import { ValidatedStatus } from '../../key-validator/declarations'
import Form from './Form'
import Button from '@/app/components/base/button'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import { useToastContext } from '@/app/components/base/toast'
import ConfirmCommon from '@/app/components/base/confirm/common'
type ModelModalProps = {
provider: ModelProvider
configurateMethod: ConfigurateMethodEnum
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields
onCancel: () => void
onSave: () => void
}
const ModelModal: FC<ModelModalProps> = ({
provider,
configurateMethod,
currentCustomConfigrationModelFixedFields,
onCancel,
onSave,
}) => {
const providerFormSchemaPredefined = configurateMethod === ConfigurateMethodEnum.predefinedModel
const formSchemasValue = useProviderCrenditialsFormSchemasValue(
provider.provider,
configurateMethod,
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
currentCustomConfigrationModelFixedFields,
)
const isEditMode = !!formSchemasValue
const { t } = useTranslation()
const { notify } = useToastContext()
const language = useLanguage()
const [loading, setLoading] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const formSchemas = useMemo(() => {
return providerFormSchemaPredefined
? provider.provider_credential_schema.credential_form_schemas
: [
genModelTypeFormSchema(provider.supported_model_types),
genModelNameFormSchema(provider.model_credential_schema?.model),
...provider.model_credential_schema.credential_form_schemas,
]
}, [
providerFormSchemaPredefined,
provider.provider_credential_schema?.credential_form_schemas,
provider.supported_model_types,
provider.model_credential_schema?.credential_form_schemas,
provider.model_credential_schema?.model,
])
const [
requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue,
showOnVariableMap,
] = useMemo(() => {
const requiredFormSchemas: CredentialFormSchema[] = []
const secretFormSchemas: CredentialFormSchema[] = []
const defaultFormSchemaValue: Record<string, string | number> = {}
const showOnVariableMap: Record<string, string[]> = {}
formSchemas.forEach((formSchema) => {
if (formSchema.required)
requiredFormSchemas.push(formSchema)
if (formSchema.type === FormTypeEnum.secretInput)
secretFormSchemas.push(formSchema)
if (formSchema.default)
defaultFormSchemaValue[formSchema.variable] = formSchema.default
if (formSchema.show_on.length) {
formSchema.show_on.forEach((showOnItem) => {
if (!showOnVariableMap[showOnItem.variable])
showOnVariableMap[showOnItem.variable] = []
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
})
}
if (formSchema.type === FormTypeEnum.select || formSchema.type === FormTypeEnum.radio) {
(formSchema as (CredentialFormSchemaRadio | CredentialFormSchemaSelect)).options.forEach((option) => {
if (option.show_on.length) {
option.show_on.forEach((showOnItem) => {
if (!showOnVariableMap[showOnItem.variable])
showOnVariableMap[showOnItem.variable] = []
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
})
}
})
}
})
return [
requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue,
showOnVariableMap,
]
}, [formSchemas])
const initialFormSchemasValue = useMemo(() => {
return {
...defaultFormSchemaValue,
...formSchemasValue,
}
}, [formSchemasValue, defaultFormSchemaValue])
const [value, setValue] = useState(initialFormSchemasValue)
useEffect(() => {
setValue(initialFormSchemasValue)
}, [initialFormSchemasValue])
const [validate, validating, validatedStatusState] = useValidate(value)
const filteredRequiredFormSchemas = requiredFormSchemas.filter((requiredFormSchema) => {
if (requiredFormSchema.show_on.length && requiredFormSchema.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
return true
if (!requiredFormSchema.show_on.length)
return true
return false
})
const getSecretValues = useCallback((v: FormValue) => {
return secretFormSchemas.reduce((prev, next) => {
if (v[next.variable] === initialFormSchemasValue[next.variable])
prev[next.variable] = '[__HIDDEN__]'
return prev
}, {} as Record<string, string>)
}, [initialFormSchemasValue, secretFormSchemas])
const handleValueChange = (v: FormValue) => {
setValue(v)
}
const handleSave = async () => {
try {
setLoading(true)
const res = await saveCredentials(
providerFormSchemaPredefined,
provider.provider,
{
...value,
...getSecretValues(value),
},
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onSave()
onCancel()
}
}
finally {
setLoading(false)
}
}
const handleRemove = async () => {
try {
setLoading(true)
const res = await removeCredentials(
providerFormSchemaPredefined,
provider.provider,
value,
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onSave()
onCancel()
}
}
finally {
setLoading(false)
}
}
const renderTitlePrefix = () => {
const prefix = configurateMethod === ConfigurateMethodEnum.customizableModel ? t('common.operation.add') : t('common.operation.setup')
return `${prefix} ${provider.label[language]}`
}
return (
<PortalToFollowElem open>
<PortalToFollowElemContent className='w-full h-full z-[60]'>
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
<div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-2'>
<div className='text-xl font-semibold text-gray-900'>{renderTitlePrefix()}</div>
<ProviderIcon provider={provider} />
</div>
<Form
value={value}
onChange={handleValueChange}
formSchemas={formSchemas}
validating={validating}
validatedSuccess={validatedStatusState.status === ValidatedStatus.Success}
showOnVariableMap={showOnVariableMap}
/>
<div className='sticky bottom-0 flex justify-between items-center py-6 flex-wrap gap-y-2 bg-white'>
{
(provider.help && (provider.help.title || provider.help.url))
? (
<a
href={provider.help?.url[language]}
target='_blank'
className='inline-flex items-center text-xs text-primary-600'
onClick={e => !provider.help.url && e.preventDefault()}
>
{provider.help.title?.[language] || provider.help.url[language]}
<LinkExternal02 className='ml-1 w-3 h-3' />
</a>
)
: <div />
}
<div>
{
isEditMode && (
<Button
className='mr-2 h-9 text-sm font-medium text-[#D92D20]'
onClick={() => setShowConfirm(true)}
>
{t('common.operation.remove')}
</Button>
)
}
<Button
className='mr-2 h-9 text-sm font-medium text-gray-700'
onClick={onCancel}
>
{t('common.operation.cancel')}
</Button>
<Button
className='h-9 text-sm font-medium'
type='primary'
onClick={handleSave}
disabled={loading || filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)}
>
{t('common.operation.save')}
</Button>
</div>
</div>
</div>
<div className='border-t-[0.5px] border-t-black/5'>
{
(validatedStatusState.status === ValidatedStatus.Error && validatedStatusState.message)
? (
<div className='flex px-[10px] py-3 bg-[#FEF3F2] text-xs text-[#D92D20]'>
<AlertCircle className='mt-[1px] mr-2 w-[14px] h-[14px]' />
{validatedStatusState.message}
</div>
)
: (
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
<Lock01 className='mr-1 w-3 h-3 text-gray-500' />
{t('common.modelProvider.encrypted.front')}
<a
className='text-primary-600 mx-1'
target={'_blank'}
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
>
PKCS1_OAEP
</a>
{t('common.modelProvider.encrypted.back')}
</div>
)
}
</div>
</div>
{
showConfirm && (
<ConfirmCommon
title='Are you sure?'
isShow={showConfirm}
onCancel={() => setShowConfirm(false)}
onConfirm={handleRemove}
confirmWrapperClassName='z-[70]'
/>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(ModelModal)

View file

@ -0,0 +1,85 @@
import type { FC } from 'react'
import {
modelTypeFormat,
sizeFormat,
} from '../utils'
import { useLanguage } from '../hooks'
import type { ModelItem } from '../declarations'
import ModelBadge from '../model-badge'
import FeatureIcon from '../model-selector/feature-icon'
type ModelNameProps = {
modelItem: ModelItem
className?: string
showModelType?: boolean
modelTypeClassName?: string
showMode?: boolean
modeClassName?: string
showFeatures?: boolean
featuresClassName?: string
showContextSize?: boolean
}
const ModelName: FC<ModelNameProps> = ({
modelItem,
className,
showModelType,
modelTypeClassName,
showMode,
modeClassName,
showFeatures,
featuresClassName,
showContextSize,
}) => {
const language = useLanguage()
if (!modelItem)
return null
return (
<div
className={`
flex items-center truncate text-[13px] font-medium text-gray-800
${className}
`}
>
<div
className='mr-2 truncate'
title={modelItem.label[language]}
>
{modelItem.label[language]}
</div>
{
showModelType && (
<ModelBadge className={`mr-0.5 ${modelTypeClassName}`}>
{modelTypeFormat(modelItem.model_type)}
</ModelBadge>
)
}
{
modelItem.model_properties.mode && showMode && (
<ModelBadge className={`mr-0.5 ${modeClassName}`}>
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
</ModelBadge>
)
}
{
showFeatures && modelItem.features?.map(feature => (
<FeatureIcon
key={feature}
feature={feature}
className={featuresClassName}
/>
))
}
{
showContextSize && modelItem.model_properties.context_size && (
<ModelBadge>
{sizeFormat(modelItem.model_properties.context_size as number)}
</ModelBadge>
)
}
</div>
)
}
export default ModelName

View file

@ -0,0 +1,223 @@
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import type {
DefaultModel,
FormValue,
ModelParameterRule,
} from '../declarations'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import ModelSelector from '../model-selector'
import { useTextGenerationCurrentProviderAndModelAndModelList } from '../hooks'
import ParameterItem from './parameter-item'
import type { ParameterValue } from './parameter-item'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import { fetchModelParameterRules } from '@/service/common'
import Loading from '@/app/components/base/loading'
type ModelParameterModalProps = {
isAdvancedMode: boolean
mode: string
modelId: string
provider: string
setModel: (model: { modelId: string; provider: string; mode?: string; features: string[] }) => void
completionParams: FormValue
onCompletionParamsChange: (newParams: FormValue) => void
disabled: boolean
}
const stopParameerRule: ModelParameterRule = {
default: [],
help: {
en_US: 'Up to four sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.',
zh_Hans: '最多四个序列,API 将停止生成更多的 token。返回的文本将不包含停止序列。',
},
label: {
en_US: 'Stop sequences',
zh_Hans: '停止序列 stop_sequences',
},
name: 'stop',
required: false,
type: 'tag',
tagPlaceholder: {
en_US: 'Enter sequence and press Tab',
zh_Hans: '输入序列并按 Tab 键',
},
}
const ModelParameterModal: FC<ModelParameterModalProps> = ({
isAdvancedMode,
modelId,
provider,
setModel,
completionParams,
onCompletionParamsChange,
disabled,
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const { data: parameterRulesData, isLoading } = useSWR(`/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}`, fetchModelParameterRules)
const {
currentProvider,
currentModel,
textGenerationModelList,
} = useTextGenerationCurrentProviderAndModelAndModelList(
{ provider, model: modelId },
)
const parameterRules = parameterRulesData?.data || []
const handleParamChange = (key: string, value: ParameterValue) => {
onCompletionParamsChange({
...completionParams,
[key]: value,
})
}
const handleChangeModel = ({ provider, model }: DefaultModel) => {
const targetProvider = textGenerationModelList.find(modelItem => modelItem.provider === provider)
const targetModelItem = targetProvider?.models.find(modelItem => modelItem.model === model)
setModel({
modelId: model,
provider,
mode: targetModelItem?.model_properties.mode as string,
features: targetModelItem?.features || [],
})
}
const handleChangeParams = () => {
const newCompletionParams = parameterRules.reduce((acc, parameter) => {
if (parameter.default !== undefined)
acc[parameter.name] = parameter.default
return acc
}, {} as Record<string, any>)
onCompletionParamsChange(newCompletionParams)
}
useEffect(() => {
handleChangeParams()
}, [parameterRules])
const handleSwitch = (key: string, value: boolean, assignValue: ParameterValue) => {
if (!value) {
const newCompletionParams = { ...completionParams }
delete newCompletionParams[key]
onCompletionParamsChange(newCompletionParams)
}
if (value) {
onCompletionParamsChange({
...completionParams,
[key]: assignValue,
})
}
}
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={4}
>
<div className='relative'>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
className='block'
>
<div
className={`
flex items-center px-2 h-8 rounded-lg border cursor-pointer hover:border-[1.5px]
${disabled ? 'border-[#F79009] bg-[#FFFAEB]' : 'border-[#444CE7] bg-primary-50'}
`}
>
{
currentProvider && (
<ModelIcon
className='mr-1.5 !w-5 !h-5'
provider={currentProvider}
modelName={currentModel?.model}
/>
)
}
{
currentModel && (
<ModelName
className='mr-1.5 text-gray-900'
modelItem={currentModel}
showMode={isAdvancedMode}
modeClassName='!text-[#444CE7] !border-[#A4BCFD]'
showFeatures={isAdvancedMode}
featuresClassName='!text-[#444CE7] !border-[#A4BCFD]'
/>
)
}
{
disabled
? (
<AlertTriangle className='w-4 h-4 text-[#F79009]' />
)
: (
<SlidersH className='w-4 h-4 text-indigo-600' />
)
}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
<div className='w-[496px] rounded-xl border border-gray-100 bg-white shadow-xl'>
<div className='flex items-center px-4 h-12 rounded-t-xl border-b border-gray-100 bg-gray-50 text-md font-medium text-gray-900'>
<CubeOutline className='mr-2 w-4 h-4 text-primary-600' />
{t('common.modelProvider.modelAndParameters')}
</div>
<div className='px-10 pt-4 pb-8'>
<div className='flex items-center justify-between h-8'>
<div className='text-sm font-medium text-gray-900'>
{t('common.modelProvider.model')}
</div>
<ModelSelector
defaultModel={{ provider, model: modelId }}
modelList={textGenerationModelList}
onSelect={handleChangeModel}
/>
</div>
<div className='my-5 h-[1px] bg-gray-100' />
{
isLoading && (
<Loading />
)
}
{
!isLoading && (
[
...parameterRules,
...(isAdvancedMode ? [stopParameerRule] : []),
].map(parameter => (
<ParameterItem
key={parameter.name}
className='mb-4'
parameterRule={parameter}
value={completionParams[parameter.name]}
onChange={v => handleParamChange(parameter.name, v)}
onSwitch={(checked, assignValue) => handleSwitch(parameter.name, checked, assignValue)}
/>
))
)
}
</div>
</div>
</PortalToFollowElemContent>
</div>
</PortalToFollowElem>
)
}
export default ModelParameterModal

View file

@ -0,0 +1,223 @@
import type { FC } from 'react'
import { useState } from 'react'
import type { ModelParameterRule } from '../declarations'
import { useLanguage } from '../hooks'
import { isNullOrUndefined } from '../utils'
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Slider from '@/app/components/base/slider'
import Radio from '@/app/components/base/radio'
import { SimpleSelect } from '@/app/components/base/select'
import TagInput from '@/app/components/base/tag-input'
export type ParameterValue = number | string | string[] | boolean | undefined
type ParameterItemProps = {
parameterRule: ModelParameterRule
value?: ParameterValue
onChange?: (value: ParameterValue) => void
className?: string
onSwitch?: (checked: boolean, assignValue: ParameterValue) => void
}
const ParameterItem: FC<ParameterItemProps> = ({
parameterRule,
value,
onChange,
className,
onSwitch,
}) => {
const language = useLanguage()
const [localValue, setLocalValue] = useState(value)
const mergedValue = isNullOrUndefined(value) ? localValue : value
const renderValue = mergedValue === undefined ? parameterRule.default : mergedValue
const handleChange = (v: ParameterValue) => {
setLocalValue(v)
if (!isNullOrUndefined(value) && onChange)
onChange(v)
}
const handleNumberInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let num = +e.target.value
if (!isNullOrUndefined(parameterRule.max) && num > parameterRule.max!)
num = parameterRule.max as number
if (!isNullOrUndefined(parameterRule.min) && num < parameterRule.min!)
num = parameterRule.min as number
handleChange(num)
}
const handleSlideChange = (num: number) => {
handleChange(num)
}
const handleRadioChange = (v: number) => {
handleChange(v === 1)
}
const handleStringInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
handleChange(e.target.value)
}
const handleSelect = (option: { value: string | number; name: string }) => {
handleChange(option.value)
}
const handleTagChange = (newSequences: string[]) => {
handleChange(newSequences)
}
const handleSwitch = (checked: boolean) => {
if (onSwitch) {
let assignValue: ParameterValue = localValue
if (isNullOrUndefined(localValue)) {
if (parameterRule.type === 'int' || parameterRule.type === 'float')
assignValue = !isNullOrUndefined(parameterRule.default) ? parameterRule.default : 0
if (parameterRule.type === 'string' && !parameterRule.options?.length)
assignValue = parameterRule.default || ''
if (parameterRule.type === 'string' && parameterRule.options?.length)
assignValue = parameterRule.options[0]
if (parameterRule.type === 'boolean')
assignValue = !isNullOrUndefined(parameterRule.default) ? parameterRule.default : false
if (parameterRule.type === 'tag')
assignValue = !isNullOrUndefined(parameterRule.default) ? parameterRule.default : []
}
onSwitch(checked, assignValue)
}
}
const numberInputWithSlide = (parameterRule.type === 'int' || parameterRule.type === 'float')
&& !isNullOrUndefined(parameterRule.min)
&& !isNullOrUndefined(parameterRule.max)
const numberInput = (parameterRule.type === 'int' || parameterRule.type === 'float')
&& (isNullOrUndefined(parameterRule.min) || isNullOrUndefined(parameterRule.max))
return (
<div className={`flex items-center justify-between ${className}`}>
<div>
<div className='shrink-0 flex items-center w-[200px]'>
<div
className='mr-0.5 text-[13px] font-medium text-gray-700 truncate'
title={parameterRule.label[language]}
>
{parameterRule.label[language]}
</div>
{
parameterRule.help && (
<Tooltip
selector={`model-parameter-rule-${parameterRule.name}`}
htmlContent={(
<div className='w-[200px] whitespace-pre-wrap'>{parameterRule.help[language]}</div>
)}
>
<HelpCircle className='mr-1.5 w-3.5 h-3.5 text-gray-400' />
</Tooltip>
)
}
{
!parameterRule.required && parameterRule.name !== 'stop' && (
<Switch
defaultValue={!isNullOrUndefined(value)}
onChange={handleSwitch}
size='md'
/>
)
}
</div>
{
parameterRule.type === 'tag' && (
<div className='w-[200px] text-gray-400 text-xs font-normal'>
{parameterRule?.tagPlaceholder?.[language]}
</div>
)
}
</div>
{
numberInputWithSlide && (
<div className='flex items-center'>
<Slider
className='w-[120px]'
value={isNullOrUndefined(renderValue) ? 0 : +renderValue!}
min={parameterRule.min}
max={parameterRule.max}
step={+`0.${parameterRule.precision || 0}`}
onChange={handleSlideChange}
/>
<input
className='shrink-0 block ml-4 pl-3 w-16 h-8 appearance-none outline-none rounded-lg bg-gray-100 text-[13px] text-gra-900'
type='number'
max={parameterRule.max}
min={parameterRule.min}
step={+`0.${parameterRule.precision || 0}`}
value={isNullOrUndefined(renderValue) ? 0 : +renderValue!}
onChange={handleNumberInputChange}
/>
</div>
)
}
{
parameterRule.type === 'boolean' && (
<Radio.Group
className='w-[200px] flex items-center'
value={isNullOrUndefined(renderValue) ? 1 : 0}
onChange={handleRadioChange}
>
<Radio value={1} className='!mr-1 w-[94px]'>True</Radio>
<Radio value={0} className='w-[94px]'>False</Radio>
</Radio.Group>
)
}
{
numberInput && (
<input
type='number'
className='flex items-center px-3 w-[200px] h-8 appearance-none outline-none rounded-lg bg-gray-100 text-[13px] text-gra-900'
value={(isNullOrUndefined(renderValue) ? '' : renderValue) as string}
onChange={handleNumberInputChange}
/>
)
}
{
parameterRule.type === 'string' && !parameterRule.options?.length && (
<input
className='flex items-center px-3 w-[200px] h-8 appearance-none outline-none rounded-lg bg-gray-100 text-[13px] text-gra-900'
value={(isNullOrUndefined(renderValue) ? '' : renderValue) as string}
onChange={handleStringInputChange}
/>
)
}
{
parameterRule.type === 'string' && parameterRule?.options?.length && (
<SimpleSelect
className='!py-0'
wrapperClassName='!w-[200px] !h-8'
defaultValue={renderValue as string}
onSelect={handleSelect}
items={parameterRule.options.map(option => ({ value: option, name: option }))}
/>
)
}
{
parameterRule.type === 'tag' && (
<div className='w-[200px]'>
<TagInput
items={isNullOrUndefined(renderValue) ? [] : (renderValue as string[])}
onChange={handleTagChange}
customizedConfirmKey='Tab'
/>
</div>
)
}
</div>
)
}
export default ParameterItem

View file

@ -0,0 +1,39 @@
import type { FC } from 'react'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
type ModelTriggerProps = {
open: boolean
className?: string
}
const ModelTrigger: FC<ModelTriggerProps> = ({
open,
className,
}) => {
return (
<div
className={`
flex items-center px-2 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 cursor-pointer
${className}
${open && '!bg-gray-200'}
`}
>
<div className='grow flex items-center'>
<div className='mr-1.5 flex items-center justify-center w-4 h-4 rounded-[5px] border border-dashed border-black/5'>
<CubeOutline className='w-3 h-3 text-gray-400' />
</div>
<div
className='text-[13px] text-gray-500 truncate'
title='Select model'
>
Select model
</div>
</div>
<div className='shrink-0 flex items-center justify-center w-4 h-4'>
<ChevronDown className='w-3.5 h-3.5 text-gray-500' />
</div>
</div>
)
}
export default ModelTrigger

View file

@ -0,0 +1,77 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import ModelBadge from '../model-badge'
import {
ModelFeatureEnum,
ModelFeatureTextEnum,
} from '../declarations'
import {
MagicBox,
MagicEyes,
MagicWand,
Robot,
} from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import TooltipPlus from '@/app/components/base/tooltip-plus'
type FeatureIconProps = {
feature: ModelFeatureEnum
className?: string
}
const FeatureIcon: FC<FeatureIconProps> = ({
className,
feature,
}) => {
const { t } = useTranslation()
if (feature === ModelFeatureEnum.agentThought) {
return (
<TooltipPlus
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.agentThought })}
>
<ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
<Robot className='w-3 h-3' />
</ModelBadge>
</TooltipPlus>
)
}
if (feature === ModelFeatureEnum.toolCall) {
return (
<TooltipPlus
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.toolCall })}
>
<ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
<MagicWand className='w-3 h-3' />
</ModelBadge>
</TooltipPlus>
)
}
if (feature === ModelFeatureEnum.multiToolCall) {
return (
<TooltipPlus
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.multiToolCall })}
>
<ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
<MagicBox className='w-3 h-3' />
</ModelBadge>
</TooltipPlus>
)
}
if (feature === ModelFeatureEnum.vision) {
return (
<TooltipPlus
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.vision })}
>
<ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
<MagicEyes className='w-3 h-3' />
</ModelBadge>
</TooltipPlus>
)
}
return null
}
export default FeatureIcon

View file

@ -0,0 +1,100 @@
import type { FC } from 'react'
import { useState } from 'react'
import type {
DefaultModel,
Model,
ModelItem,
} from '../declarations'
import { useCurrentProviderAndModel } from '../hooks'
import ModelTrigger from './model-trigger'
import EmptyTrigger from './empty-trigger'
import Popup from './popup'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
type ModelSelectorProps = {
defaultModel?: DefaultModel
modelList: Model[]
triggerClassName?: string
popupClassName?: string
onSelect?: (model: DefaultModel) => void
readonly?: boolean
}
const ModelSelector: FC<ModelSelectorProps> = ({
defaultModel,
modelList,
triggerClassName,
popupClassName,
onSelect,
readonly,
}) => {
const [open, setOpen] = useState(false)
const {
currentProvider,
currentModel,
} = useCurrentProviderAndModel(
modelList,
defaultModel,
)
const handleSelect = (provider: string, model: ModelItem) => {
setOpen(false)
if (onSelect)
onSelect({ provider, model: model.model })
}
const handleToggle = () => {
if (readonly)
return
setOpen(v => !v)
}
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
>
<div className='relative'>
<PortalToFollowElemTrigger
onClick={handleToggle}
className='block'
>
{
currentModel && currentProvider && (
<ModelTrigger
open={open}
provider={currentProvider}
model={currentModel}
className={triggerClassName}
/>
)
}
{
!currentModel && (
<EmptyTrigger
open={open}
className={triggerClassName}
/>
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className={`z-[60] ${popupClassName}`}>
<Popup
defaultModel={defaultModel}
modelList={modelList}
onSelect={handleSelect}
/>
</PortalToFollowElemContent>
</div>
</PortalToFollowElem>
)
}
export default ModelSelector

View file

@ -0,0 +1,51 @@
import type { FC } from 'react'
import type {
Model,
ModelItem,
} from '../declarations'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
// import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
type ModelTriggerProps = {
open: boolean
provider: Model
model: ModelItem
className?: string
}
const ModelTrigger: FC<ModelTriggerProps> = ({
open,
provider,
model,
className,
}) => {
return (
<div
className={`
group flex items-center px-2 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 cursor-pointer
${className}
${open && '!bg-gray-200'}
`}
>
<ModelIcon
className='shrink-0 mr-1.5'
provider={provider}
modelName={model.model}
/>
<ModelName
className='grow'
modelItem={model}
showMode
showFeatures
/>
<div className='shrink-0 flex items-center justify-center w-4 h-4'>
<ChevronDown
className='w-3.5 h-3.5 text-gray-500'
/>
</div>
</div>
)
}
export default ModelTrigger

View file

@ -0,0 +1,127 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import type {
DefaultModel,
Model,
ModelItem,
} from '../declarations'
import {
useLanguage,
useUpdateModelList,
useUpdateModelProvidersAndModelList,
} from '../hooks'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import {
ConfigurateMethodEnum,
MODEL_STATUS_TEXT,
ModelStatusEnum,
ModelTypeEnum,
} from '../declarations'
import { Check } from '@/app/components/base/icons/src/vender/line/general'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import Tooltip from '@/app/components/base/tooltip'
type PopupItemProps = {
defaultModel?: DefaultModel
model: Model
onSelect: (provider: string, model: ModelItem) => void
}
const PopupItem: FC<PopupItemProps> = ({
defaultModel,
model,
onSelect,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const { setShowModelModal } = useModalContext()
const { modelProviders } = useProviderContext()
const updateModelList = useUpdateModelList()
const updateModelProvidersAndModelList = useUpdateModelProvidersAndModelList()
const currentProvider = modelProviders.find(provider => provider.provider === model.provider)!
const handleSelect = (provider: string, modelItem: ModelItem) => {
if (modelItem.status !== ModelStatusEnum.active)
return
onSelect(provider, modelItem)
}
const handleOpenModelModal = () => {
setShowModelModal({
payload: {
currentProvider,
currentConfigurateMethod: ConfigurateMethodEnum.predefinedModel,
},
onSaveCallback: () => {
updateModelProvidersAndModelList()
const modelType = model.models[0].model_type
if (modelType !== ModelTypeEnum.textGeneration)
updateModelList(modelType)
},
})
}
return (
<div className='mb-1'>
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>
{model.label[language]}
</div>
{
model.models.map(modelItem => (
<Tooltip
selector={`${modelItem.model}-${modelItem.status}`}
key={modelItem.model}
content={modelItem.status !== ModelStatusEnum.active ? MODEL_STATUS_TEXT[modelItem.status][language] : undefined}
position='right'
>
<div
key={modelItem.model}
className={`
group relative flex items-center px-3 py-1.5 h-8 rounded-lg
${modelItem.status === ModelStatusEnum.active ? 'cursor-pointer hover:bg-gray-50' : 'cursor-not-allowed hover:bg-gray-50/60'}
`}
onClick={() => handleSelect(model.provider, modelItem)}
>
<ModelIcon
className={`
shrink-0 mr-2 w-4 h-4
${modelItem.status !== ModelStatusEnum.active && 'opacity-60'}
`}
provider={model}
modelName={modelItem.model}
/>
<ModelName
className={`
grow text-sm font-normal text-gray-900
${modelItem.status !== ModelStatusEnum.active && 'opacity-60'}
`}
modelItem={modelItem}
showMode
showFeatures
/>
{
defaultModel?.model === modelItem.model && (
<Check className='shrink-0 w-4 h-4 text-primary-600' />
)
}
{
modelItem.status === ModelStatusEnum.noConfigure && (
<div
className='hidden group-hover:block text-xs font-medium text-primary-600 cursor-pointer'
onClick={handleOpenModelModal}
>
{t('common.operation.add').toLocaleUpperCase()}
</div>
)
}
</div>
</Tooltip>
))
}
</div>
)
}
export default PopupItem

View file

@ -0,0 +1,80 @@
import type { FC } from 'react'
import { useState } from 'react'
import type {
DefaultModel,
Model,
ModelItem,
} from '../declarations'
import { useLanguage } from '../hooks'
import PopupItem from './popup-item'
import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
type PopupProps = {
defaultModel?: DefaultModel
modelList: Model[]
onSelect: (provider: string, model: ModelItem) => void
}
const Popup: FC<PopupProps> = ({
defaultModel,
modelList,
onSelect,
}) => {
const language = useLanguage()
const [searchText, setSearchText] = useState('')
const filteredModelList = modelList.filter(model => model.models.filter(modelItem => modelItem.label[language].includes(searchText)).length)
return (
<div className='w-[320px] max-h-[480px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg overflow-y-auto'>
<div className='sticky top-0 pl-3 pt-3 pr-2 pb-1 bg-white z-10'>
<div className={`
flex items-center pl-[9px] pr-[10px] h-8 rounded-lg border
${searchText ? 'bg-white border-gray-300 shadow-xs' : 'bg-gray-100 border-transparent'}
`}>
<SearchLg
className={`
shrink-0 mr-[7px] w-[14px] h-[14px]
${searchText ? 'text-gray-500' : 'text-gray-400'}
`}
/>
<input
className='block grow h-[18px] text-[13px] appearance-none outline-none bg-transparent'
placeholder='Search model'
value={searchText}
onChange={e => setSearchText(e.target.value)}
/>
{
searchText && (
<XCircle
className='shrink-0 ml-1.5 w-[14px] h-[14px] text-gray-400 cursor-pointer'
onClick={() => setSearchText('')}
/>
)
}
</div>
</div>
<div className='p-1'>
{
filteredModelList.map(model => (
<PopupItem
key={model.provider}
defaultModel={defaultModel}
model={model}
onSelect={onSelect}
/>
))
}
{
!filteredModelList.length && (
<div className='px-3 py-1.5 leading-[18px] text-center text-xs text-gray-500 break-all'>
{`No model found for “${searchText}”`}
</div>
)
}
</div>
</div>
)
}
export default Popup

View file

@ -0,0 +1,25 @@
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import { LinkExternal01 } from '@/app/components/base/icons/src/vender/line/general'
const ModelTrigger = () => {
return (
<div className='flex items-center px-2 h-8 rounded-lg bg-gray-100 hover:bg-gray-200 cursor-pointer'>
<div className='grow flex items-center'>
<div className='mr-1.5 flex items-center justify-center w-4 h-4 rounded-[5px] border-dashed border-black/5'>
<CubeOutline className='w-[11px] h-[11px] text-gray-400' />
</div>
<div
className='text-[13px] text-gray-500 truncate'
title='Select model'
>
Please setup the Rerank model
</div>
</div>
<div className='shrink-0 flex items-center justify-center w-4 h-4'>
<LinkExternal01 className='w-3.5 h-3.5 text-gray-500' />
</div>
</div>
)
}
export default ModelTrigger

View file

@ -0,0 +1,29 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { PlusCircle } from '@/app/components/base/icons/src/vender/solid/general'
type AddModelButtonProps = {
className?: string
onClick: () => void
}
const AddModelButton: FC<AddModelButtonProps> = ({
className,
onClick,
}) => {
const { t } = useTranslation()
return (
<span
className={`
shrink-0 flex items-center px-1.5 h-6 text-xs font-medium text-gray-500 cursor-pointer
hover:bg-primary-50 hover:text-primary-600 rounded-md ${className}
`}
onClick={onClick}
>
<PlusCircle className='mr-1 w-3 h-3' />
{t('common.modelProvider.addModel')}
</span>
)
}
export default AddModelButton

View file

@ -0,0 +1,81 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useSWRConfig } from 'swr'
import type { ModelProvider } from '../declarations'
import {
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
} from '../declarations'
import { useUpdateModelList } from '../hooks'
import PrioritySelector from './priority-selector'
import PriorityUseTip from './priority-use-tip'
import Indicator from '@/app/components/header/indicator'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button'
import { changeModelProviderPriority } from '@/service/common'
import { useToastContext } from '@/app/components/base/toast'
type CredentialPanelProps = {
provider: ModelProvider
onSetup: () => void
}
const CredentialPanel: FC<CredentialPanelProps> = ({
provider,
onSetup,
}) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { mutate } = useSWRConfig()
const updateModelList = useUpdateModelList()
const customConfig = provider.custom_configuration
const systemConfig = provider.system_configuration
const priorityUseType = provider.preferred_provider_type
const customConfiged = customConfig.status === CustomConfigurationStatusEnum.active
const handleChangePriority = async (key: PreferredProviderTypeEnum) => {
const res = await changeModelProviderPriority({
url: `/workspaces/current/model-providers/${provider.provider}/preferred-provider-type`,
body: {
preferred_provider_type: key,
},
})
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate('/workspaces/current/model-providers')
updateModelList(1)
}
}
return (
<div className='shrink-0 relative ml-1 p-1 w-[112px] rounded-lg bg-white/[0.3] border-[0.5px] border-black/5'>
<div className='flex items-center justify-between mb-1 pt-1 pl-2 pr-[7px] h-5 text-xs font-medium text-gray-500'>
API-KEY
<Indicator color={customConfiged ? 'green' : 'gray'} />
</div>
<div className='flex items-center gap-0.5'>
<Button
className='grow px-0 h-6 bg-white text-xs font-medium rounded-md'
onClick={onSetup}
>
<Settings01 className='mr-1 w-3 h-3' />
{t('common.operation.setup')}
</Button>
{
systemConfig.enabled && customConfiged && (
<PrioritySelector
value={priorityUseType}
onSelect={handleChangePriority}
/>
)
}
</div>
{
priorityUseType === PreferredProviderTypeEnum.custom && systemConfig.enabled && (
<PriorityUseTip />
)
}
</div>
)
}
export default CredentialPanel

View file

@ -0,0 +1,161 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigrationModelFixedFields,
ModelItem,
ModelProvider,
} from '../declarations'
import { ConfigurateMethodEnum } from '../declarations'
import {
DEFAULT_BACKGROUND_COLOR,
modelTypeFormat,
} from '../utils'
import ProviderIcon from '../provider-icon'
import ModelBadge from '../model-badge'
import CredentialPanel from './credential-panel'
import QuotaPanel from './quota-panel'
import ModelList from './model-list'
import AddModelButton from './add-model-button'
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
import { fetchModelProviderModelList } from '@/service/common'
import { useEventEmitterContextContext } from '@/context/event-emitter'
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
type ProviderAddedCardProps = {
provider: ModelProvider
onOpenModal: (configurateMethod: ConfigurateMethodEnum, currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void
}
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
provider,
onOpenModal,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
const [fetched, setFetched] = useState(false)
const [loading, setLoading] = useState(false)
const [collapsed, setCollapsed] = useState(true)
const [modelList, setModelList] = useState<ModelItem[]>([])
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
const systemConfig = provider.system_configuration
const hasModelList = fetched && !!modelList.length
const showQuota = systemConfig.enabled || ['minimax', 'spark', 'zhipuai', 'anthropic'].includes(provider.provider)
const getModelList = async (providerName: string) => {
if (loading)
return
try {
setLoading(true)
const modelsData = await fetchModelProviderModelList(`/workspaces/current/model-providers/${providerName}/models`)
setModelList(modelsData.data)
setCollapsed(false)
setFetched(true)
}
finally {
setLoading(false)
}
}
const handleOpenModelList = () => {
if (fetched) {
setCollapsed(false)
return
}
getModelList(provider.provider)
}
eventEmitter?.useSubscription((v: any) => {
if (v?.type === UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST && v.payload === provider.provider)
getModelList(v.payload)
})
return (
<div
className='mb-2 rounded-xl border-[0.5px] border-black/5 shadow-xs'
style={{ background: provider.background || DEFAULT_BACKGROUND_COLOR }}
>
<div className='flex pl-3 py-2 pr-2 rounded-t-xl'>
<div className='grow px-1 pt-1 pb-0.5'>
<ProviderIcon
className='mb-2'
provider={provider}
/>
<div className='flex gap-0.5'>
{
provider.supported_model_types.map(modelType => (
<ModelBadge key={modelType}>
{modelTypeFormat(modelType)}
</ModelBadge>
))
}
</div>
</div>
{
showQuota && (
<QuotaPanel
provider={provider}
/>
)
}
{
configurateMethods.includes(ConfigurateMethodEnum.predefinedModel) && (
<CredentialPanel
onSetup={() => onOpenModal(ConfigurateMethodEnum.predefinedModel)}
provider={provider}
/>
)
}
</div>
{
collapsed && (
<div className='group flex items-center justify-between pl-2 py-1.5 pr-[11px] border-t border-t-black/5 bg-white/30 text-xs font-medium text-gray-500'>
<div className='group-hover:hidden pl-1 pr-1.5 h-6 leading-6'>
{
hasModelList
? t('common.modelProvider.modelsNum', { num: modelList.length })
: t('common.modelProvider.showModels')
}
</div>
<div
className='hidden group-hover:flex items-center pl-1 pr-1.5 h-6 rounded-lg hover:bg-white cursor-pointer'
onClick={handleOpenModelList}
>
<ChevronDownDouble className='mr-0.5 w-3 h-3' />
{
hasModelList
? t('common.modelProvider.showModelsNum', { num: modelList.length })
: t('common.modelProvider.showModels')
}
{
loading && (
<Loading02 className='ml-0.5 animate-spin w-3 h-3' />
)
}
</div>
{
configurateMethods.includes(ConfigurateMethodEnum.customizableModel) && (
<AddModelButton
onClick={() => onOpenModal(ConfigurateMethodEnum.customizableModel)}
className='hidden group-hover:flex group-hover:text-primary-600'
/>
)
}
</div>
)
}
{
!collapsed && (
<ModelList
provider={provider}
models={modelList}
onCollapse={() => setCollapsed(true)}
onConfig={currentCustomConfigrationModelFixedFields => onOpenModal(ConfigurateMethodEnum.customizableModel, currentCustomConfigrationModelFixedFields)}
/>
)
}
</div>
)
}
export default ProviderAddedCard

View file

@ -0,0 +1,121 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigrationModelFixedFields,
ModelItem,
ModelProvider,
} from '../declarations'
import {
ConfigurateMethodEnum,
ModelStatusEnum,
} from '../declarations'
import { useLanguage } from '../hooks'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
// import Tab from './tab'
import AddModelButton from './add-model-button'
import Indicator from '@/app/components/header/indicator'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
import Button from '@/app/components/base/button'
type ModelListProps = {
provider: ModelProvider
models: ModelItem[]
onCollapse: () => void
onConfig: (currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void
}
const ModelList: FC<ModelListProps> = ({
provider,
models,
onCollapse,
onConfig,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
const canCustomConfig = configurateMethods.includes(ConfigurateMethodEnum.customizableModel)
// const canSystemConfig = configurateMethods.includes(ConfigurateMethodEnum.predefinedModel)
return (
<div className='px-2 pb-2 rounded-b-xl'>
<div className='py-1 bg-white rounded-lg'>
<div className='flex items-center pl-1 pr-[3px]'>
<span className='group shrink-0 flex items-center mr-2'>
<span className='group-hover:hidden pl-1 pr-1.5 h-6 leading-6 text-xs font-medium text-gray-500'>
{t('common.modelProvider.modelsNum', { num: models.length })}
</span>
<span
className={`
hidden group-hover:inline-flex items-center pl-1 pr-1.5 h-6 bg-gray-50
text-xs font-medium text-gray-500 cursor-pointer rounded-lg
`}
onClick={() => onCollapse()}
>
<ChevronDownDouble className='mr-0.5 w-3 h-3 rotate-180' />
{t('common.modelProvider.collapse')}
</span>
</span>
{/* {
canCustomConfig && canSystemConfig && (
<span className='flex items-center'>
<Tab active='all' onSelect={() => {}} />
</span>
)
} */}
{
canCustomConfig && (
<div className='grow flex justify-end'>
<AddModelButton onClick={() => onConfig()} />
</div>
)
}
</div>
{
models.map(model => (
<div
key={model.model}
className={`
group flex items-center pl-2 pr-2.5 h-8 rounded-lg
${canCustomConfig && 'hover:bg-gray-50'}
`}
>
<div className='shrink-0 mr-2' style={{ background: provider.icon_small[language] }} />
<ModelIcon
className='shrink-0 mr-2'
provider={provider}
modelName={model.model}
/>
<ModelName
className='grow text-sm font-normal text-gray-900'
modelItem={model}
showModelType
showMode
showContextSize
/>
<div className='shrink-0 flex items-center'>
{
model.fetch_from === ConfigurateMethodEnum.customizableModel && (
<Button
className='hidden group-hover:flex py-0 h-7 text-xs font-medium text-gray-700'
onClick={() => onConfig({ __model_name: model.model, __model_type: model.model_type })}
>
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
{t('common.modelProvider.config')}
</Button>
)
}
<Indicator
className='ml-2.5'
color={model.status === ModelStatusEnum.active ? 'green' : 'gray'}
/>
</div>
</div>
))
}
</div>
</div>
)
}
export default ModelList

View file

@ -0,0 +1,72 @@
import { Fragment } from 'react'
import type { FC } from 'react'
import { Popover, Transition } from '@headlessui/react'
import { useTranslation } from 'react-i18next'
import { PreferredProviderTypeEnum } from '../declarations'
import { Check, DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button'
type SelectorProps = {
value?: string
onSelect: (key: PreferredProviderTypeEnum) => void
}
const Selector: FC<SelectorProps> = ({
value,
onSelect,
}) => {
const { t } = useTranslation()
const options = [
{
key: PreferredProviderTypeEnum.custom,
text: 'API',
},
{
key: PreferredProviderTypeEnum.system,
text: t('common.modelProvider.quota'),
},
]
return (
<Popover className='relative'>
<Popover.Button>
{
({ open }) => (
<Button className={`
px-0 w-6 h-6 bg-white rounded-md
${open && '!bg-gray-100'}
`}>
<DotsHorizontal className='w-3 h-3 text-gray-700' />
</Button>
)
}
</Popover.Button>
<Transition
as={Fragment}
leave='transition ease-in duration-100'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<Popover.Panel className='absolute top-7 right-0 w-[144px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg z-10'>
<div className='p-1'>
<div className='px-3 pt-2 pb-1 text-sm font-medium text-gray-700'>{t('common.modelProvider.card.priorityUse')}</div>
{
options.map(option => (
<Popover.Button as={Fragment} key={option.key}>
<div
className='flex items-center justify-between px-3 h-9 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
onClick={() => onSelect(option.key)}
>
<div className='grow'>{option.text}</div>
{value === option.key && <Check className='w-4 h-4 text-primary-600' />}
</div>
</Popover.Button>
))
}
</div>
</Popover.Panel>
</Transition>
</Popover>
)
}
export default Selector

View file

@ -0,0 +1,20 @@
import { useTranslation } from 'react-i18next'
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
import Tooltip from '@/app/components/base/tooltip'
const PriorityUseTip = () => {
const { t } = useTranslation()
return (
<Tooltip
selector='provider-quota-credential-priority-using'
content={t('common.modelProvider.priorityUsing') || ''}
>
<div className='absolute -right-[5px] -top-[5px] bg-indigo-50 rounded-[5px] border-[0.5px] border-indigo-100 cursor-pointer'>
<ChevronDownDouble className='rotate-180 w-3 h-3 text-indigo-600' />
</div>
</Tooltip>
)
}
export default PriorityUseTip

View file

@ -0,0 +1,98 @@
import type { FC } from 'react'
import { useSWRConfig } from 'swr'
import { useTranslation } from 'react-i18next'
import type { ModelProvider } from '../declarations'
import {
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
QuotaUnitEnum,
} from '../declarations'
import {
useAnthropicBuyQuota,
useFreeQuota,
} from '../hooks'
import PriorityUseTip from './priority-use-tip'
import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button'
type QuotaPanelProps = {
provider: ModelProvider
}
const QuotaPanel: FC<QuotaPanelProps> = ({
provider,
}) => {
const { t } = useTranslation()
const { mutate } = useSWRConfig()
const handlePay = useAnthropicBuyQuota()
const handleFreeQuotaSuccess = () => {
mutate('/workspaces/current/model-providers')
}
const handleFreeQuota = useFreeQuota(handleFreeQuotaSuccess)
const customConfig = provider.custom_configuration
const priorityUseType = provider.preferred_provider_type
const systemConfig = provider.system_configuration
const currentQuota = systemConfig.enabled && systemConfig.quota_configurations.find(item => item.quota_type === systemConfig.current_quota_type)
return (
<div className='group relative shrink-0 min-w-[112px] px-3 py-2 rounded-lg bg-white/[0.3] border-[0.5px] border-black/5'>
<div className='flex items-center mb-2 h-4 text-xs font-medium text-gray-500'>
{t('common.modelProvider.quota')}
<InfoCircle className='ml-0.5 w-3 h-3 text-gray-400' />
</div>
{
currentQuota && (
<div className='flex items-center h-4 text-xs text-gray-500'>
<span className='mr-0.5 text-sm font-semibold text-gray-700'>{(currentQuota?.quota_limit || 0) - (currentQuota?.quota_used || 0)}</span>
{
currentQuota?.quota_unit === QuotaUnitEnum.tokens && 'Tokens'
}
{
currentQuota?.quota_unit === QuotaUnitEnum.times && t('common.modelProvider.callTimes')
}
</div>
)
}
{
!currentQuota && provider.provider === 'anthropic' && (
<Button
className='h-6 bg-white text-xs font-medium rounded-md'
onClick={handlePay}
>
{t('common.modelProvider.buyQuota')}
</Button>
)
}
{
!currentQuota && ['minimax', 'spark', 'zhipuai'].includes(provider.provider) && (
<Button
className='h-6 bg-white text-xs font-medium rounded-md'
onClick={() => handleFreeQuota(provider.provider)}
>
{t('common.modelProvider.getFreeTokens')}
</Button>
)
}
{
provider.provider === 'anthropic' && systemConfig.enabled && (
<div
className={`
absolute left-0 bottom-0 hidden group-hover:flex items-center justify-center
w-full h-[30px] backdrop-blur-[2px] bg-gradient-to-r from-[rgba(238,244,255,0.80)] to-[rgba(237,237,240,0.70)]
text-xs font-medium text-primary-600 cursor-pointer rounded-b-lg
`}
onClick={handlePay}
>
{t('common.modelProvider.buyQuota')}
</div>
)
}
{
priorityUseType === PreferredProviderTypeEnum.system && customConfig.status === CustomConfigurationStatusEnum.active && (
<PriorityUseTip />
)
}
</div>
)
}
export default QuotaPanel

View file

@ -0,0 +1,45 @@
import type { FC } from 'react'
type TabProps = {
active: string
onSelect: (active: string) => void
}
const Tab: FC<TabProps> = ({
active,
onSelect,
}) => {
const tabs = [
{
key: 'all',
text: 'All',
},
{
key: 'added',
text: 'Added',
},
{
key: 'build-in',
text: 'Build-in',
},
]
return (
<div className='flex items-center'>
{
tabs.map(tab => (
<div
key={tab.key}
className={`
flex items-center mr-1 px-[5px] h-[18px] rounded-md text-xs cursor-pointer
${active === tab.key ? 'bg-gray-200 font-medium text-gray-900' : 'text-gray-500 font-normal'}
`}
onClick={() => onSelect(tab.key)}
>
{tab.text}
</div>
))
}
</div>
)
}
export default Tab

View file

@ -0,0 +1,154 @@
import type { FC } from 'react'
import { useSWRConfig } from 'swr'
import { useTranslation } from 'react-i18next'
import type {
ModelProvider,
TypeWithI18N,
} from '../declarations'
import { ConfigurateMethodEnum } from '../declarations'
import {
DEFAULT_BACKGROUND_COLOR,
modelTypeFormat,
} from '../utils'
import {
useAnthropicBuyQuota,
useFreeQuota,
useLanguage,
} from '../hooks'
import ModelBadge from '../model-badge'
import ProviderIcon from '../provider-icon'
import s from './index.module.css'
import { Plus, Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import { CoinsStacked01 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import Button from '@/app/components/base/button'
type ProviderCardProps = {
provider: ModelProvider
onOpenModal: (configurateMethod: ConfigurateMethodEnum) => void
}
const TIP_MAP: { [k: string]: TypeWithI18N } = {
minimax: {
en_US: 'Earn 1 million tokens for free',
zh_Hans: '免费获取 100 万个 token',
},
spark: {
en_US: 'Earn 3 million tokens (v3.0) for free',
zh_Hans: '免费获取 300 万个 token (v3.0)',
},
zhipuai: {
en_US: 'Earn 10 million tokens for free',
zh_Hans: '免费获取 1000 万个 token',
},
}
const ProviderCard: FC<ProviderCardProps> = ({
provider,
onOpenModal,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const { mutate } = useSWRConfig()
const handlePay = useAnthropicBuyQuota()
const handleFreeQuotaSuccess = () => {
mutate('/workspaces/current/model-providers')
}
const handleFreeQuota = useFreeQuota(handleFreeQuotaSuccess)
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
const canGetFreeQuota = ['mininmax', 'spark', 'zhipuai'].includes(provider.provider)
return (
<div
className='group relative flex flex-col justify-between px-4 py-3 h-[148px] border-[0.5px] border-black/5 rounded-xl shadow-xs hover:shadow-lg'
style={{ background: provider.background || DEFAULT_BACKGROUND_COLOR }}
>
<div>
<div className='py-0.5'>
<ProviderIcon provider={provider} />
</div>
{
provider.description && (
<div className='mt-1 leading-4 text-xs text-black/[48]'>{provider.description[language]}</div>
)
}
</div>
<div>
<div className={`flex flex-wrap group-hover:hidden gap-0.5 ${canGetFreeQuota && 'pb-[18px]'}`}>
{
provider.supported_model_types.map(modelType => (
<ModelBadge key={modelType}>
{modelTypeFormat(modelType)}
</ModelBadge>
))
}
{
canGetFreeQuota && (
<div className='absolute left-0 right-0 bottom-0 flex items-center h-[26px] px-4 bg-white/50 rounded-b-xl'>
📣&nbsp;
<div
className={`${s.vender} text-xs font-medium text-transparent truncate`}
title={TIP_MAP[provider.provider][language]}
>
{TIP_MAP[provider.provider][language]}
</div>
</div>
)
}
</div>
{
canGetFreeQuota && (
<div className='hidden group-hover:block'>
<Button
className='mb-1 w-full h-7 text-xs'
type='primary'
onClick={() => handleFreeQuota(provider.provider)}
>
{t('common.modelProvider.getFreeTokens')}
</Button>
</div>
)
}
<div className={`hidden group-hover:grid grid-cols-${configurateMethods.length} gap-1`}>
{
configurateMethods.map((method) => {
if (method === ConfigurateMethodEnum.predefinedModel) {
return (
<Button
key={method}
className='h-7 bg-white text-xs text-gray-700'
onClick={() => onOpenModal(method)}
>
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
{t('common.operation.setup')}
</Button>
)
}
return (
<Button
key={method}
className='px-0 h-7 bg-white text-xs text-gray-700'
onClick={() => onOpenModal(method)}
>
<Plus className='mr-[5px] w-3.5 h-3.5' />
{t('common.modelProvider.addModel')}
</Button>
)
})
}
{
provider.provider === 'anthropic' && (
<Button
className='h-7 text-xs text-gray-700'
onClick={handlePay}
>
<CoinsStacked01 className='mr-[5px] w-3.5 h-3.5' />
{t('common.modelProvider.buyQuota')}
</Button>
)
}
</div>
</div>
</div>
)
}
export default ProviderCard

View file

@ -0,0 +1,34 @@
import type { FC } from 'react'
import type { ModelProvider } from '../declarations'
import { useLanguage } from '../hooks'
type ProviderIconProps = {
provider: ModelProvider
className?: string
}
const ProviderIcon: FC<ProviderIconProps> = ({
provider,
className,
}) => {
const language = useLanguage()
if (provider.icon_large) {
return (
<img
alt='provider-icon'
src={`${provider.icon_large[language]}?_token=${localStorage.getItem('console_token')}`}
className={`w-auto h-6 ${className}`}
/>
)
}
return (
<div className={`inline-flex items-center ${className}`}>
<div className='text-xs font-semibold text-black'>
{provider.label[language]}
</div>
</div>
)
}
export default ProviderIcon

View file

@ -2,9 +2,16 @@ import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ModelSelector from '../model-selector'
import {
useModelList,
useSystemDefaultModelAndModelList,
useUpdateModelList,
} from '../hooks'
import type {
BackendModel, ProviderEnum,
DefaultModel,
DefaultModelResponse,
} from '../declarations'
import { ModelTypeEnum } from '../declarations'
import Tooltip from '@/app/components/base/tooltip'
import { HelpCircle, Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import {
@ -12,76 +19,89 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import { useProviderContext } from '@/context/provider-context'
import { updateDefaultModel } from '@/service/common'
import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
import { useToastContext } from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
type SystemModelProps = {
onUpdate: () => void
type SystemModelSelectorProps = {
textGenerationDefaultModel: DefaultModelResponse | undefined
embeddingsDefaultModel: DefaultModelResponse | undefined
rerankDefaultModel: DefaultModelResponse | undefined
speech2textDefaultModel: DefaultModelResponse | undefined
}
const SystemModel: FC<SystemModelProps> = ({
onUpdate,
const SystemModel: FC<SystemModelSelectorProps> = ({
textGenerationDefaultModel,
embeddingsDefaultModel,
rerankDefaultModel,
speech2textDefaultModel,
}) => {
const { t } = useTranslation()
const {
textGenerationDefaultModel,
mutateTextGenerationDefaultModel,
embeddingsDefaultModel,
mutateEmbeddingsDefaultModel,
speech2textDefaultModel,
mutateSpeech2textDefaultModel,
rerankDefaultModel,
mutateRerankDefaultModel,
} = useProviderContext()
const { notify } = useToastContext()
const { textGenerationModelList } = useProviderContext()
const updateModelList = useUpdateModelList()
const { data: embeddingModelList } = useModelList(2)
const { data: rerankModelList } = useModelList(3)
const { data: speech2textModelList } = useModelList(4)
const [changedModelTypes, setChangedModelTypes] = useState<ModelTypeEnum[]>([])
const [currentTextGenerationDefaultModel, changeCurrentTextGenerationDefaultModel] = useSystemDefaultModelAndModelList(textGenerationDefaultModel, textGenerationModelList)
const [currentEmbeddingsDefaultModel, changeCurrentEmbeddingsDefaultModel] = useSystemDefaultModelAndModelList(embeddingsDefaultModel, embeddingModelList)
const [currentRerankDefaultModel, changeCurrentRerankDefaultModel] = useSystemDefaultModelAndModelList(rerankDefaultModel, rerankModelList)
const [currentSpeech2textDefaultModel, changeCurrentSpeech2textDefaultModel] = useSystemDefaultModelAndModelList(speech2textDefaultModel, speech2textModelList)
const [open, setOpen] = useState(false)
const [selectedModel, setSelectedModel] = useState<Record<ModelType, { providerName: ProviderEnum; modelName: string } | undefined>>({
[ModelType.textGeneration]: textGenerationDefaultModel && { providerName: textGenerationDefaultModel.model_provider.provider_name, modelName: textGenerationDefaultModel.model_name },
[ModelType.embeddings]: embeddingsDefaultModel && { providerName: embeddingsDefaultModel.model_provider.provider_name, modelName: embeddingsDefaultModel.model_name },
[ModelType.speech2text]: speech2textDefaultModel && { providerName: speech2textDefaultModel.model_provider.provider_name, modelName: speech2textDefaultModel.model_name },
[ModelType.reranking]: rerankDefaultModel && { providerName: rerankDefaultModel.model_provider.provider_name, modelName: rerankDefaultModel.model_name },
})
const mutateDefaultModel = (types: ModelType[]) => {
types.forEach((type) => {
if (type === ModelType.textGeneration)
mutateTextGenerationDefaultModel()
if (type === ModelType.embeddings)
mutateEmbeddingsDefaultModel()
if (type === ModelType.speech2text)
mutateSpeech2textDefaultModel()
if (type === ModelType.reranking)
mutateRerankDefaultModel()
})
const getCurrentDefaultModelByModelType = (modelType: ModelTypeEnum) => {
if (modelType === ModelTypeEnum.textGeneration)
return currentTextGenerationDefaultModel
else if (modelType === ModelTypeEnum.textEmbedding)
return currentEmbeddingsDefaultModel
else if (modelType === ModelTypeEnum.rerank)
return currentRerankDefaultModel
else if (modelType === ModelTypeEnum.speech2text)
return currentSpeech2textDefaultModel
return undefined
}
const handleChangeDefaultModel = async (type: ModelType, v: BackendModel) => {
setSelectedModel({
...selectedModel,
[type]: {
providerName: v.model_provider.provider_name,
modelName: v.model_name,
},
})
const handleChangeDefaultModel = (modelType: ModelTypeEnum, model: DefaultModel) => {
if (modelType === ModelTypeEnum.textGeneration)
changeCurrentTextGenerationDefaultModel(model)
else if (modelType === ModelTypeEnum.textEmbedding)
changeCurrentEmbeddingsDefaultModel(model)
else if (modelType === ModelTypeEnum.rerank)
changeCurrentRerankDefaultModel(model)
else if (modelType === ModelTypeEnum.speech2text)
changeCurrentSpeech2textDefaultModel(model)
if (!changedModelTypes.includes(modelType))
setChangedModelTypes([...changedModelTypes, modelType])
}
const handleSave = async () => {
const kesArray = Object.keys(selectedModel) as ModelType[]
const res = await updateDefaultModel({
url: '/workspaces/current/default-model',
body: {
model_settings: kesArray.map((key) => {
model_settings: [ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank, ModelTypeEnum.speech2text].map((modelType) => {
return {
model_type: key,
provider_name: selectedModel?.[key]?.providerName,
model_name: selectedModel?.[key]?.modelName,
model_type: modelType,
provider: getCurrentDefaultModelByModelType(modelType)?.provider,
model: getCurrentDefaultModelByModelType(modelType)?.model,
}
}),
},
})
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutateDefaultModel(kesArray)
setOpen(false)
changedModelTypes.forEach((modelType) => {
if (modelType === ModelTypeEnum.textGeneration)
updateModelList(modelType)
else if (modelType === ModelTypeEnum.textEmbedding)
updateModelList(modelType)
else if (modelType === ModelTypeEnum.rerank)
updateModelList(modelType)
else if (modelType === ModelTypeEnum.speech2text)
updateModelList(modelType)
})
}
}
@ -121,9 +141,9 @@ const SystemModel: FC<SystemModelProps> = ({
</div>
<div>
<ModelSelector
value={selectedModel[ModelType.textGeneration]}
modelType={ModelType.textGeneration}
onChange={v => handleChangeDefaultModel(ModelType.textGeneration, v)}
defaultModel={currentTextGenerationDefaultModel}
modelList={textGenerationModelList}
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.textGeneration, model)}
/>
</div>
</div>
@ -141,9 +161,9 @@ const SystemModel: FC<SystemModelProps> = ({
</div>
<div>
<ModelSelector
value={selectedModel[ModelType.embeddings]}
modelType={ModelType.embeddings}
onChange={v => handleChangeDefaultModel(ModelType.embeddings, v)}
defaultModel={currentEmbeddingsDefaultModel}
modelList={embeddingModelList}
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.textEmbedding, model)}
/>
</div>
</div>
@ -161,11 +181,9 @@ const SystemModel: FC<SystemModelProps> = ({
</div>
<div>
<ModelSelector
value={selectedModel[ModelType.reranking]}
modelType={ModelType.reranking}
onChange={v => handleChangeDefaultModel(ModelType.reranking, v)}
whenEmptyGoToSetting
onUpdate={onUpdate}
defaultModel={currentRerankDefaultModel}
modelList={rerankModelList}
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.rerank, model)}
/>
</div>
</div>
@ -183,9 +201,9 @@ const SystemModel: FC<SystemModelProps> = ({
</div>
<div>
<ModelSelector
value={selectedModel[ModelType.speech2text]}
modelType={ModelType.speech2text}
onChange={v => handleChangeDefaultModel(ModelType.speech2text, v)}
defaultModel={currentSpeech2textDefaultModel}
modelList={speech2textModelList}
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.speech2text, model)}
/>
</div>
</div>

View file

@ -0,0 +1,159 @@
import { ValidatedStatus } from '../key-validator/declarations'
import type {
CredentialFormSchemaRadio,
CredentialFormSchemaTextInput,
FormValue,
} from './declarations'
import {
FormTypeEnum,
MODEL_TYPE_TEXT,
ModelTypeEnum,
} from './declarations'
import {
deleteModelProvider,
setModelProvider,
validateModelProvider,
} from '@/service/common'
export const languageMaps = {
'en': 'en_US',
'zh-Hans': 'zh_Hans',
} as {
'en': 'en_US'
'zh-Hans': 'zh_Hans'
}
export const DEFAULT_BACKGROUND_COLOR = '#F3F4F6'
export const isNullOrUndefined = (value: any) => {
return value === undefined || value === null
}
export const validateCredentials = async (predefined: boolean, provider: string, v: FormValue) => {
let body, url
if (predefined) {
body = {
credentials: v,
}
url = `/workspaces/current/model-providers/${provider}/credentials/validate`
}
else {
const { __model_name, __model_type, ...credentials } = v
body = {
model: __model_name,
model_type: __model_type,
credentials,
}
url = `/workspaces/current/model-providers/${provider}/models/credentials/validate`
}
try {
const res = await validateModelProvider({ url, body })
if (res.result === 'success')
return Promise.resolve({ status: ValidatedStatus.Success })
else
return Promise.resolve({ status: ValidatedStatus.Error, message: res.error || 'error' })
}
catch (e: any) {
return Promise.resolve({ status: ValidatedStatus.Error, message: e.message })
}
}
export const saveCredentials = async (predefined: boolean, provider: string, v: FormValue) => {
let body, url
if (predefined) {
body = {
credentials: v,
}
url = `/workspaces/current/model-providers/${provider}`
}
else {
const { __model_name, __model_type, ...credentials } = v
body = {
model: __model_name,
model_type: __model_type,
credentials,
}
url = `/workspaces/current/model-providers/${provider}/models`
}
return setModelProvider({ url, body })
}
export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue) => {
let url = ''
let body
if (predefined) {
url = `/workspaces/current/model-providers/${provider}`
}
else {
if (v) {
const { __model_name, __model_type } = v
body = {
model: __model_name,
model_type: __model_type,
}
url = `/workspaces/current/model-providers/${provider}/models`
}
}
return deleteModelProvider({ url, body })
}
export const sizeFormat = (size: number) => {
const remainder = Math.floor(size / 1000)
if (remainder < 1)
return `${size}`
else
return `${remainder}K`
}
export const modelTypeFormat = (modelType: ModelTypeEnum) => {
if (modelType === ModelTypeEnum.textEmbedding)
return 'TEXT EMBEDDING'
return modelType.toLocaleUpperCase()
}
export const genModelTypeFormSchema = (modelTypes: ModelTypeEnum[]) => {
return {
type: FormTypeEnum.radio,
label: {
zh_Hans: '模型类型',
en_US: 'Model Type',
},
variable: '__model_type',
default: modelTypes[0],
required: true,
show_on: [],
options: modelTypes.map((modelType: ModelTypeEnum) => {
return {
value: modelType,
label: {
zh_Hans: MODEL_TYPE_TEXT[modelType],
en_US: MODEL_TYPE_TEXT[modelType],
},
show_on: [],
}
}),
} as CredentialFormSchemaRadio
}
export const genModelNameFormSchema = (model?: Pick<CredentialFormSchemaTextInput, 'label' | 'placeholder'>) => {
return {
type: FormTypeEnum.textInput,
label: model?.label || {
zh_Hans: '模型名称',
en_US: 'Model Name',
},
variable: '__model_name',
required: true,
show_on: [],
placeholder: model?.placeholder || {
zh_Hans: '请输入模型名称',
en_US: 'Please enter model name',
},
} as CredentialFormSchemaTextInput
}