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>
</PortalToFollowElemTrigger>
<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>
<SelectItem
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'
type IDebug = {
hasSetAPIKEY: boolean
isAPIKeySet: boolean
onSetting: () => void
inputs: Inputs
modelParameterParams: Pick<ModelParameterModalProps, 'setModel' | 'onCompletionParamsChange'>
@ -51,7 +51,7 @@ type IDebug = {
}
const Debug: FC<IDebug> = ({
hasSetAPIKEY = true,
isAPIKeySet = true,
onSetting,
inputs,
modelParameterParams,
@ -503,7 +503,7 @@ const Debug: FC<IDebug> = ({
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 {
currentModel: currModel,
textGenerationModelList,
@ -678,7 +678,7 @@ const Configuration: FC = () => {
return (
<ConfigContext.Provider value={{
appId,
hasSetAPIKEY: hasSettedApiKey,
isAPIKeySet,
isTrailFinished: false,
mode,
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)' }}>
<div className='flex flex-col h-0 border-t border-l grow rounded-tl-2xl bg-gray-50 '>
<Debug
hasSetAPIKEY={hasSettedApiKey}
isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs}
modelParameterParams={{
@ -881,7 +881,7 @@ const Configuration: FC = () => {
{isMobile && (
<Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null} panelClassname='!bg-gray-50'>
<Debug
hasSetAPIKEY={hasSettedApiKey}
isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs}
modelParameterParams={{

View File

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

View File

@ -132,8 +132,7 @@ function AppCard({
return (
<div
className={`shadow-xs border-[0.5px] rounded-lg border-gray-200 ${
className ?? ''
className={`shadow-xs border-[0.5px] rounded-lg border-gray-200 ${className ?? ''
}`}
>
<div className={`px-6 py-5 ${customBgColor ?? bgColor} rounded-lg`}>
@ -165,7 +164,7 @@ function AppCard({
? t('appOverview.overview.appInfo.accessibleAddress')
: t('appOverview.overview.apiInfo.accessibleAddress')}
</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="text-gray-700 text-xs font-medium text-ellipsis overflow-hidden whitespace-nowrap">
{isApp ? appUrl : apiUrl}
@ -203,8 +202,7 @@ function AppCard({
onClick={() => setShowConfirmDelete(true)}
>
<div
className={`w-full h-full ${style.refreshIcon} ${
genLoading ? style.generateLogo : ''
className={`w-full h-full ${style.refreshIcon} ${genLoading ? style.generateLogo : ''
}`}
></div>
</div>

View File

@ -3,10 +3,10 @@
@layer components {
.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;
}
};
.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 {
@ -28,4 +28,4 @@
.btn-warning-disabled {
@apply bg-red-600/75 cursor-not-allowed text-white;
}
}
}

View File

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

View File

@ -77,8 +77,7 @@ const ImageList: FC<ImageListProps> = ({
<div
className={`
absolute inset-0 flex items-center justify-center rounded-lg z-[1] border
${
item.progress === -1
${item.progress === -1
? 'bg-[#FEF0C7] border-[#DC6803]'
: 'bg-black/[0.16] border-transparent'
}
@ -120,7 +119,7 @@ const ImageList: FC<ImageListProps> = ({
type="button"
className={cn(
'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',
)}
onClick={() => onRemove && onRemove(item._id)}

View File

@ -18,7 +18,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({
className='max-w-full max-h-full'
/>
<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}
>
<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 { Fragment } from 'react'
import { XMarkIcon } from '@heroicons/react/24/outline'
import classNames from 'classnames'
// https://headlessui.com/react/dialog
type IModal = {
className?: string
wrapperClassName?: string
isShow: boolean
onClose: () => void
onClose?: () => void
title?: React.ReactNode
description?: React.ReactNode
children: React.ReactNode
children?: React.ReactNode
closable?: boolean
overflowVisible?: boolean
}
@ -19,7 +20,7 @@ export default function Modal({
className,
wrapperClassName,
isShow,
onClose,
onClose = () => { },
title,
description,
children,
@ -28,7 +29,7 @@ export default function Modal({
}: IModal) {
return (
<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
as={Fragment}
enter="ease-out duration-300"
@ -58,7 +59,11 @@ export default function Modal({
leaveFrom="opacity-100 scale-100"
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
as="h3"
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'
type SwitchProps = {
onChange: (value: boolean) => void
onChange?: (value: boolean) => void
size?: 'sm' | 'md' | 'lg' | 'l'
defaultValue?: 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)
useEffect(() => {
setEnabled(defaultValue)
@ -42,13 +43,14 @@ const Switch = ({ onChange, size = 'lg', defaultValue = false, disabled = false
if (disabled)
return
setEnabled(checked)
onChange(checked)
onChange?.(checked)
}}
className={classNames(
wrapStyle[size],
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',
disabled ? '!opacity-50 !cursor-not-allowed' : '',
className,
)}
>
<span

View File

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

View File

@ -14,7 +14,7 @@ const CustomAppHeaderBrand = () => {
return (
<div className='py-3'>
<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='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)]'>
@ -43,7 +43,7 @@ const CustomAppHeaderBrand = () => {
<div className='flex items-center mb-2'>
<Button
className={`
!h-8 !px-3 bg-white !text-[13px]
!h-8 !px-3 bg-white !text-[13px]
${plan.type === Plan.sandbox ? 'opacity-40' : ''}
`}
disabled={plan.type === Plan.sandbox}
@ -54,7 +54,7 @@ const CustomAppHeaderBrand = () => {
<div className='mx-2 h-5 w-[1px] bg-black/5'></div>
<Button
className={`
!h-8 !px-3 bg-white !text-[13px]
!h-8 !px-3 bg-white !text-[13px]
${plan.type === Plan.sandbox ? 'opacity-40' : ''}
`}
disabled={plan.type === Plan.sandbox}

View File

@ -106,7 +106,7 @@ const CustomWebAppBrand = () => {
return (
<div className='py-4'>
<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='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'>
@ -152,7 +152,7 @@ const CustomWebAppBrand = () => {
!uploading && (
<Button
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' : ''}
`}
disabled={uploadDisabled}
@ -212,7 +212,7 @@ const CustomWebAppBrand = () => {
<div className='mr-2 h-5 w-[1px] bg-black/5'></div>
<Button
className={`
!h-8 !px-3 bg-white !text-[13px]
!h-8 !px-3 bg-white !text-[13px]
${(uploadDisabled || (!webappLogo && !webappBrandRemoved)) ? 'opacity-40' : ''}
`}
disabled={uploadDisabled || (!webappLogo && !webappBrandRemoved)}

View File

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

View File

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

View File

@ -39,7 +39,7 @@ export const MODEL_TYPE_TEXT = {
[ModelTypeEnum.tts]: 'TTS',
}
export enum ConfigurateMethodEnum {
export enum ConfigurationMethodEnum {
predefinedModel = 'predefined-model',
customizableModel = 'customizable-model',
fetchFromRemote = 'fetch-from-remote',
@ -64,6 +64,7 @@ export enum ModelStatusEnum {
noConfigure = 'no-configure',
quotaExceeded = 'quota-exceeded',
noPermission = 'no-permission',
disabled = 'disabled',
}
export const MODEL_STATUS_TEXT: { [k: string]: TypeWithI18N } = {
@ -114,9 +115,10 @@ export type ModelItem = {
label: TypeWithI18N
model_type: ModelTypeEnum
features?: ModelFeatureEnum[]
fetch_from: ConfigurateMethodEnum
fetch_from: ConfigurationMethodEnum
status: ModelStatusEnum
model_properties: Record<string, string | number>
load_balancing_enabled: boolean
deprecated?: boolean
}
@ -158,7 +160,7 @@ export type ModelProvider = {
icon_large: TypeWithI18N
background?: string
supported_model_types: ModelTypeEnum[]
configurate_methods: ConfigurateMethodEnum[]
configurate_methods: ConfigurationMethodEnum[]
provider_credential_schema: {
credential_form_schemas: CredentialFormSchema[]
}
@ -204,7 +206,7 @@ export type DefaultModel = {
model: string
}
export type CustomConfigrationModelFixedFields = {
export type CustomConfigurationModelFixedFields = {
__model_name: string
__model_type: ModelTypeEnum
}
@ -223,3 +225,23 @@ export type ModelParameterRule = {
options?: string[]
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 { useContext } from 'use-context-selector'
import type {
CustomConfigrationModelFixedFields,
CustomConfigurationModelFixedFields,
DefaultModel,
DefaultModelResponse,
Model,
ModelTypeEnum,
} from './declarations'
import {
ConfigurateMethodEnum,
ConfigurationMethodEnum,
ModelStatusEnum,
} from './declarations'
import I18n from '@/context/i18n'
@ -61,42 +61,55 @@ export const useLanguage = () => {
return locale.replace('-', '_')
}
export const useProviderCrenditialsFormSchemasValue = (
export const useProviderCredentialsAndLoadBalancing = (
provider: string,
configurateMethod: ConfigurateMethodEnum,
configurationMethod: ConfigurationMethodEnum,
configured?: boolean,
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
) => {
const { data: predefinedFormSchemasValue } = useSWR(
(configurateMethod === ConfigurateMethodEnum.predefinedModel && configured)
const { data: predefinedFormSchemasValue, mutate: mutatePredefined } = useSWR(
(configurationMethod === ConfigurationMethodEnum.predefinedModel && configured)
? `/workspaces/current/model-providers/${provider}/credentials`
: null,
fetchModelProviderCredentials,
)
const { data: customFormSchemasValue } = useSWR(
(configurateMethod === ConfigurateMethodEnum.customizableModel && currentCustomConfigrationModelFixedFields)
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigrationModelFixedFields?.__model_name}&model_type=${currentCustomConfigrationModelFixedFields?.__model_type}`
const { data: customFormSchemasValue, mutate: mutateCustomized } = useSWR(
(configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields)
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}`
: null,
fetchModelProviderCredentials,
)
const value = useMemo(() => {
return configurateMethod === ConfigurateMethodEnum.predefinedModel
const credentials = useMemo(() => {
return configurationMethod === ConfigurationMethodEnum.predefinedModel
? predefinedFormSchemasValue?.credentials
: customFormSchemasValue?.credentials
? {
...customFormSchemasValue?.credentials,
...currentCustomConfigrationModelFixedFields,
...currentCustomConfigurationModelFixedFields,
}
: undefined
}, [
configurateMethod,
currentCustomConfigrationModelFixedFields,
configurationMethod,
currentCustomConfigurationModelFixedFields,
customFormSchemasValue?.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) => {

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 ProviderCard from './provider-card'
import type {
CustomConfigrationModelFixedFields,
CustomConfigurationModelFixedFields,
ModelProvider,
} from './declarations'
import {
ConfigurateMethodEnum,
ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
ModelTypeEnum,
} from './declarations'
@ -19,7 +19,7 @@ import {
} from './hooks'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import { useModalContextSelector } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter'
const ModelProviderPage = () => {
@ -33,7 +33,7 @@ const ModelProviderPage = () => {
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: ttsDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
const { modelProviders: providers } = useProviderContext()
const { setShowModelModal } = useModalContext()
const setShowModelModal = useModalContextSelector(state => state.setShowModelModal)
const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
const [configedProviders, notConfigedProviders] = useMemo(() => {
const configedProviders: ModelProvider[] = []
@ -57,32 +57,32 @@ const ModelProviderPage = () => {
const handleOpenModal = (
provider: ModelProvider,
configurateMethod: ConfigurateMethodEnum,
customConfigrationModelFixedFields?: CustomConfigrationModelFixedFields,
configurateMethod: ConfigurationMethodEnum,
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
) => {
setShowModelModal({
payload: {
currentProvider: provider,
currentConfigurateMethod: configurateMethod,
currentCustomConfigrationModelFixedFields: customConfigrationModelFixedFields,
currentConfigurationMethod: configurateMethod,
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
},
onSaveCallback: () => {
updateModelProviders()
if (configurateMethod === ConfigurateMethodEnum.predefinedModel) {
if (configurateMethod === ConfigurationMethodEnum.predefinedModel) {
provider.supported_model_types.forEach((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({
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
payload: provider.provider,
} as any)
if (customConfigrationModelFixedFields?.__model_type)
updateModelList(customConfigrationModelFixedFields?.__model_type)
if (CustomConfigurationModelFixedFields?.__model_type)
updateModelList(CustomConfigurationModelFixedFields?.__model_type)
}
},
})
@ -117,7 +117,7 @@ const ModelProviderPage = () => {
<ProviderAddedCard
key={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
key={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'
type ModelBadgeProps = {
@ -9,11 +10,10 @@ const ModelBadge: FC<ModelBadgeProps> = ({
children,
}) => {
return (
<div className={`
flex items-center px-1 h-[18px] rounded-[5px] border border-black/[0.08] bg-white/[0.48]
text-[10px] font-medium text-gray-500
${className}
`}>
<div className={classNames(
'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',
className,
)}>
{children}
</div>
)

View File

@ -11,12 +11,14 @@ import type {
CredentialFormSchema,
CredentialFormSchemaRadio,
CredentialFormSchemaSelect,
CustomConfigrationModelFixedFields,
CustomConfigurationModelFixedFields,
FormValue,
ModelLoadBalancingConfig,
ModelLoadBalancingConfigEntry,
ModelProvider,
} from '../declarations'
import {
ConfigurateMethodEnum,
ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
FormTypeEnum,
} from '../declarations'
@ -28,11 +30,12 @@ import {
} from '../utils'
import {
useLanguage,
useProviderCrenditialsFormSchemasValue,
useProviderCredentialsAndLoadBalancing,
} from '../hooks'
import ProviderIcon from '../provider-icon'
import { useValidate } from '../../key-validator/hooks'
import { ValidatedStatus } from '../../key-validator/declarations'
import ModelLoadBalancingConfigs from '../provider-added-card/model-load-balancing-configs'
import Form from './Form'
import Button from '@/app/components/base/button'
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 = {
provider: ModelProvider
configurateMethod: ConfigurateMethodEnum
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields
configurateMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
onCancel: () => void
onSave: () => void
}
@ -56,16 +59,20 @@ type ModelModalProps = {
const ModelModal: FC<ModelModalProps> = ({
provider,
configurateMethod,
currentCustomConfigrationModelFixedFields,
currentCustomConfigurationModelFixedFields,
onCancel,
onSave,
}) => {
const providerFormSchemaPredefined = configurateMethod === ConfigurateMethodEnum.predefinedModel
const formSchemasValue = useProviderCrenditialsFormSchemasValue(
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
const {
credentials: formSchemasValue,
loadBalancing: originalConfig,
mutate,
} = useProviderCredentialsAndLoadBalancing(
provider.provider,
configurateMethod,
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
currentCustomConfigrationModelFixedFields,
currentCustomConfigurationModelFixedFields,
)
const isEditMode = !!formSchemasValue
const { t } = useTranslation()
@ -73,13 +80,29 @@ const ModelModal: FC<ModelModalProps> = ({
const language = useLanguage()
const [loading, setLoading] = 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(() => {
return 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,
...(draftConfig?.enabled ? [] : provider.model_credential_schema.credential_form_schemas),
]
}, [
providerFormSchemaPredefined,
@ -87,15 +110,14 @@ const ModelModal: FC<ModelModalProps> = ({
provider.supported_model_types,
provider.model_credential_schema?.credential_form_schemas,
provider.model_credential_schema?.model,
draftConfig?.enabled,
])
const [
requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue,
showOnVariableMap,
] = useMemo(() => {
const requiredFormSchemas: CredentialFormSchema[] = []
const secretFormSchemas: CredentialFormSchema[] = []
const defaultFormSchemaValue: Record<string, string | number> = {}
const showOnVariableMap: Record<string, string[]> = {}
@ -103,9 +125,6 @@ const ModelModal: FC<ModelModalProps> = ({
if (formSchema.required)
requiredFormSchemas.push(formSchema)
if (formSchema.type === FormTypeEnum.secretInput)
secretFormSchemas.push(formSchema)
if (formSchema.default)
defaultFormSchemaValue[formSchema.variable] = formSchema.default
@ -136,22 +155,21 @@ const ModelModal: FC<ModelModalProps> = ({
return [
requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue,
showOnVariableMap,
]
}, [formSchemas])
const initialFormSchemasValue = useMemo(() => {
const initialFormSchemasValue: Record<string, string | number> = useMemo(() => {
return {
...defaultFormSchemaValue,
...formSchemasValue,
}
} as unknown as Record<string, string | number>
}, [formSchemasValue, defaultFormSchemaValue])
const [value, setValue] = useState(initialFormSchemasValue)
useEffect(() => {
setValue(initialFormSchemasValue)
}, [initialFormSchemasValue])
const [validate, validating, validatedStatusState] = useValidate(value)
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
@ -161,32 +179,63 @@ const ModelModal: FC<ModelModalProps> = ({
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) => {
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 () => {
try {
setLoading(true)
const res = await saveCredentials(
providerFormSchemaPredefined,
provider.provider,
encodeSecretValues(value),
{
...value,
...getSecretValues(value),
...draftConfig,
enabled: Boolean(draftConfig?.enabled),
configs: draftConfig?.configs.map(encodeConfigEntrySecretValues) || [],
},
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate()
onSave()
onCancel()
}
@ -207,6 +256,7 @@ const ModelModal: FC<ModelModalProps> = ({
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate()
onSave()
onCancel()
}
@ -217,7 +267,7 @@ const ModelModal: FC<ModelModalProps> = ({
}
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}`
}
@ -232,6 +282,7 @@ const ModelModal: FC<ModelModalProps> = ({
<div className='text-xl font-semibold text-gray-900'>{renderTitlePrefix()}</div>
<ProviderIcon provider={provider} />
</div>
<Form
value={value}
onChange={handleValueChange}
@ -241,7 +292,17 @@ const ModelModal: FC<ModelModalProps> = ({
showOnVariableMap={showOnVariableMap}
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))
? (
@ -278,7 +339,11 @@ const ModelModal: FC<ModelModalProps> = ({
className='h-9 text-sm font-medium'
type='primary'
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')}
</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 {
modelTypeFormat,
sizeFormat,
@ -8,7 +9,7 @@ import type { ModelItem } from '../declarations'
import ModelBadge from '../model-badge'
import FeatureIcon from '../model-selector/feature-icon'
type ModelNameProps = {
type ModelNameProps = PropsWithChildren<{
modelItem: ModelItem
className?: string
showModelType?: boolean
@ -18,7 +19,7 @@ type ModelNameProps = {
showFeatures?: boolean
featuresClassName?: string
showContextSize?: boolean
}
}>
const ModelName: FC<ModelNameProps> = ({
modelItem,
className,
@ -29,6 +30,7 @@ const ModelName: FC<ModelNameProps> = ({
showFeatures,
featuresClassName,
showContextSize,
children,
}) => {
const language = useLanguage()
@ -42,21 +44,21 @@ const ModelName: FC<ModelNameProps> = ({
`}
>
<div
className='mr-1 truncate'
className='truncate'
title={modelItem.label[language] || modelItem.label.en_US}
>
{modelItem.label[language] || modelItem.label.en_US}
</div>
{
showModelType && modelItem.model_type && (
<ModelBadge className={`mr-0.5 ${modelTypeClassName}`}>
<ModelBadge className={classNames('ml-1', modelTypeClassName)}>
{modelTypeFormat(modelItem.model_type)}
</ModelBadge>
)
}
{
modelItem.model_properties.mode && showMode && (
<ModelBadge className={`mr-0.5 ${modeClassName}`}>
<ModelBadge className={classNames('ml-1', modeClassName)}>
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
</ModelBadge>
)
@ -72,11 +74,12 @@ const ModelName: FC<ModelNameProps> = ({
}
{
showContextSize && modelItem.model_properties.context_size && (
<ModelBadge>
<ModelBadge className='ml-1'>
{sizeFormat(modelItem.model_properties.context_size as number)}
</ModelBadge>
)
}
{children}
</div>
)
}

View File

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

View File

@ -13,7 +13,7 @@ import {
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import {
ConfigurateMethodEnum,
ConfigurationMethodEnum,
MODEL_STATUS_TEXT,
ModelStatusEnum,
} from '../declarations'
@ -49,7 +49,7 @@ const PopupItem: FC<PopupItemProps> = ({
setShowModelModal({
payload: {
currentProvider,
currentConfigurateMethod: ConfigurateMethodEnum.predefinedModel,
currentConfigurationMethod: ConfigurationMethodEnum.predefinedModel,
},
onSaveCallback: () => {
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 type { ModelProvider } from '../declarations'
import {
ConfigurateMethodEnum,
ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
} from '../declarations'
@ -51,7 +51,7 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
updateModelProviders()
configurateMethods.forEach((method) => {
if (method === ConfigurateMethodEnum.predefinedModel)
if (method === ConfigurationMethodEnum.predefinedModel)
provider.supported_model_types.forEach(modelType => updateModelList(modelType))
})

View File

@ -2,11 +2,11 @@ import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigrationModelFixedFields,
CustomConfigurationModelFixedFields,
ModelItem,
ModelProvider,
} from '../declarations'
import { ConfigurateMethodEnum } from '../declarations'
import { ConfigurationMethodEnum } from '../declarations'
import {
DEFAULT_BACKGROUND_COLOR,
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'
type ProviderAddedCardProps = {
provider: ModelProvider
onOpenModal: (configurateMethod: ConfigurateMethodEnum, currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void
onOpenModal: (configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
}
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
provider,
@ -39,7 +39,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
const [loading, setLoading] = useState(false)
const [collapsed, setCollapsed] = useState(true)
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 hasModelList = fetched && !!modelList.length
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
onSetup={() => onOpenModal(ConfigurateMethodEnum.predefinedModel)}
onSetup={() => onOpenModal(ConfigurationMethodEnum.predefinedModel)}
provider={provider}
/>
)
@ -136,9 +136,9 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
}
</div>
{
configurateMethods.includes(ConfigurateMethodEnum.customizableModel) && (
configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && (
<AddModelButton
onClick={() => onOpenModal(ConfigurateMethodEnum.customizableModel)}
onClick={() => onOpenModal(ConfigurationMethodEnum.customizableModel)}
className='hidden group-hover:flex group-hover:text-primary-600'
/>
)
@ -152,7 +152,8 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
provider={provider}
models={modelList}
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 { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigrationModelFixedFields,
CustomConfigurationModelFixedFields,
ModelItem,
ModelProvider,
} from '../declarations'
import {
ConfigurateMethodEnum,
ModelStatusEnum,
ConfigurationMethodEnum,
} from '../declarations'
import { useLanguage } from '../hooks'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
// import Tab from './tab'
import AddModelButton from './add-model-button'
import Indicator from '@/app/components/header/indicator'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import ModelListItem from './model-list-item'
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 = {
provider: ModelProvider
models: ModelItem[]
onCollapse: () => void
onConfig: (currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void
onConfig: (currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
onChange?: (provider: string) => void
}
const ModelList: FC<ModelListProps> = ({
provider,
models,
onCollapse,
onConfig,
onChange,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
const canCustomConfig = configurateMethods.includes(ConfigurateMethodEnum.customizableModel)
// const canSystemConfig = configurateMethods.includes(ConfigurateMethodEnum.predefinedModel)
const configurativeMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel)
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 (
<div className='px-2 pb-2 rounded-b-xl'>
@ -46,10 +53,7 @@ const ModelList: FC<ModelListProps> = ({
{t('common.modelProvider.modelsNum', { num: models.length })}
</span>
<span
className={`
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
`}
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'
onClick={() => onCollapse()}
>
<ChevronDownDouble className='mr-0.5 w-3 h-3 rotate-180' />
@ -57,14 +61,14 @@ const ModelList: FC<ModelListProps> = ({
</span>
</span>
{/* {
canCustomConfig && canSystemConfig && (
isConfigurable && canSystemConfig && (
<span className='flex items-center'>
<Tab active='all' onSelect={() => {}} />
</span>
)
} */}
{
canCustomConfig && (
isConfigurable && (
<div className='grow flex justify-end'>
<AddModelButton onClick={() => onConfig()} />
</div>
@ -73,44 +77,16 @@ const ModelList: FC<ModelListProps> = ({
</div>
{
models.map(model => (
<div
<ModelListItem
key={model.model}
className={`
group flex items-center pl-2 pr-2.5 h-8 rounded-lg
${canCustomConfig && '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
/>
<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>
{...{
model,
provider,
isConfigurable,
onConfig,
onModifyLoadBalancing,
}}
/>
))
}
</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 = [
{
key: PreferredProviderTypeEnum.custom,
text: 'API',
text: t('common.modelProvider.apiKey'),
},
{
key: PreferredProviderTypeEnum.system,

View File

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

View File

@ -3,8 +3,10 @@ import type {
CredentialFormSchemaRadio,
CredentialFormSchemaTextInput,
FormValue,
ModelLoadBalancingConfig,
} from './declarations'
import {
ConfigurationMethodEnum,
FormTypeEnum,
MODEL_TYPE_TEXT,
ModelTypeEnum,
@ -12,6 +14,7 @@ import {
import {
deleteModelProvider,
setModelProvider,
validateModelLoadBalancingCredentials,
validateModelProvider,
} 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
if (predefined) {
body = {
config_from: ConfigurationMethodEnum.predefinedModel,
credentials: v,
load_balancing: loadBalancing,
}
url = `/workspaces/current/model-providers/${provider}`
}
@ -68,6 +97,7 @@ export const saveCredentials = async (predefined: boolean, provider: string, v:
model: __model_name,
model_type: __model_type,
credentials,
load_balancing: loadBalancing,
}
url = `/workspaces/current/model-providers/${provider}/models`
}
@ -75,6 +105,20 @@ export const saveCredentials = async (predefined: boolean, provider: string, v:
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) => {
let url = ''
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 (
<div className={`
flex items-center justify-center border-[0.5px] border-white/[0.02] text-white
${ICON_CONTAINER_CLASSNAME_SIZE_MAP[size]}
flex items-center justify-center border-[0.5px] border-white/2 text-white
${ICON_CONTAINER_CLASSNAME_SIZE_MAP[size]}
${ICON_CONTAINER_BG_COLOR_MAP[type]}
${toolIcon && '!shadow-none'}
${className}

View File

@ -61,7 +61,7 @@ const WorkflowChecklist = ({
>
<div
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
${open && 'bg-primary-50'}
`}
@ -122,7 +122,7 @@ const WorkflowChecklist = ({
/>
{node.title}
</div>
<div className='border-t-[0.5px] border-t-black/[0.02]'>
<div className='border-t-[0.5px] border-t-black/2'>
{
node.unConnected && (
<div className='px-3 py-2 bg-gray-25 rounded-b-lg'>

View File

@ -11,7 +11,7 @@ const Operator = () => {
width: 102,
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]'>
<ZoomInOut />

View File

@ -71,7 +71,7 @@ const ChatRecord = () => {
return (
<div
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={{
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 { t } = useTranslation()
const chatRef = useRef({ handleRestart: () => {} })
const chatRef = useRef({ handleRestart: () => { } })
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const { handleNodeCancelRunningStatus } = useNodesInteractions()
const { handleEdgeCancelRunningStatus } = useEdgesInteractions()
@ -40,7 +40,7 @@ const DebugAndPreview = () => {
return (
<div
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={{
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;
}
@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 = {
appId: string
hasSetAPIKEY: boolean
isAPIKeySet: boolean
isTrailFinished: boolean
mode: string
modelModeType: ModelModeType
@ -101,7 +101,7 @@ type IDebugConfiguration = {
const DebugConfigurationContext = createContext<IDebugConfiguration>({
appId: '',
hasSetAPIKEY: false,
isAPIKeySet: false,
isTrailFinished: false,
mode: '',
modelModeType: ModelModeType.chat,
@ -134,7 +134,7 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
introduction: '',
setIntroduction: () => { },
suggestedQuestions: [],
setSuggestedQuestions: () => {},
setSuggestedQuestions: () => { },
controlClearChatMessage: 0,
setControlClearChatMessage: () => { },
prevPromptConfig: {

View File

@ -2,7 +2,7 @@
import type { Dispatch, SetStateAction } 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 AccountSetting from '@/app/components/header/account-setting'
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 ModelModal from '@/app/components/header/account-setting/model-provider-page/model-modal'
import type {
ConfigurateMethodEnum,
CustomConfigrationModelFixedFields,
ConfigurationMethodEnum,
CustomConfigurationModelFixedFields,
ModelLoadBalancingConfigEntry,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -22,20 +23,28 @@ import type {
ApiBasedExtension,
ExternalDataTool,
} 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> = {
payload: T
onCancelCallback?: () => void
onSaveCallback?: (newPayload: T) => void
onRemoveCallback?: (newPayload: T) => void
onValidateBeforeSaveCallback?: (newPayload: T) => boolean
}
export type ModelModalType = {
currentProvider: ModelProvider
currentConfigurateMethod: ConfigurateMethodEnum
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields
currentConfigurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
}
const ModalContext = createContext<{
export type LoadBalancingEntryModalType = ModelModalType & {
entry?: ModelLoadBalancingConfigEntry
index?: number
}
export type ModalContextState = {
setShowAccountSettingModal: Dispatch<SetStateAction<ModalState<string> | null>>
setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<ApiBasedExtension> | null>>
setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>>
@ -43,18 +52,29 @@ const ModalContext = createContext<{
setShowPricingModal: () => void
setShowAnnotationFullModal: () => void
setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>>
}>({
setShowAccountSettingModal: () => { },
setShowApiBasedExtensionModal: () => { },
setShowModerationSettingModal: () => { },
setShowExternalDataToolModal: () => { },
setShowPricingModal: () => { },
setShowAnnotationFullModal: () => { },
setShowModelModal: () => { },
})
setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
setShowModelLoadBalancingEntryModal: Dispatch<SetStateAction<ModalState<LoadBalancingEntryModalType> | null>>
}
const ModalContext = createContext<ModalContextState>({
setShowAccountSettingModal: () => { },
setShowApiBasedExtensionModal: () => { },
setShowModerationSettingModal: () => { },
setShowExternalDataToolModal: () => { },
setShowPricingModal: () => { },
setShowAnnotationFullModal: () => { },
setShowModelModal: () => { },
setShowModelLoadBalancingModal: () => { },
setShowModelLoadBalancingEntryModal: () => { },
})
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 = {
children: React.ReactNode
}
@ -66,34 +86,32 @@ export const ModalContextProvider = ({
const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null)
const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | 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 router = useRouter()
const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1')
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
const handleCancelAccountSettingModal = () => {
setShowAccountSettingModal(null)
if (showAccountSettingModal?.onCancelCallback)
showAccountSettingModal?.onCancelCallback()
}
const handleCancelModerationSettingModal = () => {
setShowModerationSettingModal(null)
if (showModerationSettingModal?.onCancelCallback)
showModerationSettingModal.onCancelCallback()
}
const handleCancelExternalDataToolModal = () => {
setShowExternalDataToolModal(null)
if (showExternalDataToolModal?.onCancelCallback)
showExternalDataToolModal.onCancelCallback()
}
const handleCancelModelModal = useCallback(() => {
setShowModelModal(null)
if (showModelModal?.onCancelCallback)
showModelModal.onCancelCallback()
}, [showModelModal])
@ -101,35 +119,48 @@ export const ModalContextProvider = ({
const handleSaveModelModal = useCallback(() => {
if (showModelModal?.onSaveCallback)
showModelModal.onSaveCallback(showModelModal.payload)
setShowModelModal(null)
}, [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) => {
if (showApiBasedExtensionModal?.onSaveCallback)
showApiBasedExtensionModal.onSaveCallback(newApiBasedExtension)
setShowApiBasedExtensionModal(null)
}
const handleSaveModeration = (newModerationConfig: ModerationConfig) => {
if (showModerationSettingModal?.onSaveCallback)
showModerationSettingModal.onSaveCallback(newModerationConfig)
setShowModerationSettingModal(null)
}
const handleSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => {
if (showExternalDataToolModal?.onSaveCallback)
showExternalDataToolModal.onSaveCallback(newExternalDataTool)
setShowExternalDataToolModal(null)
}
const handleValidateBeforeSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => {
if (showExternalDataToolModal?.onValidateBeforeSaveCallback)
return showExternalDataToolModal?.onValidateBeforeSaveCallback(newExternalDataTool)
return true
}
@ -142,6 +173,8 @@ export const ModalContextProvider = ({
setShowPricingModal: () => setShowPricingModal(true),
setShowAnnotationFullModal: () => setShowAnnotationFullModal(true),
setShowModelModal,
setShowModelLoadBalancingModal,
setShowModelLoadBalancingEntryModal,
}}>
<>
{children}
@ -205,13 +238,31 @@ export const ModalContextProvider = ({
!!showModelModal && (
<ModelModal
provider={showModelModal.payload.currentProvider}
configurateMethod={showModelModal.payload.currentConfigurateMethod}
currentCustomConfigrationModelFixedFields={showModelModal.payload.currentCustomConfigrationModelFixedFields}
configurateMethod={showModelModal.payload.currentConfigurationMethod}
currentCustomConfigurationModelFixedFields={showModelModal.payload.currentCustomConfigurationModelFixedFields}
onCancel={handleCancelModelModal}
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>
)

View File

@ -1,6 +1,6 @@
'use client'
import { createContext, useContext } from 'use-context-selector'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import useSWR from 'swr'
import { useEffect, useState } from 'react'
import {
@ -19,11 +19,11 @@ import { fetchCurrentPlanInfo } from '@/service/billing'
import { parseCurrentPlan } from '@/app/components/billing/utils'
import { defaultPlan } from '@/app/components/billing/config'
const ProviderContext = createContext<{
type ProviderContextState = {
modelProviders: ModelProvider[]
textGenerationModelList: Model[]
supportRetrievalMethods: RETRIEVE_METHOD[]
hasSettedApiKey: boolean
isAPIKeySet: boolean
plan: {
type: Plan
usage: UsagePlanInfo
@ -33,34 +33,43 @@ const ProviderContext = createContext<{
enableBilling: boolean
onPlanInfoChanged: () => void
enableReplaceWebAppLogo: boolean
}>({
modelProviders: [],
textGenerationModelList: [],
supportRetrievalMethods: [],
hasSettedApiKey: true,
plan: {
type: Plan.sandbox,
usage: {
vectorSpace: 32,
buildApps: 12,
teamMembers: 1,
annotatedResponse: 1,
},
total: {
vectorSpace: 200,
buildApps: 50,
teamMembers: 1,
annotatedResponse: 10,
},
},
isFetchedPlan: false,
enableBilling: false,
onPlanInfoChanged: () => { },
enableReplaceWebAppLogo: false,
})
modelLoadBalancingEnabled: boolean
}
const ProviderContext = createContext<ProviderContextState>({
modelProviders: [],
textGenerationModelList: [],
supportRetrievalMethods: [],
isAPIKeySet: true,
plan: {
type: Plan.sandbox,
usage: {
vectorSpace: 32,
buildApps: 12,
teamMembers: 1,
annotatedResponse: 1,
},
total: {
vectorSpace: 200,
buildApps: 50,
teamMembers: 1,
annotatedResponse: 10,
},
},
isFetchedPlan: false,
enableBilling: false,
onPlanInfoChanged: () => { },
enableReplaceWebAppLogo: false,
modelLoadBalancingEnabled: false,
})
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 = {
children: React.ReactNode
}
@ -76,6 +85,7 @@ export const ProviderContextProvider = ({
const [isFetchedPlan, setIsFetchedPlan] = useState(false)
const [enableBilling, setEnableBilling] = useState(true)
const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false)
const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
const fetchPlan = async () => {
const data = await fetchCurrentPlanInfo()
@ -86,6 +96,8 @@ export const ProviderContextProvider = ({
setPlan(parseCurrentPlan(data))
setIsFetchedPlan(true)
}
if (data.model_load_balancing_enabled)
setModelLoadBalancingEnabled(true)
}
useEffect(() => {
fetchPlan()
@ -95,13 +107,14 @@ export const ProviderContextProvider = ({
<ProviderContext.Provider value={{
modelProviders: providersData?.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 || [],
plan,
isFetchedPlan,
enableBilling,
onPlanInfoChanged: fetchPlan,
enableReplaceWebAppLogo,
modelLoadBalancingEnabled,
}}>
{children}
</ProviderContext.Provider>

View File

@ -278,6 +278,7 @@ const translation = {
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',
},
apiKey: 'API-KEY',
quota: 'Quota',
searchModel: 'Search model',
noModelFound: 'No model found for {{model}}',
@ -334,6 +335,21 @@ const translation = {
quotaTip: 'Remaining available free tokens',
loadPresets: 'Load Presents',
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: {
add: 'Add a data source',

View File

@ -334,6 +334,21 @@ const translation = {
quotaTip: '剩余免费额度',
loadPresets: '加载预设',
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: {
add: '添加数据源',

View File

@ -30,8 +30,10 @@ import type {
DefaultModelResponse,
Model,
ModelItem,
ModelLoadBalancingConfig,
ModelParameterRule,
ModelProvider,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { RETRIEVE_METHOD } from '@/types/app'
import type { SystemFeatures } from '@/types/feature'
@ -166,8 +168,22 @@ export const fetchModelProviders: Fetcher<{ data: ModelProvider[] }, string> = (
return get<{ data: ModelProvider[] }>(url)
}
export const fetchModelProviderCredentials: Fetcher<{ credentials?: Record<string, string | undefined | boolean> }, string> = (url) => {
return get<{ credentials?: Record<string, string | undefined | boolean> }>(url)
export type ModelProviderCredentials = {
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) => {
@ -182,6 +198,10 @@ export const validateModelProvider: Fetcher<ValidateOpenAIKeyResponse, { url: st
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 }) => {
return post<CommonResponse>(url, { body })
}
@ -272,3 +292,9 @@ export const fetchSupportRetrievalMethods: Fetcher<RetrievalMethodsRes, string>
export const getSystemFeatures = () => {
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: {
colors: {
gray: {
25: '#FCFCFD',
50: '#F9FAFB',
100: '#F3F4F6',
200: '#E5E7EB',
300: '#D1D5DB',
400: '#9CA3AF',
500: '#6B7280',
700: '#374151',
800: '#1F2A37',
900: '#111928',
25: '#fcfcfd',
50: '#f9fafb',
100: '#f2f4f7',
200: '#eaecf0',
300: '#d0d5dd',
400: '#98a2b3',
500: '#667085',
700: '#475467',
600: '#344054',
800: '#1d2939',
900: '#101828',
},
primary: {
25: '#F5F8FF',
50: '#EBF5FF',
100: '#E1EFFE',
200: '#C3DDFD',
300: '#A4CAFE',
400: '#528BFF',
500: '#2970FF',
600: '#1C64F2',
700: '#1A56DB',
25: '#f5f8ff',
50: '#eff4ff',
100: '#d1e0ff',
200: '#b2ccff',
300: '#84adff',
400: '#528bff',
500: '#2970ff',
600: '#155eef',
700: '#004eeb',
800: '#0040c1',
900: '#00359e',
},
blue: {
500: '#E1EFFE',
@ -75,6 +78,13 @@ module.exports = {
'2xl': '0px 24px 48px -12px rgba(16, 24, 40, 0.18)',
'3xl': '0px 32px 64px -12px rgba(16, 24, 40, 0.14)',
},
opacity: {
2: '0.02',
8: '0.08',
},
fontSize: {
'2xs': '0.625rem',
},
},
},
plugins: [