mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 10:56:13 +08:00
280 lines
11 KiB
TypeScript
280 lines
11 KiB
TypeScript
import type {
|
|
ModelProviderPluginSummaryResponse,
|
|
ModelProviderSummaryResponse,
|
|
} from '@dify/contracts/api/console/workspaces/types.gen'
|
|
import type { ReactNode } from 'react'
|
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
|
import { useDebounce } from 'ahooks'
|
|
import { noop } from 'es-toolkit/function'
|
|
import { useMemo } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { SearchInput } from '@/app/components/base/search-input'
|
|
import { usePluginSettingsAccess } from '@/app/components/plugins/plugin-page/use-reference-setting'
|
|
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
|
import { useProviderContext } from '@/context/provider-context'
|
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
|
import { consoleQuery } from '@/service/client'
|
|
import UpdateSettingDialog from '../update-setting-dialog'
|
|
import { ModelTypeEnum } from './declarations'
|
|
import { useDefaultModel } from './hooks'
|
|
import ModelProviderPageBody from './model-provider-page-body'
|
|
import SystemModelSelector from './system-model-selector'
|
|
|
|
type SystemModelConfigStatus =
|
|
| 'no-provider'
|
|
| 'none-configured'
|
|
| 'partially-configured'
|
|
| 'fully-configured'
|
|
|
|
type Props = Readonly<{
|
|
layout?: (parts: { body: ReactNode; toolbar: ReactNode }) => ReactNode
|
|
onOpenMarketplace?: () => void
|
|
onSearchTextChange?: (value: string) => void
|
|
searchText: string
|
|
stickyToolbar?: boolean
|
|
hideSystemModelSelectorProviderSettingsFooter?: boolean
|
|
}>
|
|
|
|
const FixedModelProvider = ['langgenius/openai/openai', 'langgenius/anthropic/anthropic']
|
|
|
|
export type ModelProviderPluginSummary = ModelProviderPluginSummaryResponse & {
|
|
latestVersion?: string
|
|
latestUniqueIdentifier?: string
|
|
}
|
|
|
|
const ModelProviderPage = ({
|
|
layout,
|
|
onOpenMarketplace,
|
|
onSearchTextChange,
|
|
searchText,
|
|
stickyToolbar,
|
|
hideSystemModelSelectorProviderSettingsFooter,
|
|
}: Props) => {
|
|
const debouncedSearchText = useDebounce(searchText, { wait: 500 })
|
|
const { t } = useTranslation()
|
|
const { canSetPluginPreferences } = usePluginSettingsAccess()
|
|
const defaultModelQueryOptions = { enabled: canSetPluginPreferences }
|
|
const { data: textGenerationDefaultModel, isLoading: isTextGenerationDefaultModelLoading } =
|
|
useDefaultModel(ModelTypeEnum.textGeneration, defaultModelQueryOptions)
|
|
const { data: embeddingsDefaultModel, isLoading: isEmbeddingsDefaultModelLoading } =
|
|
useDefaultModel(ModelTypeEnum.textEmbedding, defaultModelQueryOptions)
|
|
const { data: rerankDefaultModel, isLoading: isRerankDefaultModelLoading } = useDefaultModel(
|
|
ModelTypeEnum.rerank,
|
|
defaultModelQueryOptions,
|
|
)
|
|
const { data: speech2textDefaultModel, isLoading: isSpeech2textDefaultModelLoading } =
|
|
useDefaultModel(ModelTypeEnum.speech2text, defaultModelQueryOptions)
|
|
const { data: ttsDefaultModel, isLoading: isTTSDefaultModelLoading } = useDefaultModel(
|
|
ModelTypeEnum.tts,
|
|
defaultModelQueryOptions,
|
|
)
|
|
const {
|
|
modelProviders: providers,
|
|
modelProviderPlugins = {},
|
|
isLoadingModelProviders,
|
|
} = useProviderContext()
|
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
|
|
|
const marketplacePluginIds = useMemo(
|
|
() =>
|
|
Object.values(modelProviderPlugins)
|
|
.filter((plugin) => plugin.source === 'marketplace')
|
|
.map((plugin) => plugin.plugin_id),
|
|
[modelProviderPlugins],
|
|
)
|
|
const { data: latestVersionData } = useQuery(
|
|
consoleQuery.workspaces.current.plugin.list.latestVersions.post.queryOptions({
|
|
input: { body: { plugin_ids: marketplacePluginIds } },
|
|
enabled: !!marketplacePluginIds.length,
|
|
}),
|
|
)
|
|
const pluginSummaryMap = useMemo(() => {
|
|
const map = new Map<string, ModelProviderPluginSummary>()
|
|
for (const plugin of Object.values(modelProviderPlugins)) {
|
|
const latestVersion = latestVersionData?.versions[plugin.plugin_id]
|
|
map.set(plugin.plugin_id, {
|
|
...plugin,
|
|
latestVersion: latestVersion?.version,
|
|
latestUniqueIdentifier: latestVersion?.unique_identifier,
|
|
})
|
|
}
|
|
return map
|
|
}, [latestVersionData, modelProviderPlugins])
|
|
const enableMarketplace = systemFeatures.enable_marketplace
|
|
const isDefaultModelLoading =
|
|
isTextGenerationDefaultModelLoading ||
|
|
isEmbeddingsDefaultModelLoading ||
|
|
isRerankDefaultModelLoading ||
|
|
isSpeech2textDefaultModelLoading ||
|
|
isTTSDefaultModelLoading
|
|
const [configuredProviders, notConfiguredProviders] = useMemo(() => {
|
|
const configuredProviders: ModelProviderSummaryResponse[] = []
|
|
const notConfiguredProviders: ModelProviderSummaryResponse[] = []
|
|
|
|
providers.forEach((provider) => {
|
|
if (provider.is_configured) {
|
|
configuredProviders.push(provider)
|
|
} else {
|
|
notConfiguredProviders.push(provider)
|
|
}
|
|
})
|
|
|
|
configuredProviders.sort((a, b) => {
|
|
if (FixedModelProvider.includes(a.provider) && FixedModelProvider.includes(b.provider))
|
|
return FixedModelProvider.indexOf(a.provider) - FixedModelProvider.indexOf(b.provider) > 0
|
|
? 1
|
|
: -1
|
|
else if (FixedModelProvider.includes(a.provider)) return -1
|
|
else if (FixedModelProvider.includes(b.provider)) return 1
|
|
return 0
|
|
})
|
|
|
|
return [configuredProviders, notConfiguredProviders]
|
|
}, [providers])
|
|
|
|
const systemModelConfigStatus: SystemModelConfigStatus = useMemo(() => {
|
|
const defaultModels = [
|
|
textGenerationDefaultModel,
|
|
embeddingsDefaultModel,
|
|
rerankDefaultModel,
|
|
speech2textDefaultModel,
|
|
ttsDefaultModel,
|
|
]
|
|
const configuredCount = defaultModels.filter(Boolean).length
|
|
if (configuredCount === 0 && configuredProviders.length === 0) return 'no-provider'
|
|
if (configuredCount === 0) return 'none-configured'
|
|
if (configuredCount < defaultModels.length) return 'partially-configured'
|
|
return 'fully-configured'
|
|
}, [
|
|
configuredProviders,
|
|
textGenerationDefaultModel,
|
|
embeddingsDefaultModel,
|
|
rerankDefaultModel,
|
|
speech2textDefaultModel,
|
|
ttsDefaultModel,
|
|
])
|
|
const warningTextKey =
|
|
systemModelConfigStatus === 'no-provider' || systemModelConfigStatus === 'none-configured'
|
|
? 'modelProvider.noneConfigured'
|
|
: null
|
|
const showWarning =
|
|
canSetPluginPreferences &&
|
|
!isLoadingModelProviders &&
|
|
!isDefaultModelLoading &&
|
|
!!warningTextKey
|
|
const systemModelSelector = (className: string) => {
|
|
if (!canSetPluginPreferences) return null
|
|
|
|
return (
|
|
<SystemModelSelector
|
|
className={className}
|
|
notConfigured={showWarning}
|
|
textGenerationDefaultModel={textGenerationDefaultModel}
|
|
embeddingsDefaultModel={embeddingsDefaultModel}
|
|
rerankDefaultModel={rerankDefaultModel}
|
|
speech2textDefaultModel={speech2textDefaultModel}
|
|
ttsDefaultModel={ttsDefaultModel}
|
|
isLoading={isDefaultModelLoading}
|
|
hideProviderSettingsFooter={hideSystemModelSelectorProviderSettingsFooter}
|
|
onOpenMarketplace={onOpenMarketplace}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const [filteredConfiguredProviders, filteredNotConfiguredProviders] = useMemo(() => {
|
|
const filteredConfiguredProviders = configuredProviders.filter(
|
|
(provider) =>
|
|
provider.provider.toLowerCase().includes(debouncedSearchText.toLowerCase()) ||
|
|
Object.values(provider.label).some((text) =>
|
|
text?.toLowerCase().includes(debouncedSearchText.toLowerCase()),
|
|
),
|
|
)
|
|
const filteredNotConfiguredProviders = notConfiguredProviders.filter(
|
|
(provider) =>
|
|
provider.provider.toLowerCase().includes(debouncedSearchText.toLowerCase()) ||
|
|
Object.values(provider.label).some((text) =>
|
|
text?.toLowerCase().includes(debouncedSearchText.toLowerCase()),
|
|
),
|
|
)
|
|
|
|
return [filteredConfiguredProviders, filteredNotConfiguredProviders]
|
|
}, [configuredProviders, debouncedSearchText, notConfiguredProviders])
|
|
const showEmptyProvider = !isLoadingModelProviders && !configuredProviders.length
|
|
const showConfiguredProviders = !isLoadingModelProviders && !!filteredConfiguredProviders?.length
|
|
const showNotConfiguredProviders =
|
|
!isLoadingModelProviders && !!filteredNotConfiguredProviders?.length
|
|
const showMarketplace = !isLoadingModelProviders && enableMarketplace
|
|
const toolbar = (
|
|
<div
|
|
className={
|
|
stickyToolbar
|
|
? layout
|
|
? 'flex w-full items-center justify-between gap-3'
|
|
: 'sticky top-0 z-10 -mx-6 mb-2 flex items-center justify-between gap-3 bg-components-panel-bg px-6 pb-2'
|
|
: 'mb-2 flex items-center justify-between gap-3'
|
|
}
|
|
>
|
|
<SearchInput
|
|
className="w-50 shrink-0"
|
|
placeholder={t(($) => $['modelProvider.searchModels'], { ns: 'common' })}
|
|
value={searchText}
|
|
onValueChange={onSearchTextChange ?? noop}
|
|
/>
|
|
<div className="flex shrink-0 items-center justify-end gap-2">
|
|
{showWarning ? (
|
|
<div className="relative inline-flex shrink-0 items-center gap-2 overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur py-1 pr-1 pl-2.5 shadow-xs backdrop-blur-[5px]">
|
|
<div className="pointer-events-none absolute -inset-px bg-[linear-gradient(119deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0)_100%)] opacity-40" />
|
|
<div className="relative flex shrink-0 items-center gap-1">
|
|
<span
|
|
aria-hidden
|
|
className="i-ri-alert-fill size-4 shrink-0 text-text-warning-secondary"
|
|
/>
|
|
<span
|
|
className="shrink-0 system-sm-medium whitespace-nowrap text-text-primary"
|
|
title={t(($) => $[warningTextKey], { ns: 'common' })}
|
|
>
|
|
{t(($) => $[warningTextKey], { ns: 'common' })}
|
|
</span>
|
|
</div>
|
|
<div className="relative shrink-0">
|
|
{systemModelSelector('h-6 px-1.5 text-xs font-medium')}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
systemModelSelector('h-8 px-3 system-sm-medium')
|
|
)}
|
|
{canSetPluginPreferences && <UpdateSettingDialog category={PluginCategoryEnum.model} />}
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
const body = (
|
|
<ModelProviderPageBody
|
|
providers={providers}
|
|
filteredConfiguredProviders={filteredConfiguredProviders}
|
|
filteredNotConfiguredProviders={filteredNotConfiguredProviders}
|
|
isLoadingModelProviders={isLoadingModelProviders}
|
|
showEmptyProvider={showEmptyProvider}
|
|
showConfiguredProviders={showConfiguredProviders}
|
|
showNotConfiguredProviders={showNotConfiguredProviders}
|
|
showMarketplace={showMarketplace}
|
|
enableMarketplace={enableMarketplace}
|
|
searchText={searchText}
|
|
pluginSummaryMap={pluginSummaryMap}
|
|
onOpenMarketplace={onOpenMarketplace}
|
|
/>
|
|
)
|
|
|
|
if (layout)
|
|
return <div className="relative flex min-h-0 flex-1 flex-col">{layout({ body, toolbar })}</div>
|
|
|
|
return (
|
|
<div className="relative">
|
|
{toolbar}
|
|
{body}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ModelProviderPage
|