refactor(trigger): derive multi-state status from credentials instead of collapsed disabled boolean

Replace the single `disabled` prop with a pure `deriveTriggerStatus` function
that maps to distinct states (empty, active, credits-exhausted, api-key-unavailable,
incompatible), each with its own badge text and tooltip. Unify non-workflow and
workflow modes into a single split layout, migrate icons to CSS icons, and add
per-status i18n tooltip keys.
This commit is contained in:
yyh
2026-03-11 16:37:12 +08:00
parent 3f27c8a9d2
commit ad4cb51983
10 changed files with 306 additions and 279 deletions

View File

@ -0,0 +1,101 @@
import type { ModelItem, ModelProvider } from '../declarations'
import type { CredentialPanelState } from '../provider-added-card/use-credential-panel-state'
import { ModelStatusEnum } from '../declarations'
import { deriveTriggerStatus } from './derive-trigger-status'
const baseCredentialState: CredentialPanelState = {
variant: 'api-active',
priority: 'apiKey',
supportsCredits: true,
showPrioritySwitcher: true,
hasCredentials: true,
isCreditsExhausted: false,
credentialName: 'Primary Key',
credits: 10,
}
const mockProvider = { provider: 'openai' } as ModelProvider
const mockModel = { model: 'gpt-4', status: ModelStatusEnum.active } as ModelItem
describe('deriveTriggerStatus', () => {
it('returns empty when modelId is missing', () => {
expect(deriveTriggerStatus(undefined, 'openai', mockProvider, mockModel, baseCredentialState)).toBe('empty')
})
it('returns empty when providerName is missing', () => {
expect(deriveTriggerStatus('gpt-4', undefined, mockProvider, mockModel, baseCredentialState)).toBe('empty')
})
it('returns incompatible when provider plugin is not installed', () => {
expect(deriveTriggerStatus('gpt-4', 'openai', undefined, mockModel, baseCredentialState)).toBe('incompatible')
})
it('returns credits-exhausted when credits priority and exhausted', () => {
const state: CredentialPanelState = {
...baseCredentialState,
priority: 'credits',
isCreditsExhausted: true,
}
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, mockModel, state)).toBe('credits-exhausted')
})
it('returns active when credits priority but not exhausted', () => {
const state: CredentialPanelState = {
...baseCredentialState,
priority: 'credits',
isCreditsExhausted: false,
}
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, mockModel, state)).toBe('active')
})
it('returns api-key-unavailable when variant is api-unavailable', () => {
const state: CredentialPanelState = {
...baseCredentialState,
variant: 'api-unavailable',
}
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, mockModel, state)).toBe('api-key-unavailable')
})
it('returns incompatible when currentModel is missing (deprecated)', () => {
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, undefined, baseCredentialState)).toBe('incompatible')
})
it('returns incompatible when model status is not active', () => {
const model = { ...mockModel, status: ModelStatusEnum.noConfigure } as ModelItem
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, model, baseCredentialState)).toBe('incompatible')
})
it('returns incompatible when model status is noPermission', () => {
const model = { ...mockModel, status: ModelStatusEnum.noPermission } as ModelItem
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, model, baseCredentialState)).toBe('incompatible')
})
it('returns incompatible when model status is disabled', () => {
const model = { ...mockModel, status: ModelStatusEnum.disabled } as ModelItem
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, model, baseCredentialState)).toBe('incompatible')
})
it('returns active when all conditions are satisfied', () => {
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, mockModel, baseCredentialState)).toBe('active')
})
it('prioritises credits-exhausted over api-unavailable', () => {
const state: CredentialPanelState = {
...baseCredentialState,
priority: 'credits',
isCreditsExhausted: true,
variant: 'api-unavailable',
}
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, mockModel, state)).toBe('credits-exhausted')
})
it('does not return credits-exhausted when supportsCredits is false', () => {
const state: CredentialPanelState = {
...baseCredentialState,
priority: 'credits',
isCreditsExhausted: true,
supportsCredits: false,
}
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, mockModel, state)).toBe('active')
})
})

View File

@ -0,0 +1,53 @@
import type { ModelItem, ModelProvider } from '../declarations'
import type { CredentialPanelState } from '../provider-added-card/use-credential-panel-state'
import { ModelStatusEnum } from '../declarations'
export type TriggerStatus
= | 'empty'
| 'active'
| 'credits-exhausted'
| 'api-key-unavailable'
| 'incompatible'
export function deriveTriggerStatus(
modelId: string | undefined,
providerName: string | undefined,
currentModelProvider: ModelProvider | undefined,
currentModel: ModelItem | undefined,
credentialState: CredentialPanelState,
): TriggerStatus {
if (!modelId || !providerName)
return 'empty'
if (!currentModelProvider)
return 'incompatible'
if (credentialState.priority === 'credits'
&& credentialState.supportsCredits
&& credentialState.isCreditsExhausted) {
return 'credits-exhausted'
}
if (credentialState.variant === 'api-unavailable')
return 'api-key-unavailable'
if (!currentModel)
return 'incompatible'
if (currentModel.status !== ModelStatusEnum.active)
return 'incompatible'
return 'active'
}
export const TRIGGER_STATUS_BADGE_I18N: Partial<Record<TriggerStatus, string>> = {
'credits-exhausted': 'modelProvider.selector.creditsExhausted',
'api-key-unavailable': 'modelProvider.selector.apiKeyUnavailable',
'incompatible': 'modelProvider.selector.incompatible',
}
export const TRIGGER_STATUS_TOOLTIP_I18N: Partial<Record<TriggerStatus, string>> = {
'credits-exhausted': 'modelProvider.selector.creditsExhaustedTip',
'api-key-unavailable': 'modelProvider.selector.apiKeyUnavailableTip',
'incompatible': 'modelProvider.selector.incompatibleTip',
}

View File

@ -49,7 +49,7 @@ vi.mock('@/service/use-common', () => ({
data: {
data: parameterRules,
},
isPending: isRulesLoading,
isLoading: isRulesLoading,
}),
}))

View File

@ -19,10 +19,8 @@ import {
PopoverTrigger,
} from '@/app/components/base/ui/popover'
import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE, TONE_LIST } from '@/config'
import { useProviderContext } from '@/context/provider-context'
import { useModelParameterRules } from '@/service/use-common'
import { cn } from '@/utils/classnames'
import { ModelStatusEnum } from '../declarations'
import {
useTextGenerationCurrentProviderAndModelAndModelList,
} from '../hooks'
@ -66,7 +64,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
isInWorkflow,
}) => {
const { t } = useTranslation()
const { isAPIKeySet } = useProviderContext()
const [open, setOpen] = useState(false)
const { data: parameterRulesData, isLoading } = useModelParameterRules(provider, modelId)
const {
@ -76,9 +73,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
} = useTextGenerationCurrentProviderAndModelAndModelList(
{ provider, model: modelId },
)
const hasDeprecated = !currentProvider || !currentModel
const modelDisabled = currentModel?.status !== ModelStatusEnum.active
const disabled = !isAPIKeySet || hasDeprecated || modelDisabled
const parameterRules: ModelParameterRule[] = useMemo(() => {
return parameterRulesData?.data || []
@ -143,9 +137,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
renderTrigger
? renderTrigger({
open,
disabled,
modelDisabled,
hasDeprecated,
currentProvider,
currentModel,
providerName: provider,
@ -153,10 +144,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
})
: (
<Trigger
disabled={disabled}
isInWorkflow={isInWorkflow}
modelDisabled={modelDisabled}
hasDeprecated={hasDeprecated}
currentProvider={currentProvider}
currentModel={currentModel}
providerName={provider}

View File

@ -43,6 +43,17 @@ vi.mock('../model-name', () => ({
),
}))
const activeCredentialState = {
variant: 'api-active' as const,
supportsCredits: true,
isCreditsExhausted: false,
priority: 'apiKey' as const,
showPrioritySwitcher: true,
hasCredentials: true,
credentialName: 'Primary Key',
credits: 10,
}
describe('Trigger', () => {
const currentProvider = {
provider: 'openai',
@ -56,20 +67,11 @@ describe('Trigger', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseCredentialPanelState.mockReturnValue({
variant: 'api-active',
supportsCredits: true,
isCreditsExhausted: false,
priority: 'apiKey',
showPrioritySwitcher: true,
hasCredentials: true,
credentialName: 'Primary Key',
credits: 10,
})
mockUseCredentialPanelState.mockReturnValue(activeCredentialState)
})
describe('Rendering', () => {
it('should render initialized state when provider and model are available', () => {
it('should render active state with model features in non-workflow mode', () => {
render(
<Trigger
currentProvider={currentProvider}
@ -96,7 +98,7 @@ describe('Trigger', () => {
expect(screen.getByText('gpt-4')).toBeInTheDocument()
})
it('should render workflow styles when workflow mode is enabled', () => {
it('should render split layout with workflow styles when workflow mode is enabled', () => {
const { container } = render(
<Trigger
currentProvider={currentProvider}
@ -107,30 +109,35 @@ describe('Trigger', () => {
/>,
)
expect(container.firstChild).toHaveClass('border-workflow-block-parma-bg')
expect(container.firstChild).toHaveClass('bg-workflow-block-parma-bg')
const leftPanel = container.querySelector('.rounded-l-lg')
expect(leftPanel).toBeInTheDocument()
expect(leftPanel).toHaveClass('border-workflow-block-parma-bg')
const rightPanel = container.querySelector('.rounded-r-lg')
expect(rightPanel).toBeInTheDocument()
expect(rightPanel).toHaveClass('border-workflow-block-parma-bg')
})
it('should render workflow empty state when no provider or model is selected', () => {
const { container } = render(<Trigger isInWorkflow />)
it('should render empty state when no provider or model is selected', () => {
render(<Trigger isInWorkflow />)
expect(screen.getByText('workflow:errorMsg.configureModel')).toBeInTheDocument()
})
it('should render non-workflow empty state with warning border', () => {
const { container } = render(<Trigger />)
expect(screen.getByText('workflow:errorMsg.configureModel')).toBeInTheDocument()
expect(container.firstChild).toHaveClass('border-text-warning')
expect(container.firstChild).toHaveClass('bg-state-warning-hover')
})
})
describe('Status badges', () => {
it('should render credits exhausted split layout in non-workflow mode', () => {
it('should render credits exhausted badge in non-workflow mode', () => {
mockUseCredentialPanelState.mockReturnValue({
...activeCredentialState,
variant: 'credits-exhausted',
supportsCredits: true,
isCreditsExhausted: true,
priority: 'credits',
showPrioritySwitcher: true,
hasCredentials: false,
credentialName: undefined,
credits: 0,
})
render(
@ -143,45 +150,14 @@ describe('Trigger', () => {
)
expect(screen.getByText('common.modelProvider.selector.creditsExhausted')).toBeInTheDocument()
expect(screen.getByTestId('model-icon')).toBeInTheDocument()
expect(screen.queryByTestId('model-name-mode')).not.toBeInTheDocument()
expect(screen.queryByTestId('model-name-features')).not.toBeInTheDocument()
})
it('should resolve provider from context when currentProvider is missing in split layout', () => {
mockUseCredentialPanelState.mockReturnValue({
variant: 'credits-exhausted',
supportsCredits: true,
isCreditsExhausted: true,
priority: 'credits',
showPrioritySwitcher: true,
hasCredentials: false,
credentialName: undefined,
credits: 0,
})
render(
<Trigger
currentModel={currentModel}
providerName="openai"
modelId="gpt-4"
/>,
)
expect(screen.getByText('common.modelProvider.selector.creditsExhausted')).toBeInTheDocument()
expect(screen.getByTestId('model-icon')).toBeInTheDocument()
})
it('should render api unavailable split layout in non-workflow mode', () => {
it('should render api unavailable badge in non-workflow mode', () => {
mockUseCredentialPanelState.mockReturnValue({
...activeCredentialState,
variant: 'api-unavailable',
supportsCredits: true,
isCreditsExhausted: false,
priority: 'apiKey',
showPrioritySwitcher: true,
hasCredentials: true,
credentialName: 'Primary Key',
credits: 0,
})
render(
@ -198,14 +174,10 @@ describe('Trigger', () => {
it('should render credits exhausted badge in workflow mode', () => {
mockUseCredentialPanelState.mockReturnValue({
...activeCredentialState,
variant: 'credits-exhausted',
supportsCredits: true,
isCreditsExhausted: true,
priority: 'credits',
showPrioritySwitcher: true,
hasCredentials: false,
credentialName: undefined,
credits: 0,
})
render(
@ -223,14 +195,8 @@ describe('Trigger', () => {
it('should render api unavailable badge in workflow mode', () => {
mockUseCredentialPanelState.mockReturnValue({
...activeCredentialState,
variant: 'api-unavailable',
supportsCredits: true,
isCreditsExhausted: false,
priority: 'apiKey',
showPrioritySwitcher: true,
hasCredentials: true,
credentialName: 'Primary Key',
credits: 0,
})
render(
@ -246,30 +212,23 @@ describe('Trigger', () => {
expect(screen.getByText('common.modelProvider.selector.apiKeyUnavailable')).toBeInTheDocument()
})
it('should render incompatible badge when deprecated model is disabled', () => {
it('should render incompatible badge when model is deprecated (currentModel missing)', () => {
render(
<Trigger
currentProvider={currentProvider}
currentModel={currentModel}
disabled
hasDeprecated
providerName="openai"
modelId="gpt-4"
/>,
)
expect(screen.getByText('common.modelProvider.selector.incompatible')).toBeInTheDocument()
expect(screen.queryByTestId('model-name-mode')).not.toBeInTheDocument()
expect(screen.queryByTestId('model-name-features')).not.toBeInTheDocument()
})
it('should render incompatible badge when model status is disabled but not deprecated', () => {
it('should render incompatible badge when model status is non-active', () => {
render(
<Trigger
currentProvider={currentProvider}
currentModel={{ ...currentModel, status: 'no-configure' } as typeof currentModel}
disabled
modelDisabled
providerName="openai"
modelId="gpt-4"
/>,
@ -277,10 +236,8 @@ describe('Trigger', () => {
expect(screen.getByText('common.modelProvider.selector.incompatible')).toBeInTheDocument()
})
})
describe('Edge cases', () => {
it('should render without crashing when providerName does not match any provider', () => {
it('should render incompatible badge when provider plugin is not installed', () => {
render(
<Trigger
modelId="gpt-4"
@ -288,7 +245,44 @@ describe('Trigger', () => {
/>,
)
expect(screen.getByText('gpt-4')).toBeInTheDocument()
expect(screen.getByText('common.modelProvider.selector.incompatible')).toBeInTheDocument()
})
})
describe('Split layout', () => {
it('should use split layout with settings button in non-workflow mode', () => {
const { container } = render(
<Trigger
currentProvider={currentProvider}
currentModel={currentModel}
providerName="openai"
modelId="gpt-4"
/>,
)
const splitContainer = container.querySelector('.rounded-l-lg')
expect(splitContainer).toBeInTheDocument()
const settingsButton = container.querySelector('.rounded-r-lg')
expect(settingsButton).toBeInTheDocument()
})
it('should use split layout for error states in non-workflow mode', () => {
mockUseCredentialPanelState.mockReturnValue({
...activeCredentialState,
variant: 'api-unavailable',
})
const { container } = render(
<Trigger
currentProvider={currentProvider}
currentModel={currentModel}
providerName="openai"
modelId="gpt-4"
/>,
)
const splitContainer = container.querySelector('.rounded-l-lg')
expect(splitContainer).toBeInTheDocument()
})
})
})

View File

@ -4,209 +4,114 @@ import type {
ModelItem,
ModelProvider,
} from '../declarations'
import { RiArrowDownSLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
import { useProviderContext } from '@/context/provider-context'
import { cn } from '@/utils/classnames'
import { ModelStatusEnum } from '../declarations'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import { useCredentialPanelState } from '../provider-added-card/use-credential-panel-state'
import { MODEL_STATUS_I18N_KEY } from '../status-mapping'
import {
deriveTriggerStatus,
TRIGGER_STATUS_BADGE_I18N,
TRIGGER_STATUS_TOOLTIP_I18N,
} from './derive-trigger-status'
export type TriggerProps = {
open?: boolean
disabled?: boolean
currentProvider?: ModelProvider | Model
currentModel?: ModelItem
providerName?: string
modelId?: string
hasDeprecated?: boolean
modelDisabled?: boolean
isInWorkflow?: boolean
}
const Trigger: FC<TriggerProps> = ({
disabled,
currentProvider,
currentModel,
providerName,
modelId,
hasDeprecated,
modelDisabled: _modelDisabled,
isInWorkflow,
}) => {
const { t } = useTranslation()
const { modelProviders } = useProviderContext()
const isEmpty = !modelId || !providerName
const currentModelProvider = modelProviders.find(p => p.provider === providerName)
const state = useCredentialPanelState(currentModelProvider)
const showCreditsExhausted = !isEmpty && state.priority === 'credits' && state.supportsCredits && state.isCreditsExhausted
const showApiKeyUnavailable = !isEmpty && state.variant === 'api-unavailable'
const effectiveStatus = showCreditsExhausted
? ModelStatusEnum.quotaExceeded
: showApiKeyUnavailable
? ModelStatusEnum.credentialRemoved
: currentModel?.status
const statusI18nKey = effectiveStatus ? MODEL_STATUS_I18N_KEY[effectiveStatus] : undefined
const isCreditsExhausted = effectiveStatus === ModelStatusEnum.quotaExceeded
const shouldShowModelMeta = effectiveStatus === ModelStatusEnum.active && !(disabled && hasDeprecated)
const credentialState = useCredentialPanelState(currentModelProvider)
const status = deriveTriggerStatus(modelId, providerName, currentModelProvider, currentModel, credentialState)
const badgeKey = TRIGGER_STATUS_BADGE_I18N[status]
const tooltipKey = TRIGGER_STATUS_TOOLTIP_I18N[status]
const isActive = status === 'active'
const iconProvider = currentProvider || modelProviders.find(item => item.provider === providerName)
// Non-workflow status error: split layout with badge + settings button
if ((showCreditsExhausted || showApiKeyUnavailable) && !isInWorkflow) {
if (status === 'empty') {
return (
<div className="flex h-8 min-w-[296px] cursor-pointer items-center gap-px overflow-hidden rounded-lg">
<div className="flex flex-1 items-center gap-0.5 rounded-l-lg bg-components-input-bg-normal p-1">
<ModelIcon
className="p-0.5"
provider={currentProvider || modelProviders.find(item => item.provider === providerName)}
modelName={currentModel?.model}
/>
<div className="flex flex-1 items-center truncate px-1 py-[3px]">
{currentModel
? (
<ModelName
className="grow"
modelItem={currentModel}
showMode={shouldShowModelMeta}
showFeatures={shouldShowModelMeta}
/>
)
: <div className="truncate text-[13px] font-normal text-components-input-text-filled">{modelId}</div>}
</div>
<div className="flex shrink-0 items-center pr-0.5">
<div className="flex min-w-[20px] shrink-0 items-center justify-center gap-[3px] rounded-md border border-text-warning bg-components-badge-bg-dimm px-[5px] py-0.5">
<span className="i-ri-alert-fill h-3 w-3 text-text-warning" />
<span className="whitespace-nowrap text-text-warning system-xs-medium">
{t(showCreditsExhausted ? 'modelProvider.selector.creditsExhausted' : 'modelProvider.selector.apiKeyUnavailable', { ns: 'common' })}
</span>
</div>
<div
className={cn(
'relative flex h-8 min-w-[296px] cursor-pointer items-center rounded-lg px-2',
isInWorkflow
? 'border border-text-warning bg-state-warning-hover pr-[30px]'
: 'border border-text-warning bg-state-warning-hover ring-inset ring-text-warning hover:ring-[0.5px]',
)}
>
<div className="mr-2 flex h-6 w-6 shrink-0 items-center justify-center">
<div className="flex h-5 w-5 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle">
<span className="i-ri-brain-2-line h-3.5 w-3.5 text-text-quaternary" />
</div>
</div>
<div className="flex shrink-0 items-center justify-center rounded-r-lg bg-components-button-tertiary-bg p-2">
<SlidersH className="h-4 w-4 text-text-tertiary" />
<div className="mr-1 flex-1 truncate text-[13px] font-normal text-text-secondary">
{t('workflow:errorMsg.configureModel')}
</div>
<span className={cn('i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-tertiary', isInWorkflow && 'absolute right-2 top-[9px] h-3.5 w-3.5')} />
</div>
)
}
return (
<div
className={cn(
'relative flex h-8 min-w-[296px] cursor-pointer items-center rounded-lg px-2',
!isInWorkflow && 'border ring-inset hover:ring-[0.5px]',
!isInWorkflow && (disabled ? 'border-text-warning bg-state-warning-hover ring-text-warning' : 'border-util-colors-indigo-indigo-600 bg-state-accent-hover ring-util-colors-indigo-indigo-600'),
isInWorkflow && !isEmpty && 'border border-workflow-block-parma-bg bg-workflow-block-parma-bg pr-[30px] hover:border-components-input-border-active',
isInWorkflow && isEmpty && 'border border-text-warning bg-state-warning-hover pr-[30px]',
)}
>
{
isEmpty && (
<div className="mr-2 flex h-6 w-6 shrink-0 items-center justify-center">
<div className="flex h-5 w-5 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle">
<span className="i-ri-brain-2-line h-3.5 w-3.5 text-text-quaternary" />
</div>
</div>
)
}
{
!isEmpty && currentProvider && (
<ModelIcon
className="mr-1.5 !h-5 !w-5"
provider={currentProvider}
modelName={currentModel?.model}
/>
)
}
{
!isEmpty && !currentProvider && (
<ModelIcon
className="mr-1.5 !h-5 !w-5"
provider={modelProviders.find(item => item.provider === providerName)}
modelName={modelId}
/>
)
}
{
!isEmpty && currentModel && (
<ModelName
className="mr-1.5 text-text-primary"
modelItem={currentModel}
showMode={shouldShowModelMeta}
showFeatures={shouldShowModelMeta}
/>
)
}
{
!isEmpty && !currentModel && (
<div className="mr-1 truncate text-[13px] font-medium text-text-primary">
{modelId}
</div>
)
}
{
isEmpty && (
<div className="mr-1 flex-1 truncate text-[13px] font-normal text-text-secondary">
{t('workflow:errorMsg.configureModel')}
</div>
)
}
{
!isEmpty && (
disabled
<div className="flex h-8 min-w-[296px] cursor-pointer items-center gap-px overflow-hidden rounded-lg">
<div className={cn('flex flex-1 items-center gap-0.5 rounded-l-lg p-1', isInWorkflow ? 'border border-workflow-block-parma-bg bg-workflow-block-parma-bg' : 'bg-components-input-bg-normal')}>
<ModelIcon
className="p-0.5"
provider={iconProvider}
modelName={currentModel?.model || modelId}
/>
<div className="flex flex-1 items-center truncate px-1 py-[3px]">
{currentModel
? (
<Tooltip>
<TooltipTrigger
render={(
<div className="ml-auto flex min-w-[20px] shrink-0 items-center justify-center gap-[3px] rounded-md border border-text-warning bg-components-badge-bg-dimm px-[5px] py-0.5">
<span className="i-ri-alert-fill h-3 w-3 text-text-warning" />
<span className="whitespace-nowrap text-text-warning system-xs-medium">
{t('modelProvider.selector.incompatible', { ns: 'common' })}
</span>
</div>
)}
/>
<TooltipContent placement="top">
{t('modelProvider.selector.incompatibleTip', { ns: 'common' })}
</TooltipContent>
</Tooltip>
<ModelName
className="grow"
modelItem={currentModel}
showMode={isActive}
showFeatures={isActive}
/>
)
: statusI18nKey
? (
<Tooltip>
<TooltipTrigger
disabled={effectiveStatus !== ModelStatusEnum.noPermission}
render={(
<div
className={cn(
'ml-auto flex min-w-[20px] shrink-0 items-center justify-center gap-[3px] rounded-md border border-text-warning px-[5px] py-0.5',
isCreditsExhausted && 'bg-components-badge-bg-dimm',
)}
>
<span className="i-ri-alert-fill h-3 w-3 text-text-warning" />
<span className="whitespace-nowrap text-text-warning system-xs-medium">
{t(statusI18nKey as 'modelProvider.selector.creditsExhausted', { ns: 'common' })}
</span>
</div>
)}
/>
<TooltipContent placement="top">
{t('modelProvider.selector.incompatibleTip', { ns: 'common' })}
</TooltipContent>
</Tooltip>
)
: (
<SlidersH className={cn(!isInWorkflow ? 'text-indigo-600' : 'text-text-tertiary', 'h-4 w-4 shrink-0')} />
)
)
}
{
isEmpty && (
<RiArrowDownSLine className={cn('h-4 w-4 shrink-0 text-text-tertiary', isInWorkflow && 'absolute right-2 top-[9px] h-3.5 w-3.5')} />
)
}
{!isEmpty && isInWorkflow && (<RiArrowDownSLine className="absolute right-2 top-[9px] h-3.5 w-3.5 text-text-tertiary" />)}
: <div className="truncate text-[13px] font-normal text-components-input-text-filled">{modelId}</div>}
</div>
{badgeKey && (
<Tooltip>
<TooltipTrigger
render={(
<div className="flex shrink-0 items-center pr-0.5">
<div className="flex min-w-[20px] shrink-0 items-center justify-center gap-[3px] rounded-md border border-text-warning bg-components-badge-bg-dimm px-[5px] py-0.5">
<span className="i-ri-alert-fill h-3 w-3 text-text-warning" />
<span className="whitespace-nowrap text-text-warning system-xs-medium">
{t(badgeKey as 'modelProvider.selector.incompatible', { ns: 'common' })}
</span>
</div>
</div>
)}
/>
<TooltipContent placement="top">
{t(tooltipKey as 'modelProvider.selector.incompatibleTip', { ns: 'common' })}
</TooltipContent>
</Tooltip>
)}
<div className="flex shrink-0 items-center pr-1">
<span className="i-ri-arrow-down-s-line h-4 w-4 text-text-tertiary" />
</div>
</div>
<div className={cn('flex shrink-0 items-center justify-center rounded-r-lg p-2', isInWorkflow ? 'border border-workflow-block-parma-bg bg-workflow-block-parma-bg' : 'bg-components-button-tertiary-bg')}>
<span className="i-ri-equalizer-2-line h-4 w-4 text-text-tertiary" />
</div>
</div>
)
}

View File

@ -114,15 +114,8 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
}
}, [scopedModelList, value?.provider, value?.model])
const hasDeprecated = useMemo(() => {
return !currentProvider || !currentModel
}, [currentModel, currentProvider])
const modelDisabled = useMemo(() => {
return currentModel?.status !== ModelStatusEnum.active
}, [currentModel?.status])
const disabled = useMemo(() => {
return !isAPIKeySet || hasDeprecated || modelDisabled
}, [hasDeprecated, isAPIKeySet, modelDisabled])
const hasDeprecated = !currentProvider || !currentModel
const disabled = !isAPIKeySet || hasDeprecated || currentModel?.status !== ModelStatusEnum.active
const handleChangeModel = async ({ provider, model }: DefaultModel) => {
const targetProvider = scopedModelList.find(modelItem => modelItem.provider === provider)
@ -203,9 +196,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
renderTrigger
? renderTrigger({
open,
disabled,
modelDisabled,
hasDeprecated,
currentProvider,
currentModel,
providerName: value?.provider,
@ -225,10 +215,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
)
: (
<Trigger
disabled={disabled}
isInWorkflow={isInWorkflow}
modelDisabled={modelDisabled}
hasDeprecated={hasDeprecated}
currentProvider={currentProvider}
currentModel={currentModel}
providerName={value?.provider}