Compare commits

...

8 Commits

Author SHA1 Message Date
yyh
1ad9305732 fix(web): avoid quota panel flicker on account-setting tab switch
- remove mount-time workspace invalidate in model provider page

- read quota with useCurrentWorkspace and keep loading only for initial empty fetch

- reuse existing useSystemFeaturesQuery for marketplace and trial models

- update model provider and quota panel tests for new query/loading behavior
2026-03-04 18:43:01 +08:00
yyh
17f38f171d lint 2026-03-04 18:21:59 +08:00
yyh
802088c8eb test(web): fix trivial assertion and add useInvalidateDefaultModel tests
Replace the no-provider test assertion from checking a nonexistent i18n
key to verifying actual warning keys are absent. Add unit tests for
useInvalidateDefaultModel following the useUpdateModelList pattern.
2026-03-04 17:51:20 +08:00
yyh
cad6d94491 refactor(web): replace remixicon imports with Tailwind CSS icons in system-model-selector 2026-03-04 17:45:41 +08:00
yyh
621d0fb2c9 fix 2026-03-04 17:42:34 +08:00
yyh
a92fb3244b fix(web): skip top warning for no-provider state and remove unused i18n key
The empty state card below already prompts users to install a provider,
so the top warning bar is redundant for the no-provider case. Remove
the unused noProviderInstalled i18n key and replace the lookup map with
a ternary to preserve i18n literal types without assertions.
2026-03-04 17:39:49 +08:00
yyh
97508f8d7b fix(web): invalidate default model cache after saving system model settings
After saving system models, only the model list cache was invalidated
but not the default model cache, causing stale config status in the UI.
Add useInvalidateDefaultModel hook and call it for all 5 model types
after a successful save.
2026-03-04 17:26:24 +08:00
yyh
70e677a6ac feat(web): refine system model settings to 4 distinct config states
Replace the single `defaultModelNotConfigured` boolean with a derived
`systemModelConfigStatus` that distinguishes between no-provider,
none-configured, partially-configured, and fully-configured states,
each showing a context-appropriate warning message. Also updates the
button label from "System Model Settings" to "Default Model Settings"
and migrates remixicon imports to Tailwind CSS icon classes.
2026-03-04 16:58:46 +08:00
11 changed files with 214 additions and 104 deletions

View File

@@ -23,6 +23,7 @@ import {
useAnthropicBuyQuota,
useCurrentProviderAndModel,
useDefaultModel,
useInvalidateDefaultModel,
useLanguage,
useMarketplaceAllPlugins,
useModelList,
@@ -864,6 +865,38 @@ describe('hooks', () => {
})
})
describe('useInvalidateDefaultModel', () => {
it('should invalidate default model queries', () => {
const invalidateQueries = vi.fn()
; (useQueryClient as Mock).mockReturnValue({ invalidateQueries })
const { result } = renderHook(() => useInvalidateDefaultModel())
act(() => {
result.current(ModelTypeEnum.textGeneration)
})
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: ['default-model', ModelTypeEnum.textGeneration],
})
})
it('should handle multiple model types', () => {
const invalidateQueries = vi.fn()
; (useQueryClient as Mock).mockReturnValue({ invalidateQueries })
const { result } = renderHook(() => useInvalidateDefaultModel())
act(() => {
result.current(ModelTypeEnum.textGeneration)
result.current(ModelTypeEnum.textEmbedding)
result.current(ModelTypeEnum.rerank)
})
expect(invalidateQueries).toHaveBeenCalledTimes(3)
})
})
describe('useAnthropicBuyQuota', () => {
beforeEach(() => {
Object.defineProperty(window, 'location', {

View File

@@ -222,6 +222,14 @@ export const useUpdateModelList = () => {
return updateModelList
}
export const useInvalidateDefaultModel = () => {
const queryClient = useQueryClient()
return useCallback((type: ModelTypeEnum) => {
queryClient.invalidateQueries({ queryKey: commonQueryKeys.defaultModel(type) })
}, [queryClient])
}
export const useAnthropicBuyQuota = () => {
const [loading, setLoading] = useState(false)

View File

@@ -7,16 +7,7 @@ import {
} from './declarations'
import ModelProviderPage from './index'
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
mutateCurrentWorkspace: vi.fn(),
isValidatingCurrentWorkspace: false,
}),
}))
const mockGlobalState = {
systemFeatures: { enable_marketplace: true },
}
let mockEnableMarketplace = true
const mockQuotaConfig = {
quota_type: CurrentSystemQuotaTypeEnum.free,
@@ -28,7 +19,11 @@ const mockQuotaConfig = {
}
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (s: { systemFeatures: { enable_marketplace: boolean } }) => unknown) => selector(mockGlobalState),
useSystemFeaturesQuery: () => ({
data: {
enable_marketplace: mockEnableMarketplace,
},
}),
}))
const mockProviders = [
@@ -60,13 +55,16 @@ vi.mock('@/context/provider-context', () => ({
}),
}))
const mockDefaultModelState = {
data: null,
isLoading: false,
const mockDefaultModels: Record<string, { data: unknown, isLoading: boolean }> = {
'llm': { data: null, isLoading: false },
'text-embedding': { data: null, isLoading: false },
'rerank': { data: null, isLoading: false },
'speech2text': { data: null, isLoading: false },
'tts': { data: null, isLoading: false },
}
vi.mock('./hooks', () => ({
useDefaultModel: () => mockDefaultModelState,
useDefaultModel: (type: string) => mockDefaultModels[type] ?? { data: null, isLoading: false },
}))
vi.mock('./install-from-marketplace', () => ({
@@ -89,9 +87,10 @@ describe('ModelProviderPage', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.clearAllMocks()
mockGlobalState.systemFeatures.enable_marketplace = true
mockDefaultModelState.data = null
mockDefaultModelState.isLoading = false
mockEnableMarketplace = true
Object.keys(mockDefaultModels).forEach((key) => {
mockDefaultModels[key] = { data: null, isLoading: false }
})
mockProviders.splice(0, mockProviders.length, {
provider: 'openai',
label: { en_US: 'OpenAI' },
@@ -149,13 +148,76 @@ describe('ModelProviderPage', () => {
})
it('should hide marketplace section when marketplace feature is disabled', () => {
mockGlobalState.systemFeatures.enable_marketplace = false
mockEnableMarketplace = false
render(<ModelProviderPage searchText="" />)
expect(screen.queryByTestId('install-from-marketplace')).not.toBeInTheDocument()
})
describe('system model config status', () => {
it('should not show top warning when no configured providers exist (empty state card handles it)', () => {
mockProviders.splice(0, mockProviders.length, {
provider: 'anthropic',
label: { en_US: 'Anthropic' },
custom_configuration: { status: CustomConfigurationStatusEnum.noConfigure },
system_configuration: {
enabled: false,
current_quota_type: CurrentSystemQuotaTypeEnum.free,
quota_configurations: [mockQuotaConfig],
},
})
render(<ModelProviderPage searchText="" />)
expect(screen.queryByText('common.modelProvider.noneConfigured')).not.toBeInTheDocument()
expect(screen.queryByText('common.modelProvider.notConfigured')).not.toBeInTheDocument()
expect(screen.getByText('common.modelProvider.emptyProviderTitle')).toBeInTheDocument()
})
it('should show none-configured warning when providers exist but no default models set', () => {
render(<ModelProviderPage searchText="" />)
expect(screen.getByText('common.modelProvider.noneConfigured')).toBeInTheDocument()
})
it('should show partially-configured warning when some default models are set', () => {
mockDefaultModels.llm = {
data: { model: 'gpt-4', model_type: 'llm', provider: { provider: 'openai', icon_small: { en_US: '' } } },
isLoading: false,
}
render(<ModelProviderPage searchText="" />)
expect(screen.getByText('common.modelProvider.notConfigured')).toBeInTheDocument()
})
it('should not show warning when all default models are configured', () => {
const makeModel = (model: string, type: string) => ({
data: { model, model_type: type, provider: { provider: 'openai', icon_small: { en_US: '' } } },
isLoading: false,
})
mockDefaultModels.llm = makeModel('gpt-4', 'llm')
mockDefaultModels['text-embedding'] = makeModel('text-embedding-3', 'text-embedding')
mockDefaultModels.rerank = makeModel('rerank-v3', 'rerank')
mockDefaultModels.speech2text = makeModel('whisper-1', 'speech2text')
mockDefaultModels.tts = makeModel('tts-1', 'tts')
render(<ModelProviderPage searchText="" />)
expect(screen.queryByText('common.modelProvider.noProviderInstalled')).not.toBeInTheDocument()
expect(screen.queryByText('common.modelProvider.noneConfigured')).not.toBeInTheDocument()
expect(screen.queryByText('common.modelProvider.notConfigured')).not.toBeInTheDocument()
})
it('should not show warning while loading', () => {
Object.keys(mockDefaultModels).forEach((key) => {
mockDefaultModels[key] = { data: null, isLoading: true }
})
render(<ModelProviderPage searchText="" />)
expect(screen.queryByText('common.modelProvider.noProviderInstalled')).not.toBeInTheDocument()
expect(screen.queryByText('common.modelProvider.noneConfigured')).not.toBeInTheDocument()
expect(screen.queryByText('common.modelProvider.notConfigured')).not.toBeInTheDocument()
})
})
it('should prioritize fixed providers in visible order', () => {
mockProviders.splice(0, mockProviders.length, {
provider: 'zeta-provider',

View File

@@ -1,16 +1,11 @@
import type {
ModelProvider,
} from './declarations'
import {
RiAlertFill,
RiBrainLine,
} from '@remixicon/react'
import { useDebounce } from 'ahooks'
import { useEffect, useMemo } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeaturesQuery } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { cn } from '@/utils/classnames'
import {
@@ -25,6 +20,8 @@ import ProviderAddedCard from './provider-added-card'
import QuotaPanel from './provider-added-card/quota-panel'
import SystemModelSelector from './system-model-selector'
type SystemModelConfigStatus = 'no-provider' | 'none-configured' | 'partially-configured' | 'fully-configured'
type Props = {
searchText: string
}
@@ -34,20 +31,19 @@ const FixedModelProvider = ['langgenius/openai/openai', 'langgenius/anthropic/an
const ModelProviderPage = ({ searchText }: Props) => {
const debouncedSearchText = useDebounce(searchText, { wait: 500 })
const { t } = useTranslation()
const { mutateCurrentWorkspace, isValidatingCurrentWorkspace } = useAppContext()
const { data: textGenerationDefaultModel, isLoading: isTextGenerationDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textGeneration)
const { data: embeddingsDefaultModel, isLoading: isEmbeddingsDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textEmbedding)
const { data: rerankDefaultModel, isLoading: isRerankDefaultModelLoading } = useDefaultModel(ModelTypeEnum.rerank)
const { data: speech2textDefaultModel, isLoading: isSpeech2textDefaultModelLoading } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: ttsDefaultModel, isLoading: isTTSDefaultModelLoading } = useDefaultModel(ModelTypeEnum.tts)
const { modelProviders: providers } = useProviderContext()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { data: systemFeatures } = useSystemFeaturesQuery()
const enableMarketplace = systemFeatures?.enable_marketplace ?? false
const isDefaultModelLoading = isTextGenerationDefaultModelLoading
|| isEmbeddingsDefaultModelLoading
|| isRerankDefaultModelLoading
|| isSpeech2textDefaultModelLoading
|| isTTSDefaultModelLoading
const defaultModelNotConfigured = !isDefaultModelLoading && !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
const [configuredProviders, notConfiguredProviders] = useMemo(() => {
const configuredProviders: ModelProvider[] = []
const notConfiguredProviders: ModelProvider[] = []
@@ -79,6 +75,26 @@ const ModelProviderPage = ({ searchText }: Props) => {
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 === 'none-configured'
? 'modelProvider.noneConfigured'
: systemModelConfigStatus === 'partially-configured'
? 'modelProvider.notConfigured'
: null
const showWarning = !isDefaultModelLoading && !!warningTextKey
const [filteredConfiguredProviders, filteredNotConfiguredProviders] = useMemo(() => {
const filteredConfiguredProviders = configuredProviders.filter(
provider => provider.provider.toLowerCase().includes(debouncedSearchText.toLowerCase())
@@ -92,28 +108,24 @@ const ModelProviderPage = ({ searchText }: Props) => {
return [filteredConfiguredProviders, filteredNotConfiguredProviders]
}, [configuredProviders, debouncedSearchText, notConfiguredProviders])
useEffect(() => {
mutateCurrentWorkspace()
}, [mutateCurrentWorkspace])
return (
<div className="relative -mt-2 pt-1">
<div className={cn('mb-2 flex items-center')}>
<div className="system-md-semibold grow text-text-primary">{t('modelProvider.models', { ns: 'common' })}</div>
<div className="grow text-text-primary system-md-semibold">{t('modelProvider.models', { ns: 'common' })}</div>
<div className={cn(
'relative flex shrink-0 items-center justify-end gap-2 rounded-lg border border-transparent p-px',
defaultModelNotConfigured && 'border-components-panel-border bg-components-panel-bg-blur pl-2 shadow-xs',
showWarning && 'border-components-panel-border bg-components-panel-bg-blur pl-2 shadow-xs',
)}
>
{defaultModelNotConfigured && <div className="absolute bottom-0 left-0 right-0 top-0 opacity-40" style={{ background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)' }} />}
{defaultModelNotConfigured && (
<div className="system-xs-medium flex items-center gap-1 text-text-primary">
<RiAlertFill className="h-4 w-4 text-text-warning-secondary" />
<span className="max-w-[460px] truncate" title={t('modelProvider.notConfigured', { ns: 'common' })}>{t('modelProvider.notConfigured', { ns: 'common' })}</span>
{showWarning && <div className="absolute bottom-0 left-0 right-0 top-0 opacity-40" style={{ background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)' }} />}
{showWarning && (
<div className="flex items-center gap-1 text-text-primary system-xs-medium">
<span className="i-ri-alert-fill h-4 w-4 text-text-warning-secondary" />
<span className="max-w-[460px] truncate" title={t(warningTextKey, { ns: 'common' })}>{t(warningTextKey, { ns: 'common' })}</span>
</div>
)}
<SystemModelSelector
notConfigured={defaultModelNotConfigured}
notConfigured={showWarning}
textGenerationDefaultModel={textGenerationDefaultModel}
embeddingsDefaultModel={embeddingsDefaultModel}
rerankDefaultModel={rerankDefaultModel}
@@ -123,14 +135,14 @@ const ModelProviderPage = ({ searchText }: Props) => {
/>
</div>
</div>
{IS_CLOUD_EDITION && <QuotaPanel providers={providers} isLoading={isValidatingCurrentWorkspace} />}
{IS_CLOUD_EDITION && <QuotaPanel providers={providers} />}
{!filteredConfiguredProviders?.length && (
<div className="mb-2 rounded-[10px] bg-workflow-process-bg 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">
<RiBrainLine className="h-5 w-5 text-text-primary" />
<span className="i-ri-brain-line h-5 w-5 text-text-primary" />
</div>
<div className="system-sm-medium mt-2 text-text-secondary">{t('modelProvider.emptyProviderTitle', { ns: 'common' })}</div>
<div className="system-xs-regular mt-1 text-text-tertiary">{t('modelProvider.emptyProviderTip', { ns: 'common' })}</div>
<div className="mt-2 text-text-secondary system-sm-medium">{t('modelProvider.emptyProviderTitle', { ns: 'common' })}</div>
<div className="mt-1 text-text-tertiary system-xs-regular">{t('modelProvider.emptyProviderTip', { ns: 'common' })}</div>
</div>
)}
{!!filteredConfiguredProviders?.length && (
@@ -145,7 +157,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
)}
{!!filteredNotConfiguredProviders?.length && (
<>
<div className="system-md-semibold mb-2 flex items-center pt-2 text-text-primary">{t('modelProvider.toBeConfigured', { ns: 'common' })}</div>
<div className="mb-2 flex items-center pt-2 text-text-primary system-md-semibold">{t('modelProvider.toBeConfigured', { ns: 'common' })}</div>
<div className="relative">
{filteredNotConfiguredProviders?.map(provider => (
<ProviderAddedCard
@@ -158,7 +170,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
</>
)}
{
enable_marketplace && (
enableMarketplace && (
<InstallFromMarketplace
providers={providers}
searchText={searchText}

View File

@@ -2,11 +2,16 @@ import type { ModelProvider } from '../declarations'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import QuotaPanel from './quota-panel'
let mockWorkspace = {
let mockWorkspaceData: {
trial_credits: number
trial_credits_used: number
next_credit_reset_date: string
} | undefined = {
trial_credits: 100,
trial_credits_used: 30,
next_credit_reset_date: '2024-12-31',
}
let mockWorkspaceIsPending = false
let mockTrialModels: string[] = ['langgenius/openai/openai']
let mockPlugins = [{
plugin_id: 'langgenius/openai',
@@ -25,15 +30,16 @@ vi.mock('@/app/components/base/icons/src/public/llm', () => {
}
})
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
currentWorkspace: mockWorkspace,
vi.mock('@/service/use-common', () => ({
useCurrentWorkspace: () => ({
data: mockWorkspaceData,
isPending: mockWorkspaceIsPending,
}),
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (state: { systemFeatures: { trial_models: string[] } }) => unknown) => selector({
systemFeatures: {
useSystemFeaturesQuery: () => ({
data: {
trial_models: mockTrialModels,
},
}),
@@ -71,22 +77,21 @@ describe('QuotaPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockWorkspace = {
mockWorkspaceData = {
trial_credits: 100,
trial_credits_used: 30,
next_credit_reset_date: '2024-12-31',
}
mockWorkspaceIsPending = false
mockTrialModels = ['langgenius/openai/openai']
mockPlugins = [{ plugin_id: 'langgenius/openai', latest_package_identifier: 'openai@1.0.0' }]
})
it('should render loading state', () => {
render(
<QuotaPanel
providers={mockProviders}
isLoading
/>,
)
mockWorkspaceData = undefined
mockWorkspaceIsPending = true
render(<QuotaPanel providers={mockProviders} />)
expect(screen.getByRole('status')).toBeInTheDocument()
})
@@ -102,8 +107,17 @@ describe('QuotaPanel', () => {
expect(screen.getByText(/modelProvider\.resetDate/)).toBeInTheDocument()
})
it('should keep quota content during background refetch when cached workspace exists', () => {
mockWorkspaceIsPending = true
render(<QuotaPanel providers={mockProviders} />)
expect(screen.queryByRole('status')).not.toBeInTheDocument()
expect(screen.getByText('70')).toBeInTheDocument()
})
it('should floor credits at zero when usage is higher than quota', () => {
mockWorkspace = {
mockWorkspaceData = {
trial_credits: 10,
trial_credits_used: 999,
next_credit_reset_date: '',

View File

@@ -9,9 +9,9 @@ import { AnthropicShortLight, Deepseek, Gemini, Grok, OpenaiSmall, Tongyi } from
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 { useSystemFeaturesQuery } from '@/context/global-public-context'
import useTimestamp from '@/hooks/use-timestamp'
import { useCurrentWorkspace } from '@/service/use-common'
import { ModelProviderQuotaGetPaid } from '@/types/model-provider'
import { cn } from '@/utils/classnames'
import { formatNumber } from '@/utils/format'
@@ -48,16 +48,16 @@ const providerKeyToPluginId: Record<ModelProviderQuotaGetPaid, string> = {
type QuotaPanelProps = {
providers: ModelProvider[]
isLoading?: boolean
}
const QuotaPanel: FC<QuotaPanelProps> = ({
providers,
isLoading = false,
}) => {
const { t } = useTranslation()
const { currentWorkspace } = useAppContext()
const { trial_models } = useGlobalPublicStore(s => s.systemFeatures)
const credits = Math.max((currentWorkspace.trial_credits - currentWorkspace.trial_credits_used) || 0, 0)
const { data: currentWorkspace, isPending: isPendingWorkspace } = useCurrentWorkspace()
const { data: systemFeatures } = useSystemFeaturesQuery()
const trialModels = systemFeatures?.trial_models ?? []
const credits = Math.max(((currentWorkspace?.trial_credits ?? 0) - (currentWorkspace?.trial_credits_used ?? 0)) || 0, 0)
const isLoading = isPendingWorkspace && !currentWorkspace
const providerMap = useMemo(() => new Map(
providers.map(p => [p.provider, p.preferred_provider_type]),
), [providers])
@@ -110,13 +110,13 @@ const QuotaPanel: FC<QuotaPanelProps> = ({
<div className={cn('my-2 min-w-[72px] shrink-0 rounded-xl border-[0.5px] pb-2.5 pl-4 pr-2.5 pt-3 shadow-xs', credits <= 0 ? 'border-state-destructive-border hover:bg-state-destructive-hover' : 'border-components-panel-border bg-third-party-model-bg-default')}>
<div className="system-xs-medium-uppercase mb-2 flex h-4 items-center text-text-tertiary">
{t('modelProvider.quota', { ns: 'common' })}
<Tooltip popupContent={t('modelProvider.card.tip', { ns: 'common', modelNames: trial_models.map(key => modelNameMap[key as keyof typeof modelNameMap]).filter(Boolean).join(', ') })} />
<Tooltip popupContent={t('modelProvider.card.tip', { ns: 'common', modelNames: trialModels.map(key => modelNameMap[key as keyof typeof modelNameMap]).filter(Boolean).join(', ') })} />
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-xs text-text-tertiary">
<span className="system-md-semibold-uppercase mr-0.5 text-text-secondary">{formatNumber(credits)}</span>
<span>{t('modelProvider.credits', { ns: 'common' })}</span>
{currentWorkspace.next_credit_reset_date
{currentWorkspace?.next_credit_reset_date
? (
<>
<span>·</span>
@@ -132,7 +132,7 @@ const QuotaPanel: FC<QuotaPanelProps> = ({
: null}
</div>
<div className="flex items-center gap-1">
{allProviders.filter(({ key }) => trial_models.includes(key)).map(({ key, Icon }) => {
{allProviders.filter(({ key }) => trialModels.includes(key)).map(({ key, Icon }) => {
const providerType = providerMap.get(key)
const isConfigured = (installedProvidersMap.get(key)?.length ?? 0) > 0 // means the provider is configured API key
const getTooltipKey = () => {

View File

@@ -26,6 +26,7 @@ vi.mock('react-i18next', async () => {
const mockNotify = vi.hoisted(() => vi.fn())
const mockUpdateModelList = vi.hoisted(() => vi.fn())
const mockInvalidateDefaultModel = vi.hoisted(() => vi.fn())
const mockUpdateDefaultModel = vi.hoisted(() => vi.fn(() => Promise.resolve({ result: 'success' })))
let mockIsCurrentWorkspaceManager = true
@@ -57,6 +58,7 @@ vi.mock('../hooks', () => ({
vi.fn(),
],
useUpdateModelList: () => mockUpdateModelList,
useInvalidateDefaultModel: () => mockInvalidateDefaultModel,
}))
vi.mock('@/service/common', () => ({
@@ -144,6 +146,7 @@ describe('SystemModel', () => {
type: 'success',
message: 'Modified successfully',
})
expect(mockInvalidateDefaultModel).toHaveBeenCalledTimes(5)
expect(mockUpdateModelList).toHaveBeenCalledTimes(5)
})
})

View File

@@ -3,7 +3,6 @@ import type {
DefaultModel,
DefaultModelResponse,
} from '../declarations'
import { RiEqualizer2Line, RiLoader2Line } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
@@ -19,6 +18,7 @@ import { useProviderContext } from '@/context/provider-context'
import { updateDefaultModel } from '@/service/common'
import { ModelTypeEnum } from '../declarations'
import {
useInvalidateDefaultModel,
useModelList,
useSystemDefaultModelAndModelList,
useUpdateModelList,
@@ -48,6 +48,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
const { isCurrentWorkspaceManager } = useAppContext()
const { textGenerationModelList } = useProviderContext()
const updateModelList = useUpdateModelList()
const invalidateDefaultModel = useInvalidateDefaultModel()
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
const { data: speech2textModelList } = useModelList(ModelTypeEnum.speech2text)
@@ -106,18 +107,9 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
setOpen(false)
changedModelTypes.forEach((modelType) => {
if (modelType === ModelTypeEnum.textGeneration)
updateModelList(modelType)
else if (modelType === ModelTypeEnum.textEmbedding)
updateModelList(modelType)
else if (modelType === ModelTypeEnum.rerank)
updateModelList(modelType)
else if (modelType === ModelTypeEnum.speech2text)
updateModelList(modelType)
else if (modelType === ModelTypeEnum.tts)
updateModelList(modelType)
})
const allModelTypes = [ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank, ModelTypeEnum.speech2text, ModelTypeEnum.tts]
allModelTypes.forEach(type => invalidateDefaultModel(type))
changedModelTypes.forEach(type => updateModelList(type))
}
}
@@ -139,8 +131,8 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
disabled={isLoading}
>
{isLoading
? <RiLoader2Line className="mr-1 h-3.5 w-3.5 animate-spin" />
: <RiEqualizer2Line className="mr-1 h-3.5 w-3.5" />}
? <span className="i-ri-loader-2-line mr-1 h-3.5 w-3.5 animate-spin" />
: <span className="i-ri-equalizer-2-line mr-1 h-3.5 w-3.5" />}
{t('modelProvider.systemModelSettings', { ns: 'common' })}
</Button>
</PortalToFollowElemTrigger>

View File

@@ -4652,11 +4652,6 @@
"count": 3
}
},
"app/components/header/account-setting/model-provider-page/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 5
}
},
"app/components/header/account-setting/model-provider-page/install-from-marketplace.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 3
@@ -9714,17 +9709,6 @@
"count": 1
}
},
"lib/utils.ts": {
"import/consistent-type-specifier-style": {
"count": 1
},
"perfectionist/sort-named-imports": {
"count": 1
},
"style/quotes": {
"count": 2
}
},
"models/common.ts": {
"ts/no-explicit-any": {
"count": 3

View File

@@ -390,6 +390,7 @@
"modelProvider.models": "Models",
"modelProvider.modelsNum": "{{num}} Models",
"modelProvider.noModelFound": "No model found for {{model}}",
"modelProvider.noneConfigured": "Configure a default system model to run applications",
"modelProvider.notConfigured": "The system model has not yet been fully configured",
"modelProvider.parameters": "PARAMETERS",
"modelProvider.parametersInvalidRemoved": "Some parameters are invalid and have been removed",
@@ -413,7 +414,7 @@
"modelProvider.showMoreModelProvider": "Show more model provider",
"modelProvider.speechToTextModel.key": "Speech-to-Text Model",
"modelProvider.speechToTextModel.tip": "Set the default model for speech-to-text input in conversation.",
"modelProvider.systemModelSettings": "System Model Settings",
"modelProvider.systemModelSettings": "Default Model Settings",
"modelProvider.systemModelSettingsLink": "Why is it necessary to set up a system model?",
"modelProvider.systemReasoningModel.key": "System Reasoning Model",
"modelProvider.systemReasoningModel.tip": "Set the default inference model to be used for creating applications, as well as features such as dialogue name generation and next question suggestion will also use the default inference model.",

View File

@@ -390,6 +390,7 @@
"modelProvider.models": "模型列表",
"modelProvider.modelsNum": "{{num}} 个模型",
"modelProvider.noModelFound": "找不到模型 {{model}}",
"modelProvider.noneConfigured": "配置默认系统模型以运行应用",
"modelProvider.notConfigured": "系统模型尚未完全配置",
"modelProvider.parameters": "参数",
"modelProvider.parametersInvalidRemoved": "部分参数无效,已移除",
@@ -413,7 +414,7 @@
"modelProvider.showMoreModelProvider": "显示更多模型提供商",
"modelProvider.speechToTextModel.key": "语音转文本模型",
"modelProvider.speechToTextModel.tip": "设置对话中语音转文字输入的默认使用模型。",
"modelProvider.systemModelSettings": "系统模型设置",
"modelProvider.systemModelSettings": "默认模型设置",
"modelProvider.systemModelSettingsLink": "为什么需要设置系统模型?",
"modelProvider.systemReasoningModel.key": "系统推理模型",
"modelProvider.systemReasoningModel.tip": "设置创建应用使用的默认推理模型,以及对话名称生成、下一步问题建议等功能也会使用该默认推理模型。",