| ? [ | ? [ | ||||
| { | { | ||||
| value: 'remove', | value: 'remove', | ||||
| text: t('common.operation.remove'), | |||||
| text: t('common.operation.remove') as string, | |||||
| }, | }, | ||||
| ] | ] | ||||
| : undefined | : undefined |
| } from '@/app/components/base/portal-to-follow-elem' | } from '@/app/components/base/portal-to-follow-elem' | ||||
| export type Item = { | export type Item = { | ||||
| value: string | |||||
| text: string | |||||
| value: string | number | |||||
| text: string | JSX.Element | |||||
| } | } | ||||
| type DropdownProps = { | type DropdownProps = { | ||||
| items: Item[] | items: Item[] | ||||
| secondItems?: Item[] | secondItems?: Item[] | ||||
| onSelect: (item: Item) => void | onSelect: (item: Item) => void | ||||
| renderTrigger?: (open: boolean) => React.ReactNode | renderTrigger?: (open: boolean) => React.ReactNode | ||||
| popupClassName?: string | |||||
| } | } | ||||
| const Dropdown: FC<DropdownProps> = ({ | const Dropdown: FC<DropdownProps> = ({ | ||||
| items, | items, | ||||
| onSelect, | onSelect, | ||||
| secondItems, | secondItems, | ||||
| renderTrigger, | renderTrigger, | ||||
| popupClassName, | |||||
| }) => { | }) => { | ||||
| const [open, setOpen] = useState(false) | const [open, setOpen] = useState(false) | ||||
| const handleSelect = (item: Item) => { | |||||
| setOpen(false) | |||||
| onSelect(item) | |||||
| } | |||||
| return ( | return ( | ||||
| <PortalToFollowElem | <PortalToFollowElem | ||||
| open={open} | open={open} | ||||
| ) | ) | ||||
| } | } | ||||
| </PortalToFollowElemTrigger> | </PortalToFollowElemTrigger> | ||||
| <PortalToFollowElemContent> | |||||
| <PortalToFollowElemContent className={popupClassName}> | |||||
| <div className='rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg text-sm text-gray-700'> | <div className='rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg text-sm text-gray-700'> | ||||
| { | { | ||||
| !!items.length && ( | !!items.length && ( | ||||
| <div | <div | ||||
| key={item.value} | key={item.value} | ||||
| className='flex items-center px-3 h-8 rounded-lg cursor-pointer hover:bg-gray-100' | className='flex items-center px-3 h-8 rounded-lg cursor-pointer hover:bg-gray-100' | ||||
| onClick={() => onSelect(item)} | |||||
| onClick={() => handleSelect(item)} | |||||
| > | > | ||||
| {item.text} | {item.text} | ||||
| </div> | </div> | ||||
| <div | <div | ||||
| key={item.value} | key={item.value} | ||||
| className='flex items-center px-3 h-8 rounded-lg cursor-pointer hover:bg-gray-100' | className='flex items-center px-3 h-8 rounded-lg cursor-pointer hover:bg-gray-100' | ||||
| onClick={() => onSelect(item)} | |||||
| onClick={() => handleSelect(item)} | |||||
| > | > | ||||
| {item.text} | {item.text} | ||||
| </div> | </div> |
| } from 'react' | } from 'react' | ||||
| import { useEffect, useMemo, useState } from 'react' | import { useEffect, useMemo, useState } from 'react' | ||||
| import useSWR from 'swr' | import useSWR from 'swr' | ||||
| import cn from 'classnames' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import type { | import type { | ||||
| DefaultModel, | DefaultModel, | ||||
| import type { ParameterValue } from './parameter-item' | import type { ParameterValue } from './parameter-item' | ||||
| import Trigger from './trigger' | import Trigger from './trigger' | ||||
| import type { TriggerProps } from './trigger' | import type { TriggerProps } from './trigger' | ||||
| import PresetsParameter from './presets-parameter' | |||||
| import { | import { | ||||
| PortalToFollowElem, | PortalToFollowElem, | ||||
| PortalToFollowElemContent, | PortalToFollowElemContent, | ||||
| import { fetchModelParameterRules } from '@/service/common' | import { fetchModelParameterRules } from '@/service/common' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import { useProviderContext } from '@/context/provider-context' | import { useProviderContext } from '@/context/provider-context' | ||||
| import Radio from '@/app/components/base/radio' | |||||
| import { TONE_LIST } from '@/config' | 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' | import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows' | ||||
| export type ModelParameterModalProps = { | export type ModelParameterModalProps = { | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { hasSettedApiKey } = useProviderContext() | const { hasSettedApiKey } = useProviderContext() | ||||
| const media = useBreakpoints() | |||||
| const isMobile = media === MediaType.mobile | |||||
| const [open, setOpen] = useState(false) | 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 { data: parameterRulesData, isLoading } = useSWR((provider && modelId) ? `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}` : null, fetchModelParameterRules) | ||||
| const { | const { | ||||
| const modelDisabled = currentModel?.status !== ModelStatusEnum.active | const modelDisabled = currentModel?.status !== ModelStatusEnum.active | ||||
| const disabled = !hasSettedApiKey || hasDeprecated || modelDisabled | const disabled = !hasSettedApiKey || hasDeprecated || modelDisabled | ||||
| const parameterRules = useMemo(() => { | |||||
| const parameterRules: ModelParameterRule[] = useMemo(() => { | |||||
| return parameterRulesData?.data || [] | return parameterRulesData?.data || [] | ||||
| }, [parameterRulesData]) | }, [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) => { | const handleParamChange = (key: string, value: ParameterValue) => { | ||||
| onCompletionParamsChange({ | onCompletionParamsChange({ | ||||
| ...completionParams, | ...completionParams, | ||||
| const handleInitialParams = () => { | const handleInitialParams = () => { | ||||
| const newCompletionParams = { ...completionParams } | const newCompletionParams = { ...completionParams } | ||||
| const defaultParams: Record<string, any> = {} | |||||
| if (parameterRules.length) { | if (parameterRules.length) { | ||||
| parameterRules.forEach((parameterRule) => { | parameterRules.forEach((parameterRule) => { | ||||
| if (!newCompletionParams[parameterRule.name]) { | if (!newCompletionParams[parameterRule.name]) { | ||||
| else | else | ||||
| delete newCompletionParams[parameterRule.name] | 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) | onCompletionParamsChange(newCompletionParams) | ||||
| } | } | ||||
| } | } | ||||
| handleInitialParams() | handleInitialParams() | ||||
| }, [parameterRules]) | }, [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 ( | return ( | ||||
| <div className='mt-5'><Loading /></div> | <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 && ( | !isLoading && !!parameterRules.length && ( | ||||
| [ | [ |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { useState } from 'react' | |||||
| import { useEffect, useRef, useState } from 'react' | |||||
| import type { ModelParameterRule } from '../declarations' | import type { ModelParameterRule } from '../declarations' | ||||
| import { useLanguage } from '../hooks' | import { useLanguage } from '../hooks' | ||||
| import { isNullOrUndefined } from '../utils' | import { isNullOrUndefined } from '../utils' | ||||
| }) => { | }) => { | ||||
| const language = useLanguage() | const language = useLanguage() | ||||
| const [localValue, setLocalValue] = useState(value) | const [localValue, setLocalValue] = useState(value) | ||||
| const numberInputRef = useRef<HTMLInputElement>(null) | |||||
| const getDefaultValue = () => { | const getDefaultValue = () => { | ||||
| let defaultValue: ParameterValue | let defaultValue: ParameterValue | ||||
| const handleNumberInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { | const handleNumberInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
| let num = +e.target.value | let num = +e.target.value | ||||
| if (!isNullOrUndefined(parameterRule.max) && num > parameterRule.max!) | |||||
| if (!isNullOrUndefined(parameterRule.max) && num > parameterRule.max!) { | |||||
| num = parameterRule.max as number | num = parameterRule.max as number | ||||
| numberInputRef.current!.value = `${num}` | |||||
| } | |||||
| if (!isNullOrUndefined(parameterRule.min) && num < parameterRule.min!) | if (!isNullOrUndefined(parameterRule.min) && num < parameterRule.min!) | ||||
| num = parameterRule.min as number | num = parameterRule.min as number | ||||
| handleInputChange(num) | handleInputChange(num) | ||||
| } | } | ||||
| const handleNumberInputBlur = () => { | |||||
| if (numberInputRef.current) | |||||
| numberInputRef.current.value = renderValue as string | |||||
| } | |||||
| const handleSlideChange = (num: number) => { | 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) | handleInputChange(num) | ||||
| numberInputRef.current!.value = `${num}` | |||||
| } | } | ||||
| const handleRadioChange = (v: number) => { | const handleRadioChange = (v: number) => { | ||||
| onChange={handleSlideChange} | onChange={handleSlideChange} | ||||
| />} | />} | ||||
| <input | <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' | 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' | type='number' | ||||
| max={parameterRule.max} | max={parameterRule.max} | ||||
| min={parameterRule.min} | min={parameterRule.min} | ||||
| step={numberInputWithSlide ? step : +`0.${parameterRule.precision || 0}`} | step={numberInputWithSlide ? step : +`0.${parameterRule.precision || 0}`} | ||||
| value={renderValue as string} | |||||
| onChange={handleNumberInputChange} | onChange={handleNumberInputChange} | ||||
| onBlur={handleNumberInputBlur} | |||||
| /> | /> | ||||
| </> | </> | ||||
| ) | ) | ||||
| return null | return null | ||||
| } | } | ||||
| useEffect(() => { | |||||
| if (numberInputRef.current) | |||||
| numberInputRef.current.value = `${renderValue}` | |||||
| }, []) | |||||
| return ( | return ( | ||||
| <div className={`flex items-center justify-between ${className}`}> | <div className={`flex items-center justify-between ${className}`}> | ||||
| <div> | <div> |
| 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 |
| deprecated: 'Deprecated', | deprecated: 'Deprecated', | ||||
| confirmDelete: 'confirm deletion?', | confirmDelete: 'confirm deletion?', | ||||
| quotaTip: 'Remaining available free tokens', | quotaTip: 'Remaining available free tokens', | ||||
| loadPresets: 'Load Presents', | |||||
| parameters: 'PARAMETERS', | |||||
| }, | }, | ||||
| dataSource: { | dataSource: { | ||||
| add: 'Add a data source', | add: 'Add a data source', |
| deprecated: 'Descontinuado', | deprecated: 'Descontinuado', | ||||
| confirmDelete: 'confirmar exclusão?', | confirmDelete: 'confirmar exclusão?', | ||||
| quotaTip: 'Tokens gratuitos disponíveis restantes', | quotaTip: 'Tokens gratuitos disponíveis restantes', | ||||
| loadPresets: 'Carregar presentes', | |||||
| parameters: 'PARÂMETROS', | |||||
| }, | }, | ||||
| dataSource: { | dataSource: { | ||||
| add: 'Adicionar uma fonte de dados', | add: 'Adicionar uma fonte de dados', |
| deprecated: '已弃用', | deprecated: '已弃用', | ||||
| confirmDelete: '确认删除?', | confirmDelete: '确认删除?', | ||||
| quotaTip: '剩余免费额度', | quotaTip: '剩余免费额度', | ||||
| loadPresets: '加载预设', | |||||
| parameters: '参数', | |||||
| }, | }, | ||||
| dataSource: { | dataSource: { | ||||
| add: '添加数据源', | add: '添加数据源', |