'use client' import type { FC } from 'react' import React, { useEffect, useState } from 'react' import cn from 'classnames' import { useTranslation } from 'react-i18next' import { useBoolean, useClickAway, useGetState } from 'ahooks' import { 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 ModelModeTypeLabel from './model-mode-type-label' import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' import Radio from '@/app/components/base/radio' import Panel from '@/app/components/base/panel' import type { CompletionParams } from '@/models/debug' 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' 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' import type { ModelModeType } from '@/types/app' export type IConfigModelProps = { isAdvancedMode: boolean mode: string modelId: string provider: ProviderEnum setModel: (model: { id: string; provider: ProviderEnum; mode: ModelModeType }) => void completionParams: CompletionParams onCompletionParamsChange: (newParams: CompletionParams) => void disabled: boolean } const ConfigModel: FC = ({ isAdvancedMode, modelId, provider, setModel, completionParams, onCompletionParamsChange, disabled, }) => { const { t } = useTranslation() const { textGenerationModelList } = useProviderContext() const [isShowConfig, { setFalse: hideConfig, toggle: toogleShowConfig }] = useBoolean(false) const [maxTokenSettingTipVisible, setMaxTokenSettingTipVisible] = useState(false) const configContentRef = React.useRef(null) const currModel = textGenerationModelList.find(item => item.model_name === modelId) // Cache loaded model param const [allParams, setAllParams, getAllParams] = useGetState>>({}) 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 if (isAdvancedMode) currSupportParams.push('stop') 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 selectedModel = { name: modelId } // options.find(option => option.id === modelId) const ensureModelParamLoaded = (provider: ProviderEnum, modelId: string) => { return new Promise((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, provider: nextProvider, mode }: { id: string; provider: ProviderEnum; mode: ModelModeType }) => { return async () => { const prevParamsRule = getAllParams()[provider]?.[modelId] setModel({ id, provider: nextProvider || ProviderEnum.openai, mode, }) 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(newConCompletionParams) } } // only openai support this function matchToneId(completionParams: CompletionParams): number { const remvoedCustomeTone = TONE_LIST.slice(0, -1) const CUSTOM_TONE_ID = 4 const tone = remvoedCustomeTone.find((tone) => { return tone.config?.temperature === completionParams.temperature && tone.config?.top_p === completionParams.top_p && tone.config?.presence_penalty === completionParams.presence_penalty && tone.config?.frequency_penalty === completionParams.frequency_penalty }) return tone ? tone.id : CUSTOM_TONE_ID } // 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) return // custom tone const tone = TONE_LIST.find(tone => tone.id === id) if (tone) { setToneId(id) onCompletionParamsChange({ ...tone.config, max_tokens: completionParams.max_tokens, } as CompletionParams) } } useEffect(() => { setToneId(matchToneId(completionParams)) }, [completionParams]) const handleParamChange = (key: string, value: number | string[]) => { if (value === undefined) return if (key === 'stop') { onCompletionParamsChange({ ...completionParams, [key]: value as string[], }) } else { const currParamsRule = getAllParams()[provider]?.[modelId] let notOutRangeValue = parseFloat((value as number).toFixed(2)) notOutRangeValue = Math.max(currParamsRule[key].min, notOutRangeValue) notOutRangeValue = Math.min(currParamsRule[key].max, notOutRangeValue) 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: , 2: , 3: , 4: , })[toneId] return res } useEffect(() => { 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) }, [currParams, completionParams.max_tokens, setMaxTokenSettingTipVisible]) return (
!disabled && toogleShowConfig()} >
{isAdvancedMode && } {disabled ? : }
{isShowConfig && ( } title={t('appDebug.modelConfig.title')} >
{t('appDebug.modelConfig.model')}
{ handleSelectModel({ id: model.model_name, provider: model.model_provider.provider_name as ProviderEnum, mode: model.model_mode, })() }} />
{hasEnableParams && (
)} {/* Tone type */} {[ProviderEnum.openai, ProviderEnum.azure_openai].includes(provider) && (
{t('appDebug.modelConfig.setTone')}
<> {TONE_LIST.slice(0, 3).map(tone => (
<> {getToneIcon(tone.id)}
{t(`common.model.tone.${tone.name}`) as string}
{tone.id !== toneId && tone.id + 1 !== toneId && (
)}
))} <> {getToneIcon(TONE_LIST[3].id)}
{t(`common.model.tone.${TONE_LIST[3].name}`) as string}
)} {/* Params */}
{(allParams[provider]?.[modelId]) ? ( currSupportParams.map(key => ()) ) : ( )}
{ maxTokenSettingTipVisible && (
{t('common.model.params.maxTokenSettingTip')}
) }
)}
) } export default React.memo(ConfigModel)