mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
refactor(web): rewrite CredentialPanel with declarative variant-driven state and new ModelAuthDropdown
- Extract useCredentialPanelState hook with discriminated union CardVariant type replacing scattered boolean conditions - Create ModelAuthDropdown compound component (Popover-based) with UsagePrioritySection, CreditsExhaustedAlert, and ApiKeySection - Enhance SystemQuotaCard.Label to accept className override for flexible styling - Add i18n keys for new card states and dropdown content (en-US, zh-Hans)
This commit is contained in:
@ -1,36 +1,40 @@
|
||||
import type {
|
||||
ModelProvider,
|
||||
PreferredProviderTypeEnum,
|
||||
} from '../declarations'
|
||||
import type { CardVariant } from './use-credential-panel-state'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { ConfigProvider } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { changeModelProviderPriority } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
CustomConfigurationStatusEnum,
|
||||
PreferredProviderTypeEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
useUpdateModelList,
|
||||
useUpdateModelProviders,
|
||||
} from '../hooks'
|
||||
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './index'
|
||||
import PrioritySelector from './priority-selector'
|
||||
import PriorityUseTip from './priority-use-tip'
|
||||
import ModelAuthDropdown from './model-auth-dropdown'
|
||||
import SystemQuotaCard from './system-quota-card'
|
||||
import { useTrialCredits } from './use-trial-credits'
|
||||
import { isDestructiveVariant, useCredentialPanelState } from './use-credential-panel-state'
|
||||
|
||||
type CredentialPanelProps = {
|
||||
provider: ModelProvider
|
||||
}
|
||||
|
||||
const TEXT_LABEL_VARIANTS = new Set<CardVariant>([
|
||||
'credits-active',
|
||||
'credits-exhausted',
|
||||
'no-usage',
|
||||
'api-required-add',
|
||||
'api-required-configure',
|
||||
])
|
||||
|
||||
const CredentialPanel = ({
|
||||
provider,
|
||||
}: CredentialPanelProps) => {
|
||||
@ -40,29 +44,12 @@ const CredentialPanel = ({
|
||||
const queryClient = useQueryClient()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const customConfig = provider.custom_configuration
|
||||
const systemConfig = provider.system_configuration
|
||||
const priorityUseType = provider.preferred_provider_type
|
||||
const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active
|
||||
const configurateMethods = provider.configurate_methods
|
||||
const {
|
||||
hasCredential,
|
||||
authorized,
|
||||
authRemoved,
|
||||
current_credential_name,
|
||||
notAllowedToUse,
|
||||
} = useCredentialStatus(provider)
|
||||
const state = useCredentialPanelState(provider)
|
||||
|
||||
const showPrioritySelector = systemConfig.enabled && isCustomConfigured && IS_CLOUD_EDITION
|
||||
const isUsingSystemQuota = systemConfig.enabled && priorityUseType === PreferredProviderTypeEnum.system && IS_CLOUD_EDITION
|
||||
const { isExhausted } = useTrialCredits()
|
||||
|
||||
const handleChangePriority = async (key: PreferredProviderTypeEnum) => {
|
||||
const handleChangePriority = useCallback(async (key: PreferredProviderTypeEnum) => {
|
||||
const res = await changeModelProviderPriority({
|
||||
url: `/workspaces/current/model-providers/${provider.provider}/preferred-provider-type`,
|
||||
body: {
|
||||
preferred_provider_type: key,
|
||||
},
|
||||
body: { preferred_provider_type: key },
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
@ -71,105 +58,82 @@ const CredentialPanel = ({
|
||||
refetchType: 'none',
|
||||
})
|
||||
updateModelProviders()
|
||||
|
||||
configurateMethods.forEach((method) => {
|
||||
provider.configurate_methods.forEach((method) => {
|
||||
if (method === ConfigurationMethodEnum.predefinedModel)
|
||||
provider.supported_model_types.forEach(modelType => updateModelList(modelType))
|
||||
})
|
||||
|
||||
eventEmitter?.emit({
|
||||
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
|
||||
payload: provider.provider,
|
||||
} as any)
|
||||
} as { type: string, payload: string })
|
||||
}
|
||||
}
|
||||
const credentialLabel = !hasCredential
|
||||
? t('modelProvider.auth.unAuthorized', { ns: 'common' })
|
||||
: authorized
|
||||
? current_credential_name
|
||||
: authRemoved
|
||||
? t('modelProvider.auth.authRemoved', { ns: 'common' })
|
||||
: ''
|
||||
}, [provider, notify, t, queryClient, updateModelProviders, updateModelList, eventEmitter])
|
||||
|
||||
const color = (authRemoved || !hasCredential)
|
||||
? 'red'
|
||||
: notAllowedToUse
|
||||
? 'gray'
|
||||
: 'green'
|
||||
const { variant, credentialName } = state
|
||||
const isDestructive = isDestructiveVariant(variant)
|
||||
const isTextLabel = TEXT_LABEL_VARIANTS.has(variant)
|
||||
|
||||
if (isUsingSystemQuota) {
|
||||
return (
|
||||
<SystemQuotaCard variant={isExhausted ? 'destructive' : 'default'}>
|
||||
<SystemQuotaCard.Label>
|
||||
{isExhausted
|
||||
? t('modelProvider.card.quotaExhausted', { ns: 'common' })
|
||||
: t('modelProvider.card.aiCreditsInUse', { ns: 'common' })}
|
||||
</SystemQuotaCard.Label>
|
||||
<SystemQuotaCard.Actions>
|
||||
<ConfigProvider provider={provider} />
|
||||
{showPrioritySelector && (
|
||||
<PrioritySelector
|
||||
value={priorityUseType}
|
||||
onSelect={handleChangePriority}
|
||||
/>
|
||||
)}
|
||||
</SystemQuotaCard.Actions>
|
||||
</SystemQuotaCard>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<SystemQuotaCard variant={isDestructive ? 'destructive' : 'default'}>
|
||||
<SystemQuotaCard.Label className={isTextLabel ? undefined : 'gap-1'}>
|
||||
{isTextLabel
|
||||
? <TextLabel variant={variant} />
|
||||
: <StatusLabel variant={variant} credentialName={credentialName} />}
|
||||
</SystemQuotaCard.Label>
|
||||
<SystemQuotaCard.Actions>
|
||||
<ModelAuthDropdown
|
||||
provider={provider}
|
||||
state={state}
|
||||
onChangePriority={handleChangePriority}
|
||||
/>
|
||||
</SystemQuotaCard.Actions>
|
||||
</SystemQuotaCard>
|
||||
)
|
||||
}
|
||||
|
||||
function TextLabel({ variant }: { variant: CardVariant }) {
|
||||
const { t } = useTranslation()
|
||||
const isDestructive = isDestructiveVariant(variant)
|
||||
const labelKey = variant === 'credits-active'
|
||||
? 'modelProvider.card.aiCreditsInUse'
|
||||
: variant === 'credits-exhausted'
|
||||
? 'modelProvider.card.quotaExhausted'
|
||||
: variant === 'no-usage'
|
||||
? 'modelProvider.card.noAvailableUsage'
|
||||
: 'modelProvider.card.apiKeyRequired'
|
||||
|
||||
return (
|
||||
<span className={isDestructive ? 'text-text-destructive' : 'text-text-secondary'}>
|
||||
{t(labelKey, { ns: 'common' })}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusLabel({ variant, credentialName }: {
|
||||
variant: CardVariant
|
||||
credentialName: string | undefined
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const dotColor = variant === 'api-unavailable' ? 'red' : 'green'
|
||||
const showWarning = variant === 'api-fallback'
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
provider.provider_credential_schema && (
|
||||
<div className={cn(
|
||||
'relative ml-1 w-[120px] shrink-0 rounded-lg border-[0.5px] border-components-panel-border bg-white/[0.18] p-1',
|
||||
authRemoved && 'border-state-destructive-border bg-state-destructive-hover',
|
||||
)}
|
||||
>
|
||||
<div className="mb-1 flex h-5 items-center justify-between pl-2 pr-[7px] pt-1 text-text-tertiary system-xs-medium">
|
||||
<div
|
||||
className={cn(
|
||||
'grow truncate',
|
||||
authRemoved && 'text-text-destructive',
|
||||
)}
|
||||
title={credentialLabel}
|
||||
>
|
||||
{credentialLabel}
|
||||
</div>
|
||||
<Indicator className="shrink-0" color={color} />
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<ConfigProvider
|
||||
provider={provider}
|
||||
/>
|
||||
{
|
||||
showPrioritySelector && (
|
||||
<PrioritySelector
|
||||
value={priorityUseType}
|
||||
onSelect={handleChangePriority}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
priorityUseType === PreferredProviderTypeEnum.custom && systemConfig.enabled && (
|
||||
<PriorityUseTip />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
showPrioritySelector && !provider.provider_credential_schema && (
|
||||
<div className="ml-1">
|
||||
<PrioritySelector
|
||||
value={priorityUseType}
|
||||
onSelect={handleChangePriority}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Indicator className="shrink-0" color={dotColor} />
|
||||
<span
|
||||
className="truncate text-text-secondary"
|
||||
title={credentialName}
|
||||
>
|
||||
{credentialName}
|
||||
</span>
|
||||
{showWarning && (
|
||||
<span className="i-ri-error-warning-fill h-3 w-3 shrink-0 text-text-warning" />
|
||||
)}
|
||||
{variant === 'api-unavailable' && (
|
||||
<span className="shrink-0 text-text-destructive system-2xs-medium">
|
||||
{t('modelProvider.card.unavailable', { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
import type { Credential, CustomModel, ModelProvider } from '../../declarations'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import CredentialItem from '../../model-auth/authorized/credential-item'
|
||||
|
||||
type ApiKeySectionProps = {
|
||||
provider: ModelProvider
|
||||
credentials: Credential[]
|
||||
selectedCredentialId: string | undefined
|
||||
onItemClick: (credential: Credential, model?: CustomModel) => void
|
||||
onEdit: (credential?: Credential) => void
|
||||
onDelete: (credential?: Credential) => void
|
||||
onAdd: () => void
|
||||
}
|
||||
|
||||
function ApiKeySection({
|
||||
provider,
|
||||
credentials,
|
||||
selectedCredentialId,
|
||||
onItemClick,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onAdd,
|
||||
}: ApiKeySectionProps) {
|
||||
const { t } = useTranslation()
|
||||
const notAllowCustomCredential = provider.allow_custom_token === false
|
||||
|
||||
const handleItemClick = useCallback((credential: Credential) => {
|
||||
onItemClick(credential)
|
||||
}, [onItemClick])
|
||||
|
||||
if (!credentials.length) {
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div className="rounded-[10px] bg-gradient-to-b from-state-base-hover to-transparent p-3">
|
||||
<div className="text-text-secondary system-xs-medium">
|
||||
{t('modelProvider.card.noApiKeysTitle', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="mt-0.5 text-text-tertiary system-2xs-regular">
|
||||
{t('modelProvider.card.noApiKeysDescription', { ns: 'common' })}
|
||||
</div>
|
||||
</div>
|
||||
{!notAllowCustomCredential && (
|
||||
<Button
|
||||
onClick={onAdd}
|
||||
className="mt-1 w-full"
|
||||
>
|
||||
{t('modelProvider.auth.addApiKey', { ns: 'common' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="px-3 pb-0.5 pt-2 text-text-tertiary system-2xs-medium-uppercase">
|
||||
{t('modelProvider.auth.apiKeys', { ns: 'common' })}
|
||||
</div>
|
||||
<div className="max-h-[200px] overflow-y-auto px-1">
|
||||
{credentials.map(credential => (
|
||||
<CredentialItem
|
||||
key={credential.credential_id}
|
||||
credential={credential}
|
||||
showSelectedIcon
|
||||
selectedCredentialId={selectedCredentialId}
|
||||
onItemClick={handleItemClick}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!notAllowCustomCredential && (
|
||||
<div className="border-t border-t-divider-subtle p-2">
|
||||
<Button
|
||||
onClick={onAdd}
|
||||
className="w-full"
|
||||
>
|
||||
{t('modelProvider.auth.addApiKey', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ApiKeySection)
|
||||
@ -0,0 +1,51 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { useTrialCredits } from '../use-trial-credits'
|
||||
|
||||
type CreditsExhaustedAlertProps = {
|
||||
hasApiKeyFallback: boolean
|
||||
}
|
||||
|
||||
export default function CreditsExhaustedAlert({ hasApiKeyFallback }: CreditsExhaustedAlertProps) {
|
||||
const { t } = useTranslation()
|
||||
const { credits } = useTrialCredits()
|
||||
const totalCredits = 10_000
|
||||
|
||||
const titleKey = hasApiKeyFallback
|
||||
? 'modelProvider.card.creditsExhaustedFallback'
|
||||
: 'modelProvider.card.creditsExhaustedMessage'
|
||||
const descriptionKey = hasApiKeyFallback
|
||||
? 'modelProvider.card.creditsExhaustedFallbackDescription'
|
||||
: 'modelProvider.card.creditsExhaustedDescription'
|
||||
|
||||
return (
|
||||
<div className="mx-3 mb-1 mt-0.5 rounded-lg bg-background-section-burn p-3">
|
||||
<div className="text-text-primary system-xs-medium">
|
||||
{t(titleKey, { ns: 'common' })}
|
||||
</div>
|
||||
<div className="mt-0.5 text-text-tertiary system-2xs-regular">
|
||||
{t(descriptionKey, {
|
||||
ns: 'common',
|
||||
upgradeLink: `<a class="text-text-accent cursor-pointer system-2xs-medium-uppercase">${t('modelProvider.card.upgradePlan', { ns: 'common' })}</a>`,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<span className="text-text-tertiary system-2xs-regular">
|
||||
{t('modelProvider.card.usageLabel', { ns: 'common' })}
|
||||
</span>
|
||||
<div className="flex items-center gap-0.5 text-text-tertiary system-2xs-regular">
|
||||
<span className="i-ri-coin-line h-3 w-3" />
|
||||
<span>
|
||||
{formatNumber(totalCredits - credits)}
|
||||
/
|
||||
{formatNumber(totalCredits)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 h-1 overflow-hidden rounded-full bg-state-destructive-hover-alt">
|
||||
<div className="h-full rounded-full bg-state-destructive-solid" style={{ width: '100%' }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
import type { Credential, ModelProvider, PreferredProviderTypeEnum } from '../../declarations'
|
||||
import type { CredentialPanelState } from '../use-credential-panel-state'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@/app/components/base/ui/alert-dialog'
|
||||
import { ConfigurationMethodEnum } from '../../declarations'
|
||||
import { useAuth } from '../../model-auth/hooks'
|
||||
import ApiKeySection from './api-key-section'
|
||||
import CreditsExhaustedAlert from './credits-exhausted-alert'
|
||||
import UsagePrioritySection from './usage-priority-section'
|
||||
|
||||
type DropdownContentProps = {
|
||||
provider: ModelProvider
|
||||
state: CredentialPanelState
|
||||
onChangePriority: (key: PreferredProviderTypeEnum) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function DropdownContent({
|
||||
provider,
|
||||
state,
|
||||
onChangePriority,
|
||||
onClose,
|
||||
}: DropdownContentProps) {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
current_credential_id,
|
||||
available_credentials,
|
||||
} = provider.custom_configuration
|
||||
|
||||
const {
|
||||
openConfirmDelete,
|
||||
closeConfirmDelete,
|
||||
doingAction,
|
||||
handleActiveCredential,
|
||||
handleConfirmDelete,
|
||||
deleteCredentialId,
|
||||
handleOpenModal,
|
||||
} = useAuth(provider, ConfigurationMethodEnum.predefinedModel)
|
||||
|
||||
const handleItemClick = useCallback((credential: Credential) => {
|
||||
handleActiveCredential(credential)
|
||||
onClose()
|
||||
}, [handleActiveCredential, onClose])
|
||||
|
||||
const handleEdit = useCallback((credential?: Credential) => {
|
||||
handleOpenModal(credential)
|
||||
onClose()
|
||||
}, [handleOpenModal, onClose])
|
||||
|
||||
const handleDelete = useCallback((credential?: Credential) => {
|
||||
if (credential)
|
||||
openConfirmDelete(credential)
|
||||
}, [openConfirmDelete])
|
||||
|
||||
const handleAdd = useCallback(() => {
|
||||
handleOpenModal()
|
||||
onClose()
|
||||
}, [handleOpenModal, onClose])
|
||||
|
||||
const showCreditsAlert = state.isCreditsExhausted && state.supportsCredits
|
||||
const hasApiKeyFallback = state.variant === 'api-fallback'
|
||||
|| (state.variant === 'api-active' && state.priority === 'apiKey')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-[320px]">
|
||||
{state.showPrioritySwitcher && (
|
||||
<UsagePrioritySection
|
||||
value={state.priority}
|
||||
onSelect={onChangePriority}
|
||||
/>
|
||||
)}
|
||||
{showCreditsAlert && (
|
||||
<CreditsExhaustedAlert hasApiKeyFallback={hasApiKeyFallback} />
|
||||
)}
|
||||
<ApiKeySection
|
||||
provider={provider}
|
||||
credentials={available_credentials ?? []}
|
||||
selectedCredentialId={current_credential_id}
|
||||
onItemClick={handleItemClick}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onAdd={handleAdd}
|
||||
/>
|
||||
</div>
|
||||
<AlertDialog
|
||||
open={!!deleteCredentialId}
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
closeConfirmDelete()
|
||||
}}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<div className="p-6 pb-0">
|
||||
<AlertDialogTitle className="text-text-primary system-xl-semibold">
|
||||
{t('modelProvider.confirmDelete', { ns: 'common' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="mt-1 text-text-secondary system-sm-regular" />
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton disabled={doingAction}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton disabled={doingAction} onClick={handleConfirmDelete}>
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(DropdownContent)
|
||||
@ -0,0 +1,78 @@
|
||||
import type { ModelProvider, PreferredProviderTypeEnum } from '../../declarations'
|
||||
import type { CardVariant, CredentialPanelState } from '../use-credential-panel-state'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/app/components/base/ui/popover'
|
||||
import DropdownContent from './dropdown-content'
|
||||
|
||||
type ModelAuthDropdownProps = {
|
||||
provider: ModelProvider
|
||||
state: CredentialPanelState
|
||||
onChangePriority: (key: PreferredProviderTypeEnum) => void
|
||||
}
|
||||
|
||||
const ACCENT_VARIANTS = new Set<CardVariant>([
|
||||
'api-required-add',
|
||||
'api-required-configure',
|
||||
])
|
||||
|
||||
function getButtonConfig(variant: CardVariant, hasCredentials: boolean, t: (key: string, opts?: Record<string, string>) => string) {
|
||||
if (ACCENT_VARIANTS.has(variant)) {
|
||||
return {
|
||||
text: variant === 'api-required-add'
|
||||
? t('modelProvider.auth.addApiKey', { ns: 'common' })
|
||||
: t('operation.config', { ns: 'common' }),
|
||||
variant: 'secondary-accent' as const,
|
||||
}
|
||||
}
|
||||
|
||||
const text = hasCredentials
|
||||
? t('operation.config', { ns: 'common' })
|
||||
: t('modelProvider.auth.addApiKey', { ns: 'common' })
|
||||
|
||||
return { text, variant: 'secondary' as const }
|
||||
}
|
||||
|
||||
function ModelAuthDropdown({ provider, state, onChangePriority }: ModelAuthDropdownProps) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleClose = useCallback(() => setOpen(false), [])
|
||||
|
||||
const buttonConfig = getButtonConfig(state.variant, state.hasCredentials, t)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<Button
|
||||
className="flex grow"
|
||||
size="small"
|
||||
variant={buttonConfig.variant}
|
||||
title={buttonConfig.text}
|
||||
>
|
||||
<span className="i-ri-equalizer-2-line mr-1 h-3.5 w-3.5 shrink-0" />
|
||||
<span className="w-0 grow truncate text-left">
|
||||
{buttonConfig.text}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent placement="bottom-end">
|
||||
<DropdownContent
|
||||
provider={provider}
|
||||
state={state}
|
||||
onChangePriority={onChangePriority}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ModelAuthDropdown)
|
||||
@ -0,0 +1,48 @@
|
||||
import type { UsagePriority } from '../use-credential-panel-state'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { PreferredProviderTypeEnum } from '../../declarations'
|
||||
|
||||
type UsagePrioritySectionProps = {
|
||||
value: UsagePriority
|
||||
onSelect: (key: PreferredProviderTypeEnum) => void
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ key: PreferredProviderTypeEnum.system, labelKey: 'modelProvider.card.aiCreditsOption' },
|
||||
{ key: PreferredProviderTypeEnum.custom, labelKey: 'modelProvider.card.apiKeyOption' },
|
||||
] as const
|
||||
|
||||
export default function UsagePrioritySection({ value, onSelect }: UsagePrioritySectionProps) {
|
||||
const { t } = useTranslation()
|
||||
const selectedKey = value === 'credits'
|
||||
? PreferredProviderTypeEnum.system
|
||||
: PreferredProviderTypeEnum.custom
|
||||
|
||||
return (
|
||||
<div className="border-b border-b-divider-subtle px-3 pb-2 pt-2.5">
|
||||
<div className="mb-1.5 flex items-center gap-1 text-text-tertiary system-xs-medium">
|
||||
<span className="i-ri-arrow-up-down-line h-4 w-4 shrink-0" />
|
||||
<span>{t('modelProvider.card.usagePriority', { ns: 'common' })}</span>
|
||||
<span className="i-ri-question-line h-3.5 w-3.5 shrink-0 text-text-quaternary" />
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
{options.map(option => (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex-1 rounded-lg px-2 py-1 text-center transition-colors system-xs-medium',
|
||||
selectedKey === option.key
|
||||
? 'border-[1.5px] border-components-option-card-option-selected-border bg-components-panel-bg text-text-primary shadow-xs'
|
||||
: 'border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary hover:bg-components-option-card-option-bg-hover',
|
||||
)}
|
||||
onClick={() => onSelect(option.key)}
|
||||
>
|
||||
{t(option.labelKey, { ns: 'common' })}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -40,12 +40,12 @@ const SystemQuotaCard = ({
|
||||
)
|
||||
}
|
||||
|
||||
const Label = ({ children }: { children: ReactNode }) => {
|
||||
const Label = ({ children, className }: { children: ReactNode, className?: string }) => {
|
||||
const variant = useContext(VariantContext)
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative z-[1] truncate px-1.5 pt-1 system-xs-medium',
|
||||
labelVariants[variant],
|
||||
'relative z-[1] flex items-center gap-1 truncate px-1.5 pt-1 system-xs-medium',
|
||||
className ?? labelVariants[variant],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import {
|
||||
CustomConfigurationStatusEnum,
|
||||
PreferredProviderTypeEnum,
|
||||
} from '../declarations'
|
||||
import { useTrialCredits } from './use-trial-credits'
|
||||
|
||||
export type UsagePriority = 'credits' | 'apiKey' | 'apiKeyOnly'
|
||||
|
||||
export type CardVariant
|
||||
= | 'credits-active'
|
||||
| 'credits-exhausted'
|
||||
| 'no-usage'
|
||||
| 'api-fallback'
|
||||
| 'api-active'
|
||||
| 'api-required-add'
|
||||
| 'api-required-configure'
|
||||
| 'api-unavailable'
|
||||
|
||||
export type CredentialPanelState = {
|
||||
variant: CardVariant
|
||||
priority: UsagePriority
|
||||
supportsCredits: boolean
|
||||
showPrioritySwitcher: boolean
|
||||
hasCredentials: boolean
|
||||
isCreditsExhausted: boolean
|
||||
credentialName: string | undefined
|
||||
credits: number
|
||||
}
|
||||
|
||||
const DESTRUCTIVE_VARIANTS = new Set<CardVariant>([
|
||||
'credits-exhausted',
|
||||
'no-usage',
|
||||
'api-unavailable',
|
||||
])
|
||||
|
||||
export const isDestructiveVariant = (variant: CardVariant) =>
|
||||
DESTRUCTIVE_VARIANTS.has(variant)
|
||||
|
||||
function deriveVariant(
|
||||
priority: UsagePriority,
|
||||
isExhausted: boolean,
|
||||
hasCredential: boolean,
|
||||
authorized: boolean | undefined,
|
||||
): CardVariant {
|
||||
if (priority === 'credits') {
|
||||
if (!isExhausted)
|
||||
return 'credits-active'
|
||||
if (hasCredential && authorized)
|
||||
return 'api-fallback'
|
||||
if (hasCredential && !authorized)
|
||||
return 'no-usage'
|
||||
return 'credits-exhausted'
|
||||
}
|
||||
|
||||
if (hasCredential && authorized)
|
||||
return 'api-active'
|
||||
if (hasCredential && !authorized)
|
||||
return 'api-unavailable'
|
||||
return 'api-required-add'
|
||||
}
|
||||
|
||||
export function useCredentialPanelState(provider: ModelProvider): CredentialPanelState {
|
||||
const { isExhausted, credits } = useTrialCredits()
|
||||
const {
|
||||
hasCredential,
|
||||
authorized,
|
||||
current_credential_name,
|
||||
} = useCredentialStatus(provider)
|
||||
|
||||
const systemConfig = provider.system_configuration
|
||||
const customConfig = provider.custom_configuration
|
||||
const preferredType = provider.preferred_provider_type
|
||||
|
||||
const supportsCredits = systemConfig.enabled && IS_CLOUD_EDITION
|
||||
const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active
|
||||
|
||||
const priority: UsagePriority = !supportsCredits
|
||||
? 'apiKeyOnly'
|
||||
: preferredType === PreferredProviderTypeEnum.system
|
||||
? 'credits'
|
||||
: 'apiKey'
|
||||
|
||||
const showPrioritySwitcher = supportsCredits && isCustomConfigured
|
||||
|
||||
const variant = deriveVariant(priority, isExhausted, hasCredential, !!authorized)
|
||||
|
||||
return {
|
||||
variant,
|
||||
priority,
|
||||
supportsCredits,
|
||||
showPrioritySwitcher,
|
||||
hasCredentials: hasCredential,
|
||||
isCreditsExhausted: isExhausted,
|
||||
credentialName: current_credential_name,
|
||||
credits,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user