mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-10 04:18:59 +08:00
feat: model load balancing (#4926)
This commit is contained in:
parent
d1dbbc1e33
commit
37f292ea91
@ -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}
|
||||
|
@ -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} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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={{
|
||||
|
@ -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))
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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 |
@ -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"
|
||||
}
|
@ -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
|
@ -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'
|
||||
|
@ -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)}
|
||||
|
@ -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' />
|
||||
|
7
web/app/components/base/modal/index.css
Normal file
7
web/app/components/base/modal/index.css
Normal 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;
|
||||
}
|
@ -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"
|
||||
|
@ -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);
|
||||
}
|
66
web/app/components/base/simple-pie-chart/index.tsx
Normal file
66
web/app/components/base/simple-pie-chart/index.tsx
Normal 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)
|
@ -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
|
||||
|
@ -65,6 +65,7 @@ export type CurrentPlanInfoBackend = {
|
||||
}
|
||||
docs_processing: DocumentProcessingPriority
|
||||
can_replace_logo: boolean
|
||||
model_load_balancing_enabled: boolean
|
||||
}
|
||||
|
||||
export type SubscriptionItem = {
|
||||
|
@ -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}
|
||||
|
@ -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)}
|
||||
|
@ -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}
|
||||
|
@ -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')} </span>
|
||||
<span className={s.click} onClick={onSetting}>{t('datasetCreation.stepTwo.click')}</span>
|
||||
|
@ -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}
|
||||
|
@ -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[]
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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)
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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 || []
|
||||
|
@ -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()
|
||||
|
@ -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)
|
@ -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))
|
||||
})
|
||||
|
||||
|
@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
@ -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>
|
||||
|
@ -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
|
@ -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)
|
@ -18,7 +18,7 @@ const Selector: FC<SelectorProps> = ({
|
||||
const options = [
|
||||
{
|
||||
key: PreferredProviderTypeEnum.custom,
|
||||
text: 'API',
|
||||
text: t('common.modelProvider.apiKey'),
|
||||
},
|
||||
{
|
||||
key: PreferredProviderTypeEnum.system,
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
220
web/app/components/tools/tool-list/index.tsx
Normal file
220
web/app/components/tools/tool-list/index.tsx
Normal 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)
|
@ -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}
|
||||
|
@ -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'>
|
||||
|
@ -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 />
|
||||
|
@ -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)',
|
||||
|
@ -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)',
|
||||
|
@ -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';
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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: '添加数据源',
|
||||
|
@ -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 })
|
||||
|
@ -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: [
|
||||
|
Loading…
x
Reference in New Issue
Block a user