mirror of
https://github.com/langgenius/dify.git
synced 2026-08-28 20:21:58 +08:00
Co-authored-by: yunlu.wen <yunlu.wen@dify.ai> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Yunlu Wen <wylswz@163.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Yanli 盐粒 <yanli@dify.ai> Co-authored-by: 盐粒 Yanli <beautyyuyanli@gmail.com> Co-authored-by: zyssyz123 <916125788@qq.com> Co-authored-by: 盐粒 Yanli <mail@yanli.one>
343 lines
14 KiB
TypeScript
343 lines
14 KiB
TypeScript
import type { DefaultModel, Model } from '../declarations'
|
|
import type { ModelSelectorPreviewPayload } from './popup-item'
|
|
import type { ModelSelectorModelPredicate } from './types'
|
|
import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
|
|
import { ComboboxList } from '@langgenius/dify-ui/combobox'
|
|
import { createPreviewCardHandle, PreviewCard, PreviewCardContent } from '@langgenius/dify-ui/preview-card'
|
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
|
import { useTheme } from 'next-themes'
|
|
import { useCallback, useMemo, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { ACCOUNT_SETTING_MODAL_ACTION, ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
|
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
|
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 useWorkspacePluginInstallPermission from '@/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission'
|
|
import { IS_CLOUD_EDITION } from '@/config'
|
|
import { useProviderContext } from '@/context/provider-context'
|
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
|
import { useSearchParams } from '@/next/navigation'
|
|
import { consoleQuery } from '@/service/client'
|
|
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
|
import { CustomConfigurationStatusEnum, ModelFeatureEnum, ModelStatusEnum, ModelTypeEnum } from '../declarations'
|
|
import { useLanguage, useMarketplaceAllPlugins } from '../hooks'
|
|
import ModelBadge from '../model-badge'
|
|
import ModelIcon from '../model-icon'
|
|
import CreditsExhaustedAlert from '../provider-added-card/model-auth-dropdown/credits-exhausted-alert'
|
|
import { useTrialCredits } from '../provider-added-card/use-trial-credits'
|
|
import { providerSupportsCredits } from '../supports-credits'
|
|
import { MODEL_PROVIDER_QUOTA_GET_PAID, modelTypeFormat, providerKeyToPluginId, sizeFormat } from '../utils'
|
|
import FeatureIcon from './feature-icon'
|
|
import MarketplaceSection from './marketplace-section'
|
|
import { createModelSelectorSearchIndex, filterModelSelectorModels } from './model-search'
|
|
import ModelSelectorEmptyState from './popup-empty-state'
|
|
import PopupItem from './popup-item'
|
|
import { CompatibleModelsNotice, ModelProviderSettingsFooter, ModelSelectorPopupFrame, ModelSelectorScrollBody, ModelSelectorSearchHeader, ShowIncompatibleModelsButton } from './popup-layout'
|
|
|
|
export type PopupProps = {
|
|
defaultModel?: DefaultModel
|
|
inputValue: string
|
|
modelList: Model[]
|
|
scopeFeatures?: ModelFeatureEnum[]
|
|
hideProviderSettingsFooter?: boolean
|
|
providerSettingsSource?: 'agent'
|
|
modelPredicate?: ModelSelectorModelPredicate
|
|
onConfigureEmptyState?: () => void
|
|
onInputValueChange: (value: string) => void
|
|
onOpenMarketplace?: () => void
|
|
onHide: () => void
|
|
}
|
|
function Popup({
|
|
defaultModel,
|
|
inputValue,
|
|
modelList,
|
|
scopeFeatures = [],
|
|
hideProviderSettingsFooter,
|
|
providerSettingsSource,
|
|
modelPredicate,
|
|
onConfigureEmptyState,
|
|
onInputValueChange,
|
|
onOpenMarketplace,
|
|
onHide,
|
|
}: PopupProps) {
|
|
const { t } = useTranslation()
|
|
const searchParams = useSearchParams()
|
|
const { theme } = useTheme()
|
|
const language = useLanguage()
|
|
const previewCardHandle = useMemo(() => createPreviewCardHandle<ModelSelectorPreviewPayload>(), [])
|
|
const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false)
|
|
const [showIncompatibleModels, setShowIncompatibleModels] = useState(false)
|
|
const openIntegrationsSetting = useIntegrationsSetting()
|
|
const { modelProviders } = useProviderContext()
|
|
const { data: enableMarketplace } = useSuspenseQuery({
|
|
...systemFeaturesQueryOptions(),
|
|
select: systemFeatures => systemFeatures.enable_marketplace,
|
|
})
|
|
const {
|
|
plugins: allPlugins,
|
|
isLoading: isMarketplacePluginsLoading,
|
|
} = useMarketplaceAllPlugins(modelProviders, '', enableMarketplace)
|
|
const { mutateAsync: installPackageFromMarketPlace } = useInstallPackageFromMarketPlace()
|
|
const { refreshPluginList } = useRefreshPluginList()
|
|
const { canInstallPlugin } = useWorkspacePluginInstallPermission()
|
|
const [installingProvider, setInstallingProvider] = useState<ModelProviderQuotaGetPaid | null>(null)
|
|
const { isExhausted: isCreditsExhausted } = useTrialCredits()
|
|
const { data: trialModels = [] } = useQuery(consoleQuery.trialModels.get.queryOptions({
|
|
enabled: IS_CLOUD_EDITION,
|
|
select: data => data.trial_models,
|
|
}))
|
|
const installedProviderMap = useMemo(() => new Map(
|
|
modelProviders.map(provider => [provider.provider, provider]),
|
|
), [modelProviders])
|
|
const aiCreditVisibleProviders = useMemo(() => {
|
|
if (!enableMarketplace || isCreditsExhausted)
|
|
return new Set<string>()
|
|
|
|
return new Set(
|
|
modelProviders
|
|
.filter(provider => providerSupportsCredits(provider, trialModels))
|
|
.map(provider => provider.provider),
|
|
)
|
|
}, [enableMarketplace, isCreditsExhausted, modelProviders, trialModels])
|
|
const showCreditsExhaustedAlert = enableMarketplace
|
|
&& isCreditsExhausted
|
|
&& modelProviders.some(provider => providerSupportsCredits(provider, trialModels))
|
|
const hasApiKeyFallback = modelProviders.some((provider) => {
|
|
const isApiKeyActive = provider.custom_configuration?.status === CustomConfigurationStatusEnum.active
|
|
return isApiKeyActive && providerSupportsCredits(provider, trialModels)
|
|
})
|
|
|
|
const handleInstallPlugin = useCallback(async (key: ModelProviderQuotaGetPaid) => {
|
|
if (!enableMarketplace || !canInstallPlugin || !allPlugins || isMarketplacePluginsLoading || 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, enableMarketplace, canInstallPlugin, installPackageFromMarketPlace, installingProvider, isMarketplacePluginsLoading, refreshPluginList])
|
|
|
|
const installedModelList = useMemo(() => {
|
|
const modelMap = new Map(modelList.map(model => [model.provider, model]))
|
|
const installedMarketplaceModels = MODEL_PROVIDER_QUOTA_GET_PAID.flatMap((providerKey) => {
|
|
const installedProvider = installedProviderMap.get(providerKey)
|
|
|
|
if (!installedProvider)
|
|
return []
|
|
|
|
const matchedModel = modelMap.get(providerKey)
|
|
if (matchedModel)
|
|
return [matchedModel]
|
|
|
|
if (!aiCreditVisibleProviders.has(providerKey))
|
|
return []
|
|
|
|
return [{
|
|
provider: installedProvider.provider,
|
|
icon_small: installedProvider.icon_small,
|
|
icon_small_dark: installedProvider.icon_small_dark,
|
|
label: installedProvider.label,
|
|
models: [],
|
|
status: ModelStatusEnum.active,
|
|
}]
|
|
})
|
|
const otherModels = modelList.filter(model => !MODEL_PROVIDER_QUOTA_GET_PAID.includes(model.provider as ModelProviderQuotaGetPaid))
|
|
|
|
return [...installedMarketplaceModels, ...otherModels]
|
|
}, [aiCreditVisibleProviders, installedProviderMap, modelList])
|
|
|
|
const searchIndex = useMemo(
|
|
() => createModelSelectorSearchIndex(installedModelList, language),
|
|
[installedModelList, language],
|
|
)
|
|
const filteredModelList = useMemo(() => filterModelSelectorModels({
|
|
aiCreditVisibleProviders,
|
|
defaultModel,
|
|
inputValue,
|
|
installedModelList,
|
|
modelPredicate: showIncompatibleModels ? undefined : modelPredicate,
|
|
scopeFeatures,
|
|
searchIndex,
|
|
}), [aiCreditVisibleProviders, defaultModel, inputValue, installedModelList, modelPredicate, scopeFeatures, searchIndex, showIncompatibleModels])
|
|
const shouldShowModelPredicateReveal = !!modelPredicate
|
|
|
|
const marketplaceProviders = useMemo(() => {
|
|
if (!enableMarketplace)
|
|
return []
|
|
|
|
const installedProviders = new Set(modelProviders.map(provider => provider.provider))
|
|
return MODEL_PROVIDER_QUOTA_GET_PAID.filter(key => !installedProviders.has(key))
|
|
}, [enableMarketplace, modelProviders])
|
|
|
|
const handleOpenSettings = useCallback(() => {
|
|
onHide()
|
|
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER, source: providerSettingsSource })
|
|
}, [onHide, openIntegrationsSetting, providerSettingsSource])
|
|
const handleClosePreviewCard = useCallback(() => {
|
|
previewCardHandle.close()
|
|
}, [previewCardHandle])
|
|
const isProviderSettingsCurrentPage = searchParams?.get('action') === ACCOUNT_SETTING_MODAL_ACTION
|
|
&& searchParams?.get('tab') === ACCOUNT_SETTING_TAB.PROVIDER
|
|
const handleConfigureEmptyState = onConfigureEmptyState ?? (isProviderSettingsCurrentPage ? onHide : handleOpenSettings)
|
|
|
|
return (
|
|
<ModelSelectorPopupFrame>
|
|
<ModelSelectorSearchHeader
|
|
inputValue={inputValue}
|
|
onInputValueChange={onInputValueChange}
|
|
/>
|
|
{showCreditsExhaustedAlert && (
|
|
<CreditsExhaustedAlert hasApiKeyFallback={hasApiKeyFallback} />
|
|
)}
|
|
<ModelSelectorScrollBody label={t('modelProvider.models', { ns: 'common' })}>
|
|
<ComboboxList className="max-h-none overflow-visible p-0">
|
|
<div className="pb-1">
|
|
{
|
|
filteredModelList.map(model => (
|
|
<PopupItem
|
|
key={model.provider}
|
|
defaultModel={defaultModel}
|
|
model={model}
|
|
previewCardHandle={previewCardHandle}
|
|
onPreviewCardClose={handleClosePreviewCard}
|
|
onHide={onHide}
|
|
/>
|
|
))
|
|
}
|
|
</div>
|
|
</ComboboxList>
|
|
<div className="pb-1">
|
|
{!filteredModelList.length && !installedModelList.length && (
|
|
<ModelSelectorEmptyState
|
|
onConfigure={handleConfigureEmptyState}
|
|
/>
|
|
)}
|
|
{!filteredModelList.length && installedModelList.length > 0 && (
|
|
<div className="px-3 py-1.5 text-center text-xs/4.5 break-all text-text-tertiary">
|
|
{t('modelProvider.selector.noModelFoundForSearch', { ns: 'common', query: inputValue })}
|
|
</div>
|
|
)}
|
|
{scopeFeatures.length > 0 && (
|
|
<CompatibleModelsNotice />
|
|
)}
|
|
{shouldShowModelPredicateReveal && (
|
|
<ShowIncompatibleModelsButton
|
|
showIncompatibleModels={showIncompatibleModels}
|
|
onClick={() => setShowIncompatibleModels(value => !value)}
|
|
/>
|
|
)}
|
|
{enableMarketplace && (
|
|
<MarketplaceSection
|
|
marketplaceProviders={marketplaceProviders}
|
|
marketplaceCollapsed={marketplaceCollapsed}
|
|
installingProvider={installingProvider}
|
|
isMarketplacePluginsLoading={isMarketplacePluginsLoading}
|
|
canInstallPlugin={canInstallPlugin}
|
|
theme={theme}
|
|
onMarketplaceCollapsedChange={setMarketplaceCollapsed}
|
|
onInstallPlugin={handleInstallPlugin}
|
|
onOpenMarketplace={onOpenMarketplace}
|
|
/>
|
|
)}
|
|
</div>
|
|
</ModelSelectorScrollBody>
|
|
<PreviewCard handle={previewCardHandle}>
|
|
{({ payload }) => (
|
|
<ModelSelectorPreviewCard
|
|
capabilitiesLabel={t('model.capabilities', { ns: 'common' })}
|
|
language={language}
|
|
payload={payload as ModelSelectorPreviewPayload | undefined}
|
|
/>
|
|
)}
|
|
</PreviewCard>
|
|
{!hideProviderSettingsFooter && !isProviderSettingsCurrentPage && (
|
|
<ModelProviderSettingsFooter onOpenSettings={handleOpenSettings} />
|
|
)}
|
|
</ModelSelectorPopupFrame>
|
|
)
|
|
}
|
|
|
|
type ModelSelectorPreviewCardProps = {
|
|
capabilitiesLabel: string
|
|
language: string
|
|
payload?: ModelSelectorPreviewPayload
|
|
}
|
|
|
|
function ModelSelectorPreviewCard({
|
|
capabilitiesLabel,
|
|
language,
|
|
payload,
|
|
}: ModelSelectorPreviewCardProps) {
|
|
if (!payload)
|
|
return null
|
|
|
|
const { provider, modelItem } = payload
|
|
|
|
return (
|
|
<PreviewCardContent
|
|
placement="right"
|
|
popupClassName="w-[206px] bg-components-panel-bg-blur p-3 shadow-none backdrop-blur-xs"
|
|
>
|
|
<div className="flex flex-col gap-1">
|
|
<div className="flex flex-col items-start gap-2">
|
|
<ModelIcon
|
|
className="size-5 shrink-0"
|
|
provider={provider}
|
|
modelName={modelItem.model}
|
|
/>
|
|
<div className="system-md-medium text-wrap wrap-break-word text-text-primary">{modelItem.label[language] || modelItem.label.en_US}</div>
|
|
</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>
|
|
{[ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank].includes(modelItem.model_type as ModelTypeEnum)
|
|
&& modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature))
|
|
&& (
|
|
<div className="pt-2">
|
|
<div className="mb-1 system-2xs-medium-uppercase text-text-tertiary">{capabilitiesLabel}</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{modelItem.features?.map(feature => (
|
|
<FeatureIcon
|
|
key={feature}
|
|
feature={feature}
|
|
showFeaturesLabel
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</PreviewCardContent>
|
|
)
|
|
}
|
|
|
|
export default Popup
|