mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
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:
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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',
|
||||
}
|
||||
@ -49,7 +49,7 @@ vi.mock('@/service/use-common', () => ({
|
||||
data: {
|
||||
data: parameterRules,
|
||||
},
|
||||
isPending: isRulesLoading,
|
||||
isLoading: isRulesLoading,
|
||||
}),
|
||||
}))
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
Reference in New Issue
Block a user