Merge branch 'feat/plugins' of github.com:langgenius/dify into feat/plugins

This commit is contained in:
Yi 2024-10-11 16:28:04 +08:00
commit 3cb0a5bd68
13 changed files with 366 additions and 142 deletions

View File

@ -43,7 +43,7 @@ const PluginList = async () => {
<h3 className='my-1'>Install model provide</h3>
<div className='grid grid-cols-2 gap-3'>
{pluginList.map((plugin, index) => (
<InstallModelItem key={index} payload={plugin as any} />
<InstallModelItem key={index} locale={locale} payload={plugin as any} />
))}
</div>

View File

@ -102,7 +102,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
</Menu.Item>
<div className="px-1 py-1">
<Menu.Item>
<div className={itemClassName} onClick={() => setShowAccountSettingModal({ payload: 'account' })}>
<div className={itemClassName} onClick={() => setShowAccountSettingModal({ payload: 'provider' })}>
<div>{t('common.userProfile.settings')}</div>
</div>
</Menu.Item>

View File

@ -6,9 +6,8 @@ import {
RiAccountCircleLine,
RiApps2AddFill,
RiApps2AddLine,
RiBox3Fill,
RiBox3Line,
RiCloseLine,
RiBrainFill,
RiBrainLine,
RiColorFilterFill,
RiColorFilterLine,
RiDatabase2Fill,
@ -31,17 +30,14 @@ import ModelProviderPage from './model-provider-page'
import cn from '@/utils/classnames'
import BillingPage from '@/app/components/billing/billing-page'
import CustomPage from '@/app/components/custom/custom-page'
import Modal from '@/app/components/base/modal'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context'
import { useAppContext } from '@/context/app-context'
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
import Input from '@/app/components/base/input'
const iconClassName = `
w-4 h-4 ml-3 mr-2
`
const scrolledClassName = `
border-b shadow-xs bg-white/[.98]
w-5 h-5 mr-2
`
type IAccountSettingProps = {
@ -59,7 +55,7 @@ type GroupItem = {
export default function AccountSetting({
onCancel,
activeTab = 'account',
activeTab = 'provider',
}: IAccountSettingProps) {
const [activeMenu, setActiveMenu] = useState(activeTab)
const { t } = useTranslation()
@ -73,8 +69,8 @@ export default function AccountSetting({
{
key: 'provider',
name: t('common.settings.provider'),
icon: <RiBox3Line className={iconClassName} />,
activeIcon: <RiBox3Fill className={iconClassName} />,
icon: <RiBrainLine className={iconClassName} />,
activeIcon: <RiBrainFill className={iconClassName} />,
},
{
key: 'members',
@ -122,7 +118,7 @@ export default function AccountSetting({
},
{
key: 'account-group',
name: t('common.settings.accountGroup'),
name: t('common.settings.generalGroup'),
items: [
{
key: 'account',
@ -161,32 +157,31 @@ export default function AccountSetting({
const activeItem = [...menuItems[0].items, ...menuItems[1].items].find(item => item.key === activeMenu)
const [searchValue, setSearchValue] = useState<string>('')
return (
<Modal
isShow
onClose={() => { }}
className='my-[60px] p-0 max-w-[1024px] rounded-xl overflow-y-auto'
wrapperClassName='pt-[60px]'
<MenuDialog
show
onClose={onCancel}
>
<div className='flex'>
<div className='w-[44px] sm:w-[200px] px-[1px] py-4 sm:p-4 border border-gray-100 shrink-0 sm:shrink-1 flex flex-col items-center sm:items-start'>
<div className='mb-8 ml-0 sm:ml-2 text-sm sm:text-base font-medium leading-6 text-gray-900'>{t('common.userProfile.settings')}</div>
<div className='mx-auto max-w-[1048px] h-[100vh] flex'>
<div className='w-[44px] sm:w-[224px] pl-4 pr-6 border-r border-divider-burn flex flex-col'>
<div className='mt-6 mb-8 px-3 py-2 text-text-primary title-2xl-semi-bold'>{t('common.userProfile.settings')}</div>
<div className='w-full'>
{
menuItems.map(menuItem => (
<div key={menuItem.key} className='mb-4'>
<div key={menuItem.key} className='mb-2'>
{!isCurrentWorkspaceDatasetOperator && (
<div className='px-2 mb-[6px] text-[10px] sm:text-xs font-medium text-gray-500'>{menuItem.name}</div>
<div className='py-2 pl-3 pb-1 mb-0.5 text-text-tertiary system-xs-medium-uppercase'>{menuItem.name}</div>
)}
<div>
{
menuItem.items.map(item => (
<div
key={item.key}
className={`
flex items-center h-[37px] mb-[2px] text-sm cursor-pointer rounded-lg
${activeMenu === item.key ? 'font-semibold text-primary-600 bg-primary-50' : 'font-light text-gray-700'}
`}
className={cn(
'flex items-center mb-0.5 p-1 pl-3 h-[37px] text-sm cursor-pointer rounded-lg',
activeMenu === item.key ? 'bg-state-base-active text-components-menu-item-text-active system-sm-semibold' : 'text-components-menu-item-text system-sm-medium')}
title={item.name}
onClick={() => setActiveMenu(item.key)}
>
@ -201,18 +196,22 @@ export default function AccountSetting({
}
</div>
</div>
<div ref={scrollRef} className='relative w-[824px] h-[720px] pb-4 overflow-y-auto'>
<div className={cn('sticky top-0 px-6 py-4 flex items-center h-14 mb-4 bg-white text-base font-medium text-gray-900 z-20', scrolled && scrolledClassName)}>
<div className='shrink-0'>{activeItem?.name}</div>
<div ref={scrollRef} className='relative w-[824px] pb-4 bg-components-panel-bg overflow-y-auto'>
<div className={cn('sticky top-0 mx-8 pt-[27px] pb-2 mb-[18px] flex items-center bg-components-panel-bg z-20', scrolled && 'border-b')}>
<div className='shrink-0 text-text-primary title-2xl-semi-bold'>{activeItem?.name}</div>
{
activeItem?.description && (
<div className='shrink-0 ml-2 text-xs text-gray-600'>{activeItem?.description}</div>
)
}
<div className='grow flex justify-end'>
<div className='flex items-center justify-center -mr-4 w-6 h-6 cursor-pointer' onClick={onCancel}>
<RiCloseLine className='w-4 h-4 text-gray-400' />
</div>
<Input
showLeftIcon
wrapperClassName='!w-[200px]'
className='!h-8 !text-[13px]'
onChange={e => setSearchValue(e.target.value)}
value={searchValue}
/>
</div>
</div>
<div className='px-4 sm:px-8 pt-2'>
@ -228,6 +227,6 @@ export default function AccountSetting({
</div>
</div>
</div>
</Modal>
</MenuDialog>
)
}

View File

@ -0,0 +1,72 @@
import { Fragment, useCallback, useEffect } from 'react'
import type { ReactNode } from 'react'
import { RiCloseLine } from '@remixicon/react'
import { Dialog, Transition } from '@headlessui/react'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
type DialogProps = {
className?: string
children: ReactNode
show: boolean
onClose?: () => void
}
const MenuDialog = ({
className,
children,
show,
onClose,
}: DialogProps) => {
const close = useCallback(() => onClose?.(), [onClose])
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape')
close()
}
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [close])
return (
<Transition appear show={show} as={Fragment}>
<Dialog as="div" className="relative z-40" onClose={() => {}}>
<div className="fixed inset-0">
<div className="flex flex-col items-center justify-center min-h-full">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className={cn('grow relative w-full h-full p-0 overflow-hidden text-left align-middle transition-all transform bg-background-sidenav-bg backdrop-blur-md', className)}>
<div className='absolute right-0 top-0 h-full w-1/2 bg-components-panel-bg'/>
<div className='absolute top-6 right-6 flex flex-col items-center'>
<Button
variant='tertiary'
size='large'
className='px-2'
onClick={close}
>
<RiCloseLine className='w-5 h-5' />
</Button>
<div className='mt-1 text-text-tertiary system-2xs-medium-uppercase'>ESC</div>
</div>
{children}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition >
)
}
export default MenuDialog

View File

@ -1,8 +1,16 @@
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import {
RiAlertFill,
RiArrowDownSLine,
RiArrowRightUpLine,
RiBrainLine,
} from '@remixicon/react'
import { useContext } from 'use-context-selector'
import SystemModelSelector from './system-model-selector'
import ProviderAddedCard, { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card'
import ProviderCard from './provider-card'
// import ProviderCard from './provider-card'
import type {
CustomConfigurationModelFixedFields,
ModelProvider,
@ -17,10 +25,15 @@ import {
useUpdateModelList,
useUpdateModelProviders,
} from './hooks'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Divider from '@/app/components/base/divider'
import ProviderCard from '@/app/components/plugins/provider-card'
import I18n from '@/context/i18n'
import { useProviderContext } from '@/context/provider-context'
import { useModalContextSelector } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import cn from '@/utils/classnames'
import { extensionDallE, modelGPT4, toolNotion } from '@/app/components/plugins/card/card-mock'
const ModelProviderPage = () => {
const { t } = useTranslation()
@ -57,25 +70,25 @@ const ModelProviderPage = () => {
const handleOpenModal = (
provider: ModelProvider,
configurateMethod: ConfigurationMethodEnum,
configurationMethod: ConfigurationMethodEnum,
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
) => {
setShowModelModal({
payload: {
currentProvider: provider,
currentConfigurationMethod: configurateMethod,
currentConfigurationMethod: configurationMethod,
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
},
onSaveCallback: () => {
updateModelProviders()
if (configurateMethod === ConfigurationMethodEnum.predefinedModel) {
if (configurationMethod === ConfigurationMethodEnum.predefinedModel) {
provider.supported_model_types.forEach((type) => {
updateModelList(type)
})
}
if (configurateMethod === ConfigurationMethodEnum.customizableModel && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
if (configurationMethod === ConfigurationMethodEnum.customizableModel && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
eventEmitter?.emit({
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
payload: provider.provider,
@ -88,63 +101,95 @@ const ModelProviderPage = () => {
})
}
const [collapse, setCollapse] = useState(false)
const { locale } = useContext(I18n)
// TODO #Plugin list API#
const pluginList = [toolNotion, extensionDallE, modelGPT4]
return (
<div className='relative pt-1 -mt-2'>
<div className={`flex items-center justify-between mb-2 h-8 ${defaultModelNotConfigured && 'px-3 bg-[#FFFAEB] rounded-lg border border-[#FEF0C7]'}`}>
{
defaultModelNotConfigured
? (
<div className='flex items-center text-xs font-medium text-gray-700'>
<AlertTriangle className='mr-1 w-3 h-3 text-[#F79009]' />
{t('common.modelProvider.notConfigured')}
</div>
)
: <div className='text-sm font-medium text-gray-800'>{t('common.modelProvider.models')}</div>
}
<SystemModelSelector
textGenerationDefaultModel={textGenerationDefaultModel}
embeddingsDefaultModel={embeddingsDefaultModel}
rerankDefaultModel={rerankDefaultModel}
speech2textDefaultModel={speech2textDefaultModel}
ttsDefaultModel={ttsDefaultModel}
/>
<div className={cn('flex items-center mb-2')}>
<div className='grow text-text-primary system-md-semibold'>{t('common.modelProvider.models')}</div>
<div className={cn(
'shrink-0 relative flex items-center justify-end gap-2 p-0.5 rounded-lg border border-transparent',
defaultModelNotConfigured && 'pl-2 bg-components-panel-bg-blur border-components-panel-border shadow-xs',
)}>
{defaultModelNotConfigured && <div className='absolute top-0 bottom-0 right-0 left-0 opacity-40' style={{ background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)' }}/>}
{defaultModelNotConfigured && (
<div className='flex items-center gap-1 text-text-primary system-xs-medium'>
<RiAlertFill className='w-4 h-4 text-text-warning-secondary' />
{t('common.modelProvider.notConfigured')}
</div>
)}
<SystemModelSelector
notConfigured={defaultModelNotConfigured}
textGenerationDefaultModel={textGenerationDefaultModel}
embeddingsDefaultModel={embeddingsDefaultModel}
rerankDefaultModel={rerankDefaultModel}
speech2textDefaultModel={speech2textDefaultModel}
ttsDefaultModel={ttsDefaultModel}
/>
</div>
</div>
{
!!configuredProviders?.length && (
<div className='pb-3'>
{
configuredProviders?.map(provider => (
<ProviderAddedCard
key={provider.provider}
provider={provider}
onOpenModal={(configurateMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurateMethod, currentCustomConfigurationModelFixedFields)}
/>
))
}
{!configuredProviders?.length && (
<div className='mb-2 p-4 rounded-[10px]' style={{ background: 'linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%)' }}>
<div className='w-10 h-10 flex items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur'>
<RiBrainLine className='w-5 h-5 text-text-primary' />
</div>
)
}
{
!!notConfiguredProviders?.length && (
<>
<div className='flex items-center mb-2 text-xs font-semibold text-gray-500'>
+ {t('common.modelProvider.addMoreModelProvider')}
<span className='grow ml-3 h-[1px] bg-gradient-to-r from-[#f3f4f6]' />
</div>
<div className='grid grid-cols-3 gap-2'>
{
notConfiguredProviders?.map(provider => (
<ProviderCard
key={provider.provider}
provider={provider}
onOpenModal={(configurateMethod: ConfigurationMethodEnum) => handleOpenModal(provider, configurateMethod)}
/>
))
}
</div>
</>
)
}
<div className='mt-2 text-text-secondary system-sm-medium'>{t('common.modelProvider.emptyProviderTitle')}</div>
<div className='mt-1 text-text-tertiary system-xs-regular'>{t('common.modelProvider.emptyProviderTip')}</div>
</div>
)}
{!!configuredProviders?.length && (
<div className='relative'>
{configuredProviders?.map(provider => (
<ProviderAddedCard
key={provider.provider}
provider={provider}
onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields)}
/>
))}
</div>
)}
{false && !!notConfiguredProviders?.length && (
<>
<div className='flex items-center mb-2 pt-2 text-text-primary system-md-semibold'>{t('common.modelProvider.configureRequired')}</div>
<div className='relative'>
{notConfiguredProviders?.map(provider => (
<ProviderAddedCard
notConfigured
key={provider.provider}
provider={provider}
onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields)}
/>
))}
</div>
</>
)}
<div className='mb-2'>
<Divider className='!mt-4 h-px' />
<div className='flex items-center justify-between'>
<div className='flex items-center gap-1 text-text-primary system-md-semibold cursor-pointer' onClick={() => setCollapse(!collapse)}>
<RiArrowDownSLine className={cn('w-4 h-4', collapse && '-rotate-90')} />
{t('common.modelProvider.installProvider')}
</div>
<div className='flex items-center mb-2 pt-2'>
<span className='pr-1 text-text-tertiary system-sm-regular'>{t('common.modelProvider.discoverMore')}</span>
<Link target="_blank" href="/plugins" className='inline-flex items-center system-sm-medium text-text-accent'>
Dify Marketplace
<RiArrowRightUpLine className='w-4 h-4' />
</Link>
</div>
</div>
{!collapse && (
<div className='grid grid-cols-2 gap-2'>
{pluginList.map((plugin, index) => (
<ProviderCard key={index} locale={locale} payload={plugin as any} />
))}
</div>
)}
</div>
</div>
)
}

View File

@ -1,5 +1,6 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { RiEqualizer2Line } from '@remixicon/react'
import type { ModelProvider } from '../declarations'
import {
ConfigurationMethodEnum,
@ -14,7 +15,6 @@ import PrioritySelector from './priority-selector'
import PriorityUseTip from './priority-use-tip'
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './index'
import Indicator from '@/app/components/header/indicator'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button'
import { changeModelProviderPriority } from '@/service/common'
import { useToastContext } from '@/app/components/base/toast'
@ -69,7 +69,7 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
<div className='shrink-0 relative ml-1 p-1 w-[112px] rounded-lg bg-white/[0.3] border-[0.5px] border-black/5'>
<div className='flex items-center justify-between mb-1 pt-1 pl-2 pr-[7px] h-5 text-xs font-medium text-gray-500'>
API-KEY
<Indicator color={isCustomConfigured ? 'green' : 'gray'} />
<Indicator color={isCustomConfigured ? 'green' : 'red'} />
</div>
<div className='flex items-center gap-0.5'>
<Button
@ -77,7 +77,7 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
size='small'
onClick={onSetup}
>
<Settings01 className='mr-1 w-3 h-3' />
<RiEqualizer2Line className='mr-1 w-3.5 h-3.5' />
{t('common.operation.setup')}
</Button>
{

View File

@ -2,6 +2,8 @@ import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
RiInformation2Fill,
RiLoader2Line,
} from '@remixicon/react'
import type {
@ -21,7 +23,6 @@ import CredentialPanel from './credential-panel'
import QuotaPanel from './quota-panel'
import ModelList from './model-list'
import AddModelButton from './add-model-button'
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
import { fetchModelProviderModelList } from '@/service/common'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { IS_CE_EDITION } from '@/config'
@ -29,10 +30,12 @@ import { useAppContext } from '@/context/app-context'
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
type ProviderAddedCardProps = {
notConfigured?: boolean
provider: ModelProvider
onOpenModal: (configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
}
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
notConfigured,
provider,
onOpenModal,
}) => {
@ -47,6 +50,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
const hasModelList = fetched && !!modelList.length
const { isCurrentWorkspaceManager } = useAppContext()
const showQuota = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider) && !IS_CE_EDITION
const showCredential = configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && isCurrentWorkspaceManager
const getModelList = async (providerName: string) => {
if (loading)
@ -105,7 +109,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
)
}
{
configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && isCurrentWorkspaceManager && (
showCredential && (
<CredentialPanel
onSetup={() => onOpenModal(ConfigurationMethodEnum.predefinedModel)}
provider={provider}
@ -116,34 +120,45 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
{
collapsed && (
<div className='group flex items-center justify-between pl-2 py-1.5 pr-[11px] border-t border-t-black/5 bg-white/30 text-xs font-medium text-gray-500'>
<div className='group-hover:hidden pl-1 pr-1.5 h-6 leading-6'>
{
hasModelList
? t('common.modelProvider.modelsNum', { num: modelList.length })
: t('common.modelProvider.showModels')
}
</div>
<div
className='hidden group-hover:flex items-center pl-1 pr-1.5 h-6 rounded-lg hover:bg-white cursor-pointer'
onClick={handleOpenModelList}
>
<ChevronDownDouble className='mr-0.5 w-3 h-3' />
{
hasModelList
? t('common.modelProvider.showModelsNum', { num: modelList.length })
: t('common.modelProvider.showModels')
}
{
loading && (
<RiLoader2Line className='ml-0.5 animate-spin w-3 h-3' />
)
}
</div>
{(showQuota || !notConfigured) && (
<>
<div className='group-hover:hidden flex items-center pl-1 pr-1.5 h-6 leading-6'>
{
hasModelList
? t('common.modelProvider.modelsNum', { num: modelList.length })
: t('common.modelProvider.showModels')
}
{!loading && <RiArrowRightSLine className='w-4 h-4' />}
</div>
<div
className='hidden group-hover:flex items-center pl-1 pr-1.5 h-6 rounded-lg hover:bg-white cursor-pointer'
onClick={handleOpenModelList}
>
{
hasModelList
? t('common.modelProvider.showModelsNum', { num: modelList.length })
: t('common.modelProvider.showModels')
}
{!loading && <RiArrowRightSLine className='w-4 h-4' />}
{
loading && (
<RiLoader2Line className='ml-0.5 animate-spin w-3 h-3' />
)
}
</div>
</>
)}
{!showQuota && notConfigured && (
<div className='flex items-center pl-1 pr-1.5 h-6'>
<RiInformation2Fill className='mr-1 w-4 h-4 text-text-accent' />
<span>{t('common.modelProvider.configureTip')}</span>
</div>
)}
{
configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && isCurrentWorkspaceManager && (
<AddModelButton
onClick={() => onOpenModal(ConfigurationMethodEnum.customizableModel)}
className='hidden group-hover:flex group-hover:text-primary-600'
className='flex'
/>
)
}

View File

@ -1,6 +1,9 @@
import type { FC } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
} from '@remixicon/react'
import type {
CustomConfigurationModelFixedFields,
ModelItem,
@ -12,7 +15,6 @@ import {
// import Tab from './tab'
import AddModelButton from './add-model-button'
import ModelListItem from './model-list-item'
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
import { useModalContextSelector } from '@/context/modal-context'
import { useAppContext } from '@/context/app-context'
@ -51,15 +53,16 @@ const ModelList: FC<ModelListProps> = ({
<div className='py-1 bg-white rounded-lg'>
<div className='flex items-center pl-1 pr-[3px]'>
<span className='group shrink-0 flex items-center mr-2'>
<span className='group-hover:hidden pl-1 pr-1.5 h-6 leading-6 text-xs font-medium text-gray-500'>
<span className='group-hover:hidden inline-flex pl-1 pr-1.5 h-6 leading-6 text-xs font-medium text-gray-500'>
{t('common.modelProvider.modelsNum', { num: models.length })}
<RiArrowRightSLine className='mr-0.5 w-4 h-4 rotate-90' />
</span>
<span
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' />
{t('common.modelProvider.collapse')}
{t('common.modelProvider.modelsNum', { num: models.length })}
<RiArrowRightSLine className='mr-0.5 w-4 h-4 rotate-90' />
</span>
</span>
{/* {

View File

@ -1,6 +1,7 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiEqualizer2Line } from '@remixicon/react'
import ModelSelector from '../model-selector'
import {
useModelList,
@ -13,7 +14,6 @@ import type {
} from '../declarations'
import { ModelTypeEnum } from '../declarations'
import Tooltip from '@/app/components/base/tooltip'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
@ -31,6 +31,7 @@ type SystemModelSelectorProps = {
rerankDefaultModel: DefaultModelResponse | undefined
speech2textDefaultModel: DefaultModelResponse | undefined
ttsDefaultModel: DefaultModelResponse | undefined
notConfigured: boolean
}
const SystemModel: FC<SystemModelSelectorProps> = ({
textGenerationDefaultModel,
@ -38,6 +39,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
rerankDefaultModel,
speech2textDefaultModel,
ttsDefaultModel,
notConfigured,
}) => {
const { t } = useTranslation()
const { notify } = useToastContext()
@ -128,14 +130,13 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
}}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className={`
flex items-center px-2 h-6 text-xs text-gray-700 cursor-pointer bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs
hover:bg-gray-100 hover:shadow-none
${open && 'bg-gray-100 shadow-none'}
`}>
<Settings01 className='mr-1 w-3 h-3 text-gray-500' />
<Button
variant={notConfigured ? 'primary' : 'secondary'}
size='small'
>
<RiEqualizer2Line className='mr-1 w-3.5 h-3.5' />
{t('common.modelProvider.systemModelSettings')}
</div>
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='pt-4 w-[360px] rounded-xl border-[0.5px] border-black/5 bg-white shadow-xl'>

View File

@ -1,5 +1,5 @@
import type { FC } from 'react'
import React from 'react'
import type { FC } from 'react'
import { RiVerifiedBadgeLine } from '@remixicon/react'
import Badge from '../base/badge'
import type { Plugin } from './types'
@ -7,19 +7,20 @@ import Description from './card/base/description'
import Icon from './card/base/card-icon'
import Title from './card/base/title'
import DownloadCount from './card/base/download-count'
import { getLocaleOnServer } from '@/i18n/server'
import type { Locale } from '@/i18n'
import cn from '@/utils/classnames'
type Props = {
className?: string
locale: Locale // The component is used in both client and server side, so we can't get the locale from both side(getLocaleOnServer and useContext)
payload: Plugin
}
const PluginItem: FC<Props> = async ({
className,
locale,
payload,
}) => {
const locale = getLocaleOnServer()
const { org, label } = payload
return (

View File

@ -0,0 +1,74 @@
import React from 'react'
import type { FC } from 'react'
import { RiArrowRightUpLine, RiVerifiedBadgeLine } from '@remixicon/react'
import Badge from '../base/badge'
import type { Plugin } from './types'
import Description from './card/base/description'
import Icon from './card/base/card-icon'
import Title from './card/base/title'
import DownloadCount from './card/base/download-count'
import Button from '@/app/components/base/button'
import type { Locale } from '@/i18n'
import cn from '@/utils/classnames'
type Props = {
className?: string
locale: Locale // The component is used in both client and server side, so we can't get the locale from both side(getLocaleOnServer and useContext)
payload: Plugin
}
const ProviderCard: FC<Props> = ({
className,
locale,
payload,
}) => {
const { org, label } = payload
return (
<div className={cn('group relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs', className)}>
{/* Header */}
<div className="flex">
<Icon src={payload.icon} />
<div className="ml-3 w-0 grow">
<div className="flex items-center h-5">
<Title title={label[locale]} />
<RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />
</div>
<div className='mb-1 flex justify-between items-center h-4'>
<div className='flex items-center'>
<div className='text-text-tertiary system-xs-regular'>{org}</div>
<div className='mx-2 text-text-quaternary system-xs-regular'>·</div>
<DownloadCount downloadCount={payload.install_count || 0} />
</div>
</div>
</div>
</div>
<Description className='mt-3' text={payload.brief[locale]} descriptionLineRows={2}></Description>
<div className='mt-3 flex space-x-0.5'>
{['LLM', 'text embedding', 'speech2text'].map(tag => (
<Badge key={tag} text={tag} />
))}
</div>
<div
className='hidden group-hover:flex items-center gap-2 absolute bottom-0 left-0 right-0 p-4 pt-8'
style={{ background: 'linear-gradient(0deg, #F9FAFB 60.27%, rgba(249, 250, 251, 0.00) 100%)' }}
>
<Button
className='flex-grow'
variant='primary'
>
Install
</Button>
<Button
className='flex-grow'
variant='secondary'
>
Details
<RiArrowRightUpLine className='w-4 h-4' />
</Button>
</div>
</div>
)
}
export default ProviderCard

View File

@ -142,6 +142,7 @@ const translation = {
settings: {
accountGroup: 'ACCOUNT',
workplaceGroup: 'WORKSPACE',
generalGroup: 'GENERAL',
account: 'My account',
members: 'Members',
billing: 'Billing',
@ -275,7 +276,7 @@ const translation = {
},
},
modelProvider: {
notConfigured: 'The system model has not yet been fully configured, and some functions may be unavailable.',
notConfigured: 'The system model has not yet been fully configured',
systemModelSettings: 'System Model Settings',
systemModelSettingsLink: 'Why is it necessary to set up a system model?',
selectModel: 'Select your model',
@ -373,6 +374,12 @@ const translation = {
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.',
configureRequired: 'Configure required',
configureTip: 'Set up api-key or add model to use',
installProvider: 'Install model providers',
discoverMore: 'Discover more in ',
emptyProviderTitle: 'Model provider not set up',
emptyProviderTip: 'Please install a model provider first.',
},
dataSource: {
add: 'Add a data source',

View File

@ -142,6 +142,7 @@ const translation = {
settings: {
accountGroup: '账户',
workplaceGroup: '工作空间',
generalGroup: '通用',
account: '我的账户',
members: '成员',
billing: '账单',
@ -275,7 +276,7 @@ const translation = {
},
},
modelProvider: {
notConfigured: '系统模型尚未完全配置,部分功能可能无法使用。',
notConfigured: '系统模型尚未完全配置',
systemModelSettings: '系统模型设置',
systemModelSettingsLink: '为什么需要设置系统模型?',
selectModel: '选择您的模型',
@ -373,6 +374,12 @@ const translation = {
loadBalancingInfo: '默认情况下,负载平衡使用 Round-robin 策略。如果触发速率限制,将应用 1 分钟的冷却时间',
upgradeForLoadBalancing: '升级以解锁负载均衡功能',
apiKey: 'API 密钥',
configureRequired: '尚未配置',
configureTip: '请配置 API 密钥,添加模型。',
installProvider: '安装模型供应商',
discoverMore: '发现更多就在',
emptyProviderTitle: '尚未安装模型供应商',
emptyProviderTip: '请安装模型供应商。',
},
dataSource: {
add: '添加数据源',