ソースを参照

fix: model parameter modal input (#2206)

zxhlyh 1 年間 前
コミット
4ff17af5de

+ 1 - 1
web/app/components/app/configuration/debug/debug-with-multiple-model/debug-item.tsx

@@ -98,7 +98,7 @@ const DebugItem: FC<DebugItemProps> = ({
               ? [
                 {
                   value: 'remove',
-                  text: t('common.operation.remove'),
+                  text: t('common.operation.remove') as string,
                 },
               ]
               : undefined

+ 12 - 5
web/app/components/base/dropdown/index.tsx

@@ -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>

+ 22 - 102
web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx

@@ -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 && (
                   [

+ 28 - 7
web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx

@@ -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>

+ 65 - 0
web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx

@@ -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

+ 2 - 0
web/i18n/lang/common.en.ts

@@ -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',

+ 2 - 0
web/i18n/lang/common.pt.ts

@@ -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',

+ 2 - 0
web/i18n/lang/common.zh.ts

@@ -309,6 +309,8 @@ const translation = {
     deprecated: '已弃用',
     confirmDelete: '确认删除?',
     quotaTip: '剩余免费额度',
+    loadPresets: '加载预设',
+    parameters: '参数',
   },
   dataSource: {
     add: '添加数据源',