-
setCollapsed(prev => !prev)}
- >
- {model.label[language] || model.label.en_US}
-
+
+
setCollapsed(prev => !prev)}
+ >
+ {model.label[language] || model.label.en_US}
+
+
+
+ {isUsingCredits
+ ? (
+ hasCredits
+ ? (
+ <>
+
+ {t('modelProvider.selector.aiCredits', { ns: 'common' })}
+ >
+ )
+ : (
+ <>
+
+ {t('modelProvider.selector.creditsExhausted', { ns: 'common' })}
+ >
+ )
+ )
+ : credentialName
+ ? (
+ <>
+
+ {credentialName}
+ >
+ )
+ : (
+ <>
+
+ {t('modelProvider.selector.configureRequired', { ns: 'common' })}
+ >
+ )}
+
+
+
{!collapsed && model.models.map(modelItem => (
= ({
{
defaultModel?.model === modelItem.model && defaultModel.provider === currentProvider.provider && (
-
+
)
}
{
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx
index 93446e1a36..dea08ab836 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx
@@ -1,22 +1,27 @@
-import type { FC } from 'react'
+import type { FC, RefObject } from 'react'
import type {
DefaultModel,
Model,
ModelItem,
} from '../declarations'
-import {
- RiArrowRightUpLine,
- RiSearchLine,
-} from '@remixicon/react'
-import { useEffect, useMemo, useRef, useState } from 'react'
+import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
+import { useTheme } from 'next-themes'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
+import Button from '@/app/components/base/button'
import { tooltipManager } from '@/app/components/base/tooltip/TooltipManager'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
+import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
+import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { useModalContext } from '@/context/modal-context'
+import { useProviderContext } from '@/context/provider-context'
+import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
+import { cn } from '@/utils/classnames'
import { supportFunctionCall } from '@/utils/tool-call'
+import { getMarketplaceUrl } from '@/utils/var'
import { ModelFeatureEnum } from '../declarations'
-import { useLanguage } from '../hooks'
+import { useLanguage, useMarketplaceAllPlugins } from '../hooks'
+import { MODEL_PROVIDER_QUOTA_GET_PAID, modelNameMap, providerIconMap, providerKeyToPluginId } from '../utils'
import PopupItem from './popup-item'
type PopupProps = {
@@ -25,6 +30,7 @@ type PopupProps = {
onSelect: (provider: string, model: ModelItem) => void
scopeFeatures?: ModelFeatureEnum[]
onHide: () => void
+ triggerRef?: RefObject
}
const Popup: FC = ({
defaultModel,
@@ -32,12 +38,48 @@ const Popup: FC = ({
onSelect,
scopeFeatures = [],
onHide,
+ triggerRef,
}) => {
const { t } = useTranslation()
+ const { theme } = useTheme()
const language = useLanguage()
const [searchText, setSearchText] = useState('')
+ const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false)
const { setShowAccountSettingModal } = useModalContext()
+ const { modelProviders } = useProviderContext()
const scrollRef = useRef(null)
+ const triggerWidth = triggerRef?.current?.offsetWidth
+
+ const {
+ plugins: allPlugins,
+ } = useMarketplaceAllPlugins(modelProviders, '')
+ const { mutateAsync: installPackageFromMarketPlace } = useInstallPackageFromMarketPlace()
+ const { refreshPluginList } = useRefreshPluginList()
+ const [installingProvider, setInstallingProvider] = useState(null)
+
+ const handleInstallPlugin = useCallback(async (key: ModelProviderQuotaGetPaid) => {
+ if (!allPlugins || installingProvider)
+ return
+ const pluginId = providerKeyToPluginId[key]
+ const plugin = allPlugins.find(p => p.plugin_id === pluginId)
+ if (!plugin)
+ return
+
+ const uniqueIdentifier = plugin.latest_package_identifier
+ setInstallingProvider(key)
+ try {
+ const { all_installed, task_id } = await installPackageFromMarketPlace(uniqueIdentifier)
+ if (!all_installed) {
+ const { check } = checkTaskStatus()
+ await check({ taskId: task_id, pluginUniqueIdentifier: uniqueIdentifier })
+ }
+ refreshPluginList(plugin)
+ }
+ catch { }
+ finally {
+ setInstallingProvider(null)
+ }
+ }, [allPlugins, installingProvider, installPackageFromMarketPlace, refreshPluginList])
// Close any open tooltips when the user scrolls to prevent them from appearing
// in incorrect positions or becoming detached from their trigger elements
@@ -81,17 +123,22 @@ const Popup: FC = ({
}).filter(model => model.models.length > 0)
}, [language, modelList, scopeFeatures, searchText])
+ const marketplaceProviders = useMemo(() => {
+ const installedProviders = new Set(modelList.map(m => m.provider))
+ return MODEL_PROVIDER_QUOTA_GET_PAID.filter(key => !installedProviders.has(key))
+ }, [modelList])
+
return (
-
+
-
@@ -103,8 +150,8 @@ const Popup: FC
= ({
/>
{
searchText && (
- setSearchText('')}
/>
)
@@ -122,13 +169,98 @@ const Popup: FC = ({
/>
))
}
- {
- !filteredModelList.length && (
-
- {`No model found for “${searchText}”`}
+ {!filteredModelList.length && !modelList.length && (
+
+
+
- )
- }
+
+
+ {t('modelProvider.selector.noProviderConfigured', { ns: 'common' })}
+
+
+ {t('modelProvider.selector.noProviderConfiguredDesc', { ns: 'common' })}
+
+
+
+
+ )}
+ {!filteredModelList.length && modelList.length > 0 && (
+
+ {`No model found for \u201C${searchText}\u201D`}
+
+ )}
+ {marketplaceProviders.length > 0 && (
+ <>
+
+
+
+
setMarketplaceCollapsed(prev => !prev)}
+ >
+ {t('modelProvider.selector.fromMarketplace', { ns: 'common' })}
+
+
+
+ {!marketplaceCollapsed && (
+ <>
+ {marketplaceProviders.map((key) => {
+ const Icon = providerIconMap[key]
+ const isInstalling = installingProvider === key
+ return (
+
+
+
+ {modelNameMap[key]}
+
+
+
+ )
+ })}
+
+
+ {t('modelProvider.selector.discoverMoreInMarketplace', { ns: 'common' })}
+
+
+
+ >
+ )}
+
+ >
+ )}
= ({
}}
>
{t('model.settingsLink', { ns: 'common' })}
-
+
)
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx
index 36a15b2a42..e9c8f930b9 100644
--- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx
@@ -1,33 +1,22 @@
-import type { ComponentType, FC } from 'react'
+import type { FC } from 'react'
import type { ModelProvider } from '../declarations'
import type { Plugin } from '@/app/components/plugins/types'
+import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { AnthropicShortLight, Deepseek, Gemini, Grok, OpenaiSmall, Tongyi } from '@/app/components/base/icons/src/public/llm'
import Loading from '@/app/components/base/loading'
import Tooltip from '@/app/components/base/tooltip'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useTimestamp from '@/hooks/use-timestamp'
-import { ModelProviderQuotaGetPaid } from '@/types/model-provider'
import { cn } from '@/utils/classnames'
import { formatNumber } from '@/utils/format'
import { PreferredProviderTypeEnum } from '../declarations'
import { useMarketplaceAllPlugins } from '../hooks'
-import { MODEL_PROVIDER_QUOTA_GET_PAID, modelNameMap } from '../utils'
-
-// Icon map for each provider - single source of truth for provider icons
-const providerIconMap: Record
> = {
- [ModelProviderQuotaGetPaid.OPENAI]: OpenaiSmall,
- [ModelProviderQuotaGetPaid.ANTHROPIC]: AnthropicShortLight,
- [ModelProviderQuotaGetPaid.GEMINI]: Gemini,
- [ModelProviderQuotaGetPaid.X]: Grok,
- [ModelProviderQuotaGetPaid.DEEPSEEK]: Deepseek,
- [ModelProviderQuotaGetPaid.TONGYI]: Tongyi,
-}
+import { MODEL_PROVIDER_QUOTA_GET_PAID, modelNameMap, providerIconMap, providerKeyToPluginId } from '../utils'
// Derive allProviders from the shared constant
const allProviders = MODEL_PROVIDER_QUOTA_GET_PAID.map(key => ({
@@ -35,17 +24,6 @@ const allProviders = MODEL_PROVIDER_QUOTA_GET_PAID.map(key => ({
Icon: providerIconMap[key],
}))
-// Map provider key to plugin ID
-// provider key format: langgenius/provider/model, plugin ID format: langgenius/provider
-const providerKeyToPluginId: Record = {
- [ModelProviderQuotaGetPaid.OPENAI]: 'langgenius/openai',
- [ModelProviderQuotaGetPaid.ANTHROPIC]: 'langgenius/anthropic',
- [ModelProviderQuotaGetPaid.GEMINI]: 'langgenius/gemini',
- [ModelProviderQuotaGetPaid.X]: 'langgenius/x',
- [ModelProviderQuotaGetPaid.DEEPSEEK]: 'langgenius/deepseek',
- [ModelProviderQuotaGetPaid.TONGYI]: 'langgenius/tongyi',
-}
-
type QuotaPanelProps = {
providers: ModelProvider[]
isLoading?: boolean
diff --git a/web/app/components/header/account-setting/model-provider-page/utils.ts b/web/app/components/header/account-setting/model-provider-page/utils.ts
index 21e32ad178..4fe6af0d71 100644
--- a/web/app/components/header/account-setting/model-provider-page/utils.ts
+++ b/web/app/components/header/account-setting/model-provider-page/utils.ts
@@ -1,9 +1,11 @@
+import type { ComponentType } from 'react'
import type {
CredentialFormSchemaSelect,
CredentialFormSchemaTextInput,
FormValue,
ModelLoadBalancingConfig,
} from './declarations'
+import { AnthropicShortLight, Deepseek, Gemini, Grok, OpenaiSmall, Tongyi } from '@/app/components/base/icons/src/public/llm'
import {
deleteModelProvider,
setModelProvider,
@@ -23,6 +25,24 @@ export { ModelProviderQuotaGetPaid } from '@/types/model-provider'
export const MODEL_PROVIDER_QUOTA_GET_PAID = [ModelProviderQuotaGetPaid.OPENAI, ModelProviderQuotaGetPaid.ANTHROPIC, ModelProviderQuotaGetPaid.GEMINI, ModelProviderQuotaGetPaid.X, ModelProviderQuotaGetPaid.DEEPSEEK, ModelProviderQuotaGetPaid.TONGYI]
+export const providerIconMap: Record> = {
+ [ModelProviderQuotaGetPaid.OPENAI]: OpenaiSmall,
+ [ModelProviderQuotaGetPaid.ANTHROPIC]: AnthropicShortLight,
+ [ModelProviderQuotaGetPaid.GEMINI]: Gemini,
+ [ModelProviderQuotaGetPaid.X]: Grok,
+ [ModelProviderQuotaGetPaid.DEEPSEEK]: Deepseek,
+ [ModelProviderQuotaGetPaid.TONGYI]: Tongyi,
+}
+
+export const providerKeyToPluginId: Record = {
+ [ModelProviderQuotaGetPaid.OPENAI]: 'langgenius/openai',
+ [ModelProviderQuotaGetPaid.ANTHROPIC]: 'langgenius/anthropic',
+ [ModelProviderQuotaGetPaid.GEMINI]: 'langgenius/gemini',
+ [ModelProviderQuotaGetPaid.X]: 'langgenius/x',
+ [ModelProviderQuotaGetPaid.DEEPSEEK]: 'langgenius/deepseek',
+ [ModelProviderQuotaGetPaid.TONGYI]: 'langgenius/tongyi',
+}
+
export const modelNameMap = {
[ModelProviderQuotaGetPaid.OPENAI]: 'OpenAI',
[ModelProviderQuotaGetPaid.ANTHROPIC]: 'Anthropic',
diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json
index 9170472642..57a0d2d939 100644
--- a/web/i18n/en-US/common.json
+++ b/web/i18n/en-US/common.json
@@ -403,8 +403,17 @@
"modelProvider.resetDate": "Reset on {{date}}",
"modelProvider.searchModel": "Search model",
"modelProvider.selectModel": "Select your model",
+ "modelProvider.selector.aiCredits": "AI credits",
+ "modelProvider.selector.configure": "Configure",
+ "modelProvider.selector.configureRequired": "Configure required",
+ "modelProvider.selector.creditsExhausted": "Credits exhausted",
+ "modelProvider.selector.discoverMoreInMarketplace": "Discover more in Marketplace",
"modelProvider.selector.emptySetting": "Please go to settings to configure",
"modelProvider.selector.emptyTip": "No available models",
+ "modelProvider.selector.fromMarketplace": "From Marketplace",
+ "modelProvider.selector.install": "Install",
+ "modelProvider.selector.noProviderConfigured": "No model provider configured",
+ "modelProvider.selector.noProviderConfiguredDesc": "Browse Marketplace to install one, or configure providers in settings.",
"modelProvider.selector.rerankTip": "Please set up the Rerank model",
"modelProvider.selector.tip": "This model has been removed. Please add a model or select another model.",
"modelProvider.setupModelFirst": "Please set up your model first",