mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 20:48:01 +08:00
Signed-off-by: yyh <yuanyouhuilyz@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Coding On Star <447357187@qq.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: statxc <tyleradams93226@gmail.com>
347 lines
15 KiB
TypeScript
347 lines
15 KiB
TypeScript
import type { FC } from 'react'
|
|
import type {
|
|
DefaultModel,
|
|
Model,
|
|
ModelItem,
|
|
} from '../declarations'
|
|
import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
|
|
import { useTheme } from 'next-themes'
|
|
import { useCallback, useMemo, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import Button from '@/app/components/base/button'
|
|
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 { useSystemFeaturesQuery } from '@/context/global-public-context'
|
|
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 {
|
|
CustomConfigurationStatusEnum,
|
|
ModelFeatureEnum,
|
|
ModelStatusEnum,
|
|
} from '../declarations'
|
|
import { useLanguage, useMarketplaceAllPlugins } from '../hooks'
|
|
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, modelNameMap, providerIconMap, providerKeyToPluginId } from '../utils'
|
|
import PopupItem from './popup-item'
|
|
|
|
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 { theme } = useTheme()
|
|
const language = useLanguage()
|
|
const [searchText, setSearchText] = useState('')
|
|
const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false)
|
|
const { setShowAccountSettingModal } = useModalContext()
|
|
const { modelProviders } = useProviderContext()
|
|
const {
|
|
plugins: allPlugins,
|
|
isLoading: isMarketplacePluginsLoading,
|
|
} = useMarketplaceAllPlugins(modelProviders, '')
|
|
const { mutateAsync: installPackageFromMarketPlace } = useInstallPackageFromMarketPlace()
|
|
const { refreshPluginList } = useRefreshPluginList()
|
|
const [installingProvider, setInstallingProvider] = useState<ModelProviderQuotaGetPaid | null>(null)
|
|
const { isExhausted: isCreditsExhausted } = useTrialCredits()
|
|
const { data: systemFeatures } = useSystemFeaturesQuery()
|
|
const trialModels = systemFeatures?.trial_models
|
|
const installedProviderMap = useMemo(() => new Map(
|
|
modelProviders.map(provider => [provider.provider, provider]),
|
|
), [modelProviders])
|
|
const aiCreditVisibleProviders = useMemo(() => {
|
|
if (isCreditsExhausted)
|
|
return new Set<string>()
|
|
|
|
return new Set(
|
|
modelProviders
|
|
.filter(provider => providerSupportsCredits(provider, trialModels))
|
|
.map(provider => provider.provider),
|
|
)
|
|
}, [isCreditsExhausted, modelProviders, trialModels])
|
|
const showCreditsExhaustedAlert = 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 (!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, 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 filteredModelList = useMemo(() => {
|
|
const filtered = installedModelList.map((model) => {
|
|
const matchesProviderSearch = !searchText
|
|
|| model.provider.toLowerCase().includes(searchText.toLowerCase())
|
|
|| Object.values(model.label).some(label => label.toLowerCase().includes(searchText.toLowerCase()))
|
|
|
|
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
|
|
})
|
|
})
|
|
if (!matchesProviderSearch || (filteredModels.length === 0 && !aiCreditVisibleProviders.has(model.provider)))
|
|
return null
|
|
|
|
return { ...model, models: filteredModels }
|
|
}).filter((model): model is Model => model !== null)
|
|
|
|
if (defaultModel?.provider) {
|
|
filtered.sort((a, b) => {
|
|
const aSelected = a.provider === defaultModel.provider ? 0 : 1
|
|
const bSelected = b.provider === defaultModel.provider ? 0 : 1
|
|
return aSelected - bSelected
|
|
})
|
|
}
|
|
|
|
return filtered
|
|
}, [aiCreditVisibleProviders, defaultModel?.provider, installedModelList, language, scopeFeatures, searchText])
|
|
|
|
const marketplaceProviders = useMemo(() => {
|
|
const installedProviders = new Set(modelProviders.map(provider => provider.provider))
|
|
return MODEL_PROVIDER_QUOTA_GET_PAID.filter(key => !installedProviders.has(key))
|
|
}, [modelProviders])
|
|
|
|
return (
|
|
<div className="max-h-[480px] overflow-y-auto no-scrollbar">
|
|
<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'}
|
|
`}
|
|
>
|
|
<span
|
|
className={`
|
|
i-ri-search-line 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('form.searchModel', { ns: 'datasetSettings' }) || ''}
|
|
value={searchText}
|
|
onChange={e => setSearchText(e.target.value)}
|
|
/>
|
|
{
|
|
searchText && (
|
|
<span
|
|
className="i-custom-vender-solid-general-x-circle ml-1.5 h-[14px] w-[14px] shrink-0 cursor-pointer text-text-quaternary"
|
|
onClick={() => setSearchText('')}
|
|
/>
|
|
)
|
|
}
|
|
</div>
|
|
{scopeFeatures.length > 0 && (
|
|
<div
|
|
data-testid="compatible-models-banner"
|
|
className="mt-2 flex items-center gap-1 rounded-lg bg-background-section-burn px-2.5 py-2"
|
|
>
|
|
<span className="i-ri-information-2-fill h-4 w-4 shrink-0 text-text-accent" />
|
|
<p className="text-text-secondary system-xs-medium">
|
|
{t('modelProvider.selector.onlyCompatibleModelsShown', { ns: 'common' })}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{showCreditsExhaustedAlert && (
|
|
<CreditsExhaustedAlert hasApiKeyFallback={hasApiKeyFallback} />
|
|
)}
|
|
<div className="px-1 pb-1">
|
|
{
|
|
filteredModelList.map(model => (
|
|
<PopupItem
|
|
key={model.provider}
|
|
defaultModel={defaultModel}
|
|
model={model}
|
|
onSelect={onSelect}
|
|
onHide={onHide}
|
|
/>
|
|
))
|
|
}
|
|
{!filteredModelList.length && !installedModelList.length && (
|
|
<div className="flex flex-col gap-2 rounded-[10px] bg-gradient-to-r from-state-base-hover to-background-gradient-mask-transparent 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-[5px]">
|
|
<span className="i-ri-brain-2-line h-5 w-5 text-text-tertiary" />
|
|
</div>
|
|
<div className="flex flex-col gap-1">
|
|
<p className="text-text-secondary system-sm-medium">
|
|
{t('modelProvider.selector.noProviderConfigured', { ns: 'common' })}
|
|
</p>
|
|
<p className="text-text-tertiary system-xs-regular">
|
|
{t('modelProvider.selector.noProviderConfiguredDesc', { ns: 'common' })}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
variant="primary"
|
|
className="w-[108px]"
|
|
onClick={() => {
|
|
onHide()
|
|
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
|
}}
|
|
>
|
|
{t('modelProvider.selector.configure', { ns: 'common' })}
|
|
<span className="i-ri-arrow-right-line h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{!filteredModelList.length && installedModelList.length > 0 && (
|
|
<div className="break-all px-3 py-1.5 text-center text-xs leading-[18px] text-text-tertiary">
|
|
{`No model found for \u201C${searchText}\u201D`}
|
|
</div>
|
|
)}
|
|
{marketplaceProviders.length > 0 && (
|
|
<>
|
|
<div className="mx-2 my-1 border-t border-divider-subtle" />
|
|
<div className="mb-1">
|
|
<div className="flex h-[22px] items-center px-3">
|
|
<div
|
|
className="flex flex-1 cursor-pointer items-center text-text-primary system-sm-medium"
|
|
onClick={() => setMarketplaceCollapsed(prev => !prev)}
|
|
>
|
|
{t('modelProvider.selector.fromMarketplace', { ns: 'common' })}
|
|
<span className={cn('i-custom-vender-solid-general-arrow-down-round-fill h-4 w-4 text-text-quaternary', marketplaceCollapsed && '-rotate-90')} />
|
|
</div>
|
|
</div>
|
|
{!marketplaceCollapsed && (
|
|
<>
|
|
{marketplaceProviders.map((key) => {
|
|
const Icon = providerIconMap[key]
|
|
const isInstalling = installingProvider === key
|
|
return (
|
|
<div
|
|
key={key}
|
|
className="group flex cursor-pointer items-center gap-1 rounded-lg py-0.5 pl-3 pr-0.5 hover:bg-state-base-hover"
|
|
>
|
|
<div className="flex flex-1 items-center gap-2 py-0.5">
|
|
<Icon className="h-5 w-5 shrink-0 rounded-md" />
|
|
<span className="text-text-secondary system-sm-regular">{modelNameMap[key]}</span>
|
|
</div>
|
|
<Button
|
|
variant="secondary"
|
|
size="small"
|
|
className={cn(
|
|
'shrink-0 backdrop-blur-[5px]',
|
|
!isInstalling && 'hidden group-hover:flex',
|
|
)}
|
|
disabled={isInstalling || isMarketplacePluginsLoading}
|
|
onClick={() => handleInstallPlugin(key)}
|
|
>
|
|
{isInstalling && <span className="i-ri-loader-2-line h-3.5 w-3.5 animate-spin" />}
|
|
{isInstalling
|
|
? t('installModal.installing', { ns: 'plugin' })
|
|
: t('modelProvider.selector.install', { ns: 'common' })}
|
|
</Button>
|
|
</div>
|
|
)
|
|
})}
|
|
<a
|
|
className="flex cursor-pointer items-center gap-0.5 px-3 pt-1.5"
|
|
href={getMarketplaceUrl('', { theme })}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<span className="flex-1 text-text-accent system-xs-regular">
|
|
{t('modelProvider.selector.discoverMoreInMarketplace', { ns: 'common' })}
|
|
</span>
|
|
<span className="i-ri-arrow-right-up-line !h-3 !w-3 text-text-accent" />
|
|
</a>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div
|
|
className="sticky bottom-0 flex cursor-pointer items-center gap-1 rounded-b-lg border-t border-divider-subtle bg-components-panel-bg px-3 py-2 text-text-tertiary hover:text-text-secondary"
|
|
onClick={() => {
|
|
onHide()
|
|
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
|
}}
|
|
>
|
|
<span className="i-ri-equalizer-2-line h-4 w-4 shrink-0" />
|
|
<span className="system-xs-medium">{t('modelProvider.selector.modelProviderSettings', { ns: 'common' })}</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Popup
|