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:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
@@ -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}
|
||||
|
||||
127
web/app/components/app/overview/apikey-info-panel/index.tsx
Normal file
127
web/app/components/app/overview/apikey-info-panel/index.tsx
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user