feat: frontend multi models support (#804)

Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
takatost
2023-08-12 00:57:13 +08:00
committed by GitHub
parent 5fa2161b05
commit d10ef17f17
259 changed files with 9105 additions and 1392 deletions

View File

@@ -3,22 +3,34 @@ import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { useBoolean, useClickAway } from 'ahooks'
import { ChevronDownIcon, Cog8ToothIcon, InformationCircleIcon } from '@heroicons/react/24/outline'
import { useBoolean, useClickAway, useGetState } from 'ahooks'
import { Cog8ToothIcon, InformationCircleIcon } from '@heroicons/react/24/outline'
import produce from 'immer'
import ParamItem from './param-item'
import ModelIcon from './model-icon'
import ModelName from './model-name'
import Radio from '@/app/components/base/radio'
import Panel from '@/app/components/base/panel'
import type { CompletionParams } from '@/models/debug'
import { AppType, ProviderType } from '@/types/app'
import { MODEL_LIST, TONE_LIST } from '@/config'
import { TONE_LIST } from '@/config'
import Toast from '@/app/components/base/toast'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { formatNumber } from '@/utils/format'
export type IConifgModelProps = {
import { Brush01 } from '@/app/components/base/icons/src/vender/solid/editor'
import { Scales02 } from '@/app/components/base/icons/src/vender/solid/FinanceAndECommerce'
import { Target04 } from '@/app/components/base/icons/src/vender/solid/general'
import { Sliders02 } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import { fetchModelParams } from '@/service/debug'
import Loading from '@/app/components/base/loading'
import ModelSelector from '@/app/components/header/account-setting/model-page/model-selector'
import { ModelType, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
import { useProviderContext } from '@/context/provider-context'
export type IConfigModelProps = {
mode: string
modelId: string
setModelId: (id: string, provider: ProviderType) => void
provider: ProviderEnum
setModelId: (id: string, provider: ProviderEnum) => void
completionParams: CompletionParams
onCompletionParamsChange: (newParams: CompletionParams) => void
disabled: boolean
@@ -26,21 +38,10 @@ export type IConifgModelProps = {
onShowUseGPT4Confirm: () => void
}
const options = MODEL_LIST
const getMaxToken = (modelId: string) => {
if (['claude-instant-1', 'claude-2'].includes(modelId))
return 30 * 1000
if (['gpt-4', 'gpt-3.5-turbo-16k'].includes(modelId))
return 8000
return 4000
}
const ConifgModel: FC<IConifgModelProps> = ({
mode,
const ConfigModel: FC<IConfigModelProps> = ({
// mode,
modelId,
provider,
setModelId,
completionParams,
onCompletionParamsChange,
@@ -49,89 +50,140 @@ const ConifgModel: FC<IConifgModelProps> = ({
onShowUseGPT4Confirm,
}) => {
const { t } = useTranslation()
const isChatApp = mode === AppType.chat
const availableModels = options.filter(item => item.type === mode)
const { textGenerationModelList } = useProviderContext()
const [isShowConfig, { setFalse: hideConfig, toggle: toogleShowConfig }] = useBoolean(false)
const [maxTokenSettingTipVisible, setMaxTokenSettingTipVisible] = useState(false)
const configContentRef = React.useRef(null)
const currModel = options.find(item => item.id === modelId)
const currModel = textGenerationModelList.find(item => item.model_name === modelId)
// Cache loaded model param
const [allParams, setAllParams, getAllParams] = useGetState<Record<string, Record<string, any>>>({})
const currParams = allParams[provider]?.[modelId]
const hasEnableParams = currParams && Object.keys(currParams).some(key => currParams[key].enabled)
const allSupportParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty', 'max_tokens']
const currSupportParams = currParams ? allSupportParams.filter(key => currParams[key].enabled) : allSupportParams
useEffect(() => {
(async () => {
if (!allParams[provider]?.[modelId]) {
const res = await fetchModelParams(provider, modelId)
const newAllParams = produce(allParams, (draft) => {
if (!draft[provider])
draft[provider] = {}
draft[provider][modelId] = res
})
setAllParams(newAllParams)
}
})()
}, [provider, modelId])
useClickAway(() => {
hideConfig()
}, configContentRef)
const params = [
{
id: 1,
name: t('common.model.params.temperature'),
key: 'temperature',
tip: t('common.model.params.temperatureTip'),
max: 2,
},
{
id: 2,
name: t('common.model.params.topP'),
key: 'top_p',
tip: t('common.model.params.topPTip'),
max: 1,
},
{
id: 3,
name: t('common.model.params.presencePenalty'),
key: 'presence_penalty',
tip: t('common.model.params.presencePenaltyTip'),
min: -2,
max: 2,
},
{
id: 4,
name: t('common.model.params.frequencyPenalty'),
key: 'frequency_penalty',
tip: t('common.model.params.frequencyPenaltyTip'),
min: -2,
max: 2,
},
{
id: 5,
name: t('common.model.params.maxToken'),
key: 'max_tokens',
tip: t('common.model.params.maxTokenTip'),
step: 100,
max: getMaxToken(modelId),
},
]
const selectModelDisabled = false // chat gpt-3.5-turbo, gpt-4; text generation text-davinci-003, gpt-3.5-turbo
const selectedModel = { name: modelId } // options.find(option => option.id === modelId)
const [isShowOption, { setFalse: hideOption, toggle: toogleOption }] = useBoolean(false)
const triggerRef = React.useRef(null)
useClickAway(() => {
hideOption()
}, triggerRef)
const handleSelectModel = (id: string, provider = ProviderType.openai) => {
return () => {
const ensureModelParamLoaded = (provider: ProviderEnum, modelId: string) => {
return new Promise<void>((resolve) => {
if (getAllParams()[provider]?.[modelId]) {
resolve()
return
}
const runId = setInterval(() => {
if (getAllParams()[provider]?.[modelId]) {
resolve()
clearInterval(runId)
}
}, 500)
})
}
const transformValue = (value: number, fromRange: [number, number], toRange: [number, number]): number => {
const [fromStart = 0, fromEnd] = fromRange
const [toStart = 0, toEnd] = toRange
// The following three if is to avoid precision loss
if (fromStart === toStart && fromEnd === toEnd)
return value
if (value <= fromStart)
return toStart
if (value >= fromEnd)
return toEnd
const fromLength = fromEnd - fromStart
const toLength = toEnd - toStart
let adjustedValue = (value - fromStart) * (toLength / fromLength) + toStart
adjustedValue = parseFloat(adjustedValue.toFixed(2))
return adjustedValue
}
const handleSelectModel = (id: string, nextProvider = ProviderEnum.openai) => {
return async () => {
if (id === 'gpt-4' && !canUseGPT4) {
hideConfig()
hideOption()
onShowUseGPT4Confirm()
return
}
const nextSelectModelMaxToken = getMaxToken(id)
if (completionParams.max_tokens > nextSelectModelMaxToken) {
Toast.notify({
type: 'warning',
message: t('common.model.params.setToCurrentModelMaxTokenTip', { maxToken: formatNumber(nextSelectModelMaxToken) }),
const prevParamsRule = getAllParams()[provider]?.[modelId]
setModelId(id, nextProvider)
await ensureModelParamLoaded(nextProvider, id)
const nextParamsRule = getAllParams()[nextProvider]?.[id]
// debugger
const nextSelectModelMaxToken = nextParamsRule.max_tokens.max
const newConCompletionParams = produce(completionParams, (draft: any) => {
if (nextParamsRule.max_tokens.enabled) {
if (completionParams.max_tokens > nextSelectModelMaxToken) {
Toast.notify({
type: 'warning',
message: t('common.model.params.setToCurrentModelMaxTokenTip', { maxToken: formatNumber(nextSelectModelMaxToken) }),
})
draft.max_tokens = parseFloat((nextSelectModelMaxToken * 0.8).toFixed(2))
}
// prev don't have max token
if (!completionParams.max_tokens)
draft.max_tokens = nextParamsRule.max_tokens.default
}
else {
delete draft.max_tokens
}
allSupportParams.forEach((key) => {
if (key === 'max_tokens')
return
if (!nextParamsRule[key].enabled) {
delete draft[key]
return
}
if (draft[key] === undefined) {
draft[key] = nextParamsRule[key].default || 0
return
}
if (!prevParamsRule[key].enabled) {
draft[key] = nextParamsRule[key].default || 0
return
}
draft[key] = transformValue(
draft[key],
[prevParamsRule[key].min, prevParamsRule[key].max],
[nextParamsRule[key].min, nextParamsRule[key].max],
)
})
onCompletionParamsChange({
...completionParams,
max_tokens: nextSelectModelMaxToken,
})
}
setModelId(id, provider)
})
onCompletionParamsChange(newConCompletionParams)
}
}
// only openai support this
function matchToneId(completionParams: CompletionParams): number {
const remvoedCustomeTone = TONE_LIST.slice(0, -1)
const CUSTOM_TONE_ID = 4
@@ -146,6 +198,11 @@ const ConifgModel: FC<IConifgModelProps> = ({
// tone is a preset of completionParams.
const [toneId, setToneId] = React.useState(matchToneId(completionParams)) // default is Balanced
const toneTabBgClassName = ({
1: 'bg-[#F5F8FF]',
2: 'bg-[#F4F3FF]',
3: 'bg-[#F6FEFC]',
})[toneId] || ''
// set completionParams by toneId
const handleToneChange = (id: number) => {
if (id === 4)
@@ -164,40 +221,59 @@ const ConifgModel: FC<IConifgModelProps> = ({
setToneId(matchToneId(completionParams))
}, [completionParams])
const handleParamChange = (id: number, value: number) => {
const key = params.find(item => item.id === id)?.key
const handleParamChange = (key: string, value: number) => {
const currParamsRule = getAllParams()[provider]?.[modelId]
let notOutRangeValue = parseFloat(value.toFixed(2))
notOutRangeValue = Math.max(currParamsRule[key].min, notOutRangeValue)
notOutRangeValue = Math.min(currParamsRule[key].max, notOutRangeValue)
if (key) {
onCompletionParamsChange({
...completionParams,
[key]: value,
})
}
onCompletionParamsChange({
...completionParams,
[key]: notOutRangeValue,
})
}
const ableStyle = 'bg-indigo-25 border-[#2A87F5] cursor-pointer'
const diabledStyle = 'bg-[#FFFCF5] border-[#F79009]'
const getToneIcon = (toneId: number) => {
const className = 'w-[14px] h-[14px]'
const res = ({
1: <Brush01 className={className}/>,
2: <Scales02 className={className} />,
3: <Target04 className={className} />,
4: <Sliders02 className={className} />,
})[toneId]
return res
}
useEffect(() => {
const max = params[4].max
if (currModel?.provider !== ProviderType.anthropic && completionParams.max_tokens > max * 2 / 3)
if (!currParams)
return
const max = currParams.max_tokens.max
const isSupportMaxToken = currParams.max_tokens.enabled
if (isSupportMaxToken && currModel?.model_provider.provider_name !== ProviderEnum.anthropic && completionParams.max_tokens > max * 2 / 3)
setMaxTokenSettingTipVisible(true)
else
setMaxTokenSettingTipVisible(false)
}, [params, completionParams.max_tokens, setMaxTokenSettingTipVisible])
}, [currParams, completionParams.max_tokens, setMaxTokenSettingTipVisible])
return (
<div className='relative' ref={configContentRef}>
<div
className={cn('flex items-center border h-8 px-2.5 space-x-2 rounded-lg', disabled ? diabledStyle : ableStyle)}
onClick={() => !disabled && toogleShowConfig()}
>
<ModelIcon modelId={currModel?.id as string} />
<div className='text-[13px] text-gray-900 font-medium'>{selectedModel.name}</div>
<ModelIcon
modelId={modelId}
providerName={provider}
/>
<div className='text-[13px] text-gray-900 font-medium'>
<ModelName modelId={selectedModel.name} />
</div>
{disabled ? <InformationCircleIcon className='w-3.5 h-3.5 text-[#F79009]' /> : <Cog8ToothIcon className='w-3.5 h-3.5 text-gray-500' />}
</div>
{isShowConfig && (
<Panel
className='absolute z-20 top-8 right-0 !w-[496px] bg-white'
className='absolute z-20 top-8 right-0 !w-[496px] bg-white !overflow-visible shadow-md'
keepUnFold
headerIcon={
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -215,49 +291,88 @@ const ConifgModel: FC<IConifgModelProps> = ({
<div className='py-3 pl-10 pr-6 text-sm'>
<div className="flex items-center justify-between my-5 h-9">
<div>{t('appDebug.modelConfig.model')}</div>
{/* model selector */}
<div className="relative" style={{ zIndex: 30 }}>
<div ref={triggerRef} onClick={() => !selectModelDisabled && toogleOption()} className={cn(selectModelDisabled ? 'cursor-not-allowed' : 'cursor-pointer', 'flex items-center h-9 px-3 space-x-2 rounded-lg bg-gray-50 ')}>
<ModelIcon modelId={currModel?.id as string} />
<div className="text-sm gray-900">{selectedModel?.name}</div>
{!selectModelDisabled && <ChevronDownIcon className={cn(isShowOption && 'rotate-180', 'w-[14px] h-[14px] text-gray-500')} />}
</div>
{isShowOption && (
<div className={cn(isChatApp ? 'min-w-[159px]' : 'w-[179px]', 'absolute right-0 bg-gray-50 rounded-lg shadow')}>
{availableModels.map(item => (
<div key={item.id} onClick={handleSelectModel(item.id, item.provider)} className="flex items-center h-9 px-3 rounded-lg cursor-pointer hover:bg-gray-100">
<ModelIcon className='shrink-0 mr-2' modelId={item?.id} />
<div className="text-sm gray-900 whitespace-nowrap">{item.name}</div>
<ModelSelector
popClassName='right-0'
triggerIconSmall
value={{
modelName: modelId,
providerName: provider,
}}
modelType={ModelType.textGeneration}
onChange={(model) => {
handleSelectModel(model.model_name, model.model_provider.provider_name as ProviderEnum)()
}}
/>
</div>
{hasEnableParams && (
<div className="border-b border-gray-100"></div>
)}
{/* Tone type */}
{[ProviderEnum.openai, ProviderEnum.azure_openai].includes(provider) && (
<div className="mt-5 mb-4">
<div className="mb-3 text-sm text-gray-900">{t('appDebug.modelConfig.setTone')}</div>
<Radio.Group className={cn('!rounded-lg', toneTabBgClassName)} value={toneId} onChange={handleToneChange}>
<>
{TONE_LIST.slice(0, 3).map(tone => (
<div className='grow flex items-center' key={tone.id}>
<Radio
value={tone.id}
className={cn(tone.id === toneId && 'rounded-md border border-gray-200 shadow-md', '!mr-0 grow !px-2 !justify-center text-[13px] font-medium')}
labelClassName={cn(tone.id === toneId
? ({
1: 'text-[#6938EF]',
2: 'text-[#444CE7]',
3: 'text-[#107569]',
})[toneId]
: 'text-[#667085]', 'flex items-center space-x-2')}
>
<>
{getToneIcon(tone.id)}
<div>{t(`common.model.tone.${tone.name}`) as string}</div>
<div className=""></div>
</>
</Radio>
{tone.id !== toneId && tone.id + 1 !== toneId && (<div className='h-5 border-r border-gray-200'></div>)}
</div>
))}
</div>
)}
</>
<Radio
value={TONE_LIST[3].id}
className={cn(toneId === 4 && 'rounded-md border border-gray-200 shadow-md', '!mr-0 grow !px-2 !justify-center text-[13px] font-medium')}
labelClassName={cn('flex items-center space-x-2 ', toneId === 4 ? 'text-[#155EEF]' : 'text-[#667085]')}
>
<>
{getToneIcon(TONE_LIST[3].id)}
<div>{t(`common.model.tone.${TONE_LIST[3].name}`) as string}</div>
</>
</Radio>
</Radio.Group>
</div>
</div>
<div className="border-b border-gray-100"></div>
{/* Response type */}
<div className="mt-5 mb-4">
<div className="mb-4 text-sm text-gray-900">{t('appDebug.modelConfig.setTone')}</div>
<Radio.Group value={toneId} onChange={handleToneChange}>
<>
{TONE_LIST.slice(0, 3).map(tone => (
<Radio key={tone.id} value={tone.id} className="grow !px-0 !justify-center">{t(`common.model.tone.${tone.name}`) as string}</Radio>
))}
</>
<div className="ml-[2px] mr-[3px] h-5 border-r border-gray-200"></div>
<Radio value={TONE_LIST[3].id}>{t(`common.model.tone.${TONE_LIST[3].name}`) as string}</Radio>
</Radio.Group>
</div>
)}
{/* Params */}
<div className="mt-4 space-y-4">
{params.map(({ key, ...param }) => (<ParamItem key={key} {...param} value={(completionParams as any)[key] as any} onChange={handleParamChange} />))}
<div className={cn(hasEnableParams && 'mt-4', 'space-y-4', !allParams[provider]?.[modelId] && 'flex items-center min-h-[200px]')}>
{allParams[provider]?.[modelId]
? (
currSupportParams.map(key => (<ParamItem
key={key}
id={key}
name={t(`common.model.params.${key}`)}
tip={t(`common.model.params.${key}Tip`)}
{...currParams[key] as any}
value={(completionParams as any)[key] as any}
onChange={handleParamChange}
/>))
)
: (
<Loading type='area'/>
)}
</div>
</div>
{
maxTokenSettingTipVisible && (
<div className='flex py-2 pr-4 pl-5 bg-[#FFFAEB] border-t border-[#FEF0C7]'>
<div className='flex py-2 pr-4 pl-5 rounded-bl-xl rounded-br-xl bg-[#FFFAEB] border-t border-[#FEF0C7]'>
<AlertTriangle className='shrink-0 mr-2 mt-[3px] w-3 h-3 text-[#F79009]' />
<div className='mr-2 text-xs font-medium text-gray-700'>{t('common.model.params.maxTokenSettingTip')}</div>
</div>
@@ -270,4 +385,4 @@ const ConifgModel: FC<IConifgModelProps> = ({
)
}
export default React.memo(ConifgModel)
export default React.memo(ConfigModel)

View File

@@ -1,25 +1,31 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { ProviderType } from '@/types/app'
import { MODEL_LIST } from '@/config'
import { Anthropic, Gpt3, Gpt4 } from '@/app/components/base/icons/src/public/llm'
import cn from 'classnames'
import {
OpenaiGreen,
OpenaiViolet,
} from '@/app/components/base/icons/src/public/llm'
import { ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
import ProviderConfig from '@/app/components/header/account-setting/model-page/configs'
export type IModelIconProps = { modelId: string; className?: string }
export type IModelIconProps = {
modelId: string
providerName: ProviderEnum
className?: string
}
const ModelIcon: FC<IModelIconProps> = ({ modelId, className }) => {
const resClassName = `w-4 h-4 ${className}`
const model = MODEL_LIST.find(item => item.id === modelId)
if (model?.id === 'gpt-4')
return <Gpt4 className={resClassName} />
const ModelIcon: FC<IModelIconProps> = ({ modelId, providerName, className }) => {
let Icon = <OpenaiGreen className='w-full h-full' />
if (providerName === ProviderEnum.openai)
Icon = modelId.includes('gpt-4') ? <OpenaiViolet className='w-full h-full' /> : <OpenaiGreen className='w-full h-full' />
else
Icon = ProviderConfig[providerName]?.selector.icon
if (model?.provider === ProviderType.anthropic) {
return (
<Anthropic className={resClassName} />
)
}
return (
<Gpt3 className={resClassName} />
<div className={cn(className, 'w-4 h-4')}>
{Icon}
</div>
)
}

View File

@@ -0,0 +1,29 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
export type IModelNameProps = {
modelId: string
}
export const supportI18nModelName = [
'gpt-3.5-turbo', 'gpt-3.5-turbo-16k',
'gpt-4', 'gpt-4-32k',
'text-davinci-003', 'text-embedding-ada-002', 'whisper-1',
'claude-instant-1', 'claude-2',
]
const ModelName: FC<IModelNameProps> = ({
modelId,
}) => {
const { t } = useTranslation()
const name = supportI18nModelName.includes(modelId) ? t(`common.modelName.${modelId}`) : modelId
return (
<span title={name}>
{name}
</span>
)
}
export default React.memo(ModelName)

View File

@@ -5,21 +5,21 @@ import Tooltip from '@/app/components/base/tooltip'
import Slider from '@/app/components/base/slider'
export type IParamIteProps = {
id: number
id: string
name: string
tip: string
value: number
step?: number
min?: number
max: number
onChange: (id: number, value: number) => void
onChange: (key: string, value: number) => void
}
const ParamIte: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max, value, onChange }) => {
return (
<div className="flex items-center justify-between">
<div className="flex items-center">
<span className="mr-[6px]">{name}</span>
<span className="mr-[6px] text-gray-500 text-[13px] font-medium">{name}</span>
{/* Give tooltip different tip to avoiding hide bug */}
<Tooltip htmlContent={<div className="w-[200px]">{tip}</div>} position='top' selector={`param-name-tooltip-${id}`}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -33,7 +33,7 @@ const ParamIte: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max,
</div>
<input type="number" min={min} max={max} step={step} className="block w-[64px] h-9 leading-9 rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600" value={value} onChange={(e) => {
const value = parseFloat(e.target.value)
if (value < 0 || value > max)
if (value < min || value > max)
return
onChange(id, value)

View File

@@ -0,0 +1,24 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector'
import I18n from '@/context/i18n'
import type { ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
import ProviderConfig from '@/app/components/header/account-setting/model-page/configs'
export type IProviderNameProps = {
provideName: ProviderEnum
}
const ProviderName: FC<IProviderNameProps> = ({
provideName,
}) => {
const { locale } = useContext(I18n)
return (
<span>
{ProviderConfig[provideName]?.selector?.name[locale]}
</span>
)
}
export default React.memo(ProviderName)

View File

@@ -12,7 +12,7 @@ import FormattingChanged from '../base/warning-mask/formatting-changed'
import GroupName from '../base/group-name'
import { AppType } from '@/types/app'
import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
import type { IChatItem } from '@/app/components/app/chat'
import type { IChatItem } from '@/app/components/app/chat/type'
import Chat from '@/app/components/app/chat'
import ConfigContext from '@/context/debug-configuration'
import { ToastContext } from '@/app/components/base/toast'

View File

@@ -284,6 +284,7 @@ const Configuration: FC = () => {
{/* Model and Parameters */}
<ConfigModel
mode={mode}
provider={modelConfig.provider as ProviderType}
completionParams={completionParams}
modelId={modelConfig.model_id}
setModelId={setModelId}

View File

@@ -0,0 +1,127 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import useSWR from 'swr'
import Progress from './progress'
import Button from '@/app/components/base/button'
import { LinkExternal02, XClose } from '@/app/components/base/icons/src/vender/line/general'
import AccountSetting from '@/app/components/header/account-setting'
import { fetchTenantInfo } from '@/service/common'
import { IS_CE_EDITION } from '@/config'
import { useProviderContext } from '@/context/provider-context'
const APIKeyInfoPanel: FC = () => {
const isCloud = !IS_CE_EDITION
const { providers }: any = useProviderContext()
const { t } = useTranslation()
const [showSetAPIKeyModal, setShowSetAPIKeyModal] = useState(false)
const [isShow, setIsShow] = useState(true)
const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
if (!userInfo)
return null
const hasBindAPI = userInfo?.providers?.find(({ token_is_set }) => token_is_set)
if (hasBindAPI)
return null
// first show in trail and not used exhausted, else find the exhausted
const [used, total, providerName] = (() => {
if (!providers || !isCloud)
return [0, 0, '']
let used = 0
let total = 0
let trailProviderName = ''
let hasFoundNotExhausted = false
Object.keys(providers).forEach((providerName) => {
if (hasFoundNotExhausted)
return
providers[providerName].providers.forEach(({ quota_type, quota_limit, quota_used }: any) => {
if (quota_type === 'trial') {
if (quota_limit !== quota_used)
hasFoundNotExhausted = true
used = quota_used
total = quota_limit
trailProviderName = providerName
}
})
})
return [used, total, trailProviderName]
})()
const usedPercent = Math.round(used / total * 100)
const exhausted = isCloud && usedPercent === 100
if (!(isShow))
return null
return (
<div className={cn(exhausted ? 'bg-[#FEF3F2] border-[#FEE4E2]' : 'bg-[#EFF4FF] border-[#D1E0FF]', 'mb-6 relative rounded-2xl shadow-md border p-8 ')}>
<div className={cn('text-[24px] text-gray-800 font-semibold', isCloud ? 'flex items-center h-8 space-x-1' : 'leading-8 mb-6')}>
{isCloud && <em-emoji id={exhausted ? '🤔' : '😀'} />}
{isCloud
? (
<div>{t(`appOverview.apiKeyInfo.cloud.${exhausted ? 'exhausted' : 'trial'}.title`, { providerName })}</div>
)
: (
<div>
<div>{t('appOverview.apiKeyInfo.selfHost.title.row1')}</div>
<div>{t('appOverview.apiKeyInfo.selfHost.title.row2')}</div>
</div>
)}
</div>
{isCloud && (
<div className='mt-1 text-sm text-gray-600 font-normal'>{t(`appOverview.apiKeyInfo.cloud.${exhausted ? 'exhausted' : 'trial'}.description`)}</div>
)}
{/* Call times info */}
{isCloud && (
<div className='my-5'>
<div className='flex items-center h-5 space-x-2 text-sm text-gray-700 font-medium'>
<div>{t('appOverview.apiKeyInfo.callTimes')}</div>
<div>·</div>
<div className={cn('font-semibold', exhausted && 'text-[#D92D20]')}>{used}/{total}</div>
</div>
<Progress className='mt-2' value={usedPercent} />
</div>
)}
<Button
type='primary'
className='space-x-2'
onClick={() => {
setShowSetAPIKeyModal(true)
}}
>
<div className='text-sm font-medium'>{t('appOverview.apiKeyInfo.setAPIBtn')}</div>
<LinkExternal02 className='w-4 h-4' />
</Button>
{!isCloud && (
<a
className='mt-2 flex items-center h-[26px] text-xs font-medium text-[#155EEF] p-1 space-x-1'
href='https://cloud.dify.ai/apps'
target='_blank'
>
<div>{t('appOverview.apiKeyInfo.tryCloud')}</div>
<LinkExternal02 className='w-3 h-3' />
</a>
)}
<div
onClick={() => setIsShow(false)}
className='absolute right-4 top-4 flex items-center justify-center w-8 h-8 cursor-pointer '>
<XClose className='w-4 h-4 text-gray-500' />
</div>
{
showSetAPIKeyModal && (
<AccountSetting activeTab="provider" onCancel={async () => {
setShowSetAPIKeyModal(false)
}} />
)
}
</div>
)
}
export default React.memo(APIKeyInfoPanel)

View File

@@ -0,0 +1,29 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
import s from './style.module.css'
export type IProgressProps = {
className?: string
value: number // percent
}
const Progress: FC<IProgressProps> = ({
className,
value,
}) => {
const exhausted = value === 100
return (
<div className={cn(className, 'relative grow h-2 flex bg-gray-200 rounded-md overflow-hidden')}>
<div
className={cn(s.bar, exhausted && s['bar-error'], 'absolute top-0 left-0 right-0 bottom-0')}
style={{ width: `${value}%` }}
/>
{Array(10).fill(0).map((i, k) => (
<div key={k} className={s['bar-item']} />
))}
</div>
)
}
export default React.memo(Progress)

View File

@@ -0,0 +1,16 @@
.bar {
background: linear-gradient(90deg, rgba(41, 112, 255, 0.9) 0%, rgba(21, 94, 239, 0.9) 100%);
}
.bar-error {
background: linear-gradient(90deg, rgba(240, 68, 56, 0.72) 0%, rgba(217, 45, 32, 0.9) 100%);
}
.bar-item {
width: 10%;
border-right: 1px solid rgba(255, 255, 255, 0.5);
}
.bar-item:last-of-type {
border-right: 0;
}

View File

@@ -110,7 +110,7 @@ function AppCard({
return (
<div
className={`flex flex-col w-full shadow-sm border-[0.5px] rounded-lg border-gray-200 ${className ?? ''}`}
className={`flex flex-col w-full shadow-xs border-[0.5px] rounded-lg border-gray-200 ${className ?? ''}`}
>
<div className={`px-6 py-4 ${customBgColor ?? bgColor} rounded-lg`}>
<div className="mb-2.5 flex flex-row items-start justify-between">

View File

@@ -225,7 +225,7 @@ const Chart: React.FC<IChartProps> = ({
const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData)
return (
<div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-sm ${className ?? ''}`}>
<div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-xs ${className ?? ''}`}>
<div className='mb-3'>
<Basic name={title} type={timePeriod} hoverTip={explanation} />
</div>