From e371bfd676d741efd27598dddaae022bbe4a6ba0 Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Fri, 6 Mar 2026 14:18:29 +0800 Subject: [PATCH] refactor: enhance model provider management with new icons, improved UI elements, and marketplace integration --- .../assets/vender/solid/general/x-circle.svg | 2 +- web/app/components/base/tag-input/index.tsx | 6 +- .../model-selector/index.tsx | 6 +- .../model-selector/popup-item.tsx | 60 +++++- .../model-selector/popup.tsx | 172 ++++++++++++++++-- .../provider-added-card/quota-panel.tsx | 28 +-- .../model-provider-page/utils.ts | 20 ++ web/i18n/en-US/common.json | 9 + 8 files changed, 246 insertions(+), 57 deletions(-) diff --git a/web/app/components/base/icons/assets/vender/solid/general/x-circle.svg b/web/app/components/base/icons/assets/vender/solid/general/x-circle.svg index 5acbe5f562..fd4461dae2 100644 --- a/web/app/components/base/icons/assets/vender/solid/general/x-circle.svg +++ b/web/app/components/base/icons/assets/vender/solid/general/x-circle.svg @@ -1,3 +1,3 @@ - + diff --git a/web/app/components/base/tag-input/index.tsx b/web/app/components/base/tag-input/index.tsx index 1c49b026fb..343386143c 100644 --- a/web/app/components/base/tag-input/index.tsx +++ b/web/app/components/base/tag-input/index.tsx @@ -1,10 +1,14 @@ import type { ChangeEvent, FC, KeyboardEvent } from 'react' import { useCallback, useState } from 'react' -import AutosizeInput from 'react-18-input-autosize' +import _AutosizeInput from 'react-18-input-autosize' import { useTranslation } from 'react-i18next' import { useToastContext } from '@/app/components/base/toast' import { cn } from '@/utils/classnames' +// CJS/ESM interop: Turbopack may resolve the module namespace object instead of the default export +// eslint-disable-next-line ts/no-explicit-any +const AutosizeInput = ('default' in (_AutosizeInput as any) ? (_AutosizeInput as any).default : _AutosizeInput) as typeof _AutosizeInput + type TagInputProps = { items: string[] onChange: (items: string[]) => void diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx index 50b56826eb..a4f8eb9a64 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx @@ -5,7 +5,7 @@ import type { ModelFeatureEnum, ModelItem, } from '../declarations' -import { useState } from 'react' +import { useRef, useState } from 'react' import { PortalToFollowElem, PortalToFollowElemContent, @@ -41,6 +41,7 @@ const ModelSelector: FC = ({ showDeprecatedWarnIcon = false, }) => { const [open, setOpen] = useState(false) + const triggerRef = useRef(null) const { currentProvider, currentModel, @@ -70,7 +71,7 @@ const ModelSelector: FC = ({ placement="bottom-start" offset={4} > -
+
= ({ onSelect={handleSelect} scopeFeatures={scopeFeatures} onHide={() => setOpen(false)} + triggerRef={triggerRef} />
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx index 25415fe0d9..5267f8b3d3 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx @@ -7,16 +7,18 @@ import type { import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { Check } from '@/app/components/base/icons/src/vender/line/general' import Tooltip from '@/app/components/base/tooltip' +import { useAppContext } from '@/context/app-context' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { cn } from '@/utils/classnames' import { ConfigurationMethodEnum, + CustomConfigurationStatusEnum, ModelFeatureEnum, ModelStatusEnum, ModelTypeEnum, + PreferredProviderTypeEnum, } from '../declarations' import { useLanguage, @@ -45,6 +47,7 @@ const PopupItem: FC = ({ const [collapsed, setCollapsed] = useState(false) const { t } = useTranslation() const language = useLanguage() + const { currentWorkspace } = useAppContext() const { setShowModelModal } = useModalContext() const { modelProviders } = useProviderContext() const updateModelList = useUpdateModelList() @@ -73,14 +76,55 @@ const PopupItem: FC = ({ }) } + const isUsingCredits = currentProvider?.preferred_provider_type === PreferredProviderTypeEnum.system + const credits = Math.max((currentWorkspace.trial_credits - currentWorkspace.trial_credits_used) || 0, 0) + const hasCredits = credits > 0 + const isApiKeyActive = currentProvider?.custom_configuration.status === CustomConfigurationStatusEnum.active + const credentialName = currentProvider?.custom_configuration.current_credential_name + return (
-
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",