diff --git a/web/app/components/header/account-setting/model-provider-page/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/index.spec.tsx index ede9c1f7fe..27cead7eb2 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.spec.tsx @@ -83,6 +83,10 @@ vi.mock('./system-model-selector', () => ({ default: () =>
, })) +vi.mock('@/service/use-plugins', () => ({ + useCheckInstalled: () => ({ data: undefined }), +})) + describe('ModelProviderPage', () => { beforeEach(() => { vi.useFakeTimers() diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx index 6e4c7663b4..9a0a046fcc 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.tsx @@ -1,11 +1,13 @@ import type { ModelProvider, } from './declarations' +import type { PluginDetail } from '@/app/components/plugins/types' import { useDebounce } from 'ahooks' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useSystemFeaturesQuery } from '@/context/global-public-context' import { useProviderContext } from '@/context/provider-context' +import { useCheckInstalled } from '@/service/use-plugins' import { cn } from '@/utils/classnames' import { CustomConfigurationStatusEnum, @@ -18,6 +20,7 @@ import InstallFromMarketplace from './install-from-marketplace' import ProviderAddedCard from './provider-added-card' import QuotaPanel from './provider-added-card/quota-panel' import SystemModelSelector from './system-model-selector' +import { providerToPluginId } from './utils' type SystemModelConfigStatus = 'no-provider' | 'none-configured' | 'partially-configured' | 'fully-configured' @@ -37,6 +40,22 @@ const ModelProviderPage = ({ searchText }: Props) => { const { data: ttsDefaultModel, isLoading: isTTSDefaultModelLoading } = useDefaultModel(ModelTypeEnum.tts) const { modelProviders: providers } = useProviderContext() const { data: systemFeatures } = useSystemFeaturesQuery() + + const allPluginIds = useMemo(() => { + return [...new Set(providers.map(p => providerToPluginId(p.provider)).filter(Boolean))] + }, [providers]) + const { data: installedPlugins } = useCheckInstalled({ + pluginIds: allPluginIds, + enabled: allPluginIds.length > 0, + }) + const pluginDetailMap = useMemo(() => { + const map = new Map() + if (installedPlugins?.plugins) { + for (const plugin of installedPlugins.plugins) + map.set(plugin.plugin_id, plugin) + } + return map + }, [installedPlugins]) const enableMarketplace = systemFeatures?.enable_marketplace ?? false const isDefaultModelLoading = isTextGenerationDefaultModelLoading || isEmbeddingsDefaultModelLoading @@ -150,6 +169,7 @@ const ModelProviderPage = ({ searchText }: Props) => { ))}
@@ -163,6 +183,7 @@ const ModelProviderPage = ({ searchText }: Props) => { notConfigured key={provider.provider} provider={provider} + pluginDetail={pluginDetailMap.get(providerToPluginId(provider.provider))} /> ))} diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx index 3243e5ac86..8361f6068d 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx @@ -4,6 +4,7 @@ import type { ModelProvider, } from '../declarations' import type { ModelProviderQuotaGetPaid } from '../utils' +import type { PluginDetail } from '@/app/components/plugins/types' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -14,6 +15,7 @@ import { import { IS_CE_EDITION } from '@/config' import { useAppContext } from '@/context/app-context' import { useEventEmitterContextContext } from '@/context/event-emitter' +import { useProviderContext } from '@/context/provider-context' import { fetchModelProviderModelList } from '@/service/common' import { cn } from '@/utils/classnames' import { ConfigurationMethodEnum } from '../declarations' @@ -25,18 +27,22 @@ import { } from '../utils' import CredentialPanel from './credential-panel' import ModelList from './model-list' +import ProviderCardActions from './provider-card-actions' export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST' type ProviderAddedCardProps = { notConfigured?: boolean provider: ModelProvider + pluginDetail?: PluginDetail } const ProviderAddedCard: FC = ({ notConfigured, provider, + pluginDetail, }) => { const { t } = useTranslation() const { eventEmitter } = useEventEmitterContextContext() + const { refreshModelProviders } = useProviderContext() const [fetched, setFetched] = useState(false) const [loading, setLoading] = useState(false) const [collapsed, setCollapsed] = useState(true) @@ -87,27 +93,28 @@ const ProviderAddedCard: FC = ({ >
- +
+ + {pluginDetail && ( + + )} +
- { - provider.supported_model_types.map(modelType => ( - - {modelTypeFormat(modelType)} - - )) - } + {provider.supported_model_types.map(modelType => ( + + {modelTypeFormat(modelType)} + + ))}
- { - showCredential && ( - - ) - } + {showCredential && ( + + )}
{ collapsed && ( diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx new file mode 100644 index 0000000000..a732a76a1c --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx @@ -0,0 +1,111 @@ +import type { FC } from 'react' +import type { PluginDetail } from '@/app/components/plugins/types' +import { useMemo } from 'react' +import Badge from '@/app/components/base/badge' +import { HeaderModals } from '@/app/components/plugins/plugin-detail-panel/detail-header/components' +import { useDetailHeaderState, usePluginOperations } from '@/app/components/plugins/plugin-detail-panel/detail-header/hooks' +import OperationDropdown from '@/app/components/plugins/plugin-detail-panel/operation-dropdown' +import { PluginSource } from '@/app/components/plugins/types' +import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker' +import { useLocale } from '@/context/i18n' +import useTheme from '@/hooks/use-theme' +import { cn } from '@/utils/classnames' +import { getMarketplaceUrl } from '@/utils/var' + +type Props = { + detail: PluginDetail + onUpdate?: () => void +} + +const ProviderCardActions: FC = ({ detail, onUpdate }) => { + const { theme } = useTheme() + const locale = useLocale() + + const { source, version, meta } = detail + const author = detail.declaration?.author ?? '' + const name = detail.declaration?.name ?? detail.name + + const { + modalStates, + versionPicker, + hasNewVersion, + isAutoUpgradeEnabled, + isFromMarketplace, + } = useDetailHeaderState(detail) + + const { + handleUpdate, + handleUpdatedFromMarketplace, + handleDelete, + } = usePluginOperations({ + detail, + modalStates, + versionPicker, + isFromMarketplace, + onUpdate, + }) + + const handleVersionSelect = (state: { version: string, unique_identifier: string, isDowngrade?: boolean }) => { + versionPicker.setTargetVersion(state) + handleUpdate(state.isDowngrade) + } + + const detailUrl = useMemo(() => { + if (source === PluginSource.github) + return meta?.repo ? `https://github.com/${meta.repo}` : '' + if (source === PluginSource.marketplace) + return getMarketplaceUrl(`/plugins/${author}/${name}`, { language: locale, theme }) + return '' + }, [source, meta?.repo, author, name, locale, theme]) + + return ( + <> + {!!version && ( + + {version} + {isFromMarketplace && } + {hasNewVersion && ( + + )} + + )} + /> + )} + + handleUpdate()} + onRemove={modalStates.showDeleteConfirm} + detailUrl={detailUrl} + /> + + + + ) +} + +export default ProviderCardActions diff --git a/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx index 199e6c931c..17382568d1 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx @@ -21,7 +21,7 @@ const ProviderIcon: FC = ({ if (provider.provider === 'langgenius/anthropic/anthropic') { return ( -
+
{theme === Theme.dark && } {theme === Theme.light && }
@@ -30,7 +30,7 @@ const ProviderIcon: FC = ({ if (provider.provider === 'langgenius/openai/openai') { return ( -
+
) 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..7b8928ec63 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 @@ -21,6 +21,11 @@ import { export { ModelProviderQuotaGetPaid } from '@/types/model-provider' +export const providerToPluginId = (providerKey: string): string => { + const lastSlash = providerKey.lastIndexOf('/') + return lastSlash > 0 ? providerKey.slice(0, lastSlash) : '' +} + export const MODEL_PROVIDER_QUOTA_GET_PAID = [ModelProviderQuotaGetPaid.OPENAI, ModelProviderQuotaGetPaid.ANTHROPIC, ModelProviderQuotaGetPaid.GEMINI, ModelProviderQuotaGetPaid.X, ModelProviderQuotaGetPaid.DEEPSEEK, ModelProviderQuotaGetPaid.TONGYI] export const modelNameMap = {