feat: model load balancing (#4926)

This commit is contained in:
Nite Knite 2024-06-05 00:13:29 +08:00 committed by GitHub
parent d1dbbc1e33
commit 37f292ea91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 1896 additions and 304 deletions

View File

@ -123,7 +123,7 @@ const AssistantTypePicker: FC<Props> = ({
</div> </div>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 1000 }}> <PortalToFollowElemContent style={{ zIndex: 1000 }}>
<div className='relative left-0.5 p-6 bg-white border border-black/[0.08] shadow-lg rounded-xl w-[480px]'> <div className='relative left-0.5 p-6 bg-white border border-black/8 shadow-lg rounded-xl w-[480px]'>
<div className='mb-2 leading-5 text-sm font-semibold text-gray-900'>{t('appDebug.assistantType.name')}</div> <div className='mb-2 leading-5 text-sm font-semibold text-gray-900'>{t('appDebug.assistantType.name')}</div>
<SelectItem <SelectItem
Icon={BubbleText} Icon={BubbleText}

View File

@ -41,7 +41,7 @@ import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
type IDebug = { type IDebug = {
hasSetAPIKEY: boolean isAPIKeySet: boolean
onSetting: () => void onSetting: () => void
inputs: Inputs inputs: Inputs
modelParameterParams: Pick<ModelParameterModalProps, 'setModel' | 'onCompletionParamsChange'> modelParameterParams: Pick<ModelParameterModalProps, 'setModel' | 'onCompletionParamsChange'>
@ -51,7 +51,7 @@ type IDebug = {
} }
const Debug: FC<IDebug> = ({ const Debug: FC<IDebug> = ({
hasSetAPIKEY = true, isAPIKeySet = true,
onSetting, onSetting,
inputs, inputs,
modelParameterParams, modelParameterParams,
@ -503,7 +503,7 @@ const Debug: FC<IDebug> = ({
onCancel={handleCancel} onCancel={handleCancel}
/> />
)} )}
{!hasSetAPIKEY && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)} {!isAPIKeySet && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
</> </>
) )
} }

View File

@ -255,7 +255,7 @@ const Configuration: FC = () => {
}) })
} }
const { hasSettedApiKey } = useProviderContext() const { isAPIKeySet } = useProviderContext()
const { const {
currentModel: currModel, currentModel: currModel,
textGenerationModelList, textGenerationModelList,
@ -678,7 +678,7 @@ const Configuration: FC = () => {
return ( return (
<ConfigContext.Provider value={{ <ConfigContext.Provider value={{
appId, appId,
hasSetAPIKEY: hasSettedApiKey, isAPIKeySet,
isTrailFinished: false, isTrailFinished: false,
mode, mode,
modelModeType, modelModeType,
@ -818,7 +818,7 @@ const Configuration: FC = () => {
{!isMobile && <div className="relative flex flex-col w-1/2 h-full overflow-y-auto grow " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}> {!isMobile && <div className="relative flex flex-col w-1/2 h-full overflow-y-auto grow " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
<div className='flex flex-col h-0 border-t border-l grow rounded-tl-2xl bg-gray-50 '> <div className='flex flex-col h-0 border-t border-l grow rounded-tl-2xl bg-gray-50 '>
<Debug <Debug
hasSetAPIKEY={hasSettedApiKey} isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })} onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs} inputs={inputs}
modelParameterParams={{ modelParameterParams={{
@ -881,7 +881,7 @@ const Configuration: FC = () => {
{isMobile && ( {isMobile && (
<Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null} panelClassname='!bg-gray-50'> <Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null} panelClassname='!bg-gray-50'>
<Debug <Debug
hasSetAPIKEY={hasSettedApiKey} isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })} onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs} inputs={inputs}
modelParameterParams={{ modelParameterParams={{

View File

@ -12,14 +12,14 @@ import { useModalContext } from '@/context/modal-context'
const APIKeyInfoPanel: FC = () => { const APIKeyInfoPanel: FC = () => {
const isCloud = !IS_CE_EDITION const isCloud = !IS_CE_EDITION
const { hasSettedApiKey } = useProviderContext() const { isAPIKeySet } = useProviderContext()
const { setShowAccountSettingModal } = useModalContext() const { setShowAccountSettingModal } = useModalContext()
const { t } = useTranslation() const { t } = useTranslation()
const [isShow, setIsShow] = useState(true) const [isShow, setIsShow] = useState(true)
if (hasSettedApiKey) if (isAPIKeySet)
return null return null
if (!(isShow)) if (!(isShow))

View File

@ -132,8 +132,7 @@ function AppCard({
return ( return (
<div <div
className={`shadow-xs border-[0.5px] rounded-lg border-gray-200 ${ className={`shadow-xs border-[0.5px] rounded-lg border-gray-200 ${className ?? ''
className ?? ''
}`} }`}
> >
<div className={`px-6 py-5 ${customBgColor ?? bgColor} rounded-lg`}> <div className={`px-6 py-5 ${customBgColor ?? bgColor} rounded-lg`}>
@ -165,7 +164,7 @@ function AppCard({
? t('appOverview.overview.appInfo.accessibleAddress') ? t('appOverview.overview.appInfo.accessibleAddress')
: t('appOverview.overview.apiInfo.accessibleAddress')} : t('appOverview.overview.apiInfo.accessibleAddress')}
</div> </div>
<div className="w-full h-9 pl-2 pr-0.5 py-0.5 bg-black bg-opacity-[0.02] rounded-lg border border-black border-opacity-5 justify-start items-center inline-flex"> <div className="w-full h-9 pl-2 pr-0.5 py-0.5 bg-black bg-opacity-2 rounded-lg border border-black border-opacity-5 justify-start items-center inline-flex">
<div className="h-4 px-2 justify-start items-start gap-2 flex flex-1 min-w-0"> <div className="h-4 px-2 justify-start items-start gap-2 flex flex-1 min-w-0">
<div className="text-gray-700 text-xs font-medium text-ellipsis overflow-hidden whitespace-nowrap"> <div className="text-gray-700 text-xs font-medium text-ellipsis overflow-hidden whitespace-nowrap">
{isApp ? appUrl : apiUrl} {isApp ? appUrl : apiUrl}
@ -203,8 +202,7 @@ function AppCard({
onClick={() => setShowConfirmDelete(true)} onClick={() => setShowConfirmDelete(true)}
> >
<div <div
className={`w-full h-full ${style.refreshIcon} ${ className={`w-full h-full ${style.refreshIcon} ${genLoading ? style.generateLogo : ''
genLoading ? style.generateLogo : ''
}`} }`}
></div> ></div>
</div> </div>

View File

@ -3,10 +3,10 @@
@layer components { @layer components {
.btn { .btn {
@apply inline-flex justify-center items-center content-center h-9 leading-5 rounded-lg px-4 py-2 text-base cursor-pointer whitespace-nowrap; @apply inline-flex justify-center items-center content-center h-9 leading-5 rounded-lg px-4 py-2 text-base cursor-pointer whitespace-nowrap;
} };
.btn-default { .btn-default {
@apply border-solid border border-gray-200 cursor-pointer text-gray-500 hover:bg-white hover:shadow-sm hover:border-gray-300; @apply border-solid border border-gray-200 cursor-pointer text-gray-700 hover:bg-white hover:shadow-sm hover:border-gray-300;
} }
.btn-default-disabled { .btn-default-disabled {
@ -28,4 +28,4 @@
.btn-warning-disabled { .btn-warning-disabled {
@apply bg-red-600/75 cursor-not-allowed text-white; @apply bg-red-600/75 cursor-not-allowed text-white;
} }
} }

View File

@ -1,16 +1,16 @@
import type { FC, MouseEventHandler } from 'react' import type { FC, MouseEventHandler, PropsWithChildren } from 'react'
import React from 'react' import React, { memo } from 'react'
import classNames from 'classnames'
import Spinner from '../spinner' import Spinner from '../spinner'
export type IButtonProps = { export type IButtonProps = PropsWithChildren<{
type?: string type?: string
className?: string className?: string
disabled?: boolean disabled?: boolean
loading?: boolean loading?: boolean
tabIndex?: number tabIndex?: number
children: React.ReactNode
onClick?: MouseEventHandler<HTMLDivElement> onClick?: MouseEventHandler<HTMLDivElement>
} }>
const Button: FC<IButtonProps> = ({ const Button: FC<IButtonProps> = ({
type, type,
@ -21,22 +21,22 @@ const Button: FC<IButtonProps> = ({
loading = false, loading = false,
tabIndex, tabIndex,
}) => { }) => {
let style = 'cursor-pointer' let typeClassNames = 'cursor-pointer'
switch (type) { switch (type) {
case 'primary': case 'primary':
style = (disabled || loading) ? 'btn-primary-disabled' : 'btn-primary' typeClassNames = (disabled || loading) ? 'btn-primary-disabled' : 'btn-primary'
break break
case 'warning': case 'warning':
style = (disabled || loading) ? 'btn-warning-disabled' : 'btn-warning' typeClassNames = (disabled || loading) ? 'btn-warning-disabled' : 'btn-warning'
break break
default: default:
style = disabled ? 'btn-default-disabled' : 'btn-default' typeClassNames = disabled ? 'btn-default-disabled' : 'btn-default'
break break
} }
return ( return (
<div <div
className={`btn ${style} ${className && className}`} className={classNames('btn', typeClassNames, className)}
tabIndex={tabIndex} tabIndex={tabIndex}
onClick={disabled ? undefined : onClick} onClick={disabled ? undefined : onClick}
> >
@ -47,4 +47,4 @@ const Button: FC<IButtonProps> = ({
) )
} }
export default React.memo(Button) export default memo(Button)

View File

@ -65,7 +65,7 @@ const WorkflowProcessItem = ({
return ( return (
<div <div
className={cn( className={cn(
'mb-2 rounded-xl border-[0.5px] border-black/[0.08]', 'mb-2 rounded-xl border-[0.5px] border-black/8',
collapse ? 'py-[7px]' : hideInfo ? 'pt-2 pb-1' : 'py-2', collapse ? 'py-[7px]' : hideInfo ? 'pt-2 pb-1' : 'py-2',
collapse && (!grayBg ? 'bg-white' : 'bg-gray-50'), collapse && (!grayBg ? 'bg-white' : 'bg-gray-50'),
hideInfo ? 'mx-[-8px] px-1' : 'w-full px-3', hideInfo ? 'mx-[-8px] px-1' : 'w-full px-3',

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3V20M12 20H6.99999M12 20H17M2.99999 6H7.52785C7.83834 6 8.14457 5.92771 8.42228 5.78885L9.5777 5.21115C9.85541 5.07229 10.1616 5 10.4721 5H13.5279C13.8384 5 14.1446 5.07229 14.4223 5.21115L15.5777 5.78885C15.8554 5.92771 16.1616 6 16.4721 6H21M5.49999 6L3.02043 13.4387C2.71807 14.3458 3.08918 15.3834 4.0053 15.657C5.0117 15.9577 5.98828 15.9577 6.99468 15.657C7.9108 15.3834 8.28191 14.3457 7.97955 13.4387L5.49999 6ZM18.5 6L16.0204 13.4387C15.7181 14.3458 16.0892 15.3834 17.0053 15.657C18.0117 15.9577 18.9883 15.9577 19.9947 15.657C20.9108 15.3834 21.2819 14.3457 20.9796 13.4387L18.5 6Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@ -0,0 +1,29 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M12 3V20M12 20H6.99999M12 20H17M2.99999 6H7.52785C7.83834 6 8.14457 5.92771 8.42228 5.78885L9.5777 5.21115C9.85541 5.07229 10.1616 5 10.4721 5H13.5279C13.8384 5 14.1446 5.07229 14.4223 5.21115L15.5777 5.78885C15.8554 5.92771 16.1616 6 16.4721 6H21M5.49999 6L3.02043 13.4387C2.71807 14.3458 3.08918 15.3834 4.0053 15.657C5.0117 15.9577 5.98828 15.9577 6.99468 15.657C7.9108 15.3834 8.28191 14.3457 7.97955 13.4387L5.49999 6ZM18.5 6L16.0204 13.4387C15.7181 14.3458 16.0892 15.3834 17.0053 15.657C18.0117 15.9577 18.9883 15.9577 19.9947 15.657C20.9108 15.3834 21.2819 14.3457 20.9796 13.4387L18.5 6Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "Balance"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Balance.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Balance'
export default Icon

View File

@ -1,3 +1,4 @@
export { default as Balance } from './Balance'
export { default as CoinsStacked01 } from './CoinsStacked01' export { default as CoinsStacked01 } from './CoinsStacked01'
export { default as GoldCoin } from './GoldCoin' export { default as GoldCoin } from './GoldCoin'
export { default as ReceiptList } from './ReceiptList' export { default as ReceiptList } from './ReceiptList'

View File

@ -77,8 +77,7 @@ const ImageList: FC<ImageListProps> = ({
<div <div
className={` className={`
absolute inset-0 flex items-center justify-center rounded-lg z-[1] border absolute inset-0 flex items-center justify-center rounded-lg z-[1] border
${ ${item.progress === -1
item.progress === -1
? 'bg-[#FEF0C7] border-[#DC6803]' ? 'bg-[#FEF0C7] border-[#DC6803]'
: 'bg-black/[0.16] border-transparent' : 'bg-black/[0.16] border-transparent'
} }
@ -120,7 +119,7 @@ const ImageList: FC<ImageListProps> = ({
type="button" type="button"
className={cn( className={cn(
'absolute z-10 -top-[9px] -right-[9px] items-center justify-center w-[18px] h-[18px]', 'absolute z-10 -top-[9px] -right-[9px] items-center justify-center w-[18px] h-[18px]',
'bg-white hover:bg-gray-50 border-[0.5px] border-black/[0.02] rounded-2xl shadow-lg', 'bg-white hover:bg-gray-50 border-[0.5px] border-black/2 rounded-2xl shadow-lg',
item.progress === -1 ? 'flex' : 'hidden group-hover:flex', item.progress === -1 ? 'flex' : 'hidden group-hover:flex',
)} )}
onClick={() => onRemove && onRemove(item._id)} onClick={() => onRemove && onRemove(item._id)}

View File

@ -18,7 +18,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({
className='max-w-full max-h-full' className='max-w-full max-h-full'
/> />
<div <div
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/[0.08] rounded-lg backdrop-blur-[2px] cursor-pointer' className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/8 rounded-lg backdrop-blur-[2px] cursor-pointer'
onClick={onCancel} onClick={onCancel}
> >
<XClose className='w-4 h-4 text-white' /> <XClose className='w-4 h-4 text-white' />

View File

@ -0,0 +1,7 @@
.modal-dialog {
@apply relative z-10;
}
.modal-panel {
@apply w-full max-w-md transform rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all;
}

View File

@ -1,16 +1,17 @@
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { Fragment } from 'react' import { Fragment } from 'react'
import { XMarkIcon } from '@heroicons/react/24/outline' import { XMarkIcon } from '@heroicons/react/24/outline'
import classNames from 'classnames'
// https://headlessui.com/react/dialog // https://headlessui.com/react/dialog
type IModal = { type IModal = {
className?: string className?: string
wrapperClassName?: string wrapperClassName?: string
isShow: boolean isShow: boolean
onClose: () => void onClose?: () => void
title?: React.ReactNode title?: React.ReactNode
description?: React.ReactNode description?: React.ReactNode
children: React.ReactNode children?: React.ReactNode
closable?: boolean closable?: boolean
overflowVisible?: boolean overflowVisible?: boolean
} }
@ -19,7 +20,7 @@ export default function Modal({
className, className,
wrapperClassName, wrapperClassName,
isShow, isShow,
onClose, onClose = () => { },
title, title,
description, description,
children, children,
@ -28,7 +29,7 @@ export default function Modal({
}: IModal) { }: IModal) {
return ( return (
<Transition appear show={isShow} as={Fragment}> <Transition appear show={isShow} as={Fragment}>
<Dialog as="div" className={`relative z-30 ${wrapperClassName}`} onClose={onClose}> <Dialog as="div" className={classNames('modal-dialog', wrapperClassName)} onClose={onClose}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
@ -58,7 +59,11 @@ export default function Modal({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className={`w-full max-w-md transform ${overflowVisible ? 'overflow-visible' : 'overflow-hidden'} rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all ${className}`}> <Dialog.Panel className={classNames(
'modal-panel',
overflowVisible ? 'overflow-visible' : 'overflow-hidden',
className,
)}>
{title && <Dialog.Title {title && <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-900" className="text-lg font-medium leading-6 text-gray-900"

View File

@ -0,0 +1,4 @@
.simplePieChart {
border-radius: 50%;
box-shadow: 0 0 5px -3px rgb(from var(--simple-pie-chart-color) r g b / 0.1), 0.5px 0.5px 3px 0 rgb(from var(--simple-pie-chart-color) r g b / 0.3);
}

View File

@ -0,0 +1,66 @@
import type { CSSProperties } from 'react'
import { memo, useMemo } from 'react'
import ReactECharts from 'echarts-for-react'
import type { EChartsOption } from 'echarts'
import classNames from 'classnames'
import style from './index.module.css'
export type SimplePieChartProps = {
percentage?: number
fill?: string
stroke?: string
size?: number
className?: string
}
const SimplePieChart = ({ percentage = 80, fill = '#fdb022', stroke = '#f79009', size = 12, className }: SimplePieChartProps) => {
const option: EChartsOption = useMemo(() => ({
series: [
{
type: 'pie',
radius: ['83%', '100%'],
animation: false,
data: [
{ value: 100, itemStyle: { color: stroke } },
],
emphasis: {
disabled: true,
},
labelLine: {
show: false,
},
cursor: 'default',
},
{
type: 'pie',
radius: '83%',
animationDuration: 600,
data: [
{ value: percentage, itemStyle: { color: fill } },
{ value: 100 - percentage, itemStyle: { color: '#fff' } },
],
emphasis: {
disabled: true,
},
labelLine: {
show: false,
},
cursor: 'default',
},
],
}), [stroke, fill, percentage])
return (
<ReactECharts
option={option}
className={classNames(style.simplePieChart, className)}
style={{
'--simple-pie-chart-color': fill,
'width': size,
'height': size,
} as CSSProperties}
/>
)
}
export default memo(SimplePieChart)

View File

@ -4,13 +4,14 @@ import classNames from 'classnames'
import { Switch as OriginalSwitch } from '@headlessui/react' import { Switch as OriginalSwitch } from '@headlessui/react'
type SwitchProps = { type SwitchProps = {
onChange: (value: boolean) => void onChange?: (value: boolean) => void
size?: 'sm' | 'md' | 'lg' | 'l' size?: 'sm' | 'md' | 'lg' | 'l'
defaultValue?: boolean defaultValue?: boolean
disabled?: boolean disabled?: boolean
className?: string
} }
const Switch = ({ onChange, size = 'lg', defaultValue = false, disabled = false }: SwitchProps) => { const Switch = ({ onChange, size = 'lg', defaultValue = false, disabled = false, className }: SwitchProps) => {
const [enabled, setEnabled] = useState(defaultValue) const [enabled, setEnabled] = useState(defaultValue)
useEffect(() => { useEffect(() => {
setEnabled(defaultValue) setEnabled(defaultValue)
@ -42,13 +43,14 @@ const Switch = ({ onChange, size = 'lg', defaultValue = false, disabled = false
if (disabled) if (disabled)
return return
setEnabled(checked) setEnabled(checked)
onChange(checked) onChange?.(checked)
}} }}
className={classNames( className={classNames(
wrapStyle[size], wrapStyle[size],
enabled ? 'bg-blue-600' : 'bg-gray-200', enabled ? 'bg-blue-600' : 'bg-gray-200',
'relative inline-flex flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out', 'relative inline-flex flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out',
disabled ? '!opacity-50 !cursor-not-allowed' : '', disabled ? '!opacity-50 !cursor-not-allowed' : '',
className,
)} )}
> >
<span <span

View File

@ -65,6 +65,7 @@ export type CurrentPlanInfoBackend = {
} }
docs_processing: DocumentProcessingPriority docs_processing: DocumentProcessingPriority
can_replace_logo: boolean can_replace_logo: boolean
model_load_balancing_enabled: boolean
} }
export type SubscriptionItem = { export type SubscriptionItem = {

View File

@ -14,7 +14,7 @@ const CustomAppHeaderBrand = () => {
return ( return (
<div className='py-3'> <div className='py-3'>
<div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.app.title')}</div> <div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.app.title')}</div>
<div className='relative mb-4 rounded-xl bg-gray-100 border-[0.5px] border-black/[0.08] shadow-xs'> <div className='relative mb-4 rounded-xl bg-gray-100 border-[0.5px] border-black/8 shadow-xs'>
<div className={`${s.mask} absolute inset-0 rounded-xl`}></div> <div className={`${s.mask} absolute inset-0 rounded-xl`}></div>
<div className='flex items-center pl-5 h-14 rounded-t-xl'> <div className='flex items-center pl-5 h-14 rounded-t-xl'>
<div className='relative flex items-center mr-[199px] w-[120px] h-10 bg-[rgba(217,45,32,0.12)]'> <div className='relative flex items-center mr-[199px] w-[120px] h-10 bg-[rgba(217,45,32,0.12)]'>
@ -43,7 +43,7 @@ const CustomAppHeaderBrand = () => {
<div className='flex items-center mb-2'> <div className='flex items-center mb-2'>
<Button <Button
className={` className={`
!h-8 !px-3 bg-white !text-[13px] !h-8 !px-3 bg-white !text-[13px]
${plan.type === Plan.sandbox ? 'opacity-40' : ''} ${plan.type === Plan.sandbox ? 'opacity-40' : ''}
`} `}
disabled={plan.type === Plan.sandbox} disabled={plan.type === Plan.sandbox}
@ -54,7 +54,7 @@ const CustomAppHeaderBrand = () => {
<div className='mx-2 h-5 w-[1px] bg-black/5'></div> <div className='mx-2 h-5 w-[1px] bg-black/5'></div>
<Button <Button
className={` className={`
!h-8 !px-3 bg-white !text-[13px] !h-8 !px-3 bg-white !text-[13px]
${plan.type === Plan.sandbox ? 'opacity-40' : ''} ${plan.type === Plan.sandbox ? 'opacity-40' : ''}
`} `}
disabled={plan.type === Plan.sandbox} disabled={plan.type === Plan.sandbox}

View File

@ -106,7 +106,7 @@ const CustomWebAppBrand = () => {
return ( return (
<div className='py-4'> <div className='py-4'>
<div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.webapp.title')}</div> <div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.webapp.title')}</div>
<div className='relative mb-4 pl-4 pb-6 pr-[119px] rounded-xl border-[0.5px] border-black/[0.08] shadow-xs bg-gray-50 overflow-hidden'> <div className='relative mb-4 pl-4 pb-6 pr-[119px] rounded-xl border-[0.5px] border-black/8 shadow-xs bg-gray-50 overflow-hidden'>
<div className={`${s.mask} absolute top-0 left-0 w-full -bottom-2 z-10`}></div> <div className={`${s.mask} absolute top-0 left-0 w-full -bottom-2 z-10`}></div>
<div className='flex items-center -mt-2 mb-4 p-6 bg-white rounded-xl'> <div className='flex items-center -mt-2 mb-4 p-6 bg-white rounded-xl'>
<div className='flex items-center px-4 w-[125px] h-9 rounded-lg bg-primary-600 border-[0.5px] border-primary-700 shadow-xs'> <div className='flex items-center px-4 w-[125px] h-9 rounded-lg bg-primary-600 border-[0.5px] border-primary-700 shadow-xs'>
@ -152,7 +152,7 @@ const CustomWebAppBrand = () => {
!uploading && ( !uploading && (
<Button <Button
className={` className={`
relative mr-2 !h-8 !px-3 bg-white !text-[13px] relative mr-2 !h-8 !px-3 bg-white !text-[13px]
${uploadDisabled ? 'opacity-40' : ''} ${uploadDisabled ? 'opacity-40' : ''}
`} `}
disabled={uploadDisabled} disabled={uploadDisabled}
@ -212,7 +212,7 @@ const CustomWebAppBrand = () => {
<div className='mr-2 h-5 w-[1px] bg-black/5'></div> <div className='mr-2 h-5 w-[1px] bg-black/5'></div>
<Button <Button
className={` className={`
!h-8 !px-3 bg-white !text-[13px] !h-8 !px-3 bg-white !text-[13px]
${(uploadDisabled || (!webappLogo && !webappBrandRemoved)) ? 'opacity-40' : ''} ${(uploadDisabled || (!webappLogo && !webappBrandRemoved)) ? 'opacity-40' : ''}
`} `}
disabled={uploadDisabled || (!webappLogo && !webappBrandRemoved)} disabled={uploadDisabled || (!webappLogo && !webappBrandRemoved)}

View File

@ -123,7 +123,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
onStepChange={nextStep} onStepChange={nextStep}
/>} />}
{(step === 2 && (!datasetId || (datasetId && !!detail))) && <StepTwo {(step === 2 && (!datasetId || (datasetId && !!detail))) && <StepTwo
hasSetAPIKEY={!!embeddingsDefaultModel} isAPIKeySet={!!embeddingsDefaultModel}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })} onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
indexingType={detail?.indexing_technique} indexingType={detail?.indexing_technique}
datasetId={datasetId} datasetId={datasetId}

View File

@ -49,7 +49,7 @@ type ValueOf<T> = T[keyof T]
type StepTwoProps = { type StepTwoProps = {
isSetting?: boolean isSetting?: boolean
documentDetail?: FullDocumentDetail documentDetail?: FullDocumentDetail
hasSetAPIKEY: boolean isAPIKeySet: boolean
onSetting: () => void onSetting: () => void
datasetId?: string datasetId?: string
indexingType?: ValueOf<IndexingType> indexingType?: ValueOf<IndexingType>
@ -75,7 +75,7 @@ enum IndexingType {
const StepTwo = ({ const StepTwo = ({
isSetting, isSetting,
documentDetail, documentDetail,
hasSetAPIKEY, isAPIKeySet,
onSetting, onSetting,
datasetId, datasetId,
indexingType, indexingType,
@ -107,7 +107,7 @@ const StepTwo = ({
const hasSetIndexType = !!indexingType const hasSetIndexType = !!indexingType
const [indexType, setIndexType] = useState<ValueOf<IndexingType>>( const [indexType, setIndexType] = useState<ValueOf<IndexingType>>(
(indexingType (indexingType
|| hasSetAPIKEY) || isAPIKeySet)
? IndexingType.QUALIFIED ? IndexingType.QUALIFIED
: IndexingType.ECONOMICAL, : IndexingType.ECONOMICAL,
) )
@ -480,8 +480,8 @@ const StepTwo = ({
setIndexType(indexingType as IndexingType) setIndexType(indexingType as IndexingType)
else else
setIndexType(hasSetAPIKEY ? IndexingType.QUALIFIED : IndexingType.ECONOMICAL) setIndexType(isAPIKeySet ? IndexingType.QUALIFIED : IndexingType.ECONOMICAL)
}, [hasSetAPIKEY, indexingType, datasetId]) }, [isAPIKeySet, indexingType, datasetId])
useEffect(() => { useEffect(() => {
if (segmentationType === SegmentType.AUTO) { if (segmentationType === SegmentType.AUTO) {
@ -636,13 +636,13 @@ const StepTwo = ({
className={cn( className={cn(
s.radioItem, s.radioItem,
s.indexItem, s.indexItem,
!hasSetAPIKEY && s.disabled, !isAPIKeySet && s.disabled,
!hasSetIndexType && indexType === IndexingType.QUALIFIED && s.active, !hasSetIndexType && indexType === IndexingType.QUALIFIED && s.active,
hasSetIndexType && s.disabled, hasSetIndexType && s.disabled,
hasSetIndexType && '!w-full', hasSetIndexType && '!w-full',
)} )}
onClick={() => { onClick={() => {
if (hasSetAPIKEY) if (isAPIKeySet)
setIndexType(IndexingType.QUALIFIED) setIndexType(IndexingType.QUALIFIED)
}} }}
> >
@ -665,7 +665,7 @@ const StepTwo = ({
) )
} }
</div> </div>
{!hasSetAPIKEY && ( {!isAPIKeySet && (
<div className={s.warningTip}> <div className={s.warningTip}>
<span>{t('datasetCreation.stepTwo.warning')}&nbsp;</span> <span>{t('datasetCreation.stepTwo.warning')}&nbsp;</span>
<span className={s.click} onClick={onSetting}>{t('datasetCreation.stepTwo.click')}</span> <span className={s.click} onClick={onSetting}>{t('datasetCreation.stepTwo.click')}</span>

View File

@ -68,7 +68,7 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
{!documentDetail && <Loading type='app' />} {!documentDetail && <Loading type='app' />}
{dataset && documentDetail && ( {dataset && documentDetail && (
<StepTwo <StepTwo
hasSetAPIKEY={!!embeddingsDefaultModel} isAPIKeySet={!!embeddingsDefaultModel}
onSetting={showSetAPIKey} onSetting={showSetAPIKey}
datasetId={datasetId} datasetId={datasetId}
dataSourceType={documentDetail.data_source_type} dataSourceType={documentDetail.data_source_type}

View File

@ -39,7 +39,7 @@ export const MODEL_TYPE_TEXT = {
[ModelTypeEnum.tts]: 'TTS', [ModelTypeEnum.tts]: 'TTS',
} }
export enum ConfigurateMethodEnum { export enum ConfigurationMethodEnum {
predefinedModel = 'predefined-model', predefinedModel = 'predefined-model',
customizableModel = 'customizable-model', customizableModel = 'customizable-model',
fetchFromRemote = 'fetch-from-remote', fetchFromRemote = 'fetch-from-remote',
@ -64,6 +64,7 @@ export enum ModelStatusEnum {
noConfigure = 'no-configure', noConfigure = 'no-configure',
quotaExceeded = 'quota-exceeded', quotaExceeded = 'quota-exceeded',
noPermission = 'no-permission', noPermission = 'no-permission',
disabled = 'disabled',
} }
export const MODEL_STATUS_TEXT: { [k: string]: TypeWithI18N } = { export const MODEL_STATUS_TEXT: { [k: string]: TypeWithI18N } = {
@ -114,9 +115,10 @@ export type ModelItem = {
label: TypeWithI18N label: TypeWithI18N
model_type: ModelTypeEnum model_type: ModelTypeEnum
features?: ModelFeatureEnum[] features?: ModelFeatureEnum[]
fetch_from: ConfigurateMethodEnum fetch_from: ConfigurationMethodEnum
status: ModelStatusEnum status: ModelStatusEnum
model_properties: Record<string, string | number> model_properties: Record<string, string | number>
load_balancing_enabled: boolean
deprecated?: boolean deprecated?: boolean
} }
@ -158,7 +160,7 @@ export type ModelProvider = {
icon_large: TypeWithI18N icon_large: TypeWithI18N
background?: string background?: string
supported_model_types: ModelTypeEnum[] supported_model_types: ModelTypeEnum[]
configurate_methods: ConfigurateMethodEnum[] configurate_methods: ConfigurationMethodEnum[]
provider_credential_schema: { provider_credential_schema: {
credential_form_schemas: CredentialFormSchema[] credential_form_schemas: CredentialFormSchema[]
} }
@ -204,7 +206,7 @@ export type DefaultModel = {
model: string model: string
} }
export type CustomConfigrationModelFixedFields = { export type CustomConfigurationModelFixedFields = {
__model_name: string __model_name: string
__model_type: ModelTypeEnum __model_type: ModelTypeEnum
} }
@ -223,3 +225,23 @@ export type ModelParameterRule = {
options?: string[] options?: string[]
tagPlaceholder?: TypeWithI18N tagPlaceholder?: TypeWithI18N
} }
export type ModelLoadBalancingConfigEntry = {
/** model balancing config entry id */
id?: string
/** is config entry enabled */
enabled?: boolean
/** config entry name */
name: string
/** model balancing credential */
credentials: Record<string, string | undefined | boolean>
/** is config entry currently removed from Round-robin queue */
in_cooldown?: boolean
/** cooldown time (in seconds) */
ttl?: number
}
export type ModelLoadBalancingConfig = {
enabled: boolean
configs: ModelLoadBalancingConfigEntry[]
}

View File

@ -7,14 +7,14 @@ import {
import useSWR, { useSWRConfig } from 'swr' import useSWR, { useSWRConfig } from 'swr'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import type { import type {
CustomConfigrationModelFixedFields, CustomConfigurationModelFixedFields,
DefaultModel, DefaultModel,
DefaultModelResponse, DefaultModelResponse,
Model, Model,
ModelTypeEnum, ModelTypeEnum,
} from './declarations' } from './declarations'
import { import {
ConfigurateMethodEnum, ConfigurationMethodEnum,
ModelStatusEnum, ModelStatusEnum,
} from './declarations' } from './declarations'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
@ -61,42 +61,55 @@ export const useLanguage = () => {
return locale.replace('-', '_') return locale.replace('-', '_')
} }
export const useProviderCrenditialsFormSchemasValue = ( export const useProviderCredentialsAndLoadBalancing = (
provider: string, provider: string,
configurateMethod: ConfigurateMethodEnum, configurationMethod: ConfigurationMethodEnum,
configured?: boolean, configured?: boolean,
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
) => { ) => {
const { data: predefinedFormSchemasValue } = useSWR( const { data: predefinedFormSchemasValue, mutate: mutatePredefined } = useSWR(
(configurateMethod === ConfigurateMethodEnum.predefinedModel && configured) (configurationMethod === ConfigurationMethodEnum.predefinedModel && configured)
? `/workspaces/current/model-providers/${provider}/credentials` ? `/workspaces/current/model-providers/${provider}/credentials`
: null, : null,
fetchModelProviderCredentials, fetchModelProviderCredentials,
) )
const { data: customFormSchemasValue } = useSWR( const { data: customFormSchemasValue, mutate: mutateCustomized } = useSWR(
(configurateMethod === ConfigurateMethodEnum.customizableModel && currentCustomConfigrationModelFixedFields) (configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields)
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigrationModelFixedFields?.__model_name}&model_type=${currentCustomConfigrationModelFixedFields?.__model_type}` ? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}`
: null, : null,
fetchModelProviderCredentials, fetchModelProviderCredentials,
) )
const value = useMemo(() => { const credentials = useMemo(() => {
return configurateMethod === ConfigurateMethodEnum.predefinedModel return configurationMethod === ConfigurationMethodEnum.predefinedModel
? predefinedFormSchemasValue?.credentials ? predefinedFormSchemasValue?.credentials
: customFormSchemasValue?.credentials : customFormSchemasValue?.credentials
? { ? {
...customFormSchemasValue?.credentials, ...customFormSchemasValue?.credentials,
...currentCustomConfigrationModelFixedFields, ...currentCustomConfigurationModelFixedFields,
} }
: undefined : undefined
}, [ }, [
configurateMethod, configurationMethod,
currentCustomConfigrationModelFixedFields, currentCustomConfigurationModelFixedFields,
customFormSchemasValue?.credentials, customFormSchemasValue?.credentials,
predefinedFormSchemasValue?.credentials, predefinedFormSchemasValue?.credentials,
]) ])
return value const mutate = useMemo(() => () => {
mutatePredefined()
mutateCustomized()
}, [mutateCustomized, mutatePredefined])
return {
credentials,
loadBalancing: (configurationMethod === ConfigurationMethodEnum.predefinedModel
? predefinedFormSchemasValue
: customFormSchemasValue
)?.load_balancing,
mutate,
}
// as ([Record<string, string | boolean | undefined> | undefined, ModelLoadBalancingConfig | undefined])
} }
export const useModelList = (type: ModelTypeEnum) => { export const useModelList = (type: ModelTypeEnum) => {

View File

@ -4,11 +4,11 @@ import SystemModelSelector from './system-model-selector'
import ProviderAddedCard, { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card' import ProviderAddedCard, { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card'
import ProviderCard from './provider-card' import ProviderCard from './provider-card'
import type { import type {
CustomConfigrationModelFixedFields, CustomConfigurationModelFixedFields,
ModelProvider, ModelProvider,
} from './declarations' } from './declarations'
import { import {
ConfigurateMethodEnum, ConfigurationMethodEnum,
CustomConfigurationStatusEnum, CustomConfigurationStatusEnum,
ModelTypeEnum, ModelTypeEnum,
} from './declarations' } from './declarations'
@ -19,7 +19,7 @@ import {
} from './hooks' } from './hooks'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context' import { useModalContextSelector } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
const ModelProviderPage = () => { const ModelProviderPage = () => {
@ -33,7 +33,7 @@ const ModelProviderPage = () => {
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text) const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: ttsDefaultModel } = useDefaultModel(ModelTypeEnum.tts) const { data: ttsDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
const { modelProviders: providers } = useProviderContext() const { modelProviders: providers } = useProviderContext()
const { setShowModelModal } = useModalContext() const setShowModelModal = useModalContextSelector(state => state.setShowModelModal)
const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
const [configedProviders, notConfigedProviders] = useMemo(() => { const [configedProviders, notConfigedProviders] = useMemo(() => {
const configedProviders: ModelProvider[] = [] const configedProviders: ModelProvider[] = []
@ -57,32 +57,32 @@ const ModelProviderPage = () => {
const handleOpenModal = ( const handleOpenModal = (
provider: ModelProvider, provider: ModelProvider,
configurateMethod: ConfigurateMethodEnum, configurateMethod: ConfigurationMethodEnum,
customConfigrationModelFixedFields?: CustomConfigrationModelFixedFields, CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
) => { ) => {
setShowModelModal({ setShowModelModal({
payload: { payload: {
currentProvider: provider, currentProvider: provider,
currentConfigurateMethod: configurateMethod, currentConfigurationMethod: configurateMethod,
currentCustomConfigrationModelFixedFields: customConfigrationModelFixedFields, currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
}, },
onSaveCallback: () => { onSaveCallback: () => {
updateModelProviders() updateModelProviders()
if (configurateMethod === ConfigurateMethodEnum.predefinedModel) { if (configurateMethod === ConfigurationMethodEnum.predefinedModel) {
provider.supported_model_types.forEach((type) => { provider.supported_model_types.forEach((type) => {
updateModelList(type) updateModelList(type)
}) })
} }
if (configurateMethod === ConfigurateMethodEnum.customizableModel && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) { if (configurateMethod === ConfigurationMethodEnum.customizableModel && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
eventEmitter?.emit({ eventEmitter?.emit({
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST, type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
payload: provider.provider, payload: provider.provider,
} as any) } as any)
if (customConfigrationModelFixedFields?.__model_type) if (CustomConfigurationModelFixedFields?.__model_type)
updateModelList(customConfigrationModelFixedFields?.__model_type) updateModelList(CustomConfigurationModelFixedFields?.__model_type)
} }
}, },
}) })
@ -117,7 +117,7 @@ const ModelProviderPage = () => {
<ProviderAddedCard <ProviderAddedCard
key={provider.provider} key={provider.provider}
provider={provider} provider={provider}
onOpenModal={(configurateMethod: ConfigurateMethodEnum, currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => handleOpenModal(provider, configurateMethod, currentCustomConfigrationModelFixedFields)} onOpenModal={(configurateMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurateMethod, currentCustomConfigurationModelFixedFields)}
/> />
)) ))
} }
@ -137,7 +137,7 @@ const ModelProviderPage = () => {
<ProviderCard <ProviderCard
key={provider.provider} key={provider.provider}
provider={provider} provider={provider}
onOpenModal={(configurateMethod: ConfigurateMethodEnum) => handleOpenModal(provider, configurateMethod)} onOpenModal={(configurateMethod: ConfigurationMethodEnum) => handleOpenModal(provider, configurateMethod)}
/> />
)) ))
} }

View File

@ -1,3 +1,4 @@
import classNames from 'classnames'
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
type ModelBadgeProps = { type ModelBadgeProps = {
@ -9,11 +10,10 @@ const ModelBadge: FC<ModelBadgeProps> = ({
children, children,
}) => { }) => {
return ( return (
<div className={` <div className={classNames(
flex items-center px-1 h-[18px] rounded-[5px] border border-black/[0.08] bg-white/[0.48] 'flex items-center px-1 h-[18px] rounded-[5px] border border-black/8 bg-white/[0.48] text-[10px] font-medium text-gray-500 cursor-default',
text-[10px] font-medium text-gray-500 className,
${className} )}>
`}>
{children} {children}
</div> </div>
) )

View File

@ -11,12 +11,14 @@ import type {
CredentialFormSchema, CredentialFormSchema,
CredentialFormSchemaRadio, CredentialFormSchemaRadio,
CredentialFormSchemaSelect, CredentialFormSchemaSelect,
CustomConfigrationModelFixedFields, CustomConfigurationModelFixedFields,
FormValue, FormValue,
ModelLoadBalancingConfig,
ModelLoadBalancingConfigEntry,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { import {
ConfigurateMethodEnum, ConfigurationMethodEnum,
CustomConfigurationStatusEnum, CustomConfigurationStatusEnum,
FormTypeEnum, FormTypeEnum,
} from '../declarations' } from '../declarations'
@ -28,11 +30,12 @@ import {
} from '../utils' } from '../utils'
import { import {
useLanguage, useLanguage,
useProviderCrenditialsFormSchemasValue, useProviderCredentialsAndLoadBalancing,
} from '../hooks' } from '../hooks'
import ProviderIcon from '../provider-icon' import ProviderIcon from '../provider-icon'
import { useValidate } from '../../key-validator/hooks' import { useValidate } from '../../key-validator/hooks'
import { ValidatedStatus } from '../../key-validator/declarations' import { ValidatedStatus } from '../../key-validator/declarations'
import ModelLoadBalancingConfigs from '../provider-added-card/model-load-balancing-configs'
import Form from './Form' import Form from './Form'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
@ -47,8 +50,8 @@ import ConfirmCommon from '@/app/components/base/confirm/common'
type ModelModalProps = { type ModelModalProps = {
provider: ModelProvider provider: ModelProvider
configurateMethod: ConfigurateMethodEnum configurateMethod: ConfigurationMethodEnum
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
onCancel: () => void onCancel: () => void
onSave: () => void onSave: () => void
} }
@ -56,16 +59,20 @@ type ModelModalProps = {
const ModelModal: FC<ModelModalProps> = ({ const ModelModal: FC<ModelModalProps> = ({
provider, provider,
configurateMethod, configurateMethod,
currentCustomConfigrationModelFixedFields, currentCustomConfigurationModelFixedFields,
onCancel, onCancel,
onSave, onSave,
}) => { }) => {
const providerFormSchemaPredefined = configurateMethod === ConfigurateMethodEnum.predefinedModel const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
const formSchemasValue = useProviderCrenditialsFormSchemasValue( const {
credentials: formSchemasValue,
loadBalancing: originalConfig,
mutate,
} = useProviderCredentialsAndLoadBalancing(
provider.provider, provider.provider,
configurateMethod, configurateMethod,
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active, providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
currentCustomConfigrationModelFixedFields, currentCustomConfigurationModelFixedFields,
) )
const isEditMode = !!formSchemasValue const isEditMode = !!formSchemasValue
const { t } = useTranslation() const { t } = useTranslation()
@ -73,13 +80,29 @@ const ModelModal: FC<ModelModalProps> = ({
const language = useLanguage() const language = useLanguage()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [showConfirm, setShowConfirm] = useState(false) const [showConfirm, setShowConfirm] = useState(false)
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
const originalConfigMap = useMemo(() => {
if (!originalConfig)
return {}
return originalConfig?.configs.reduce((prev, config) => {
if (config.id)
prev[config.id] = config
return prev
}, {} as Record<string, ModelLoadBalancingConfigEntry>)
}, [originalConfig])
useEffect(() => {
if (originalConfig && !draftConfig)
setDraftConfig(originalConfig)
}, [draftConfig, originalConfig])
const formSchemas = useMemo(() => { const formSchemas = useMemo(() => {
return providerFormSchemaPredefined return providerFormSchemaPredefined
? provider.provider_credential_schema.credential_form_schemas ? provider.provider_credential_schema.credential_form_schemas
: [ : [
genModelTypeFormSchema(provider.supported_model_types), genModelTypeFormSchema(provider.supported_model_types),
genModelNameFormSchema(provider.model_credential_schema?.model), genModelNameFormSchema(provider.model_credential_schema?.model),
...provider.model_credential_schema.credential_form_schemas, ...(draftConfig?.enabled ? [] : provider.model_credential_schema.credential_form_schemas),
] ]
}, [ }, [
providerFormSchemaPredefined, providerFormSchemaPredefined,
@ -87,15 +110,14 @@ const ModelModal: FC<ModelModalProps> = ({
provider.supported_model_types, provider.supported_model_types,
provider.model_credential_schema?.credential_form_schemas, provider.model_credential_schema?.credential_form_schemas,
provider.model_credential_schema?.model, provider.model_credential_schema?.model,
draftConfig?.enabled,
]) ])
const [ const [
requiredFormSchemas, requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue, defaultFormSchemaValue,
showOnVariableMap, showOnVariableMap,
] = useMemo(() => { ] = useMemo(() => {
const requiredFormSchemas: CredentialFormSchema[] = [] const requiredFormSchemas: CredentialFormSchema[] = []
const secretFormSchemas: CredentialFormSchema[] = []
const defaultFormSchemaValue: Record<string, string | number> = {} const defaultFormSchemaValue: Record<string, string | number> = {}
const showOnVariableMap: Record<string, string[]> = {} const showOnVariableMap: Record<string, string[]> = {}
@ -103,9 +125,6 @@ const ModelModal: FC<ModelModalProps> = ({
if (formSchema.required) if (formSchema.required)
requiredFormSchemas.push(formSchema) requiredFormSchemas.push(formSchema)
if (formSchema.type === FormTypeEnum.secretInput)
secretFormSchemas.push(formSchema)
if (formSchema.default) if (formSchema.default)
defaultFormSchemaValue[formSchema.variable] = formSchema.default defaultFormSchemaValue[formSchema.variable] = formSchema.default
@ -136,22 +155,21 @@ const ModelModal: FC<ModelModalProps> = ({
return [ return [
requiredFormSchemas, requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue, defaultFormSchemaValue,
showOnVariableMap, showOnVariableMap,
] ]
}, [formSchemas]) }, [formSchemas])
const initialFormSchemasValue = useMemo(() => { const initialFormSchemasValue: Record<string, string | number> = useMemo(() => {
return { return {
...defaultFormSchemaValue, ...defaultFormSchemaValue,
...formSchemasValue, ...formSchemasValue,
} } as unknown as Record<string, string | number>
}, [formSchemasValue, defaultFormSchemaValue]) }, [formSchemasValue, defaultFormSchemaValue])
const [value, setValue] = useState(initialFormSchemasValue) const [value, setValue] = useState(initialFormSchemasValue)
useEffect(() => { useEffect(() => {
setValue(initialFormSchemasValue) setValue(initialFormSchemasValue)
}, [initialFormSchemasValue]) }, [initialFormSchemasValue])
const [validate, validating, validatedStatusState] = useValidate(value) const [_, validating, validatedStatusState] = useValidate(value)
const filteredRequiredFormSchemas = requiredFormSchemas.filter((requiredFormSchema) => { const filteredRequiredFormSchemas = requiredFormSchemas.filter((requiredFormSchema) => {
if (requiredFormSchema.show_on.length && requiredFormSchema.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)) if (requiredFormSchema.show_on.length && requiredFormSchema.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
return true return true
@ -161,32 +179,63 @@ const ModelModal: FC<ModelModalProps> = ({
return false return false
}) })
const getSecretValues = useCallback((v: FormValue) => {
return secretFormSchemas.reduce((prev, next) => {
if (v[next.variable] === initialFormSchemasValue[next.variable])
prev[next.variable] = '[__HIDDEN__]'
return prev
}, {} as Record<string, string>)
}, [initialFormSchemasValue, secretFormSchemas])
const handleValueChange = (v: FormValue) => { const handleValueChange = (v: FormValue) => {
setValue(v) setValue(v)
} }
const extendedSecretFormSchemas = useMemo(
() =>
(providerFormSchemaPredefined
? provider.provider_credential_schema.credential_form_schemas
: [
genModelTypeFormSchema(provider.supported_model_types),
genModelNameFormSchema(provider.model_credential_schema?.model),
...provider.model_credential_schema.credential_form_schemas,
]).filter(({ type }) => type === FormTypeEnum.secretInput),
[
provider.model_credential_schema?.credential_form_schemas,
provider.model_credential_schema?.model,
provider.provider_credential_schema?.credential_form_schemas,
provider.supported_model_types,
providerFormSchemaPredefined,
],
)
const encodeSecretValues = useCallback((v: FormValue) => {
const result = { ...v }
extendedSecretFormSchemas.forEach(({ variable }) => {
if (result[variable] === formSchemasValue?.[variable])
result[variable] = '[__HIDDEN__]'
})
return result
}, [extendedSecretFormSchemas, formSchemasValue])
const encodeConfigEntrySecretValues = useCallback((entry: ModelLoadBalancingConfigEntry) => {
const result = { ...entry }
extendedSecretFormSchemas.forEach(({ variable }) => {
if (entry.id && result.credentials[variable] === originalConfigMap[entry.id]?.credentials?.[variable])
result.credentials[variable] = '[__HIDDEN__]'
})
return result
}, [extendedSecretFormSchemas, originalConfigMap])
const handleSave = async () => { const handleSave = async () => {
try { try {
setLoading(true) setLoading(true)
const res = await saveCredentials( const res = await saveCredentials(
providerFormSchemaPredefined, providerFormSchemaPredefined,
provider.provider, provider.provider,
encodeSecretValues(value),
{ {
...value, ...draftConfig,
...getSecretValues(value), enabled: Boolean(draftConfig?.enabled),
configs: draftConfig?.configs.map(encodeConfigEntrySecretValues) || [],
}, },
) )
if (res.result === 'success') { if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate()
onSave() onSave()
onCancel() onCancel()
} }
@ -207,6 +256,7 @@ const ModelModal: FC<ModelModalProps> = ({
) )
if (res.result === 'success') { if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate()
onSave() onSave()
onCancel() onCancel()
} }
@ -217,7 +267,7 @@ const ModelModal: FC<ModelModalProps> = ({
} }
const renderTitlePrefix = () => { const renderTitlePrefix = () => {
const prefix = configurateMethod === ConfigurateMethodEnum.customizableModel ? t('common.operation.add') : t('common.operation.setup') const prefix = configurateMethod === ConfigurationMethodEnum.customizableModel ? t('common.operation.add') : t('common.operation.setup')
return `${prefix} ${provider.label[language] || provider.label.en_US}` return `${prefix} ${provider.label[language] || provider.label.en_US}`
} }
@ -232,6 +282,7 @@ const ModelModal: FC<ModelModalProps> = ({
<div className='text-xl font-semibold text-gray-900'>{renderTitlePrefix()}</div> <div className='text-xl font-semibold text-gray-900'>{renderTitlePrefix()}</div>
<ProviderIcon provider={provider} /> <ProviderIcon provider={provider} />
</div> </div>
<Form <Form
value={value} value={value}
onChange={handleValueChange} onChange={handleValueChange}
@ -241,7 +292,17 @@ const ModelModal: FC<ModelModalProps> = ({
showOnVariableMap={showOnVariableMap} showOnVariableMap={showOnVariableMap}
isEditMode={isEditMode} isEditMode={isEditMode}
/> />
<div className='sticky bottom-0 flex justify-between items-center py-6 flex-wrap gap-y-2 bg-white'>
<div className='mt-1 mb-4 border-t-[0.5px] border-t-gray-100' />
<ModelLoadBalancingConfigs withSwitch {...{
draftConfig,
setDraftConfig,
provider,
currentCustomConfigurationModelFixedFields,
configurationMethod: configurateMethod,
}} />
<div className='sticky bottom-0 flex justify-between items-center mt-2 -mx-2 pt-4 px-2 pb-6 flex-wrap gap-y-2 bg-white z-10'>
{ {
(provider.help && (provider.help.title || provider.help.url)) (provider.help && (provider.help.title || provider.help.url))
? ( ? (
@ -278,7 +339,11 @@ const ModelModal: FC<ModelModalProps> = ({
className='h-9 text-sm font-medium' className='h-9 text-sm font-medium'
type='primary' type='primary'
onClick={handleSave} onClick={handleSave}
disabled={loading || filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)} disabled={
loading
|| filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
}
> >
{t('common.operation.save')} {t('common.operation.save')}
</Button> </Button>

View File

@ -0,0 +1,344 @@
import type { FC } from 'react'
import {
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import type {
CredentialFormSchema,
CredentialFormSchemaRadio,
CredentialFormSchemaSelect,
CredentialFormSchemaTextInput,
CustomConfigurationModelFixedFields,
FormValue,
ModelLoadBalancingConfigEntry,
ModelProvider,
} from '../declarations'
import {
ConfigurationMethodEnum,
FormTypeEnum,
} from '../declarations'
import {
useLanguage,
} from '../hooks'
import { useValidate } from '../../key-validator/hooks'
import { ValidatedStatus } from '../../key-validator/declarations'
import { validateLoadBalancingCredentials } from '../utils'
import Form from './Form'
import Button from '@/app/components/base/button'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import { useToastContext } from '@/app/components/base/toast'
import ConfirmCommon from '@/app/components/base/confirm/common'
type ModelModalProps = {
provider: ModelProvider
configurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
entry?: ModelLoadBalancingConfigEntry
onCancel: () => void
onSave: (entry: ModelLoadBalancingConfigEntry) => void
onRemove: () => void
}
const ModelLoadBalancingEntryModal: FC<ModelModalProps> = ({
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
entry,
onCancel,
onSave,
onRemove,
}) => {
const providerFormSchemaPredefined = configurationMethod === ConfigurationMethodEnum.predefinedModel
// const { credentials: formSchemasValue } = useProviderCredentialsAndLoadBalancing(
// provider.provider,
// configurationMethod,
// providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
// currentCustomConfigurationModelFixedFields,
// )
const isEditMode = !!entry
const { t } = useTranslation()
const { notify } = useToastContext()
const language = useLanguage()
const [loading, setLoading] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const formSchemas = useMemo(() => {
return [
{
type: FormTypeEnum.textInput,
label: {
en_US: 'Config Name',
zh_Hans: '配置名称',
},
variable: 'name',
required: true,
show_on: [],
placeholder: {
en_US: 'Enter your Config Name here',
zh_Hans: '输入配置名称',
},
} as CredentialFormSchemaTextInput,
...(
providerFormSchemaPredefined
? provider.provider_credential_schema.credential_form_schemas
: provider.model_credential_schema.credential_form_schemas
),
]
}, [
providerFormSchemaPredefined,
provider.provider_credential_schema?.credential_form_schemas,
provider.model_credential_schema?.credential_form_schemas,
])
const [
requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue,
showOnVariableMap,
] = useMemo(() => {
const requiredFormSchemas: CredentialFormSchema[] = []
const secretFormSchemas: CredentialFormSchema[] = []
const defaultFormSchemaValue: Record<string, string | number> = {}
const showOnVariableMap: Record<string, string[]> = {}
formSchemas.forEach((formSchema) => {
if (formSchema.required)
requiredFormSchemas.push(formSchema)
if (formSchema.type === FormTypeEnum.secretInput)
secretFormSchemas.push(formSchema)
if (formSchema.default)
defaultFormSchemaValue[formSchema.variable] = formSchema.default
if (formSchema.show_on.length) {
formSchema.show_on.forEach((showOnItem) => {
if (!showOnVariableMap[showOnItem.variable])
showOnVariableMap[showOnItem.variable] = []
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
})
}
if (formSchema.type === FormTypeEnum.select || formSchema.type === FormTypeEnum.radio) {
(formSchema as (CredentialFormSchemaRadio | CredentialFormSchemaSelect)).options.forEach((option) => {
if (option.show_on.length) {
option.show_on.forEach((showOnItem) => {
if (!showOnVariableMap[showOnItem.variable])
showOnVariableMap[showOnItem.variable] = []
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
})
}
})
}
})
return [
requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue,
showOnVariableMap,
]
}, [formSchemas])
const [initialValue, setInitialValue] = useState<ModelLoadBalancingConfigEntry['credentials']>()
useEffect(() => {
if (entry && !initialValue) {
setInitialValue({
...defaultFormSchemaValue,
...entry.credentials,
id: entry.id,
name: entry.name,
} as Record<string, string | undefined | boolean>)
}
}, [entry, defaultFormSchemaValue, initialValue])
const formSchemasValue = useMemo(() => ({
...currentCustomConfigurationModelFixedFields,
...initialValue,
}), [currentCustomConfigurationModelFixedFields, initialValue])
const initialFormSchemasValue: Record<string, string | number> = useMemo(() => {
return {
...defaultFormSchemaValue,
...formSchemasValue,
} as Record<string, string | number>
}, [formSchemasValue, defaultFormSchemaValue])
const [value, setValue] = useState(initialFormSchemasValue)
useEffect(() => {
setValue(initialFormSchemasValue)
}, [initialFormSchemasValue])
const [_, validating, validatedStatusState] = useValidate(value)
const filteredRequiredFormSchemas = requiredFormSchemas.filter((requiredFormSchema) => {
if (requiredFormSchema.show_on.length && requiredFormSchema.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
return true
if (!requiredFormSchema.show_on.length)
return true
return false
})
const getSecretValues = useCallback((v: FormValue) => {
return secretFormSchemas.reduce((prev, next) => {
if (v[next.variable] === initialFormSchemasValue[next.variable])
prev[next.variable] = '[__HIDDEN__]'
return prev
}, {} as Record<string, string>)
}, [initialFormSchemasValue, secretFormSchemas])
// const handleValueChange = ({ __model_type, __model_name, ...v }: FormValue) => {
const handleValueChange = (v: FormValue) => {
setValue(v)
}
const handleSave = async () => {
try {
setLoading(true)
const res = await validateLoadBalancingCredentials(
providerFormSchemaPredefined,
provider.provider,
{
...value,
...getSecretValues(value),
},
)
if (res.status === ValidatedStatus.Success) {
// notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
const { __model_type, __model_name, name, ...credentials } = value
onSave({
...(entry || {}),
name: name as string,
credentials: credentials as Record<string, string | boolean | undefined>,
})
// onCancel()
}
else {
notify({ type: 'error', message: res.message || '' })
}
}
finally {
setLoading(false)
}
}
const handleRemove = () => {
onRemove?.()
}
return (
<PortalToFollowElem open>
<PortalToFollowElemContent className='w-full h-full z-[60]'>
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
<div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-2'>
<div className='text-xl font-semibold text-gray-900'>{t(isEditMode ? 'common.modelProvider.editConfig' : 'common.modelProvider.addConfig')}</div>
</div>
<Form
value={value}
onChange={handleValueChange}
formSchemas={formSchemas}
validating={validating}
validatedSuccess={validatedStatusState.status === ValidatedStatus.Success}
showOnVariableMap={showOnVariableMap}
isEditMode={isEditMode}
/>
<div className='sticky bottom-0 flex justify-between items-center py-6 flex-wrap gap-y-2 bg-white'>
{
(provider.help && (provider.help.title || provider.help.url))
? (
<a
href={provider.help?.url[language] || provider.help?.url.en_US}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-primary-600'
onClick={e => !provider.help.url && e.preventDefault()}
>
{provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US}
<LinkExternal02 className='ml-1 w-3 h-3' />
</a>
)
: <div />
}
<div>
{
isEditMode && (
<Button
className='mr-2 h-9 text-sm font-medium text-[#D92D20]'
onClick={() => setShowConfirm(true)}
>
{t('common.operation.remove')}
</Button>
)
}
<Button
className='mr-2 h-9 text-sm font-medium text-gray-700'
onClick={onCancel}
>
{t('common.operation.cancel')}
</Button>
<Button
className='h-9 text-sm font-medium'
type='primary'
onClick={handleSave}
disabled={loading || filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)}
>
{t('common.operation.save')}
</Button>
</div>
</div>
</div>
<div className='border-t-[0.5px] border-t-black/5'>
{
(validatedStatusState.status === ValidatedStatus.Error && validatedStatusState.message)
? (
<div className='flex px-[10px] py-3 bg-[#FEF3F2] text-xs text-[#D92D20]'>
<AlertCircle className='mt-[1px] mr-2 w-[14px] h-[14px]' />
{validatedStatusState.message}
</div>
)
: (
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
<Lock01 className='mr-1 w-3 h-3 text-gray-500' />
{t('common.modelProvider.encrypted.front')}
<a
className='text-primary-600 mx-1'
target='_blank' rel='noopener noreferrer'
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
>
PKCS1_OAEP
</a>
{t('common.modelProvider.encrypted.back')}
</div>
)
}
</div>
</div>
{
showConfirm && (
<ConfirmCommon
title={t('common.modelProvider.confirmDelete')}
isShow={showConfirm}
onCancel={() => setShowConfirm(false)}
onConfirm={handleRemove}
confirmWrapperClassName='z-[70]'
/>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(ModelLoadBalancingEntryModal)

View File

@ -1,4 +1,5 @@
import type { FC } from 'react' import type { FC, PropsWithChildren } from 'react'
import classNames from 'classnames'
import { import {
modelTypeFormat, modelTypeFormat,
sizeFormat, sizeFormat,
@ -8,7 +9,7 @@ import type { ModelItem } from '../declarations'
import ModelBadge from '../model-badge' import ModelBadge from '../model-badge'
import FeatureIcon from '../model-selector/feature-icon' import FeatureIcon from '../model-selector/feature-icon'
type ModelNameProps = { type ModelNameProps = PropsWithChildren<{
modelItem: ModelItem modelItem: ModelItem
className?: string className?: string
showModelType?: boolean showModelType?: boolean
@ -18,7 +19,7 @@ type ModelNameProps = {
showFeatures?: boolean showFeatures?: boolean
featuresClassName?: string featuresClassName?: string
showContextSize?: boolean showContextSize?: boolean
} }>
const ModelName: FC<ModelNameProps> = ({ const ModelName: FC<ModelNameProps> = ({
modelItem, modelItem,
className, className,
@ -29,6 +30,7 @@ const ModelName: FC<ModelNameProps> = ({
showFeatures, showFeatures,
featuresClassName, featuresClassName,
showContextSize, showContextSize,
children,
}) => { }) => {
const language = useLanguage() const language = useLanguage()
@ -42,21 +44,21 @@ const ModelName: FC<ModelNameProps> = ({
`} `}
> >
<div <div
className='mr-1 truncate' className='truncate'
title={modelItem.label[language] || modelItem.label.en_US} title={modelItem.label[language] || modelItem.label.en_US}
> >
{modelItem.label[language] || modelItem.label.en_US} {modelItem.label[language] || modelItem.label.en_US}
</div> </div>
{ {
showModelType && modelItem.model_type && ( showModelType && modelItem.model_type && (
<ModelBadge className={`mr-0.5 ${modelTypeClassName}`}> <ModelBadge className={classNames('ml-1', modelTypeClassName)}>
{modelTypeFormat(modelItem.model_type)} {modelTypeFormat(modelItem.model_type)}
</ModelBadge> </ModelBadge>
) )
} }
{ {
modelItem.model_properties.mode && showMode && ( modelItem.model_properties.mode && showMode && (
<ModelBadge className={`mr-0.5 ${modeClassName}`}> <ModelBadge className={classNames('ml-1', modeClassName)}>
{(modelItem.model_properties.mode as string).toLocaleUpperCase()} {(modelItem.model_properties.mode as string).toLocaleUpperCase()}
</ModelBadge> </ModelBadge>
) )
@ -72,11 +74,12 @@ const ModelName: FC<ModelNameProps> = ({
} }
{ {
showContextSize && modelItem.model_properties.context_size && ( showContextSize && modelItem.model_properties.context_size && (
<ModelBadge> <ModelBadge className='ml-1'>
{sizeFormat(modelItem.model_properties.context_size as number)} {sizeFormat(modelItem.model_properties.context_size as number)}
</ModelBadge> </ModelBadge>
) )
} }
{children}
</div> </div>
) )
} }

View File

@ -86,7 +86,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
isInWorkflow, isInWorkflow,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { hasSettedApiKey } = useProviderContext() const { isAPIKeySet } = useProviderContext()
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 {
@ -99,7 +99,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
const hasDeprecated = !currentProvider || !currentModel const hasDeprecated = !currentProvider || !currentModel
const modelDisabled = currentModel?.status !== ModelStatusEnum.active const modelDisabled = currentModel?.status !== ModelStatusEnum.active
const disabled = !hasSettedApiKey || hasDeprecated || modelDisabled const disabled = !isAPIKeySet || hasDeprecated || modelDisabled
const parameterRules: ModelParameterRule[] = useMemo(() => { const parameterRules: ModelParameterRule[] = useMemo(() => {
return parameterRulesData?.data || [] return parameterRulesData?.data || []

View File

@ -13,7 +13,7 @@ import {
import ModelIcon from '../model-icon' import ModelIcon from '../model-icon'
import ModelName from '../model-name' import ModelName from '../model-name'
import { import {
ConfigurateMethodEnum, ConfigurationMethodEnum,
MODEL_STATUS_TEXT, MODEL_STATUS_TEXT,
ModelStatusEnum, ModelStatusEnum,
} from '../declarations' } from '../declarations'
@ -49,7 +49,7 @@ const PopupItem: FC<PopupItemProps> = ({
setShowModelModal({ setShowModelModal({
payload: { payload: {
currentProvider, currentProvider,
currentConfigurateMethod: ConfigurateMethodEnum.predefinedModel, currentConfigurationMethod: ConfigurationMethodEnum.predefinedModel,
}, },
onSaveCallback: () => { onSaveCallback: () => {
updateModelProviders() updateModelProviders()

View File

@ -0,0 +1,64 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLatest } from 'ahooks'
import SimplePieChart from '@/app/components/base/simple-pie-chart'
import TooltipPlus from '@/app/components/base/tooltip-plus'
export type CooldownTimerProps = {
secondsRemaining?: number
onFinish?: () => void
}
const CooldownTimer = ({ secondsRemaining, onFinish }: CooldownTimerProps) => {
const { t } = useTranslation()
const targetTime = useRef<number>(Date.now())
const [currentTime, setCurrentTime] = useState(targetTime.current)
const displayTime = useMemo(
() => Math.ceil((targetTime.current - currentTime) / 1000),
[currentTime],
)
const countdownTimeout = useRef<NodeJS.Timeout>()
const clearCountdown = useCallback(() => {
if (countdownTimeout.current) {
clearTimeout(countdownTimeout.current)
countdownTimeout.current = undefined
}
}, [])
const onFinishRef = useLatest(onFinish)
const countdown = useCallback(() => {
clearCountdown()
countdownTimeout.current = setTimeout(() => {
const now = Date.now()
if (now <= targetTime.current) {
setCurrentTime(Date.now())
countdown()
}
else {
onFinishRef.current?.()
clearCountdown()
}
}, 1000)
}, [clearCountdown, onFinishRef])
useEffect(() => {
const now = Date.now()
targetTime.current = now + (secondsRemaining ?? 0) * 1000
setCurrentTime(now)
countdown()
return clearCountdown
}, [clearCountdown, countdown, secondsRemaining])
return displayTime
? (
<TooltipPlus popupContent={t('common.modelProvider.apiKeyRateLimit', { seconds: displayTime })}>
<SimplePieChart percentage={Math.round(displayTime / 60 * 100)} className='w-3 h-3' />
</TooltipPlus>
)
: null
}
export default memo(CooldownTimer)

View File

@ -2,7 +2,7 @@ import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ModelProvider } from '../declarations' import type { ModelProvider } from '../declarations'
import { import {
ConfigurateMethodEnum, ConfigurationMethodEnum,
CustomConfigurationStatusEnum, CustomConfigurationStatusEnum,
PreferredProviderTypeEnum, PreferredProviderTypeEnum,
} from '../declarations' } from '../declarations'
@ -51,7 +51,7 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
updateModelProviders() updateModelProviders()
configurateMethods.forEach((method) => { configurateMethods.forEach((method) => {
if (method === ConfigurateMethodEnum.predefinedModel) if (method === ConfigurationMethodEnum.predefinedModel)
provider.supported_model_types.forEach(modelType => updateModelList(modelType)) provider.supported_model_types.forEach(modelType => updateModelList(modelType))
}) })

View File

@ -2,11 +2,11 @@ import type { FC } from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { import type {
CustomConfigrationModelFixedFields, CustomConfigurationModelFixedFields,
ModelItem, ModelItem,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { ConfigurateMethodEnum } from '../declarations' import { ConfigurationMethodEnum } from '../declarations'
import { import {
DEFAULT_BACKGROUND_COLOR, DEFAULT_BACKGROUND_COLOR,
MODEL_PROVIDER_QUOTA_GET_PAID, MODEL_PROVIDER_QUOTA_GET_PAID,
@ -27,7 +27,7 @@ import { IS_CE_EDITION } from '@/config'
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST' export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
type ProviderAddedCardProps = { type ProviderAddedCardProps = {
provider: ModelProvider provider: ModelProvider
onOpenModal: (configurateMethod: ConfigurateMethodEnum, currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void onOpenModal: (configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
} }
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
provider, provider,
@ -39,7 +39,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [collapsed, setCollapsed] = useState(true) const [collapsed, setCollapsed] = useState(true)
const [modelList, setModelList] = useState<ModelItem[]>([]) const [modelList, setModelList] = useState<ModelItem[]>([])
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote) const configurationMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
const systemConfig = provider.system_configuration const systemConfig = provider.system_configuration
const hasModelList = fetched && !!modelList.length const hasModelList = fetched && !!modelList.length
const showQuota = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider) && !IS_CE_EDITION const showQuota = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider) && !IS_CE_EDITION
@ -101,9 +101,9 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
) )
} }
{ {
configurateMethods.includes(ConfigurateMethodEnum.predefinedModel) && ( configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && (
<CredentialPanel <CredentialPanel
onSetup={() => onOpenModal(ConfigurateMethodEnum.predefinedModel)} onSetup={() => onOpenModal(ConfigurationMethodEnum.predefinedModel)}
provider={provider} provider={provider}
/> />
) )
@ -136,9 +136,9 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
} }
</div> </div>
{ {
configurateMethods.includes(ConfigurateMethodEnum.customizableModel) && ( configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && (
<AddModelButton <AddModelButton
onClick={() => onOpenModal(ConfigurateMethodEnum.customizableModel)} onClick={() => onOpenModal(ConfigurationMethodEnum.customizableModel)}
className='hidden group-hover:flex group-hover:text-primary-600' className='hidden group-hover:flex group-hover:text-primary-600'
/> />
) )
@ -152,7 +152,8 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
provider={provider} provider={provider}
models={modelList} models={modelList}
onCollapse={() => setCollapsed(true)} onCollapse={() => setCollapsed(true)}
onConfig={currentCustomConfigrationModelFixedFields => onOpenModal(ConfigurateMethodEnum.customizableModel, currentCustomConfigrationModelFixedFields)} onConfig={currentCustomConfigurationModelFixedFields => onOpenModal(ConfigurationMethodEnum.customizableModel, currentCustomConfigurationModelFixedFields)}
onChange={(provider: string) => getModelList(provider)}
/> />
) )
} }

View File

@ -0,0 +1,119 @@
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import { useDebounceFn } from 'ahooks'
import type { CustomConfigurationModelFixedFields, ModelItem, ModelProvider } from '../declarations'
import { ConfigurationMethodEnum, ModelStatusEnum } from '../declarations'
import ModelBadge from '../model-badge'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import Button from '@/app/components/base/button'
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import Switch from '@/app/components/base/switch'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import { useProviderContext, useProviderContextSelector } from '@/context/provider-context'
import { disableModel, enableModel } from '@/service/common'
import { Plan } from '@/app/components/billing/type'
export type ModelListItemProps = {
model: ModelItem
provider: ModelProvider
isConfigurable: boolean
onConfig: (currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
onModifyLoadBalancing?: (model: ModelItem) => void
}
const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoadBalancing }: ModelListItemProps) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
const toggleModelEnablingStatus = useCallback(async (enabled: boolean) => {
if (enabled)
await enableModel(`/workspaces/current/model-providers/${provider.provider}/models/enable`, { model: model.model, model_type: model.model_type })
else
await disableModel(`/workspaces/current/model-providers/${provider.provider}/models/disable`, { model: model.model, model_type: model.model_type })
}, [model.model, model.model_type, provider.provider])
const { run: debouncedToggleModelEnablingStatus } = useDebounceFn(toggleModelEnablingStatus, { wait: 500 })
const onEnablingStateChange = useCallback(async (value: boolean) => {
debouncedToggleModelEnablingStatus(value)
}, [debouncedToggleModelEnablingStatus])
return (
<div
key={model.model}
className={classNames(
'group flex items-center pl-2 pr-2.5 h-8 rounded-lg',
isConfigurable && 'hover:bg-gray-50',
model.deprecated && 'opacity-60',
)}
>
<ModelIcon
className='shrink-0 mr-2'
provider={provider}
modelName={model.model}
/>
<ModelName
className='grow text-sm font-normal text-gray-900'
modelItem={model}
showModelType
showMode
showContextSize
>
{modelLoadBalancingEnabled && !model.deprecated && model.load_balancing_enabled && (
<ModelBadge className='ml-1 uppercase text-indigo-600 border-indigo-300'>
<Balance className='w-3 h-3 mr-0.5' />
{t('common.modelProvider.loadBalancingHeadline')}
</ModelBadge>
)}
</ModelName>
<div className='shrink-0 flex items-center'>
{
model.fetch_from === ConfigurationMethodEnum.customizableModel
? (
<Button
className='hidden group-hover:flex py-0 h-7 text-xs font-medium text-gray-700'
onClick={() => onConfig({ __model_name: model.model, __model_type: model.model_type })}
>
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
{t('common.modelProvider.config')}
</Button>
)
: ((modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status))
? (
<Button
className='opacity-0 group-hover:opacity-100 px-3 h-[28px] text-xs text-gray-700 rounded-md transition-opacity'
onClick={() => onModifyLoadBalancing?.(model)}
>
<Balance className='mr-1 w-[14px] h-[14px]' />
{t('common.modelProvider.configLoadBalancing')}
</Button>
)
: null
}
{
model.deprecated
? (
<TooltipPlus popupContent={<span className='font-semibold'>{t('common.modelProvider.modelHasBeenDeprecated')}</span>} offset={{ mainAxis: 4 }}>
<Switch defaultValue={false} disabled size='md' />
</TooltipPlus>
)
: (
<Switch
className='ml-2'
defaultValue={model?.status === ModelStatusEnum.active}
disabled={![ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status)}
size='md'
onChange={onEnablingStateChange}
/>
)
}
</div>
</div>
)
}
export default memo(ModelListItem)

View File

@ -1,41 +1,48 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { import type {
CustomConfigrationModelFixedFields, CustomConfigurationModelFixedFields,
ModelItem, ModelItem,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { import {
ConfigurateMethodEnum, ConfigurationMethodEnum,
ModelStatusEnum,
} from '../declarations' } from '../declarations'
import { useLanguage } from '../hooks'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
// import Tab from './tab' // import Tab from './tab'
import AddModelButton from './add-model-button' import AddModelButton from './add-model-button'
import Indicator from '@/app/components/header/indicator' import ModelListItem from './model-list-item'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows' import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
import Button from '@/app/components/base/button' import { useModalContextSelector } from '@/context/modal-context'
type ModelListProps = { type ModelListProps = {
provider: ModelProvider provider: ModelProvider
models: ModelItem[] models: ModelItem[]
onCollapse: () => void onCollapse: () => void
onConfig: (currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void onConfig: (currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
onChange?: (provider: string) => void
} }
const ModelList: FC<ModelListProps> = ({ const ModelList: FC<ModelListProps> = ({
provider, provider,
models, models,
onCollapse, onCollapse,
onConfig, onConfig,
onChange,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const language = useLanguage() const configurativeMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote) const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel)
const canCustomConfig = configurateMethods.includes(ConfigurateMethodEnum.customizableModel)
// const canSystemConfig = configurateMethods.includes(ConfigurateMethodEnum.predefinedModel) const setShowModelLoadBalancingModal = useModalContextSelector(state => state.setShowModelLoadBalancingModal)
const onModifyLoadBalancing = useCallback((model: ModelItem) => {
setShowModelLoadBalancingModal({
provider,
model: model!,
open: !!model,
onClose: () => setShowModelLoadBalancingModal(null),
onSave: onChange,
})
}, [onChange, provider, setShowModelLoadBalancingModal])
return ( return (
<div className='px-2 pb-2 rounded-b-xl'> <div className='px-2 pb-2 rounded-b-xl'>
@ -46,10 +53,7 @@ const ModelList: FC<ModelListProps> = ({
{t('common.modelProvider.modelsNum', { num: models.length })} {t('common.modelProvider.modelsNum', { num: models.length })}
</span> </span>
<span <span
className={` className='hidden group-hover:inline-flex items-center pl-1 pr-1.5 h-6 text-xs font-medium text-gray-500 bg-gray-50 cursor-pointer rounded-lg'
hidden group-hover:inline-flex items-center pl-1 pr-1.5 h-6 bg-gray-50
text-xs font-medium text-gray-500 cursor-pointer rounded-lg
`}
onClick={() => onCollapse()} onClick={() => onCollapse()}
> >
<ChevronDownDouble className='mr-0.5 w-3 h-3 rotate-180' /> <ChevronDownDouble className='mr-0.5 w-3 h-3 rotate-180' />
@ -57,14 +61,14 @@ const ModelList: FC<ModelListProps> = ({
</span> </span>
</span> </span>
{/* { {/* {
canCustomConfig && canSystemConfig && ( isConfigurable && canSystemConfig && (
<span className='flex items-center'> <span className='flex items-center'>
<Tab active='all' onSelect={() => {}} /> <Tab active='all' onSelect={() => {}} />
</span> </span>
) )
} */} } */}
{ {
canCustomConfig && ( isConfigurable && (
<div className='grow flex justify-end'> <div className='grow flex justify-end'>
<AddModelButton onClick={() => onConfig()} /> <AddModelButton onClick={() => onConfig()} />
</div> </div>
@ -73,44 +77,16 @@ const ModelList: FC<ModelListProps> = ({
</div> </div>
{ {
models.map(model => ( models.map(model => (
<div <ModelListItem
key={model.model} key={model.model}
className={` {...{
group flex items-center pl-2 pr-2.5 h-8 rounded-lg model,
${canCustomConfig && 'hover:bg-gray-50'} provider,
${model.deprecated && 'opacity-60'} isConfigurable,
`} onConfig,
> onModifyLoadBalancing,
<ModelIcon }}
className='shrink-0 mr-2' />
provider={provider}
modelName={model.model}
/>
<ModelName
className='grow text-sm font-normal text-gray-900'
modelItem={model}
showModelType
showMode
showContextSize
/>
<div className='shrink-0 flex items-center'>
{
model.fetch_from === ConfigurateMethodEnum.customizableModel && (
<Button
className='hidden group-hover:flex py-0 h-7 text-xs font-medium text-gray-700'
onClick={() => onConfig({ __model_name: model.model, __model_type: model.model_type })}
>
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
{t('common.modelProvider.config')}
</Button>
)
}
<Indicator
className='ml-2.5'
color={model.status === ModelStatusEnum.active ? 'green' : 'gray'}
/>
</div>
</div>
)) ))
} }
</div> </div>

View File

@ -0,0 +1,269 @@
import classNames from 'classnames'
import type { Dispatch, SetStateAction } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { ConfigurationMethodEnum, CustomConfigurationModelFixedFields, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } from '../declarations'
import Indicator from '../../../indicator'
import CooldownTimer from './cooldown-timer'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import Switch from '@/app/components/base/switch'
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import { Edit02, HelpCircle, Plus02, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { useModalContextSelector } from '@/context/modal-context'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import s from '@/app/components/custom/style.module.css'
import GridMask from '@/app/components/base/grid-mask'
import { useProviderContextSelector } from '@/context/provider-context'
import { IS_CE_EDITION } from '@/config'
export type ModelLoadBalancingConfigsProps = {
draftConfig?: ModelLoadBalancingConfig
setDraftConfig: Dispatch<SetStateAction<ModelLoadBalancingConfig | undefined>>
provider: ModelProvider
configurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
withSwitch?: boolean
className?: string
}
const ModelLoadBalancingConfigs = ({
draftConfig,
setDraftConfig,
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
withSwitch = false,
className,
}: ModelLoadBalancingConfigsProps) => {
const { t } = useTranslation()
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
const updateConfigEntry = useCallback(
(
index: number,
modifier: (entry: ModelLoadBalancingConfigEntry) => ModelLoadBalancingConfigEntry | undefined,
) => {
setDraftConfig((prev) => {
if (!prev)
return prev
const newConfigs = [...prev.configs]
const modifiedConfig = modifier(newConfigs[index])
if (modifiedConfig)
newConfigs[index] = modifiedConfig
else
newConfigs.splice(index, 1)
return {
...prev,
configs: newConfigs,
}
})
},
[setDraftConfig],
)
const toggleModalBalancing = useCallback((enabled: boolean) => {
if ((modelLoadBalancingEnabled || !enabled) && draftConfig) {
setDraftConfig({
...draftConfig,
enabled,
})
}
}, [draftConfig, modelLoadBalancingEnabled, setDraftConfig])
const toggleConfigEntryEnabled = useCallback((index: number, state?: boolean) => {
updateConfigEntry(index, entry => ({
...entry,
enabled: typeof state === 'boolean' ? state : !entry.enabled,
}))
}, [updateConfigEntry])
const setShowModelLoadBalancingEntryModal = useModalContextSelector(state => state.setShowModelLoadBalancingEntryModal)
const toggleEntryModal = useCallback((index?: number, entry?: ModelLoadBalancingConfigEntry) => {
setShowModelLoadBalancingEntryModal({
payload: {
currentProvider: provider,
currentConfigurationMethod: configurationMethod,
currentCustomConfigurationModelFixedFields,
entry,
index,
},
onSaveCallback: ({ entry: result }) => {
if (entry) {
// edit
setDraftConfig(prev => ({
...prev,
enabled: !!prev?.enabled,
configs: prev?.configs.map((config, i) => i === index ? result! : config) || [],
}))
}
else {
// add
setDraftConfig(prev => ({
...prev,
enabled: !!prev?.enabled,
configs: (prev?.configs || []).concat([{ ...result!, enabled: true }]),
}))
}
},
onRemoveCallback: ({ index }) => {
if (index !== undefined && (draftConfig?.configs?.length ?? 0) > index) {
setDraftConfig(prev => ({
...prev,
enabled: !!prev?.enabled,
configs: prev?.configs.filter((_, i) => i !== index) || [],
}))
}
},
})
}, [
configurationMethod,
currentCustomConfigurationModelFixedFields,
draftConfig?.configs?.length,
provider,
setDraftConfig,
setShowModelLoadBalancingEntryModal,
])
const clearCountdown = useCallback((index: number) => {
updateConfigEntry(index, ({ ttl: _, ...entry }) => {
return {
...entry,
in_cooldown: false,
}
})
}, [updateConfigEntry])
if (!draftConfig)
return null
return (
<>
<div
className={classNames(
'min-h-16 bg-gray-50 border rounded-xl transition-colors',
(withSwitch || !draftConfig.enabled) ? 'border-gray-200' : 'border-primary-400',
(withSwitch || draftConfig.enabled) ? 'cursor-default' : 'cursor-pointer',
className,
)}
onClick={(!withSwitch && !draftConfig.enabled) ? () => toggleModalBalancing(true) : undefined}
>
<div className='flex items-center px-[15px] py-3 gap-2 select-none'>
<div className='grow-0 shrink-0 flex items-center justify-center w-8 h-8 text-primary-600 bg-indigo-50 border border-indigo-100 rounded-lg'>
<Balance className='w-4 h-4' />
</div>
<div className='grow'>
<div className='flex items-center gap-1 text-sm'>
{t('common.modelProvider.loadBalancing')}
<TooltipPlus popupContent={t('common.modelProvider.loadBalancingInfo')} popupClassName='max-w-[300px]'>
<HelpCircle className='w-3 h-3 text-gray-400' />
</TooltipPlus>
</div>
<div className='text-xs text-gray-500'>{t('common.modelProvider.loadBalancingDescription')}</div>
</div>
{
withSwitch && (
<Switch
defaultValue={Boolean(draftConfig.enabled)}
size='l'
className='ml-3 justify-self-end'
disabled={!modelLoadBalancingEnabled && !draftConfig.enabled}
onChange={value => toggleModalBalancing(value)}
/>
)
}
</div>
{draftConfig.enabled && (
<div className='flex flex-col gap-1 px-3 pb-3'>
{draftConfig.configs.map((config, index) => {
const isProviderManaged = config.name === '__inherit__'
return (
<div key={config.id || index} className='group flex items-center px-3 h-10 bg-white border border-gray-200 rounded-lg shadow-xs'>
<div className='grow flex items-center'>
<div className='flex items-center justify-center mr-2 w-3 h-3'>
{(config.in_cooldown && Boolean(config.ttl))
? (
<CooldownTimer secondsRemaining={config.ttl} onFinish={() => clearCountdown(index)} />
)
: (
<TooltipPlus popupContent={t('common.modelProvider.apiKeyStatusNormal')}>
<Indicator color='green' />
</TooltipPlus>
)}
</div>
<div className='text-[13px] mr-1'>
{isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name}
</div>
{isProviderManaged && (
<span className='px-1 text-2xs uppercase text-gray-500 border border-black/8 rounded-[5px]'>{t('common.modelProvider.providerManaged')}</span>
)}
</div>
<div className='flex items-center gap-1'>
{!isProviderManaged && (
<>
<div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
<span
className='flex items-center justify-center w-8 h-8 text-gray-500 bg-white rounded-lg transition-colors cursor-pointer hover:bg-black/5'
onClick={() => toggleEntryModal(index, config)}
>
<Edit02 className='w-4 h-4' />
</span>
<span
className='flex items-center justify-center w-8 h-8 text-gray-500 bg-white rounded-lg transition-colors cursor-pointer hover:bg-black/5'
onClick={() => updateConfigEntry(index, () => undefined)}
>
<Trash03 className='w-4 h-4' />
</span>
<span className='mr-2 h-3 border-r border-r-gray-100' />
</div>
</>
)}
<Switch
defaultValue={Boolean(config.enabled)}
size='md'
className='justify-self-end'
onChange={value => toggleConfigEntryEnabled(index, value)}
/>
</div>
</div>
)
})}
<div
className='flex items-center px-3 mt-1 h-8 text-[13px] font-medium text-primary-600'
onClick={() => toggleEntryModal()}
>
<div className='flex items-center cursor-pointer'>
<Plus02 className='mr-2 w-3 h-3' />{t('common.modelProvider.addConfig')}
</div>
</div>
</div>
)}
{
draftConfig.enabled && draftConfig.configs.length < 2 && (
<div className='flex items-center px-6 h-[34px] text-xs text-gray-700 bg-black/2 border-t border-t-black/5'>
<AlertTriangle className='mr-1 w-3 h-3 text-[#f79009]' />
{t('common.modelProvider.loadBalancingLeastKeyWarning')}
</div>
)
}
</div>
{!modelLoadBalancingEnabled && !IS_CE_EDITION && (
<GridMask canvasClassName='!rounded-xl'>
<div className='flex items-center justify-between mt-2 px-4 h-14 border-[0.5px] border-gray-200 rounded-xl shadow-md'>
<div
className={classNames('text-sm font-semibold leading-tight text-gradient', s.textGradient)}
>
{t('common.modelProvider.upgradeForLoadBalancing')}
</div>
<UpgradeBtn />
</div>
</GridMask>
)}
</>
)
}
export default ModelLoadBalancingConfigs

View File

@ -0,0 +1,190 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import useSWR from 'swr'
import type { ModelItem, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } from '../declarations'
import { FormTypeEnum } from '../declarations'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import { savePredefinedLoadBalancingConfig } from '../utils'
import ModelLoadBalancingConfigs from './model-load-balancing-configs'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { fetchModelLoadBalancingConfig } from '@/service/common'
import Loading from '@/app/components/base/loading'
import { useToastContext } from '@/app/components/base/toast'
export type ModelLoadBalancingModalProps = {
provider: ModelProvider
model: ModelItem
open?: boolean
onClose?: () => void
onSave?: (provider: string) => void
}
// model balancing config modal
const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSave }: ModelLoadBalancingModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [loading, setLoading] = useState(false)
const { data, mutate } = useSWR(
`/workspaces/current/model-providers/${provider.provider}/models/credentials?model=${model.model}&model_type=${model.model_type}`,
fetchModelLoadBalancingConfig,
)
const originalConfig = data?.load_balancing
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
const originalConfigMap = useMemo(() => {
if (!originalConfig)
return {}
return originalConfig?.configs.reduce((prev, config) => {
if (config.id)
prev[config.id] = config
return prev
}, {} as Record<string, ModelLoadBalancingConfigEntry>)
}, [originalConfig])
useEffect(() => {
if (originalConfig)
setDraftConfig(originalConfig)
}, [originalConfig])
const toggleModalBalancing = useCallback((enabled: boolean) => {
if (draftConfig) {
setDraftConfig({
...draftConfig,
enabled,
})
}
}, [draftConfig])
const extendedSecretFormSchemas = useMemo(
() => provider.provider_credential_schema.credential_form_schemas.filter(
({ type }) => type === FormTypeEnum.secretInput,
),
[provider.provider_credential_schema.credential_form_schemas],
)
const encodeConfigEntrySecretValues = useCallback((entry: ModelLoadBalancingConfigEntry) => {
const result = { ...entry }
extendedSecretFormSchemas.forEach(({ variable }) => {
if (entry.id && result.credentials[variable] === originalConfigMap[entry.id]?.credentials?.[variable])
result.credentials[variable] = '[__HIDDEN__]'
})
return result
}, [extendedSecretFormSchemas, originalConfigMap])
const handleSave = async () => {
try {
setLoading(true)
const res = await savePredefinedLoadBalancingConfig(
provider.provider,
({
...(data?.credentials ?? {}),
__model_type: model.model_type,
__model_name: model.model,
}),
{
...draftConfig,
enabled: Boolean(draftConfig?.enabled),
configs: draftConfig!.configs.map(encodeConfigEntrySecretValues),
},
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate()
onSave?.(provider.provider)
onClose?.()
}
}
finally {
setLoading(false)
}
}
return (
<Modal
isShow={Boolean(model) && open}
onClose={onClose}
wrapperClassName='!z-30'
className='max-w-none pt-8 px-8 w-[640px]'
title={
<div className='pb-3 font-semibold'>
<div className='h-[30px]'>{t('common.modelProvider.configLoadBalancing')}</div>
{Boolean(model) && (
<div className='flex items-center h-5'>
<ModelIcon
className='shrink-0 mr-2'
provider={provider}
modelName={model!.model}
/>
<ModelName
className='grow text-sm font-normal text-gray-900'
modelItem={model!}
showModelType
showMode
showContextSize
/>
</div>
)}
</div>
}
>
{!draftConfig
? <Loading type='area' />
: (
<>
<div className='py-2'>
<div
className={classNames(
'min-h-16 bg-gray-50 border rounded-xl transition-colors',
draftConfig.enabled ? 'border-gray-200 cursor-pointer' : 'border-primary-400 cursor-default',
)}
onClick={draftConfig.enabled ? () => toggleModalBalancing(false) : undefined}
>
<div className='flex items-center px-[15px] py-3 gap-2 select-none'>
<div className='grow-0 shrink-0 flex items-center justify-center w-8 h-8 bg-white border rounded-lg'>
{Boolean(model) && (
<ModelIcon className='shrink-0' provider={provider} modelName={model!.model} />
)}
</div>
<div className='grow'>
<div className='text-sm'>{t('common.modelProvider.providerManaged')}</div>
<div className='text-xs text-gray-500'>{t('common.modelProvider.providerManagedDescription')}</div>
</div>
</div>
</div>
<ModelLoadBalancingConfigs {...{
draftConfig,
setDraftConfig,
provider,
currentCustomConfigurationModelFixedFields: {
__model_name: model.model,
__model_type: model.model_type,
},
configurationMethod: model.fetch_from,
className: 'mt-2',
}} />
</div>
<div className='flex items-center justify-end gap-2 mt-6'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button
type='primary'
onClick={handleSave}
disabled={
loading
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
}
>{t('common.operation.save')}</Button>
</div>
</>
)
}
</Modal >
)
}
export default memo(ModelLoadBalancingModal)

View File

@ -18,7 +18,7 @@ const Selector: FC<SelectorProps> = ({
const options = [ const options = [
{ {
key: PreferredProviderTypeEnum.custom, key: PreferredProviderTypeEnum.custom,
text: 'API', text: t('common.modelProvider.apiKey'),
}, },
{ {
key: PreferredProviderTypeEnum.system, key: PreferredProviderTypeEnum.system,

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import type { import type {
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { ConfigurateMethodEnum } from '../declarations' import { ConfigurationMethodEnum } from '../declarations'
import { import {
DEFAULT_BACKGROUND_COLOR, DEFAULT_BACKGROUND_COLOR,
modelTypeFormat, modelTypeFormat,
@ -19,7 +19,7 @@ import Button from '@/app/components/base/button'
type ProviderCardProps = { type ProviderCardProps = {
provider: ModelProvider provider: ModelProvider
onOpenModal: (configurateMethod: ConfigurateMethodEnum) => void onOpenModal: (configurateMethod: ConfigurationMethodEnum) => void
} }
const ProviderCard: FC<ProviderCardProps> = ({ const ProviderCard: FC<ProviderCardProps> = ({
@ -28,8 +28,7 @@ const ProviderCard: FC<ProviderCardProps> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const language = useLanguage() const language = useLanguage()
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
return ( return (
<div <div
@ -59,7 +58,7 @@ const ProviderCard: FC<ProviderCardProps> = ({
<div className={`hidden group-hover:grid grid-cols-${configurateMethods.length} gap-1`}> <div className={`hidden group-hover:grid grid-cols-${configurateMethods.length} gap-1`}>
{ {
configurateMethods.map((method) => { configurateMethods.map((method) => {
if (method === ConfigurateMethodEnum.predefinedModel) { if (method === ConfigurationMethodEnum.predefinedModel) {
return ( return (
<Button <Button
key={method} key={method}

View File

@ -3,8 +3,10 @@ import type {
CredentialFormSchemaRadio, CredentialFormSchemaRadio,
CredentialFormSchemaTextInput, CredentialFormSchemaTextInput,
FormValue, FormValue,
ModelLoadBalancingConfig,
} from './declarations' } from './declarations'
import { import {
ConfigurationMethodEnum,
FormTypeEnum, FormTypeEnum,
MODEL_TYPE_TEXT, MODEL_TYPE_TEXT,
ModelTypeEnum, ModelTypeEnum,
@ -12,6 +14,7 @@ import {
import { import {
deleteModelProvider, deleteModelProvider,
setModelProvider, setModelProvider,
validateModelLoadBalancingCredentials,
validateModelProvider, validateModelProvider,
} from '@/service/common' } from '@/service/common'
@ -53,12 +56,38 @@ export const validateCredentials = async (predefined: boolean, provider: string,
} }
} }
export const saveCredentials = async (predefined: boolean, provider: string, v: FormValue) => { export const validateLoadBalancingCredentials = async (predefined: boolean, provider: string, v: FormValue): Promise<{
status: ValidatedStatus
message?: string
}> => {
const { __model_name, __model_type, ...credentials } = v
try {
const res = await validateModelLoadBalancingCredentials({
url: `/workspaces/current/model-providers/${provider}/models/load-balancing-configs/credentials-validate`,
body: {
model: __model_name,
model_type: __model_type,
credentials,
},
})
if (res.result === 'success')
return Promise.resolve({ status: ValidatedStatus.Success })
else
return Promise.resolve({ status: ValidatedStatus.Error, message: res.error || 'error' })
}
catch (e: any) {
return Promise.resolve({ status: ValidatedStatus.Error, message: e.message })
}
}
export const saveCredentials = async (predefined: boolean, provider: string, v: FormValue, loadBalancing?: ModelLoadBalancingConfig) => {
let body, url let body, url
if (predefined) { if (predefined) {
body = { body = {
config_from: ConfigurationMethodEnum.predefinedModel,
credentials: v, credentials: v,
load_balancing: loadBalancing,
} }
url = `/workspaces/current/model-providers/${provider}` url = `/workspaces/current/model-providers/${provider}`
} }
@ -68,6 +97,7 @@ export const saveCredentials = async (predefined: boolean, provider: string, v:
model: __model_name, model: __model_name,
model_type: __model_type, model_type: __model_type,
credentials, credentials,
load_balancing: loadBalancing,
} }
url = `/workspaces/current/model-providers/${provider}/models` url = `/workspaces/current/model-providers/${provider}/models`
} }
@ -75,6 +105,20 @@ export const saveCredentials = async (predefined: boolean, provider: string, v:
return setModelProvider({ url, body }) return setModelProvider({ url, body })
} }
export const savePredefinedLoadBalancingConfig = async (provider: string, v: FormValue, loadBalancing?: ModelLoadBalancingConfig) => {
const { __model_name, __model_type, ...credentials } = v
const body = {
config_from: ConfigurationMethodEnum.predefinedModel,
model: __model_name,
model_type: __model_type,
credentials,
load_balancing: loadBalancing,
}
const url = `/workspaces/current/model-providers/${provider}/models`
return setModelProvider({ url, body })
}
export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue) => { export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue) => {
let url = '' let url = ''
let body let body

View File

@ -0,0 +1,220 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { AuthHeaderPrefix, AuthType, CollectionType, LOC } from '../types'
import type { Collection, CustomCollectionBackend, Tool } from '../types'
import Loading from '../../base/loading'
import { ArrowNarrowRight } from '../../base/icons/src/vender/line/arrows'
import Toast from '../../base/toast'
import { ConfigurationMethodEnum } from '../../header/account-setting/model-provider-page/declarations'
import Header from './header'
import Item from './item'
import AppIcon from '@/app/components/base/app-icon'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import { fetchCustomCollection, removeBuiltInToolCredential, removeCustomCollection, updateBuiltInToolCredential, updateCustomCollection } from '@/service/tools'
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import type { AgentTool } from '@/types/app'
import { MAX_TOOLS_NUM } from '@/config'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
type Props = {
collection: Collection | null
list: Tool[]
// onToolListChange: () => void // custom tools change
loc: LOC
addedTools?: AgentTool[]
onAddTool?: (collection: Collection, payload: Tool) => void
onRefreshData: () => void
onCollectionRemoved: () => void
isLoading: boolean
}
const ToolList: FC<Props> = ({
collection,
list,
loc,
addedTools,
onAddTool,
onRefreshData,
onCollectionRemoved,
isLoading,
}) => {
const { t } = useTranslation()
const isInToolsPage = loc === LOC.tools
const isBuiltIn = collection?.type === CollectionType.builtIn
const isModel = collection?.type === CollectionType.model
const needAuth = collection?.allow_delete
const { setShowModelModal } = useModalContext()
const [showSettingAuth, setShowSettingAuth] = useState(false)
const { modelProviders: providers } = useProviderContext()
const showSettingAuthModal = () => {
if (isModel) {
const provider = providers.find(item => item.provider === collection?.id)
if (provider) {
setShowModelModal({
payload: {
currentProvider: provider,
currentConfigurationMethod: ConfigurationMethodEnum.predefinedModel,
currentCustomConfigurationModelFixedFields: undefined,
},
onSaveCallback: () => {
onRefreshData()
},
})
}
}
else {
setShowSettingAuth(true)
}
}
const [customCollection, setCustomCollection] = useState<CustomCollectionBackend | null>(null)
useEffect(() => {
if (!collection)
return
(async () => {
if (collection.type === CollectionType.custom) {
const res = await fetchCustomCollection(collection.name)
if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
if (res.credentials.api_key_value)
res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
}
setCustomCollection({
...res,
provider: collection.name,
})
}
})()
}, [collection])
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => {
await updateCustomCollection(data)
onRefreshData()
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setIsShowEditCustomCollectionModal(false)
}
const doRemoveCustomToolCollection = async () => {
await removeCustomCollection(collection?.name as string)
onCollectionRemoved()
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setIsShowEditCustomCollectionModal(false)
}
if (!collection || isLoading)
return <Loading type='app' />
const icon = <>{typeof collection.icon === 'string'
? (
<div
className='p-2 bg-cover bg-center border border-gray-100 rounded-lg'
>
<div className='w-6 h-6 bg-center bg-contain rounded-md'
style={{
backgroundImage: `url(${collection.icon})`,
}}
></div>
</div>
)
: (
<AppIcon
size='large'
icon={collection.icon.content}
background={collection.icon.background}
/>
)}
</>
return (
<div className='flex flex-col h-full pb-4'>
<Header
icon={icon}
collection={collection}
loc={loc}
onShowAuth={() => showSettingAuthModal()}
onShowEditCustomCollection={() => setIsShowEditCustomCollectionModal(true)}
/>
<div className={cn(isInToolsPage ? 'px-6 pt-4' : 'px-4 pt-3')}>
<div className='flex items-center h-[4.5] space-x-2 text-xs font-medium text-gray-500'>
<div className=''>{t('tools.includeToolNum', {
num: list.length,
})}</div>
{needAuth && (isBuiltIn || isModel) && !collection.is_team_authorization && (
<>
<div>·</div>
<div
className='flex items-center text-[#155EEF] cursor-pointer'
onClick={() => showSettingAuthModal()}
>
<div>{t('tools.auth.setup')}</div>
<ArrowNarrowRight className='ml-0.5 w-3 h-3' />
</div>
</>
)}
</div>
</div>
<div className={cn(isInToolsPage ? 'px-6' : 'px-4', 'grow h-0 pt-2 overflow-y-auto')}>
{/* list */}
<div className={cn(isInToolsPage ? 'grid-cols-3 gap-4' : 'grid-cols-1 gap-2', 'grid')}>
{list.map(item => (
<Item
key={item.name}
icon={icon}
payload={item}
collection={collection}
isInToolsPage={isInToolsPage}
isToolNumMax={(addedTools?.length || 0) >= MAX_TOOLS_NUM}
added={!!addedTools?.find(v => v.provider_id === collection.id && v.provider_type === collection.type && v.tool_name === item.name)}
onAdd={!isInToolsPage ? tool => onAddTool?.(collection as Collection, tool) : undefined}
/>
))}
</div>
</div>
{showSettingAuth && (
<ConfigCredential
collection={collection}
onCancel={() => setShowSettingAuth(false)}
onSaved={async (value) => {
await updateBuiltInToolCredential(collection.name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await onRefreshData()
setShowSettingAuth(false)
}}
onRemove={async () => {
await removeBuiltInToolCredential(collection.name)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await onRefreshData()
setShowSettingAuth(false)
}}
/>
)}
{isShowEditCollectionToolModal && (
<EditCustomToolModal
payload={customCollection}
onHide={() => setIsShowEditCustomCollectionModal(false)}
onEdit={doUpdateCustomToolCollection}
onRemove={doRemoveCustomToolCollection}
/>
)}
</div>
)
}
export default React.memo(ToolList)

View File

@ -72,8 +72,8 @@ const BlockIcon: FC<BlockIconProps> = ({
}) => { }) => {
return ( return (
<div className={` <div className={`
flex items-center justify-center border-[0.5px] border-white/[0.02] text-white flex items-center justify-center border-[0.5px] border-white/2 text-white
${ICON_CONTAINER_CLASSNAME_SIZE_MAP[size]} ${ICON_CONTAINER_CLASSNAME_SIZE_MAP[size]}
${ICON_CONTAINER_BG_COLOR_MAP[type]} ${ICON_CONTAINER_BG_COLOR_MAP[type]}
${toolIcon && '!shadow-none'} ${toolIcon && '!shadow-none'}
${className} ${className}

View File

@ -61,7 +61,7 @@ const WorkflowChecklist = ({
> >
<div <div
className={` className={`
group flex items-center justify-center w-full h-full rounded-md cursor-pointer group flex items-center justify-center w-full h-full rounded-md cursor-pointer
hover:bg-primary-50 hover:bg-primary-50
${open && 'bg-primary-50'} ${open && 'bg-primary-50'}
`} `}
@ -122,7 +122,7 @@ const WorkflowChecklist = ({
/> />
{node.title} {node.title}
</div> </div>
<div className='border-t-[0.5px] border-t-black/[0.02]'> <div className='border-t-[0.5px] border-t-black/2'>
{ {
node.unConnected && ( node.unConnected && (
<div className='px-3 py-2 bg-gray-25 rounded-b-lg'> <div className='px-3 py-2 bg-gray-25 rounded-b-lg'>

View File

@ -11,7 +11,7 @@ const Operator = () => {
width: 102, width: 102,
height: 72, height: 72,
}} }}
className='!absolute !left-4 !bottom-14 z-[9] !m-0 !w-[102px] !h-[72px] !border-[0.5px] !border-black/[0.08] !rounded-lg !shadow-lg' className='!absolute !left-4 !bottom-14 z-[9] !m-0 !w-[102px] !h-[72px] !border-[0.5px] !border-black/8 !rounded-lg !shadow-lg'
/> />
<div className='flex items-center mt-1 gap-2 absolute left-4 bottom-4 z-[9]'> <div className='flex items-center mt-1 gap-2 absolute left-4 bottom-4 z-[9]'>
<ZoomInOut /> <ZoomInOut />

View File

@ -71,7 +71,7 @@ const ChatRecord = () => {
return ( return (
<div <div
className={` className={`
flex flex-col w-[400px] rounded-l-2xl h-full border border-black/[0.02] shadow-xl flex flex-col w-[400px] rounded-l-2xl h-full border border-black/2 shadow-xl
`} `}
style={{ style={{
background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)', background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)',

View File

@ -20,7 +20,7 @@ export type ChatWrapperRefType = {
} }
const DebugAndPreview = () => { const DebugAndPreview = () => {
const { t } = useTranslation() const { t } = useTranslation()
const chatRef = useRef({ handleRestart: () => {} }) const chatRef = useRef({ handleRestart: () => { } })
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions() const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const { handleNodeCancelRunningStatus } = useNodesInteractions() const { handleNodeCancelRunningStatus } = useNodesInteractions()
const { handleEdgeCancelRunningStatus } = useEdgesInteractions() const { handleEdgeCancelRunningStatus } = useEdgesInteractions()
@ -40,7 +40,7 @@ const DebugAndPreview = () => {
return ( return (
<div <div
className={cn( className={cn(
'flex flex-col w-[400px] rounded-l-2xl h-full border border-black/[0.02]', 'flex flex-col w-[400px] rounded-l-2xl h-full border border-black/2',
)} )}
style={{ style={{
background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)', background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)',

View File

@ -147,4 +147,5 @@ button:focus-within {
bottom: 0; bottom: 0;
} }
@import '../components/base/button/index.css'; @import '../components/base/button/index.css';
@import '../components/base/modal/index.css';

View File

@ -28,7 +28,7 @@ import type { Collection } from '@/app/components/tools/types'
type IDebugConfiguration = { type IDebugConfiguration = {
appId: string appId: string
hasSetAPIKEY: boolean isAPIKeySet: boolean
isTrailFinished: boolean isTrailFinished: boolean
mode: string mode: string
modelModeType: ModelModeType modelModeType: ModelModeType
@ -101,7 +101,7 @@ type IDebugConfiguration = {
const DebugConfigurationContext = createContext<IDebugConfiguration>({ const DebugConfigurationContext = createContext<IDebugConfiguration>({
appId: '', appId: '',
hasSetAPIKEY: false, isAPIKeySet: false,
isTrailFinished: false, isTrailFinished: false,
mode: '', mode: '',
modelModeType: ModelModeType.chat, modelModeType: ModelModeType.chat,
@ -134,7 +134,7 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
introduction: '', introduction: '',
setIntroduction: () => { }, setIntroduction: () => { },
suggestedQuestions: [], suggestedQuestions: [],
setSuggestedQuestions: () => {}, setSuggestedQuestions: () => { },
controlClearChatMessage: 0, controlClearChatMessage: 0,
setControlClearChatMessage: () => { }, setControlClearChatMessage: () => { },
prevPromptConfig: { prevPromptConfig: {

View File

@ -2,7 +2,7 @@
import type { Dispatch, SetStateAction } from 'react' import type { Dispatch, SetStateAction } from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { createContext, useContext } from 'use-context-selector' import { createContext, useContext, useContextSelector } from 'use-context-selector'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import AccountSetting from '@/app/components/header/account-setting' import AccountSetting from '@/app/components/header/account-setting'
import ApiBasedExtensionModal from '@/app/components/header/account-setting/api-based-extension-page/modal' import ApiBasedExtensionModal from '@/app/components/header/account-setting/api-based-extension-page/modal'
@ -11,8 +11,9 @@ import ExternalDataToolModal from '@/app/components/app/configuration/tools/exte
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal' import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
import ModelModal from '@/app/components/header/account-setting/model-provider-page/model-modal' import ModelModal from '@/app/components/header/account-setting/model-provider-page/model-modal'
import type { import type {
ConfigurateMethodEnum, ConfigurationMethodEnum,
CustomConfigrationModelFixedFields, CustomConfigurationModelFixedFields,
ModelLoadBalancingConfigEntry,
ModelProvider, ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -22,20 +23,28 @@ import type {
ApiBasedExtension, ApiBasedExtension,
ExternalDataTool, ExternalDataTool,
} from '@/models/common' } from '@/models/common'
import ModelLoadBalancingEntryModal from '@/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal'
import type { ModelLoadBalancingModalProps } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
import ModelLoadBalancingModal from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
export type ModalState<T> = { export type ModalState<T> = {
payload: T payload: T
onCancelCallback?: () => void onCancelCallback?: () => void
onSaveCallback?: (newPayload: T) => void onSaveCallback?: (newPayload: T) => void
onRemoveCallback?: (newPayload: T) => void
onValidateBeforeSaveCallback?: (newPayload: T) => boolean onValidateBeforeSaveCallback?: (newPayload: T) => boolean
} }
export type ModelModalType = { export type ModelModalType = {
currentProvider: ModelProvider currentProvider: ModelProvider
currentConfigurateMethod: ConfigurateMethodEnum currentConfigurationMethod: ConfigurationMethodEnum
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
} }
const ModalContext = createContext<{ export type LoadBalancingEntryModalType = ModelModalType & {
entry?: ModelLoadBalancingConfigEntry
index?: number
}
export type ModalContextState = {
setShowAccountSettingModal: Dispatch<SetStateAction<ModalState<string> | null>> setShowAccountSettingModal: Dispatch<SetStateAction<ModalState<string> | null>>
setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<ApiBasedExtension> | null>> setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<ApiBasedExtension> | null>>
setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>> setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>>
@ -43,18 +52,29 @@ const ModalContext = createContext<{
setShowPricingModal: () => void setShowPricingModal: () => void
setShowAnnotationFullModal: () => void setShowAnnotationFullModal: () => void
setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>> setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>>
}>({ setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
setShowAccountSettingModal: () => { }, setShowModelLoadBalancingEntryModal: Dispatch<SetStateAction<ModalState<LoadBalancingEntryModalType> | null>>
setShowApiBasedExtensionModal: () => { }, }
setShowModerationSettingModal: () => { }, const ModalContext = createContext<ModalContextState>({
setShowExternalDataToolModal: () => { }, setShowAccountSettingModal: () => { },
setShowPricingModal: () => { }, setShowApiBasedExtensionModal: () => { },
setShowAnnotationFullModal: () => { }, setShowModerationSettingModal: () => { },
setShowModelModal: () => { }, setShowExternalDataToolModal: () => { },
}) setShowPricingModal: () => { },
setShowAnnotationFullModal: () => { },
setShowModelModal: () => { },
setShowModelLoadBalancingModal: () => { },
setShowModelLoadBalancingEntryModal: () => { },
})
export const useModalContext = () => useContext(ModalContext) export const useModalContext = () => useContext(ModalContext)
// Adding a dangling comma to avoid the generic parsing issue in tsx, see:
// https://github.com/microsoft/TypeScript/issues/15713
// eslint-disable-next-line @typescript-eslint/comma-dangle
export const useModalContextSelector = <T,>(selector: (state: ModalContextState) => T): T =>
useContextSelector(ModalContext, selector)
type ModalContextProviderProps = { type ModalContextProviderProps = {
children: React.ReactNode children: React.ReactNode
} }
@ -66,34 +86,32 @@ export const ModalContextProvider = ({
const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null) const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null)
const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | null>(null) const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | null>(null)
const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null) const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null)
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState<ModelLoadBalancingModalProps | null>(null)
const [showModelLoadBalancingEntryModal, setShowModelLoadBalancingEntryModal] = useState<ModalState<LoadBalancingEntryModalType> | null>(null)
const searchParams = useSearchParams() const searchParams = useSearchParams()
const router = useRouter() const router = useRouter()
const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1') const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1')
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false) const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
const handleCancelAccountSettingModal = () => { const handleCancelAccountSettingModal = () => {
setShowAccountSettingModal(null) setShowAccountSettingModal(null)
if (showAccountSettingModal?.onCancelCallback) if (showAccountSettingModal?.onCancelCallback)
showAccountSettingModal?.onCancelCallback() showAccountSettingModal?.onCancelCallback()
} }
const handleCancelModerationSettingModal = () => { const handleCancelModerationSettingModal = () => {
setShowModerationSettingModal(null) setShowModerationSettingModal(null)
if (showModerationSettingModal?.onCancelCallback) if (showModerationSettingModal?.onCancelCallback)
showModerationSettingModal.onCancelCallback() showModerationSettingModal.onCancelCallback()
} }
const handleCancelExternalDataToolModal = () => { const handleCancelExternalDataToolModal = () => {
setShowExternalDataToolModal(null) setShowExternalDataToolModal(null)
if (showExternalDataToolModal?.onCancelCallback) if (showExternalDataToolModal?.onCancelCallback)
showExternalDataToolModal.onCancelCallback() showExternalDataToolModal.onCancelCallback()
} }
const handleCancelModelModal = useCallback(() => { const handleCancelModelModal = useCallback(() => {
setShowModelModal(null) setShowModelModal(null)
if (showModelModal?.onCancelCallback) if (showModelModal?.onCancelCallback)
showModelModal.onCancelCallback() showModelModal.onCancelCallback()
}, [showModelModal]) }, [showModelModal])
@ -101,35 +119,48 @@ export const ModalContextProvider = ({
const handleSaveModelModal = useCallback(() => { const handleSaveModelModal = useCallback(() => {
if (showModelModal?.onSaveCallback) if (showModelModal?.onSaveCallback)
showModelModal.onSaveCallback(showModelModal.payload) showModelModal.onSaveCallback(showModelModal.payload)
setShowModelModal(null) setShowModelModal(null)
}, [showModelModal]) }, [showModelModal])
const handleCancelModelLoadBalancingEntryModal = useCallback(() => {
showModelLoadBalancingEntryModal?.onCancelCallback?.()
setShowModelLoadBalancingEntryModal(null)
}, [showModelLoadBalancingEntryModal])
const handleSaveModelLoadBalancingEntryModal = useCallback((entry: ModelLoadBalancingConfigEntry) => {
showModelLoadBalancingEntryModal?.onSaveCallback?.({
...showModelLoadBalancingEntryModal.payload,
entry,
})
setShowModelLoadBalancingEntryModal(null)
}, [showModelLoadBalancingEntryModal])
const handleRemoveModelLoadBalancingEntry = useCallback(() => {
showModelLoadBalancingEntryModal?.onRemoveCallback?.(showModelLoadBalancingEntryModal.payload)
setShowModelLoadBalancingEntryModal(null)
}, [showModelLoadBalancingEntryModal])
const handleSaveApiBasedExtension = (newApiBasedExtension: ApiBasedExtension) => { const handleSaveApiBasedExtension = (newApiBasedExtension: ApiBasedExtension) => {
if (showApiBasedExtensionModal?.onSaveCallback) if (showApiBasedExtensionModal?.onSaveCallback)
showApiBasedExtensionModal.onSaveCallback(newApiBasedExtension) showApiBasedExtensionModal.onSaveCallback(newApiBasedExtension)
setShowApiBasedExtensionModal(null) setShowApiBasedExtensionModal(null)
} }
const handleSaveModeration = (newModerationConfig: ModerationConfig) => { const handleSaveModeration = (newModerationConfig: ModerationConfig) => {
if (showModerationSettingModal?.onSaveCallback) if (showModerationSettingModal?.onSaveCallback)
showModerationSettingModal.onSaveCallback(newModerationConfig) showModerationSettingModal.onSaveCallback(newModerationConfig)
setShowModerationSettingModal(null) setShowModerationSettingModal(null)
} }
const handleSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => { const handleSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => {
if (showExternalDataToolModal?.onSaveCallback) if (showExternalDataToolModal?.onSaveCallback)
showExternalDataToolModal.onSaveCallback(newExternalDataTool) showExternalDataToolModal.onSaveCallback(newExternalDataTool)
setShowExternalDataToolModal(null) setShowExternalDataToolModal(null)
} }
const handleValidateBeforeSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => { const handleValidateBeforeSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => {
if (showExternalDataToolModal?.onValidateBeforeSaveCallback) if (showExternalDataToolModal?.onValidateBeforeSaveCallback)
return showExternalDataToolModal?.onValidateBeforeSaveCallback(newExternalDataTool) return showExternalDataToolModal?.onValidateBeforeSaveCallback(newExternalDataTool)
return true return true
} }
@ -142,6 +173,8 @@ export const ModalContextProvider = ({
setShowPricingModal: () => setShowPricingModal(true), setShowPricingModal: () => setShowPricingModal(true),
setShowAnnotationFullModal: () => setShowAnnotationFullModal(true), setShowAnnotationFullModal: () => setShowAnnotationFullModal(true),
setShowModelModal, setShowModelModal,
setShowModelLoadBalancingModal,
setShowModelLoadBalancingEntryModal,
}}> }}>
<> <>
{children} {children}
@ -205,13 +238,31 @@ export const ModalContextProvider = ({
!!showModelModal && ( !!showModelModal && (
<ModelModal <ModelModal
provider={showModelModal.payload.currentProvider} provider={showModelModal.payload.currentProvider}
configurateMethod={showModelModal.payload.currentConfigurateMethod} configurateMethod={showModelModal.payload.currentConfigurationMethod}
currentCustomConfigrationModelFixedFields={showModelModal.payload.currentCustomConfigrationModelFixedFields} currentCustomConfigurationModelFixedFields={showModelModal.payload.currentCustomConfigurationModelFixedFields}
onCancel={handleCancelModelModal} onCancel={handleCancelModelModal}
onSave={handleSaveModelModal} onSave={handleSaveModelModal}
/> />
) )
} }
{
Boolean(showModelLoadBalancingModal) && (
<ModelLoadBalancingModal {...showModelLoadBalancingModal!} />
)
}
{
!!showModelLoadBalancingEntryModal && (
<ModelLoadBalancingEntryModal
provider={showModelLoadBalancingEntryModal.payload.currentProvider}
configurationMethod={showModelLoadBalancingEntryModal.payload.currentConfigurationMethod}
currentCustomConfigurationModelFixedFields={showModelLoadBalancingEntryModal.payload.currentCustomConfigurationModelFixedFields}
entry={showModelLoadBalancingEntryModal.payload.entry}
onCancel={handleCancelModelLoadBalancingEntryModal}
onSave={handleSaveModelLoadBalancingEntryModal}
onRemove={handleRemoveModelLoadBalancingEntry}
/>
)
}
</> </>
</ModalContext.Provider> </ModalContext.Provider>
) )

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { createContext, useContext } from 'use-context-selector' import { createContext, useContext, useContextSelector } from 'use-context-selector'
import useSWR from 'swr' import useSWR from 'swr'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { import {
@ -19,11 +19,11 @@ import { fetchCurrentPlanInfo } from '@/service/billing'
import { parseCurrentPlan } from '@/app/components/billing/utils' import { parseCurrentPlan } from '@/app/components/billing/utils'
import { defaultPlan } from '@/app/components/billing/config' import { defaultPlan } from '@/app/components/billing/config'
const ProviderContext = createContext<{ type ProviderContextState = {
modelProviders: ModelProvider[] modelProviders: ModelProvider[]
textGenerationModelList: Model[] textGenerationModelList: Model[]
supportRetrievalMethods: RETRIEVE_METHOD[] supportRetrievalMethods: RETRIEVE_METHOD[]
hasSettedApiKey: boolean isAPIKeySet: boolean
plan: { plan: {
type: Plan type: Plan
usage: UsagePlanInfo usage: UsagePlanInfo
@ -33,34 +33,43 @@ const ProviderContext = createContext<{
enableBilling: boolean enableBilling: boolean
onPlanInfoChanged: () => void onPlanInfoChanged: () => void
enableReplaceWebAppLogo: boolean enableReplaceWebAppLogo: boolean
}>({ modelLoadBalancingEnabled: boolean
modelProviders: [], }
textGenerationModelList: [], const ProviderContext = createContext<ProviderContextState>({
supportRetrievalMethods: [], modelProviders: [],
hasSettedApiKey: true, textGenerationModelList: [],
plan: { supportRetrievalMethods: [],
type: Plan.sandbox, isAPIKeySet: true,
usage: { plan: {
vectorSpace: 32, type: Plan.sandbox,
buildApps: 12, usage: {
teamMembers: 1, vectorSpace: 32,
annotatedResponse: 1, buildApps: 12,
}, teamMembers: 1,
total: { annotatedResponse: 1,
vectorSpace: 200, },
buildApps: 50, total: {
teamMembers: 1, vectorSpace: 200,
annotatedResponse: 10, buildApps: 50,
}, teamMembers: 1,
}, annotatedResponse: 10,
isFetchedPlan: false, },
enableBilling: false, },
onPlanInfoChanged: () => { }, isFetchedPlan: false,
enableReplaceWebAppLogo: false, enableBilling: false,
}) onPlanInfoChanged: () => { },
enableReplaceWebAppLogo: false,
modelLoadBalancingEnabled: false,
})
export const useProviderContext = () => useContext(ProviderContext) export const useProviderContext = () => useContext(ProviderContext)
// Adding a dangling comma to avoid the generic parsing issue in tsx, see:
// https://github.com/microsoft/TypeScript/issues/15713
// eslint-disable-next-line @typescript-eslint/comma-dangle
export const useProviderContextSelector = <T,>(selector: (state: ProviderContextState) => T): T =>
useContextSelector(ProviderContext, selector)
type ProviderContextProviderProps = { type ProviderContextProviderProps = {
children: React.ReactNode children: React.ReactNode
} }
@ -76,6 +85,7 @@ export const ProviderContextProvider = ({
const [isFetchedPlan, setIsFetchedPlan] = useState(false) const [isFetchedPlan, setIsFetchedPlan] = useState(false)
const [enableBilling, setEnableBilling] = useState(true) const [enableBilling, setEnableBilling] = useState(true)
const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false) const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false)
const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
const fetchPlan = async () => { const fetchPlan = async () => {
const data = await fetchCurrentPlanInfo() const data = await fetchCurrentPlanInfo()
@ -86,6 +96,8 @@ export const ProviderContextProvider = ({
setPlan(parseCurrentPlan(data)) setPlan(parseCurrentPlan(data))
setIsFetchedPlan(true) setIsFetchedPlan(true)
} }
if (data.model_load_balancing_enabled)
setModelLoadBalancingEnabled(true)
} }
useEffect(() => { useEffect(() => {
fetchPlan() fetchPlan()
@ -95,13 +107,14 @@ export const ProviderContextProvider = ({
<ProviderContext.Provider value={{ <ProviderContext.Provider value={{
modelProviders: providersData?.data || [], modelProviders: providersData?.data || [],
textGenerationModelList: textGenerationModelList?.data || [], textGenerationModelList: textGenerationModelList?.data || [],
hasSettedApiKey: !!textGenerationModelList?.data.some(model => model.status === ModelStatusEnum.active), isAPIKeySet: !!textGenerationModelList?.data.some(model => model.status === ModelStatusEnum.active),
supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [], supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [],
plan, plan,
isFetchedPlan, isFetchedPlan,
enableBilling, enableBilling,
onPlanInfoChanged: fetchPlan, onPlanInfoChanged: fetchPlan,
enableReplaceWebAppLogo, enableReplaceWebAppLogo,
modelLoadBalancingEnabled,
}}> }}>
{children} {children}
</ProviderContext.Provider> </ProviderContext.Provider>

View File

@ -278,6 +278,7 @@ const translation = {
key: 'Rerank Model', key: 'Rerank Model',
tip: 'Rerank model will reorder the candidate document list based on the semantic match with user query, improving the results of semantic ranking', tip: 'Rerank model will reorder the candidate document list based on the semantic match with user query, improving the results of semantic ranking',
}, },
apiKey: 'API-KEY',
quota: 'Quota', quota: 'Quota',
searchModel: 'Search model', searchModel: 'Search model',
noModelFound: 'No model found for {{model}}', noModelFound: 'No model found for {{model}}',
@ -334,6 +335,21 @@ const translation = {
quotaTip: 'Remaining available free tokens', quotaTip: 'Remaining available free tokens',
loadPresets: 'Load Presents', loadPresets: 'Load Presents',
parameters: 'PARAMETERS', parameters: 'PARAMETERS',
loadBalancing: 'Load balancing',
loadBalancingDescription: 'Reduce pressure with multiple sets of credentials.',
loadBalancingHeadline: 'Load Balancing',
configLoadBalancing: 'Config Load Balancing',
modelHasBeenDeprecated: 'This model has been deprecated',
providerManaged: 'Provider managed',
providerManagedDescription: 'Use the single set of credentials provided by the model provider.',
defaultConfig: 'Default Config',
apiKeyStatusNormal: 'APIKey status is normal',
apiKeyRateLimit: 'Rate limit was reached, available after {{seconds}}s',
addConfig: 'Add Config',
editConfig: 'Edit Config',
loadBalancingLeastKeyWarning: 'To enable load balancing at least 2 keys must be enabled.',
loadBalancingInfo: 'By default, load balancing uses the Round-robin strategy. If rate limiting is triggered, a 1-minute cooldown period will be applied.',
upgradeForLoadBalancing: 'Upgrade your plan to enable Load Balancing.',
}, },
dataSource: { dataSource: {
add: 'Add a data source', add: 'Add a data source',

View File

@ -334,6 +334,21 @@ const translation = {
quotaTip: '剩余免费额度', quotaTip: '剩余免费额度',
loadPresets: '加载预设', loadPresets: '加载预设',
parameters: '参数', parameters: '参数',
loadBalancing: '负载均衡',
loadBalancingDescription: '为了减轻单组凭据的压力,您可以为模型调用配置多组凭据。',
loadBalancingHeadline: '负载均衡',
configLoadBalancing: '设置负载均衡',
modelHasBeenDeprecated: '该模型已废弃',
providerManaged: '由模型供应商管理',
providerManagedDescription: '使用模型供应商提供的单组凭据',
defaultConfig: '默认配置',
apiKeyStatusNormal: 'API Key 正常',
apiKeyRateLimit: '已达频率上限,{{seconds}}秒后恢复',
addConfig: '增加配置',
editConfig: '修改配置',
loadBalancingLeastKeyWarning: '至少启用 2 个 Key 以使用负载均衡',
loadBalancingInfo: '默认情况下,负载平衡使用 Round-robin 策略。如果触发速率限制,将应用 1 分钟的冷却时间',
upgradeForLoadBalancing: '升级以解锁负载均衡功能',
}, },
dataSource: { dataSource: {
add: '添加数据源', add: '添加数据源',

View File

@ -30,8 +30,10 @@ import type {
DefaultModelResponse, DefaultModelResponse,
Model, Model,
ModelItem, ModelItem,
ModelLoadBalancingConfig,
ModelParameterRule, ModelParameterRule,
ModelProvider, ModelProvider,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { RETRIEVE_METHOD } from '@/types/app' import type { RETRIEVE_METHOD } from '@/types/app'
import type { SystemFeatures } from '@/types/feature' import type { SystemFeatures } from '@/types/feature'
@ -166,8 +168,22 @@ export const fetchModelProviders: Fetcher<{ data: ModelProvider[] }, string> = (
return get<{ data: ModelProvider[] }>(url) return get<{ data: ModelProvider[] }>(url)
} }
export const fetchModelProviderCredentials: Fetcher<{ credentials?: Record<string, string | undefined | boolean> }, string> = (url) => { export type ModelProviderCredentials = {
return get<{ credentials?: Record<string, string | undefined | boolean> }>(url) credentials?: Record<string, string | undefined | boolean>
load_balancing: ModelLoadBalancingConfig
}
export const fetchModelProviderCredentials: Fetcher<ModelProviderCredentials, string> = (url) => {
return get<ModelProviderCredentials>(url)
}
export const fetchModelLoadBalancingConfig: Fetcher<{
credentials?: Record<string, string | undefined | boolean>
load_balancing: ModelLoadBalancingConfig
}, string> = (url) => {
return get<{
credentials?: Record<string, string | undefined | boolean>
load_balancing: ModelLoadBalancingConfig
}>(url)
} }
export const fetchModelProviderModelList: Fetcher<{ data: ModelItem[] }, string> = (url) => { export const fetchModelProviderModelList: Fetcher<{ data: ModelItem[] }, string> = (url) => {
@ -182,6 +198,10 @@ export const validateModelProvider: Fetcher<ValidateOpenAIKeyResponse, { url: st
return post<ValidateOpenAIKeyResponse>(url, { body }) return post<ValidateOpenAIKeyResponse>(url, { body })
} }
export const validateModelLoadBalancingCredentials: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: any }> = ({ url, body }) => {
return post<ValidateOpenAIKeyResponse>(url, { body })
}
export const setModelProvider: Fetcher<CommonResponse, { url: string; body: any }> = ({ url, body }) => { export const setModelProvider: Fetcher<CommonResponse, { url: string; body: any }> = ({ url, body }) => {
return post<CommonResponse>(url, { body }) return post<CommonResponse>(url, { body })
} }
@ -272,3 +292,9 @@ export const fetchSupportRetrievalMethods: Fetcher<RetrievalMethodsRes, string>
export const getSystemFeatures = () => { export const getSystemFeatures = () => {
return get<SystemFeatures>('/system-features') return get<SystemFeatures>('/system-features')
} }
export const enableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }) =>
patch<CommonResponse>(url, { body })
export const disableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }) =>
patch<CommonResponse>(url, { body })

View File

@ -9,27 +9,30 @@ module.exports = {
extend: { extend: {
colors: { colors: {
gray: { gray: {
25: '#FCFCFD', 25: '#fcfcfd',
50: '#F9FAFB', 50: '#f9fafb',
100: '#F3F4F6', 100: '#f2f4f7',
200: '#E5E7EB', 200: '#eaecf0',
300: '#D1D5DB', 300: '#d0d5dd',
400: '#9CA3AF', 400: '#98a2b3',
500: '#6B7280', 500: '#667085',
700: '#374151', 700: '#475467',
800: '#1F2A37', 600: '#344054',
900: '#111928', 800: '#1d2939',
900: '#101828',
}, },
primary: { primary: {
25: '#F5F8FF', 25: '#f5f8ff',
50: '#EBF5FF', 50: '#eff4ff',
100: '#E1EFFE', 100: '#d1e0ff',
200: '#C3DDFD', 200: '#b2ccff',
300: '#A4CAFE', 300: '#84adff',
400: '#528BFF', 400: '#528bff',
500: '#2970FF', 500: '#2970ff',
600: '#1C64F2', 600: '#155eef',
700: '#1A56DB', 700: '#004eeb',
800: '#0040c1',
900: '#00359e',
}, },
blue: { blue: {
500: '#E1EFFE', 500: '#E1EFFE',
@ -75,6 +78,13 @@ module.exports = {
'2xl': '0px 24px 48px -12px rgba(16, 24, 40, 0.18)', '2xl': '0px 24px 48px -12px rgba(16, 24, 40, 0.18)',
'3xl': '0px 32px 64px -12px rgba(16, 24, 40, 0.14)', '3xl': '0px 32px 64px -12px rgba(16, 24, 40, 0.14)',
}, },
opacity: {
2: '0.02',
8: '0.08',
},
fontSize: {
'2xs': '0.625rem',
},
}, },
}, },
plugins: [ plugins: [