| @@ -98,7 +98,7 @@ const DebugItem: FC<DebugItemProps> = ({ | |||
| ? [ | |||
| { | |||
| value: 'remove', | |||
| text: t('common.operation.remove'), | |||
| text: t('common.operation.remove') as string, | |||
| }, | |||
| ] | |||
| : undefined | |||
| @@ -8,23 +8,30 @@ import { | |||
| } from '@/app/components/base/portal-to-follow-elem' | |||
| export type Item = { | |||
| value: string | |||
| text: string | |||
| value: string | number | |||
| text: string | JSX.Element | |||
| } | |||
| type DropdownProps = { | |||
| items: Item[] | |||
| secondItems?: Item[] | |||
| onSelect: (item: Item) => void | |||
| renderTrigger?: (open: boolean) => React.ReactNode | |||
| popupClassName?: string | |||
| } | |||
| const Dropdown: FC<DropdownProps> = ({ | |||
| items, | |||
| onSelect, | |||
| secondItems, | |||
| renderTrigger, | |||
| popupClassName, | |||
| }) => { | |||
| const [open, setOpen] = useState(false) | |||
| const handleSelect = (item: Item) => { | |||
| setOpen(false) | |||
| onSelect(item) | |||
| } | |||
| return ( | |||
| <PortalToFollowElem | |||
| open={open} | |||
| @@ -47,7 +54,7 @@ const Dropdown: FC<DropdownProps> = ({ | |||
| ) | |||
| } | |||
| </PortalToFollowElemTrigger> | |||
| <PortalToFollowElemContent> | |||
| <PortalToFollowElemContent className={popupClassName}> | |||
| <div className='rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg text-sm text-gray-700'> | |||
| { | |||
| !!items.length && ( | |||
| @@ -57,7 +64,7 @@ const Dropdown: FC<DropdownProps> = ({ | |||
| <div | |||
| key={item.value} | |||
| className='flex items-center px-3 h-8 rounded-lg cursor-pointer hover:bg-gray-100' | |||
| onClick={() => onSelect(item)} | |||
| onClick={() => handleSelect(item)} | |||
| > | |||
| {item.text} | |||
| </div> | |||
| @@ -79,7 +86,7 @@ const Dropdown: FC<DropdownProps> = ({ | |||
| <div | |||
| key={item.value} | |||
| className='flex items-center px-3 h-8 rounded-lg cursor-pointer hover:bg-gray-100' | |||
| onClick={() => onSelect(item)} | |||
| onClick={() => handleSelect(item)} | |||
| > | |||
| {item.text} | |||
| </div> | |||
| @@ -4,7 +4,6 @@ import type { | |||
| } from 'react' | |||
| import { useEffect, useMemo, useState } from 'react' | |||
| import useSWR from 'swr' | |||
| import cn from 'classnames' | |||
| import { useTranslation } from 'react-i18next' | |||
| import type { | |||
| DefaultModel, | |||
| @@ -21,6 +20,7 @@ import ParameterItem from './parameter-item' | |||
| import type { ParameterValue } from './parameter-item' | |||
| import Trigger from './trigger' | |||
| import type { TriggerProps } from './trigger' | |||
| import PresetsParameter from './presets-parameter' | |||
| import { | |||
| PortalToFollowElem, | |||
| PortalToFollowElemContent, | |||
| @@ -30,13 +30,7 @@ import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes' | |||
| import { fetchModelParameterRules } from '@/service/common' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { useProviderContext } from '@/context/provider-context' | |||
| import Radio from '@/app/components/base/radio' | |||
| import { TONE_LIST } from '@/config' | |||
| 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 useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | |||
| import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows' | |||
| export type ModelParameterModalProps = { | |||
| @@ -84,8 +78,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({ | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { hasSettedApiKey } = useProviderContext() | |||
| const media = useBreakpoints() | |||
| const isMobile = media === MediaType.mobile | |||
| const [open, setOpen] = useState(false) | |||
| const { data: parameterRulesData, isLoading } = useSWR((provider && modelId) ? `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}` : null, fetchModelParameterRules) | |||
| const { | |||
| @@ -100,46 +92,10 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({ | |||
| const modelDisabled = currentModel?.status !== ModelStatusEnum.active | |||
| const disabled = !hasSettedApiKey || hasDeprecated || modelDisabled | |||
| const parameterRules = useMemo(() => { | |||
| const parameterRules: ModelParameterRule[] = useMemo(() => { | |||
| return parameterRulesData?.data || [] | |||
| }, [parameterRulesData]) | |||
| // only openai support this | |||
| function matchToneId(completionParams: FormValue): number { | |||
| const remvoedCustomeTone = TONE_LIST.slice(0, -1) | |||
| const CUSTOM_TONE_ID = 4 | |||
| const tone = remvoedCustomeTone.find((tone) => { | |||
| const config: Record<string, any> = tone.config || {} | |||
| return Object.keys(config).every((key) => { | |||
| return config[key] === completionParams[key] | |||
| }) | |||
| }) | |||
| return tone ? tone.id : CUSTOM_TONE_ID | |||
| } | |||
| // tone is a preset of completionParams. | |||
| const [toneId, setToneId] = 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) => { | |||
| const tone = TONE_LIST.find(tone => tone.id === id) | |||
| if (tone) { | |||
| setToneId(id) | |||
| onCompletionParamsChange({ | |||
| ...tone.config, | |||
| }) | |||
| } | |||
| } | |||
| useEffect(() => { | |||
| setToneId(matchToneId(completionParams)) | |||
| }, [completionParams]) | |||
| const handleParamChange = (key: string, value: ParameterValue) => { | |||
| onCompletionParamsChange({ | |||
| ...completionParams, | |||
| @@ -175,7 +131,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({ | |||
| const handleInitialParams = () => { | |||
| const newCompletionParams = { ...completionParams } | |||
| const defaultParams: Record<string, any> = {} | |||
| if (parameterRules.length) { | |||
| parameterRules.forEach((parameterRule) => { | |||
| if (!newCompletionParams[parameterRule.name]) { | |||
| @@ -184,13 +139,8 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({ | |||
| else | |||
| delete newCompletionParams[parameterRule.name] | |||
| } | |||
| if (!isNullOrUndefined(parameterRule.default)) | |||
| defaultParams[parameterRule.name] = parameterRule.default | |||
| }) | |||
| if (PROVIDER_WITH_PRESET_TONE.includes(provider)) | |||
| TONE_LIST[3].config = defaultParams as any | |||
| onCompletionParamsChange(newCompletionParams) | |||
| } | |||
| } | |||
| @@ -199,15 +149,14 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({ | |||
| handleInitialParams() | |||
| }, [parameterRules]) | |||
| 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 | |||
| const handleSelectPresetParameter = (toneId: number) => { | |||
| const tone = TONE_LIST.find(tone => tone.id === toneId) | |||
| if (tone) { | |||
| onCompletionParamsChange({ | |||
| ...completionParams, | |||
| ...tone.config, | |||
| }) | |||
| } | |||
| } | |||
| return ( | |||
| @@ -274,47 +223,18 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({ | |||
| <div className='mt-5'><Loading /></div> | |||
| ) | |||
| } | |||
| {PROVIDER_WITH_PRESET_TONE.includes(provider) && !isLoading && !!parameterRules.length && ( | |||
| <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-1 sm:!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)} | |||
| {!isMobile && <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> | |||
| ))} | |||
| </> | |||
| <Radio | |||
| value={TONE_LIST[3].id} | |||
| className={cn(toneId === 4 && 'rounded-md border border-gray-200 shadow-md', '!mr-0 grow !px-1 sm:!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)} | |||
| {!isMobile && <div>{t(`common.model.tone.${TONE_LIST[3].name}`) as string}</div>} | |||
| </> | |||
| </Radio> | |||
| </Radio.Group> | |||
| </div> | |||
| )} | |||
| { | |||
| !isLoading && !!parameterRules.length && ( | |||
| <div className='flex items-center justify-between mb-4'> | |||
| <div className='text-gray-900 font-semibold'>{t('common.modelProvider.parameters')}</div> | |||
| { | |||
| PROVIDER_WITH_PRESET_TONE.includes(provider) && ( | |||
| <PresetsParameter onSelect={handleSelectPresetParameter} /> | |||
| ) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| { | |||
| !isLoading && !!parameterRules.length && ( | |||
| [ | |||
| @@ -1,5 +1,5 @@ | |||
| import type { FC } from 'react' | |||
| import { useState } from 'react' | |||
| import { useEffect, useRef, useState } from 'react' | |||
| import type { ModelParameterRule } from '../declarations' | |||
| import { useLanguage } from '../hooks' | |||
| import { isNullOrUndefined } from '../utils' | |||
| @@ -29,6 +29,7 @@ const ParameterItem: FC<ParameterItemProps> = ({ | |||
| }) => { | |||
| const language = useLanguage() | |||
| const [localValue, setLocalValue] = useState(value) | |||
| const numberInputRef = useRef<HTMLInputElement>(null) | |||
| const getDefaultValue = () => { | |||
| let defaultValue: ParameterValue | |||
| @@ -57,8 +58,10 @@ const ParameterItem: FC<ParameterItemProps> = ({ | |||
| const handleNumberInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||
| let num = +e.target.value | |||
| if (!isNullOrUndefined(parameterRule.max) && num > parameterRule.max!) | |||
| if (!isNullOrUndefined(parameterRule.max) && num > parameterRule.max!) { | |||
| num = parameterRule.max as number | |||
| numberInputRef.current!.value = `${num}` | |||
| } | |||
| if (!isNullOrUndefined(parameterRule.min) && num < parameterRule.min!) | |||
| num = parameterRule.min as number | |||
| @@ -66,14 +69,26 @@ const ParameterItem: FC<ParameterItemProps> = ({ | |||
| handleInputChange(num) | |||
| } | |||
| const handleNumberInputBlur = () => { | |||
| if (numberInputRef.current) | |||
| numberInputRef.current.value = renderValue as string | |||
| } | |||
| const handleSlideChange = (num: number) => { | |||
| if (!isNullOrUndefined(parameterRule.max) && num > parameterRule.max!) | |||
| return handleInputChange(parameterRule.max) | |||
| if (!isNullOrUndefined(parameterRule.max) && num > parameterRule.max!) { | |||
| handleInputChange(parameterRule.max) | |||
| numberInputRef.current!.value = `${parameterRule.max}` | |||
| return | |||
| } | |||
| if (!isNullOrUndefined(parameterRule.min) && num < parameterRule.min!) | |||
| return handleInputChange(parameterRule.min) | |||
| if (!isNullOrUndefined(parameterRule.min) && num < parameterRule.min!) { | |||
| handleInputChange(parameterRule.min) | |||
| numberInputRef.current!.value = `${parameterRule.min}` | |||
| return | |||
| } | |||
| handleInputChange(num) | |||
| numberInputRef.current!.value = `${num}` | |||
| } | |||
| const handleRadioChange = (v: number) => { | |||
| @@ -129,13 +144,14 @@ const ParameterItem: FC<ParameterItemProps> = ({ | |||
| onChange={handleSlideChange} | |||
| />} | |||
| <input | |||
| ref={numberInputRef} | |||
| className='shrink-0 block ml-4 pl-3 w-16 h-8 appearance-none outline-none rounded-lg bg-gray-100 text-[13px] text-gra-900' | |||
| type='number' | |||
| max={parameterRule.max} | |||
| min={parameterRule.min} | |||
| step={numberInputWithSlide ? step : +`0.${parameterRule.precision || 0}`} | |||
| value={renderValue as string} | |||
| onChange={handleNumberInputChange} | |||
| onBlur={handleNumberInputBlur} | |||
| /> | |||
| </> | |||
| ) | |||
| @@ -191,6 +207,11 @@ const ParameterItem: FC<ParameterItemProps> = ({ | |||
| return null | |||
| } | |||
| useEffect(() => { | |||
| if (numberInputRef.current) | |||
| numberInputRef.current.value = `${renderValue}` | |||
| }, []) | |||
| return ( | |||
| <div className={`flex items-center justify-between ${className}`}> | |||
| <div> | |||
| @@ -0,0 +1,65 @@ | |||
| import type { FC } from 'react' | |||
| import { useCallback } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import Dropdown from '@/app/components/base/dropdown' | |||
| import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' | |||
| import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows' | |||
| 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 { TONE_LIST } from '@/config' | |||
| type PresetsParameterProps = { | |||
| onSelect: (toneId: number) => void | |||
| } | |||
| const PresetsParameter: FC<PresetsParameterProps> = ({ | |||
| onSelect, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const renderTrigger = useCallback((open: boolean) => { | |||
| return ( | |||
| <div | |||
| className={` | |||
| flex items-center px-[7px] h-7 rounded-md border-[0.5px] border-gray-200 shadow-xs | |||
| text-xs font-medium text-gray-700 cursor-pointer | |||
| ${open && 'bg-gray-100'} | |||
| `} | |||
| > | |||
| <SlidersH className='mr-[5px] w-3.5 h-3.5 text-gray-500' /> | |||
| {t('common.modelProvider.loadPresets')} | |||
| <ChevronDown className='ml-0.5 w-3.5 h-3.5 text-gray-500' /> | |||
| </div> | |||
| ) | |||
| }, []) | |||
| const getToneIcon = (toneId: number) => { | |||
| const className = 'mr-2 w-[14px] h-[14px]' | |||
| const res = ({ | |||
| 1: <Brush01 className={`${className} text-[#6938EF]`} />, | |||
| 2: <Scales02 className={`${className} text-indigo-600`} />, | |||
| 3: <Target04 className={`${className} text-[#107569]`} />, | |||
| })[toneId] | |||
| return res | |||
| } | |||
| const options = TONE_LIST.slice(0, 3).map((tone) => { | |||
| return { | |||
| value: tone.id, | |||
| text: ( | |||
| <div className='flex items-center h-full'> | |||
| {getToneIcon(tone.id)} | |||
| {t(`common.model.tone.${tone.name}`) as string} | |||
| </div> | |||
| ), | |||
| } | |||
| }) | |||
| return ( | |||
| <Dropdown | |||
| renderTrigger={renderTrigger} | |||
| items={options} | |||
| onSelect={item => onSelect(item.value as number)} | |||
| popupClassName='z-[70]' | |||
| /> | |||
| ) | |||
| } | |||
| export default PresetsParameter | |||
| @@ -309,6 +309,8 @@ const translation = { | |||
| deprecated: 'Deprecated', | |||
| confirmDelete: 'confirm deletion?', | |||
| quotaTip: 'Remaining available free tokens', | |||
| loadPresets: 'Load Presents', | |||
| parameters: 'PARAMETERS', | |||
| }, | |||
| dataSource: { | |||
| add: 'Add a data source', | |||
| @@ -138,6 +138,8 @@ const translation = { | |||
| deprecated: 'Descontinuado', | |||
| confirmDelete: 'confirmar exclusão?', | |||
| quotaTip: 'Tokens gratuitos disponíveis restantes', | |||
| loadPresets: 'Carregar presentes', | |||
| parameters: 'PARÂMETROS', | |||
| }, | |||
| dataSource: { | |||
| add: 'Adicionar uma fonte de dados', | |||
| @@ -309,6 +309,8 @@ const translation = { | |||
| deprecated: '已弃用', | |||
| confirmDelete: '确认删除?', | |||
| quotaTip: '剩余免费额度', | |||
| loadPresets: '加载预设', | |||
| parameters: '参数', | |||
| }, | |||
| dataSource: { | |||
| add: '添加数据源', | |||