Merge remote-tracking branch 'origin/feat/external-knowledge-api' into feat/external-knowledge-api
This commit is contained in:
commit
2a1cba9f4d
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { FC, SVGProps } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -203,12 +203,23 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
datasetId,
|
||||
}, apiParams => fetchDatasetRelatedApps(apiParams.datasetId))
|
||||
|
||||
const navigation = [
|
||||
{ name: t('common.datasetMenus.documents'), href: `/datasets/${datasetId}/documents`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
|
||||
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
|
||||
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
|
||||
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
|
||||
]
|
||||
const navigation = useMemo(() => {
|
||||
const baseNavigation = [
|
||||
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
|
||||
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
|
||||
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
|
||||
]
|
||||
|
||||
if (datasetRes?.provider !== 'external') {
|
||||
baseNavigation.unshift({
|
||||
name: t('common.datasetMenus.documents'),
|
||||
href: `/datasets/${datasetId}/documents`,
|
||||
icon: DocumentTextIcon,
|
||||
selectedIcon: DocumentTextSolidIcon,
|
||||
})
|
||||
}
|
||||
return baseNavigation
|
||||
}, [datasetRes?.provider, datasetId, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (datasetRes)
|
||||
@ -233,6 +244,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
|
||||
icon_background={datasetRes?.icon_background || '#F5F5F5'}
|
||||
desc={datasetRes?.description || '--'}
|
||||
isExternal={datasetRes?.provider === 'external'}
|
||||
navigation={navigation}
|
||||
extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => <ExtraInfo isMobile={mode === 'collapse'} relatedApps={relatedApps} /> : undefined}
|
||||
iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'}
|
||||
|
@ -8,6 +8,7 @@ import { useDebounceFn } from 'ahooks'
|
||||
import useSWR from 'swr'
|
||||
|
||||
// Components
|
||||
import ExternalAPIPanel from '../../components/datasets/external-api/external-api-panel'
|
||||
import Datasets from './Datasets'
|
||||
import DatasetFooter from './DatasetFooter'
|
||||
import ApiServer from './ApiServer'
|
||||
@ -16,6 +17,9 @@ import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import TagManagementModal from '@/app/components/base/tag-management'
|
||||
import TagFilter from '@/app/components/base/tag-management/filter'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context'
|
||||
|
||||
// Services
|
||||
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
|
||||
@ -30,6 +34,7 @@ const Container = () => {
|
||||
const router = useRouter()
|
||||
const { currentWorkspace } = useAppContext()
|
||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||
const [showExternalApiPanel, setShowExternalApiPanel] = useState(false)
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [
|
||||
@ -66,38 +71,49 @@ const Container = () => {
|
||||
useEffect(() => {
|
||||
if (currentWorkspace.role === 'normal')
|
||||
return router.replace('/apps')
|
||||
}, [currentWorkspace])
|
||||
}, [currentWorkspace, router])
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
|
||||
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
|
||||
<TabSliderNew
|
||||
value={activeTab}
|
||||
onChange={newActiveTab => setActiveTab(newActiveTab)}
|
||||
options={options}
|
||||
/>
|
||||
{activeTab === 'dataset' && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
|
||||
<SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
|
||||
</div>
|
||||
|
||||
{activeTab === 'dataset' && (
|
||||
<>
|
||||
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} />
|
||||
<DatasetFooter />
|
||||
{showTagManagementModal && (
|
||||
<TagManagementModal type='knowledge' show={showTagManagementModal} />
|
||||
<ExternalKnowledgeApiProvider>
|
||||
<div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
|
||||
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
|
||||
<TabSliderNew
|
||||
value={activeTab}
|
||||
onChange={newActiveTab => setActiveTab(newActiveTab)}
|
||||
options={options}
|
||||
/>
|
||||
{activeTab === 'dataset' && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
|
||||
<SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
|
||||
<div className="w-[1px] h-4 bg-divider-regular" />
|
||||
<Button
|
||||
className='gap-0.5 shadows-shadow-xs'
|
||||
onClick={() => setShowExternalApiPanel(true)}
|
||||
>
|
||||
<ApiConnectionMod className='w-4 h-4 text-components-button-secondary-text' />
|
||||
<div className='flex px-0.5 justify-center items-center gap-1 text-components-button-secondary-text system-sm-medium'>{t('dataset.externalAPI')}</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
|
||||
</div>
|
||||
|
||||
{activeTab === 'api' && data && <Doc apiBaseUrl={data.api_base_url || ''} />}
|
||||
</div>
|
||||
{activeTab === 'dataset' && (
|
||||
<>
|
||||
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} />
|
||||
<DatasetFooter />
|
||||
{showTagManagementModal && (
|
||||
<TagManagementModal type='knowledge' show={showTagManagementModal} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'api' && data && <Doc apiBaseUrl={data.api_base_url || ''} />}
|
||||
|
||||
{showExternalApiPanel && <ExternalAPIPanel onClose={() => setShowExternalApiPanel(false)} isShow={showExternalApiPanel} datasetBindings={[]} />}
|
||||
</div>
|
||||
</ExternalKnowledgeApiProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ import Divider from '@/app/components/base/divider'
|
||||
import RenameDatasetModal from '@/app/components/datasets/rename-modal'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
import CornerLabel from '@/app/components/base/corner-label'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
export type DatasetCardProps = {
|
||||
@ -32,6 +33,7 @@ const DatasetCard = ({
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { push } = useRouter()
|
||||
const EXTERNAL_PROVIDER = 'external' as const
|
||||
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const [tags, setTags] = useState<Tag[]>(dataset.tags)
|
||||
@ -39,6 +41,7 @@ const DatasetCard = ({
|
||||
const [showRenameModal, setShowRenameModal] = useState(false)
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [confirmMessage, setConfirmMessage] = useState<string>('')
|
||||
const isExternalProvider = (provider: string): boolean => provider === EXTERNAL_PROVIDER
|
||||
const detectIsUsedByApp = useCallback(async () => {
|
||||
try {
|
||||
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
|
||||
@ -108,13 +111,16 @@ const DatasetCard = ({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='group col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
|
||||
className='group relative col-span-1 bg-white border-[0.5px] border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
|
||||
data-disable-nprogress={true}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
push(`/datasets/${dataset.id}/documents`)
|
||||
isExternalProvider(dataset.provider)
|
||||
? push(`/datasets/${dataset.id}/hitTesting`)
|
||||
: push(`/datasets/${dataset.id}/documents`)
|
||||
}}
|
||||
>
|
||||
{isExternalProvider(dataset.provider) && <CornerLabel label='External' className='absolute right-0' labelClassName='rounded-tr-xl' />}
|
||||
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
|
||||
<div className={cn(
|
||||
'shrink-0 flex items-center justify-center p-2.5 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]',
|
||||
@ -138,11 +144,18 @@ const DatasetCard = ({
|
||||
className={cn('truncate', (!dataset.embedding_available || !dataset.document_count) && 'opacity-50')}
|
||||
title={`${dataset.document_count}${t('dataset.documentCount')} · ${Math.round(dataset.word_count / 1000)}${t('dataset.wordCount')} · ${dataset.app_count}${t('dataset.appCount')}`}
|
||||
>
|
||||
<span>{dataset.document_count}{t('dataset.documentCount')}</span>
|
||||
<span className='shrink-0 mx-0.5 w-1 text-gray-400'>·</span>
|
||||
<span>{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}</span>
|
||||
<span className='shrink-0 mx-0.5 w-1 text-gray-400'>·</span>
|
||||
<span>{dataset.app_count}{t('dataset.appCount')}</span>
|
||||
{dataset.provider === 'external'
|
||||
? <>
|
||||
<span>{dataset.app_count}{t('dataset.appCount')}</span>
|
||||
</>
|
||||
: <>
|
||||
<span>{dataset.document_count}{t('dataset.documentCount')}</span>
|
||||
<span className='shrink-0 mx-0.5 w-1 text-gray-400'>·</span>
|
||||
<span>{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}</span>
|
||||
<span className='shrink-0 mx-0.5 w-1 text-gray-400'>·</span>
|
||||
<span>{dataset.app_count}{t('dataset.appCount')}</span>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,21 +4,31 @@ import { forwardRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiArrowRightLine,
|
||||
} from '@remixicon/react'
|
||||
|
||||
const CreateAppCard = forwardRef<HTMLAnchorElement>((_, ref) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<a ref={ref} className='group flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-white hover:shadow-lg' href='/datasets/create'>
|
||||
<div className='shrink-0 flex items-center p-4 pb-3'>
|
||||
<div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg'>
|
||||
<RiAddLine className='w-4 h-4 text-gray-500'/>
|
||||
<div className='flex flex-col bg-background-default-dimm border-[0.5px] border-components-panel-border rounded-xl
|
||||
min-h-[160px] transition-all duration-200 ease-in-out'
|
||||
>
|
||||
<a ref={ref} className='group flex flex-grow items-start p-4 pb-2 cursor-pointer' href='/datasets/create'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='w-10 h-10 p-2 flex items-center justify-center border border-dashed border-divider-regular rounded-lg
|
||||
bg-background-default-lighter group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge'
|
||||
>
|
||||
<RiAddLine className='w-4 h-4 text-text-tertiary group-hover:text-text-accent'/>
|
||||
</div>
|
||||
<div className='system-md-semibold text-text-secondary group-hover:text-text-accent'>{t('dataset.createDataset')}</div>
|
||||
</div>
|
||||
<div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('dataset.createDataset')}</div>
|
||||
</div>
|
||||
<div className='mb-1 px-4 text-xs leading-normal text-gray-500 line-clamp-4'>{t('dataset.createDatasetIntro')}</div>
|
||||
</a>
|
||||
</a>
|
||||
<a className='group flex p-4 items-center gap-1 border-t-[0.5px] border-divider-subtle rounded-b-xl cursor-pointer' href='/datasets/connect'>
|
||||
<div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div>
|
||||
<RiArrowRightLine className='w-3.5 h-3.5 text-text-tertiary group-hover:text-text-accent' />
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
|
13
web/app/(commonLayout)/datasets/connect/page.tsx
Normal file
13
web/app/(commonLayout)/datasets/connect/page.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import ExternalKnowledgeBaseConnector from '@/app/components/datasets/external-knowledge-base/connector'
|
||||
import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context'
|
||||
|
||||
const ExternalKnowledgeBaseCreation = async () => {
|
||||
return (
|
||||
<ExternalKnowledgeApiProvider>
|
||||
<ExternalKnowledgeBaseConnector />
|
||||
</ExternalKnowledgeApiProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExternalKnowledgeBaseCreation
|
@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '../base/app-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
@ -6,6 +7,7 @@ export type IAppBasicProps = {
|
||||
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
|
||||
icon?: string
|
||||
icon_background?: string | null
|
||||
isExternal?: boolean
|
||||
name: string
|
||||
type: string | React.ReactNode
|
||||
hoverTip?: string
|
||||
@ -52,7 +54,9 @@ const ICON_MAP = {
|
||||
notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />,
|
||||
}
|
||||
|
||||
export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, mode = 'expand', iconType = 'app' }: IAppBasicProps) {
|
||||
export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, mode = 'expand', iconType = 'app' }: IAppBasicProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex items-start p-1">
|
||||
{icon && icon_background && iconType === 'app' && (
|
||||
@ -83,6 +87,7 @@ export default function AppBasic({ icon, icon_background, name, type, hoverTip,
|
||||
}
|
||||
</div>
|
||||
<div className={`text-xs font-normal text-gray-500 group-hover:text-gray-700 break-all ${textStyle?.extra ?? ''}`}>{type}</div>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? t('dataset.externalTag') : ''}</div>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
|
@ -15,6 +15,7 @@ export type IAppDetailNavProps = {
|
||||
iconType?: 'app' | 'dataset' | 'notion'
|
||||
title: string
|
||||
desc: string
|
||||
isExternal?: boolean
|
||||
icon: string
|
||||
icon_background: string
|
||||
navigation: Array<{
|
||||
@ -26,7 +27,7 @@ export type IAppDetailNavProps = {
|
||||
extraInfo?: (modeState: string) => React.ReactNode
|
||||
}
|
||||
|
||||
const AppDetailNav = ({ title, desc, icon, icon_background, navigation, extraInfo, iconType = 'app' }: IAppDetailNavProps) => {
|
||||
const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigation, extraInfo, iconType = 'app' }: IAppDetailNavProps) => {
|
||||
const { appSidebarExpand, setAppSiderbarExpand } = useAppStore(useShallow(state => ({
|
||||
appSidebarExpand: state.appSidebarExpand,
|
||||
setAppSiderbarExpand: state.setAppSiderbarExpand,
|
||||
@ -70,6 +71,7 @@ const AppDetailNav = ({ title, desc, icon, icon_background, navigation, extraInf
|
||||
icon_background={icon_background}
|
||||
name={title}
|
||||
type={desc}
|
||||
isExternal={isExternal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -174,6 +174,20 @@ const ConfigContent: FC<Props> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
selectedDatasetsMode.mixtureInternalAndExternal && (
|
||||
<div className='mt-4 system-xs-medium text-text-warning'>
|
||||
{t('dataset.mixtureInternalAndExternalTip')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
selectedDatasetsMode.allExternal && (
|
||||
<div className='mt-4 system-xs-medium text-text-warning'>
|
||||
{t('dataset.allExternalTip')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
selectedDatasetsMode.mixtureHighQualityAndEconomic
|
||||
&& (
|
||||
@ -229,15 +243,15 @@ const ConfigContent: FC<Props> = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className='ml-2 leading-[32px] text-[13px] font-medium text-gray-900'>{t('common.modelProvider.rerankModel.key')}</div>
|
||||
<div className='leading-[32px] text-text-secondary system-sm-semibold'>{t('common.modelProvider.rerankModel.key')}</div>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className="w-[200px]">
|
||||
{t('common.modelProvider.rerankModel.tip')}
|
||||
</div>
|
||||
}
|
||||
popupClassName='ml-0.5'
|
||||
triggerClassName='ml-0.5 w-3.5 h-3.5'
|
||||
popupClassName='ml-1'
|
||||
triggerClassName='ml-1 w-4 h-4'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -47,7 +47,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
const { data, has_more } = await fetchDatasets({ url: '/datasets', params: { page } })
|
||||
setPage(getPage() + 1)
|
||||
setIsNoMore(!has_more)
|
||||
const newList = [...(datasets || []), ...data.filter(item => item.indexing_technique)]
|
||||
const newList = [...(datasets || []), ...data.filter(item => item.indexing_technique || item.provider === 'external')]
|
||||
setDataSets(newList)
|
||||
setLoaded(true)
|
||||
if (!selected.find(item => !item.name))
|
||||
@ -145,6 +145,11 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
item.provider === 'external' && (
|
||||
<Badge text={t('dataset.externalTag')} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -5,8 +5,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import { isEqual } from 'lodash-es'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { BookOpenIcon } from '@heroicons/react/24/outline'
|
||||
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import cn from '@/utils/classnames'
|
||||
import IndexMethodRadio from '@/app/components/datasets/settings/index-method-radio'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
@ -14,6 +16,7 @@ import { updateDatasetSetting } from '@/service/datasets'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import RetrievalSettings from '@/app/components/datasets/external-knowledge-base/create/RetrievalSettings'
|
||||
import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
|
||||
import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
|
||||
import { ensureRerankModelSelected, isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
|
||||
@ -56,6 +59,8 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const ref = useRef(null)
|
||||
const [topK, setTopK] = useState(currentDataset?.external_retrieval_model.top_k ?? 2)
|
||||
const [scoreThreshold, setScoreThreshold] = useState(currentDataset?.external_retrieval_model.score_threshold ?? 0.5)
|
||||
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const [loading, setLoading] = useState(false)
|
||||
@ -73,6 +78,13 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
const [isHideChangedTip, setIsHideChangedTip] = useState(false)
|
||||
const isRetrievalChanged = !isEqual(retrievalConfig, localeCurrentDataset?.retrieval_model_dict) || indexMethod !== localeCurrentDataset?.indexing_technique
|
||||
|
||||
const handleSettingsChange = (data: { top_k?: number; score_threshold?: number }) => {
|
||||
if (data.top_k !== undefined)
|
||||
setTopK(data.top_k)
|
||||
if (data.score_threshold !== undefined)
|
||||
setScoreThreshold(data.score_threshold)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (loading)
|
||||
return
|
||||
@ -107,10 +119,16 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
description,
|
||||
permission,
|
||||
indexing_technique: indexMethod,
|
||||
external_retrieval_model: {
|
||||
top_k: topK,
|
||||
score_threshold: scoreThreshold,
|
||||
},
|
||||
retrieval_model: {
|
||||
...postRetrievalConfig,
|
||||
score_threshold: postRetrievalConfig.score_threshold_enabled ? postRetrievalConfig.score_threshold : 0,
|
||||
},
|
||||
external_knowledge_id: currentDataset!.external_knowledge_info.external_knowledge_id,
|
||||
external_knowledge_api_id: currentDataset!.external_knowledge_info.external_knowledge_api_id,
|
||||
embedding_model: localeCurrentDataset.embedding_model,
|
||||
embedding_model_provider: localeCurrentDataset.embedding_model_provider,
|
||||
},
|
||||
@ -178,7 +196,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
}}>
|
||||
<div className={cn(rowClass, 'items-center')}>
|
||||
<div className={labelClass}>
|
||||
{t('datasetSettings.form.name')}
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.name')}</div>
|
||||
</div>
|
||||
<input
|
||||
value={localeCurrentDataset.name}
|
||||
@ -189,7 +207,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
</div>
|
||||
<div className={cn(rowClass)}>
|
||||
<div className={labelClass}>
|
||||
{t('datasetSettings.form.desc')}
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.desc')}</div>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<textarea
|
||||
@ -206,7 +224,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
</div>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div>{t('datasetSettings.form.permissions')}</div>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.permissions')}</div>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<PermissionSelector
|
||||
@ -219,24 +237,25 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-0 border-b-[0.5px] border-b-gray-200 my-2"></div>
|
||||
<div className={cn(rowClass)}>
|
||||
<div className={labelClass}>
|
||||
{t('datasetSettings.form.indexMethod')}
|
||||
{currentDataset && currentDataset.indexing_technique && (
|
||||
<div className={cn(rowClass)}>
|
||||
<div className={labelClass}>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.indexMethod')}</div>
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<IndexMethodRadio
|
||||
disable={!localeCurrentDataset?.embedding_available}
|
||||
value={indexMethod}
|
||||
onChange={v => setIndexMethod(v!)}
|
||||
itemClassName='sm:!w-[280px]'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<IndexMethodRadio
|
||||
disable={!localeCurrentDataset?.embedding_available}
|
||||
value={indexMethod}
|
||||
onChange={v => setIndexMethod(v!)}
|
||||
itemClassName='sm:!w-[280px]'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{indexMethod === 'high_quality' && (
|
||||
<div className={cn(rowClass)}>
|
||||
<div className={labelClass}>
|
||||
{t('datasetSettings.form.embeddingModel')}
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.embeddingModel')}</div>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<div className='w-full h-9 rounded-lg bg-gray-100 opacity-60'>
|
||||
@ -258,32 +277,74 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
)}
|
||||
|
||||
{/* Retrieval Method Config */}
|
||||
<div className={rowClass}>
|
||||
<div className={cn(labelClass, 'w-auto min-w-[168px]')}>
|
||||
<div>
|
||||
<div>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-500'>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
{t('datasetSettings.form.retrievalSetting.description')}
|
||||
{currentDataset?.provider === 'external'
|
||||
? <>
|
||||
<div className={rowClass}><Divider/></div>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
</div>
|
||||
<RetrievalSettings
|
||||
topK={topK}
|
||||
scoreThreshold={scoreThreshold}
|
||||
onChange={handleSettingsChange}
|
||||
isInRetrievalSetting={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClass}><Divider/></div>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.externalKnowledgeAPI')}</div>
|
||||
</div>
|
||||
<div className='w-full max-w-[480px]'>
|
||||
<div className='flex h-full px-3 py-2 items-center gap-1 rounded-lg bg-components-input-bg-normal'>
|
||||
<ApiConnectionMod className='w-4 h-4 text-text-secondary' />
|
||||
<div className='overflow-hidden text-text-secondary text-ellipsis system-sm-medium'>
|
||||
{currentDataset?.external_knowledge_info.external_knowledge_api_name}
|
||||
</div>
|
||||
<div className='text-text-tertiary system-xs-regular'>·</div>
|
||||
<div className='text-text-tertiary system-xs-regular'>{currentDataset?.external_knowledge_info.external_knowledge_api_endpoint}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{indexMethod === 'high_quality'
|
||||
? (
|
||||
<RetrievalMethodConfig
|
||||
value={retrievalConfig}
|
||||
onChange={setRetrievalConfig}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<EconomicalRetrievalMethodConfig
|
||||
value={retrievalConfig}
|
||||
onChange={setRetrievalConfig}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.externalKnowledgeID')}</div>
|
||||
</div>
|
||||
<div className='w-full max-w-[480px]'>
|
||||
<div className='flex h-full px-3 py-2 items-center gap-1 rounded-lg bg-components-input-bg-normal'>
|
||||
<div className='text-text-tertiary system-xs-regular'>{currentDataset?.external_knowledge_info.external_knowledge_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={rowClass}><Divider/></div>
|
||||
</>
|
||||
: <div className={rowClass}>
|
||||
<div className={cn(labelClass, 'w-auto min-w-[168px]')}>
|
||||
<div>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-500'>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
{t('datasetSettings.form.retrievalSetting.description')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{indexMethod === 'high_quality'
|
||||
? (
|
||||
<RetrievalMethodConfig
|
||||
value={retrievalConfig}
|
||||
onChange={setRetrievalConfig}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<EconomicalRetrievalMethodConfig
|
||||
value={retrievalConfig}
|
||||
onChange={setRetrievalConfig}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
{isRetrievalChanged && !isHideChangedTip && (
|
||||
<div className='absolute z-10 left-[30px] right-[30px] bottom-[76px] flex h-10 items-center px-3 rounded-lg border border-[#FEF0C7] bg-[#FFFAEB] shadow-lg justify-between'>
|
||||
|
21
web/app/components/base/corner-label/index.tsx
Normal file
21
web/app/components/base/corner-label/index.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Corner } from '../icons/src/vender/solid/shapes'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type CornerLabelProps = {
|
||||
label: string
|
||||
className?: string
|
||||
labelClassName?: string
|
||||
}
|
||||
|
||||
const CornerLabel: React.FC<CornerLabelProps> = ({ label, className, labelClassName }) => {
|
||||
return (
|
||||
<div className={cn('group/corner-label inline-flex items-start', className)}>
|
||||
<Corner className='w-[13px] h-5 text-background-section group-hover/corner-label:text-background-section-burn' />
|
||||
<div className={cn('flex py-1 pr-2 items-center gap-0.5 bg-background-section group-hover/corner-label:bg-background-section-burn', labelClassName)}>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{label}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CornerLabel
|
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon L">
|
||||
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M7.99996 3.33333C5.42263 3.33333 3.33329 5.42267 3.33329 8C3.33329 10.5773 5.42263 12.6667 7.99996 12.6667C9.72643 12.6667 11.2348 11.7295 12.0427 10.3329C12.227 10.0141 12.6349 9.90523 12.9536 10.0896C13.2723 10.274 13.3812 10.6818 13.1968 11.0005C12.1604 12.7921 10.2216 14 7.99996 14C4.91159 14 2.36821 11.6666 2.03658 8.66667H1.33329C0.965103 8.66667 0.666626 8.36819 0.666626 8C0.666626 7.63181 0.965103 7.33333 1.33329 7.33333H2.03658C2.36821 4.33337 4.91159 2 7.99996 2C10.2216 2 12.1604 3.20785 13.1968 4.99952C13.3812 5.31823 13.2723 5.72605 12.9536 5.91041C12.6349 6.09477 12.227 5.98585 12.0427 5.66714C11.2348 4.27054 9.72643 3.33333 7.99996 3.33333ZM7.99996 6C6.89539 6 5.99996 6.89543 5.99996 8C5.99996 9.10455 6.89539 10 7.99996 10C9.1045 10 9.99996 9.10454 9.99996 8C9.99996 6.89543 9.10451 6 7.99996 6ZM4.66663 8C4.66663 6.15905 6.15901 4.66667 7.99996 4.66667C9.61257 4.66667 10.9578 5.81184 11.2666 7.33333H14.6666C15.0348 7.33333 15.3333 7.63181 15.3333 8C15.3333 8.36819 15.0348 8.66667 14.6666 8.66667H11.2666C10.9578 10.1881 9.61257 11.3333 7.99996 11.3333C6.159 11.3333 4.66663 9.84092 4.66663 8Z" fill="#354052"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="13" height="20" viewBox="0 0 13 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Shape" d="M0 0H13V20C9.98017 20 7.26458 18.1615 6.14305 15.3576L0 0Z" fill="#F9FAFB"/>
|
||||
</svg>
|
After Width: | Height: | Size: 200 B |
@ -0,0 +1,38 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Icon L"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M7.99996 3.33333C5.42263 3.33333 3.33329 5.42267 3.33329 8C3.33329 10.5773 5.42263 12.6667 7.99996 12.6667C9.72643 12.6667 11.2348 11.7295 12.0427 10.3329C12.227 10.0141 12.6349 9.90523 12.9536 10.0896C13.2723 10.274 13.3812 10.6818 13.1968 11.0005C12.1604 12.7921 10.2216 14 7.99996 14C4.91159 14 2.36821 11.6666 2.03658 8.66667H1.33329C0.965103 8.66667 0.666626 8.36819 0.666626 8C0.666626 7.63181 0.965103 7.33333 1.33329 7.33333H2.03658C2.36821 4.33337 4.91159 2 7.99996 2C10.2216 2 12.1604 3.20785 13.1968 4.99952C13.3812 5.31823 13.2723 5.72605 12.9536 5.91041C12.6349 6.09477 12.227 5.98585 12.0427 5.66714C11.2348 4.27054 9.72643 3.33333 7.99996 3.33333ZM7.99996 6C6.89539 6 5.99996 6.89543 5.99996 8C5.99996 9.10455 6.89539 10 7.99996 10C9.1045 10 9.99996 9.10454 9.99996 8C9.99996 6.89543 9.10451 6 7.99996 6ZM4.66663 8C4.66663 6.15905 6.15901 4.66667 7.99996 4.66667C9.61257 4.66667 10.9578 5.81184 11.2666 7.33333H14.6666C15.0348 7.33333 15.3333 7.63181 15.3333 8C15.3333 8.36819 15.0348 8.66667 14.6666 8.66667H11.2666C10.9578 10.1881 9.61257 11.3333 7.99996 11.3333C6.159 11.3333 4.66663 9.84092 4.66663 8Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "ApiConnectionMod"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './ApiConnectionMod.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 = 'ApiConnectionMod'
|
||||
|
||||
export default Icon
|
@ -1,3 +1,4 @@
|
||||
export { default as ApiConnectionMod } from './ApiConnectionMod'
|
||||
export { default as ApiConnection } from './ApiConnection'
|
||||
export { default as BarChartSquare02 } from './BarChartSquare02'
|
||||
export { default as Container } from './Container'
|
||||
|
@ -0,0 +1,27 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "13",
|
||||
"height": "20",
|
||||
"viewBox": "0 0 13 20",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Shape",
|
||||
"d": "M0 0H13V20C9.98017 20 7.26458 18.1615 6.14305 15.3576L0 0Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Corner"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Corner.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 = 'Corner'
|
||||
|
||||
export default Icon
|
@ -1,2 +1,3 @@
|
||||
export { default as Corner } from './Corner'
|
||||
export { default as Star04 } from './Star04'
|
||||
export { default as Star06 } from './Star06'
|
||||
|
@ -3,5 +3,5 @@
|
||||
}
|
||||
|
||||
.modal-panel {
|
||||
@apply w-full max-w-md transform rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all;
|
||||
@apply w-full max-w-[480px] transform rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all;
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ const ParamItem: FC<Props> = ({ className, id, name, noTooltip, tip, step = 0.1,
|
||||
<span className="mx-1 text-gray-900 text-[13px] leading-[18px] font-medium">{name}</span>
|
||||
{!noTooltip && (
|
||||
<Tooltip
|
||||
triggerClassName='w-4 h-4 shrink-0'
|
||||
popupContent={<div className="w-[200px]">{tip}</div>}
|
||||
/>
|
||||
)}
|
||||
|
@ -87,7 +87,7 @@ const Select: FC<ISelectProps> = ({
|
||||
<div className='group text-gray-800'>
|
||||
{allowSearch
|
||||
? <Combobox.Input
|
||||
className={`w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-not-allowed`}
|
||||
className={`w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
onChange={(event) => {
|
||||
if (!disabled)
|
||||
setQuery(event.target.value)
|
||||
|
@ -36,6 +36,12 @@ export type UsageScene = 'doc' | 'hitTesting'
|
||||
type ISegmentCardProps = {
|
||||
loading: boolean
|
||||
detail?: SegmentDetailModel & { document: { name: string } }
|
||||
contentExternal?: string
|
||||
refSource?: {
|
||||
title: string
|
||||
uri: string
|
||||
}
|
||||
isExternal?: boolean
|
||||
score?: number
|
||||
onClick?: () => void
|
||||
onChangeSwitch?: (segId: string, enabled: boolean) => Promise<void>
|
||||
@ -48,6 +54,8 @@ type ISegmentCardProps = {
|
||||
|
||||
const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
detail = {},
|
||||
contentExternal,
|
||||
refSource,
|
||||
score,
|
||||
onClick,
|
||||
onChangeSwitch,
|
||||
@ -88,6 +96,9 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (contentExternal)
|
||||
return contentExternal
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
@ -201,8 +212,8 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
<Divider />
|
||||
<div className="relative flex items-center w-full">
|
||||
<DocumentTitle
|
||||
name={detail?.document?.name || ''}
|
||||
extension={(detail?.document?.name || '').split('.').pop() || 'txt'}
|
||||
name={detail?.document?.name || refSource?.title || ''}
|
||||
extension={(detail?.document?.name || refSource?.title || '').split('.').pop() || 'txt'}
|
||||
wrapperCls='w-full'
|
||||
iconCls="!h-4 !w-4 !bg-contain"
|
||||
textCls="text-xs text-gray-700 !font-normal overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
|
16
web/app/components/datasets/external-api/declarations.ts
Normal file
16
web/app/components/datasets/external-api/declarations.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export type CreateExternalAPIReq = {
|
||||
name: string
|
||||
settings: {
|
||||
endpoint: string
|
||||
api_key: string
|
||||
}
|
||||
}
|
||||
|
||||
export type FormSchema = {
|
||||
variable: string
|
||||
type: 'text' | 'secret'
|
||||
label: {
|
||||
[key: string]: string
|
||||
}
|
||||
required: boolean
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
import React, { useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiBookOpenLine } from '@remixicon/react'
|
||||
import type { CreateExternalAPIReq, FormSchema } from '../declarations'
|
||||
import Input from '@/app/components/base/input'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type FormProps = {
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
fieldLabelClassName?: string
|
||||
value: CreateExternalAPIReq
|
||||
onChange: (val: CreateExternalAPIReq) => void
|
||||
formSchemas: FormSchema[]
|
||||
inputClassName?: string
|
||||
}
|
||||
|
||||
const Form: FC<FormProps> = React.memo(({
|
||||
className,
|
||||
itemClassName,
|
||||
fieldLabelClassName,
|
||||
value,
|
||||
onChange,
|
||||
formSchemas,
|
||||
inputClassName,
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const [changeKey, setChangeKey] = useState('')
|
||||
|
||||
const handleFormChange = (key: string, val: string) => {
|
||||
setChangeKey(key)
|
||||
if (key === 'name') {
|
||||
onChange({ ...value, [key]: val })
|
||||
}
|
||||
else {
|
||||
onChange({
|
||||
...value,
|
||||
settings: {
|
||||
...value.settings,
|
||||
[key]: val,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const renderField = (formSchema: FormSchema) => {
|
||||
const { variable, type, label, required } = formSchema
|
||||
const fieldValue = variable === 'name' ? value[variable] : (value.settings[variable as keyof typeof value.settings] || '')
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'flex flex-col items-start gap-1 self-stretch')}>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<label className={cn(fieldLabelClassName, 'text-text-secondary system-sm-semibold')} htmlFor={variable}>
|
||||
{label[i18n.language] || label.en_US}
|
||||
{required && <span className='ml-1 text-red-500'>*</span>}
|
||||
</label>
|
||||
{variable === 'endpoint' && (
|
||||
<a
|
||||
href={'https://docs.dify.ai/guides/knowledge-base/external-knowledge-api-documentation' || '/'}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-text-accent body-xs-regular flex items-center'
|
||||
>
|
||||
<RiBookOpenLine className='w-3 h-3 text-text-accent mr-1' />
|
||||
{t('dataset.externalAPIPanelDocumentation')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
type={type === 'secret' ? 'password' : 'text'}
|
||||
id={variable}
|
||||
name={variable}
|
||||
value={fieldValue}
|
||||
onChange={val => handleFormChange(variable, val.target.value)}
|
||||
required={required}
|
||||
className={cn(inputClassName)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form className={cn('flex flex-col justify-center items-start gap-4 self-stretch', className)}>
|
||||
{formSchemas.map(formSchema => renderField(formSchema))}
|
||||
</form>
|
||||
)
|
||||
})
|
||||
|
||||
export default Form
|
@ -0,0 +1,218 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiBook2Line,
|
||||
RiCloseLine,
|
||||
RiInformation2Line,
|
||||
RiLock2Fill,
|
||||
} from '@remixicon/react'
|
||||
import type { CreateExternalAPIReq, FormSchema } from '../declarations'
|
||||
import Form from './Form'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { createExternalAPI } from '@/service/datasets'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type AddExternalAPIModalProps = {
|
||||
data?: CreateExternalAPIReq
|
||||
onSave: (formValue: CreateExternalAPIReq) => void
|
||||
onCancel: () => void
|
||||
onEdit?: (formValue: CreateExternalAPIReq) => Promise<void>
|
||||
datasetBindings?: { id: string; name: string }[]
|
||||
isEditMode: boolean
|
||||
}
|
||||
|
||||
const formSchemas: FormSchema[] = [
|
||||
{
|
||||
variable: 'name',
|
||||
type: 'text',
|
||||
label: {
|
||||
en_US: 'Name',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
variable: 'endpoint',
|
||||
type: 'text',
|
||||
label: {
|
||||
en_US: 'API Endpoint',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
variable: 'api_key',
|
||||
type: 'secret',
|
||||
label: {
|
||||
en_US: 'API Key',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ data, onSave, onCancel, datasetBindings, isEditMode, onEdit }) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [formData, setFormData] = useState<CreateExternalAPIReq>({ name: '', settings: { endpoint: '', api_key: '' } })
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && data)
|
||||
setFormData(data)
|
||||
}, [isEditMode, data])
|
||||
|
||||
const hasEmptyInputs = Object.values(formData).some(value =>
|
||||
typeof value === 'string' ? value.trim() === '' : Object.values(value).some(v => v.trim() === ''),
|
||||
)
|
||||
const handleDataChange = (val: CreateExternalAPIReq) => {
|
||||
setFormData(val)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (formData && formData.settings.api_key && formData.settings.api_key?.length < 5) {
|
||||
notify({ type: 'error', message: t('common.apiBasedExtension.modal.apiKey.lengthError') })
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
setLoading(true)
|
||||
if (isEditMode && onEdit) {
|
||||
await onEdit(
|
||||
{
|
||||
...formData,
|
||||
settings: { ...formData.settings, api_key: formData.settings.api_key ? '[__HIDDEN__]' : formData.settings.api_key },
|
||||
},
|
||||
)
|
||||
notify({ type: 'success', message: 'External API updated successfully' })
|
||||
}
|
||||
else {
|
||||
const res = await createExternalAPI({ body: formData })
|
||||
if (res && res.id) {
|
||||
notify({ type: 'success', message: 'External API saved successfully' })
|
||||
onSave(res)
|
||||
}
|
||||
}
|
||||
onCancel()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error saving/updating external API:', error)
|
||||
notify({ type: 'error', message: 'Failed to save/update External API' })
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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='flex relative w-[480px] flex-col items-start bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadows-shadow-xl'>
|
||||
<div className='flex flex-col pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
|
||||
<div className='self-stretch text-text-primary title-2xl-semi-bold flex-grow'>
|
||||
{
|
||||
isEditMode ? t('dataset.editExternalAPIFormTitle') : t('dataset.createExternalAPI')
|
||||
}
|
||||
</div>
|
||||
{isEditMode && (datasetBindings?.length ?? 0) > 0 && (
|
||||
<div className='text-text-tertiary system-xs-regular flex items-center'>
|
||||
{t('dataset.editExternalAPIFormWarning.front')}
|
||||
<span className='text-text-accent cursor-pointer flex items-center'>
|
||||
{datasetBindings?.length} {t('dataset.editExternalAPIFormWarning.end')}
|
||||
<Tooltip
|
||||
popupClassName='flex items-center self-stretch w-[320px]'
|
||||
popupContent={
|
||||
<div className='p-1'>
|
||||
<div className='flex pt-1 pb-0.5 pl-2 pr-3 items-start self-stretch'>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{`${datasetBindings?.length} ${t('dataset.editExternalAPITooltipTitle')}`}</div>
|
||||
</div>
|
||||
{datasetBindings?.map(binding => (
|
||||
<div key={binding.id} className='flex px-2 py-1 items-center gap-1 self-stretch'>
|
||||
<RiBook2Line className='w-4 h-4 text-text-secondary' />
|
||||
<div className='text-text-secondary system-sm-medium'>{binding.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
asChild={false}
|
||||
position='bottom'
|
||||
>
|
||||
<RiInformation2Line className='w-3.5 h-3.5' />
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ActionButton className='absolute top-5 right-5' onClick={onCancel}>
|
||||
<RiCloseLine className='w-[18px] h-[18px] text-text-tertiary flex-shrink-0' />
|
||||
</ActionButton>
|
||||
<Form
|
||||
value={formData}
|
||||
onChange={handleDataChange}
|
||||
formSchemas={formSchemas}
|
||||
className='flex px-6 py-3 flex-col justify-center items-start gap-4 self-stretch'
|
||||
/>
|
||||
<div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
|
||||
<Button type='button' variant='secondary' onClick={onCancel}>
|
||||
{t('dataset.externalAPIForm.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='primary'
|
||||
onClick={() => {
|
||||
if (isEditMode && (datasetBindings?.length ?? 0) > 0)
|
||||
setShowConfirm(true)
|
||||
else if (isEditMode && onEdit)
|
||||
onEdit(formData)
|
||||
|
||||
else
|
||||
handleSave()
|
||||
}}
|
||||
disabled={hasEmptyInputs || loading}
|
||||
>
|
||||
{t('dataset.externalAPIForm.save')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex px-2 py-3 justify-center items-center gap-1 self-stretch rounded-b-2xl
|
||||
border-t-[0.5px] border-divider-subtle bg-background-soft text-text-tertiary system-xs-regular'
|
||||
>
|
||||
<RiLock2Fill className='w-3 h-3 text-text-quaternary' />
|
||||
{t('dataset.externalAPIForm.encrypted.front')}
|
||||
<a
|
||||
className='text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('dataset.externalAPIForm.encrypted.end')}
|
||||
</div>
|
||||
</div>
|
||||
{showConfirm && (datasetBindings?.length ?? 0) > 0 && (
|
||||
<Confirm
|
||||
isShow={showConfirm}
|
||||
type='warning'
|
||||
title='Warning'
|
||||
content={`${t('datasets.editExternalAPIConfirmWarningContent.front')} ${datasetBindings?.length} ${t('datasets.editExternalAPIConfirmWarningContent.end')}`}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
onConfirm={handleSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddExternalAPIModal)
|
@ -0,0 +1,92 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiBookOpenLine,
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ExternalKnowledgeAPICard from '../external-knowledge-api-card'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
|
||||
type ExternalAPIPanelProps = {
|
||||
onClose: () => void
|
||||
isShow: boolean
|
||||
datasetBindings: { id: string; name: string }[]
|
||||
}
|
||||
|
||||
const ExternalAPIPanel: React.FC<ExternalAPIPanelProps> = ({ onClose, isShow, datasetBindings }) => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowExternalKnowledgeAPIModal } = useModalContext()
|
||||
const { externalKnowledgeApiList, mutateExternalKnowledgeApis, isLoading } = useExternalKnowledgeApi()
|
||||
|
||||
const handleOpenExternalAPIModal = () => {
|
||||
setShowExternalKnowledgeAPIModal({
|
||||
payload: { name: '', settings: { endpoint: '', api_key: '' } },
|
||||
datasetBindings: [],
|
||||
onSaveCallback: () => {
|
||||
mutateExternalKnowledgeApis()
|
||||
},
|
||||
onCancelCallback: () => {
|
||||
mutateExternalKnowledgeApis()
|
||||
},
|
||||
isEditMode: false,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={-1}
|
||||
className={cn('absolute top-14 right-0 bottom-2 flex z-10 outline-none')}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-col w-[420px] bg-components-panel-bg-alt rounded-l-2xl h-full border border-components-panel-border',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-start self-stretch p-4 pb-0'>
|
||||
<div className='flex flex-col items-start gap-1 flex-grow'>
|
||||
<div className='self-stretch text-text-primary system-xl-semibold'>{t('dataset.externalAPIPanelTitle')}</div>
|
||||
<div className='self-stretch text-text-tertiary body-xs-regular'>{t('dataset.externalAPIPanelDescription')}</div>
|
||||
<a className='flex justify-center items-center gap-1 self-stretch cursor-pointer' href='https://docs.dify.ai/guides/knowledge-base/external-knowledge-api-documentation' target='_blank'>
|
||||
<RiBookOpenLine className='w-3 h-3 text-text-accent' />
|
||||
<div className='flex-grow text-text-accent body-xs-regular'>{t('dataset.externalAPIPanelDocumentation')}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<ActionButton onClick={() => onClose()}>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex px-4 py-3 flex-col justify-center items-start gap-2 self-stretch'>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
className='flex justify-center items-center px-3 py-2 gap-0.5'
|
||||
onClick={handleOpenExternalAPIModal}
|
||||
>
|
||||
<RiAddLine className='w-4 h-4 text-components-button-primary-text' />
|
||||
<div className='text-components-button-primary-text system-sm-medium'>{t('dataset.createExternalAPI')}</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex py-0 px-4 flex-col items-start gap-1 flex-grow self-stretch'>
|
||||
{isLoading
|
||||
? (
|
||||
<Loading />
|
||||
)
|
||||
: (
|
||||
externalKnowledgeApiList.map(api => (
|
||||
<ExternalKnowledgeAPICard key={api.id} api={api} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExternalAPIPanel
|
@ -0,0 +1,151 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
} from '@remixicon/react'
|
||||
import type { CreateExternalAPIReq } from '../declarations'
|
||||
import type { ExternalAPIItem } from '@/models/datasets'
|
||||
import { checkUsageExternalAPI, deleteExternalAPI, fetchExternalAPI, updateExternalAPI } from '@/service/datasets'
|
||||
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
|
||||
type ExternalKnowledgeAPICardProps = {
|
||||
api: ExternalAPIItem
|
||||
}
|
||||
|
||||
const ExternalKnowledgeAPICard: React.FC<ExternalKnowledgeAPICardProps> = ({ api }) => {
|
||||
const { setShowExternalKnowledgeAPIModal } = useModalContext()
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [usageCount, setUsageCount] = useState(0)
|
||||
const { mutateExternalKnowledgeApis } = useExternalKnowledgeApi()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleEditClick = async () => {
|
||||
try {
|
||||
const response = await fetchExternalAPI({ apiTemplateId: api.id })
|
||||
const formValue: CreateExternalAPIReq = {
|
||||
name: response.name,
|
||||
settings: {
|
||||
endpoint: response.settings.endpoint,
|
||||
api_key: response.settings.api_key,
|
||||
},
|
||||
}
|
||||
|
||||
setShowExternalKnowledgeAPIModal({
|
||||
payload: formValue,
|
||||
onSaveCallback: () => {
|
||||
mutateExternalKnowledgeApis()
|
||||
},
|
||||
onCancelCallback: () => {
|
||||
mutateExternalKnowledgeApis()
|
||||
},
|
||||
isEditMode: true,
|
||||
datasetBindings: response.dataset_bindings,
|
||||
onEditCallback: async (updatedData: CreateExternalAPIReq) => {
|
||||
try {
|
||||
await updateExternalAPI({
|
||||
apiTemplateId: api.id,
|
||||
body: {
|
||||
...response,
|
||||
name: updatedData.name,
|
||||
settings: {
|
||||
...response.settings,
|
||||
endpoint: updatedData.settings.endpoint,
|
||||
api_key: updatedData.settings.api_key,
|
||||
},
|
||||
},
|
||||
})
|
||||
mutateExternalKnowledgeApis()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error updating external knowledge API:', error)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching external knowledge API data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteClick = async () => {
|
||||
try {
|
||||
const usage = await checkUsageExternalAPI({ apiTemplateId: api.id })
|
||||
if (usage.is_using)
|
||||
setUsageCount(usage.count)
|
||||
|
||||
setShowConfirm(true)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error checking external API usage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
try {
|
||||
const response = await deleteExternalAPI({ apiTemplateId: api.id })
|
||||
if (response && response.result === 'success') {
|
||||
setShowConfirm(false)
|
||||
mutateExternalKnowledgeApis()
|
||||
}
|
||||
else {
|
||||
console.error('Failed to delete external API')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error deleting external knowledge API:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`flex p-2 pl-3 items-start self-stretch rounded-lg border-[0.5px]
|
||||
border-components-panel-border-subtle bg-components-panel-on-panel-item-bg
|
||||
shadows-shadow-xs ${isHovered ? 'bg-state-destructive-hover border-state-destructive-border' : ''}`}
|
||||
>
|
||||
<div className='flex py-1 flex-col justify-center items-start gap-1.5 flex-grow'>
|
||||
<div className='flex items-center gap-1 self-stretch text-text-secondary'>
|
||||
<ApiConnectionMod className='w-4 h-4' />
|
||||
<div className='system-sm-medium'>{api.name}</div>
|
||||
</div>
|
||||
<div className='self-stretch text-text-tertiary system-xs-regular'>{api.settings.endpoint}</div>
|
||||
</div>
|
||||
<div className='flex items-start gap-1'>
|
||||
<ActionButton onClick={handleEditClick}>
|
||||
<RiEditLine className='w-4 h-4 text-text-tertiary hover:text-text-secondary' />
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
className='hover:bg-state-destructive-hover'
|
||||
onClick={handleDeleteClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary hover:text-text-destructive' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
{showConfirm && (
|
||||
<Confirm
|
||||
isShow={showConfirm}
|
||||
title={`${t('dataset.deleteExternalAPIConfirmWarningContent.title.front')} ${api.name}${t('dataset.deleteExternalAPIConfirmWarningContent.title.end')}`}
|
||||
content={
|
||||
usageCount > 0
|
||||
? `${t('dataset.deleteExternalAPIConfirmWarningContent.content.front')} ${usageCount} ${t('dataset.deleteExternalAPIConfirmWarningContent.content.end')}`
|
||||
: t('dataset.deleteExternalAPIConfirmWarningContent.noConnectionContent')
|
||||
}
|
||||
type='warning'
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExternalKnowledgeAPICard
|
@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import ExternalKnowledgeBaseCreate from '@/app/components/datasets/external-knowledge-base/create'
|
||||
import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations'
|
||||
import { createExternalKnowledgeBase } from '@/service/datasets'
|
||||
|
||||
const ExternalKnowledgeBaseConnector = () => {
|
||||
const { notify } = useToastContext()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const handleConnect = async (formValue: CreateKnowledgeBaseReq) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await createExternalKnowledgeBase({ body: formValue })
|
||||
if (result && result.id) {
|
||||
notify({ type: 'success', message: 'External Knowledge Base Connected Successfully' })
|
||||
router.back()
|
||||
}
|
||||
else { throw new Error('Failed to create external knowledge base') }
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error creating external knowledge base:', error)
|
||||
notify({ type: 'error', message: 'Failed to connect External Knowledge Base' })
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
return <ExternalKnowledgeBaseCreate onConnect={handleConnect} loading={loading} />
|
||||
}
|
||||
|
||||
export default ExternalKnowledgeBaseConnector
|
@ -0,0 +1,53 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Select from '@/app/components/base/select'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
|
||||
type ExternalApiSelectionProps = {
|
||||
external_knowledge_api_id: string
|
||||
external_knowledge_id: string
|
||||
onChange: (data: { external_knowledge_api_id?: string; external_knowledge_id?: string }) => void
|
||||
}
|
||||
|
||||
const ExternalApiSelection: React.FC<ExternalApiSelectionProps> = ({ external_knowledge_api_id, external_knowledge_id, onChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const { externalKnowledgeApiList } = useExternalKnowledgeApi()
|
||||
|
||||
const apiItems = externalKnowledgeApiList.map(api => ({
|
||||
value: api.id,
|
||||
name: api.name,
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
if (!external_knowledge_api_id && apiItems.length > 0)
|
||||
onChange({ external_knowledge_api_id: apiItems[0].value, external_knowledge_id })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form className='flex flex-col gap-4 self-stretch'>
|
||||
<div className='flex flex-col gap-1 self-stretch'>
|
||||
<div className='flex flex-col self-stretch'>
|
||||
<label className='text-text-secondary system-sm-semibold'>{t('dataset.externalAPIPanelTitle')}</label>
|
||||
</div>
|
||||
<Select
|
||||
className='w-full'
|
||||
items={apiItems}
|
||||
defaultValue={apiItems.length > 0 ? apiItems[0].value : ''}
|
||||
onSelect={e => onChange({ external_knowledge_api_id: e.value as string, external_knowledge_id })}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1 self-stretch'>
|
||||
<div className='flex flex-col self-stretch'>
|
||||
<label className='text-text-secondary system-sm-semibold'>{t('dataset.externalKnowledgeId')}</label>
|
||||
</div>
|
||||
<Input
|
||||
value={external_knowledge_id}
|
||||
onChange={e => onChange({ external_knowledge_id: e.target.value, external_knowledge_api_id })}
|
||||
placeholder={t('dataset.externalKnowledgeIdPlaceholder') ?? ''}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExternalApiSelection
|
@ -0,0 +1,33 @@
|
||||
import { RiBookOpenLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const InfoPanel = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex w-[360px] pt-[108px] pb-2 pr-8 flex-col items-start'>
|
||||
<div className='flex min-w-[240px] w-full p-6 flex-col items-start gap-3 self-stretch rounded-xl bg-background-section'>
|
||||
<div className='flex p-1 w-10 h-10 justify-center items-center gap-2 flex-grow self-stretch rounded-lg border-[0.5px] border-components-card-border bg-components-card-bg'>
|
||||
<RiBookOpenLine className='w-5 h-5 text-text-accent' />
|
||||
</div>
|
||||
<p className='flex flex-col items-start gap-2 self-stretch'>
|
||||
<span className='self-stretch text-text-secondary system-xl-semibold'>
|
||||
{t('dataset.connectDatasetIntro.title')}
|
||||
</span>
|
||||
<span className='text-text-tertiary system-sm-regular'>
|
||||
{t('dataset.connectDatasetIntro.content.front')}
|
||||
<a className='text-text-accent system-sm-regular ml-1' href='https://docs.dify.ai/guides/knowledge-base/external-knowledge-api-documentation' target='_blank' rel="noopener noreferrer">
|
||||
{t('dataset.connectDatasetIntro.content.link')}
|
||||
</a>
|
||||
{t('dataset.connectDatasetIntro.content.end')}
|
||||
</span>
|
||||
<a className='self-stretch text-text-accent system-sm-regular' href='https://docs.dify.ai/guides/knowledge-base/connect-external-knowledge' target='_blank' rel="noopener noreferrer">
|
||||
{t('dataset.connectDatasetIntro.learnMore')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfoPanel
|
@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import { RiBookOpenLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
|
||||
type KnowledgeBaseInfoProps = {
|
||||
name: string
|
||||
description?: string
|
||||
onChange: (data: { name?: string; description?: string }) => void
|
||||
}
|
||||
|
||||
const KnowledgeBaseInfo: React.FC<KnowledgeBaseInfoProps> = ({ name, description, onChange }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({ name: e.target.value })
|
||||
}
|
||||
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onChange({ description: e.target.value })
|
||||
}
|
||||
|
||||
return (
|
||||
<form className='flex flex-col gap-4 self-stretch'>
|
||||
<div className='flex flex-col gap-4 self-stretch'>
|
||||
<div className='flex flex-col gap-1 self-stretch'>
|
||||
<div className='flex flex-col justify-center self-stretch'>
|
||||
<label className='text-text-secondary system-sm-semibold'>{t('dataset.externalKnowledgeName')}</label>
|
||||
</div>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
placeholder={t('dataset.externalKnowledgeNamePlaceholder') ?? ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1 self-stretch'>
|
||||
<div className='flex flex-col justify-center self-stretch'>
|
||||
<label className='text-text-secondary system-sm-semibold'>{t('dataset.externalKnowledgeDescription')}</label>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1 self-stretch'>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={ e => handleDescriptionChange(e)}
|
||||
placeholder={t('dataset.externalKnowledgeDescriptionPlaceholder') ?? ''}
|
||||
className={`flex h-20 p-2 self-stretch items-start rounded-lg bg-components-input-bg-normal ${description ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder'} system-sm-regular`}
|
||||
/>
|
||||
<a
|
||||
className='flex py-0.5 gap-1 self-stretch'
|
||||
href='https://docs.dify.ai/features/datasets#how-to-write-a-good-dataset-description'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className='flex p-0.5 items-center gap-2'>
|
||||
<RiBookOpenLine className='w-3 h-3 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='flex-grow text-text-tertiary body-xs-regular'>{t('dataset.learnHowToWriteGoodKnowledgeDescription')}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default KnowledgeBaseInfo
|
@ -0,0 +1,55 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import TopKItem from '@/app/components/base/param-item/top-k-item'
|
||||
import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type RetrievalSettingsProps = {
|
||||
topK: number
|
||||
scoreThreshold: number
|
||||
isInHitTesting?: boolean
|
||||
isInRetrievalSetting?: boolean
|
||||
onChange: (data: { top_k?: number; score_threshold?: number }) => void
|
||||
}
|
||||
|
||||
const RetrievalSettings: FC<RetrievalSettingsProps> = ({ topK, scoreThreshold, onChange, isInHitTesting = false, isInRetrievalSetting = false }) => {
|
||||
const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-2 self-stretch', isInRetrievalSetting && 'w-full max-w-[480px]')}>
|
||||
{!isInHitTesting && !isInRetrievalSetting && <div className='flex h-7 pt-1 flex-col gap-2 self-stretch'>
|
||||
<label className='text-text-secondary system-sm-semibold'>{t('dataset.retrievalSettings')}</label>
|
||||
</div>}
|
||||
<div className={cn(
|
||||
'flex gap-4 self-stretch',
|
||||
{
|
||||
'flex-col': isInHitTesting,
|
||||
'flex-row': isInRetrievalSetting,
|
||||
'flex-col sm:flex-row': !isInHitTesting && !isInRetrievalSetting,
|
||||
},
|
||||
)}>
|
||||
<div className='flex flex-col gap-1 flex-grow'>
|
||||
<TopKItem
|
||||
className='grow'
|
||||
value={topK}
|
||||
onChange={(_key, v) => onChange({ top_k: v })}
|
||||
enable={true}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1 flex-grow'>
|
||||
<ScoreThresholdItem
|
||||
className='grow'
|
||||
value={scoreThreshold}
|
||||
onChange={(_key, v) => onChange({ score_threshold: v })}
|
||||
enable={scoreThresholdEnabled}
|
||||
hasSwitch={true}
|
||||
onSwitchChange={(_key, v) => setScoreThresholdEnabled(v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RetrievalSettings
|
@ -0,0 +1,11 @@
|
||||
export type CreateKnowledgeBaseReq = {
|
||||
name: string
|
||||
description?: string
|
||||
external_knowledge_api_id: string
|
||||
provider: 'external'
|
||||
external_knowledge_id: string
|
||||
external_retrieval_model: {
|
||||
top_k: number
|
||||
score_threshold: number
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { RiArrowLeftLine, RiArrowRightLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import KnowledgeBaseInfo from './KnowledgeBaseInfo'
|
||||
import ExternalApiSelection from './ExternalApiSelection'
|
||||
import RetrievalSettings from './RetrievalSettings'
|
||||
import InfoPanel from './InfoPanel'
|
||||
import type { CreateKnowledgeBaseReq } from './declarations'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type ExternalKnowledgeBaseCreateProps = {
|
||||
onConnect: (formValue: CreateKnowledgeBaseReq) => void
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> = ({ onConnect, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const [formData, setFormData] = useState<CreateKnowledgeBaseReq>({
|
||||
name: '',
|
||||
description: '',
|
||||
external_knowledge_api_id: '',
|
||||
external_knowledge_id: '',
|
||||
external_retrieval_model: {
|
||||
top_k: 2,
|
||||
score_threshold: 0.5,
|
||||
},
|
||||
provider: 'external',
|
||||
|
||||
})
|
||||
|
||||
const navBackHandle = useCallback(() => {
|
||||
router.replace('/datasets')
|
||||
}, [router])
|
||||
|
||||
const handleFormChange = (newData: CreateKnowledgeBaseReq) => {
|
||||
setFormData(newData)
|
||||
}
|
||||
|
||||
const isFormValid = formData.name.trim() !== ''
|
||||
&& formData.external_knowledge_api_id !== ''
|
||||
&& formData.external_knowledge_id !== ''
|
||||
&& formData.external_retrieval_model.top_k !== undefined
|
||||
&& formData.external_retrieval_model.score_threshold !== undefined
|
||||
|
||||
return (
|
||||
<div className='flex flex-col flex-grow self-stretch rounded-t-2xl border-t border-effects-highlight bg-components-panel-bg'>
|
||||
<div className='flex justify-center flex-grow self-stretch'>
|
||||
<div className='flex w-full max-w-[960px] px-14 py-0 flex-col items-center'>
|
||||
<div className='flex w-full max-w-[640px] pt-6 pb-8 flex-col grow items-center gap-4'>
|
||||
<div className='relative flex py-2 items-center gap-2 self-stretch'>
|
||||
<div className='flex-grow text-text-primary system-xl-semibold'>{t('dataset.connectDataset')}</div>
|
||||
<Button
|
||||
className='flex w-8 h-8 p-2 items-center justify-center absolute left-[-44px] top-1 rounded-full'
|
||||
variant='tertiary'
|
||||
onClick={navBackHandle}
|
||||
>
|
||||
<RiArrowLeftLine className='w-4 h-4 text-text-tertiary' />
|
||||
</Button>
|
||||
</div>
|
||||
<KnowledgeBaseInfo
|
||||
name={formData.name}
|
||||
description={formData.description ?? ''}
|
||||
onChange={data => handleFormChange({
|
||||
...formData,
|
||||
...data,
|
||||
})}
|
||||
/>
|
||||
<Divider />
|
||||
<ExternalApiSelection
|
||||
external_knowledge_api_id={formData.external_knowledge_api_id}
|
||||
external_knowledge_id={formData.external_knowledge_id}
|
||||
onChange={data => handleFormChange({
|
||||
...formData,
|
||||
...data,
|
||||
})}
|
||||
/>
|
||||
<RetrievalSettings
|
||||
topK={formData.external_retrieval_model.top_k}
|
||||
scoreThreshold={formData.external_retrieval_model.score_threshold}
|
||||
onChange={data => handleFormChange({
|
||||
...formData,
|
||||
external_retrieval_model: {
|
||||
...formData.external_retrieval_model,
|
||||
...data,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<div className='flex py-2 justify-end items-center gap-2 self-stretch'>
|
||||
<Button variant='secondary' onClick={navBackHandle}>
|
||||
<div className='text-components-button-secondary-text system-sm-medium'>{t('dataset.externalKnowledgeForm.cancel')}</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={() => {
|
||||
onConnect(formData)
|
||||
}}
|
||||
disabled={!isFormValid}
|
||||
loading={loading}
|
||||
>
|
||||
<div className='text-components-button-primary-text system-sm-medium'>{t('dataset.externalKnowledgeForm.connect')}</div>
|
||||
<RiArrowRightLine className='w-4 h-4 text-components-button-primary-text' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InfoPanel />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExternalKnowledgeBaseCreate
|
@ -26,40 +26,44 @@ const HitDetail: FC<IHitDetailProps> = ({ segInfo }) => {
|
||||
)
|
||||
}
|
||||
|
||||
return segInfo?.content
|
||||
return <div className='mb-4 text-md text-gray-800 h-full'>{segInfo?.content}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='overflow-x-auto'>
|
||||
<div className="bg-gray-25 p-6">
|
||||
<div className="flex items-center">
|
||||
<SegmentIndexTag
|
||||
positionId={segInfo?.position || ''}
|
||||
className="w-fit mr-6"
|
||||
/>
|
||||
<div className={cn(s.commonIcon, s.typeSquareIcon)} />
|
||||
<span className={cn('mr-6', s.numberInfo)}>
|
||||
{segInfo?.word_count} {t('datasetDocuments.segment.characters')}
|
||||
</span>
|
||||
<div className={cn(s.commonIcon, s.targetIcon)} />
|
||||
<span className={s.numberInfo}>
|
||||
{segInfo?.hit_count} {t('datasetDocuments.segment.hitCount')}
|
||||
</span>
|
||||
</div>
|
||||
<Divider />
|
||||
segInfo?.id === 'external'
|
||||
? <div className='w-full overflow-x-auto px-2'>
|
||||
<div className={s.segModalContent}>{renderContent()}</div>
|
||||
<div className={s.keywordTitle}>
|
||||
{t('datasetDocuments.segment.keywords')}
|
||||
</div>
|
||||
<div className={s.keywordWrapper}>
|
||||
{!segInfo?.keywords?.length
|
||||
? '-'
|
||||
: segInfo?.keywords?.map((word, index) => {
|
||||
return <div key={index} className={s.keyword}>{word}</div>
|
||||
})}
|
||||
</div>
|
||||
: <div className='overflow-x-auto'>
|
||||
<div className="bg-gray-25 p-6">
|
||||
<div className="flex items-center">
|
||||
<SegmentIndexTag
|
||||
positionId={segInfo?.position || ''}
|
||||
className="w-fit mr-6"
|
||||
/>
|
||||
<div className={cn(s.commonIcon, s.typeSquareIcon)} />
|
||||
<span className={cn('mr-6', s.numberInfo)}>
|
||||
{segInfo?.word_count} {t('datasetDocuments.segment.characters')}
|
||||
</span>
|
||||
<div className={cn(s.commonIcon, s.targetIcon)} />
|
||||
<span className={s.numberInfo}>
|
||||
{segInfo?.hit_count} {t('datasetDocuments.segment.hitCount')}
|
||||
</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className={s.segModalContent}>{renderContent()}</div>
|
||||
<div className={s.keywordTitle}>
|
||||
{t('datasetDocuments.segment.keywords')}
|
||||
</div>
|
||||
<div className={s.keywordWrapper}>
|
||||
{!segInfo?.keywords?.length
|
||||
? '-'
|
||||
: segInfo?.keywords?.map((word, index) => {
|
||||
return <div key={index} className={s.keyword}>{word}</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ import s from './style.module.css'
|
||||
import HitDetail from './hit-detail'
|
||||
import ModifyRetrievalModal from './modify-retrieval-modal'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
|
||||
import type { ExternalKnowledgeBaseHitTestingResponse, ExternalKnowledgeBaseHitTesting as ExternalKnowledgeBaseHitTestingType, HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
@ -49,8 +49,10 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
const [hitResult, setHitResult] = useState<HitTestingResponse | undefined>() // 初始化记录为空数组
|
||||
const [externalHitResult, setExternalHitResult] = useState<ExternalKnowledgeBaseHitTestingResponse | undefined>()
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [currParagraph, setCurrParagraph] = useState<{ paraInfo?: HitTestingType; showModal: boolean }>({ showModal: false })
|
||||
const [externalCurrParagraph, setExternalCurrParagraph] = useState<{ paraInfo?: ExternalKnowledgeBaseHitTestingType; showModal: boolean }>({ showModal: false })
|
||||
const [text, setText] = useState('')
|
||||
|
||||
const [currPage, setCurrPage] = React.useState<number>(0)
|
||||
@ -66,12 +68,50 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
||||
setCurrParagraph({ paraInfo: detail, showModal: true })
|
||||
}
|
||||
|
||||
const onClickExternalCard = (detail: ExternalKnowledgeBaseHitTestingType) => {
|
||||
setExternalCurrParagraph({ paraInfo: detail, showModal: true })
|
||||
}
|
||||
const { dataset: currentDataset } = useContext(DatasetDetailContext)
|
||||
|
||||
const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig)
|
||||
const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false)
|
||||
const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile)
|
||||
|
||||
const renderHitResults = (results: any[], onClickCard: (record: any) => void) => (
|
||||
<>
|
||||
<div className='text-gray-600 font-semibold mb-4'>{t('datasetHitTesting.hit.title')}</div>
|
||||
<div className='overflow-auto flex-1'>
|
||||
<div className={s.cardWrapper}>
|
||||
{results.map((record, idx) => (
|
||||
<SegmentCard
|
||||
key={idx}
|
||||
loading={false}
|
||||
refSource= {{
|
||||
title: record.title,
|
||||
uri: record.metadata ? record.metadata['x-amz-bedrock-kb-source-uri'] : '',
|
||||
}}
|
||||
detail={record.segment}
|
||||
contentExternal={record.content}
|
||||
score={record.score}
|
||||
scene='hitTesting'
|
||||
className='h-[216px] mb-4'
|
||||
onClick={() => onClickCard(record)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
const renderEmptyState = () => (
|
||||
<div className='h-full flex flex-col justify-center items-center'>
|
||||
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!bg-gray-200 !h-14 !w-14')} />
|
||||
<div className='text-gray-300 text-[13px] mt-3'>
|
||||
{t('datasetHitTesting.hit.emptyTip')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setShowRightPanel(!isMobile)
|
||||
}, [isMobile, setShowRightPanel])
|
||||
@ -86,12 +126,14 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
||||
<Textarea
|
||||
datasetId={datasetId}
|
||||
setHitResult={setHitResult}
|
||||
setExternalHitResult={setExternalHitResult}
|
||||
onSubmit={showRightPanel}
|
||||
onUpdateList={recordsMutate}
|
||||
loading={submitLoading}
|
||||
setLoading={setSubmitLoading}
|
||||
setText={setText}
|
||||
text={text}
|
||||
isExternal={currentDataset?.provider === 'external'}
|
||||
onClickRetrievalMethod={() => setIsShowModifyRetrievalModal(true)}
|
||||
retrievalConfig={retrievalConfig}
|
||||
isEconomy={currentDataset?.indexing_technique === 'economy'}
|
||||
@ -159,47 +201,42 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
||||
className='h-[216px]'
|
||||
/>
|
||||
</div>
|
||||
: !hitResult?.records.length
|
||||
? (
|
||||
<div className='h-full flex flex-col justify-center items-center'>
|
||||
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!bg-gray-200 !h-14 !w-14')} />
|
||||
<div className='text-gray-300 text-[13px] mt-3'>
|
||||
{t('datasetHitTesting.hit.emptyTip')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className='text-gray-600 font-semibold mb-4'>{t('datasetHitTesting.hit.title')}</div>
|
||||
<div className='overflow-auto flex-1'>
|
||||
<div className={s.cardWrapper}>
|
||||
{hitResult?.records.map((record, idx) => {
|
||||
return <SegmentCard
|
||||
key={idx}
|
||||
loading={false}
|
||||
detail={record.segment as any}
|
||||
score={record.score}
|
||||
scene='hitTesting'
|
||||
className='h-[216px] mb-4'
|
||||
onClick={() => onClickCard(record as any)}
|
||||
/>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
(() => {
|
||||
if (!hitResult?.records.length && !externalHitResult?.records.length)
|
||||
return renderEmptyState()
|
||||
|
||||
if (hitResult?.records.length)
|
||||
return renderHitResults(hitResult.records, onClickCard)
|
||||
|
||||
return renderHitResults(externalHitResult?.records || [], onClickExternalCard)
|
||||
})()
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</FloatRightContainer>
|
||||
<Modal
|
||||
className='w-[520px] p-0'
|
||||
className='w-full px-10 py-6'
|
||||
closable
|
||||
onClose={() => setCurrParagraph({ showModal: false })}
|
||||
isShow={currParagraph.showModal}
|
||||
onClose={() => {
|
||||
setCurrParagraph({ showModal: false })
|
||||
setExternalCurrParagraph({ showModal: false })
|
||||
}}
|
||||
isShow={currParagraph.showModal || externalCurrParagraph.showModal}
|
||||
>
|
||||
{currParagraph.showModal && <HitDetail
|
||||
segInfo={currParagraph.paraInfo?.segment}
|
||||
/>}
|
||||
{currParagraph.showModal && (
|
||||
<HitDetail
|
||||
segInfo={currParagraph.paraInfo?.segment}
|
||||
/>
|
||||
)}
|
||||
{externalCurrParagraph.showModal && (
|
||||
<HitDetail
|
||||
segInfo={{
|
||||
id: 'external',
|
||||
content: externalCurrParagraph.paraInfo?.content,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
<Drawer isOpen={isShowModifyRetrievalModal} onClose={() => setIsShowModifyRetrievalModal(false)} footer={null} mask={isMobile} panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
|
||||
<ModifyRetrievalModal
|
||||
|
@ -0,0 +1,65 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RetrievalSettings from '../external-knowledge-base/create/RetrievalSettings'
|
||||
import Button from '@/app/components/base/button'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
type ModifyExternalRetrievalModalProps = {
|
||||
onClose: () => void
|
||||
onSave: (data: { top_k: number; score_threshold: number }) => void
|
||||
initialTopK: number
|
||||
initialScoreThreshold: number
|
||||
}
|
||||
|
||||
const ModifyExternalRetrievalModal: React.FC<ModifyExternalRetrievalModalProps> = ({
|
||||
onClose,
|
||||
onSave,
|
||||
initialTopK,
|
||||
initialScoreThreshold,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [topK, setTopK] = useState(initialTopK)
|
||||
const [scoreThreshold, setScoreThreshold] = useState(initialScoreThreshold)
|
||||
|
||||
const handleSettingsChange = (data: { top_k?: number; score_threshold?: number }) => {
|
||||
if (data.top_k !== undefined)
|
||||
setTopK(data.top_k)
|
||||
if (data.score_threshold !== undefined)
|
||||
setScoreThreshold(data.score_threshold)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({ top_k: topK, score_threshold: scoreThreshold })
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='absolute z-10 top-[36px] right-[14px] flex w-[320px] flex-col items-start rounded-2xl border-[0.5px]
|
||||
border-components-panel-border bg-components-panel-bg shadows-shadow-2xl'
|
||||
>
|
||||
<div className='flex p-4 pb-2 items-center justify-between self-stretch'>
|
||||
<div className='text-text-primary system-xl-semibold flex-grow'>{t('datasetHitTesting.settingTitle')}</div>
|
||||
<ActionButton className='ml-auto' onClick={onClose}>
|
||||
<RiCloseLine className='w-4 h-4 flex-shrink-0' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className='flex p-4 pt-2 flex-col justify-center items-start gap-4 self-stretch'>
|
||||
<RetrievalSettings
|
||||
topK={topK}
|
||||
scoreThreshold={scoreThreshold}
|
||||
onChange={handleSettingsChange}
|
||||
isInHitTesting={true}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex p-4 pt-2 justify-end items-end gap-1 w-full'>
|
||||
<Button className='flex-shrink-0 min-w-[72px]' onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' className='flex-shrink-0 min-w-[72px]' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModifyExternalRetrievalModal
|
@ -1,12 +1,17 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import Button from '../../base/button'
|
||||
import Tag from '../../base/tag'
|
||||
import { getIcon } from '../common/retrieval-method-info'
|
||||
import s from './style.module.css'
|
||||
import ModifyExternalRetrievalModal from './modify-external-retrieval-modal'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { HitTestingResponse } from '@/models/datasets'
|
||||
import { hitTesting } from '@/service/datasets'
|
||||
import type { ExternalKnowledgeBaseHitTestingResponse, HitTestingResponse } from '@/models/datasets'
|
||||
import { externalKnowledgeBaseHitTesting, hitTesting } from '@/service/datasets'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { RETRIEVE_METHOD, type RetrievalConfig } from '@/types/app'
|
||||
|
||||
@ -14,10 +19,12 @@ type TextAreaWithButtonIProps = {
|
||||
datasetId: string
|
||||
onUpdateList: () => void
|
||||
setHitResult: (res: HitTestingResponse) => void
|
||||
setExternalHitResult: (res: ExternalKnowledgeBaseHitTestingResponse) => void
|
||||
loading: boolean
|
||||
setLoading: (v: boolean) => void
|
||||
text: string
|
||||
setText: (v: string) => void
|
||||
isExternal?: boolean
|
||||
onClickRetrievalMethod: () => void
|
||||
retrievalConfig: RetrievalConfig
|
||||
isEconomy: boolean
|
||||
@ -28,16 +35,28 @@ const TextAreaWithButton = ({
|
||||
datasetId,
|
||||
onUpdateList,
|
||||
setHitResult,
|
||||
setExternalHitResult,
|
||||
setLoading,
|
||||
loading,
|
||||
text,
|
||||
setText,
|
||||
isExternal = false,
|
||||
onClickRetrievalMethod,
|
||||
retrievalConfig,
|
||||
isEconomy,
|
||||
onSubmit: _onSubmit,
|
||||
}: TextAreaWithButtonIProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||||
const [externalRetrievalSettings, setExternalRetrievalSettings] = useState({
|
||||
top_k: 2,
|
||||
score_threshold: 0.5,
|
||||
})
|
||||
|
||||
const handleSaveExternalRetrievalSettings = (data: { top_k: number; score_threshold: number }) => {
|
||||
setExternalRetrievalSettings(data)
|
||||
setIsSettingsOpen(false)
|
||||
}
|
||||
|
||||
function handleTextChange(event: any) {
|
||||
setText(event.target.value)
|
||||
@ -63,28 +82,70 @@ const TextAreaWithButton = ({
|
||||
_onSubmit && _onSubmit()
|
||||
}
|
||||
|
||||
const externalRetrievalTestingOnSubmit = async () => {
|
||||
setLoading(true)
|
||||
const [e, res] = await asyncRunSafe<ExternalKnowledgeBaseHitTestingResponse>(
|
||||
externalKnowledgeBaseHitTesting({
|
||||
datasetId,
|
||||
query: text,
|
||||
external_retrieval_model: {
|
||||
top_k: externalRetrievalSettings.top_k,
|
||||
score_threshold: externalRetrievalSettings.score_threshold,
|
||||
},
|
||||
}) as Promise<ExternalKnowledgeBaseHitTestingResponse>,
|
||||
)
|
||||
if (!e) {
|
||||
setExternalHitResult(res)
|
||||
onUpdateList?.()
|
||||
}
|
||||
setLoading(false)
|
||||
_onSubmit && _onSubmit()
|
||||
}
|
||||
|
||||
const retrievalMethod = isEconomy ? RETRIEVE_METHOD.invertedIndex : retrievalConfig.search_method
|
||||
const Icon = getIcon(retrievalMethod)
|
||||
return (
|
||||
<>
|
||||
<div className={s.wrapper}>
|
||||
<div className='pt-2 rounded-tl-xl rounded-tr-xl bg-[#EEF4FF]'>
|
||||
<div className='relative pt-2 rounded-tl-xl rounded-tr-xl bg-[#EEF4FF]'>
|
||||
<div className="px-4 pb-2 flex justify-between h-8 items-center">
|
||||
<span className="text-gray-800 font-semibold text-sm">
|
||||
{t('datasetHitTesting.input.title')}
|
||||
</span>
|
||||
<Tooltip
|
||||
popupContent={t('dataset.retrieval.changeRetrievalMethod')}
|
||||
>
|
||||
<div
|
||||
onClick={onClickRetrievalMethod}
|
||||
className='flex px-2 h-7 items-center space-x-1 bg-white hover:bg-[#ECE9FE] rounded-md shadow-sm cursor-pointer text-[#6927DA]'
|
||||
{isExternal
|
||||
? <Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||
>
|
||||
<Icon className='w-3.5 h-3.5'></Icon>
|
||||
<div className='text-xs font-medium'>{t(`dataset.retrieval.${retrievalMethod}.title`)}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<RiEqualizer2Line className='text-components-button-secondary-text w-3.5 h-3.5' />
|
||||
<div className='flex px-[3px] justify-center items-center gap-1'>
|
||||
<span className='text-components-button-secondary-text system-xs-medium'>{t('datasetHitTesting.settingTitle')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
: <Tooltip
|
||||
popupContent={t('dataset.retrieval.changeRetrievalMethod')}
|
||||
>
|
||||
<div
|
||||
onClick={onClickRetrievalMethod}
|
||||
className='flex px-2 h-7 items-center space-x-1 bg-white hover:bg-[#ECE9FE] rounded-md shadow-sm cursor-pointer text-[#6927DA]'
|
||||
>
|
||||
<Icon className='w-3.5 h-3.5'></Icon>
|
||||
<div className='text-xs font-medium'>{t(`dataset.retrieval.${retrievalMethod}.title`)}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
isSettingsOpen && (
|
||||
<ModifyExternalRetrievalModal
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
onSave={handleSaveExternalRetrievalSettings}
|
||||
initialTopK={externalRetrievalSettings.top_k}
|
||||
initialScoreThreshold={externalRetrievalSettings.score_threshold}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className='h-2 rounded-tl-xl rounded-tr-xl bg-white'></div>
|
||||
</div>
|
||||
<div className='px-4 pb-11'>
|
||||
@ -122,7 +183,7 @@ const TextAreaWithButton = ({
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
onClick={isExternal ? externalRetrievalTestingOnSubmit : onSubmit}
|
||||
variant="primary"
|
||||
loading={loading}
|
||||
disabled={(!text?.length || text?.length > 200)}
|
||||
@ -132,7 +193,6 @@ const TextAreaWithButton = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
@ -8,11 +8,14 @@ import { useSWRConfig } from 'swr'
|
||||
import { unstable_serialize } from 'swr/infinite'
|
||||
import PermissionSelector from '../permission-selector'
|
||||
import IndexMethodRadio from '../index-method-radio'
|
||||
import RetrievalSettings from '../../external-knowledge-base/create/RetrievalSettings'
|
||||
import cn from '@/utils/classnames'
|
||||
import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
|
||||
import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { updateDatasetSetting } from '@/service/datasets'
|
||||
import type { DataSetListResponse } from '@/models/datasets'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
@ -55,6 +58,8 @@ const Form = () => {
|
||||
const [name, setName] = useState(currentDataset?.name ?? '')
|
||||
const [description, setDescription] = useState(currentDataset?.description ?? '')
|
||||
const [permission, setPermission] = useState(currentDataset?.permission)
|
||||
const [topK, setTopK] = useState(currentDataset?.external_retrieval_model.top_k ?? 2)
|
||||
const [scoreThreshold, setScoreThreshold] = useState(currentDataset?.external_retrieval_model.score_threshold ?? 0.5)
|
||||
const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>(currentDataset?.partial_member_list || [])
|
||||
const [memberList, setMemberList] = useState<Member[]>([])
|
||||
const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
|
||||
@ -85,6 +90,13 @@ const Form = () => {
|
||||
setMemberList(accounts)
|
||||
}
|
||||
|
||||
const handleSettingsChange = (data: { top_k?: number; score_threshold?: number }) => {
|
||||
if (data.top_k !== undefined)
|
||||
setTopK(data.top_k)
|
||||
if (data.score_threshold !== undefined)
|
||||
setScoreThreshold(data.score_threshold)
|
||||
}
|
||||
|
||||
useMount(() => {
|
||||
getMembers()
|
||||
})
|
||||
@ -126,10 +138,16 @@ const Form = () => {
|
||||
description,
|
||||
permission,
|
||||
indexing_technique: indexMethod,
|
||||
external_retrieval_model: {
|
||||
top_k: topK,
|
||||
score_threshold: scoreThreshold,
|
||||
},
|
||||
retrieval_model: {
|
||||
...postRetrievalConfig,
|
||||
score_threshold: postRetrievalConfig.score_threshold_enabled ? postRetrievalConfig.score_threshold : 0,
|
||||
},
|
||||
external_knowledge_id: currentDataset!.external_knowledge_info.external_knowledge_id,
|
||||
external_knowledge_api_id: currentDataset!.external_knowledge_info.external_knowledge_api_id,
|
||||
embedding_model: embeddingModel.model,
|
||||
embedding_model_provider: embeddingModel.provider,
|
||||
},
|
||||
@ -161,7 +179,7 @@ const Form = () => {
|
||||
<div className='w-full sm:w-[800px] p-4 sm:px-16 sm:py-6'>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div>{t('datasetSettings.form.name')}</div>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.name')}</div>
|
||||
</div>
|
||||
<div className='w-full max-w-[480px]'>
|
||||
<input
|
||||
@ -174,7 +192,7 @@ const Form = () => {
|
||||
</div>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div>{t('datasetSettings.form.desc')}</div>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.desc')}</div>
|
||||
</div>
|
||||
<div className='w-full max-w-[480px]'>
|
||||
<textarea
|
||||
@ -192,7 +210,7 @@ const Form = () => {
|
||||
</div>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div>{t('datasetSettings.form.permissions')}</div>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.permissions')}</div>
|
||||
</div>
|
||||
<div className='w-full sm:w-[480px]'>
|
||||
<PermissionSelector
|
||||
@ -210,7 +228,7 @@ const Form = () => {
|
||||
<div className='w-full h-0 border-b-[0.5px] border-b-gray-200 my-2' />
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div>{t('datasetSettings.form.indexMethod')}</div>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.indexMethod')}</div>
|
||||
</div>
|
||||
<div className='w-full sm:w-[480px]'>
|
||||
<IndexMethodRadio
|
||||
@ -225,7 +243,7 @@ const Form = () => {
|
||||
{indexMethod === 'high_quality' && (
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div>{t('datasetSettings.form.embeddingModel')}</div>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.embeddingModel')}</div>
|
||||
</div>
|
||||
<div className='w-[480px]'>
|
||||
<ModelSelector
|
||||
@ -240,32 +258,75 @@ const Form = () => {
|
||||
</div>
|
||||
)}
|
||||
{/* Retrieval Method Config */}
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div>
|
||||
<div>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-500'>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
{t('datasetSettings.form.retrievalSetting.description')}
|
||||
{currentDataset?.provider === 'external'
|
||||
? <>
|
||||
<div className={rowClass}><Divider/></div>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
</div>
|
||||
<RetrievalSettings
|
||||
topK={topK}
|
||||
scoreThreshold={scoreThreshold}
|
||||
onChange={handleSettingsChange}
|
||||
isInRetrievalSetting={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClass}><Divider/></div>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.externalKnowledgeAPI')}</div>
|
||||
</div>
|
||||
<div className='w-full max-w-[480px]'>
|
||||
<div className='flex h-full px-3 py-2 items-center gap-1 rounded-lg bg-components-input-bg-normal'>
|
||||
<ApiConnectionMod className='w-4 h-4 text-text-secondary' />
|
||||
<div className='overflow-hidden text-text-secondary text-ellipsis system-sm-medium'>
|
||||
{currentDataset?.external_knowledge_info.external_knowledge_api_name}
|
||||
</div>
|
||||
<div className='text-text-tertiary system-xs-regular'>·</div>
|
||||
<div className='text-text-tertiary system-xs-regular'>{currentDataset?.external_knowledge_info.external_knowledge_api_endpoint}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.externalKnowledgeID')}</div>
|
||||
</div>
|
||||
<div className='w-full max-w-[480px]'>
|
||||
<div className='flex h-full px-3 py-2 items-center gap-1 rounded-lg bg-components-input-bg-normal'>
|
||||
<div className='text-text-tertiary system-xs-regular'>{currentDataset?.external_knowledge_info.external_knowledge_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={rowClass}><Divider/></div>
|
||||
</>
|
||||
: <div className={rowClass}>
|
||||
<div className={labelClass}>
|
||||
<div>
|
||||
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-500'>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
{t('datasetSettings.form.retrievalSetting.description')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-[480px]'>
|
||||
{indexMethod === 'high_quality'
|
||||
? (
|
||||
<RetrievalMethodConfig
|
||||
value={retrievalConfig}
|
||||
onChange={setRetrievalConfig}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<EconomicalRetrievalMethodConfig
|
||||
value={retrievalConfig}
|
||||
onChange={setRetrievalConfig}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-[480px]'>
|
||||
{indexMethod === 'high_quality'
|
||||
? (
|
||||
<RetrievalMethodConfig
|
||||
value={retrievalConfig}
|
||||
onChange={setRetrievalConfig}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<EconomicalRetrievalMethodConfig
|
||||
value={retrievalConfig}
|
||||
onChange={setRetrievalConfig}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={rowClass}>
|
||||
<div className={labelClass} />
|
||||
<div className='w-[480px]'>
|
||||
|
@ -51,7 +51,7 @@ const DatasetNav = () => {
|
||||
navs={datasetItems.map(dataset => ({
|
||||
id: dataset.id,
|
||||
name: dataset.name,
|
||||
link: `/datasets/${dataset.id}/documents`,
|
||||
link: dataset.provider === 'external' ? `/datasets/${dataset.id}/hitTesting` : `/datasets/${dataset.id}/documents`,
|
||||
icon: dataset.icon,
|
||||
icon_background: dataset.icon_background,
|
||||
})) as NavItem[]}
|
||||
|
@ -1,13 +1,15 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import FileIcon from '@/app/components/base/file-icon'
|
||||
import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import SettingsModal from '@/app/components/app/configuration/dataset-config/settings-modal'
|
||||
@ -30,8 +32,10 @@ const DatasetItem: FC<Props> = ({
|
||||
readonly,
|
||||
}) => {
|
||||
const media = useBreakpoints()
|
||||
const { t } = useTranslation()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const { formatIndexingTechniqueAndMethod } = useKnowledge()
|
||||
const [isDeleteHovered, setIsDeleteHovered] = useState(false)
|
||||
|
||||
const [isShowSettingsModal, {
|
||||
setTrue: showSettingsModal,
|
||||
@ -44,7 +48,12 @@ const DatasetItem: FC<Props> = ({
|
||||
}, [hideSettingsModal, onChange])
|
||||
|
||||
return (
|
||||
<div className='flex items-center h-10 justify-between rounded-xl px-2 bg-white border border-gray-200 cursor-pointer group/dataset-item'>
|
||||
<div className={`flex items-center h-10 justify-between rounded-xl px-2 border-[0.5px]
|
||||
border-components-panel-border-subtle cursor-pointer group/dataset-item
|
||||
${isDeleteHovered
|
||||
? 'bg-state-destructive-hover border-state-destructive-border'
|
||||
: 'bg-components-panel-on-panel-item-bg hover:bg-components-panel-on-panel-item-bg-hover'
|
||||
}`}>
|
||||
<div className='w-0 grow flex items-center space-x-1.5'>
|
||||
{
|
||||
payload.data_source_type === DataSourceType.NOTION
|
||||
@ -61,24 +70,33 @@ const DatasetItem: FC<Props> = ({
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className='hidden group-hover/dataset-item:flex shrink-0 ml-2 items-center space-x-1'>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
|
||||
<ActionButton
|
||||
onClick={showSettingsModal}
|
||||
>
|
||||
<RiEditLine className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
|
||||
<RiEditLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={onRemove}
|
||||
state={ActionButtonState.Destructive}
|
||||
onMouseEnter={() => setIsDeleteHovered(true)}
|
||||
onMouseLeave={() => setIsDeleteHovered(false)}
|
||||
>
|
||||
<RiDeleteBinLine className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
<RiDeleteBinLine className={`w-4 h-4 flex-shrink-0 ${isDeleteHovered ? 'text-text-destructive' : 'text-text-tertiary'}`} />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
<Badge
|
||||
className='group-hover/dataset-item:hidden shrink-0'
|
||||
text={formatIndexingTechniqueAndMethod(payload.indexing_technique, payload.retrieval_model_dict?.search_method)}
|
||||
/>
|
||||
{
|
||||
payload.indexing_technique && <Badge
|
||||
className='group-hover/dataset-item:hidden shrink-0'
|
||||
text={formatIndexingTechniqueAndMethod(payload.indexing_technique, payload.retrieval_model_dict?.search_method)}
|
||||
/>
|
||||
}
|
||||
{
|
||||
payload.provider === 'external' && <Badge
|
||||
className='group-hover/dataset-item:hidden shrink-0'
|
||||
text={t('dataset.externalTag')}
|
||||
/>
|
||||
}
|
||||
|
||||
{isShowSettingsModal && (
|
||||
<Drawer isOpen={isShowSettingsModal} onClose={hideSettingsModal} footer={null} mask={isMobile} panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
|
||||
|
@ -21,6 +21,9 @@ export const getSelectedDatasetsMode = (datasets: DataSet[]) => {
|
||||
let allHighQualityFullTextSearch = true
|
||||
let allEconomic = true
|
||||
let mixtureHighQualityAndEconomic = true
|
||||
let allExternal = true
|
||||
let allInternal = true
|
||||
let mixtureInternalAndExternal = true
|
||||
let inconsistentEmbeddingModel = false
|
||||
if (!datasets.length) {
|
||||
allHighQuality = false
|
||||
@ -29,6 +32,9 @@ export const getSelectedDatasetsMode = (datasets: DataSet[]) => {
|
||||
allEconomic = false
|
||||
mixtureHighQualityAndEconomic = false
|
||||
inconsistentEmbeddingModel = false
|
||||
allExternal = false
|
||||
allInternal = false
|
||||
mixtureInternalAndExternal = false
|
||||
}
|
||||
datasets.forEach((dataset) => {
|
||||
if (dataset.indexing_technique === 'economy') {
|
||||
@ -45,8 +51,21 @@ export const getSelectedDatasetsMode = (datasets: DataSet[]) => {
|
||||
if (dataset.retrieval_model_dict.search_method !== RETRIEVE_METHOD.fullText)
|
||||
allHighQualityFullTextSearch = false
|
||||
}
|
||||
if (dataset.provider !== 'external') {
|
||||
allExternal = false
|
||||
}
|
||||
else {
|
||||
allInternal = false
|
||||
allHighQuality = false
|
||||
allHighQualityVectorSearch = false
|
||||
allHighQualityFullTextSearch = false
|
||||
mixtureHighQualityAndEconomic = false
|
||||
}
|
||||
})
|
||||
|
||||
if (allExternal || allInternal)
|
||||
mixtureInternalAndExternal = false
|
||||
|
||||
if (allHighQuality || allEconomic)
|
||||
mixtureHighQualityAndEconomic = false
|
||||
|
||||
@ -59,6 +78,9 @@ export const getSelectedDatasetsMode = (datasets: DataSet[]) => {
|
||||
allHighQualityFullTextSearch,
|
||||
allEconomic,
|
||||
mixtureHighQualityAndEconomic,
|
||||
allInternal,
|
||||
allExternal,
|
||||
mixtureInternalAndExternal,
|
||||
inconsistentEmbeddingModel,
|
||||
} as SelectedDatasetsMode
|
||||
}
|
||||
@ -70,6 +92,9 @@ export const getMultipleRetrievalConfig = (multipleRetrievalConfig: MultipleRetr
|
||||
allHighQualityFullTextSearch,
|
||||
allEconomic,
|
||||
mixtureHighQualityAndEconomic,
|
||||
allInternal,
|
||||
allExternal,
|
||||
mixtureInternalAndExternal,
|
||||
inconsistentEmbeddingModel,
|
||||
} = getSelectedDatasetsMode(selectedDatasets)
|
||||
|
||||
@ -91,13 +116,13 @@ export const getMultipleRetrievalConfig = (multipleRetrievalConfig: MultipleRetr
|
||||
reranking_enable: allEconomic ? reranking_enable : true,
|
||||
}
|
||||
|
||||
if (allEconomic || mixtureHighQualityAndEconomic || inconsistentEmbeddingModel)
|
||||
if (allEconomic || mixtureHighQualityAndEconomic || inconsistentEmbeddingModel || allExternal || mixtureInternalAndExternal)
|
||||
result.reranking_mode = RerankingModeEnum.RerankingModel
|
||||
|
||||
if (allHighQuality && !inconsistentEmbeddingModel && reranking_mode === undefined)
|
||||
if (allHighQuality && !inconsistentEmbeddingModel && reranking_mode === undefined && allInternal)
|
||||
result.reranking_mode = RerankingModeEnum.WeightedScore
|
||||
|
||||
if (allHighQuality && !inconsistentEmbeddingModel && (reranking_mode === RerankingModeEnum.WeightedScore || reranking_mode === undefined) && !weights) {
|
||||
if (allHighQuality && !inconsistentEmbeddingModel && (reranking_mode === RerankingModeEnum.WeightedScore || reranking_mode === undefined) && allInternal && !weights) {
|
||||
result.weights = {
|
||||
vector_setting: {
|
||||
vector_weight: allHighQualityVectorSearch
|
||||
|
46
web/context/external-knowledge-api-context.tsx
Normal file
46
web/context/external-knowledge-api-context.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useMemo } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import type { ExternalAPIItem, ExternalAPIListResponse } from '@/models/datasets'
|
||||
import { fetchExternalAPIList } from '@/service/datasets'
|
||||
|
||||
type ExternalKnowledgeApiContextType = {
|
||||
externalKnowledgeApiList: ExternalAPIItem[]
|
||||
mutateExternalKnowledgeApis: () => Promise<ExternalAPIListResponse | undefined>
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const ExternalKnowledgeApiContext = createContext<ExternalKnowledgeApiContextType | undefined>(undefined)
|
||||
|
||||
export type ExternalKnowledgeApiProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const ExternalKnowledgeApiProvider: FC<ExternalKnowledgeApiProviderProps> = ({ children }) => {
|
||||
const { data, mutate: mutateExternalKnowledgeApis, isLoading } = useSWR<ExternalAPIListResponse>(
|
||||
{ url: '/datasets/external-knowledge-api' },
|
||||
fetchExternalAPIList,
|
||||
)
|
||||
|
||||
const contextValue = useMemo<ExternalKnowledgeApiContextType>(() => ({
|
||||
externalKnowledgeApiList: data?.data || [],
|
||||
mutateExternalKnowledgeApis,
|
||||
isLoading,
|
||||
}), [data, mutateExternalKnowledgeApis, isLoading])
|
||||
|
||||
return (
|
||||
<ExternalKnowledgeApiContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ExternalKnowledgeApiContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useExternalKnowledgeApi = () => {
|
||||
const context = useContext(ExternalKnowledgeApiContext)
|
||||
if (context === undefined)
|
||||
throw new Error('useExternalKnowledgeApi must be used within a ExternalKnowledgeApiProvider')
|
||||
|
||||
return context
|
||||
}
|
@ -10,6 +10,7 @@ import ModerationSettingModal from '@/app/components/app/configuration/toolbox/m
|
||||
import ExternalDataToolModal from '@/app/components/app/configuration/tools/external-data-tool-modal'
|
||||
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
||||
import ModelModal from '@/app/components/header/account-setting/model-provider-page/model-modal'
|
||||
import ExternalAPIModal from '@/app/components/datasets/external-api/external-api-modal'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
CustomConfigurationModelFixedFields,
|
||||
@ -23,6 +24,7 @@ import type {
|
||||
ApiBasedExtension,
|
||||
ExternalDataTool,
|
||||
} from '@/models/common'
|
||||
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations'
|
||||
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'
|
||||
@ -32,7 +34,10 @@ export type ModalState<T> = {
|
||||
onCancelCallback?: () => void
|
||||
onSaveCallback?: (newPayload: T) => void
|
||||
onRemoveCallback?: (newPayload: T) => void
|
||||
onEditCallback?: (newPayload: T) => void
|
||||
onValidateBeforeSaveCallback?: (newPayload: T) => boolean
|
||||
isEditMode?: boolean
|
||||
datasetBindings?: { id: string; name: string }[]
|
||||
}
|
||||
|
||||
export type ModelModalType = {
|
||||
@ -52,6 +57,7 @@ export type ModalContextState = {
|
||||
setShowPricingModal: () => void
|
||||
setShowAnnotationFullModal: () => void
|
||||
setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>>
|
||||
setShowExternalKnowledgeAPIModal: Dispatch<SetStateAction<ModalState<CreateExternalAPIReq> | null>>
|
||||
setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
|
||||
setShowModelLoadBalancingEntryModal: Dispatch<SetStateAction<ModalState<LoadBalancingEntryModalType> | null>>
|
||||
}
|
||||
@ -63,6 +69,7 @@ const ModalContext = createContext<ModalContextState>({
|
||||
setShowPricingModal: () => { },
|
||||
setShowAnnotationFullModal: () => { },
|
||||
setShowModelModal: () => { },
|
||||
setShowExternalKnowledgeAPIModal: () => { },
|
||||
setShowModelLoadBalancingModal: () => { },
|
||||
setShowModelLoadBalancingEntryModal: () => { },
|
||||
})
|
||||
@ -86,6 +93,7 @@ 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 [showExternalKnowledgeAPIModal, setShowExternalKnowledgeAPIModal] = useState<ModalState<CreateExternalAPIReq> | null>(null)
|
||||
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState<ModelLoadBalancingModalProps | null>(null)
|
||||
const [showModelLoadBalancingEntryModal, setShowModelLoadBalancingEntryModal] = useState<ModalState<LoadBalancingEntryModalType> | null>(null)
|
||||
const searchParams = useSearchParams()
|
||||
@ -122,6 +130,24 @@ export const ModalContextProvider = ({
|
||||
setShowModelModal(null)
|
||||
}, [showModelModal])
|
||||
|
||||
const handleCancelExternalApiModal = useCallback(() => {
|
||||
setShowExternalKnowledgeAPIModal(null)
|
||||
if (showExternalKnowledgeAPIModal?.onCancelCallback)
|
||||
showExternalKnowledgeAPIModal.onCancelCallback()
|
||||
}, [showExternalKnowledgeAPIModal])
|
||||
|
||||
const handleSaveExternalApiModal = useCallback(async (updatedFormValue: CreateExternalAPIReq) => {
|
||||
if (showExternalKnowledgeAPIModal?.onSaveCallback)
|
||||
showExternalKnowledgeAPIModal.onSaveCallback(updatedFormValue)
|
||||
setShowExternalKnowledgeAPIModal(null)
|
||||
}, [showExternalKnowledgeAPIModal])
|
||||
|
||||
const handleEditExternalApiModal = useCallback(async (updatedFormValue: CreateExternalAPIReq) => {
|
||||
if (showExternalKnowledgeAPIModal?.onEditCallback)
|
||||
showExternalKnowledgeAPIModal.onEditCallback(updatedFormValue)
|
||||
setShowExternalKnowledgeAPIModal(null)
|
||||
}, [showExternalKnowledgeAPIModal])
|
||||
|
||||
const handleCancelModelLoadBalancingEntryModal = useCallback(() => {
|
||||
showModelLoadBalancingEntryModal?.onCancelCallback?.()
|
||||
setShowModelLoadBalancingEntryModal(null)
|
||||
@ -173,6 +199,7 @@ export const ModalContextProvider = ({
|
||||
setShowPricingModal: () => setShowPricingModal(true),
|
||||
setShowAnnotationFullModal: () => setShowAnnotationFullModal(true),
|
||||
setShowModelModal,
|
||||
setShowExternalKnowledgeAPIModal,
|
||||
setShowModelLoadBalancingModal,
|
||||
setShowModelLoadBalancingEntryModal,
|
||||
}}>
|
||||
@ -245,6 +272,18 @@ export const ModalContextProvider = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!showExternalKnowledgeAPIModal && (
|
||||
<ExternalAPIModal
|
||||
data={showExternalKnowledgeAPIModal.payload}
|
||||
datasetBindings={showExternalKnowledgeAPIModal.datasetBindings ?? []}
|
||||
onSave={handleSaveExternalApiModal}
|
||||
onCancel={handleCancelExternalApiModal}
|
||||
onEdit={handleEditExternalApiModal}
|
||||
isEditMode={showExternalKnowledgeAPIModal.isEditMode ?? false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
Boolean(showModelLoadBalancingModal) && (
|
||||
<ModelLoadBalancingModal {...showModelLoadBalancingModal!} />
|
||||
|
@ -1,6 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Retrieval Testing',
|
||||
desc: 'Test the hitting effect of the Knowledge based on the given query text.',
|
||||
settingTitle: 'Retrieval Setting',
|
||||
desc: 'Test the hitting effect of the Knowledge based on the given query text',
|
||||
dateTimeFormat: 'MM/DD/YYYY hh:mm A',
|
||||
recents: 'Recents',
|
||||
table: {
|
||||
|
@ -1,13 +1,13 @@
|
||||
const translation = {
|
||||
title: 'Knowledge settings',
|
||||
desc: 'Here you can modify the properties and working methods of the Knowledge.',
|
||||
desc: 'Here you can modify the properties and retrieval settings of this Knowledge.',
|
||||
form: {
|
||||
name: 'Knowledge Name',
|
||||
namePlaceholder: 'Please enter the Knowledge name',
|
||||
nameError: 'Name cannot be empty',
|
||||
desc: 'Knowledge description',
|
||||
desc: 'Knowledge Description',
|
||||
descInfo: 'Please write a clear textual description to outline the content of the Knowledge. This description will be used as a basis for matching when selecting from multiple Knowledge for inference.',
|
||||
descPlaceholder: 'Describe what is in this Knowledge. A detailed description allows AI to access the content of the Knowledge in a timely manner. If empty, Dify will use the default hit strategy.',
|
||||
descPlaceholder: 'Describe what\'s in this Knowledge (optional)',
|
||||
descWrite: 'Learn how to write a good Knowledge description.',
|
||||
permissions: 'Permissions',
|
||||
permissionsOnlyMe: 'Only me',
|
||||
@ -23,11 +23,14 @@ const translation = {
|
||||
embeddingModelTip: 'Change the embedded model, please go to ',
|
||||
embeddingModelTipLink: 'Settings',
|
||||
retrievalSetting: {
|
||||
title: 'Retrieval setting',
|
||||
title: 'Retrieval Setting',
|
||||
learnMore: 'Learn more',
|
||||
description: ' about retrieval method.',
|
||||
longDescription: ' about retrieval method, you can change this at any time in the Knowledge settings.',
|
||||
},
|
||||
externalKnowledgeAPI: 'External Knowledge API',
|
||||
externalKnowledgeID: 'External Knowledge ID',
|
||||
retrievalSettings: 'Retrieval Settings',
|
||||
save: 'Save',
|
||||
},
|
||||
}
|
||||
|
@ -1,9 +1,53 @@
|
||||
const translation = {
|
||||
knowledge: 'Knowledge',
|
||||
externalTag: 'External',
|
||||
externalAPI: 'External API',
|
||||
externalAPIPanelTitle: 'External Knowledge API',
|
||||
externalKnowledgeId: 'External Knowledge ID',
|
||||
externalKnowledgeName: 'External Knowledge Name',
|
||||
externalKnowledgeDescription: 'Knowledge Description',
|
||||
externalKnowledgeIdPlaceholder: 'Please enter the Knowledge ID',
|
||||
externalKnowledgeNamePlaceholder: 'Please enter the name of the knowledge base',
|
||||
externalKnowledgeDescriptionPlaceholder: 'Describe what\'s in this Knowledge Base (optional)',
|
||||
learnHowToWriteGoodKnowledgeDescription: 'Learn how to write a good knowledge description',
|
||||
externalAPIPanelDescription: 'The external knowledge API is used to connect to a knowledge base outside of Dify and retrieve knowledge from that knowledge base.',
|
||||
externalAPIPanelDocumentation: 'Learn how to create an External Knowledge API',
|
||||
documentCount: ' docs',
|
||||
wordCount: ' k words',
|
||||
appCount: ' linked apps',
|
||||
createDataset: 'Create Knowledge',
|
||||
createExternalAPI: 'Add an External Knowledge API',
|
||||
editExternalAPIFormTitle: 'Edit the External Knowledge API',
|
||||
editExternalAPITooltipTitle: 'LINKED KNOWLEDGE',
|
||||
editExternalAPIConfirmWarningContent: {
|
||||
front: 'This External Knowledge API is linked to',
|
||||
end: 'external knowledge, and this modification will be applied to all of them. Are you sure you want to save this change?',
|
||||
},
|
||||
editExternalAPIFormWarning: {
|
||||
front: 'This External API is linked to',
|
||||
end: 'external knowledge',
|
||||
},
|
||||
deleteExternalAPIConfirmWarningContent: {
|
||||
title: {
|
||||
front: 'Delete',
|
||||
end: '?',
|
||||
},
|
||||
content: {
|
||||
front: 'This External Knowledge API is linked to',
|
||||
end: 'external knowledge. Deleting this API will invalidate all of them. Are you sure you want to delete this API?',
|
||||
},
|
||||
noConnectionContent: 'Are you sure to delete this API?',
|
||||
},
|
||||
connectDataset: 'Connect to an External Knowledge Base',
|
||||
connectDatasetIntro: {
|
||||
title: 'How to Connect to an External Knowledge Base',
|
||||
content: {
|
||||
front: 'To connect to an external knowledge base, you need to create an external API first. Please read carefully and refer to',
|
||||
link: 'Learn how to create an external API',
|
||||
end: '. Then find the corresponding knowledge ID and fill it in the form on the left. If all the information is correct, it will automatically jump to the retrieval test in the knowledge base after clicking the connect button.',
|
||||
},
|
||||
learnMore: 'Learn More',
|
||||
},
|
||||
createDatasetIntro: 'Import your own text data or write data in real-time via Webhook for LLM context enhancement.',
|
||||
deleteDatasetConfirmTitle: 'Delete this Knowledge?',
|
||||
deleteDatasetConfirmContent:
|
||||
@ -22,6 +66,22 @@ const translation = {
|
||||
unavailableTip: 'Embedding model is not available, the default embedding model needs to be configured',
|
||||
datasets: 'KNOWLEDGE',
|
||||
datasetsApi: 'API ACCESS',
|
||||
externalKnowledgeForm: {
|
||||
connect: 'Connect',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
externalAPIForm: {
|
||||
name: 'Name',
|
||||
endpoint: 'API Endpoint',
|
||||
apiKey: 'API Key',
|
||||
save: 'Save',
|
||||
cancel: 'Cancel',
|
||||
edit: 'Edit',
|
||||
encrypted: {
|
||||
front: 'Your API Token will be encrypted and stored using',
|
||||
end: 'technology.',
|
||||
},
|
||||
},
|
||||
retrieval: {
|
||||
semantic_search: {
|
||||
title: 'Vector Search',
|
||||
@ -58,6 +118,8 @@ const translation = {
|
||||
defaultRetrievalTip: 'Multi-path retrieval is used by default. Knowledge is retrieved from multiple knowledge bases and then re-ranked.',
|
||||
mixtureHighQualityAndEconomicTip: 'The Rerank model is required for mixture of high quality and economical knowledge bases.',
|
||||
inconsistentEmbeddingModelTip: 'The Rerank model is required if the Embedding models of the selected knowledge bases are inconsistent.',
|
||||
mixtureInternalAndExternalTip: 'The Rerank model is required for mixture of internal and external knowledge.',
|
||||
allExternalTip: 'When using external knowledge only, the user can choose whether to enable the Rerank model. If not enabled, retrieved chunks will be sorted based on scores. When the retrieval strategies of different knowledge bases are inconsistent, it will be inaccurate.',
|
||||
retrievalSettings: 'Retrieval Setting',
|
||||
rerankSettings: 'Rerank Setting',
|
||||
weightedScore: {
|
||||
|
@ -1,6 +1,7 @@
|
||||
const translation = {
|
||||
title: '召回测试',
|
||||
desc: '基于给定的查询文本测试知识库的召回效果。',
|
||||
settingTitle: '召回设置',
|
||||
desc: '基于给定的查询文本测试知识库的召回效果',
|
||||
dateTimeFormat: 'YYYY-MM-DD HH:mm',
|
||||
recents: '最近查询',
|
||||
table: {
|
||||
|
@ -1,13 +1,13 @@
|
||||
const translation = {
|
||||
title: '知识库设置',
|
||||
desc: '在这里您可以修改知识库的工作方式以及其它设置。',
|
||||
desc: '在这里,您可以修改此知识库的属性和检索设置',
|
||||
form: {
|
||||
name: '知识库名称',
|
||||
namePlaceholder: '请输入知识库名称',
|
||||
nameError: '名称不能为空',
|
||||
desc: '知识库描述',
|
||||
descInfo: '请写出清楚的文字描述来概述知识库的内容。当从多个知识库中进行选择匹配时,该描述将用作匹配的基础。',
|
||||
descPlaceholder: '描述这个知识库中的内容。详细的描述可以让 AI 及时访问知识库的内容。如果为空,Dify 将使用默认的命中策略。',
|
||||
descPlaceholder: '请描述这个知识库包含的内容(可选)',
|
||||
descWrite: '了解如何编写更好的知识库描述。',
|
||||
permissions: '可见权限',
|
||||
permissionsOnlyMe: '只有我',
|
||||
@ -28,6 +28,8 @@ const translation = {
|
||||
description: '关于检索方法。',
|
||||
longDescription: '关于检索方法,您可以随时在知识库设置中更改此设置。',
|
||||
},
|
||||
externalKnowledgeAPI: '外部知识 API',
|
||||
externalKnowledgeID: '外部知识库 ID',
|
||||
save: '保存',
|
||||
},
|
||||
}
|
||||
|
@ -1,9 +1,53 @@
|
||||
const translation = {
|
||||
knowledge: '知识库',
|
||||
externalTag: '外部',
|
||||
externalAPI: '外部 API',
|
||||
externalAPIPanelTitle: '外部知识库 API',
|
||||
externalKnowledgeId: '外部知识库 ID',
|
||||
externalKnowledgeName: '外部知识库名称',
|
||||
externalKnowledgeDescription: '知识库描述',
|
||||
externalKnowledgeIdPlaceholder: '请输入外部知识库 ID',
|
||||
externalKnowledgeNamePlaceholder: '请输入外部知识库名称',
|
||||
externalKnowledgeDescriptionPlaceholder: '描述知识库内容(可选)',
|
||||
learnHowToWriteGoodKnowledgeDescription: '了解如何编写良好的知识库描述',
|
||||
externalAPIPanelDescription: '外部知识库 API 用于连接到 Dify 之外的知识库并从中检索知识。',
|
||||
externalAPIPanelDocumentation: '了解如何创建外部知识库 API',
|
||||
documentCount: ' 文档',
|
||||
wordCount: ' 千字符',
|
||||
appCount: ' 关联应用',
|
||||
createDataset: '创建知识库',
|
||||
createExternalAPI: '添加外部知识库 API',
|
||||
editExternalAPIFormTitle: '编辑外部知识库 API',
|
||||
editExternalAPITooltipTitle: '个关联知识库',
|
||||
editExternalAPIConfirmWarningContent: {
|
||||
front: '此外部知识库 API 已链接到',
|
||||
end: '个外部知识库,此修改将应用于所有这些知识库。您确定要保存此更改吗?',
|
||||
},
|
||||
editExternalAPIFormWarning: {
|
||||
front: '此外部 API 已链接到',
|
||||
end: '外部知识库',
|
||||
},
|
||||
deleteExternalAPIConfirmWarningContent: {
|
||||
title: {
|
||||
front: '删除',
|
||||
end: '?',
|
||||
},
|
||||
content: {
|
||||
front: '此外部知识库 API 已链接到',
|
||||
end: '个外部知识库。删除此 API 将使所有这些知识库失效。您确定要删除此 API 吗?',
|
||||
},
|
||||
noConnectionContent: '您确定要删除此 API 吗?',
|
||||
},
|
||||
connectDatasetIntro: {
|
||||
title: '如何连接到外部知识库',
|
||||
content: {
|
||||
front: '要连接到外部知识库,您需要先创建一个外部 API。请仔细阅读并参考',
|
||||
link: '了解如何创建外部 API',
|
||||
end: '。然后找到相应的知识库 ID 并填写在左侧表单中。如果所有信息正确,点击连接按钮后将自动跳转到知识库中的检索测试。',
|
||||
},
|
||||
learnMore: '了解更多',
|
||||
},
|
||||
connectDataset: '连接外部知识库',
|
||||
createDatasetIntro: '导入您自己的文本数据或通过 Webhook 实时写入数据以增强 LLM 的上下文。',
|
||||
deleteDatasetConfirmTitle: '要删除知识库吗?',
|
||||
deleteDatasetConfirmContent:
|
||||
@ -22,6 +66,22 @@ const translation = {
|
||||
unavailableTip: '由于 embedding 模型不可用,需要配置默认 embedding 模型',
|
||||
datasets: '知识库',
|
||||
datasetsApi: 'API',
|
||||
externalKnowledgeForm: {
|
||||
connect: '连接',
|
||||
cancel: '取消',
|
||||
},
|
||||
externalAPIForm: {
|
||||
name: '名称',
|
||||
endpoint: 'API 端点',
|
||||
apiKey: 'API 密钥',
|
||||
save: '保存',
|
||||
cancel: '取消',
|
||||
edit: '编辑',
|
||||
encrypted: {
|
||||
front: '您的 API Token 将使用',
|
||||
end: '加密并存储。',
|
||||
},
|
||||
},
|
||||
retrieval: {
|
||||
semantic_search: {
|
||||
title: '向量检索',
|
||||
@ -58,6 +118,8 @@ const translation = {
|
||||
defaultRetrievalTip: '默认情况下使用多路召回。从多个知识库中检索知识,然后重新排序。',
|
||||
mixtureHighQualityAndEconomicTip: '混合使用高质量和经济型知识库需要配置 Rerank 模型。',
|
||||
inconsistentEmbeddingModelTip: '当所选知识库配置的 Embedding 模型不一致时,需要配置 Rerank 模型。',
|
||||
mixtureInternalAndExternalTip: '混合使用内部和外部知识时需要配置 Rerank 模型。',
|
||||
allExternalTip: '仅使用外部知识时,用户可以选择是否启用 Rerank 模型。如果不启用,检索到的文本块将根据分数排序。当不同知识库的检索策略不一致时,结果可能不准确。',
|
||||
retrievalSettings: '召回设置',
|
||||
rerankSettings: 'Rerank 设置',
|
||||
weightedScore: {
|
||||
|
@ -25,6 +25,7 @@ export type DataSet = {
|
||||
app_count: number
|
||||
document_count: number
|
||||
word_count: number
|
||||
provider: string
|
||||
embedding_model: string
|
||||
embedding_model_provider: string
|
||||
embedding_available: boolean
|
||||
@ -32,6 +33,57 @@ export type DataSet = {
|
||||
retrieval_model: RetrievalConfig
|
||||
tags: Tag[]
|
||||
partial_member_list?: any[]
|
||||
external_knowledge_info: {
|
||||
external_knowledge_id: string
|
||||
external_knowledge_api_id: string
|
||||
external_knowledge_api_name: string
|
||||
external_knowledge_api_endpoint: string
|
||||
}
|
||||
external_retrieval_model: {
|
||||
top_k: number
|
||||
score_threshold: number
|
||||
}
|
||||
}
|
||||
|
||||
export type ExternalAPIItem = {
|
||||
id: string
|
||||
tenant_id: string
|
||||
name: string
|
||||
description: string
|
||||
settings: {
|
||||
endpoint: string
|
||||
api_key: string
|
||||
}
|
||||
dataset_bindings: { id: string; name: string }[]
|
||||
created_by: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type ExternalKnowledgeItem = {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
provider: 'external'
|
||||
permission: DatasetPermission
|
||||
data_source_type: null
|
||||
indexing_technique: null
|
||||
app_count: number
|
||||
document_count: number
|
||||
word_count: number
|
||||
created_by: string
|
||||
created_at: string
|
||||
updated_by: string
|
||||
updated_at: string
|
||||
tags: Tag[]
|
||||
}
|
||||
|
||||
export type ExternalAPIDeleteResponse = {
|
||||
result: 'success' | 'error'
|
||||
}
|
||||
|
||||
export type ExternalAPIUsage = {
|
||||
is_using: boolean
|
||||
count: number
|
||||
}
|
||||
|
||||
export type CustomFile = File & {
|
||||
@ -72,6 +124,14 @@ export type DataSetListResponse = {
|
||||
total: number
|
||||
}
|
||||
|
||||
export type ExternalAPIListResponse = {
|
||||
data: ExternalAPIItem[]
|
||||
has_more: boolean
|
||||
limit: number
|
||||
page: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type QA = {
|
||||
question: string
|
||||
answer: string
|
||||
@ -384,6 +444,16 @@ export type HitTesting = {
|
||||
tsne_position: TsnePosition
|
||||
}
|
||||
|
||||
export type ExternalKnowledgeBaseHitTesting = {
|
||||
content: string
|
||||
title: string
|
||||
score: number
|
||||
metadata: {
|
||||
'x-amz-bedrock-kb-source-uri': string
|
||||
'x-amz-bedrock-kb-data-source-id': string
|
||||
}
|
||||
}
|
||||
|
||||
export type Segment = {
|
||||
id: string
|
||||
document: Document
|
||||
@ -424,6 +494,13 @@ export type HitTestingResponse = {
|
||||
records: Array<HitTesting>
|
||||
}
|
||||
|
||||
export type ExternalKnowledgeBaseHitTestingResponse = {
|
||||
query: {
|
||||
content: string
|
||||
}
|
||||
records: Array<ExternalKnowledgeBaseHitTesting>
|
||||
}
|
||||
|
||||
export type RelatedApp = {
|
||||
id: string
|
||||
name: string
|
||||
@ -461,6 +538,9 @@ export type SelectedDatasetsMode = {
|
||||
allHighQualityFullTextSearch: boolean
|
||||
allEconomic: boolean
|
||||
mixtureHighQualityAndEconomic: boolean
|
||||
allInternal: boolean
|
||||
allExternal: boolean
|
||||
mixtureInternalAndExternal: boolean
|
||||
inconsistentEmbeddingModel: boolean
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,12 @@ import type {
|
||||
DocumentDetailResponse,
|
||||
DocumentListResponse,
|
||||
ErrorDocsResponse,
|
||||
ExternalAPIDeleteResponse,
|
||||
ExternalAPIItem,
|
||||
ExternalAPIListResponse,
|
||||
ExternalAPIUsage,
|
||||
ExternalKnowledgeBaseHitTestingResponse,
|
||||
ExternalKnowledgeItem,
|
||||
FileIndexingEstimateResponse,
|
||||
HitTestingRecordsResponse,
|
||||
HitTestingResponse,
|
||||
@ -23,6 +29,8 @@ import type {
|
||||
SegmentsResponse,
|
||||
createDocumentResponse,
|
||||
} from '@/models/datasets'
|
||||
import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations'
|
||||
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations.ts'
|
||||
import type { CommonResponse, DataSourceNotionWorkspace } from '@/models/common'
|
||||
import type {
|
||||
ApiKeysListResponse,
|
||||
@ -82,6 +90,34 @@ export const deleteDataset: Fetcher<DataSet, string> = (datasetID) => {
|
||||
return del<DataSet>(`/datasets/${datasetID}`)
|
||||
}
|
||||
|
||||
export const fetchExternalAPIList: Fetcher<ExternalAPIListResponse, { url: string }> = ({ url }) => {
|
||||
return get<ExternalAPIListResponse>(url)
|
||||
}
|
||||
|
||||
export const fetchExternalAPI: Fetcher<ExternalAPIItem, { apiTemplateId: string }> = ({ apiTemplateId }) => {
|
||||
return get<ExternalAPIItem>(`/datasets/external-knowledge-api/${apiTemplateId}`)
|
||||
}
|
||||
|
||||
export const updateExternalAPI: Fetcher<ExternalAPIItem, { apiTemplateId: string; body: ExternalAPIItem }> = ({ apiTemplateId, body }) => {
|
||||
return patch<ExternalAPIItem>(`/datasets/external-knowledge-api/${apiTemplateId}`, { body })
|
||||
}
|
||||
|
||||
export const deleteExternalAPI: Fetcher<ExternalAPIDeleteResponse, { apiTemplateId: string }> = ({ apiTemplateId }) => {
|
||||
return del<ExternalAPIDeleteResponse>(`/datasets/external-knowledge-api/${apiTemplateId}`)
|
||||
}
|
||||
|
||||
export const checkUsageExternalAPI: Fetcher<ExternalAPIUsage, { apiTemplateId: string }> = ({ apiTemplateId }) => {
|
||||
return get<ExternalAPIUsage>(`/datasets/external-knowledge-api/${apiTemplateId}/use-check`)
|
||||
}
|
||||
|
||||
export const createExternalAPI: Fetcher<ExternalAPIItem, { body: CreateExternalAPIReq }> = ({ body }) => {
|
||||
return post<ExternalAPIItem>('/datasets/external-knowledge-api', { body })
|
||||
}
|
||||
|
||||
export const createExternalKnowledgeBase: Fetcher<ExternalKnowledgeItem, { body: CreateKnowledgeBaseReq }> = ({ body }) => {
|
||||
return post<ExternalKnowledgeItem>('/datasets/external', { body })
|
||||
}
|
||||
|
||||
export const fetchDefaultProcessRule: Fetcher<ProcessRuleResponse, { url: string }> = ({ url }) => {
|
||||
return get<ProcessRuleResponse>(url)
|
||||
}
|
||||
@ -209,6 +245,10 @@ export const hitTesting: Fetcher<HitTestingResponse, { datasetId: string; queryT
|
||||
return post<HitTestingResponse>(`/datasets/${datasetId}/hit-testing`, { body: { query: queryText, retrieval_model } })
|
||||
}
|
||||
|
||||
export const externalKnowledgeBaseHitTesting: Fetcher<ExternalKnowledgeBaseHitTestingResponse, { datasetId: string; query: string; external_retrieval_model: { top_k: number; score_threshold: number } }> = ({ datasetId, query, external_retrieval_model }) => {
|
||||
return post<ExternalKnowledgeBaseHitTestingResponse>(`/datasets/${datasetId}/external-hit-testing`, { body: { query, external_retrieval_model } })
|
||||
}
|
||||
|
||||
export const fetchTestingRecords: Fetcher<HitTestingRecordsResponse, { datasetId: string; params: { page: number; limit: number } }> = ({ datasetId, params }) => {
|
||||
return get<HitTestingRecordsResponse>(`/datasets/${datasetId}/queries`, { params })
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user