dify
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
|
||||
|
||||
export type FormValue = Record<string, any>
|
||||
|
||||
export type TypeWithI18N<T = string> = {
|
||||
en_US: T
|
||||
zh_Hans: T
|
||||
[key: string]: T
|
||||
}
|
||||
|
||||
export enum FormTypeEnum {
|
||||
textInput = 'text-input',
|
||||
textNumber = 'number-input',
|
||||
secretInput = 'secret-input',
|
||||
select = 'select',
|
||||
radio = 'radio',
|
||||
checkbox = 'checkbox',
|
||||
boolean = 'boolean',
|
||||
files = 'files',
|
||||
file = 'file',
|
||||
modelSelector = 'model-selector',
|
||||
toolSelector = 'tool-selector',
|
||||
multiToolSelector = 'array[tools]',
|
||||
appSelector = 'app-selector',
|
||||
any = 'any',
|
||||
object = 'object',
|
||||
array = 'array',
|
||||
dynamicSelect = 'dynamic-select',
|
||||
}
|
||||
|
||||
export type FormOption = {
|
||||
label: TypeWithI18N
|
||||
value: string
|
||||
show_on: FormShowOnObject[]
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export enum ModelTypeEnum {
|
||||
textGeneration = 'llm',
|
||||
textEmbedding = 'text-embedding',
|
||||
rerank = 'rerank',
|
||||
speech2text = 'speech2text',
|
||||
moderation = 'moderation',
|
||||
tts = 'tts',
|
||||
}
|
||||
|
||||
export const MODEL_TYPE_TEXT = {
|
||||
[ModelTypeEnum.textGeneration]: 'LLM',
|
||||
[ModelTypeEnum.textEmbedding]: 'Text Embedding',
|
||||
[ModelTypeEnum.rerank]: 'Rerank',
|
||||
[ModelTypeEnum.speech2text]: 'Speech2text',
|
||||
[ModelTypeEnum.moderation]: 'Moderation',
|
||||
[ModelTypeEnum.tts]: 'TTS',
|
||||
}
|
||||
|
||||
export enum ConfigurationMethodEnum {
|
||||
predefinedModel = 'predefined-model',
|
||||
customizableModel = 'customizable-model',
|
||||
fetchFromRemote = 'fetch-from-remote',
|
||||
}
|
||||
|
||||
export enum ModelFeatureEnum {
|
||||
toolCall = 'tool-call',
|
||||
multiToolCall = 'multi-tool-call',
|
||||
agentThought = 'agent-thought',
|
||||
streamToolCall = 'stream-tool-call',
|
||||
vision = 'vision',
|
||||
video = 'video',
|
||||
document = 'document',
|
||||
audio = 'audio',
|
||||
StructuredOutput = 'structured-output',
|
||||
}
|
||||
|
||||
export enum ModelFeatureTextEnum {
|
||||
toolCall = 'Tool Call',
|
||||
multiToolCall = 'Multi Tool Call',
|
||||
agentThought = 'Agent Thought',
|
||||
vision = 'Vision',
|
||||
video = 'Video',
|
||||
document = 'Document',
|
||||
audio = 'Audio',
|
||||
}
|
||||
|
||||
export enum ModelStatusEnum {
|
||||
active = 'active',
|
||||
noConfigure = 'no-configure',
|
||||
quotaExceeded = 'quota-exceeded',
|
||||
noPermission = 'no-permission',
|
||||
disabled = 'disabled',
|
||||
credentialRemoved = 'credential-removed',
|
||||
}
|
||||
|
||||
export const MODEL_STATUS_TEXT: { [k: string]: TypeWithI18N } = {
|
||||
'no-configure': {
|
||||
en_US: 'No Configure',
|
||||
zh_Hans: '未配置凭据',
|
||||
},
|
||||
'quota-exceeded': {
|
||||
en_US: 'Quota Exceeded',
|
||||
zh_Hans: '额度不足',
|
||||
},
|
||||
'no-permission': {
|
||||
en_US: 'No Permission',
|
||||
zh_Hans: '无使用权限',
|
||||
},
|
||||
}
|
||||
|
||||
export enum CustomConfigurationStatusEnum {
|
||||
active = 'active',
|
||||
noConfigure = 'no-configure',
|
||||
}
|
||||
|
||||
export type FormShowOnObject = {
|
||||
variable: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type CredentialFormSchemaBase = {
|
||||
name: string
|
||||
variable: string
|
||||
label: TypeWithI18N
|
||||
type: FormTypeEnum
|
||||
required: boolean
|
||||
default?: string
|
||||
tooltip?: TypeWithI18N
|
||||
show_on: FormShowOnObject[]
|
||||
url?: string
|
||||
scope?: string
|
||||
input_schema?: SchemaRoot
|
||||
}
|
||||
|
||||
export type CredentialFormSchemaTextInput = CredentialFormSchemaBase & {
|
||||
max_length?: number;
|
||||
placeholder?: TypeWithI18N,
|
||||
template?: {
|
||||
enabled: boolean
|
||||
},
|
||||
auto_generate?: {
|
||||
type: string
|
||||
}
|
||||
}
|
||||
export type CredentialFormSchemaNumberInput = CredentialFormSchemaBase & { min?: number; max?: 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: ConfigurationMethodEnum
|
||||
status: ModelStatusEnum
|
||||
model_properties: Record<string, string | number>
|
||||
load_balancing_enabled: boolean
|
||||
deprecated?: boolean
|
||||
has_invalid_load_balancing_configs?: boolean
|
||||
}
|
||||
|
||||
export enum PreferredProviderTypeEnum {
|
||||
system = 'system',
|
||||
custom = 'custom',
|
||||
}
|
||||
|
||||
export enum CurrentSystemQuotaTypeEnum {
|
||||
trial = 'trial',
|
||||
free = 'free',
|
||||
paid = 'paid',
|
||||
}
|
||||
|
||||
export enum QuotaUnitEnum {
|
||||
times = 'times',
|
||||
tokens = 'tokens',
|
||||
credits = 'credits',
|
||||
}
|
||||
|
||||
export type QuotaConfiguration = {
|
||||
quota_type: CurrentSystemQuotaTypeEnum
|
||||
quota_unit: QuotaUnitEnum
|
||||
quota_limit: number
|
||||
quota_used: number
|
||||
last_used: number
|
||||
is_valid: boolean
|
||||
}
|
||||
|
||||
export type Credential = {
|
||||
credential_id: string
|
||||
credential_name?: string
|
||||
from_enterprise?: boolean
|
||||
not_allowed_to_use?: boolean
|
||||
}
|
||||
|
||||
export type CustomModel = {
|
||||
model: string
|
||||
model_type: ModelTypeEnum
|
||||
}
|
||||
|
||||
export type CustomModelCredential = CustomModel & {
|
||||
credentials?: Record<string, any>
|
||||
available_model_credentials?: Credential[]
|
||||
current_credential_id?: string
|
||||
current_credential_name?: string
|
||||
}
|
||||
|
||||
export type CredentialWithModel = Credential & {
|
||||
model: string
|
||||
model_type: ModelTypeEnum
|
||||
}
|
||||
|
||||
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: ConfigurationMethodEnum[]
|
||||
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
|
||||
current_credential_id?: string
|
||||
current_credential_name?: string
|
||||
available_credentials?: Credential[]
|
||||
custom_models?: CustomModelCredential[]
|
||||
can_added_models?: {
|
||||
model: string
|
||||
model_type: ModelTypeEnum
|
||||
}[]
|
||||
}
|
||||
system_configuration: {
|
||||
enabled: boolean
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum
|
||||
quota_configurations: QuotaConfiguration[]
|
||||
}
|
||||
allow_custom_token?: boolean
|
||||
}
|
||||
|
||||
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 CustomConfigurationModelFixedFields = {
|
||||
__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
|
||||
}
|
||||
|
||||
export type ModelLoadBalancingConfigEntry = {
|
||||
/** model balancing config entry id */
|
||||
id?: string
|
||||
/** is config entry enabled */
|
||||
enabled?: boolean
|
||||
/** config entry name */
|
||||
name: string
|
||||
/** model balancing credential */
|
||||
credentials: Record<string, string | undefined | boolean>
|
||||
/** is config entry currently removed from Round-robin queue */
|
||||
in_cooldown?: boolean
|
||||
/** cooldown time (in seconds) */
|
||||
ttl?: number
|
||||
credential_id?: string
|
||||
}
|
||||
|
||||
export type ModelLoadBalancingConfig = {
|
||||
enabled: boolean
|
||||
configs: ModelLoadBalancingConfigEntry[]
|
||||
}
|
||||
|
||||
export type ProviderCredential = {
|
||||
credentials: Record<string, any>
|
||||
name: string
|
||||
credential_id: string
|
||||
}
|
||||
|
||||
export type ModelCredential = {
|
||||
credentials: Record<string, any>
|
||||
load_balancing: ModelLoadBalancingConfig
|
||||
available_credentials: Credential[]
|
||||
current_credential_id?: string
|
||||
current_credential_name?: string
|
||||
}
|
||||
|
||||
export enum ModelModalModeEnum {
|
||||
configProviderCredential = 'config-provider-credential',
|
||||
configCustomModel = 'config-custom-model',
|
||||
addCustomModelToModelList = 'add-custom-model-to-model-list',
|
||||
configModelCredential = 'config-model-credential',
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useLanguage } from './hooks'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { after } from 'node:test'
|
||||
|
||||
jest.mock('swr', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(), // mock useSWR
|
||||
useSWRConfig: jest.fn(),
|
||||
}))
|
||||
|
||||
// mock use-context-selector
|
||||
jest.mock('use-context-selector', () => ({
|
||||
useContext: jest.fn(),
|
||||
}))
|
||||
|
||||
// mock service/common functions
|
||||
jest.mock('@/service/common', () => ({
|
||||
fetchDefaultModal: jest.fn(),
|
||||
fetchModelList: jest.fn(),
|
||||
fetchModelProviderCredentials: jest.fn(),
|
||||
fetchModelProviders: jest.fn(),
|
||||
getPayUrl: jest.fn(),
|
||||
}))
|
||||
|
||||
// mock context hooks
|
||||
jest.mock('@/context/i18n', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/context/modal-context', () => ({
|
||||
useModalContextSelector: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: jest.fn(),
|
||||
}))
|
||||
|
||||
// mock plugins
|
||||
jest.mock('@/app/components/plugins/marketplace/hooks', () => ({
|
||||
useMarketplacePlugins: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/plugins/marketplace/utils', () => ({
|
||||
getMarketplacePluginsByCollectionId: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('./provider-added-card', () => jest.fn())
|
||||
|
||||
after(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('useLanguage', () => {
|
||||
it('should replace hyphen with underscore in locale', () => {
|
||||
(useContext as jest.Mock).mockReturnValue({
|
||||
locale: 'en-US',
|
||||
})
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('en_US')
|
||||
})
|
||||
|
||||
it('should return locale as is if no hyphen exists', () => {
|
||||
(useContext as jest.Mock).mockReturnValue({
|
||||
locale: 'enUS',
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('enUS')
|
||||
})
|
||||
|
||||
it('should handle multiple hyphens', () => {
|
||||
// Mock the I18n context return value
|
||||
(useContext as jest.Mock).mockReturnValue({
|
||||
locale: 'zh-Hans-CN',
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('zh_Hans-CN')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,383 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import useSWR, { useSWRConfig } from 'swr'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModel,
|
||||
DefaultModel,
|
||||
DefaultModelResponse,
|
||||
Model,
|
||||
ModelModalModeEnum,
|
||||
ModelProvider,
|
||||
ModelTypeEnum,
|
||||
} from './declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
CustomConfigurationStatusEnum,
|
||||
ModelStatusEnum,
|
||||
} from './declarations'
|
||||
import I18n from '@/context/i18n'
|
||||
import {
|
||||
fetchDefaultModal,
|
||||
fetchModelList,
|
||||
fetchModelProviderCredentials,
|
||||
fetchModelProviders,
|
||||
getPayUrl,
|
||||
} from '@/service/common'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
useMarketplacePlugins,
|
||||
} from '@/app/components/plugins/marketplace/hooks'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { getMarketplacePluginsByCollectionId } from '@/app/components/plugins/marketplace/utils'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card'
|
||||
|
||||
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 locale.replace('-', '_')
|
||||
}
|
||||
|
||||
export const useProviderCredentialsAndLoadBalancing = (
|
||||
provider: string,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
configured?: boolean,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
credentialId?: string,
|
||||
) => {
|
||||
const { data: predefinedFormSchemasValue, mutate: mutatePredefined, isLoading: isPredefinedLoading } = useSWR(
|
||||
(configurationMethod === ConfigurationMethodEnum.predefinedModel && configured && credentialId)
|
||||
? `/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`
|
||||
: null,
|
||||
fetchModelProviderCredentials,
|
||||
)
|
||||
const { data: customFormSchemasValue, mutate: mutateCustomized, isLoading: isCustomizedLoading } = useSWR(
|
||||
(configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields && credentialId)
|
||||
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}${credentialId ? `&credential_id=${credentialId}` : ''}`
|
||||
: null,
|
||||
fetchModelProviderCredentials,
|
||||
)
|
||||
|
||||
const credentials = useMemo(() => {
|
||||
return configurationMethod === ConfigurationMethodEnum.predefinedModel
|
||||
? predefinedFormSchemasValue?.credentials
|
||||
: customFormSchemasValue?.credentials
|
||||
? {
|
||||
...customFormSchemasValue?.credentials,
|
||||
...currentCustomConfigurationModelFixedFields,
|
||||
}
|
||||
: undefined
|
||||
}, [
|
||||
configurationMethod,
|
||||
credentialId,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
customFormSchemasValue?.credentials,
|
||||
predefinedFormSchemasValue?.credentials,
|
||||
])
|
||||
|
||||
const mutate = useMemo(() => () => {
|
||||
mutatePredefined()
|
||||
mutateCustomized()
|
||||
}, [mutateCustomized, mutatePredefined])
|
||||
|
||||
return {
|
||||
credentials,
|
||||
loadBalancing: (configurationMethod === ConfigurationMethodEnum.predefinedModel
|
||||
? predefinedFormSchemasValue
|
||||
: customFormSchemasValue
|
||||
)?.load_balancing,
|
||||
mutate,
|
||||
isLoading: isPredefinedLoading || isCustomizedLoading,
|
||||
}
|
||||
// as ([Record<string, string | boolean | undefined> | undefined, ModelLoadBalancingConfig | undefined])
|
||||
}
|
||||
|
||||
export const useModelList = (type: ModelTypeEnum) => {
|
||||
const { data, mutate, isLoading } = useSWR(`/workspaces/current/models/model-types/${type}`, fetchModelList)
|
||||
|
||||
return {
|
||||
data: data?.data || [],
|
||||
mutate,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
export const useDefaultModel = (type: ModelTypeEnum) => {
|
||||
const { data, mutate, isLoading } = useSWR(`/workspaces/current/default-model?model_type=${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 activeTextGenerationModelList = textGenerationModelList.filter(model => model.status === ModelStatusEnum.active)
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
} = useCurrentProviderAndModel(textGenerationModelList, defaultModel)
|
||||
|
||||
return {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
textGenerationModelList,
|
||||
activeTextGenerationModelList,
|
||||
}
|
||||
}
|
||||
|
||||
export const useModelListAndDefaultModel = (type: ModelTypeEnum) => {
|
||||
const { data: modelList } = useModelList(type)
|
||||
const { data: defaultModel } = useDefaultModel(type)
|
||||
|
||||
return {
|
||||
modelList,
|
||||
defaultModel,
|
||||
}
|
||||
}
|
||||
|
||||
export const useModelListAndDefaultModelAndCurrentProviderAndModel = (type: ModelTypeEnum) => {
|
||||
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: ModelTypeEnum) => {
|
||||
mutate(`/workspaces/current/models/model-types/${type}`)
|
||||
}, [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 useModelProviders = () => {
|
||||
const { data: providersData, mutate, isLoading } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
|
||||
|
||||
return {
|
||||
data: providersData?.data || [],
|
||||
mutate,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
export const useUpdateModelProviders = () => {
|
||||
const { mutate } = useSWRConfig()
|
||||
|
||||
const updateModelProviders = useCallback(() => {
|
||||
mutate('/workspaces/current/model-providers')
|
||||
}, [mutate])
|
||||
|
||||
return updateModelProviders
|
||||
}
|
||||
|
||||
export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText: string) => {
|
||||
const exclude = useMemo(() => {
|
||||
return providers.map(provider => provider.provider.replace(/(.+)\/([^/]+)$/, '$1'))
|
||||
}, [providers])
|
||||
const [collectionPlugins, setCollectionPlugins] = useState<Plugin[]>([])
|
||||
|
||||
const {
|
||||
plugins,
|
||||
queryPlugins,
|
||||
queryPluginsWithDebounced,
|
||||
isLoading,
|
||||
} = useMarketplacePlugins()
|
||||
|
||||
const getCollectionPlugins = useCallback(async () => {
|
||||
const collectionPlugins = await getMarketplacePluginsByCollectionId('__model-settings-pinned-models')
|
||||
|
||||
setCollectionPlugins(collectionPlugins)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
getCollectionPlugins()
|
||||
}, [getCollectionPlugins])
|
||||
|
||||
useEffect(() => {
|
||||
if (searchText) {
|
||||
queryPluginsWithDebounced({
|
||||
query: searchText,
|
||||
category: PluginCategoryEnum.model,
|
||||
exclude,
|
||||
type: 'plugin',
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
})
|
||||
}
|
||||
else {
|
||||
queryPlugins({
|
||||
query: '',
|
||||
category: PluginCategoryEnum.model,
|
||||
type: 'plugin',
|
||||
pageSize: 1000,
|
||||
exclude,
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
})
|
||||
}
|
||||
}, [queryPlugins, queryPluginsWithDebounced, searchText, exclude])
|
||||
|
||||
const allPlugins = useMemo(() => {
|
||||
const allPlugins = collectionPlugins.filter(plugin => !exclude.includes(plugin.plugin_id))
|
||||
|
||||
if (plugins?.length) {
|
||||
for (let i = 0; i < plugins.length; i++) {
|
||||
const plugin = plugins[i]
|
||||
|
||||
if (plugin.type !== 'bundle' && !allPlugins.find(p => p.plugin_id === plugin.plugin_id))
|
||||
allPlugins.push(plugin)
|
||||
}
|
||||
}
|
||||
|
||||
return allPlugins
|
||||
}, [plugins, collectionPlugins, exclude])
|
||||
|
||||
return {
|
||||
plugins: allPlugins,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
export const useRefreshModel = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const handleRefreshModel = useCallback((
|
||||
provider: ModelProvider,
|
||||
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
refreshModelList?: boolean,
|
||||
) => {
|
||||
updateModelProviders()
|
||||
|
||||
provider.supported_model_types.forEach((type) => {
|
||||
updateModelList(type)
|
||||
})
|
||||
|
||||
if (refreshModelList && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
|
||||
eventEmitter?.emit({
|
||||
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
|
||||
payload: provider.provider,
|
||||
} as any)
|
||||
|
||||
if (CustomConfigurationModelFixedFields?.__model_type)
|
||||
updateModelList(CustomConfigurationModelFixedFields.__model_type)
|
||||
}
|
||||
}, [eventEmitter, updateModelList, updateModelProviders])
|
||||
|
||||
return {
|
||||
handleRefreshModel,
|
||||
}
|
||||
}
|
||||
|
||||
export const useModelModalHandler = () => {
|
||||
const setShowModelModal = useModalContextSelector(state => state.setShowModelModal)
|
||||
|
||||
return (
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
extra: {
|
||||
isModelCredential?: boolean,
|
||||
credential?: Credential,
|
||||
model?: CustomModel,
|
||||
onUpdate?: (newPayload: any, formValues?: Record<string, any>) => void,
|
||||
mode?: ModelModalModeEnum,
|
||||
} = {},
|
||||
) => {
|
||||
setShowModelModal({
|
||||
payload: {
|
||||
currentProvider: provider,
|
||||
currentConfigurationMethod: configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
|
||||
isModelCredential: extra.isModelCredential,
|
||||
credential: extra.credential,
|
||||
model: extra.model,
|
||||
mode: extra.mode,
|
||||
},
|
||||
onSaveCallback: (newPayload, formValues) => {
|
||||
extra.onUpdate?.(newPayload, formValues)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiBrainLine,
|
||||
} from '@remixicon/react'
|
||||
import SystemModelSelector from './system-model-selector'
|
||||
import ProviderAddedCard from './provider-added-card'
|
||||
import type {
|
||||
ModelProvider,
|
||||
} from './declarations'
|
||||
import {
|
||||
CustomConfigurationStatusEnum,
|
||||
ModelTypeEnum,
|
||||
} from './declarations'
|
||||
import {
|
||||
useDefaultModel,
|
||||
} from './hooks'
|
||||
import InstallFromMarketplace from './install-from-marketplace'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
type Props = {
|
||||
searchText: string
|
||||
}
|
||||
|
||||
const FixedModelProvider = ['langgenius/openai/openai', 'langgenius/anthropic/anthropic']
|
||||
|
||||
const ModelProviderPage = ({ searchText }: Props) => {
|
||||
const debouncedSearchText = useDebounce(searchText, { wait: 500 })
|
||||
const { t } = useTranslation()
|
||||
const { data: textGenerationDefaultModel } = useDefaultModel(ModelTypeEnum.textGeneration)
|
||||
const { data: embeddingsDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
|
||||
const { data: rerankDefaultModel } = useDefaultModel(ModelTypeEnum.rerank)
|
||||
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
|
||||
const { data: ttsDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
|
||||
const { modelProviders: providers } = useProviderContext()
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
|
||||
const [configuredProviders, notConfiguredProviders] = useMemo(() => {
|
||||
const configuredProviders: ModelProvider[] = []
|
||||
const notConfiguredProviders: ModelProvider[] = []
|
||||
|
||||
providers.forEach((provider) => {
|
||||
if (
|
||||
provider.custom_configuration.status === CustomConfigurationStatusEnum.active
|
||||
|| (
|
||||
provider.system_configuration.enabled === true
|
||||
&& provider.system_configuration.quota_configurations.find(item => item.quota_type === provider.system_configuration.current_quota_type)
|
||||
)
|
||||
)
|
||||
configuredProviders.push(provider)
|
||||
else
|
||||
notConfiguredProviders.push(provider)
|
||||
})
|
||||
|
||||
configuredProviders.sort((a, b) => {
|
||||
if (FixedModelProvider.includes(a.provider) && FixedModelProvider.includes(b.provider))
|
||||
return FixedModelProvider.indexOf(a.provider) - FixedModelProvider.indexOf(b.provider) > 0 ? 1 : -1
|
||||
else if (FixedModelProvider.includes(a.provider))
|
||||
return -1
|
||||
else if (FixedModelProvider.includes(b.provider))
|
||||
return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
return [configuredProviders, notConfiguredProviders]
|
||||
}, [providers])
|
||||
const [filteredConfiguredProviders, filteredNotConfiguredProviders] = useMemo(() => {
|
||||
const filteredConfiguredProviders = configuredProviders.filter(
|
||||
provider => provider.provider.toLowerCase().includes(debouncedSearchText.toLowerCase())
|
||||
|| Object.values(provider.label).some(text => text.toLowerCase().includes(debouncedSearchText.toLowerCase())),
|
||||
)
|
||||
const filteredNotConfiguredProviders = notConfiguredProviders.filter(
|
||||
provider => provider.provider.toLowerCase().includes(debouncedSearchText.toLowerCase())
|
||||
|| Object.values(provider.label).some(text => text.toLowerCase().includes(debouncedSearchText.toLowerCase())),
|
||||
)
|
||||
|
||||
return [filteredConfiguredProviders, filteredNotConfiguredProviders]
|
||||
}, [configuredProviders, debouncedSearchText, notConfiguredProviders])
|
||||
|
||||
return (
|
||||
<div className='relative -mt-2 pt-1'>
|
||||
<div className={cn('mb-2 flex items-center')}>
|
||||
<div className='system-md-semibold grow text-text-primary'>{t('common.modelProvider.models')}</div>
|
||||
<div className={cn(
|
||||
'relative flex shrink-0 items-center justify-end gap-2 rounded-lg border border-transparent p-px',
|
||||
defaultModelNotConfigured && 'border-components-panel-border bg-components-panel-bg-blur pl-2 shadow-xs',
|
||||
)}>
|
||||
{defaultModelNotConfigured && <div className='absolute bottom-0 left-0 right-0 top-0 opacity-40' style={{ background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)' }} />}
|
||||
{defaultModelNotConfigured && (
|
||||
<div className='system-xs-medium flex items-center gap-1 text-text-primary'>
|
||||
<RiAlertFill className='h-4 w-4 text-text-warning-secondary' />
|
||||
<span className='max-w-[460px] truncate' title={t('common.modelProvider.notConfigured')}>{t('common.modelProvider.notConfigured')}</span>
|
||||
</div>
|
||||
)}
|
||||
<SystemModelSelector
|
||||
notConfigured={defaultModelNotConfigured}
|
||||
textGenerationDefaultModel={textGenerationDefaultModel}
|
||||
embeddingsDefaultModel={embeddingsDefaultModel}
|
||||
rerankDefaultModel={rerankDefaultModel}
|
||||
speech2textDefaultModel={speech2textDefaultModel}
|
||||
ttsDefaultModel={ttsDefaultModel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!filteredConfiguredProviders?.length && (
|
||||
<div className='mb-2 rounded-[10px] bg-workflow-process-bg p-4'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur'>
|
||||
<RiBrainLine className='h-5 w-5 text-text-primary' />
|
||||
</div>
|
||||
<div className='system-sm-medium mt-2 text-text-secondary'>{t('common.modelProvider.emptyProviderTitle')}</div>
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>{t('common.modelProvider.emptyProviderTip')}</div>
|
||||
</div>
|
||||
)}
|
||||
{!!filteredConfiguredProviders?.length && (
|
||||
<div className='relative'>
|
||||
{filteredConfiguredProviders?.map(provider => (
|
||||
<ProviderAddedCard
|
||||
key={provider.provider}
|
||||
provider={provider}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!!filteredNotConfiguredProviders?.length && (
|
||||
<>
|
||||
<div className='system-md-semibold mb-2 flex items-center pt-2 text-text-primary'>{t('common.modelProvider.toBeConfigured')}</div>
|
||||
<div className='relative'>
|
||||
{filteredNotConfiguredProviders?.map(provider => (
|
||||
<ProviderAddedCard
|
||||
notConfigured
|
||||
key={provider.provider}
|
||||
provider={provider}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{
|
||||
enable_marketplace && (
|
||||
<InstallFromMarketplace
|
||||
providers={providers}
|
||||
searchText={searchText}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelProviderPage
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightUpLine,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
ModelProvider,
|
||||
} from './declarations'
|
||||
import {
|
||||
useMarketplaceAllPlugins,
|
||||
} from './hooks'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import ProviderCard from '@/app/components/plugins/provider-card'
|
||||
import List from '@/app/components/plugins/marketplace/list'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { getLocaleOnClient } from '@/i18n-config'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
type InstallFromMarketplaceProps = {
|
||||
providers: ModelProvider[]
|
||||
searchText: string
|
||||
}
|
||||
const InstallFromMarketplace = ({
|
||||
providers,
|
||||
searchText,
|
||||
}: InstallFromMarketplaceProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const [collapse, setCollapse] = useState(false)
|
||||
const locale = getLocaleOnClient()
|
||||
const {
|
||||
plugins: allPlugins,
|
||||
isLoading: isAllPluginsLoading,
|
||||
} = useMarketplaceAllPlugins(providers, searchText)
|
||||
|
||||
const cardRender = useCallback((plugin: Plugin) => {
|
||||
if (plugin.type === 'bundle')
|
||||
return null
|
||||
|
||||
return <ProviderCard key={plugin.plugin_id} payload={plugin} />
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='mb-2'>
|
||||
<Divider className='!mt-4 h-px' />
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='system-md-semibold flex cursor-pointer items-center gap-1 text-text-primary' onClick={() => setCollapse(!collapse)}>
|
||||
<RiArrowDownSLine className={cn('h-4 w-4', collapse && '-rotate-90')} />
|
||||
{t('common.modelProvider.installProvider')}
|
||||
</div>
|
||||
<div className='mb-2 flex items-center pt-2'>
|
||||
<span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span>
|
||||
<Link target="_blank" href={getMarketplaceUrl('', { theme })} className='system-sm-medium inline-flex items-center text-text-accent'>
|
||||
{t('plugin.marketplace.difyMarketplace')}
|
||||
<RiArrowRightUpLine className='h-4 w-4' />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{!collapse && isAllPluginsLoading && <Loading type='area' />}
|
||||
{
|
||||
!isAllPluginsLoading && !collapse && (
|
||||
<List
|
||||
marketplaceCollections={[]}
|
||||
marketplaceCollectionPluginsMap={{}}
|
||||
plugins={allPlugins}
|
||||
showInstallButton
|
||||
locale={locale}
|
||||
cardContainerClassName='grid grid-cols-2 gap-2'
|
||||
cardRender={cardRender}
|
||||
emptyClassName='h-auto'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstallFromMarketplace
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Authorized } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import cn from '@/utils/classnames'
|
||||
import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModelCredential,
|
||||
ModelCredential,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
type AddCredentialInLoadBalancingProps = {
|
||||
provider: ModelProvider
|
||||
model: CustomModelCredential
|
||||
configurationMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
modelCredential: ModelCredential
|
||||
onSelectCredential: (credential: Credential) => void
|
||||
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
|
||||
onRemove?: (credentialId: string) => void
|
||||
}
|
||||
const AddCredentialInLoadBalancing = ({
|
||||
provider,
|
||||
model,
|
||||
configurationMethod,
|
||||
modelCredential,
|
||||
onSelectCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}: AddCredentialInLoadBalancingProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
available_credentials,
|
||||
} = modelCredential
|
||||
const isCustomModel = configurationMethod === ConfigurationMethodEnum.customizableModel
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
const handleUpdate = useCallback((payload?: any, formValues?: Record<string, any>) => {
|
||||
onUpdate?.(payload, formValues)
|
||||
}, [onUpdate])
|
||||
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
const Item = (
|
||||
<div className={cn(
|
||||
'system-sm-medium flex h-8 items-center rounded-lg px-3 text-text-accent hover:bg-state-base-hover',
|
||||
open && 'bg-state-base-hover',
|
||||
)}>
|
||||
<RiAddLine className='mr-2 h-4 w-4' />
|
||||
{t('common.modelProvider.auth.addCredential')}
|
||||
</div>
|
||||
)
|
||||
|
||||
return Item
|
||||
}, [t, isCustomModel])
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
renderTrigger={renderTrigger}
|
||||
authParams={{
|
||||
isModelCredential: isCustomModel,
|
||||
mode: ModelModalModeEnum.configModelCredential,
|
||||
onUpdate: handleUpdate,
|
||||
onRemove,
|
||||
}}
|
||||
triggerOnlyOpenModal={!available_credentials?.length && !notAllowCustomCredential}
|
||||
items={[
|
||||
{
|
||||
title: isCustomModel ? '' : t('common.modelProvider.auth.apiKeys'),
|
||||
model: isCustomModel ? model : undefined,
|
||||
credentials: available_credentials ?? [],
|
||||
},
|
||||
]}
|
||||
showModelTitle={!isCustomModel}
|
||||
configurationMethod={configurationMethod}
|
||||
currentCustomConfigurationModelFixedFields={isCustomModel ? {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
} : undefined}
|
||||
onItemClick={onSelectCredential}
|
||||
placement='bottom-start'
|
||||
popupTitle={isCustomModel ? t('common.modelProvider.auth.modelCredentials') : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddCredentialInLoadBalancing)
|
||||
@@ -0,0 +1,167 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddCircleFill,
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
Button,
|
||||
} from '@/app/components/base/button'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import ModelIcon from '../model-icon'
|
||||
import { useCanAddedModels } from './hooks/use-custom-models'
|
||||
import { useAuth } from './hooks/use-auth'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type AddCustomModelProps = {
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
const AddCustomModel = ({
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
}: AddCustomModelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const canAddedModels = useCanAddedModels(provider)
|
||||
const noModels = !canAddedModels.length
|
||||
const {
|
||||
handleOpenModal: handleOpenModalForAddNewCustomModel,
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential: true,
|
||||
mode: ModelModalModeEnum.configCustomModel,
|
||||
},
|
||||
)
|
||||
const {
|
||||
handleOpenModal: handleOpenModalForAddCustomModelToModelList,
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential: true,
|
||||
mode: ModelModalModeEnum.addCustomModelToModelList,
|
||||
},
|
||||
)
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
const Item = (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='small'
|
||||
className={cn(
|
||||
'text-text-tertiary',
|
||||
open && 'bg-components-button-ghost-bg-hover',
|
||||
notAllowCustomCredential && !!noModels && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
>
|
||||
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.modelProvider.addModel')}
|
||||
</Button>
|
||||
)
|
||||
if (notAllowCustomCredential && !!noModels) {
|
||||
return (
|
||||
<Tooltip asChild popupContent={t('plugin.auth.credentialUnavailable')}>
|
||||
{Item}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return Item
|
||||
}, [t, notAllowCustomCredential, noModels])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => {
|
||||
if (noModels) {
|
||||
if (notAllowCustomCredential)
|
||||
return
|
||||
handleOpenModalForAddNewCustomModel()
|
||||
return
|
||||
}
|
||||
|
||||
setOpen(prev => !prev)
|
||||
}}>
|
||||
{renderTrigger(open)}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[100]'>
|
||||
<div className='w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
|
||||
<div className='max-h-[304px] overflow-y-auto p-1'>
|
||||
{
|
||||
canAddedModels.map(model => (
|
||||
<div
|
||||
key={model.model}
|
||||
className='flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
handleOpenModalForAddCustomModelToModelList(undefined, model)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<ModelIcon
|
||||
className='mr-1 h-5 w-5 shrink-0'
|
||||
iconClassName='h-5 w-5'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
<div
|
||||
className='system-md-regular grow truncate text-text-primary'
|
||||
title={model.model}
|
||||
>
|
||||
{model.model}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!notAllowCustomCredential && (
|
||||
<div
|
||||
className='system-xs-medium flex cursor-pointer items-center border-t border-t-divider-subtle p-3 text-text-accent-light-mode-only'
|
||||
onClick={() => {
|
||||
handleOpenModalForAddNewCustomModel()
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
{t('common.modelProvider.auth.addNewModel')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddCustomModel)
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import CredentialItem from './credential-item'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModel,
|
||||
CustomModelCredential,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import ModelIcon from '../../model-icon'
|
||||
|
||||
type AuthorizedItemProps = {
|
||||
provider: ModelProvider
|
||||
model?: CustomModelCredential
|
||||
title?: string
|
||||
disabled?: boolean
|
||||
onDelete?: (credential?: Credential, model?: CustomModel) => void
|
||||
onEdit?: (credential?: Credential, model?: CustomModel) => void
|
||||
showItemSelectedIcon?: boolean
|
||||
selectedCredentialId?: string
|
||||
credentials: Credential[]
|
||||
onItemClick?: (credential: Credential, model?: CustomModel) => void
|
||||
enableAddModelCredential?: boolean
|
||||
notAllowCustomCredential?: boolean
|
||||
showModelTitle?: boolean
|
||||
disableDeleteButShowAction?: boolean
|
||||
disableDeleteTip?: string
|
||||
}
|
||||
export const AuthorizedItem = ({
|
||||
provider,
|
||||
model,
|
||||
title,
|
||||
credentials,
|
||||
disabled,
|
||||
onDelete,
|
||||
onEdit,
|
||||
showItemSelectedIcon,
|
||||
selectedCredentialId,
|
||||
onItemClick,
|
||||
showModelTitle,
|
||||
disableDeleteButShowAction,
|
||||
disableDeleteTip,
|
||||
}: AuthorizedItemProps) => {
|
||||
const handleEdit = useCallback((credential?: Credential) => {
|
||||
onEdit?.(credential, model)
|
||||
}, [onEdit, model])
|
||||
const handleDelete = useCallback((credential?: Credential) => {
|
||||
onDelete?.(credential, model)
|
||||
}, [onDelete, model])
|
||||
const handleItemClick = useCallback((credential: Credential) => {
|
||||
onItemClick?.(credential, model)
|
||||
}, [onItemClick, model])
|
||||
|
||||
return (
|
||||
<div className='p-1'>
|
||||
{
|
||||
showModelTitle && (
|
||||
<div
|
||||
className='flex h-9 items-center px-2'
|
||||
>
|
||||
{
|
||||
model?.model && (
|
||||
<ModelIcon
|
||||
className='mr-1 h-5 w-5 shrink-0'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className='system-md-medium mx-1 grow truncate text-text-primary'
|
||||
title={title ?? model?.model}
|
||||
>
|
||||
{title ?? model?.model}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
credentials.map(credential => (
|
||||
<CredentialItem
|
||||
key={credential.credential_id}
|
||||
credential={credential}
|
||||
disabled={disabled}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
showSelectedIcon={showItemSelectedIcon}
|
||||
selectedCredentialId={selectedCredentialId}
|
||||
onItemClick={handleItemClick}
|
||||
disableDeleteButShowAction={disableDeleteButShowAction}
|
||||
disableDeleteTip={disableDeleteTip}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AuthorizedItem)
|
||||
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCheckLine,
|
||||
RiDeleteBinLine,
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { Credential } from '../../declarations'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
type CredentialItemProps = {
|
||||
credential: Credential
|
||||
disabled?: boolean
|
||||
onDelete?: (credential: Credential) => void
|
||||
onEdit?: (credential?: Credential) => void
|
||||
onItemClick?: (credential: Credential) => void
|
||||
disableRename?: boolean
|
||||
disableEdit?: boolean
|
||||
disableDelete?: boolean
|
||||
disableDeleteButShowAction?: boolean
|
||||
disableDeleteTip?: string
|
||||
showSelectedIcon?: boolean
|
||||
selectedCredentialId?: string
|
||||
}
|
||||
const CredentialItem = ({
|
||||
credential,
|
||||
disabled,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onItemClick,
|
||||
disableRename,
|
||||
disableEdit,
|
||||
disableDelete,
|
||||
disableDeleteButShowAction,
|
||||
disableDeleteTip,
|
||||
showSelectedIcon,
|
||||
selectedCredentialId,
|
||||
}: CredentialItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const showAction = useMemo(() => {
|
||||
return !(disableRename && disableEdit && disableDelete)
|
||||
}, [disableRename, disableEdit, disableDelete])
|
||||
const disableDeleteWhenSelected = useMemo(() => {
|
||||
return disableDeleteButShowAction && selectedCredentialId === credential.credential_id
|
||||
}, [disableDeleteButShowAction, selectedCredentialId, credential.credential_id])
|
||||
|
||||
const Item = (
|
||||
<div
|
||||
key={credential.credential_id}
|
||||
className={cn(
|
||||
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
|
||||
(disabled || credential.not_allowed_to_use) && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (disabled || credential.not_allowed_to_use)
|
||||
return
|
||||
onItemClick?.(credential)
|
||||
}}
|
||||
>
|
||||
<div className='flex w-0 grow items-center space-x-1.5'>
|
||||
{
|
||||
showSelectedIcon && (
|
||||
<div className='h-4 w-4'>
|
||||
{
|
||||
selectedCredentialId === credential.credential_id && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Indicator className='ml-2 mr-1.5 shrink-0' />
|
||||
<div
|
||||
className='system-md-regular truncate text-text-secondary'
|
||||
title={credential.credential_name}
|
||||
>
|
||||
{credential.credential_name}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
credential.from_enterprise && (
|
||||
<Badge className='shrink-0'>
|
||||
Enterprise
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
{
|
||||
showAction && !credential.from_enterprise && (
|
||||
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
|
||||
{
|
||||
!disableEdit && !credential.not_allowed_to_use && (
|
||||
<Tooltip popupContent={t('common.operation.edit')}>
|
||||
<ActionButton
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onEdit?.(credential)
|
||||
}}
|
||||
>
|
||||
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
{
|
||||
!disableDelete && (
|
||||
<Tooltip popupContent={disableDeleteWhenSelected ? disableDeleteTip : t('common.operation.delete')}>
|
||||
<ActionButton
|
||||
className='hover:bg-transparent'
|
||||
onClick={(e) => {
|
||||
if (disabled || disableDeleteWhenSelected)
|
||||
return
|
||||
e.stopPropagation()
|
||||
onDelete?.(credential)
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className={cn(
|
||||
'h-4 w-4 text-text-tertiary',
|
||||
!disableDeleteWhenSelected && 'hover:text-text-destructive',
|
||||
disableDeleteWhenSelected && 'opacity-50',
|
||||
)} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (credential.not_allowed_to_use) {
|
||||
return (
|
||||
<Tooltip popupContent={t('plugin.auth.customCredentialUnavailable')}>
|
||||
{Item}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return Item
|
||||
}
|
||||
|
||||
export default memo(CredentialItem)
|
||||
@@ -0,0 +1,257 @@
|
||||
import {
|
||||
Fragment,
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type {
|
||||
PortalToFollowElemOptions,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModel,
|
||||
ModelModalModeEnum,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import { useAuth } from '../hooks'
|
||||
import AuthorizedItem from './authorized-item'
|
||||
|
||||
type AuthorizedProps = {
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
authParams?: {
|
||||
isModelCredential?: boolean
|
||||
onUpdate?: (newPayload?: any, formValues?: Record<string, any>) => void
|
||||
onRemove?: (credentialId: string) => void
|
||||
mode?: ModelModalModeEnum
|
||||
}
|
||||
items: {
|
||||
title?: string
|
||||
model?: CustomModel
|
||||
selectedCredential?: Credential
|
||||
credentials: Credential[]
|
||||
}[]
|
||||
disabled?: boolean
|
||||
renderTrigger: (open?: boolean) => React.ReactNode
|
||||
isOpen?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
offset?: PortalToFollowElemOptions['offset']
|
||||
placement?: PortalToFollowElemOptions['placement']
|
||||
triggerPopupSameWidth?: boolean
|
||||
popupClassName?: string
|
||||
showItemSelectedIcon?: boolean
|
||||
onItemClick?: (credential: Credential, model?: CustomModel) => void
|
||||
enableAddModelCredential?: boolean
|
||||
triggerOnlyOpenModal?: boolean
|
||||
hideAddAction?: boolean
|
||||
disableItemClick?: boolean
|
||||
popupTitle?: string
|
||||
showModelTitle?: boolean
|
||||
disableDeleteButShowAction?: boolean
|
||||
disableDeleteTip?: string
|
||||
}
|
||||
const Authorized = ({
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
items,
|
||||
authParams,
|
||||
disabled,
|
||||
renderTrigger,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
offset = 8,
|
||||
placement = 'bottom-end',
|
||||
triggerPopupSameWidth = false,
|
||||
popupClassName,
|
||||
showItemSelectedIcon,
|
||||
onItemClick,
|
||||
triggerOnlyOpenModal,
|
||||
hideAddAction,
|
||||
disableItemClick,
|
||||
popupTitle,
|
||||
showModelTitle,
|
||||
disableDeleteButShowAction,
|
||||
disableDeleteTip,
|
||||
}: AuthorizedProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isLocalOpen, setIsLocalOpen] = useState(false)
|
||||
const mergedIsOpen = isOpen ?? isLocalOpen
|
||||
const setMergedIsOpen = useCallback((open: boolean) => {
|
||||
if (onOpenChange)
|
||||
onOpenChange(open)
|
||||
|
||||
setIsLocalOpen(open)
|
||||
}, [onOpenChange])
|
||||
const {
|
||||
isModelCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
mode,
|
||||
} = authParams || {}
|
||||
const {
|
||||
openConfirmDelete,
|
||||
closeConfirmDelete,
|
||||
doingAction,
|
||||
handleActiveCredential,
|
||||
handleConfirmDelete,
|
||||
deleteCredentialId,
|
||||
handleOpenModal,
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
mode,
|
||||
},
|
||||
)
|
||||
|
||||
const handleEdit = useCallback((credential?: Credential, model?: CustomModel) => {
|
||||
handleOpenModal(credential, model)
|
||||
setMergedIsOpen(false)
|
||||
}, [handleOpenModal, setMergedIsOpen])
|
||||
|
||||
const handleItemClick = useCallback((credential: Credential, model?: CustomModel) => {
|
||||
if (disableItemClick)
|
||||
return
|
||||
|
||||
if (onItemClick)
|
||||
onItemClick(credential, model)
|
||||
else
|
||||
handleActiveCredential(credential, model)
|
||||
|
||||
setMergedIsOpen(false)
|
||||
}, [handleActiveCredential, onItemClick, setMergedIsOpen, disableItemClick])
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
|
||||
return (
|
||||
<>
|
||||
<PortalToFollowElem
|
||||
open={mergedIsOpen}
|
||||
onOpenChange={setMergedIsOpen}
|
||||
placement={placement}
|
||||
offset={offset}
|
||||
triggerPopupSameWidth={triggerPopupSameWidth}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => {
|
||||
if (triggerOnlyOpenModal) {
|
||||
handleOpenModal()
|
||||
return
|
||||
}
|
||||
|
||||
setMergedIsOpen(!mergedIsOpen)
|
||||
}}
|
||||
asChild
|
||||
>
|
||||
{renderTrigger(mergedIsOpen)}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[100]'>
|
||||
<div className={cn(
|
||||
'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]',
|
||||
popupClassName,
|
||||
)}>
|
||||
{
|
||||
popupTitle && (
|
||||
<div className='system-xs-medium px-3 pb-0.5 pt-[10px] text-text-tertiary'>
|
||||
{popupTitle}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='max-h-[304px] overflow-y-auto'>
|
||||
{
|
||||
items.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
<AuthorizedItem
|
||||
provider={provider}
|
||||
title={item.title}
|
||||
model={item.model}
|
||||
credentials={item.credentials}
|
||||
disabled={disabled}
|
||||
onDelete={openConfirmDelete}
|
||||
disableDeleteButShowAction={disableDeleteButShowAction}
|
||||
disableDeleteTip={disableDeleteTip}
|
||||
onEdit={handleEdit}
|
||||
showItemSelectedIcon={showItemSelectedIcon}
|
||||
selectedCredentialId={item.selectedCredential?.credential_id}
|
||||
onItemClick={handleItemClick}
|
||||
showModelTitle={showModelTitle}
|
||||
/>
|
||||
{
|
||||
index !== items.length - 1 && (
|
||||
<div className='h-[1px] bg-divider-subtle'></div>
|
||||
)
|
||||
}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='h-[1px] bg-divider-subtle'></div>
|
||||
{
|
||||
isModelCredential && !notAllowCustomCredential && !hideAddAction && (
|
||||
<div
|
||||
onClick={() => handleEdit(
|
||||
undefined,
|
||||
currentCustomConfigurationModelFixedFields
|
||||
? {
|
||||
model: currentCustomConfigurationModelFixedFields.__model_name,
|
||||
model_type: currentCustomConfigurationModelFixedFields.__model_type,
|
||||
}
|
||||
: undefined,
|
||||
)}
|
||||
className='system-xs-medium flex h-[40px] cursor-pointer items-center px-3 text-text-accent-light-mode-only'
|
||||
>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
{t('common.modelProvider.auth.addModelCredential')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isModelCredential && !notAllowCustomCredential && !hideAddAction && (
|
||||
<div className='p-2'>
|
||||
<Button
|
||||
onClick={() => handleEdit()}
|
||||
className='w-full'
|
||||
>
|
||||
{t('common.modelProvider.auth.addApiKey')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
{
|
||||
deleteCredentialId && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('common.modelProvider.confirmDelete')}
|
||||
isDisabled={doingAction}
|
||||
onCancel={closeConfirmDelete}
|
||||
onConfirm={handleConfirmDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Authorized)
|
||||
@@ -0,0 +1,76 @@
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
RiEqualizer2Line,
|
||||
RiScales3Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ConfigModelProps = {
|
||||
onClick?: () => void
|
||||
loadBalancingEnabled?: boolean
|
||||
loadBalancingInvalid?: boolean
|
||||
credentialRemoved?: boolean
|
||||
}
|
||||
const ConfigModel = ({
|
||||
onClick,
|
||||
loadBalancingEnabled,
|
||||
loadBalancingInvalid,
|
||||
credentialRemoved,
|
||||
}: ConfigModelProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (loadBalancingInvalid) {
|
||||
return (
|
||||
<div
|
||||
className='system-2xs-medium-uppercase relative flex h-[18px] cursor-pointer items-center rounded-[5px] border border-text-warning bg-components-badge-bg-dimm px-1.5 text-text-warning'
|
||||
onClick={onClick}
|
||||
>
|
||||
<RiScales3Line className='mr-0.5 h-3 w-3' />
|
||||
{t('common.modelProvider.auth.authorizationError')}
|
||||
<Indicator color='orange' className='absolute right-[-1px] top-[-1px] h-1.5 w-1.5' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
className={cn(
|
||||
'hidden shrink-0 group-hover:flex',
|
||||
credentialRemoved && 'flex',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{
|
||||
credentialRemoved && (
|
||||
<>
|
||||
{t('common.modelProvider.auth.credentialRemoved')}
|
||||
<Indicator color='red' className='ml-2' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!loadBalancingEnabled && !credentialRemoved && !loadBalancingInvalid && (
|
||||
<>
|
||||
<RiEqualizer2Line className='mr-1 h-4 w-4' />
|
||||
{t('common.operation.config')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
loadBalancingEnabled && !credentialRemoved && !loadBalancingInvalid && (
|
||||
<>
|
||||
<RiScales3Line className='mr-1 h-4 w-4' />
|
||||
{t('common.modelProvider.auth.configLoadBalancing')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConfigModel)
|
||||
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
Button,
|
||||
} from '@/app/components/base/button'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Authorized from './authorized'
|
||||
import { useCredentialStatus } from './hooks'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type ConfigProviderProps = {
|
||||
provider: ModelProvider,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
}
|
||||
const ConfigProvider = ({
|
||||
provider,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
}: ConfigProviderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
hasCredential,
|
||||
authorized,
|
||||
current_credential_id,
|
||||
current_credential_name,
|
||||
available_credentials,
|
||||
} = useCredentialStatus(provider)
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
const text = hasCredential ? t('common.operation.config') : t('common.operation.setup')
|
||||
const Item = (
|
||||
<Button
|
||||
className='flex grow'
|
||||
size='small'
|
||||
variant={!authorized ? 'secondary-accent' : 'secondary'}
|
||||
title={text}
|
||||
>
|
||||
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5 shrink-0' />
|
||||
<span className='w-0 grow truncate text-left'>
|
||||
{text}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
if (notAllowCustomCredential && !hasCredential) {
|
||||
return (
|
||||
<Tooltip
|
||||
asChild
|
||||
popupContent={t('plugin.auth.credentialUnavailable')}
|
||||
>
|
||||
{Item}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return Item
|
||||
}, [authorized, hasCredential, notAllowCustomCredential, t])
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
currentCustomConfigurationModelFixedFields={currentCustomConfigurationModelFixedFields}
|
||||
items={[
|
||||
{
|
||||
title: t('common.modelProvider.auth.apiKeys'),
|
||||
credentials: available_credentials ?? [],
|
||||
selectedCredential: {
|
||||
credential_id: current_credential_id ?? '',
|
||||
credential_name: current_credential_name ?? '',
|
||||
},
|
||||
},
|
||||
]}
|
||||
showItemSelectedIcon
|
||||
showModelTitle
|
||||
renderTrigger={renderTrigger}
|
||||
triggerOnlyOpenModal={!hasCredential && !notAllowCustomCredential}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConfigProvider)
|
||||
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiArrowDownSLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { Credential } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import CredentialItem from './authorized/credential-item'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
|
||||
type CredentialSelectorProps = {
|
||||
selectedCredential?: Credential & { addNewCredential?: boolean }
|
||||
credentials: Credential[]
|
||||
onSelect: (credential: Credential & { addNewCredential?: boolean }) => void
|
||||
disabled?: boolean
|
||||
notAllowAddNewCredential?: boolean
|
||||
}
|
||||
const CredentialSelector = ({
|
||||
selectedCredential,
|
||||
credentials,
|
||||
onSelect,
|
||||
disabled,
|
||||
notAllowAddNewCredential,
|
||||
}: CredentialSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const handleSelect = useCallback((credential: Credential & { addNewCredential?: boolean }) => {
|
||||
setOpen(false)
|
||||
onSelect(credential)
|
||||
}, [onSelect])
|
||||
const handleAddNewCredential = useCallback(() => {
|
||||
handleSelect({
|
||||
credential_id: '__add_new_credential',
|
||||
addNewCredential: true,
|
||||
credential_name: t('common.modelProvider.auth.addNewModelCredential'),
|
||||
})
|
||||
}, [handleSelect, t])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
triggerPopupSameWidth
|
||||
>
|
||||
<PortalToFollowElemTrigger asChild onClick={() => !disabled && setOpen(v => !v)}>
|
||||
<div className='system-sm-regular flex h-8 w-full items-center justify-between rounded-lg bg-components-input-bg-normal px-2'>
|
||||
{
|
||||
selectedCredential && (
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
!selectedCredential.addNewCredential && <Indicator className='ml-1 mr-2 shrink-0' />
|
||||
}
|
||||
<div className='system-sm-regular truncate text-components-input-text-filled' title={selectedCredential.credential_name}>{selectedCredential.credential_name}</div>
|
||||
{
|
||||
selectedCredential.from_enterprise && (
|
||||
<Badge className='shrink-0'>Enterprise</Badge>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!selectedCredential && (
|
||||
<div className='system-sm-regular grow truncate text-components-input-text-placeholder'>{t('common.modelProvider.auth.selectModelCredential')}</div>
|
||||
)
|
||||
}
|
||||
<RiArrowDownSLine className='h-4 w-4 text-text-quaternary' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[100]'>
|
||||
<div className='border-ccomponents-panel-border rounded-xl border-[0.5px] bg-components-panel-bg-blur shadow-lg'>
|
||||
<div className='max-h-[320px] overflow-y-auto p-1'>
|
||||
{
|
||||
credentials.map(credential => (
|
||||
<CredentialItem
|
||||
key={credential.credential_id}
|
||||
credential={credential}
|
||||
disableDelete
|
||||
disableEdit
|
||||
disableRename
|
||||
onItemClick={handleSelect}
|
||||
showSelectedIcon
|
||||
selectedCredentialId={selectedCredential?.credential_id}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!notAllowAddNewCredential && (
|
||||
<div
|
||||
className='system-xs-medium flex h-10 cursor-pointer items-center border-t border-t-divider-subtle px-7 text-text-accent-light-mode-only'
|
||||
onClick={handleAddNewCredential}
|
||||
>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
{t('common.modelProvider.auth.addNewModelCredential')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CredentialSelector)
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './use-model-form-schemas'
|
||||
export * from './use-credential-status'
|
||||
export * from './use-custom-models'
|
||||
export * from './use-auth'
|
||||
export * from './use-auth-service'
|
||||
export * from './use-credential-data'
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useActiveModelCredential,
|
||||
useActiveProviderCredential,
|
||||
useAddModelCredential,
|
||||
useAddProviderCredential,
|
||||
useDeleteModelCredential,
|
||||
useDeleteProviderCredential,
|
||||
useEditModelCredential,
|
||||
useEditProviderCredential,
|
||||
useGetModelCredential,
|
||||
useGetProviderCredential,
|
||||
} from '@/service/use-models'
|
||||
import type {
|
||||
CustomModel,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
export const useGetCredential = (provider: string, isModelCredential?: boolean, credentialId?: string, model?: CustomModel, configFrom?: string) => {
|
||||
const providerData = useGetProviderCredential(!isModelCredential && !!credentialId, provider, credentialId)
|
||||
const modelData = useGetModelCredential(!!isModelCredential && (!!credentialId || !!model), provider, credentialId, model?.model, model?.model_type, configFrom)
|
||||
return isModelCredential ? modelData : providerData
|
||||
}
|
||||
|
||||
export const useAuthService = (provider: string) => {
|
||||
const { mutateAsync: addProviderCredential } = useAddProviderCredential(provider)
|
||||
const { mutateAsync: editProviderCredential } = useEditProviderCredential(provider)
|
||||
const { mutateAsync: deleteProviderCredential } = useDeleteProviderCredential(provider)
|
||||
const { mutateAsync: activeProviderCredential } = useActiveProviderCredential(provider)
|
||||
|
||||
const { mutateAsync: addModelCredential } = useAddModelCredential(provider)
|
||||
const { mutateAsync: activeModelCredential } = useActiveModelCredential(provider)
|
||||
const { mutateAsync: deleteModelCredential } = useDeleteModelCredential(provider)
|
||||
const { mutateAsync: editModelCredential } = useEditModelCredential(provider)
|
||||
|
||||
const getAddCredentialService = useCallback((isModel: boolean) => {
|
||||
return isModel ? addModelCredential : addProviderCredential
|
||||
}, [addModelCredential, addProviderCredential])
|
||||
|
||||
const getEditCredentialService = useCallback((isModel: boolean) => {
|
||||
return isModel ? editModelCredential : editProviderCredential
|
||||
}, [editModelCredential, editProviderCredential])
|
||||
|
||||
const getDeleteCredentialService = useCallback((isModel: boolean) => {
|
||||
return isModel ? deleteModelCredential : deleteProviderCredential
|
||||
}, [deleteModelCredential, deleteProviderCredential])
|
||||
|
||||
const getActiveCredentialService = useCallback((isModel: boolean) => {
|
||||
return isModel ? activeModelCredential : activeProviderCredential
|
||||
}, [activeModelCredential, activeProviderCredential])
|
||||
|
||||
return {
|
||||
getAddCredentialService,
|
||||
getEditCredentialService,
|
||||
getDeleteCredentialService,
|
||||
getActiveCredentialService,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useAuthService } from './use-auth-service'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModel,
|
||||
ModelModalModeEnum,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import {
|
||||
useModelModalHandler,
|
||||
useRefreshModel,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useDeleteModel } from '@/service/use-models'
|
||||
|
||||
export const useAuth = (
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
extra: {
|
||||
isModelCredential?: boolean,
|
||||
onUpdate?: (newPayload?: any, formValues?: Record<string, any>) => void,
|
||||
onRemove?: (credentialId: string) => void,
|
||||
mode?: ModelModalModeEnum,
|
||||
} = {},
|
||||
) => {
|
||||
const {
|
||||
isModelCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
mode,
|
||||
} = extra
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const {
|
||||
getDeleteCredentialService,
|
||||
getActiveCredentialService,
|
||||
getEditCredentialService,
|
||||
getAddCredentialService,
|
||||
} = useAuthService(provider.provider)
|
||||
const { mutateAsync: deleteModelService } = useDeleteModel(provider.provider)
|
||||
const handleOpenModelModal = useModelModalHandler()
|
||||
const { handleRefreshModel } = useRefreshModel()
|
||||
const pendingOperationCredentialId = useRef<string | null>(null)
|
||||
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
|
||||
const handleSetDeleteCredentialId = useCallback((credentialId: string | null) => {
|
||||
setDeleteCredentialId(credentialId)
|
||||
pendingOperationCredentialId.current = credentialId
|
||||
}, [])
|
||||
const pendingOperationModel = useRef<CustomModel | null>(null)
|
||||
const [deleteModel, setDeleteModel] = useState<CustomModel | null>(null)
|
||||
const handleSetDeleteModel = useCallback((model: CustomModel | null) => {
|
||||
setDeleteModel(model)
|
||||
pendingOperationModel.current = model
|
||||
}, [])
|
||||
const openConfirmDelete = useCallback((credential?: Credential, model?: CustomModel) => {
|
||||
if (credential)
|
||||
handleSetDeleteCredentialId(credential.credential_id)
|
||||
if (model)
|
||||
handleSetDeleteModel(model)
|
||||
}, [])
|
||||
const closeConfirmDelete = useCallback(() => {
|
||||
handleSetDeleteCredentialId(null)
|
||||
handleSetDeleteModel(null)
|
||||
}, [])
|
||||
const [doingAction, setDoingAction] = useState(false)
|
||||
const doingActionRef = useRef(doingAction)
|
||||
const handleSetDoingAction = useCallback((doing: boolean) => {
|
||||
doingActionRef.current = doing
|
||||
setDoingAction(doing)
|
||||
}, [])
|
||||
const handleActiveCredential = useCallback(async (credential: Credential, model?: CustomModel) => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
try {
|
||||
handleSetDoingAction(true)
|
||||
await getActiveCredentialService(!!model)({
|
||||
credential_id: credential.credential_id,
|
||||
model: model?.model,
|
||||
model_type: model?.model_type,
|
||||
})
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
handleRefreshModel(provider, undefined, true)
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [getActiveCredentialService, notify, t, handleSetDoingAction])
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
if (!pendingOperationCredentialId.current && !pendingOperationModel.current) {
|
||||
closeConfirmDelete()
|
||||
return
|
||||
}
|
||||
try {
|
||||
handleSetDoingAction(true)
|
||||
let payload: any = {}
|
||||
if (pendingOperationCredentialId.current) {
|
||||
payload = {
|
||||
credential_id: pendingOperationCredentialId.current,
|
||||
model: pendingOperationModel.current?.model,
|
||||
model_type: pendingOperationModel.current?.model_type,
|
||||
}
|
||||
await getDeleteCredentialService(!!isModelCredential)(payload)
|
||||
}
|
||||
if (!pendingOperationCredentialId.current && pendingOperationModel.current) {
|
||||
payload = {
|
||||
model: pendingOperationModel.current.model,
|
||||
model_type: pendingOperationModel.current.model_type,
|
||||
}
|
||||
await deleteModelService(payload)
|
||||
}
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
handleRefreshModel(provider, undefined, true)
|
||||
onRemove?.(pendingOperationCredentialId.current ?? '')
|
||||
closeConfirmDelete()
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [notify, t, handleSetDoingAction, getDeleteCredentialService, isModelCredential, closeConfirmDelete, handleRefreshModel, provider, configurationMethod, deleteModelService])
|
||||
const handleSaveCredential = useCallback(async (payload: Record<string, any>) => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
try {
|
||||
handleSetDoingAction(true)
|
||||
|
||||
let res: { result?: string } = {}
|
||||
if (payload.credential_id)
|
||||
res = await getEditCredentialService(!!isModelCredential)(payload as any)
|
||||
else
|
||||
res = await getAddCredentialService(!!isModelCredential)(payload as any)
|
||||
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
handleRefreshModel(provider, undefined, !payload.credential_id)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [notify, t, handleSetDoingAction, getEditCredentialService, getAddCredentialService])
|
||||
const handleOpenModal = useCallback((credential?: Credential, model?: CustomModel) => {
|
||||
handleOpenModelModal(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential,
|
||||
credential,
|
||||
model,
|
||||
onUpdate,
|
||||
mode,
|
||||
},
|
||||
)
|
||||
}, [
|
||||
handleOpenModelModal,
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
isModelCredential,
|
||||
onUpdate,
|
||||
mode,
|
||||
])
|
||||
|
||||
return {
|
||||
pendingOperationCredentialId,
|
||||
pendingOperationModel,
|
||||
openConfirmDelete,
|
||||
closeConfirmDelete,
|
||||
doingAction,
|
||||
handleActiveCredential,
|
||||
handleConfirmDelete,
|
||||
deleteCredentialId,
|
||||
deleteModel,
|
||||
handleSaveCredential,
|
||||
handleOpenModal,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useGetCredential } from './use-auth-service'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModelCredential,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
export const useCredentialData = (provider: ModelProvider, providerFormSchemaPredefined: boolean, isModelCredential?: boolean, credential?: Credential, model?: CustomModelCredential) => {
|
||||
const configFrom = useMemo(() => {
|
||||
if (providerFormSchemaPredefined)
|
||||
return 'predefined-model'
|
||||
return 'custom-model'
|
||||
}, [providerFormSchemaPredefined])
|
||||
const {
|
||||
isLoading,
|
||||
data: credentialData = {},
|
||||
} = useGetCredential(provider.provider, isModelCredential, credential?.credential_id, model, configFrom)
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
credentialData,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useMemo } from 'react'
|
||||
import type {
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
|
||||
export const useCredentialStatus = (provider: ModelProvider) => {
|
||||
const {
|
||||
current_credential_id,
|
||||
current_credential_name,
|
||||
available_credentials,
|
||||
} = provider.custom_configuration
|
||||
const hasCredential = !!available_credentials?.length
|
||||
const authorized = current_credential_id && current_credential_name
|
||||
const authRemoved = hasCredential && !current_credential_id && !current_credential_name
|
||||
const currentCredential = available_credentials?.find(credential => credential.credential_id === current_credential_id)
|
||||
|
||||
return useMemo(() => ({
|
||||
hasCredential,
|
||||
authorized,
|
||||
authRemoved,
|
||||
current_credential_id,
|
||||
current_credential_name,
|
||||
available_credentials,
|
||||
notAllowedToUse: currentCredential?.not_allowed_to_use,
|
||||
}), [hasCredential, authorized, authRemoved, current_credential_id, current_credential_name, available_credentials])
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type {
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
|
||||
export const useCustomModels = (provider: ModelProvider) => {
|
||||
const { custom_models } = provider.custom_configuration
|
||||
|
||||
return custom_models || []
|
||||
}
|
||||
|
||||
export const useCanAddedModels = (provider: ModelProvider) => {
|
||||
const { can_added_models } = provider.custom_configuration
|
||||
|
||||
return can_added_models || []
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModelCredential,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import {
|
||||
genModelNameFormSchema,
|
||||
genModelTypeFormSchema,
|
||||
} from '../../utils'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
|
||||
export const useModelFormSchemas = (
|
||||
provider: ModelProvider,
|
||||
providerFormSchemaPredefined: boolean,
|
||||
credentials?: Record<string, any>,
|
||||
credential?: Credential,
|
||||
model?: CustomModelCredential,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
provider_credential_schema,
|
||||
supported_model_types,
|
||||
model_credential_schema,
|
||||
} = provider
|
||||
const formSchemas = useMemo(() => {
|
||||
return providerFormSchemaPredefined
|
||||
? provider_credential_schema.credential_form_schemas
|
||||
: model_credential_schema.credential_form_schemas
|
||||
}, [
|
||||
providerFormSchemaPredefined,
|
||||
provider_credential_schema?.credential_form_schemas,
|
||||
supported_model_types,
|
||||
model_credential_schema?.credential_form_schemas,
|
||||
model_credential_schema?.model,
|
||||
model,
|
||||
])
|
||||
|
||||
const formSchemasWithAuthorizationName = useMemo(() => {
|
||||
const authorizationNameSchema = {
|
||||
type: FormTypeEnum.textInput,
|
||||
variable: '__authorization_name__',
|
||||
label: t('plugin.auth.authorizationName'),
|
||||
required: false,
|
||||
}
|
||||
|
||||
return [
|
||||
authorizationNameSchema,
|
||||
...formSchemas,
|
||||
]
|
||||
}, [formSchemas, t])
|
||||
|
||||
const formValues = useMemo(() => {
|
||||
let result: any = {}
|
||||
formSchemas.forEach((schema) => {
|
||||
result[schema.variable] = schema.default
|
||||
})
|
||||
if (credential) {
|
||||
result = { ...result, __authorization_name__: credential?.credential_name }
|
||||
if (credentials)
|
||||
result = { ...result, ...credentials }
|
||||
}
|
||||
if (model)
|
||||
result = { ...result, __model_name: model?.model, __model_type: model?.model_type }
|
||||
return result
|
||||
}, [credentials, credential, model, formSchemas])
|
||||
|
||||
const modelNameAndTypeFormSchemas = useMemo(() => {
|
||||
if (providerFormSchemaPredefined)
|
||||
return []
|
||||
|
||||
const modelNameSchema = genModelNameFormSchema(model_credential_schema?.model)
|
||||
const modelTypeSchema = genModelTypeFormSchema(supported_model_types)
|
||||
return [
|
||||
modelNameSchema,
|
||||
modelTypeSchema,
|
||||
]
|
||||
}, [supported_model_types, model_credential_schema?.model, providerFormSchemaPredefined])
|
||||
|
||||
const modelNameAndTypeFormValues = useMemo(() => {
|
||||
let result = {}
|
||||
if (providerFormSchemaPredefined)
|
||||
return result
|
||||
|
||||
if (model)
|
||||
result = { ...result, __model_name: model?.model, __model_type: model?.model_type }
|
||||
|
||||
return result
|
||||
}, [model, providerFormSchemaPredefined])
|
||||
|
||||
return {
|
||||
formSchemas: formSchemasWithAuthorizationName,
|
||||
formValues,
|
||||
modelNameAndTypeFormSchemas,
|
||||
modelNameAndTypeFormValues,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { default as Authorized } from './authorized'
|
||||
export { default as SwitchCredentialInLoadBalancing } from './switch-credential-in-load-balancing'
|
||||
export { default as AddCredentialInLoadBalancing } from './add-credential-in-load-balancing'
|
||||
export { default as AddCustomModel } from './add-custom-model'
|
||||
export { default as ConfigProvider } from './config-provider'
|
||||
export { default as ConfigModel } from './config-model'
|
||||
export { default as ManageCustomModelCredentials } from './manage-custom-model-credentials'
|
||||
export { default as CredentialSelector } from './credential-selector'
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Button,
|
||||
} from '@/app/components/base/button'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
ModelModalModeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Authorized from './authorized'
|
||||
import {
|
||||
useCustomModels,
|
||||
} from './hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ManageCustomModelCredentialsProps = {
|
||||
provider: ModelProvider,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
}
|
||||
const ManageCustomModelCredentials = ({
|
||||
provider,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
}: ManageCustomModelCredentialsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const customModels = useCustomModels(provider)
|
||||
const noModels = !customModels.length
|
||||
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
const Item = (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='small'
|
||||
className={cn(
|
||||
'mr-0.5 text-text-tertiary',
|
||||
open && 'bg-components-button-ghost-bg-hover',
|
||||
)}
|
||||
>
|
||||
{t('common.modelProvider.auth.manageCredentials')}
|
||||
</Button>
|
||||
)
|
||||
return Item
|
||||
}, [t])
|
||||
|
||||
if (noModels)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
currentCustomConfigurationModelFixedFields={currentCustomConfigurationModelFixedFields}
|
||||
items={customModels.map(model => ({
|
||||
model,
|
||||
credentials: model.available_model_credentials ?? [],
|
||||
selectedCredential: model.current_credential_id ? {
|
||||
credential_id: model.current_credential_id,
|
||||
credential_name: model.current_credential_name,
|
||||
} : undefined,
|
||||
}))}
|
||||
renderTrigger={renderTrigger}
|
||||
authParams={{
|
||||
isModelCredential: true,
|
||||
mode: ModelModalModeEnum.configModelCredential,
|
||||
}}
|
||||
hideAddAction
|
||||
disableItemClick
|
||||
popupTitle={t('common.modelProvider.auth.customModelCredentials')}
|
||||
showModelTitle
|
||||
disableDeleteButShowAction
|
||||
disableDeleteTip={t('common.modelProvider.auth.customModelCredentialsDeleteTip')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ManageCustomModelCredentials)
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Authorized from './authorized'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModel,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import cn from '@/utils/classnames'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
type SwitchCredentialInLoadBalancingProps = {
|
||||
provider: ModelProvider
|
||||
model: CustomModel
|
||||
credentials?: Credential[]
|
||||
customModelCredential?: Credential
|
||||
setCustomModelCredential: Dispatch<SetStateAction<Credential | undefined>>
|
||||
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
|
||||
onRemove?: (credentialId: string) => void
|
||||
}
|
||||
const SwitchCredentialInLoadBalancing = ({
|
||||
provider,
|
||||
model,
|
||||
customModelCredential,
|
||||
setCustomModelCredential,
|
||||
credentials,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}: SwitchCredentialInLoadBalancingProps) => {
|
||||
const { t } = useTranslation()
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
const handleItemClick = useCallback((credential: Credential) => {
|
||||
setCustomModelCredential(credential)
|
||||
}, [setCustomModelCredential])
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
const selectedCredentialId = customModelCredential?.credential_id
|
||||
const currentCredential = credentials?.find(c => c.credential_id === selectedCredentialId)
|
||||
const empty = !credentials?.length
|
||||
const authRemoved = selectedCredentialId && !currentCredential && !empty
|
||||
const unavailable = currentCredential?.not_allowed_to_use
|
||||
|
||||
let color = 'green'
|
||||
if (authRemoved || unavailable)
|
||||
color = 'red'
|
||||
|
||||
const Item = (
|
||||
<Button
|
||||
variant='secondary'
|
||||
className={cn(
|
||||
'shrink-0 space-x-1',
|
||||
(authRemoved || unavailable) && 'text-components-button-destructive-secondary-text',
|
||||
empty && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
>
|
||||
{
|
||||
!empty && (
|
||||
<Indicator
|
||||
className='mr-2'
|
||||
color={color as any}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
authRemoved && t('common.modelProvider.auth.authRemoved')
|
||||
}
|
||||
{
|
||||
(unavailable || empty) && t('plugin.auth.credentialUnavailableInButton')
|
||||
}
|
||||
{
|
||||
!authRemoved && !unavailable && !empty && customModelCredential?.credential_name
|
||||
}
|
||||
{
|
||||
currentCredential?.from_enterprise && (
|
||||
<Badge className='ml-2'>Enterprise</Badge>
|
||||
)
|
||||
}
|
||||
<RiArrowDownSLine className='h-4 w-4' />
|
||||
</Button>
|
||||
)
|
||||
if (empty && notAllowCustomCredential) {
|
||||
return (
|
||||
<Tooltip
|
||||
asChild
|
||||
popupContent={t('plugin.auth.credentialUnavailable')}
|
||||
>
|
||||
{Item}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return Item
|
||||
}, [customModelCredential, t, credentials, notAllowCustomCredential])
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
currentCustomConfigurationModelFixedFields={model ? {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
} : undefined}
|
||||
authParams={{
|
||||
isModelCredential: true,
|
||||
mode: ModelModalModeEnum.configModelCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
model,
|
||||
credentials: credentials || [],
|
||||
selectedCredential: customModelCredential ? {
|
||||
credential_id: customModelCredential?.credential_id || '',
|
||||
credential_name: customModelCredential?.credential_name || '',
|
||||
} : undefined,
|
||||
},
|
||||
]}
|
||||
renderTrigger={renderTrigger}
|
||||
onItemClick={handleItemClick}
|
||||
enableAddModelCredential
|
||||
showItemSelectedIcon
|
||||
popupTitle={t('common.modelProvider.auth.modelCredentials')}
|
||||
triggerOnlyOpenModal={!credentials?.length}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SwitchCredentialInLoadBalancing)
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type ModelBadgeProps = {
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
const ModelBadge: FC<ModelBadgeProps> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className={classNames(
|
||||
'system-2xs-medium-uppercase flex h-[18px] cursor-default items-center rounded-[5px] border border-divider-deep px-1 text-text-tertiary',
|
||||
className,
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelBadge
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
Model,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import { OpenaiBlue, OpenaiTeal, OpenaiViolet, OpenaiYellow } from '@/app/components/base/icons/src/public/llm'
|
||||
import cn from '@/utils/classnames'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
|
||||
type ModelIconProps = {
|
||||
provider?: Model | ModelProvider
|
||||
modelName?: string
|
||||
className?: string
|
||||
iconClassName?: string
|
||||
isDeprecated?: boolean
|
||||
}
|
||||
const ModelIcon: FC<ModelIconProps> = ({
|
||||
provider,
|
||||
className,
|
||||
modelName,
|
||||
iconClassName,
|
||||
isDeprecated = false,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o'))
|
||||
return <div className='flex items-center justify-center'><OpenaiYellow className={cn('h-5 w-5', className)} /></div>
|
||||
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4.1'))
|
||||
return <div className='flex items-center justify-center'><OpenaiTeal className={cn('h-5 w-5', className)} /></div>
|
||||
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4o'))
|
||||
return <div className='flex items-center justify-center'><OpenaiBlue className={cn('h-5 w-5', className)} /></div>
|
||||
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('gpt-4'))
|
||||
return <div className='flex items-center justify-center'><OpenaiViolet className={cn('h-5 w-5', className)} /></div>
|
||||
|
||||
if (provider?.icon_small) {
|
||||
return (
|
||||
<div className={cn('flex h-5 w-5 items-center justify-center', isDeprecated && 'opacity-50', className)}>
|
||||
<img alt='model-icon' src={renderI18nObject(provider.icon_small, language)} className={iconClassName} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex h-5 w-5 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle',
|
||||
className,
|
||||
)}>
|
||||
<div className={cn('flex h-5 w-5 items-center justify-center opacity-35', iconClassName)}>
|
||||
<Group className='h-3 w-3 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelIcon
|
||||
@@ -0,0 +1,460 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { ValidatingTip } from '../../key-validator/ValidateStatus'
|
||||
import type {
|
||||
CredentialFormSchema,
|
||||
CredentialFormSchemaNumberInput,
|
||||
CredentialFormSchemaRadio,
|
||||
CredentialFormSchemaSecretInput,
|
||||
CredentialFormSchemaSelect,
|
||||
CredentialFormSchemaTextInput,
|
||||
FormValue,
|
||||
} from '../declarations'
|
||||
import { FormTypeEnum } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import Input from './Input'
|
||||
import cn from '@/utils/classnames'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
|
||||
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
|
||||
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
|
||||
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import RadioE from '@/app/components/base/radio/ui'
|
||||
import type {
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { Node } from 'reactflow'
|
||||
|
||||
type FormProps<
|
||||
CustomFormSchema extends Omit<CredentialFormSchema, 'type'> & { type: string } = never,
|
||||
> = {
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
fieldLabelClassName?: string
|
||||
value: FormValue
|
||||
onChange: (val: FormValue) => void
|
||||
formSchemas: Array<CredentialFormSchema | CustomFormSchema>
|
||||
validating: boolean
|
||||
validatedSuccess?: boolean
|
||||
showOnVariableMap: Record<string, string[]>
|
||||
isEditMode: boolean
|
||||
isAgentStrategy?: boolean
|
||||
readonly?: boolean
|
||||
inputClassName?: string
|
||||
isShowDefaultValue?: boolean
|
||||
fieldMoreInfo?: (payload: CredentialFormSchema | CustomFormSchema) => ReactNode
|
||||
customRenderField?: (
|
||||
formSchema: CustomFormSchema,
|
||||
props: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'>,
|
||||
) => ReactNode,
|
||||
// If return falsy value, this field will fallback to default render
|
||||
override?: [Array<FormTypeEnum>, (formSchema: CredentialFormSchema, props: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'>) => ReactNode]
|
||||
nodeId?: string
|
||||
nodeOutputVars?: NodeOutPutVar[],
|
||||
availableNodes?: Node[],
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
function Form<
|
||||
CustomFormSchema extends Omit<CredentialFormSchema, 'type'> & { type: string } = never,
|
||||
>({
|
||||
className,
|
||||
itemClassName,
|
||||
fieldLabelClassName,
|
||||
value,
|
||||
onChange,
|
||||
formSchemas,
|
||||
validating,
|
||||
validatedSuccess,
|
||||
showOnVariableMap,
|
||||
isEditMode,
|
||||
isAgentStrategy = false,
|
||||
readonly,
|
||||
inputClassName,
|
||||
isShowDefaultValue = false,
|
||||
fieldMoreInfo,
|
||||
customRenderField,
|
||||
override,
|
||||
nodeId,
|
||||
nodeOutputVars,
|
||||
availableNodes,
|
||||
canChooseMCPTool,
|
||||
}: FormProps<CustomFormSchema>) {
|
||||
const language = useLanguage()
|
||||
const [changeKey, setChangeKey] = useState('')
|
||||
const filteredProps: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'> = {
|
||||
className,
|
||||
itemClassName,
|
||||
fieldLabelClassName,
|
||||
value,
|
||||
onChange,
|
||||
formSchemas,
|
||||
validating,
|
||||
validatedSuccess,
|
||||
showOnVariableMap,
|
||||
isEditMode,
|
||||
readonly,
|
||||
inputClassName,
|
||||
isShowDefaultValue,
|
||||
fieldMoreInfo,
|
||||
}
|
||||
|
||||
const handleFormChange = (key: string, val: string | boolean) => {
|
||||
if (isEditMode && (key === '__model_type' || key === '__model_name'))
|
||||
return
|
||||
|
||||
setChangeKey(key)
|
||||
const shouldClearVariable: Record<string, string | undefined> = {}
|
||||
if (showOnVariableMap[key]?.length) {
|
||||
showOnVariableMap[key].forEach((clearVariable) => {
|
||||
const schema = formSchemas.find(it => it.variable === clearVariable)
|
||||
shouldClearVariable[clearVariable] = schema ? schema.default : undefined
|
||||
})
|
||||
}
|
||||
onChange({ ...value, [key]: val, ...shouldClearVariable })
|
||||
}
|
||||
|
||||
const handleModelChanged = useCallback((key: string, model: any) => {
|
||||
const newValue = {
|
||||
...value[key],
|
||||
...model,
|
||||
type: FormTypeEnum.modelSelector,
|
||||
}
|
||||
onChange({ ...value, [key]: newValue })
|
||||
}, [onChange, value])
|
||||
|
||||
const renderField = (formSchema: CredentialFormSchema | CustomFormSchema) => {
|
||||
const tooltip = formSchema.tooltip
|
||||
const tooltipContent = (tooltip && (
|
||||
<Tooltip
|
||||
popupContent={<div className='w-[200px]'>
|
||||
{tooltip[language] || tooltip.en_US}
|
||||
</div>}
|
||||
triggerClassName='ml-1 w-4 h-4'
|
||||
asChild={false} />
|
||||
))
|
||||
if (override) {
|
||||
const [overrideTypes, overrideRender] = override
|
||||
if (overrideTypes.includes(formSchema.type as FormTypeEnum)) {
|
||||
const node = overrideRender(formSchema as CredentialFormSchema, filteredProps)
|
||||
if (node)
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.textInput || formSchema.type === FormTypeEnum.secretInput || formSchema.type === FormTypeEnum.textNumber) {
|
||||
const {
|
||||
variable, label, placeholder, required, show_on,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
|
||||
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return null
|
||||
|
||||
const disabled = readonly || (isEditMode && (variable === '__model_type' || variable === '__model_name'))
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<Input
|
||||
className={cn(inputClassName, `${disabled && 'cursor-not-allowed opacity-60'}`)}
|
||||
value={(isShowDefaultValue && ((value[variable] as string) === '' || value[variable] === undefined || value[variable] === null)) ? formSchema.default : value[variable]}
|
||||
onChange={val => handleFormChange(variable, val)}
|
||||
validated={validatedSuccess}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
disabled={disabled}
|
||||
type={formSchema.type === FormTypeEnum.secretInput ? 'password'
|
||||
: formSchema.type === FormTypeEnum.textNumber ? 'number'
|
||||
: 'text'}
|
||||
{...(formSchema.type === FormTypeEnum.textNumber ? { min: (formSchema as CredentialFormSchemaNumberInput).min, max: (formSchema as CredentialFormSchemaNumberInput).max } : {})} />
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.radio) {
|
||||
const {
|
||||
options, variable, label, show_on, required,
|
||||
} = formSchema as CredentialFormSchemaRadio
|
||||
|
||||
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return null
|
||||
|
||||
const disabled = isEditMode && (variable === '__model_type' || variable === '__model_name')
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<div className={cn('grid gap-3', `grid-cols-${options?.length}`)}>
|
||||
{options.filter((option) => {
|
||||
if (option.show_on.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map(option => (
|
||||
<div
|
||||
className={`
|
||||
flex cursor-pointer items-center gap-2 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg px-3 py-2
|
||||
${value[variable] === option.value && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-sm'}
|
||||
${disabled && '!cursor-not-allowed opacity-60'}
|
||||
`}
|
||||
onClick={() => handleFormChange(variable, option.value)}
|
||||
key={`${variable}-${option.value}`}
|
||||
>
|
||||
<RadioE isChecked={value[variable] === option.value} />
|
||||
|
||||
<div className='system-sm-regular text-text-secondary'>{option.label[language] || option.label.en_US}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.select) {
|
||||
const {
|
||||
options, variable, label, show_on, required, placeholder,
|
||||
} = formSchema as CredentialFormSchemaSelect
|
||||
|
||||
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return null
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<SimpleSelect
|
||||
wrapperClassName='h-8'
|
||||
className={cn(inputClassName)}
|
||||
disabled={readonly}
|
||||
defaultValue={(isShowDefaultValue && ((value[variable] as string) === '' || value[variable] === undefined || value[variable] === null)) ? formSchema.default : value[variable]}
|
||||
items={options.filter((option) => {
|
||||
if (option.show_on.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
|
||||
onSelect={item => handleFormChange(variable, item.value as string)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US} />
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.checkbox) {
|
||||
const {
|
||||
variable, label, show_on, required,
|
||||
} = formSchema as CredentialFormSchemaRadio
|
||||
|
||||
if (show_on.length && !show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return null
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<span className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>{label[language] || label.en_US}</span>
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<Radio.Group
|
||||
className='flex items-center'
|
||||
value={value[variable]}
|
||||
onChange={val => handleFormChange(variable, val)}
|
||||
>
|
||||
<Radio value={true} className='!mr-1'>True</Radio>
|
||||
<Radio value={false}>False</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.modelSelector) {
|
||||
const {
|
||||
variable, label, required, scope,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<ModelParameterModal
|
||||
popupClassName='!w-[387px]'
|
||||
isAdvancedMode
|
||||
isInWorkflow
|
||||
isAgentStrategy={isAgentStrategy}
|
||||
value={value[variable]}
|
||||
setModel={model => handleModelChanged(variable, model)}
|
||||
readonly={readonly}
|
||||
scope={scope} />
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.toolSelector) {
|
||||
const {
|
||||
variable,
|
||||
label,
|
||||
required,
|
||||
scope,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<ToolSelector
|
||||
scope={scope}
|
||||
nodeId={nodeId}
|
||||
nodeOutputVars={nodeOutputVars || []}
|
||||
availableNodes={availableNodes || []}
|
||||
disabled={readonly}
|
||||
value={value[variable]}
|
||||
// selectedTools={value[variable] ? [value[variable]] : []}
|
||||
onSelect={item => handleFormChange(variable, item as any)}
|
||||
onDelete={() => handleFormChange(variable, null as any)}
|
||||
/>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.multiToolSelector) {
|
||||
const {
|
||||
variable,
|
||||
label,
|
||||
tooltip,
|
||||
required,
|
||||
scope,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<MultipleToolSelector
|
||||
disabled={readonly}
|
||||
nodeId={nodeId}
|
||||
nodeOutputVars={nodeOutputVars || []}
|
||||
availableNodes={availableNodes || []}
|
||||
scope={scope}
|
||||
label={label[language] || label.en_US}
|
||||
required={required}
|
||||
tooltip={tooltip?.[language] || tooltip?.en_US}
|
||||
value={value[variable] || []}
|
||||
onChange={item => handleFormChange(variable, item as any)}
|
||||
supportCollapse
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.appSelector) {
|
||||
const {
|
||||
variable, label, required, scope,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<AppSelector
|
||||
disabled={readonly}
|
||||
scope={scope}
|
||||
value={value[variable]}
|
||||
onSelect={item => handleFormChange(variable, { ...item, type: FormTypeEnum.appSelector } as any)} />
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.any) {
|
||||
const {
|
||||
variable, label, required, scope,
|
||||
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
<VarReferencePicker
|
||||
zIndex={1001}
|
||||
readonly={false}
|
||||
isShowNodeName
|
||||
nodeId={nodeId || ''}
|
||||
value={value[variable] || []}
|
||||
onChange={item => handleFormChange(variable, item as any)}
|
||||
filterVar={(varPayload) => {
|
||||
if (!scope) return true
|
||||
return scope.split('&').includes(varPayload.type)
|
||||
}}
|
||||
/>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error it work
|
||||
if (!Object.values(FormTypeEnum).includes(formSchema.type))
|
||||
return customRenderField?.(formSchema as CustomFormSchema, filteredProps)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{formSchemas.map(formSchema => renderField(formSchema))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@@ -0,0 +1,16 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import Input from './Input'
|
||||
|
||||
test('Input renders correctly as password type with no autocomplete', () => {
|
||||
const { asFragment, getByPlaceholderText } = render(
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="API Key"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
const input = getByPlaceholderText('API Key')
|
||||
expect(input).toHaveAttribute('type', 'password')
|
||||
expect(input).not.toHaveAttribute('autocomplete')
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { FC } from 'react'
|
||||
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
|
||||
type InputProps = {
|
||||
value?: string
|
||||
onChange: (v: string) => void
|
||||
onFocus?: () => void
|
||||
placeholder?: string
|
||||
validated?: boolean
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
type?: string
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
|
||||
const Input: FC<InputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
placeholder,
|
||||
validated,
|
||||
className,
|
||||
disabled,
|
||||
type = 'text',
|
||||
min,
|
||||
max,
|
||||
}) => {
|
||||
const toLimit = (v: string) => {
|
||||
const minNum = Number.parseFloat(`${min}`)
|
||||
const maxNum = Number.parseFloat(`${max}`)
|
||||
if (!isNaN(minNum) && Number.parseFloat(v) < minNum) {
|
||||
onChange(`${min}`)
|
||||
return
|
||||
}
|
||||
if (!isNaN(maxNum) && Number.parseFloat(v) > maxNum)
|
||||
onChange(`${max}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<input
|
||||
tabIndex={0}
|
||||
// Do not set autoComplete for security - prevents browser from storing sensitive API keys
|
||||
className={`
|
||||
block h-8 w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-sm
|
||||
text-components-input-text-filled caret-primary-600 outline-none
|
||||
placeholder:text-sm placeholder:text-text-tertiary
|
||||
hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active
|
||||
focus:bg-components-input-bg-active focus:shadow-xs
|
||||
${validated ? 'pr-[30px]' : ''}
|
||||
${className || ''}
|
||||
`}
|
||||
placeholder={placeholder || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onBlur={e => toLimit(e.target.value)}
|
||||
onFocus={onFocus}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
{validated && (
|
||||
<div className='absolute right-2.5 top-2.5'>
|
||||
<CheckCircle className='h-4 w-4 text-[#039855]' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
||||
@@ -0,0 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Input renders correctly as password type with no autocomplete 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="relative"
|
||||
>
|
||||
<input
|
||||
class="
|
||||
block h-8 w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-sm
|
||||
text-components-input-text-filled caret-primary-600 outline-none
|
||||
placeholder:text-sm placeholder:text-text-tertiary
|
||||
hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active
|
||||
focus:bg-components-input-bg-active focus:shadow-xs
|
||||
|
||||
|
||||
"
|
||||
placeholder="API Key"
|
||||
tabindex="0"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -0,0 +1,449 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
FormTypeEnum,
|
||||
ModelModalModeEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
useLanguage,
|
||||
} from '../hooks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
|
||||
import type {
|
||||
FormRefObject,
|
||||
FormSchema,
|
||||
} from '@/app/components/base/form/types'
|
||||
import { useModelFormSchemas } from '../model-auth/hooks'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModel,
|
||||
} from '../declarations'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import {
|
||||
useAuth,
|
||||
useCredentialData,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { CredentialSelector } from '../model-auth'
|
||||
|
||||
type ModelModalProps = {
|
||||
provider: ModelProvider
|
||||
configurateMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
onCancel: () => void
|
||||
onSave: (formValues?: Record<string, any>) => void
|
||||
onRemove: (formValues?: Record<string, any>) => void
|
||||
model?: CustomModel
|
||||
credential?: Credential
|
||||
isModelCredential?: boolean
|
||||
mode?: ModelModalModeEnum
|
||||
}
|
||||
|
||||
const ModelModal: FC<ModelModalProps> = ({
|
||||
provider,
|
||||
configurateMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
onCancel,
|
||||
onSave,
|
||||
model,
|
||||
credential,
|
||||
isModelCredential,
|
||||
mode = ModelModalModeEnum.configProviderCredential,
|
||||
}) => {
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
|
||||
const {
|
||||
isLoading,
|
||||
credentialData,
|
||||
} = useCredentialData(provider, providerFormSchemaPredefined, isModelCredential, credential, model)
|
||||
const {
|
||||
handleSaveCredential,
|
||||
handleConfirmDelete,
|
||||
deleteCredentialId,
|
||||
closeConfirmDelete,
|
||||
openConfirmDelete,
|
||||
doingAction,
|
||||
handleActiveCredential,
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurateMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential,
|
||||
mode,
|
||||
},
|
||||
)
|
||||
const {
|
||||
credentials: formSchemasValue,
|
||||
available_credentials,
|
||||
} = credentialData as any
|
||||
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const {
|
||||
formSchemas,
|
||||
formValues,
|
||||
modelNameAndTypeFormSchemas,
|
||||
modelNameAndTypeFormValues,
|
||||
} = useModelFormSchemas(provider, providerFormSchemaPredefined, formSchemasValue, credential, model)
|
||||
const formRef1 = useRef<FormRefObject>(null)
|
||||
const [selectedCredential, setSelectedCredential] = useState<Credential & { addNewCredential?: boolean } | undefined>()
|
||||
const formRef2 = useRef<FormRefObject>(null)
|
||||
const isEditMode = !!credential && !!Object.keys(formSchemasValue || {}).filter((key) => {
|
||||
return key !== '__model_name' && key !== '__model_type' && !!formValues[key]
|
||||
}).length && isCurrentWorkspaceManager
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList && selectedCredential && !selectedCredential?.addNewCredential) {
|
||||
handleActiveCredential(selectedCredential, model)
|
||||
onCancel()
|
||||
return
|
||||
}
|
||||
|
||||
let modelNameAndTypeIsCheckValidated = true
|
||||
let modelNameAndTypeValues: Record<string, any> = {}
|
||||
|
||||
if (mode === ModelModalModeEnum.configCustomModel) {
|
||||
const formResult = formRef1.current?.getFormValues({
|
||||
needCheckValidatedValues: true,
|
||||
}) || { isCheckValidated: false, values: {} }
|
||||
modelNameAndTypeIsCheckValidated = formResult.isCheckValidated
|
||||
modelNameAndTypeValues = formResult.values
|
||||
}
|
||||
|
||||
if (mode === ModelModalModeEnum.configModelCredential && model) {
|
||||
modelNameAndTypeValues = {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList && selectedCredential?.addNewCredential && model) {
|
||||
modelNameAndTypeValues = {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
}
|
||||
}
|
||||
const {
|
||||
isCheckValidated,
|
||||
values,
|
||||
} = formRef2.current?.getFormValues({
|
||||
needCheckValidatedValues: true,
|
||||
needTransformWhenSecretFieldIsPristine: true,
|
||||
}) || { isCheckValidated: false, values: {} }
|
||||
if (!isCheckValidated || !modelNameAndTypeIsCheckValidated)
|
||||
return
|
||||
|
||||
const {
|
||||
__model_name,
|
||||
__model_type,
|
||||
} = modelNameAndTypeValues
|
||||
const {
|
||||
__authorization_name__,
|
||||
...rest
|
||||
} = values
|
||||
if (__model_name && __model_type) {
|
||||
await handleSaveCredential({
|
||||
credential_id: credential?.credential_id,
|
||||
credentials: rest,
|
||||
name: __authorization_name__,
|
||||
model: __model_name,
|
||||
model_type: __model_type,
|
||||
})
|
||||
}
|
||||
else {
|
||||
await handleSaveCredential({
|
||||
credential_id: credential?.credential_id,
|
||||
credentials: rest,
|
||||
name: __authorization_name__,
|
||||
})
|
||||
}
|
||||
onSave(values)
|
||||
}, [handleSaveCredential, credential?.credential_id, model, onSave, mode, selectedCredential, handleActiveCredential])
|
||||
|
||||
const modalTitle = useMemo(() => {
|
||||
let label = t('common.modelProvider.auth.apiKeyModal.title')
|
||||
|
||||
if (mode === ModelModalModeEnum.configCustomModel || mode === ModelModalModeEnum.addCustomModelToModelList)
|
||||
label = t('common.modelProvider.auth.addModel')
|
||||
if (mode === ModelModalModeEnum.configModelCredential) {
|
||||
if (credential)
|
||||
label = t('common.modelProvider.auth.editModelCredential')
|
||||
else
|
||||
label = t('common.modelProvider.auth.addModelCredential')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='title-2xl-semi-bold text-text-primary'>
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
}, [t, mode, credential])
|
||||
|
||||
const modalDesc = useMemo(() => {
|
||||
if (providerFormSchemaPredefined) {
|
||||
return (
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>
|
||||
{t('common.modelProvider.auth.apiKeyModal.desc')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [providerFormSchemaPredefined, t])
|
||||
|
||||
const modalModel = useMemo(() => {
|
||||
if (mode === ModelModalModeEnum.configCustomModel) {
|
||||
return (
|
||||
<div className='mt-2 flex items-center'>
|
||||
<ModelIcon
|
||||
className='mr-2 h-4 w-4 shrink-0'
|
||||
provider={provider}
|
||||
/>
|
||||
<div className='system-md-regular mr-1 text-text-secondary'>{renderI18nObject(provider.label)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (model && (mode === ModelModalModeEnum.configModelCredential || mode === ModelModalModeEnum.addCustomModelToModelList)) {
|
||||
return (
|
||||
<div className='mt-2 flex items-center'>
|
||||
<ModelIcon
|
||||
className='mr-2 h-4 w-4 shrink-0'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
<div className='system-md-regular mr-1 text-text-secondary'>{model.model}</div>
|
||||
<Badge>{model.model_type}</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [model, provider, mode, renderI18nObject])
|
||||
|
||||
const showCredentialLabel = useMemo(() => {
|
||||
if (mode === ModelModalModeEnum.configCustomModel)
|
||||
return true
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList)
|
||||
return selectedCredential?.addNewCredential
|
||||
}, [mode, selectedCredential])
|
||||
const showCredentialForm = useMemo(() => {
|
||||
if (mode !== ModelModalModeEnum.addCustomModelToModelList)
|
||||
return true
|
||||
return selectedCredential?.addNewCredential
|
||||
}, [mode, selectedCredential])
|
||||
const saveButtonText = useMemo(() => {
|
||||
if (mode === ModelModalModeEnum.addCustomModelToModelList || mode === ModelModalModeEnum.configCustomModel)
|
||||
return t('common.operation.add')
|
||||
return t('common.operation.save')
|
||||
}, [mode, t])
|
||||
|
||||
const handleDeleteCredential = useCallback(() => {
|
||||
handleConfirmDelete()
|
||||
onCancel()
|
||||
}, [handleConfirmDelete])
|
||||
|
||||
const handleModelNameAndTypeChange = useCallback((field: string, value: any) => {
|
||||
const {
|
||||
getForm,
|
||||
} = formRef2.current as FormRefObject || {}
|
||||
if (getForm())
|
||||
getForm()?.setFieldValue(field, value)
|
||||
}, [])
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.stopPropagation()
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown, true)
|
||||
}
|
||||
}, [onCancel])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem open>
|
||||
<PortalToFollowElemContent className='z-[60] h-full w-full'>
|
||||
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
|
||||
<div className='relative w-[640px] rounded-2xl bg-components-panel-bg shadow-xl'>
|
||||
<div
|
||||
className='absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center'
|
||||
onClick={onCancel}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='p-6 pb-3'>
|
||||
{modalTitle}
|
||||
{modalDesc}
|
||||
{modalModel}
|
||||
</div>
|
||||
<div className='max-h-[calc(100vh-320px)] overflow-y-auto px-6 py-3'>
|
||||
{
|
||||
mode === ModelModalModeEnum.configCustomModel && (
|
||||
<AuthForm
|
||||
formSchemas={modelNameAndTypeFormSchemas.map((formSchema) => {
|
||||
return {
|
||||
...formSchema,
|
||||
name: formSchema.variable,
|
||||
}
|
||||
}) as FormSchema[]}
|
||||
defaultValues={modelNameAndTypeFormValues}
|
||||
inputClassName='justify-start'
|
||||
ref={formRef1}
|
||||
onChange={handleModelNameAndTypeChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
mode === ModelModalModeEnum.addCustomModelToModelList && (
|
||||
<CredentialSelector
|
||||
credentials={available_credentials || []}
|
||||
onSelect={setSelectedCredential}
|
||||
selectedCredential={selectedCredential}
|
||||
disabled={isLoading}
|
||||
notAllowAddNewCredential={notAllowCustomCredential}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showCredentialLabel && (
|
||||
<div className='system-xs-medium-uppercase mb-3 mt-6 flex items-center text-text-tertiary'>
|
||||
{t('common.modelProvider.auth.modelCredential')}
|
||||
<div className='ml-2 h-px grow bg-gradient-to-r from-divider-regular to-background-gradient-mask-transparent' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
isLoading && (
|
||||
<div className='mt-3 flex items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isLoading
|
||||
&& showCredentialForm
|
||||
&& (
|
||||
<AuthForm
|
||||
formSchemas={formSchemas.map((formSchema) => {
|
||||
return {
|
||||
...formSchema,
|
||||
name: formSchema.variable,
|
||||
showRadioUI: formSchema.type === FormTypeEnum.radio,
|
||||
}
|
||||
}) as FormSchema[]}
|
||||
defaultValues={formValues}
|
||||
inputClassName='justify-start'
|
||||
ref={formRef2}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='flex justify-between p-6 pt-5'>
|
||||
{
|
||||
(provider.help && (provider.help.title || provider.help.url))
|
||||
? (
|
||||
<a
|
||||
href={provider.help?.url[language] || provider.help?.url.en_US}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='system-xs-regular mt-2 inline-block align-middle text-text-accent'
|
||||
onClick={e => !provider.help.url && e.preventDefault()}
|
||||
>
|
||||
{provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US}
|
||||
<LinkExternal02 className='ml-1 mt-[-2px] inline-block h-3 w-3' />
|
||||
</a>
|
||||
)
|
||||
: <div />
|
||||
}
|
||||
<div className='ml-2 flex items-center justify-end space-x-2'>
|
||||
{
|
||||
isEditMode && (
|
||||
<Button
|
||||
variant='warning'
|
||||
onClick={() => openConfirmDelete(credential, model)}
|
||||
>
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || doingAction}
|
||||
>
|
||||
{saveButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
(mode === ModelModalModeEnum.configCustomModel || mode === ModelModalModeEnum.configProviderCredential) && (
|
||||
<div className='border-t-[0.5px] border-t-divider-regular'>
|
||||
<div className='flex items-center justify-center rounded-b-2xl bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
deleteCredentialId && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('common.modelProvider.confirmDelete')}
|
||||
isDisabled={doingAction}
|
||||
onCancel={closeConfirmDelete}
|
||||
onConfirm={handleDeleteCredential}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ModelModal)
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { FC, PropsWithChildren } 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'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ModelNameProps = PropsWithChildren<{
|
||||
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,
|
||||
children,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
|
||||
if (!modelItem)
|
||||
return null
|
||||
return (
|
||||
<div className={cn('system-sm-regular flex items-center gap-0.5 overflow-hidden truncate text-ellipsis text-components-input-text-filled', className)}>
|
||||
<div
|
||||
className='truncate'
|
||||
title={modelItem.label[language] || modelItem.label.en_US}
|
||||
>
|
||||
{modelItem.label[language] || modelItem.label.en_US}
|
||||
</div>
|
||||
<div className='flex items-center gap-0.5'>
|
||||
{
|
||||
showModelType && modelItem.model_type && (
|
||||
<ModelBadge className={modelTypeClassName}>
|
||||
{modelTypeFormat(modelItem.model_type)}
|
||||
</ModelBadge>
|
||||
)
|
||||
}
|
||||
{
|
||||
modelItem.model_properties.mode && showMode && (
|
||||
<ModelBadge className={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>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelName
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { FC } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
CustomConfigurationStatusEnum,
|
||||
ModelTypeEnum,
|
||||
} from '../declarations'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import ConfigurationButton from './configuration-button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import {
|
||||
useModelModalHandler,
|
||||
useUpdateModelList,
|
||||
useUpdateModelProviders,
|
||||
} from '../hooks'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelDisplay from './model-display'
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import StatusIndicators from './status-indicators'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { useModelInList, usePluginInfo } from '@/service/use-plugins'
|
||||
|
||||
export type AgentModelTriggerProps = {
|
||||
open?: boolean
|
||||
disabled?: boolean
|
||||
currentProvider?: ModelProvider
|
||||
currentModel?: ModelItem
|
||||
providerName?: string
|
||||
modelId?: string
|
||||
hasDeprecated?: boolean
|
||||
scope?: string
|
||||
}
|
||||
|
||||
const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
|
||||
disabled,
|
||||
currentProvider,
|
||||
currentModel,
|
||||
providerName,
|
||||
modelId,
|
||||
hasDeprecated,
|
||||
scope,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modelProviders } = useProviderContext()
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const { modelProvider, needsConfiguration } = useMemo(() => {
|
||||
const modelProvider = modelProviders.find(item => item.provider === providerName)
|
||||
const needsConfiguration = modelProvider?.custom_configuration.status === CustomConfigurationStatusEnum.noConfigure && !(
|
||||
modelProvider.system_configuration.enabled === true
|
||||
&& modelProvider.system_configuration.quota_configurations.find(
|
||||
item => item.quota_type === modelProvider.system_configuration.current_quota_type,
|
||||
)
|
||||
)
|
||||
return {
|
||||
modelProvider,
|
||||
needsConfiguration,
|
||||
}
|
||||
}, [modelProviders, providerName])
|
||||
const [installed, setInstalled] = useState(false)
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
const handleOpenModal = useModelModalHandler()
|
||||
|
||||
const { data: inModelList = false } = useModelInList(currentProvider, modelId)
|
||||
const { data: pluginInfo, isLoading: isPluginLoading } = usePluginInfo(providerName)
|
||||
|
||||
if (modelId && isPluginLoading)
|
||||
return <Loading />
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group relative flex grow cursor-pointer items-center gap-[2px] rounded-lg bg-components-input-bg-normal p-1 hover:bg-state-base-hover-alt',
|
||||
)}
|
||||
>
|
||||
{modelId ? (
|
||||
<>
|
||||
<ModelIcon
|
||||
className='p-0.5'
|
||||
provider={currentProvider || modelProvider}
|
||||
modelName={currentModel?.model || modelId}
|
||||
isDeprecated={hasDeprecated}
|
||||
/>
|
||||
<ModelDisplay
|
||||
currentModel={currentModel}
|
||||
modelId={modelId}
|
||||
/>
|
||||
{needsConfiguration && (
|
||||
<ConfigurationButton
|
||||
modelProvider={modelProvider}
|
||||
handleOpenModal={handleOpenModal}
|
||||
/>
|
||||
)}
|
||||
<StatusIndicators
|
||||
needsConfiguration={needsConfiguration}
|
||||
modelProvider={!!modelProvider}
|
||||
inModelList={inModelList}
|
||||
disabled={!!disabled}
|
||||
pluginInfo={pluginInfo}
|
||||
t={t}
|
||||
/>
|
||||
{!installed && !modelProvider && pluginInfo && (
|
||||
<InstallPluginButton
|
||||
onClick={e => e.stopPropagation()}
|
||||
size={'small'}
|
||||
uniqueIdentifier={pluginInfo.latest_package_identifier}
|
||||
onSuccess={() => {
|
||||
[
|
||||
ModelTypeEnum.textGeneration,
|
||||
ModelTypeEnum.textEmbedding,
|
||||
ModelTypeEnum.rerank,
|
||||
ModelTypeEnum.moderation,
|
||||
ModelTypeEnum.speech2text,
|
||||
ModelTypeEnum.tts,
|
||||
].forEach((type: ModelTypeEnum) => {
|
||||
if (scope?.includes(type))
|
||||
updateModelList(type)
|
||||
},
|
||||
)
|
||||
updateModelProviders()
|
||||
invalidateInstalledPluginList()
|
||||
setInstalled(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{modelProvider && !disabled && !needsConfiguration && (
|
||||
<div className="flex items-center pr-1">
|
||||
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary group-hover:text-text-secondary" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex grow items-center gap-1 p-1 pl-2">
|
||||
<span className="system-sm-regular overflow-hidden text-ellipsis whitespace-nowrap text-components-input-text-placeholder">
|
||||
{t('workflow.nodes.agent.configureModel')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center pr-1">
|
||||
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary group-hover:text-text-secondary" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentModelTrigger
|
||||
@@ -0,0 +1,32 @@
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ConfigurationMethodEnum } from '../declarations'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ConfigurationButtonProps = {
|
||||
modelProvider: any
|
||||
handleOpenModal: any
|
||||
}
|
||||
|
||||
const ConfigurationButton = ({ modelProvider, handleOpenModal }: ConfigurationButtonProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
className="z-[100]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleOpenModal(modelProvider, ConfigurationMethodEnum.predefinedModel, undefined)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1 px-[3px]">
|
||||
{t('workflow.nodes.agent.notAuthorized')}
|
||||
</div>
|
||||
<div className="flex h-[14px] w-[14px] items-center justify-center">
|
||||
<div className="h-2 w-2 shrink-0 rounded-[3px] border border-components-badge-status-light-warning-border-inner
|
||||
bg-components-badge-status-light-warning-bg shadow-components-badge-status-light-warning-halo" />
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfigurationButton
|
||||
@@ -0,0 +1,247 @@
|
||||
import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
DefaultModel,
|
||||
FormValue,
|
||||
ModelParameterRule,
|
||||
} from '../declarations'
|
||||
import { ModelStatusEnum } from '../declarations'
|
||||
import ModelSelector from '../model-selector'
|
||||
import {
|
||||
useTextGenerationCurrentProviderAndModelAndModelList,
|
||||
} from '../hooks'
|
||||
import ParameterItem from './parameter-item'
|
||||
import type { ParameterValue } from './parameter-item'
|
||||
import Trigger from './trigger'
|
||||
import type { TriggerProps } from './trigger'
|
||||
import PresetsParameter from './presets-parameter'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { fetchModelParameterRules } from '@/service/common'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE, TONE_LIST } from '@/config'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
export type ModelParameterModalProps = {
|
||||
popupClassName?: string
|
||||
portalToFollowElemContentClassName?: string
|
||||
isAdvancedMode: boolean
|
||||
modelId: string
|
||||
provider: string
|
||||
setModel: (model: { modelId: string; provider: string; mode?: string; features?: string[] }) => void
|
||||
completionParams: FormValue
|
||||
onCompletionParamsChange: (newParams: FormValue) => void
|
||||
hideDebugWithMultipleModel?: boolean
|
||||
debugWithMultipleModel?: boolean
|
||||
onDebugWithMultipleModelChange?: () => void
|
||||
renderTrigger?: (v: TriggerProps) => ReactNode
|
||||
readonly?: boolean
|
||||
isInWorkflow?: boolean
|
||||
scope?: string
|
||||
}
|
||||
|
||||
const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
popupClassName,
|
||||
portalToFollowElemContentClassName,
|
||||
isAdvancedMode,
|
||||
modelId,
|
||||
provider,
|
||||
setModel,
|
||||
completionParams,
|
||||
onCompletionParamsChange,
|
||||
hideDebugWithMultipleModel,
|
||||
debugWithMultipleModel,
|
||||
onDebugWithMultipleModelChange,
|
||||
renderTrigger,
|
||||
readonly,
|
||||
isInWorkflow,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { isAPIKeySet } = useProviderContext()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { data: parameterRulesData, isLoading } = useSWR((provider && modelId) ? `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}` : null, fetchModelParameterRules)
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
activeTextGenerationModelList,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{ provider, model: modelId },
|
||||
)
|
||||
|
||||
const hasDeprecated = !currentProvider || !currentModel
|
||||
const modelDisabled = currentModel?.status !== ModelStatusEnum.active
|
||||
const disabled = !isAPIKeySet || hasDeprecated || modelDisabled
|
||||
|
||||
const parameterRules: ModelParameterRule[] = useMemo(() => {
|
||||
return parameterRulesData?.data || []
|
||||
}, [parameterRulesData])
|
||||
|
||||
const handleParamChange = (key: string, value: ParameterValue) => {
|
||||
onCompletionParamsChange({
|
||||
...completionParams,
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleChangeModel = ({ provider, model }: DefaultModel) => {
|
||||
const targetProvider = activeTextGenerationModelList.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 handleSwitch = (key: string, value: boolean, assignValue: ParameterValue) => {
|
||||
if (!value) {
|
||||
const newCompletionParams = { ...completionParams }
|
||||
delete newCompletionParams[key]
|
||||
|
||||
onCompletionParamsChange(newCompletionParams)
|
||||
}
|
||||
if (value) {
|
||||
onCompletionParamsChange({
|
||||
...completionParams,
|
||||
[key]: assignValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectPresetParameter = (toneId: number) => {
|
||||
const tone = TONE_LIST.find(tone => tone.id === toneId)
|
||||
if (tone) {
|
||||
onCompletionParamsChange({
|
||||
...completionParams,
|
||||
...tone.config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement={isInWorkflow ? 'left' : 'bottom-end'}
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
setOpen(v => !v)
|
||||
}}
|
||||
className='block'
|
||||
>
|
||||
{
|
||||
renderTrigger
|
||||
? renderTrigger({
|
||||
open,
|
||||
disabled,
|
||||
modelDisabled,
|
||||
hasDeprecated,
|
||||
currentProvider,
|
||||
currentModel,
|
||||
providerName: provider,
|
||||
modelId,
|
||||
})
|
||||
: (
|
||||
<Trigger
|
||||
disabled={disabled}
|
||||
isInWorkflow={isInWorkflow}
|
||||
modelDisabled={modelDisabled}
|
||||
hasDeprecated={hasDeprecated}
|
||||
currentProvider={currentProvider}
|
||||
currentModel={currentModel}
|
||||
providerName={provider}
|
||||
modelId={modelId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className={cn('z-[60]', portalToFollowElemContentClassName)}>
|
||||
<div className={cn(popupClassName, 'w-[389px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg')}>
|
||||
<div className={cn('max-h-[420px] overflow-y-auto p-4 pt-3')}>
|
||||
<div className='relative'>
|
||||
<div className={cn('system-sm-semibold mb-1 flex h-6 items-center text-text-secondary')}>
|
||||
{t('common.modelProvider.model').toLocaleUpperCase()}
|
||||
</div>
|
||||
<ModelSelector
|
||||
defaultModel={(provider || modelId) ? { provider, model: modelId } : undefined}
|
||||
modelList={activeTextGenerationModelList}
|
||||
onSelect={handleChangeModel}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
!!parameterRules.length && (
|
||||
<div className='my-3 h-px bg-divider-subtle' />
|
||||
)
|
||||
}
|
||||
{
|
||||
isLoading && (
|
||||
<div className='mt-5'><Loading /></div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isLoading && !!parameterRules.length && (
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<div className={cn('system-sm-semibold flex h-6 items-center text-text-secondary')}>{t('common.modelProvider.parameters')}</div>
|
||||
{
|
||||
PROVIDER_WITH_PRESET_TONE.includes(provider) && (
|
||||
<PresetsParameter onSelect={handleSelectPresetParameter} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isLoading && !!parameterRules.length && (
|
||||
[
|
||||
...parameterRules,
|
||||
...(isAdvancedMode ? [STOP_PARAMETER_RULE] : []),
|
||||
].map(parameter => (
|
||||
<ParameterItem
|
||||
key={`${modelId}-${parameter.name}`}
|
||||
parameterRule={parameter}
|
||||
value={completionParams?.[parameter.name]}
|
||||
onChange={v => handleParamChange(parameter.name, v)}
|
||||
onSwitch={(checked, assignValue) => handleSwitch(parameter.name, checked, assignValue)}
|
||||
isInWorkflow={isInWorkflow}
|
||||
/>
|
||||
))
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{!hideDebugWithMultipleModel && (
|
||||
<div
|
||||
className='bg-components-section-burn system-sm-regular flex h-[50px] cursor-pointer items-center justify-between rounded-b-xl border-t border-t-divider-subtle px-4 text-text-accent'
|
||||
onClick={() => onDebugWithMultipleModelChange?.()}
|
||||
>
|
||||
{
|
||||
debugWithMultipleModel
|
||||
? t('appDebug.debugAsSingleModel')
|
||||
: t('appDebug.debugAsMultipleModel')
|
||||
}
|
||||
<ArrowNarrowLeft className='h-3 w-3 rotate-180' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelParameterModal
|
||||
@@ -0,0 +1,25 @@
|
||||
import ModelName from '../model-name'
|
||||
|
||||
type ModelDisplayProps = {
|
||||
currentModel: any
|
||||
modelId: string
|
||||
}
|
||||
|
||||
const ModelDisplay = ({ currentModel, modelId }: ModelDisplayProps) => {
|
||||
return currentModel ? (
|
||||
<ModelName
|
||||
className="flex grow items-center gap-1 px-1 py-[3px]"
|
||||
modelItem={currentModel}
|
||||
showMode
|
||||
showFeatures
|
||||
/>
|
||||
) : (
|
||||
<div className="flex grow items-center gap-1 truncate px-1 py-[3px] opacity-50">
|
||||
<div className="system-sm-regular overflow-hidden text-ellipsis text-components-input-text-filled">
|
||||
{modelId}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelDisplay
|
||||
@@ -0,0 +1,294 @@
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { ModelParameterRule } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import { isNullOrUndefined } from '../utils'
|
||||
import cn from '@/utils/classnames'
|
||||
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
|
||||
onSwitch?: (checked: boolean, assignValue: ParameterValue) => void
|
||||
isInWorkflow?: boolean
|
||||
}
|
||||
const ParameterItem: FC<ParameterItemProps> = ({
|
||||
parameterRule,
|
||||
value,
|
||||
onChange,
|
||||
onSwitch,
|
||||
isInWorkflow,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const [localValue, setLocalValue] = useState(value)
|
||||
const numberInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const getDefaultValue = () => {
|
||||
let defaultValue: ParameterValue
|
||||
|
||||
if (parameterRule.type === 'int' || parameterRule.type === 'float')
|
||||
defaultValue = isNullOrUndefined(parameterRule.default) ? (parameterRule.min || 0) : parameterRule.default
|
||||
else if (parameterRule.type === 'string' || parameterRule.type === 'text')
|
||||
defaultValue = parameterRule.default || ''
|
||||
else if (parameterRule.type === 'boolean')
|
||||
defaultValue = !isNullOrUndefined(parameterRule.default) ? parameterRule.default : false
|
||||
else if (parameterRule.type === 'tag')
|
||||
defaultValue = !isNullOrUndefined(parameterRule.default) ? parameterRule.default : []
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
const renderValue = value ?? localValue ?? getDefaultValue()
|
||||
|
||||
const handleInputChange = (newValue: ParameterValue) => {
|
||||
setLocalValue(newValue)
|
||||
|
||||
if (onChange && (parameterRule.name === 'stop' || !isNullOrUndefined(value) || parameterRule.required))
|
||||
onChange(newValue)
|
||||
}
|
||||
|
||||
const handleNumberInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let num = +e.target.value
|
||||
|
||||
if (!isNullOrUndefined(parameterRule.max) && num > parameterRule.max!) {
|
||||
num = parameterRule.max as number
|
||||
numberInputRef.current!.value = `${num}`
|
||||
}
|
||||
|
||||
if (!isNullOrUndefined(parameterRule.min) && num < parameterRule.min!)
|
||||
num = parameterRule.min as number
|
||||
|
||||
handleInputChange(num)
|
||||
}
|
||||
|
||||
const handleNumberInputBlur = () => {
|
||||
if (numberInputRef.current)
|
||||
numberInputRef.current.value = renderValue as string
|
||||
}
|
||||
|
||||
const handleSlideChange = (num: number) => {
|
||||
if (!isNullOrUndefined(parameterRule.max) && num > parameterRule.max!) {
|
||||
handleInputChange(parameterRule.max)
|
||||
numberInputRef.current!.value = `${parameterRule.max}`
|
||||
return
|
||||
}
|
||||
|
||||
if (!isNullOrUndefined(parameterRule.min) && num < parameterRule.min!) {
|
||||
handleInputChange(parameterRule.min)
|
||||
numberInputRef.current!.value = `${parameterRule.min}`
|
||||
return
|
||||
}
|
||||
|
||||
handleInputChange(num)
|
||||
numberInputRef.current!.value = `${num}`
|
||||
}
|
||||
|
||||
const handleRadioChange = (v: boolean) => {
|
||||
handleInputChange(v)
|
||||
}
|
||||
|
||||
const handleStringInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
handleInputChange(e.target.value)
|
||||
}
|
||||
|
||||
const handleSelect = (option: { value: string | number; name: string }) => {
|
||||
handleInputChange(option.value)
|
||||
}
|
||||
|
||||
const handleTagChange = (newSequences: string[]) => {
|
||||
handleInputChange(newSequences)
|
||||
}
|
||||
|
||||
const handleSwitch = (checked: boolean) => {
|
||||
if (onSwitch) {
|
||||
const assignValue: ParameterValue = localValue || getDefaultValue()
|
||||
|
||||
onSwitch(checked, assignValue)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if ((parameterRule.type === 'int' || parameterRule.type === 'float') && numberInputRef.current)
|
||||
numberInputRef.current.value = `${renderValue}`
|
||||
}, [value])
|
||||
|
||||
const renderInput = () => {
|
||||
const numberInputWithSlide = (parameterRule.type === 'int' || parameterRule.type === 'float')
|
||||
&& !isNullOrUndefined(parameterRule.min)
|
||||
&& !isNullOrUndefined(parameterRule.max)
|
||||
|
||||
if (parameterRule.type === 'int') {
|
||||
let step = 100
|
||||
if (parameterRule.max) {
|
||||
if (parameterRule.max < 100)
|
||||
step = 1
|
||||
else if (parameterRule.max < 1000)
|
||||
step = 10
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{numberInputWithSlide && <Slider
|
||||
className='w-[120px]'
|
||||
value={renderValue as number}
|
||||
min={parameterRule.min}
|
||||
max={parameterRule.max}
|
||||
step={step}
|
||||
onChange={handleSlideChange}
|
||||
/>}
|
||||
<input
|
||||
ref={numberInputRef}
|
||||
className='system-sm-regular ml-4 block h-8 w-16 shrink-0 appearance-none rounded-lg bg-components-input-bg-normal pl-3 text-components-input-text-filled outline-none'
|
||||
type='number'
|
||||
max={parameterRule.max}
|
||||
min={parameterRule.min}
|
||||
step={numberInputWithSlide ? step : +`0.${parameterRule.precision || 0}`}
|
||||
onChange={handleNumberInputChange}
|
||||
onBlur={handleNumberInputBlur}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (parameterRule.type === 'float') {
|
||||
return (
|
||||
<>
|
||||
{numberInputWithSlide && <Slider
|
||||
className='w-[120px]'
|
||||
value={renderValue as number}
|
||||
min={parameterRule.min}
|
||||
max={parameterRule.max}
|
||||
step={0.1}
|
||||
onChange={handleSlideChange}
|
||||
/>}
|
||||
<input
|
||||
ref={numberInputRef}
|
||||
className='system-sm-regular ml-4 block h-8 w-16 shrink-0 appearance-none rounded-lg bg-components-input-bg-normal pl-3 text-components-input-text-filled outline-none'
|
||||
type='number'
|
||||
max={parameterRule.max}
|
||||
min={parameterRule.min}
|
||||
step={numberInputWithSlide ? 0.1 : +`0.${parameterRule.precision || 0}`}
|
||||
onChange={handleNumberInputChange}
|
||||
onBlur={handleNumberInputBlur}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (parameterRule.type === 'boolean') {
|
||||
return (
|
||||
<Radio.Group
|
||||
className='flex w-[150px] items-center'
|
||||
value={renderValue as boolean}
|
||||
onChange={handleRadioChange}
|
||||
>
|
||||
<Radio value={true} className='w-[70px] px-[18px]'>True</Radio>
|
||||
<Radio value={false} className='w-[70px] px-[18px]'>False</Radio>
|
||||
</Radio.Group>
|
||||
)
|
||||
}
|
||||
|
||||
if (parameterRule.type === 'string' && !parameterRule.options?.length) {
|
||||
return (
|
||||
<input
|
||||
className={cn(isInWorkflow ? 'w-[150px]' : 'w-full', 'system-sm-regular ml-4 flex h-8 appearance-none items-center rounded-lg bg-components-input-bg-normal px-3 text-components-input-text-filled outline-none')}
|
||||
value={renderValue as string}
|
||||
onChange={handleStringInputChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (parameterRule.type === 'text') {
|
||||
return (
|
||||
<textarea
|
||||
className='system-sm-regular ml-4 h-20 w-full rounded-lg bg-components-input-bg-normal px-1 text-components-input-text-filled'
|
||||
value={renderValue as string}
|
||||
onChange={handleStringInputChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (parameterRule.type === 'string' && !!parameterRule?.options?.length) {
|
||||
return (
|
||||
<SimpleSelect
|
||||
className='!py-0'
|
||||
wrapperClassName={cn('!h-8 w-full')}
|
||||
defaultValue={renderValue as string}
|
||||
onSelect={handleSelect}
|
||||
items={parameterRule.options.map(option => ({ value: option, name: option }))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (parameterRule.type === 'tag') {
|
||||
return (
|
||||
<div className={cn('!h-8 w-full')}>
|
||||
<TagInput
|
||||
items={renderValue as string[]}
|
||||
onChange={handleTagChange}
|
||||
customizedConfirmKey='Tab'
|
||||
isInWorkflow={isInWorkflow}
|
||||
required={parameterRule.required}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<div className='shrink-0 basis-1/2'>
|
||||
<div className={cn('flex w-full shrink-0 items-center')}>
|
||||
{
|
||||
!parameterRule.required && parameterRule.name !== 'stop' && (
|
||||
<div className='mr-2 w-7'>
|
||||
<Switch
|
||||
defaultValue={!isNullOrUndefined(value)}
|
||||
onChange={handleSwitch}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className='system-xs-regular mr-0.5 truncate text-text-secondary'
|
||||
title={parameterRule.label[language] || parameterRule.label.en_US}
|
||||
>
|
||||
{parameterRule.label[language] || parameterRule.label.en_US}
|
||||
</div>
|
||||
{
|
||||
parameterRule.help && (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className='w-[150px] whitespace-pre-wrap'>{parameterRule.help[language] || parameterRule.help.en_US}</div>
|
||||
)}
|
||||
popupClassName='mr-1'
|
||||
triggerClassName='mr-1 w-4 h-4 shrink-0'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
parameterRule.type === 'tag' && (
|
||||
<div className={cn(!isInWorkflow && 'w-[150px]', 'system-xs-regular text-text-tertiary')}>
|
||||
{parameterRule?.tagPlaceholder?.[language]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{renderInput()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ParameterItem
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Dropdown from '@/app/components/base/dropdown'
|
||||
import { Brush01 } from '@/app/components/base/icons/src/vender/solid/editor'
|
||||
import { Scales02 } from '@/app/components/base/icons/src/vender/solid/FinanceAndECommerce'
|
||||
import { Target04 } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { TONE_LIST } from '@/config'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type PresetsParameterProps = {
|
||||
onSelect: (toneId: number) => void
|
||||
}
|
||||
const PresetsParameter: FC<PresetsParameterProps> = ({
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const renderTrigger = useCallback((open: boolean) => {
|
||||
return (
|
||||
<Button
|
||||
size={'small'}
|
||||
variant={'secondary'}
|
||||
className={cn(open && 'bg-state-base-hover')}
|
||||
>
|
||||
{t('common.modelProvider.loadPresets')}
|
||||
<RiArrowDownSLine className='ml-0.5 h-3.5 w-3.5' />
|
||||
</Button>
|
||||
)
|
||||
}, [t])
|
||||
const getToneIcon = (toneId: number) => {
|
||||
const className = 'mr-2 w-[14px] h-[14px]'
|
||||
const res = ({
|
||||
1: <Brush01 className={`${className} text-[#6938EF]`} />,
|
||||
2: <Scales02 className={`${className} text-indigo-600`} />,
|
||||
3: <Target04 className={`${className} text-[#107569]`} />,
|
||||
})[toneId]
|
||||
return res
|
||||
}
|
||||
const options = TONE_LIST.slice(0, 3).map((tone) => {
|
||||
return {
|
||||
value: tone.id,
|
||||
text: (
|
||||
<div className='flex h-full items-center'>
|
||||
{getToneIcon(tone.id)}
|
||||
{t(`common.model.tone.${tone.name}`) as string}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
renderTrigger={renderTrigger}
|
||||
items={options}
|
||||
onSelect={item => onSelect(item.value as number)}
|
||||
popupClassName='z-[1003]'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default PresetsParameter
|
||||
@@ -0,0 +1,97 @@
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Link from 'next/link'
|
||||
import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version'
|
||||
import { useInstalledPluginList } from '@/service/use-plugins'
|
||||
import { RiErrorWarningFill } from '@remixicon/react'
|
||||
|
||||
type StatusIndicatorsProps = {
|
||||
needsConfiguration: boolean
|
||||
modelProvider: boolean
|
||||
inModelList: boolean
|
||||
disabled: boolean
|
||||
pluginInfo: any
|
||||
t: any
|
||||
}
|
||||
|
||||
const StatusIndicators = ({ needsConfiguration, modelProvider, inModelList, disabled, pluginInfo, t }: StatusIndicatorsProps) => {
|
||||
const { data: pluginList } = useInstalledPluginList()
|
||||
const renderTooltipContent = (title: string, description?: string, linkText?: string, linkHref?: string) => {
|
||||
return (
|
||||
<div className='flex w-[240px] max-w-[240px] flex-col gap-1 px-1 py-1.5' onClick={e => e.stopPropagation()}>
|
||||
<div className='title-xs-semi-bold text-text-primary'>{title}</div>
|
||||
{description && (
|
||||
<div className='body-xs-regular min-w-[200px] text-text-secondary'>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
{linkText && linkHref && (
|
||||
<div className='body-xs-regular z-[100] cursor-pointer text-text-accent'>
|
||||
<Link
|
||||
href={linkHref}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{linkText}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// const installedPluginUniqueIdentifier = pluginList?.plugins.find(plugin => plugin.name === pluginInfo.name)?.plugin_unique_identifier
|
||||
return (
|
||||
<>
|
||||
{/* plugin installed and model is in model list but disabled */}
|
||||
{/* plugin installed from github/local and model is not in model list */}
|
||||
{!needsConfiguration && modelProvider && disabled && (
|
||||
<>
|
||||
{inModelList ? (
|
||||
<Tooltip
|
||||
popupContent={t('workflow.nodes.agent.modelSelectorTooltips.deprecated')}
|
||||
asChild={false}
|
||||
needsDelay={false}
|
||||
>
|
||||
<RiErrorWarningFill className='h-4 w-4 text-text-destructive' />
|
||||
</Tooltip>
|
||||
) : !pluginInfo ? (
|
||||
<Tooltip
|
||||
popupContent={renderTooltipContent(
|
||||
t('workflow.nodes.agent.modelNotSupport.title'),
|
||||
t('workflow.nodes.agent.modelNotSupport.desc'),
|
||||
t('workflow.nodes.agent.linkToPlugin'),
|
||||
'/plugins',
|
||||
)}
|
||||
asChild={false}
|
||||
>
|
||||
<RiErrorWarningFill className='h-4 w-4 text-text-destructive' />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<SwitchPluginVersion
|
||||
tooltip={renderTooltipContent(
|
||||
t('workflow.nodes.agent.modelNotSupport.title'),
|
||||
t('workflow.nodes.agent.modelNotSupport.descForVersionSwitch'),
|
||||
)}
|
||||
uniqueIdentifier={pluginList?.plugins.find(plugin => plugin.name === pluginInfo.name)?.plugin_unique_identifier ?? ''}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!modelProvider && !pluginInfo && (
|
||||
<Tooltip
|
||||
popupContent={renderTooltipContent(
|
||||
t('workflow.nodes.agent.modelNotInMarketplace.title'),
|
||||
t('workflow.nodes.agent.modelNotInMarketplace.desc'),
|
||||
t('workflow.nodes.agent.linkToPlugin'),
|
||||
'/plugins',
|
||||
)}
|
||||
asChild={false}
|
||||
>
|
||||
<RiErrorWarningFill className='h-4 w-4 text-text-destructive' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusIndicators
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import type {
|
||||
Model,
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { MODEL_STATUS_TEXT } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
export type TriggerProps = {
|
||||
open?: boolean
|
||||
disabled?: boolean
|
||||
currentProvider?: ModelProvider | Model
|
||||
currentModel?: ModelItem
|
||||
providerName?: string
|
||||
modelId?: string
|
||||
hasDeprecated?: boolean
|
||||
modelDisabled?: boolean
|
||||
isInWorkflow?: boolean
|
||||
}
|
||||
const Trigger: FC<TriggerProps> = ({
|
||||
disabled,
|
||||
currentProvider,
|
||||
currentModel,
|
||||
providerName,
|
||||
modelId,
|
||||
hasDeprecated,
|
||||
modelDisabled,
|
||||
isInWorkflow,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const { modelProviders } = useProviderContext()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-8 cursor-pointer items-center rounded-lg px-2',
|
||||
!isInWorkflow && 'border ring-inset hover:ring-[0.5px]',
|
||||
!isInWorkflow && (disabled ? 'border-text-warning bg-state-warning-hover ring-text-warning' : 'border-util-colors-indigo-indigo-600 bg-state-accent-hover ring-util-colors-indigo-indigo-600'),
|
||||
isInWorkflow && 'border border-workflow-block-parma-bg bg-workflow-block-parma-bg pr-[30px] hover:border-components-input-border-active',
|
||||
)}
|
||||
>
|
||||
{
|
||||
currentProvider && (
|
||||
<ModelIcon
|
||||
className='mr-1.5 !h-5 !w-5'
|
||||
provider={currentProvider}
|
||||
modelName={currentModel?.model}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentProvider && (
|
||||
<ModelIcon
|
||||
className='mr-1.5 !h-5 !w-5'
|
||||
provider={modelProviders.find(item => item.provider === providerName)}
|
||||
modelName={modelId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
currentModel && (
|
||||
<ModelName
|
||||
className='mr-1.5 text-text-primary'
|
||||
modelItem={currentModel}
|
||||
showMode
|
||||
showFeatures
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentModel && (
|
||||
<div className='mr-1 truncate text-[13px] font-medium text-text-primary'>
|
||||
{modelId}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
disabled
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
hasDeprecated
|
||||
? t('common.modelProvider.deprecated')
|
||||
: (modelDisabled && currentModel)
|
||||
? MODEL_STATUS_TEXT[currentModel.status as string][language]
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<AlertTriangle className='h-4 w-4 text-[#F79009]' />
|
||||
</Tooltip>
|
||||
)
|
||||
: (
|
||||
<SlidersH className={cn(!isInWorkflow ? 'text-indigo-600' : 'text-text-tertiary', 'h-4 w-4 shrink-0')} />
|
||||
)
|
||||
}
|
||||
{isInWorkflow && (<RiArrowDownSLine className='absolute right-2 top-[9px] h-3.5 w-3.5 text-text-tertiary' />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Trigger
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelIcon from '../model-icon'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ModelTriggerProps = {
|
||||
modelName: string
|
||||
providerName: string
|
||||
className?: string
|
||||
showWarnIcon?: boolean
|
||||
contentClassName?: string
|
||||
}
|
||||
const ModelTrigger: FC<ModelTriggerProps> = ({
|
||||
modelName,
|
||||
providerName,
|
||||
className,
|
||||
showWarnIcon,
|
||||
contentClassName,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modelProviders } = useProviderContext()
|
||||
const currentProvider = modelProviders.find(provider => provider.provider === providerName)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group box-content flex h-8 grow cursor-pointer items-center gap-1 rounded-lg bg-components-input-bg-disabled p-[3px] pl-1', className)}
|
||||
>
|
||||
<div className={cn('flex w-full items-center', contentClassName)}>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-1 py-[1px]'>
|
||||
<ModelIcon
|
||||
className="h-4 w-4"
|
||||
provider={currentProvider}
|
||||
modelName={modelName}
|
||||
/>
|
||||
<div className='system-sm-regular truncate text-components-input-text-filled'>
|
||||
{modelName}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center justify-center'>
|
||||
{showWarnIcon && (
|
||||
<Tooltip popupContent={t('common.modelProvider.deprecated')}>
|
||||
<AlertTriangle className='h-4 w-4 text-text-warning-secondary' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelTrigger
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
type ModelTriggerProps = {
|
||||
open: boolean
|
||||
className?: string
|
||||
}
|
||||
const ModelTrigger: FC<ModelTriggerProps> = ({
|
||||
open,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 hover:bg-components-input-bg-hover', open && 'bg-components-input-bg-hover',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
<div className='mr-1.5 flex h-4 w-4 items-center justify-center rounded-[5px] border border-dashed border-divider-regular'>
|
||||
<CubeOutline className='h-3 w-3 text-text-quaternary' />
|
||||
</div>
|
||||
<div
|
||||
className='truncate text-[13px] text-text-tertiary'
|
||||
title='Configure model'
|
||||
>
|
||||
{t('plugin.detailPanel.configureModel')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-4 w-4 shrink-0 items-center justify-center'>
|
||||
<RiEqualizer2Line className='h-3.5 w-3.5 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelTrigger
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelBadge from '../model-badge'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
ModelFeatureTextEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
AudioSupportIcon,
|
||||
DocumentSupportIcon,
|
||||
// MagicBox,
|
||||
MagicEyes,
|
||||
// MagicWand,
|
||||
// Robot,
|
||||
VideoSupportIcon,
|
||||
} from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type FeatureIconProps = {
|
||||
feature: ModelFeatureEnum
|
||||
className?: string
|
||||
}
|
||||
const FeatureIcon: FC<FeatureIconProps> = ({
|
||||
className,
|
||||
feature,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// if (feature === ModelFeatureEnum.agentThought) {
|
||||
// return (
|
||||
// <Tooltip
|
||||
// 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>
|
||||
// </Tooltip>
|
||||
// )
|
||||
// }
|
||||
|
||||
// if (feature === ModelFeatureEnum.toolCall) {
|
||||
// return (
|
||||
// <Tooltip
|
||||
// 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>
|
||||
// </Tooltip>
|
||||
// )
|
||||
// }
|
||||
|
||||
// if (feature === ModelFeatureEnum.multiToolCall) {
|
||||
// return (
|
||||
// <Tooltip
|
||||
// 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>
|
||||
// </Tooltip>
|
||||
// )
|
||||
// }
|
||||
|
||||
if (feature === ModelFeatureEnum.vision) {
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.vision })}
|
||||
>
|
||||
<div className='inline-block cursor-help'>
|
||||
<ModelBadge className={`w-[18px] justify-center !px-0 text-text-tertiary ${className}`}>
|
||||
<MagicEyes className='h-3 w-3' />
|
||||
</ModelBadge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (feature === ModelFeatureEnum.document) {
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.document })}
|
||||
>
|
||||
<div className='inline-block cursor-help'>
|
||||
<ModelBadge className={`w-[18px] justify-center !px-0 text-text-tertiary ${className}`}>
|
||||
<DocumentSupportIcon className='h-3 w-3' />
|
||||
</ModelBadge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (feature === ModelFeatureEnum.audio) {
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.audio })}
|
||||
>
|
||||
<div className='inline-block cursor-help'>
|
||||
<ModelBadge className={`w-[18px] justify-center !px-0 text-text-tertiary ${className}`}>
|
||||
<AudioSupportIcon className='h-3 w-3' />
|
||||
</ModelBadge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (feature === ModelFeatureEnum.video) {
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.video })}
|
||||
>
|
||||
<div className='inline-block cursor-help'>
|
||||
<ModelBadge className={`w-[18px] justify-center !px-0 text-text-tertiary ${className}`}>
|
||||
<VideoSupportIcon className='h-3 w-3' />
|
||||
</ModelBadge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default FeatureIcon
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import type {
|
||||
DefaultModel,
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import type { ModelFeatureEnum } from '../declarations'
|
||||
import { useCurrentProviderAndModel } from '../hooks'
|
||||
import ModelTrigger from './model-trigger'
|
||||
import EmptyTrigger from './empty-trigger'
|
||||
import DeprecatedModelTrigger from './deprecated-model-trigger'
|
||||
import Popup from './popup'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type ModelSelectorProps = {
|
||||
defaultModel?: DefaultModel
|
||||
modelList: Model[]
|
||||
triggerClassName?: string
|
||||
popupClassName?: string
|
||||
onSelect?: (model: DefaultModel) => void
|
||||
readonly?: boolean
|
||||
scopeFeatures?: ModelFeatureEnum[]
|
||||
deprecatedClassName?: string
|
||||
showDeprecatedWarnIcon?: boolean
|
||||
}
|
||||
const ModelSelector: FC<ModelSelectorProps> = ({
|
||||
defaultModel,
|
||||
modelList,
|
||||
triggerClassName,
|
||||
popupClassName,
|
||||
onSelect,
|
||||
readonly,
|
||||
scopeFeatures = [],
|
||||
deprecatedClassName,
|
||||
showDeprecatedWarnIcon = false,
|
||||
}) => {
|
||||
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={classNames('relative')}>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={handleToggle}
|
||||
className='block'
|
||||
>
|
||||
{
|
||||
currentModel && currentProvider && (
|
||||
<ModelTrigger
|
||||
open={open}
|
||||
provider={currentProvider}
|
||||
model={currentModel}
|
||||
className={triggerClassName}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentModel && defaultModel && (
|
||||
<DeprecatedModelTrigger
|
||||
modelName={defaultModel?.model || ''}
|
||||
providerName={defaultModel?.provider || ''}
|
||||
className={triggerClassName}
|
||||
showWarnIcon={showDeprecatedWarnIcon}
|
||||
contentClassName={deprecatedClassName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!defaultModel && (
|
||||
<EmptyTrigger
|
||||
open={open}
|
||||
className={triggerClassName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className={`z-[1002] ${popupClassName}`}>
|
||||
<Popup
|
||||
defaultModel={defaultModel}
|
||||
modelList={modelList}
|
||||
onSelect={handleSelect}
|
||||
scopeFeatures={scopeFeatures}
|
||||
onHide={() => setOpen(false)}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelSelector
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import type {
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import {
|
||||
MODEL_STATUS_TEXT,
|
||||
ModelStatusEnum,
|
||||
} from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ModelTriggerProps = {
|
||||
open: boolean
|
||||
provider: Model
|
||||
model: ModelItem
|
||||
className?: string
|
||||
readonly?: boolean
|
||||
}
|
||||
const ModelTrigger: FC<ModelTriggerProps> = ({
|
||||
open,
|
||||
provider,
|
||||
model,
|
||||
className,
|
||||
readonly,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group flex h-8 items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1',
|
||||
!readonly && 'cursor-pointer hover:bg-components-input-bg-hover',
|
||||
open && 'bg-components-input-bg-hover',
|
||||
model.status !== ModelStatusEnum.active && 'bg-components-input-bg-disabled hover:bg-components-input-bg-disabled',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ModelIcon
|
||||
className='p-0.5'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
<div className='flex grow items-center gap-1 truncate px-1 py-[3px]'>
|
||||
<ModelName
|
||||
className='grow'
|
||||
modelItem={model}
|
||||
showMode
|
||||
showFeatures
|
||||
/>
|
||||
{!readonly && (
|
||||
<div className='flex h-4 w-4 shrink-0 items-center justify-center'>
|
||||
{
|
||||
model.status !== ModelStatusEnum.active
|
||||
? (
|
||||
<Tooltip popupContent={MODEL_STATUS_TEXT[model.status][language]}>
|
||||
<AlertTriangle className='h-4 w-4 text-text-warning-secondary' />
|
||||
</Tooltip>
|
||||
)
|
||||
: (
|
||||
<RiArrowDownSLine
|
||||
className='h-3.5 w-3.5 text-text-tertiary'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelTrigger
|
||||
@@ -0,0 +1,195 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiFileTextLine,
|
||||
RiFilmAiLine,
|
||||
RiImageCircleAiLine,
|
||||
RiVoiceAiFill,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
DefaultModel,
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
ModelFeatureTextEnum,
|
||||
ModelTypeEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
modelTypeFormat,
|
||||
sizeFormat,
|
||||
} from '../utils'
|
||||
import {
|
||||
useLanguage,
|
||||
useUpdateModelList,
|
||||
useUpdateModelProviders,
|
||||
} from '../hooks'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import ModelBadge from '../model-badge'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
ModelStatusEnum,
|
||||
} 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'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
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 updateModelProviders = useUpdateModelProviders()
|
||||
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,
|
||||
currentConfigurationMethod: ConfigurationMethodEnum.predefinedModel,
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
updateModelProviders()
|
||||
|
||||
const modelType = model.models[0].model_type
|
||||
|
||||
if (modelType)
|
||||
updateModelList(modelType)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mb-1'>
|
||||
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>
|
||||
{model.label[language] || model.label.en_US}
|
||||
</div>
|
||||
{
|
||||
model.models.map(modelItem => (
|
||||
<Tooltip
|
||||
key={modelItem.model}
|
||||
position='right'
|
||||
popupClassName='p-3 !w-[206px] bg-components-panel-bg-blur backdrop-blur-sm border-[0.5px] border-components-panel-border rounded-xl'
|
||||
popupContent={
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex flex-col items-start gap-2'>
|
||||
<ModelIcon
|
||||
className={cn('h-5 w-5 shrink-0')}
|
||||
provider={model}
|
||||
modelName={modelItem.model}
|
||||
/>
|
||||
<div className='system-md-medium text-wrap break-words text-text-primary'>{modelItem.label[language] || modelItem.label.en_US}</div>
|
||||
</div>
|
||||
{/* {currentProvider?.description && (
|
||||
<div className='text-text-tertiary system-xs-regular'>{currentProvider?.description?.[language] || currentProvider?.description?.en_US}</div>
|
||||
)} */}
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{modelItem.model_type && (
|
||||
<ModelBadge>
|
||||
{modelTypeFormat(modelItem.model_type)}
|
||||
</ModelBadge>
|
||||
)}
|
||||
{modelItem.model_properties.mode && (
|
||||
<ModelBadge>
|
||||
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
|
||||
</ModelBadge>
|
||||
)}
|
||||
{modelItem.model_properties.context_size && (
|
||||
<ModelBadge>
|
||||
{sizeFormat(modelItem.model_properties.context_size as number)}
|
||||
</ModelBadge>
|
||||
)}
|
||||
</div>
|
||||
{modelItem.model_type === ModelTypeEnum.textGeneration && modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature)) && (
|
||||
<div className='pt-2'>
|
||||
<div className='system-2xs-medium-uppercase mb-1 text-text-tertiary'>{t('common.model.capabilities')}</div>
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{modelItem.features?.includes(ModelFeatureEnum.vision) && (
|
||||
<ModelBadge>
|
||||
<RiImageCircleAiLine className='mr-0.5 h-3.5 w-3.5' />
|
||||
<span>{ModelFeatureTextEnum.vision}</span>
|
||||
</ModelBadge>
|
||||
)}
|
||||
{modelItem.features?.includes(ModelFeatureEnum.audio) && (
|
||||
<ModelBadge>
|
||||
<RiVoiceAiFill className='mr-0.5 h-3.5 w-3.5' />
|
||||
<span>{ModelFeatureTextEnum.audio}</span>
|
||||
</ModelBadge>
|
||||
)}
|
||||
{modelItem.features?.includes(ModelFeatureEnum.video) && (
|
||||
<ModelBadge>
|
||||
<RiFilmAiLine className='mr-0.5 h-3.5 w-3.5' />
|
||||
<span>{ModelFeatureTextEnum.video}</span>
|
||||
</ModelBadge>
|
||||
)}
|
||||
{modelItem.features?.includes(ModelFeatureEnum.document) && (
|
||||
<ModelBadge>
|
||||
<RiFileTextLine className='mr-0.5 h-3.5 w-3.5' />
|
||||
<span>{ModelFeatureTextEnum.document}</span>
|
||||
</ModelBadge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
key={modelItem.model}
|
||||
className={cn('group relative flex h-8 items-center gap-1 rounded-lg px-3 py-1.5', modelItem.status === ModelStatusEnum.active ? 'cursor-pointer hover:bg-state-base-hover' : 'cursor-not-allowed hover:bg-state-base-hover-alt')}
|
||||
onClick={() => handleSelect(model.provider, modelItem)}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<ModelIcon
|
||||
className={cn('h-5 w-5 shrink-0')}
|
||||
provider={model}
|
||||
modelName={modelItem.model}
|
||||
/>
|
||||
<ModelName
|
||||
className={cn('system-sm-medium text-text-secondary', modelItem.status !== ModelStatusEnum.active && 'opacity-60')}
|
||||
modelItem={modelItem}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
defaultModel?.model === modelItem.model && defaultModel.provider === currentProvider.provider && (
|
||||
<Check className='h-4 w-4 shrink-0 text-text-accent' />
|
||||
)
|
||||
}
|
||||
{
|
||||
modelItem.status === ModelStatusEnum.noConfigure && (
|
||||
<div
|
||||
className='hidden cursor-pointer text-xs font-medium text-text-accent group-hover:block'
|
||||
onClick={handleOpenModelModal}
|
||||
>
|
||||
{t('common.operation.add').toLocaleUpperCase()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PopupItem
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
RiSearchLine,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
DefaultModel,
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import { ModelFeatureEnum } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import PopupItem from './popup-item'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { supportFunctionCall } from '@/utils/tool-call'
|
||||
import { tooltipManager } from '@/app/components/base/tooltip/TooltipManager'
|
||||
|
||||
type PopupProps = {
|
||||
defaultModel?: DefaultModel
|
||||
modelList: Model[]
|
||||
onSelect: (provider: string, model: ModelItem) => void
|
||||
scopeFeatures?: ModelFeatureEnum[]
|
||||
onHide: () => void
|
||||
}
|
||||
const Popup: FC<PopupProps> = ({
|
||||
defaultModel,
|
||||
modelList,
|
||||
onSelect,
|
||||
scopeFeatures = [],
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Close any open tooltips when the user scrolls to prevent them from appearing
|
||||
// in incorrect positions or becoming detached from their trigger elements
|
||||
useEffect(() => {
|
||||
const handleTooltipCloseOnScroll = () => {
|
||||
tooltipManager.closeActiveTooltip()
|
||||
}
|
||||
|
||||
const scrollContainer = scrollRef.current
|
||||
if (!scrollContainer) return
|
||||
|
||||
// Use passive listener for better performance since we don't prevent default
|
||||
scrollContainer.addEventListener('scroll', handleTooltipCloseOnScroll, { passive: true })
|
||||
|
||||
return () => {
|
||||
scrollContainer.removeEventListener('scroll', handleTooltipCloseOnScroll)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const filteredModelList = useMemo(() => {
|
||||
return modelList.map((model) => {
|
||||
const filteredModels = model.models
|
||||
.filter((modelItem) => {
|
||||
if (modelItem.label[language] !== undefined)
|
||||
return modelItem.label[language].toLowerCase().includes(searchText.toLowerCase())
|
||||
return Object.values(modelItem.label).some(label =>
|
||||
label.toLowerCase().includes(searchText.toLowerCase()),
|
||||
)
|
||||
})
|
||||
.filter((modelItem) => {
|
||||
if (scopeFeatures.length === 0)
|
||||
return true
|
||||
return scopeFeatures.every((feature) => {
|
||||
if (feature === ModelFeatureEnum.toolCall)
|
||||
return supportFunctionCall(modelItem.features)
|
||||
return modelItem.features?.includes(feature) ?? false
|
||||
})
|
||||
})
|
||||
return { ...model, models: filteredModels }
|
||||
}).filter(model => model.models.length > 0)
|
||||
}, [language, modelList, scopeFeatures, searchText])
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} className='max-h-[480px] w-[320px] overflow-y-auto rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg'>
|
||||
<div className='sticky top-0 z-10 bg-components-panel-bg pb-1 pl-3 pr-2 pt-3'>
|
||||
<div className={`
|
||||
flex h-8 items-center rounded-lg border pl-[9px] pr-[10px]
|
||||
${searchText ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-transparent bg-components-input-bg-normal'}
|
||||
`}>
|
||||
<RiSearchLine
|
||||
className={`
|
||||
mr-[7px] h-[14px] w-[14px] shrink-0
|
||||
${searchText ? 'text-text-tertiary' : 'text-text-quaternary'}
|
||||
`}
|
||||
/>
|
||||
<input
|
||||
className='block h-[18px] grow appearance-none bg-transparent text-[13px] text-text-primary outline-none'
|
||||
placeholder={t('datasetSettings.form.searchModel') || ''}
|
||||
value={searchText}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
/>
|
||||
{
|
||||
searchText && (
|
||||
<XCircle
|
||||
className='ml-1.5 h-[14px] w-[14px] shrink-0 cursor-pointer text-text-quaternary'
|
||||
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='break-all px-3 py-1.5 text-center text-xs leading-[18px] text-text-tertiary'>
|
||||
{`No model found for “${searchText}”`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='sticky bottom-0 flex cursor-pointer items-center rounded-b-lg border-t border-divider-subtle bg-components-panel-bg px-4 py-2 text-text-accent-light-mode-only' onClick={() => {
|
||||
onHide()
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
}}>
|
||||
<span className='system-xs-medium'>{t('common.model.settingsLink')}</span>
|
||||
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Popup
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type AddModelButtonProps = {
|
||||
className?: string
|
||||
onClick: () => void
|
||||
}
|
||||
const AddModelButton: FC<AddModelButtonProps> = ({
|
||||
className,
|
||||
onClick,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn('system-xs-medium flex h-6 shrink-0 cursor-pointer items-center rounded-md px-1.5 text-text-tertiary hover:bg-components-button-ghost-bg-hover hover:text-components-button-ghost-text', className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusCircle className='mr-1 h-3 w-3' />
|
||||
{t('common.modelProvider.addModel')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddModelButton
|
||||
@@ -0,0 +1,64 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLatest } from 'ahooks'
|
||||
import SimplePieChart from '@/app/components/base/simple-pie-chart'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
export type CooldownTimerProps = {
|
||||
secondsRemaining?: number
|
||||
onFinish?: () => void
|
||||
}
|
||||
|
||||
const CooldownTimer = ({ secondsRemaining, onFinish }: CooldownTimerProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const targetTime = useRef<number>(Date.now())
|
||||
const [currentTime, setCurrentTime] = useState(targetTime.current)
|
||||
const displayTime = useMemo(
|
||||
() => Math.ceil((targetTime.current - currentTime) / 1000),
|
||||
[currentTime],
|
||||
)
|
||||
|
||||
const countdownTimeout = useRef<number>(undefined)
|
||||
const clearCountdown = useCallback(() => {
|
||||
if (countdownTimeout.current) {
|
||||
window.clearTimeout(countdownTimeout.current)
|
||||
countdownTimeout.current = undefined
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onFinishRef = useLatest(onFinish)
|
||||
|
||||
const countdown = useCallback(() => {
|
||||
clearCountdown()
|
||||
countdownTimeout.current = window.setTimeout(() => {
|
||||
const now = Date.now()
|
||||
if (now <= targetTime.current) {
|
||||
setCurrentTime(Date.now())
|
||||
countdown()
|
||||
}
|
||||
else {
|
||||
onFinishRef.current?.()
|
||||
clearCountdown()
|
||||
}
|
||||
}, 1000)
|
||||
}, [clearCountdown, onFinishRef])
|
||||
|
||||
useEffect(() => {
|
||||
const now = Date.now()
|
||||
targetTime.current = now + (secondsRemaining ?? 0) * 1000
|
||||
setCurrentTime(now)
|
||||
countdown()
|
||||
return clearCountdown
|
||||
}, [clearCountdown, countdown, secondsRemaining])
|
||||
|
||||
return displayTime
|
||||
? (
|
||||
<Tooltip popupContent={t('common.modelProvider.apiKeyRateLimit', { seconds: displayTime })}>
|
||||
<SimplePieChart percentage={Math.round(displayTime / 60 * 100)} className='h-3 w-3' />
|
||||
</Tooltip>
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
export default memo(CooldownTimer)
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
CustomConfigurationStatusEnum,
|
||||
PreferredProviderTypeEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
useUpdateModelList,
|
||||
useUpdateModelProviders,
|
||||
} from '../hooks'
|
||||
import PrioritySelector from './priority-selector'
|
||||
import PriorityUseTip from './priority-use-tip'
|
||||
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './index'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { changeModelProviderPriority } from '@/service/common'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||
import { ConfigProvider } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
|
||||
type CredentialPanelProps = {
|
||||
provider: ModelProvider
|
||||
}
|
||||
const CredentialPanel = ({
|
||||
provider,
|
||||
}: CredentialPanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const customConfig = provider.custom_configuration
|
||||
const systemConfig = provider.system_configuration
|
||||
const priorityUseType = provider.preferred_provider_type
|
||||
const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active
|
||||
const configurateMethods = provider.configurate_methods
|
||||
const {
|
||||
hasCredential,
|
||||
authorized,
|
||||
authRemoved,
|
||||
current_credential_name,
|
||||
notAllowedToUse,
|
||||
} = useCredentialStatus(provider)
|
||||
|
||||
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') })
|
||||
updateModelProviders()
|
||||
|
||||
configurateMethods.forEach((method) => {
|
||||
if (method === ConfigurationMethodEnum.predefinedModel)
|
||||
provider.supported_model_types.forEach(modelType => updateModelList(modelType))
|
||||
})
|
||||
|
||||
eventEmitter?.emit({
|
||||
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
|
||||
payload: provider.provider,
|
||||
} as any)
|
||||
}
|
||||
}
|
||||
const credentialLabel = useMemo(() => {
|
||||
if (!hasCredential)
|
||||
return t('common.modelProvider.auth.unAuthorized')
|
||||
if (authorized)
|
||||
return current_credential_name
|
||||
if (authRemoved)
|
||||
return t('common.modelProvider.auth.authRemoved')
|
||||
|
||||
return ''
|
||||
}, [authorized, authRemoved, current_credential_name, hasCredential])
|
||||
|
||||
const color = useMemo(() => {
|
||||
if (authRemoved || !hasCredential)
|
||||
return 'red'
|
||||
if (notAllowedToUse)
|
||||
return 'gray'
|
||||
return 'green'
|
||||
}, [authRemoved, notAllowedToUse, hasCredential])
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
provider.provider_credential_schema && (
|
||||
<div className={cn(
|
||||
'relative ml-1 w-[120px] shrink-0 rounded-lg border-[0.5px] border-components-panel-border bg-white/[0.18] p-1',
|
||||
authRemoved && 'border-state-destructive-border bg-state-destructive-hover',
|
||||
)}>
|
||||
<div className='system-xs-medium mb-1 flex h-5 items-center justify-between pl-2 pr-[7px] pt-1 text-text-tertiary'>
|
||||
<div
|
||||
className={cn(
|
||||
'grow truncate',
|
||||
authRemoved && 'text-text-destructive',
|
||||
)}
|
||||
title={credentialLabel}
|
||||
>
|
||||
{credentialLabel}
|
||||
</div>
|
||||
<Indicator className='shrink-0' color={color} />
|
||||
</div>
|
||||
<div className='flex items-center gap-0.5'>
|
||||
<ConfigProvider
|
||||
provider={provider}
|
||||
/>
|
||||
{
|
||||
systemConfig.enabled && isCustomConfigured && (
|
||||
<PrioritySelector
|
||||
value={priorityUseType}
|
||||
onSelect={handleChangePriority}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
priorityUseType === PreferredProviderTypeEnum.custom && systemConfig.enabled && (
|
||||
<PriorityUseTip />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
systemConfig.enabled && isCustomConfigured && !provider.provider_credential_schema && (
|
||||
<div className='ml-1'>
|
||||
<PrioritySelector
|
||||
value={priorityUseType}
|
||||
onSelect={handleChangePriority}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CredentialPanel
|
||||
@@ -0,0 +1,191 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiInformation2Fill,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { ConfigurationMethodEnum } from '../declarations'
|
||||
import {
|
||||
MODEL_PROVIDER_QUOTA_GET_PAID,
|
||||
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 { fetchModelProviderModelList } from '@/service/common'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
AddCustomModel,
|
||||
ManageCustomModelCredentials,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
|
||||
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
|
||||
type ProviderAddedCardProps = {
|
||||
notConfigured?: boolean
|
||||
provider: ModelProvider
|
||||
}
|
||||
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
notConfigured,
|
||||
provider,
|
||||
}) => {
|
||||
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 configurationMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
|
||||
const systemConfig = provider.system_configuration
|
||||
const hasModelList = fetched && !!modelList.length
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const showQuota = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider) && !IS_CE_EDITION
|
||||
const showCredential = configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && isCurrentWorkspaceManager
|
||||
|
||||
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={cn(
|
||||
'mb-2 rounded-xl border-[0.5px] border-divider-regular bg-third-party-model-bg-default shadow-xs',
|
||||
provider.provider === 'langgenius/openai/openai' && 'bg-third-party-model-bg-openai',
|
||||
provider.provider === 'langgenius/anthropic/anthropic' && 'bg-third-party-model-bg-anthropic',
|
||||
)}
|
||||
>
|
||||
<div className='flex rounded-t-xl py-2 pl-3 pr-2'>
|
||||
<div className='grow px-1 pb-0.5 pt-1'>
|
||||
<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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showCredential && (
|
||||
<CredentialPanel
|
||||
provider={provider}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
collapsed && (
|
||||
<div className='system-xs-medium group flex items-center justify-between border-t border-t-divider-subtle py-1.5 pl-2 pr-[11px] text-text-tertiary'>
|
||||
{(showQuota || !notConfigured) && (
|
||||
<>
|
||||
<div className='flex h-6 items-center pl-1 pr-1.5 leading-6 group-hover:hidden'>
|
||||
{
|
||||
hasModelList
|
||||
? t('common.modelProvider.modelsNum', { num: modelList.length })
|
||||
: t('common.modelProvider.showModels')
|
||||
}
|
||||
{!loading && <RiArrowRightSLine className='h-4 w-4' />}
|
||||
</div>
|
||||
<div
|
||||
className='hidden h-6 cursor-pointer items-center rounded-lg pl-1 pr-1.5 hover:bg-components-button-ghost-bg-hover group-hover:flex'
|
||||
onClick={handleOpenModelList}
|
||||
>
|
||||
{
|
||||
hasModelList
|
||||
? t('common.modelProvider.showModelsNum', { num: modelList.length })
|
||||
: t('common.modelProvider.showModels')
|
||||
}
|
||||
{!loading && <RiArrowRightSLine className='h-4 w-4' />}
|
||||
{
|
||||
loading && (
|
||||
<RiLoader2Line className='ml-0.5 h-3 w-3 animate-spin' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!showQuota && notConfigured && (
|
||||
<div className='flex h-6 items-center pl-1 pr-1.5'>
|
||||
<RiInformation2Fill className='mr-1 h-4 w-4 text-text-accent' />
|
||||
<span className='system-xs-medium text-text-secondary'>{t('common.modelProvider.configureTip')}</span>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && isCurrentWorkspaceManager && (
|
||||
<div className='flex grow justify-end'>
|
||||
<ManageCustomModelCredentials
|
||||
provider={provider}
|
||||
currentCustomConfigurationModelFixedFields={undefined}
|
||||
/>
|
||||
<AddCustomModel
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
currentCustomConfigurationModelFixedFields={undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!collapsed && (
|
||||
<ModelList
|
||||
provider={provider}
|
||||
models={modelList}
|
||||
onCollapse={() => setCollapsed(true)}
|
||||
onChange={(provider: string) => getModelList(provider)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderAddedCard
|
||||
@@ -0,0 +1,109 @@
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import type { ModelItem, ModelProvider } from '../declarations'
|
||||
import { ModelStatusEnum } from '../declarations'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useProviderContext, useProviderContextSelector } from '@/context/provider-context'
|
||||
import { disableModel, enableModel } from '@/service/common'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { ConfigModel } from '../model-auth'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
export type ModelListItemProps = {
|
||||
model: ModelItem
|
||||
provider: ModelProvider
|
||||
isConfigurable: boolean
|
||||
onModifyLoadBalancing?: (model: ModelItem) => void
|
||||
}
|
||||
|
||||
const ModelListItem = ({ model, provider, isConfigurable, onModifyLoadBalancing }: ModelListItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const toggleModelEnablingStatus = useCallback(async (enabled: boolean) => {
|
||||
if (enabled)
|
||||
await enableModel(`/workspaces/current/model-providers/${provider.provider}/models/enable`, { model: model.model, model_type: model.model_type })
|
||||
else
|
||||
await disableModel(`/workspaces/current/model-providers/${provider.provider}/models/disable`, { model: model.model, model_type: model.model_type })
|
||||
}, [model.model, model.model_type, provider.provider])
|
||||
|
||||
const { run: debouncedToggleModelEnablingStatus } = useDebounceFn(toggleModelEnablingStatus, { wait: 500 })
|
||||
|
||||
const onEnablingStateChange = useCallback(async (value: boolean) => {
|
||||
debouncedToggleModelEnablingStatus(value)
|
||||
}, [debouncedToggleModelEnablingStatus])
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${model.model}-${model.fetch_from}`}
|
||||
className={classNames(
|
||||
'group flex h-8 items-center rounded-lg pl-2 pr-2.5',
|
||||
isConfigurable && 'hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
model.deprecated && 'opacity-60',
|
||||
)}
|
||||
>
|
||||
<ModelIcon
|
||||
className='mr-2 shrink-0'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
<ModelName
|
||||
className='system-md-regular grow text-text-secondary'
|
||||
modelItem={model}
|
||||
showModelType
|
||||
showMode
|
||||
showContextSize
|
||||
>
|
||||
</ModelName>
|
||||
<div className='flex shrink-0 items-center'>
|
||||
{modelLoadBalancingEnabled && !model.deprecated && model.load_balancing_enabled && !model.has_invalid_load_balancing_configs && (
|
||||
<Badge className='mr-1 h-[18px] w-[18px] items-center justify-center border-text-accent-secondary p-0'>
|
||||
<Balance className='h-3 w-3 text-text-accent-secondary' />
|
||||
</Badge>
|
||||
)}
|
||||
{
|
||||
(isCurrentWorkspaceManager && (modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status)) && (
|
||||
<ConfigModel
|
||||
onClick={() => onModifyLoadBalancing?.(model)}
|
||||
loadBalancingEnabled={model.load_balancing_enabled}
|
||||
loadBalancingInvalid={model.has_invalid_load_balancing_configs}
|
||||
credentialRemoved={model.status === ModelStatusEnum.credentialRemoved}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
model.deprecated
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<span className='font-semibold'>{t('common.modelProvider.modelHasBeenDeprecated')}</span>} offset={{ mainAxis: 4 }
|
||||
}
|
||||
>
|
||||
<Switch defaultValue={false} disabled size='md' />
|
||||
</Tooltip>
|
||||
)
|
||||
: (isCurrentWorkspaceManager && (
|
||||
<Switch
|
||||
className='ml-2'
|
||||
defaultValue={model?.status === ModelStatusEnum.active}
|
||||
disabled={![ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status)}
|
||||
size='md'
|
||||
onChange={onEnablingStateChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ModelListItem)
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
Credential,
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
} from '../declarations'
|
||||
// import Tab from './tab'
|
||||
import ModelListItem from './model-list-item'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import {
|
||||
AddCustomModel,
|
||||
ManageCustomModelCredentials,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
|
||||
type ModelListProps = {
|
||||
provider: ModelProvider
|
||||
models: ModelItem[]
|
||||
onCollapse: () => void
|
||||
onChange?: (provider: string) => void
|
||||
}
|
||||
const ModelList: FC<ModelListProps> = ({
|
||||
provider,
|
||||
models,
|
||||
onCollapse,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const configurativeMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel)
|
||||
const setShowModelLoadBalancingModal = useModalContextSelector(state => state.setShowModelLoadBalancingModal)
|
||||
const onModifyLoadBalancing = useCallback((model: ModelItem, credential?: Credential) => {
|
||||
setShowModelLoadBalancingModal({
|
||||
provider,
|
||||
credential,
|
||||
configurateMethod: model.fetch_from,
|
||||
model: model!,
|
||||
open: !!model,
|
||||
onClose: () => setShowModelLoadBalancingModal(null),
|
||||
onSave: onChange,
|
||||
})
|
||||
}, [onChange, provider, setShowModelLoadBalancingModal])
|
||||
|
||||
return (
|
||||
<div className='rounded-b-xl px-2 pb-2'>
|
||||
<div className='rounded-lg bg-components-panel-bg py-1'>
|
||||
<div className='flex items-center pl-1 pr-[3px]'>
|
||||
<span className='group mr-2 flex shrink-0 items-center'>
|
||||
<span className='system-xs-medium inline-flex h-6 items-center pl-1 pr-1.5 text-text-tertiary group-hover:hidden'>
|
||||
{t('common.modelProvider.modelsNum', { num: models.length })}
|
||||
<RiArrowRightSLine className='mr-0.5 h-4 w-4 rotate-90' />
|
||||
</span>
|
||||
<span
|
||||
className='system-xs-medium hidden h-6 cursor-pointer items-center rounded-lg bg-state-base-hover pl-1 pr-1.5 text-text-tertiary group-hover:inline-flex'
|
||||
onClick={() => onCollapse()}
|
||||
>
|
||||
{t('common.modelProvider.modelsNum', { num: models.length })}
|
||||
<RiArrowRightSLine className='mr-0.5 h-4 w-4 rotate-90' />
|
||||
</span>
|
||||
</span>
|
||||
{
|
||||
isConfigurable && isCurrentWorkspaceManager && (
|
||||
<div className='flex grow justify-end'>
|
||||
<ManageCustomModelCredentials
|
||||
provider={provider}
|
||||
currentCustomConfigurationModelFixedFields={undefined}
|
||||
/>
|
||||
<AddCustomModel
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
currentCustomConfigurationModelFixedFields={undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
models.map(model => (
|
||||
<ModelListItem
|
||||
key={`${model.model}-${model.model_type}-${model.fetch_from}`}
|
||||
{...{
|
||||
model,
|
||||
provider,
|
||||
isConfigurable,
|
||||
onModifyLoadBalancing,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelList
|
||||
@@ -0,0 +1,285 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiIndeterminateCircleLine,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModelCredential,
|
||||
ModelCredential,
|
||||
ModelLoadBalancingConfig,
|
||||
ModelLoadBalancingConfigEntry,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { ConfigurationMethodEnum } from '../declarations'
|
||||
import Indicator from '../../../indicator'
|
||||
import CooldownTimer from './cooldown-timer'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import s from '@/app/components/custom/style.module.css'
|
||||
import GridMask from '@/app/components/base/grid-mask'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import Badge from '@/app/components/base/badge/index'
|
||||
|
||||
export type ModelLoadBalancingConfigsProps = {
|
||||
draftConfig?: ModelLoadBalancingConfig
|
||||
setDraftConfig: Dispatch<SetStateAction<ModelLoadBalancingConfig | undefined>>
|
||||
provider: ModelProvider
|
||||
configurationMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
withSwitch?: boolean
|
||||
className?: string
|
||||
modelCredential: ModelCredential
|
||||
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
|
||||
onRemove?: (credentialId: string) => void
|
||||
model: CustomModelCredential
|
||||
}
|
||||
|
||||
const ModelLoadBalancingConfigs = ({
|
||||
draftConfig,
|
||||
setDraftConfig,
|
||||
provider,
|
||||
model,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields: _currentCustomConfigurationModelFixedFields,
|
||||
withSwitch = false,
|
||||
className,
|
||||
modelCredential,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}: ModelLoadBalancingConfigsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const providerFormSchemaPredefined = configurationMethod === ConfigurationMethodEnum.predefinedModel
|
||||
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
|
||||
|
||||
const updateConfigEntry = useCallback(
|
||||
(
|
||||
index: number,
|
||||
modifier: (entry: ModelLoadBalancingConfigEntry) => ModelLoadBalancingConfigEntry | undefined,
|
||||
) => {
|
||||
setDraftConfig((prev) => {
|
||||
if (!prev)
|
||||
return prev
|
||||
const newConfigs = [...prev.configs]
|
||||
const modifiedConfig = modifier(newConfigs[index])
|
||||
if (modifiedConfig)
|
||||
newConfigs[index] = modifiedConfig
|
||||
else
|
||||
newConfigs.splice(index, 1)
|
||||
return {
|
||||
...prev,
|
||||
configs: newConfigs,
|
||||
}
|
||||
})
|
||||
},
|
||||
[setDraftConfig],
|
||||
)
|
||||
|
||||
const addConfigEntry = useCallback((credential: Credential) => {
|
||||
setDraftConfig((prev: any) => {
|
||||
if (!prev)
|
||||
return prev
|
||||
return {
|
||||
...prev,
|
||||
configs: [...prev.configs, {
|
||||
credential_id: credential.credential_id,
|
||||
enabled: true,
|
||||
name: credential.credential_name,
|
||||
}],
|
||||
}
|
||||
})
|
||||
}, [setDraftConfig])
|
||||
|
||||
const toggleModalBalancing = useCallback((enabled: boolean) => {
|
||||
if ((modelLoadBalancingEnabled || !enabled) && draftConfig) {
|
||||
setDraftConfig({
|
||||
...draftConfig,
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
}, [draftConfig, modelLoadBalancingEnabled, setDraftConfig])
|
||||
|
||||
const toggleConfigEntryEnabled = useCallback((index: number, state?: boolean) => {
|
||||
updateConfigEntry(index, entry => ({
|
||||
...entry,
|
||||
enabled: typeof state === 'boolean' ? state : !entry.enabled,
|
||||
}))
|
||||
}, [updateConfigEntry])
|
||||
|
||||
const clearCountdown = useCallback((index: number) => {
|
||||
updateConfigEntry(index, ({ ttl: _, ...entry }) => {
|
||||
return {
|
||||
...entry,
|
||||
in_cooldown: false,
|
||||
}
|
||||
})
|
||||
}, [updateConfigEntry])
|
||||
|
||||
const validDraftConfigList = useMemo(() => {
|
||||
if (!draftConfig)
|
||||
return []
|
||||
return draftConfig.configs
|
||||
}, [draftConfig])
|
||||
|
||||
const handleUpdate = useCallback((payload?: any, formValues?: Record<string, any>) => {
|
||||
onUpdate?.(payload, formValues)
|
||||
}, [onUpdate])
|
||||
|
||||
const handleRemove = useCallback((credentialId: string) => {
|
||||
const index = draftConfig?.configs.findIndex(item => item.credential_id === credentialId && item.name !== '__inherit__')
|
||||
if (index && index > -1)
|
||||
updateConfigEntry(index, () => undefined)
|
||||
onRemove?.(credentialId)
|
||||
}, [draftConfig?.configs, updateConfigEntry, onRemove])
|
||||
|
||||
if (!draftConfig)
|
||||
return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'min-h-16 rounded-xl border bg-components-panel-bg transition-colors',
|
||||
(withSwitch || !draftConfig.enabled) ? 'border-components-panel-border' : 'border-util-colors-blue-blue-600',
|
||||
(withSwitch || draftConfig.enabled) ? 'cursor-default' : 'cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
onClick={(!withSwitch && !draftConfig.enabled) ? () => toggleModalBalancing(true) : undefined}
|
||||
>
|
||||
<div className='flex select-none items-center gap-2 px-[15px] py-3'>
|
||||
<div className='flex h-8 w-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-util-colors-indigo-indigo-100 bg-util-colors-indigo-indigo-50 text-util-colors-blue-blue-600'>
|
||||
<Balance className='h-4 w-4' />
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='flex items-center gap-1 text-sm text-text-primary'>
|
||||
{t('common.modelProvider.loadBalancing')}
|
||||
<Tooltip
|
||||
popupContent={t('common.modelProvider.loadBalancingInfo')}
|
||||
popupClassName='max-w-[300px]'
|
||||
triggerClassName='w-3 h-3'
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs text-text-tertiary'>{t('common.modelProvider.loadBalancingDescription')}</div>
|
||||
</div>
|
||||
{
|
||||
withSwitch && (
|
||||
<Switch
|
||||
defaultValue={Boolean(draftConfig.enabled)}
|
||||
size='l'
|
||||
className='ml-3 justify-self-end'
|
||||
disabled={!modelLoadBalancingEnabled && !draftConfig.enabled}
|
||||
onChange={value => toggleModalBalancing(value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{draftConfig.enabled && (
|
||||
<div className='flex flex-col gap-1 px-3 pb-3'>
|
||||
{validDraftConfigList.map((config, index) => {
|
||||
const isProviderManaged = config.name === '__inherit__'
|
||||
const credential = modelCredential.available_credentials.find(c => c.credential_id === config.credential_id)
|
||||
return (
|
||||
<div key={config.id || index} className='group flex h-10 items-center rounded-lg border border-components-panel-border bg-components-panel-on-panel-item-bg px-3 shadow-xs'>
|
||||
<div className='flex grow items-center'>
|
||||
<div className='mr-2 flex h-3 w-3 items-center justify-center'>
|
||||
{(config.in_cooldown && Boolean(config.ttl))
|
||||
? (
|
||||
<CooldownTimer secondsRemaining={config.ttl} onFinish={() => clearCountdown(index)} />
|
||||
)
|
||||
: (
|
||||
<Tooltip popupContent={t('common.modelProvider.apiKeyStatusNormal')}>
|
||||
<Indicator color={credential?.not_allowed_to_use ? 'gray' : 'green'} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className='mr-1 text-[13px] text-text-secondary'>
|
||||
{isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name}
|
||||
</div>
|
||||
{isProviderManaged && providerFormSchemaPredefined && (
|
||||
<Badge className='ml-2'>{t('common.modelProvider.providerManaged')}</Badge>
|
||||
)}
|
||||
{
|
||||
credential?.from_enterprise && (
|
||||
<Badge className='ml-2'>Enterprise</Badge>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
{!isProviderManaged && (
|
||||
<>
|
||||
<div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
|
||||
<Tooltip popupContent={t('common.operation.remove')}>
|
||||
<span
|
||||
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover'
|
||||
onClick={() => updateConfigEntry(index, () => undefined)}
|
||||
>
|
||||
<RiIndeterminateCircleLine className='h-4 w-4' />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{
|
||||
(config.credential_id || config.name === '__inherit__') && (
|
||||
<>
|
||||
<span className='mr-2 h-3 border-r border-r-divider-subtle' />
|
||||
<Switch
|
||||
defaultValue={credential?.not_allowed_to_use ? false : Boolean(config.enabled)}
|
||||
size='md'
|
||||
className='justify-self-end'
|
||||
onChange={value => toggleConfigEntryEnabled(index, value)}
|
||||
disabled={credential?.not_allowed_to_use}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<AddCredentialInLoadBalancing
|
||||
provider={provider}
|
||||
model={model}
|
||||
configurationMethod={configurationMethod}
|
||||
modelCredential={modelCredential}
|
||||
onSelectCredential={addConfigEntry}
|
||||
onUpdate={handleUpdate}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
draftConfig.enabled && validDraftConfigList.length < 2 && (
|
||||
<div className='flex h-[34px] items-center rounded-b-xl border-t border-t-divider-subtle bg-components-panel-bg px-6 text-xs text-text-secondary'>
|
||||
<AlertTriangle className='mr-1 h-3 w-3 text-[#f79009]' />
|
||||
{t('common.modelProvider.loadBalancingLeastKeyWarning')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{!modelLoadBalancingEnabled && !IS_CE_EDITION && (
|
||||
<GridMask canvasClassName='!rounded-xl'>
|
||||
<div className='mt-2 flex h-14 items-center justify-between rounded-xl border-[0.5px] border-components-panel-border px-4 shadow-md'>
|
||||
<div
|
||||
className={classNames('text-gradient text-sm font-semibold leading-tight', s.textGradient)}
|
||||
>
|
||||
{t('common.modelProvider.upgradeForLoadBalancing')}
|
||||
</div>
|
||||
<UpgradeBtn />
|
||||
</div>
|
||||
</GridMask>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelLoadBalancingConfigs
|
||||
@@ -0,0 +1,376 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelItem,
|
||||
ModelLoadBalancingConfig,
|
||||
ModelLoadBalancingConfigEntry,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
FormTypeEnum,
|
||||
} from '../declarations'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import ModelLoadBalancingConfigs from './model-load-balancing-configs'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import {
|
||||
useGetModelCredential,
|
||||
useUpdateModelLoadBalancingConfig,
|
||||
} from '@/service/use-models'
|
||||
import { useAuth } from '../model-auth/hooks/use-auth'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { useRefreshModel } from '../hooks'
|
||||
|
||||
export type ModelLoadBalancingModalProps = {
|
||||
provider: ModelProvider
|
||||
configurateMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
model: ModelItem
|
||||
credential?: Credential
|
||||
open?: boolean
|
||||
onClose?: () => void
|
||||
onSave?: (provider: string) => void
|
||||
}
|
||||
|
||||
// model balancing config modal
|
||||
const ModelLoadBalancingModal = ({
|
||||
provider,
|
||||
configurateMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
model,
|
||||
credential,
|
||||
open = false,
|
||||
onClose,
|
||||
onSave,
|
||||
}: ModelLoadBalancingModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const {
|
||||
doingAction,
|
||||
deleteModel,
|
||||
openConfirmDelete,
|
||||
closeConfirmDelete,
|
||||
handleConfirmDelete,
|
||||
} = useAuth(
|
||||
provider,
|
||||
configurateMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
{
|
||||
isModelCredential: true,
|
||||
},
|
||||
)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
|
||||
const configFrom = providerFormSchemaPredefined ? 'predefined-model' : 'custom-model'
|
||||
const {
|
||||
isLoading,
|
||||
data,
|
||||
refetch,
|
||||
} = useGetModelCredential(true, provider.provider, credential?.credential_id, model.model, model.model_type, configFrom)
|
||||
const modelCredential = data
|
||||
const {
|
||||
load_balancing,
|
||||
current_credential_id,
|
||||
available_credentials,
|
||||
current_credential_name,
|
||||
} = modelCredential ?? {}
|
||||
const originalConfig = load_balancing
|
||||
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
|
||||
const originalConfigMap = useMemo(() => {
|
||||
if (!originalConfig)
|
||||
return {}
|
||||
return originalConfig?.configs.reduce((prev, config) => {
|
||||
if (config.id)
|
||||
prev[config.id] = config
|
||||
return prev
|
||||
}, {} as Record<string, ModelLoadBalancingConfigEntry>)
|
||||
}, [originalConfig])
|
||||
useEffect(() => {
|
||||
if (originalConfig)
|
||||
setDraftConfig(originalConfig)
|
||||
}, [originalConfig])
|
||||
|
||||
const toggleModalBalancing = useCallback((enabled: boolean) => {
|
||||
if (draftConfig) {
|
||||
setDraftConfig({
|
||||
...draftConfig,
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
}, [draftConfig])
|
||||
|
||||
const extendedSecretFormSchemas = useMemo(
|
||||
() => {
|
||||
if (providerFormSchemaPredefined) {
|
||||
return provider?.provider_credential_schema?.credential_form_schemas?.filter(
|
||||
({ type }) => type === FormTypeEnum.secretInput,
|
||||
) ?? []
|
||||
}
|
||||
return provider?.model_credential_schema?.credential_form_schemas?.filter(
|
||||
({ type }) => type === FormTypeEnum.secretInput,
|
||||
) ?? []
|
||||
},
|
||||
[provider?.model_credential_schema?.credential_form_schemas, provider?.provider_credential_schema?.credential_form_schemas, providerFormSchemaPredefined],
|
||||
)
|
||||
|
||||
const encodeConfigEntrySecretValues = useCallback((entry: ModelLoadBalancingConfigEntry) => {
|
||||
const result = { ...entry }
|
||||
extendedSecretFormSchemas.forEach(({ variable }) => {
|
||||
if (entry.id && result.credentials[variable] === originalConfigMap[entry.id]?.credentials?.[variable])
|
||||
result.credentials[variable] = '[__HIDDEN__]'
|
||||
})
|
||||
return result
|
||||
}, [extendedSecretFormSchemas, originalConfigMap])
|
||||
|
||||
const { mutateAsync: updateModelLoadBalancingConfig } = useUpdateModelLoadBalancingConfig(provider.provider)
|
||||
const initialCustomModelCredential = useMemo(() => {
|
||||
if (!current_credential_id)
|
||||
return undefined
|
||||
return {
|
||||
credential_id: current_credential_id,
|
||||
credential_name: current_credential_name,
|
||||
}
|
||||
}, [current_credential_id, current_credential_name])
|
||||
const [customModelCredential, setCustomModelCredential] = useState<Credential | undefined>(initialCustomModelCredential)
|
||||
const { handleRefreshModel } = useRefreshModel()
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await updateModelLoadBalancingConfig(
|
||||
{
|
||||
credential_id: customModelCredential?.credential_id || current_credential_id,
|
||||
config_from: configFrom,
|
||||
model: model.model,
|
||||
model_type: model.model_type,
|
||||
load_balancing: {
|
||||
...draftConfig,
|
||||
configs: draftConfig!.configs.map(encodeConfigEntrySecretValues),
|
||||
enabled: Boolean(draftConfig?.enabled),
|
||||
},
|
||||
},
|
||||
)
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
handleRefreshModel(provider, currentCustomConfigurationModelFixedFields, false)
|
||||
onSave?.(provider.provider)
|
||||
onClose?.()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
const handleDeleteModel = useCallback(async () => {
|
||||
await handleConfirmDelete()
|
||||
onClose?.()
|
||||
}, [handleConfirmDelete, onClose])
|
||||
|
||||
const handleUpdate = useCallback(async (payload?: any, formValues?: Record<string, any>) => {
|
||||
const result = await refetch()
|
||||
const available_credentials = result.data?.available_credentials || []
|
||||
const credentialName = formValues?.__authorization_name__
|
||||
const modelCredential = payload?.credential
|
||||
|
||||
if (!available_credentials.length) {
|
||||
onClose?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (!modelCredential) {
|
||||
const currentCredential = available_credentials.find(c => c.credential_name === credentialName)
|
||||
if (currentCredential) {
|
||||
setDraftConfig((prev: any) => {
|
||||
if (!prev)
|
||||
return prev
|
||||
return {
|
||||
...prev,
|
||||
configs: [...prev.configs, {
|
||||
credential_id: currentCredential.credential_id,
|
||||
enabled: true,
|
||||
name: currentCredential.credential_name,
|
||||
}],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
setDraftConfig((prev) => {
|
||||
if (!prev)
|
||||
return prev
|
||||
const newConfigs = [...prev.configs]
|
||||
const prevIndex = newConfigs.findIndex(item => item.credential_id === modelCredential.credential_id && item.name !== '__inherit__')
|
||||
const newIndex = available_credentials.findIndex(c => c.credential_id === modelCredential.credential_id)
|
||||
|
||||
if (newIndex > -1 && prevIndex > -1)
|
||||
newConfigs[prevIndex].name = available_credentials[newIndex].credential_name || ''
|
||||
|
||||
return {
|
||||
...prev,
|
||||
configs: newConfigs,
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [refetch, credential])
|
||||
|
||||
const handleUpdateWhenSwitchCredential = useCallback(async () => {
|
||||
const result = await refetch()
|
||||
const available_credentials = result.data?.available_credentials || []
|
||||
if (!available_credentials.length)
|
||||
onClose?.()
|
||||
}, [refetch, onClose])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isShow={Boolean(model) && open}
|
||||
onClose={onClose}
|
||||
className='w-[640px] max-w-none px-8 pt-8'
|
||||
title={
|
||||
<div className='pb-3 font-semibold'>
|
||||
<div className='h-[30px]'>{
|
||||
draftConfig?.enabled
|
||||
? t('common.modelProvider.auth.configLoadBalancing')
|
||||
: t('common.modelProvider.auth.configModel')
|
||||
}</div>
|
||||
{Boolean(model) && (
|
||||
<div className='flex h-5 items-center'>
|
||||
<ModelIcon
|
||||
className='mr-2 shrink-0'
|
||||
provider={provider}
|
||||
modelName={model!.model}
|
||||
/>
|
||||
<ModelName
|
||||
className='system-md-regular grow text-text-secondary'
|
||||
modelItem={model!}
|
||||
showModelType
|
||||
showMode
|
||||
showContextSize
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{!draftConfig
|
||||
? <Loading type='area' />
|
||||
: (
|
||||
<>
|
||||
<div className='py-2'>
|
||||
<div
|
||||
className={classNames(
|
||||
'min-h-16 rounded-xl border bg-components-panel-bg transition-colors',
|
||||
draftConfig.enabled ? 'cursor-pointer border-components-panel-border' : 'cursor-default border-util-colors-blue-blue-600',
|
||||
)}
|
||||
onClick={draftConfig.enabled ? () => toggleModalBalancing(false) : undefined}
|
||||
>
|
||||
<div className='flex select-none items-center gap-2 px-[15px] py-3'>
|
||||
<div className='flex h-8 w-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-components-card-border bg-components-card-bg'>
|
||||
{Boolean(model) && (
|
||||
<ModelIcon className='shrink-0' provider={provider} modelName={model!.model} />
|
||||
)}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='text-sm text-text-secondary'>{
|
||||
providerFormSchemaPredefined
|
||||
? t('common.modelProvider.auth.providerManaged')
|
||||
: t('common.modelProvider.auth.specifyModelCredential')
|
||||
}</div>
|
||||
<div className='text-xs text-text-tertiary'>{
|
||||
providerFormSchemaPredefined
|
||||
? t('common.modelProvider.auth.providerManagedTip')
|
||||
: t('common.modelProvider.auth.specifyModelCredentialTip')
|
||||
}</div>
|
||||
</div>
|
||||
{
|
||||
!providerFormSchemaPredefined && (
|
||||
<SwitchCredentialInLoadBalancing
|
||||
provider={provider}
|
||||
customModelCredential={customModelCredential ?? initialCustomModelCredential}
|
||||
setCustomModelCredential={setCustomModelCredential}
|
||||
model={model}
|
||||
credentials={available_credentials}
|
||||
onUpdate={handleUpdateWhenSwitchCredential}
|
||||
onRemove={handleUpdateWhenSwitchCredential}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
modelCredential && (
|
||||
<ModelLoadBalancingConfigs {...{
|
||||
draftConfig,
|
||||
setDraftConfig,
|
||||
provider,
|
||||
currentCustomConfigurationModelFixedFields: {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
},
|
||||
configurationMethod: model.fetch_from,
|
||||
className: 'mt-2',
|
||||
modelCredential,
|
||||
onUpdate: handleUpdate,
|
||||
onRemove: handleUpdateWhenSwitchCredential,
|
||||
model: {
|
||||
model: model.model,
|
||||
model_type: model.model_type,
|
||||
},
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex items-center justify-between gap-2'>
|
||||
<div>
|
||||
{
|
||||
!providerFormSchemaPredefined && (
|
||||
<Button
|
||||
onClick={() => openConfirmDelete(undefined, { model: model.model, model_type: model.model_type })}
|
||||
className='text-components-button-destructive-secondary-text'
|
||||
>
|
||||
{t('common.modelProvider.auth.removeModel')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='space-x-2'>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={
|
||||
loading
|
||||
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
|
||||
|| isLoading
|
||||
}
|
||||
>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Modal >
|
||||
{
|
||||
deleteModel && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('common.modelProvider.confirmDelete')}
|
||||
onCancel={closeConfirmDelete}
|
||||
onConfirm={handleDeleteModel}
|
||||
isDisabled={doingAction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ModelLoadBalancingModal)
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Fragment } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { Popover, PopoverButton, PopoverPanel, Transition } from '@headlessui/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCheckLine,
|
||||
RiMoreFill,
|
||||
} from '@remixicon/react'
|
||||
import { PreferredProviderTypeEnum } from '../declarations'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type SelectorProps = {
|
||||
value?: string
|
||||
onSelect: (key: PreferredProviderTypeEnum) => void
|
||||
}
|
||||
const Selector: FC<SelectorProps> = ({
|
||||
value,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const options = [
|
||||
{
|
||||
key: PreferredProviderTypeEnum.custom,
|
||||
text: t('common.modelProvider.apiKey'),
|
||||
},
|
||||
{
|
||||
key: PreferredProviderTypeEnum.system,
|
||||
text: t('common.modelProvider.quota'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Popover className='relative'>
|
||||
<PopoverButton as='div'>
|
||||
{
|
||||
({ open }) => (
|
||||
<Button className={cn(
|
||||
'h-6 w-6 rounded-md px-0',
|
||||
open && 'bg-components-button-secondary-bg-hover',
|
||||
)}>
|
||||
<RiMoreFill className='h-3 w-3' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</PopoverButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave='transition ease-in duration-100'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<PopoverPanel className='absolute right-0 top-7 z-10 w-[144px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg'>
|
||||
<div className='p-1'>
|
||||
<div className='px-3 pb-1 pt-2 text-sm font-medium text-text-secondary'>{t('common.modelProvider.card.priorityUse')}</div>
|
||||
{
|
||||
options.map(option => (
|
||||
<PopoverButton as={Fragment} key={option.key}>
|
||||
<div
|
||||
className='flex h-9 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-components-panel-on-panel-item-bg-hover'
|
||||
onClick={() => onSelect(option.key)}
|
||||
>
|
||||
<div className='grow'>{option.text}</div>
|
||||
{value === option.key && <RiCheckLine className='h-4 w-4 text-text-accent' />}
|
||||
</div>
|
||||
</PopoverButton>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default Selector
|
||||
@@ -0,0 +1,19 @@
|
||||
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
|
||||
popupContent={t('common.modelProvider.priorityUsing') || ''}
|
||||
>
|
||||
<div className='absolute -right-[5px] -top-[5px] cursor-pointer rounded-[5px] border-[0.5px] border-components-panel-border-subtle bg-util-colors-indigo-indigo-50 shadow-xs'>
|
||||
<ChevronDownDouble className='h-3 w-3 rotate-180 text-util-colors-indigo-indigo-600' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriorityUseTip
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import {
|
||||
CustomConfigurationStatusEnum,
|
||||
PreferredProviderTypeEnum,
|
||||
QuotaUnitEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
MODEL_PROVIDER_QUOTA_GET_PAID,
|
||||
} from '../utils'
|
||||
import PriorityUseTip from './priority-use-tip'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
type QuotaPanelProps = {
|
||||
provider: ModelProvider
|
||||
}
|
||||
const QuotaPanel: FC<QuotaPanelProps> = ({
|
||||
provider,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
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)
|
||||
const openaiOrAnthropic = MODEL_PROVIDER_QUOTA_GET_PAID.includes(provider.provider)
|
||||
|
||||
return (
|
||||
<div className='group relative min-w-[112px] shrink-0 rounded-lg border-[0.5px] border-components-panel-border bg-white/[0.18] px-3 py-2 shadow-xs'>
|
||||
<div className='system-xs-medium-uppercase mb-2 flex h-4 items-center text-text-tertiary'>
|
||||
{t('common.modelProvider.quota')}
|
||||
<Tooltip popupContent={
|
||||
openaiOrAnthropic
|
||||
? t('common.modelProvider.card.tip')
|
||||
: t('common.modelProvider.quotaTip')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
currentQuota && (
|
||||
<div className='flex h-4 items-center text-xs text-text-tertiary'>
|
||||
<span className='system-md-semibold-uppercase mr-0.5 text-text-secondary'>{formatNumber(Math.max((currentQuota?.quota_limit || 0) - (currentQuota?.quota_used || 0), 0))}</span>
|
||||
{
|
||||
currentQuota?.quota_unit === QuotaUnitEnum.tokens && 'Tokens'
|
||||
}
|
||||
{
|
||||
currentQuota?.quota_unit === QuotaUnitEnum.times && t('common.modelProvider.callTimes')
|
||||
}
|
||||
{
|
||||
currentQuota?.quota_unit === QuotaUnitEnum.credits && t('common.modelProvider.credits')
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
priorityUseType === PreferredProviderTypeEnum.system && customConfig.status === CustomConfigurationStatusEnum.active && (
|
||||
<PriorityUseTip />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuotaPanel
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import { Openai } from '@/app/components/base/icons/src/vender/other'
|
||||
import { AnthropicDark, AnthropicLight } from '@/app/components/base/icons/src/public/llm'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
import { Theme } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
|
||||
type ProviderIconProps = {
|
||||
provider: ModelProvider
|
||||
className?: string
|
||||
}
|
||||
const ProviderIcon: FC<ProviderIconProps> = ({
|
||||
provider,
|
||||
className,
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const language = useLanguage()
|
||||
|
||||
if (provider.provider === 'langgenius/anthropic/anthropic') {
|
||||
return (
|
||||
<div className='mb-2 py-[7px]'>
|
||||
{theme === Theme.dark && <AnthropicLight className='h-2.5 w-[90px]' />}
|
||||
{theme === Theme.light && <AnthropicDark className='h-2.5 w-[90px]' />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (provider.provider === 'langgenius/openai/openai') {
|
||||
return (
|
||||
<div className='mb-2'>
|
||||
<Openai className='h-6 w-auto text-text-inverted-dimmed' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('inline-flex items-center gap-2', className)}>
|
||||
<img
|
||||
alt='provider-icon'
|
||||
src={renderI18nObject(provider.icon_small, language)}
|
||||
className='h-6 w-6'
|
||||
/>
|
||||
<div className='system-md-semibold text-text-primary'>
|
||||
{renderI18nObject(provider.label, language)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderIcon
|
||||
@@ -0,0 +1,265 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import ModelSelector from '../model-selector'
|
||||
import {
|
||||
useModelList,
|
||||
useSystemDefaultModelAndModelList,
|
||||
useUpdateModelList,
|
||||
} from '../hooks'
|
||||
import type {
|
||||
DefaultModel,
|
||||
DefaultModelResponse,
|
||||
} from '../declarations'
|
||||
import { ModelTypeEnum } from '../declarations'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { updateDefaultModel } from '@/service/common'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
type SystemModelSelectorProps = {
|
||||
textGenerationDefaultModel: DefaultModelResponse | undefined
|
||||
embeddingsDefaultModel: DefaultModelResponse | undefined
|
||||
rerankDefaultModel: DefaultModelResponse | undefined
|
||||
speech2textDefaultModel: DefaultModelResponse | undefined
|
||||
ttsDefaultModel: DefaultModelResponse | undefined
|
||||
notConfigured: boolean
|
||||
}
|
||||
const SystemModel: FC<SystemModelSelectorProps> = ({
|
||||
textGenerationDefaultModel,
|
||||
embeddingsDefaultModel,
|
||||
rerankDefaultModel,
|
||||
speech2textDefaultModel,
|
||||
ttsDefaultModel,
|
||||
notConfigured,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
|
||||
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
|
||||
const { data: speech2textModelList } = useModelList(ModelTypeEnum.speech2text)
|
||||
const { data: ttsModelList } = useModelList(ModelTypeEnum.tts)
|
||||
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 [currentTTSDefaultModel, changeCurrentTTSDefaultModel] = useSystemDefaultModelAndModelList(ttsDefaultModel, ttsModelList)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const getCurrentDefaultModelByModelType = (modelType: ModelTypeEnum) => {
|
||||
if (modelType === ModelTypeEnum.textGeneration)
|
||||
return currentTextGenerationDefaultModel
|
||||
else if (modelType === ModelTypeEnum.textEmbedding)
|
||||
return currentEmbeddingsDefaultModel
|
||||
else if (modelType === ModelTypeEnum.rerank)
|
||||
return currentRerankDefaultModel
|
||||
else if (modelType === ModelTypeEnum.speech2text)
|
||||
return currentSpeech2textDefaultModel
|
||||
else if (modelType === ModelTypeEnum.tts)
|
||||
return currentTTSDefaultModel
|
||||
|
||||
return undefined
|
||||
}
|
||||
const handleChangeDefaultModel = (modelType: ModelTypeEnum, model: DefaultModel) => {
|
||||
if (modelType === ModelTypeEnum.textGeneration)
|
||||
changeCurrentTextGenerationDefaultModel(model)
|
||||
else if (modelType === ModelTypeEnum.textEmbedding)
|
||||
changeCurrentEmbeddingsDefaultModel(model)
|
||||
else if (modelType === ModelTypeEnum.rerank)
|
||||
changeCurrentRerankDefaultModel(model)
|
||||
else if (modelType === ModelTypeEnum.speech2text)
|
||||
changeCurrentSpeech2textDefaultModel(model)
|
||||
else if (modelType === ModelTypeEnum.tts)
|
||||
changeCurrentTTSDefaultModel(model)
|
||||
|
||||
if (!changedModelTypes.includes(modelType))
|
||||
setChangedModelTypes([...changedModelTypes, modelType])
|
||||
}
|
||||
const handleSave = async () => {
|
||||
const res = await updateDefaultModel({
|
||||
url: '/workspaces/current/default-model',
|
||||
body: {
|
||||
model_settings: [ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank, ModelTypeEnum.speech2text, ModelTypeEnum.tts].map((modelType) => {
|
||||
return {
|
||||
model_type: modelType,
|
||||
provider: getCurrentDefaultModelByModelType(modelType)?.provider,
|
||||
model: getCurrentDefaultModelByModelType(modelType)?.model,
|
||||
}
|
||||
}),
|
||||
},
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
setOpen(false)
|
||||
|
||||
changedModelTypes.forEach((modelType) => {
|
||||
if (modelType === ModelTypeEnum.textGeneration)
|
||||
updateModelList(modelType)
|
||||
else if (modelType === ModelTypeEnum.textEmbedding)
|
||||
updateModelList(modelType)
|
||||
else if (modelType === ModelTypeEnum.rerank)
|
||||
updateModelList(modelType)
|
||||
else if (modelType === ModelTypeEnum.speech2text)
|
||||
updateModelList(modelType)
|
||||
else if (modelType === ModelTypeEnum.tts)
|
||||
updateModelList(modelType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 8,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<Button
|
||||
className='relative'
|
||||
variant={notConfigured ? 'primary' : 'secondary'}
|
||||
size='small'
|
||||
>
|
||||
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.modelProvider.systemModelSettings')}
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[60]'>
|
||||
<div className='w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg pt-4 shadow-xl'>
|
||||
<div className='px-6 py-1'>
|
||||
<div className='flex h-8 items-center text-[13px] font-medium text-text-primary'>
|
||||
{t('common.modelProvider.systemReasoningModel.key')}
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[261px] text-text-tertiary'>
|
||||
{t('common.modelProvider.systemReasoningModel.tip')}
|
||||
</div>
|
||||
}
|
||||
triggerClassName='ml-0.5 w-4 h-4 shrink-0'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
defaultModel={currentTextGenerationDefaultModel}
|
||||
modelList={textGenerationModelList}
|
||||
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.textGeneration, model)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 py-1'>
|
||||
<div className='flex h-8 items-center text-[13px] font-medium text-text-primary'>
|
||||
{t('common.modelProvider.embeddingModel.key')}
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[261px] text-text-tertiary'>
|
||||
{t('common.modelProvider.embeddingModel.tip')}
|
||||
</div>
|
||||
}
|
||||
triggerClassName='ml-0.5 w-4 h-4 shrink-0'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
defaultModel={currentEmbeddingsDefaultModel}
|
||||
modelList={embeddingModelList}
|
||||
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.textEmbedding, model)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 py-1'>
|
||||
<div className='flex h-8 items-center text-[13px] font-medium text-text-primary'>
|
||||
{t('common.modelProvider.rerankModel.key')}
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[261px] text-text-tertiary'>
|
||||
{t('common.modelProvider.rerankModel.tip')}
|
||||
</div>
|
||||
}
|
||||
triggerClassName='ml-0.5 w-4 h-4 shrink-0'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
defaultModel={currentRerankDefaultModel}
|
||||
modelList={rerankModelList}
|
||||
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.rerank, model)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 py-1'>
|
||||
<div className='flex h-8 items-center text-[13px] font-medium text-text-primary'>
|
||||
{t('common.modelProvider.speechToTextModel.key')}
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[261px] text-text-tertiary'>
|
||||
{t('common.modelProvider.speechToTextModel.tip')}
|
||||
</div>
|
||||
}
|
||||
triggerClassName='ml-0.5 w-4 h-4 shrink-0'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
defaultModel={currentSpeech2textDefaultModel}
|
||||
modelList={speech2textModelList}
|
||||
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.speech2text, model)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 py-1'>
|
||||
<div className='flex h-8 items-center text-[13px] font-medium text-text-primary'>
|
||||
{t('common.modelProvider.ttsModel.key')}
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[261px] text-text-tertiary'>
|
||||
{t('common.modelProvider.ttsModel.tip')}
|
||||
</div>
|
||||
}
|
||||
triggerClassName='ml-0.5 w-4 h-4 shrink-0'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ModelSelector
|
||||
defaultModel={currentTTSDefaultModel}
|
||||
modelList={ttsModelList}
|
||||
onSelect={model => handleChangeDefaultModel(ModelTypeEnum.tts, model)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-end px-6 py-4'>
|
||||
<Button
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className='ml-2'
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default SystemModel
|
||||
@@ -0,0 +1,201 @@
|
||||
import { ValidatedStatus } from '../key-validator/declarations'
|
||||
import type {
|
||||
CredentialFormSchemaTextInput,
|
||||
FormValue,
|
||||
ModelLoadBalancingConfig,
|
||||
} from './declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
FormTypeEnum,
|
||||
MODEL_TYPE_TEXT,
|
||||
ModelTypeEnum,
|
||||
} from './declarations'
|
||||
import {
|
||||
deleteModelProvider,
|
||||
setModelProvider,
|
||||
validateModelLoadBalancingCredentials,
|
||||
validateModelProvider,
|
||||
} from '@/service/common'
|
||||
|
||||
export const MODEL_PROVIDER_QUOTA_GET_PAID = ['langgenius/anthropic/anthropic', 'langgenius/openai/openai', 'langgenius/azure_openai/azure_openai']
|
||||
|
||||
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 validateLoadBalancingCredentials = async (predefined: boolean, provider: string, v: FormValue, id?: string): Promise<{
|
||||
status: ValidatedStatus
|
||||
message?: string
|
||||
}> => {
|
||||
const { __model_name, __model_type, ...credentials } = v
|
||||
try {
|
||||
const res = await validateModelLoadBalancingCredentials({
|
||||
url: `/workspaces/current/model-providers/${provider}/models/load-balancing-configs/${id ? `${id}/` : ''}credentials-validate`,
|
||||
body: {
|
||||
model: __model_name,
|
||||
model_type: __model_type,
|
||||
credentials,
|
||||
},
|
||||
})
|
||||
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, loadBalancing?: ModelLoadBalancingConfig) => {
|
||||
let body, url
|
||||
|
||||
if (predefined) {
|
||||
const { __authorization_name__, ...rest } = v
|
||||
body = {
|
||||
config_from: ConfigurationMethodEnum.predefinedModel,
|
||||
credentials: rest,
|
||||
load_balancing: loadBalancing,
|
||||
name: __authorization_name__,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${provider}/credentials`
|
||||
}
|
||||
else {
|
||||
const { __model_name, __model_type, ...credentials } = v
|
||||
body = {
|
||||
model: __model_name,
|
||||
model_type: __model_type,
|
||||
credentials,
|
||||
load_balancing: loadBalancing,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${provider}/models`
|
||||
}
|
||||
|
||||
return setModelProvider({ url, body })
|
||||
}
|
||||
|
||||
export const savePredefinedLoadBalancingConfig = async (provider: string, v: FormValue, loadBalancing?: ModelLoadBalancingConfig) => {
|
||||
const { __model_name, __model_type, ...credentials } = v
|
||||
const body = {
|
||||
config_from: ConfigurationMethodEnum.predefinedModel,
|
||||
model: __model_name,
|
||||
model_type: __model_type,
|
||||
credentials,
|
||||
load_balancing: loadBalancing,
|
||||
}
|
||||
const url = `/workspaces/current/model-providers/${provider}/models`
|
||||
|
||||
return setModelProvider({ url, body })
|
||||
}
|
||||
|
||||
export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue, credentialId?: string) => {
|
||||
let url = ''
|
||||
let body
|
||||
|
||||
if (predefined) {
|
||||
url = `/workspaces/current/model-providers/${provider}/credentials`
|
||||
if (credentialId) {
|
||||
body = {
|
||||
credential_id: credentialId,
|
||||
}
|
||||
}
|
||||
}
|
||||
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.select,
|
||||
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 any
|
||||
}
|
||||
|
||||
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 any
|
||||
}
|
||||
Reference in New Issue
Block a user