dify
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelIcon from '../model-icon'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ModelTriggerProps = {
|
||||
modelName: string
|
||||
providerName: string
|
||||
className?: string
|
||||
showWarnIcon?: boolean
|
||||
contentClassName?: string
|
||||
}
|
||||
const ModelTrigger: FC<ModelTriggerProps> = ({
|
||||
modelName,
|
||||
providerName,
|
||||
className,
|
||||
showWarnIcon,
|
||||
contentClassName,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modelProviders } = useProviderContext()
|
||||
const currentProvider = modelProviders.find(provider => provider.provider === providerName)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group box-content flex h-8 grow cursor-pointer items-center gap-1 rounded-lg bg-components-input-bg-disabled p-[3px] pl-1', className)}
|
||||
>
|
||||
<div className={cn('flex w-full items-center', contentClassName)}>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-1 py-[1px]'>
|
||||
<ModelIcon
|
||||
className="h-4 w-4"
|
||||
provider={currentProvider}
|
||||
modelName={modelName}
|
||||
/>
|
||||
<div className='system-sm-regular truncate text-components-input-text-filled'>
|
||||
{modelName}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center justify-center'>
|
||||
{showWarnIcon && (
|
||||
<Tooltip popupContent={t('common.modelProvider.deprecated')}>
|
||||
<AlertTriangle className='h-4 w-4 text-text-warning-secondary' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelTrigger
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
type ModelTriggerProps = {
|
||||
open: boolean
|
||||
className?: string
|
||||
}
|
||||
const ModelTrigger: FC<ModelTriggerProps> = ({
|
||||
open,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 hover:bg-components-input-bg-hover', open && 'bg-components-input-bg-hover',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
<div className='mr-1.5 flex h-4 w-4 items-center justify-center rounded-[5px] border border-dashed border-divider-regular'>
|
||||
<CubeOutline className='h-3 w-3 text-text-quaternary' />
|
||||
</div>
|
||||
<div
|
||||
className='truncate text-[13px] text-text-tertiary'
|
||||
title='Configure model'
|
||||
>
|
||||
{t('plugin.detailPanel.configureModel')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-4 w-4 shrink-0 items-center justify-center'>
|
||||
<RiEqualizer2Line className='h-3.5 w-3.5 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelTrigger
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelBadge from '../model-badge'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
ModelFeatureTextEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
AudioSupportIcon,
|
||||
DocumentSupportIcon,
|
||||
// MagicBox,
|
||||
MagicEyes,
|
||||
// MagicWand,
|
||||
// Robot,
|
||||
VideoSupportIcon,
|
||||
} from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type FeatureIconProps = {
|
||||
feature: ModelFeatureEnum
|
||||
className?: string
|
||||
}
|
||||
const FeatureIcon: FC<FeatureIconProps> = ({
|
||||
className,
|
||||
feature,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// if (feature === ModelFeatureEnum.agentThought) {
|
||||
// return (
|
||||
// <Tooltip
|
||||
// popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.agentThought })}
|
||||
// >
|
||||
// <ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
|
||||
// <Robot className='w-3 h-3' />
|
||||
// </ModelBadge>
|
||||
// </Tooltip>
|
||||
// )
|
||||
// }
|
||||
|
||||
// if (feature === ModelFeatureEnum.toolCall) {
|
||||
// return (
|
||||
// <Tooltip
|
||||
// popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.toolCall })}
|
||||
// >
|
||||
// <ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
|
||||
// <MagicWand className='w-3 h-3' />
|
||||
// </ModelBadge>
|
||||
// </Tooltip>
|
||||
// )
|
||||
// }
|
||||
|
||||
// if (feature === ModelFeatureEnum.multiToolCall) {
|
||||
// return (
|
||||
// <Tooltip
|
||||
// popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.multiToolCall })}
|
||||
// >
|
||||
// <ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
|
||||
// <MagicBox className='w-3 h-3' />
|
||||
// </ModelBadge>
|
||||
// </Tooltip>
|
||||
// )
|
||||
// }
|
||||
|
||||
if (feature === ModelFeatureEnum.vision) {
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.vision })}
|
||||
>
|
||||
<div className='inline-block cursor-help'>
|
||||
<ModelBadge className={`w-[18px] justify-center !px-0 text-text-tertiary ${className}`}>
|
||||
<MagicEyes className='h-3 w-3' />
|
||||
</ModelBadge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (feature === ModelFeatureEnum.document) {
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.document })}
|
||||
>
|
||||
<div className='inline-block cursor-help'>
|
||||
<ModelBadge className={`w-[18px] justify-center !px-0 text-text-tertiary ${className}`}>
|
||||
<DocumentSupportIcon className='h-3 w-3' />
|
||||
</ModelBadge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (feature === ModelFeatureEnum.audio) {
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.audio })}
|
||||
>
|
||||
<div className='inline-block cursor-help'>
|
||||
<ModelBadge className={`w-[18px] justify-center !px-0 text-text-tertiary ${className}`}>
|
||||
<AudioSupportIcon className='h-3 w-3' />
|
||||
</ModelBadge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (feature === ModelFeatureEnum.video) {
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.video })}
|
||||
>
|
||||
<div className='inline-block cursor-help'>
|
||||
<ModelBadge className={`w-[18px] justify-center !px-0 text-text-tertiary ${className}`}>
|
||||
<VideoSupportIcon className='h-3 w-3' />
|
||||
</ModelBadge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default FeatureIcon
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import type {
|
||||
DefaultModel,
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import type { ModelFeatureEnum } from '../declarations'
|
||||
import { useCurrentProviderAndModel } from '../hooks'
|
||||
import ModelTrigger from './model-trigger'
|
||||
import EmptyTrigger from './empty-trigger'
|
||||
import DeprecatedModelTrigger from './deprecated-model-trigger'
|
||||
import Popup from './popup'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type ModelSelectorProps = {
|
||||
defaultModel?: DefaultModel
|
||||
modelList: Model[]
|
||||
triggerClassName?: string
|
||||
popupClassName?: string
|
||||
onSelect?: (model: DefaultModel) => void
|
||||
readonly?: boolean
|
||||
scopeFeatures?: ModelFeatureEnum[]
|
||||
deprecatedClassName?: string
|
||||
showDeprecatedWarnIcon?: boolean
|
||||
}
|
||||
const ModelSelector: FC<ModelSelectorProps> = ({
|
||||
defaultModel,
|
||||
modelList,
|
||||
triggerClassName,
|
||||
popupClassName,
|
||||
onSelect,
|
||||
readonly,
|
||||
scopeFeatures = [],
|
||||
deprecatedClassName,
|
||||
showDeprecatedWarnIcon = false,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
} = useCurrentProviderAndModel(
|
||||
modelList,
|
||||
defaultModel,
|
||||
)
|
||||
|
||||
const handleSelect = (provider: string, model: ModelItem) => {
|
||||
setOpen(false)
|
||||
|
||||
if (onSelect)
|
||||
onSelect({ provider, model: model.model })
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
if (readonly)
|
||||
return
|
||||
|
||||
setOpen(v => !v)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<div className={classNames('relative')}>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={handleToggle}
|
||||
className='block'
|
||||
>
|
||||
{
|
||||
currentModel && currentProvider && (
|
||||
<ModelTrigger
|
||||
open={open}
|
||||
provider={currentProvider}
|
||||
model={currentModel}
|
||||
className={triggerClassName}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!currentModel && defaultModel && (
|
||||
<DeprecatedModelTrigger
|
||||
modelName={defaultModel?.model || ''}
|
||||
providerName={defaultModel?.provider || ''}
|
||||
className={triggerClassName}
|
||||
showWarnIcon={showDeprecatedWarnIcon}
|
||||
contentClassName={deprecatedClassName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!defaultModel && (
|
||||
<EmptyTrigger
|
||||
open={open}
|
||||
className={triggerClassName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className={`z-[1002] ${popupClassName}`}>
|
||||
<Popup
|
||||
defaultModel={defaultModel}
|
||||
modelList={modelList}
|
||||
onSelect={handleSelect}
|
||||
scopeFeatures={scopeFeatures}
|
||||
onHide={() => setOpen(false)}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelSelector
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import type {
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import {
|
||||
MODEL_STATUS_TEXT,
|
||||
ModelStatusEnum,
|
||||
} from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ModelTriggerProps = {
|
||||
open: boolean
|
||||
provider: Model
|
||||
model: ModelItem
|
||||
className?: string
|
||||
readonly?: boolean
|
||||
}
|
||||
const ModelTrigger: FC<ModelTriggerProps> = ({
|
||||
open,
|
||||
provider,
|
||||
model,
|
||||
className,
|
||||
readonly,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group flex h-8 items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1',
|
||||
!readonly && 'cursor-pointer hover:bg-components-input-bg-hover',
|
||||
open && 'bg-components-input-bg-hover',
|
||||
model.status !== ModelStatusEnum.active && 'bg-components-input-bg-disabled hover:bg-components-input-bg-disabled',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ModelIcon
|
||||
className='p-0.5'
|
||||
provider={provider}
|
||||
modelName={model.model}
|
||||
/>
|
||||
<div className='flex grow items-center gap-1 truncate px-1 py-[3px]'>
|
||||
<ModelName
|
||||
className='grow'
|
||||
modelItem={model}
|
||||
showMode
|
||||
showFeatures
|
||||
/>
|
||||
{!readonly && (
|
||||
<div className='flex h-4 w-4 shrink-0 items-center justify-center'>
|
||||
{
|
||||
model.status !== ModelStatusEnum.active
|
||||
? (
|
||||
<Tooltip popupContent={MODEL_STATUS_TEXT[model.status][language]}>
|
||||
<AlertTriangle className='h-4 w-4 text-text-warning-secondary' />
|
||||
</Tooltip>
|
||||
)
|
||||
: (
|
||||
<RiArrowDownSLine
|
||||
className='h-3.5 w-3.5 text-text-tertiary'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelTrigger
|
||||
@@ -0,0 +1,195 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiFileTextLine,
|
||||
RiFilmAiLine,
|
||||
RiImageCircleAiLine,
|
||||
RiVoiceAiFill,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
DefaultModel,
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
ModelFeatureTextEnum,
|
||||
ModelTypeEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
modelTypeFormat,
|
||||
sizeFormat,
|
||||
} from '../utils'
|
||||
import {
|
||||
useLanguage,
|
||||
useUpdateModelList,
|
||||
useUpdateModelProviders,
|
||||
} from '../hooks'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import ModelBadge from '../model-badge'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
ModelStatusEnum,
|
||||
} from '../declarations'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type PopupItemProps = {
|
||||
defaultModel?: DefaultModel
|
||||
model: Model
|
||||
onSelect: (provider: string, model: ModelItem) => void
|
||||
}
|
||||
const PopupItem: FC<PopupItemProps> = ({
|
||||
defaultModel,
|
||||
model,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const { setShowModelModal } = useModalContext()
|
||||
const { modelProviders } = useProviderContext()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const currentProvider = modelProviders.find(provider => provider.provider === model.provider)!
|
||||
const handleSelect = (provider: string, modelItem: ModelItem) => {
|
||||
if (modelItem.status !== ModelStatusEnum.active)
|
||||
return
|
||||
|
||||
onSelect(provider, modelItem)
|
||||
}
|
||||
const handleOpenModelModal = () => {
|
||||
setShowModelModal({
|
||||
payload: {
|
||||
currentProvider,
|
||||
currentConfigurationMethod: ConfigurationMethodEnum.predefinedModel,
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
updateModelProviders()
|
||||
|
||||
const modelType = model.models[0].model_type
|
||||
|
||||
if (modelType)
|
||||
updateModelList(modelType)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mb-1'>
|
||||
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>
|
||||
{model.label[language] || model.label.en_US}
|
||||
</div>
|
||||
{
|
||||
model.models.map(modelItem => (
|
||||
<Tooltip
|
||||
key={modelItem.model}
|
||||
position='right'
|
||||
popupClassName='p-3 !w-[206px] bg-components-panel-bg-blur backdrop-blur-sm border-[0.5px] border-components-panel-border rounded-xl'
|
||||
popupContent={
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex flex-col items-start gap-2'>
|
||||
<ModelIcon
|
||||
className={cn('h-5 w-5 shrink-0')}
|
||||
provider={model}
|
||||
modelName={modelItem.model}
|
||||
/>
|
||||
<div className='system-md-medium text-wrap break-words text-text-primary'>{modelItem.label[language] || modelItem.label.en_US}</div>
|
||||
</div>
|
||||
{/* {currentProvider?.description && (
|
||||
<div className='text-text-tertiary system-xs-regular'>{currentProvider?.description?.[language] || currentProvider?.description?.en_US}</div>
|
||||
)} */}
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{modelItem.model_type && (
|
||||
<ModelBadge>
|
||||
{modelTypeFormat(modelItem.model_type)}
|
||||
</ModelBadge>
|
||||
)}
|
||||
{modelItem.model_properties.mode && (
|
||||
<ModelBadge>
|
||||
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
|
||||
</ModelBadge>
|
||||
)}
|
||||
{modelItem.model_properties.context_size && (
|
||||
<ModelBadge>
|
||||
{sizeFormat(modelItem.model_properties.context_size as number)}
|
||||
</ModelBadge>
|
||||
)}
|
||||
</div>
|
||||
{modelItem.model_type === ModelTypeEnum.textGeneration && modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature)) && (
|
||||
<div className='pt-2'>
|
||||
<div className='system-2xs-medium-uppercase mb-1 text-text-tertiary'>{t('common.model.capabilities')}</div>
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{modelItem.features?.includes(ModelFeatureEnum.vision) && (
|
||||
<ModelBadge>
|
||||
<RiImageCircleAiLine className='mr-0.5 h-3.5 w-3.5' />
|
||||
<span>{ModelFeatureTextEnum.vision}</span>
|
||||
</ModelBadge>
|
||||
)}
|
||||
{modelItem.features?.includes(ModelFeatureEnum.audio) && (
|
||||
<ModelBadge>
|
||||
<RiVoiceAiFill className='mr-0.5 h-3.5 w-3.5' />
|
||||
<span>{ModelFeatureTextEnum.audio}</span>
|
||||
</ModelBadge>
|
||||
)}
|
||||
{modelItem.features?.includes(ModelFeatureEnum.video) && (
|
||||
<ModelBadge>
|
||||
<RiFilmAiLine className='mr-0.5 h-3.5 w-3.5' />
|
||||
<span>{ModelFeatureTextEnum.video}</span>
|
||||
</ModelBadge>
|
||||
)}
|
||||
{modelItem.features?.includes(ModelFeatureEnum.document) && (
|
||||
<ModelBadge>
|
||||
<RiFileTextLine className='mr-0.5 h-3.5 w-3.5' />
|
||||
<span>{ModelFeatureTextEnum.document}</span>
|
||||
</ModelBadge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
key={modelItem.model}
|
||||
className={cn('group relative flex h-8 items-center gap-1 rounded-lg px-3 py-1.5', modelItem.status === ModelStatusEnum.active ? 'cursor-pointer hover:bg-state-base-hover' : 'cursor-not-allowed hover:bg-state-base-hover-alt')}
|
||||
onClick={() => handleSelect(model.provider, modelItem)}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<ModelIcon
|
||||
className={cn('h-5 w-5 shrink-0')}
|
||||
provider={model}
|
||||
modelName={modelItem.model}
|
||||
/>
|
||||
<ModelName
|
||||
className={cn('system-sm-medium text-text-secondary', modelItem.status !== ModelStatusEnum.active && 'opacity-60')}
|
||||
modelItem={modelItem}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
defaultModel?.model === modelItem.model && defaultModel.provider === currentProvider.provider && (
|
||||
<Check className='h-4 w-4 shrink-0 text-text-accent' />
|
||||
)
|
||||
}
|
||||
{
|
||||
modelItem.status === ModelStatusEnum.noConfigure && (
|
||||
<div
|
||||
className='hidden cursor-pointer text-xs font-medium text-text-accent group-hover:block'
|
||||
onClick={handleOpenModelModal}
|
||||
>
|
||||
{t('common.operation.add').toLocaleUpperCase()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PopupItem
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
RiSearchLine,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
DefaultModel,
|
||||
Model,
|
||||
ModelItem,
|
||||
} from '../declarations'
|
||||
import { ModelFeatureEnum } from '../declarations'
|
||||
import { useLanguage } from '../hooks'
|
||||
import PopupItem from './popup-item'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { supportFunctionCall } from '@/utils/tool-call'
|
||||
import { tooltipManager } from '@/app/components/base/tooltip/TooltipManager'
|
||||
|
||||
type PopupProps = {
|
||||
defaultModel?: DefaultModel
|
||||
modelList: Model[]
|
||||
onSelect: (provider: string, model: ModelItem) => void
|
||||
scopeFeatures?: ModelFeatureEnum[]
|
||||
onHide: () => void
|
||||
}
|
||||
const Popup: FC<PopupProps> = ({
|
||||
defaultModel,
|
||||
modelList,
|
||||
onSelect,
|
||||
scopeFeatures = [],
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Close any open tooltips when the user scrolls to prevent them from appearing
|
||||
// in incorrect positions or becoming detached from their trigger elements
|
||||
useEffect(() => {
|
||||
const handleTooltipCloseOnScroll = () => {
|
||||
tooltipManager.closeActiveTooltip()
|
||||
}
|
||||
|
||||
const scrollContainer = scrollRef.current
|
||||
if (!scrollContainer) return
|
||||
|
||||
// Use passive listener for better performance since we don't prevent default
|
||||
scrollContainer.addEventListener('scroll', handleTooltipCloseOnScroll, { passive: true })
|
||||
|
||||
return () => {
|
||||
scrollContainer.removeEventListener('scroll', handleTooltipCloseOnScroll)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const filteredModelList = useMemo(() => {
|
||||
return modelList.map((model) => {
|
||||
const filteredModels = model.models
|
||||
.filter((modelItem) => {
|
||||
if (modelItem.label[language] !== undefined)
|
||||
return modelItem.label[language].toLowerCase().includes(searchText.toLowerCase())
|
||||
return Object.values(modelItem.label).some(label =>
|
||||
label.toLowerCase().includes(searchText.toLowerCase()),
|
||||
)
|
||||
})
|
||||
.filter((modelItem) => {
|
||||
if (scopeFeatures.length === 0)
|
||||
return true
|
||||
return scopeFeatures.every((feature) => {
|
||||
if (feature === ModelFeatureEnum.toolCall)
|
||||
return supportFunctionCall(modelItem.features)
|
||||
return modelItem.features?.includes(feature) ?? false
|
||||
})
|
||||
})
|
||||
return { ...model, models: filteredModels }
|
||||
}).filter(model => model.models.length > 0)
|
||||
}, [language, modelList, scopeFeatures, searchText])
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} className='max-h-[480px] w-[320px] overflow-y-auto rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg'>
|
||||
<div className='sticky top-0 z-10 bg-components-panel-bg pb-1 pl-3 pr-2 pt-3'>
|
||||
<div className={`
|
||||
flex h-8 items-center rounded-lg border pl-[9px] pr-[10px]
|
||||
${searchText ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-transparent bg-components-input-bg-normal'}
|
||||
`}>
|
||||
<RiSearchLine
|
||||
className={`
|
||||
mr-[7px] h-[14px] w-[14px] shrink-0
|
||||
${searchText ? 'text-text-tertiary' : 'text-text-quaternary'}
|
||||
`}
|
||||
/>
|
||||
<input
|
||||
className='block h-[18px] grow appearance-none bg-transparent text-[13px] text-text-primary outline-none'
|
||||
placeholder={t('datasetSettings.form.searchModel') || ''}
|
||||
value={searchText}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
/>
|
||||
{
|
||||
searchText && (
|
||||
<XCircle
|
||||
className='ml-1.5 h-[14px] w-[14px] shrink-0 cursor-pointer text-text-quaternary'
|
||||
onClick={() => setSearchText('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
{
|
||||
filteredModelList.map(model => (
|
||||
<PopupItem
|
||||
key={model.provider}
|
||||
defaultModel={defaultModel}
|
||||
model={model}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
!filteredModelList.length && (
|
||||
<div className='break-all px-3 py-1.5 text-center text-xs leading-[18px] text-text-tertiary'>
|
||||
{`No model found for “${searchText}”`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='sticky bottom-0 flex cursor-pointer items-center rounded-b-lg border-t border-divider-subtle bg-components-panel-bg px-4 py-2 text-text-accent-light-mode-only' onClick={() => {
|
||||
onHide()
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
}}>
|
||||
<span className='system-xs-medium'>{t('common.model.settingsLink')}</span>
|
||||
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Popup
|
||||
Reference in New Issue
Block a user