From fbedd08292827d7404247ca930f4df3b15ae3df6 Mon Sep 17 00:00:00 2001 From: Yi Date: Mon, 23 Sep 2024 23:34:01 +0800 Subject: [PATCH 1/5] feat: add external api --- web/app/(commonLayout)/datasets/Container.tsx | 14 ++ .../datasets/NewDatasetCard.tsx | 26 +++- .../solid/development/api-connection-mod.svg | 5 + .../solid/development/ApiConnectionMod.json | 38 +++++ .../solid/development/ApiConnectionMod.tsx | 16 ++ .../src/vender/solid/development/index.ts | 1 + web/app/components/base/modal/index.css | 2 +- .../external-api/external-api-modal/Form.tsx | 0 .../external-api/external-api-modal/index.tsx | 141 ++++++++++++++++++ .../external-api/external-api-panel/index.tsx | 70 +++++++++ web/context/modal-context.tsx | 25 ++++ web/i18n/en-US/dataset.ts | 24 +++ web/i18n/zh-Hans/dataset.ts | 24 +++ web/models/datasets.ts | 25 ++++ web/service/datasets.ts | 10 ++ 15 files changed, 412 insertions(+), 9 deletions(-) create mode 100644 web/app/components/base/icons/assets/vender/solid/development/api-connection-mod.svg create mode 100644 web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.json create mode 100644 web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.tsx create mode 100644 web/app/components/datasets/external-api/external-api-modal/Form.tsx create mode 100644 web/app/components/datasets/external-api/external-api-modal/index.tsx create mode 100644 web/app/components/datasets/external-api/external-api-panel/index.tsx diff --git a/web/app/(commonLayout)/datasets/Container.tsx b/web/app/(commonLayout)/datasets/Container.tsx index f532ca416f..b626da05f5 100644 --- a/web/app/(commonLayout)/datasets/Container.tsx +++ b/web/app/(commonLayout)/datasets/Container.tsx @@ -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,8 @@ 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' // Services import { fetchDatasetApiBaseUrl } from '@/service/datasets' @@ -30,6 +33,7 @@ const Container = () => { const router = useRouter() const { currentWorkspace } = useAppContext() const showTagManagementModal = useTagStore(s => s.showTagManagementModal) + const [showExternalApiPanel, setShowExternalApiPanel] = useState(false) const options = useMemo(() => { return [ @@ -80,6 +84,14 @@ const Container = () => {
+
+
)} {activeTab === 'api' && data && } @@ -96,6 +108,8 @@ const Container = () => { )} {activeTab === 'api' && data && } + + {showExternalApiPanel && setShowExternalApiPanel(false)} isShow={showExternalApiPanel} />}
) diff --git a/web/app/(commonLayout)/datasets/NewDatasetCard.tsx b/web/app/(commonLayout)/datasets/NewDatasetCard.tsx index f76efa5769..19a7ba2ab6 100644 --- a/web/app/(commonLayout)/datasets/NewDatasetCard.tsx +++ b/web/app/(commonLayout)/datasets/NewDatasetCard.tsx @@ -4,21 +4,31 @@ import { forwardRef } from 'react' import { useTranslation } from 'react-i18next' import { RiAddLine, + RiArrowRightLine, } from '@remixicon/react' const CreateAppCard = forwardRef((_, ref) => { const { t } = useTranslation() return ( - -
-
- +
+ +
+
+ +
+
{t('dataset.createDataset')}
-
{t('dataset.createDataset')}
-
-
{t('dataset.createDatasetIntro')}
-
+ + +
{t('dataset.connectDataset')}
+ +
+
) }) diff --git a/web/app/components/base/icons/assets/vender/solid/development/api-connection-mod.svg b/web/app/components/base/icons/assets/vender/solid/development/api-connection-mod.svg new file mode 100644 index 0000000000..9e4b0c81ec --- /dev/null +++ b/web/app/components/base/icons/assets/vender/solid/development/api-connection-mod.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.json b/web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.json new file mode 100644 index 0000000000..e8ebcc7448 --- /dev/null +++ b/web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.json @@ -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" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.tsx b/web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.tsx new file mode 100644 index 0000000000..f88431a237 --- /dev/null +++ b/web/app/components/base/icons/src/vender/solid/development/ApiConnectionMod.tsx @@ -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, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'ApiConnectionMod' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/solid/development/index.ts b/web/app/components/base/icons/src/vender/solid/development/index.ts index 7fe781e2ce..159a1cbc0b 100644 --- a/web/app/components/base/icons/src/vender/solid/development/index.ts +++ b/web/app/components/base/icons/src/vender/solid/development/index.ts @@ -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' diff --git a/web/app/components/base/modal/index.css b/web/app/components/base/modal/index.css index 799b375168..727a9455d7 100644 --- a/web/app/components/base/modal/index.css +++ b/web/app/components/base/modal/index.css @@ -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; } diff --git a/web/app/components/datasets/external-api/external-api-modal/Form.tsx b/web/app/components/datasets/external-api/external-api-modal/Form.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/app/components/datasets/external-api/external-api-modal/index.tsx b/web/app/components/datasets/external-api/external-api-modal/index.tsx new file mode 100644 index 0000000000..243ae1c6d3 --- /dev/null +++ b/web/app/components/datasets/external-api/external-api-modal/index.tsx @@ -0,0 +1,141 @@ +import type { FC } from 'react' +import { + memo, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCloseLine, + RiInformation2Line, + RiLock2Fill, +} from '@remixicon/react' +import { useToastContext } from '@/app/components/base/toast' +import { + PortalToFollowElem, + PortalToFollowElemContent, +} from '@/app/components/base/portal-to-follow-elem' +import ActionButton from '@/app/components/base/action-button' +import Input from '@/app/components/base/input' +import Button from '@/app/components/base/button' +import Tooltip from '@/app/components/base/tooltip' + +type AddExternalAPIModalProps = { + show: boolean + onHide: () => void +} + +const AddExternalAPIModal: FC = ({ show, onHide }) => { + const { t } = useTranslation() + const { notify } = useToastContext() + const [showConfirm, setShowConfirm] = useState(false) + const [formData, setFormData] = useState({ name: '', endpoint: '', apiKey: '' }) + const isEditMode = true + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData({ ...formData, [name]: value }) + } + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault() + // Handle form submission logic here + console.log('Form Data:', formData) + onHide() + } + + return ( + + +
+
+
+
+ { + isEditMode ? t('dataset.editExternalAPIFormTitle') : t('dataset.createExternalAPIFormTitle') + } +
+ {isEditMode && ( +
+ {t('dataset.editExternalAPIFormWarning.front')} + +  3 {t('dataset.editExternalAPIFormWarning.end')}  + +
+ )} +
+ + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + {t('dataset.externalAPIForm.encrypted.front')} + + PKCS1_OAEP + + {t('dataset.externalAPIForm.encrypted.end')} +
+
+
+
+
+ ) +} + +export default memo(AddExternalAPIModal) diff --git a/web/app/components/datasets/external-api/external-api-panel/index.tsx b/web/app/components/datasets/external-api/external-api-panel/index.tsx new file mode 100644 index 0000000000..881540617e --- /dev/null +++ b/web/app/components/datasets/external-api/external-api-panel/index.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { + RiAddLine, + RiBookOpenLine, + RiCloseLine, +} from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import cn from '@/utils/classnames' +// import AddExternalAPIForm from '../create/add-external-api' +import ActionButton from '@/app/components/base/action-button' +import Button from '@/app/components/base/button' +import { useModalContext } from '@/context/modal-context' + +type ExternalAPIPanelProps = { + onClose: () => void + isShow: boolean +} + +const ExternalAPIPanel: React.FC = ({ onClose, isShow }) => { + const { t } = useTranslation() + const { setShowExternalAPIModal } = useModalContext() + + const handleOpenExternalAPIModal = () => { + setShowExternalAPIModal() + } + + return ( +
+
+
+
+
{t('dataset.externalAPIPanelTitle')}
+
{t('dataset.externalAPIPanelDescription')}
+ + +
{t('dataset.externalAPIPanelDocumentation')}
+
+
+
+ onClose()}> + + +
+
+
+ +
+
+ +
+
+
+ ) +} + +export default ExternalAPIPanel diff --git a/web/context/modal-context.tsx b/web/context/modal-context.tsx index 3547c45aac..31e9037fdb 100644 --- a/web/context/modal-context.tsx +++ b/web/context/modal-context.tsx @@ -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, @@ -52,6 +53,7 @@ export type ModalContextState = { setShowPricingModal: () => void setShowAnnotationFullModal: () => void setShowModelModal: Dispatch | null>> + setShowExternalAPIModal: () => void setShowModelLoadBalancingModal: Dispatch> setShowModelLoadBalancingEntryModal: Dispatch | null>> } @@ -63,6 +65,7 @@ const ModalContext = createContext({ setShowPricingModal: () => { }, setShowAnnotationFullModal: () => { }, setShowModelModal: () => { }, + setShowExternalAPIModal: () => { }, setShowModelLoadBalancingModal: () => { }, setShowModelLoadBalancingEntryModal: () => { }, }) @@ -86,6 +89,7 @@ export const ModalContextProvider = ({ const [showModerationSettingModal, setShowModerationSettingModal] = useState | null>(null) const [showExternalDataToolModal, setShowExternalDataToolModal] = useState | null>(null) const [showModelModal, setShowModelModal] = useState | null>(null) + const [showExternalAPIModal, setShowExternalAPIModal] = useState(false) const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState(null) const [showModelLoadBalancingEntryModal, setShowModelLoadBalancingEntryModal] = useState | null>(null) const searchParams = useSearchParams() @@ -122,6 +126,18 @@ export const ModalContextProvider = ({ setShowModelModal(null) }, [showModelModal]) + // const handleCancelExternalApiModal = useCallback(() => { + // setShowExternalAPIModal(null) + // if (showExternalAPIModal?.onCancelCallback) + // showExternalAPIModal.onCancelCallback() + // }, [showExternalAPIModal]) + + // const handleSaveExternalApiModal = useCallback(() => { + // if (showExternalAPIModal?.onSaveCallback) + // showExternalAPIModal.onSaveCallback(null) + // setShowExternalAPIModal(null) + // }, [showExternalAPIModal]) + const handleCancelModelLoadBalancingEntryModal = useCallback(() => { showModelLoadBalancingEntryModal?.onCancelCallback?.() setShowModelLoadBalancingEntryModal(null) @@ -173,6 +189,7 @@ export const ModalContextProvider = ({ setShowPricingModal: () => setShowPricingModal(true), setShowAnnotationFullModal: () => setShowAnnotationFullModal(true), setShowModelModal, + setShowExternalAPIModal: () => setShowExternalAPIModal(true), setShowModelLoadBalancingModal, setShowModelLoadBalancingEntryModal, }}> @@ -245,6 +262,14 @@ export const ModalContextProvider = ({ /> ) } + { + !!showExternalAPIModal && ( + setShowExternalAPIModal(false)} + /> + ) + } { Boolean(showModelLoadBalancingModal) && ( diff --git a/web/i18n/en-US/dataset.ts b/web/i18n/en-US/dataset.ts index a15efe5dc0..3572459464 100644 --- a/web/i18n/en-US/dataset.ts +++ b/web/i18n/en-US/dataset.ts @@ -1,9 +1,21 @@ const translation = { knowledge: 'Knowledge', + externalAPI: 'External API', + externalAPIPanelTitle: 'External Knowledge API', + 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 API', documentCount: ' docs', wordCount: ' k words', appCount: ' linked apps', createDataset: 'Create Knowledge', + createExternalAPI: 'Add an External API', + createExternalAPIFormTitle: 'Add an External Knowledge API', + editExternalAPIFormTitle: 'Edit the External Knowledge API', + editExternalAPIFormWarning: { + front: 'This External API is linked to', + end: 'external knowledge', + }, + connectDataset: 'Connect to an external knowledge base', 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 +34,18 @@ const translation = { unavailableTip: 'Embedding model is not available, the default embedding model needs to be configured', datasets: 'KNOWLEDGE', datasetsApi: 'API ACCESS', + 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', diff --git a/web/i18n/zh-Hans/dataset.ts b/web/i18n/zh-Hans/dataset.ts index 013830af6f..6ea94410bb 100644 --- a/web/i18n/zh-Hans/dataset.ts +++ b/web/i18n/zh-Hans/dataset.ts @@ -1,9 +1,21 @@ const translation = { knowledge: '知识库', + externalAPI: '外部 API', + externalAPIPanelTitle: '外部知识库 API', + externalAPIPanelDescription: '外部知识库 API 用于连接到 Dify 之外的知识库并从中检索知识。', + externalAPIPanelDocumentation: '了解如何创建外部 API', documentCount: ' 文档', wordCount: ' 千字符', appCount: ' 关联应用', createDataset: '创建知识库', + createExternalAPI: '添加外部 API', + createExternalAPIFormTitle: '添加外部知识库 API', + editExternalAPIFormTitle: '编辑外部知识库 API', + editExternalAPIFormWarning: { + front: '此外部 API 已链接到', + end: '外部知识库', + }, + connectDataset: '连接外部知识库', createDatasetIntro: '导入您自己的文本数据或通过 Webhook 实时写入数据以增强 LLM 的上下文。', deleteDatasetConfirmTitle: '要删除知识库吗?', deleteDatasetConfirmContent: @@ -22,6 +34,18 @@ const translation = { unavailableTip: '由于 embedding 模型不可用,需要配置默认 embedding 模型', datasets: '知识库', datasetsApi: 'API', + externalAPIForm: { + name: '名称', + endpoint: 'API 端点', + apiKey: 'API 密钥', + save: '保存', + cancel: '取消', + edit: '编辑', + encrypted: { + front: '您的 API Token 将使用', + end: '加密并存储。', + }, + }, retrieval: { semantic_search: { title: '向量检索', diff --git a/web/models/datasets.ts b/web/models/datasets.ts index 23d1fe6136..294eb73af8 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -34,6 +34,23 @@ export type DataSet = { partial_member_list?: any[] } +export type ExternalAPIItem = { + id: string + tenant_id: string + name: string + description: string + settings: { + endpoint: string + api_key: string + document_retrieval_setting: { + top_k: number + score_threshold: number + } + } + created_by: string + created_at: string +} + export type CustomFile = File & { id?: string extension?: string @@ -72,6 +89,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 diff --git a/web/service/datasets.ts b/web/service/datasets.ts index 4ca269a7d6..812bcd602d 100644 --- a/web/service/datasets.ts +++ b/web/service/datasets.ts @@ -8,6 +8,8 @@ import type { DocumentDetailResponse, DocumentListResponse, ErrorDocsResponse, + ExternalAPIItem, + ExternalAPIListResponse, FileIndexingEstimateResponse, HitTestingRecordsResponse, HitTestingResponse, @@ -82,6 +84,14 @@ export const deleteDataset: Fetcher = (datasetID) => { return del(`/datasets/${datasetID}`) } +export const fetchExternalAPIList: Fetcher = ({ url, params }) => { + return get(url, { params }) +} + +export const createExternalAPI: Fetcher = ({ body }) => { + return post('/datasets/api-template', { body }) +} + export const fetchDefaultProcessRule: Fetcher = ({ url }) => { return get(url) } From cfa482507377fa24e94860d899e7f6a497056b64 Mon Sep 17 00:00:00 2001 From: Yi Date: Thu, 26 Sep 2024 01:00:49 +0800 Subject: [PATCH 2/5] feat: external knowledge api crud frontend & connect external knowledge base --- web/app/(commonLayout)/datasets/Container.tsx | 74 ++++--- .../(commonLayout)/datasets/DatasetCard.tsx | 4 +- .../(commonLayout)/datasets/connect/page.tsx | 13 ++ .../components/base/corner-label/index.tsx | 21 ++ .../assets/vender/solid/shapes/corner.svg | 3 + .../icons/src/vender/solid/shapes/Corner.json | 27 +++ .../icons/src/vender/solid/shapes/Corner.tsx | 16 ++ .../icons/src/vender/solid/shapes/index.ts | 1 + web/app/components/base/param-item/index.tsx | 1 + web/app/components/base/select/index.tsx | 2 +- .../datasets/external-api/declarations.ts | 16 ++ .../endpoint-validator/declarations.ts | 42 ++++ .../external-api/endpoint-validator/hooks.ts | 31 +++ .../external-api/external-api-modal/Form.tsx | 84 +++++++ .../external-api/external-api-modal/index.tsx | 205 ++++++++++++------ .../external-api/external-api-panel/index.tsx | 32 ++- .../external-knowledge-api-card/index.tsx | 151 +++++++++++++ .../key-validator/declarations.ts | 42 ++++ .../external-api/key-validator/hooks.ts | 31 +++ .../connector/index.tsx | 20 ++ .../create/ExternalApiSelection.tsx | 47 ++++ .../create/InfoPanel.tsx | 29 +++ .../create/KnowledgeBaseInfo.tsx | 85 ++++++++ .../create/RetrievalSettings.tsx | 45 ++++ .../create/declarations.ts | 11 + .../external-knowledge-base/create/index.tsx | 110 ++++++++++ .../external-knowledge-api-context.tsx | 46 ++++ web/context/modal-context.tsx | 48 ++-- web/i18n/en-US/dataset.ts | 37 +++- web/i18n/zh-Hans/dataset.ts | 35 ++- web/models/datasets.ts | 33 ++- web/service/datasets.ts | 33 ++- 32 files changed, 1237 insertions(+), 138 deletions(-) create mode 100644 web/app/(commonLayout)/datasets/connect/page.tsx create mode 100644 web/app/components/base/corner-label/index.tsx create mode 100644 web/app/components/base/icons/assets/vender/solid/shapes/corner.svg create mode 100644 web/app/components/base/icons/src/vender/solid/shapes/Corner.json create mode 100644 web/app/components/base/icons/src/vender/solid/shapes/Corner.tsx create mode 100644 web/app/components/datasets/external-api/declarations.ts create mode 100644 web/app/components/datasets/external-api/endpoint-validator/declarations.ts create mode 100644 web/app/components/datasets/external-api/endpoint-validator/hooks.ts create mode 100644 web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx create mode 100644 web/app/components/datasets/external-api/key-validator/declarations.ts create mode 100644 web/app/components/datasets/external-api/key-validator/hooks.ts create mode 100644 web/app/components/datasets/external-knowledge-base/connector/index.tsx create mode 100644 web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx create mode 100644 web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx create mode 100644 web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx create mode 100644 web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx create mode 100644 web/app/components/datasets/external-knowledge-base/create/declarations.ts create mode 100644 web/app/components/datasets/external-knowledge-base/create/index.tsx create mode 100644 web/context/external-knowledge-api-context.tsx diff --git a/web/app/(commonLayout)/datasets/Container.tsx b/web/app/(commonLayout)/datasets/Container.tsx index b626da05f5..3fbbf12d4e 100644 --- a/web/app/(commonLayout)/datasets/Container.tsx +++ b/web/app/(commonLayout)/datasets/Container.tsx @@ -19,6 +19,7 @@ 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' @@ -70,48 +71,49 @@ const Container = () => { useEffect(() => { if (currentWorkspace.role === 'normal') return router.replace('/apps') - }, [currentWorkspace]) + }, [currentWorkspace, router]) return ( -
-
- setActiveTab(newActiveTab)} - options={options} - /> - {activeTab === 'dataset' && ( -
- - -
- -
- )} - {activeTab === 'api' && data && } -
- - {activeTab === 'dataset' && ( - <> - - - {showTagManagementModal && ( - + +
+
+ setActiveTab(newActiveTab)} + options={options} + /> + {activeTab === 'dataset' && ( +
+ + +
+ +
)} - - )} + {activeTab === 'api' && data && } +
- {activeTab === 'api' && data && } + {activeTab === 'dataset' && ( + <> + + + {showTagManagementModal && ( + + )} + + )} - {showExternalApiPanel && setShowExternalApiPanel(false)} isShow={showExternalApiPanel} />} -
+ {activeTab === 'api' && data && } + {showExternalApiPanel && setShowExternalApiPanel(false)} isShow={showExternalApiPanel} datasetBindings={[]} />} +
+
) } diff --git a/web/app/(commonLayout)/datasets/DatasetCard.tsx b/web/app/(commonLayout)/datasets/DatasetCard.tsx index f66c6bccf4..5542ddd0d6 100644 --- a/web/app/(commonLayout)/datasets/DatasetCard.tsx +++ b/web/app/(commonLayout)/datasets/DatasetCard.tsx @@ -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 = { @@ -108,13 +109,14 @@ const DatasetCard = ({ return ( <>
{ e.preventDefault() push(`/datasets/${dataset.id}/documents`) }} > + {dataset.provider === 'external' && }
{ + return ( + + + + ) +} + +export default ExternalKnowledgeBaseCreation diff --git a/web/app/components/base/corner-label/index.tsx b/web/app/components/base/corner-label/index.tsx new file mode 100644 index 0000000000..0ad33ce8a6 --- /dev/null +++ b/web/app/components/base/corner-label/index.tsx @@ -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 = ({ label, className, labelClassName }) => { + return ( +
+ +
+
{label}
+
+
+ ) +} + +export default CornerLabel diff --git a/web/app/components/base/icons/assets/vender/solid/shapes/corner.svg b/web/app/components/base/icons/assets/vender/solid/shapes/corner.svg new file mode 100644 index 0000000000..9b360e4be7 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/solid/shapes/corner.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/components/base/icons/src/vender/solid/shapes/Corner.json b/web/app/components/base/icons/src/vender/solid/shapes/Corner.json new file mode 100644 index 0000000000..2f35483a66 --- /dev/null +++ b/web/app/components/base/icons/src/vender/solid/shapes/Corner.json @@ -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" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/solid/shapes/Corner.tsx b/web/app/components/base/icons/src/vender/solid/shapes/Corner.tsx new file mode 100644 index 0000000000..0edeb2cda1 --- /dev/null +++ b/web/app/components/base/icons/src/vender/solid/shapes/Corner.tsx @@ -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, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Corner' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/solid/shapes/index.ts b/web/app/components/base/icons/src/vender/solid/shapes/index.ts index 3122e776b5..2768e3949a 100644 --- a/web/app/components/base/icons/src/vender/solid/shapes/index.ts +++ b/web/app/components/base/icons/src/vender/solid/shapes/index.ts @@ -1,2 +1,3 @@ +export { default as Corner } from './Corner' export { default as Star04 } from './Star04' export { default as Star06 } from './Star06' diff --git a/web/app/components/base/param-item/index.tsx b/web/app/components/base/param-item/index.tsx index 318b0fb0e3..49acc81484 100644 --- a/web/app/components/base/param-item/index.tsx +++ b/web/app/components/base/param-item/index.tsx @@ -37,6 +37,7 @@ const ParamItem: FC = ({ className, id, name, noTooltip, tip, step = 0.1, {name} {!noTooltip && ( {tip}
} /> )} diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx index dee983690b..e821cb1545 100644 --- a/web/app/components/base/select/index.tsx +++ b/web/app/components/base/select/index.tsx @@ -87,7 +87,7 @@ const Select: FC = ({
{allowSearch ? { if (!disabled) setQuery(event.target.value) diff --git a/web/app/components/datasets/external-api/declarations.ts b/web/app/components/datasets/external-api/declarations.ts new file mode 100644 index 0000000000..ded736d167 --- /dev/null +++ b/web/app/components/datasets/external-api/declarations.ts @@ -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 +} diff --git a/web/app/components/datasets/external-api/endpoint-validator/declarations.ts b/web/app/components/datasets/external-api/endpoint-validator/declarations.ts new file mode 100644 index 0000000000..77655c4c8e --- /dev/null +++ b/web/app/components/datasets/external-api/endpoint-validator/declarations.ts @@ -0,0 +1,42 @@ +import type { Dispatch, SetStateAction } from 'react' + +export enum ValidatedEndpointStatus { + Success = 'success', + Error = 'error', +} + +export type ValidatedStatusState = { + status?: ValidatedEndpointStatus + message?: string +} + +export type Status = 'add' | 'fail' | 'success' + +export type ValidateValue = string + +export type ValidateCallback = { + before: (v?: ValidateValue) => boolean | undefined + run?: (v?: ValidateValue) => Promise +} + +export type Form = { + key: string + title: string + placeholder: string + value?: string + validate?: ValidateCallback + handleFocus?: (v: ValidateValue, dispatch: Dispatch>) => void +} + +export type KeyFrom = { + text: string + link: string +} + +export type KeyValidatorProps = { + type: string + title: React.ReactNode + status: Status + forms: Form[] + keyFrom: KeyFrom +} diff --git a/web/app/components/datasets/external-api/endpoint-validator/hooks.ts b/web/app/components/datasets/external-api/endpoint-validator/hooks.ts new file mode 100644 index 0000000000..fe1b490320 --- /dev/null +++ b/web/app/components/datasets/external-api/endpoint-validator/hooks.ts @@ -0,0 +1,31 @@ +import { useState } from 'react' +import { useDebounceFn } from 'ahooks' +import type { DebouncedFunc } from 'lodash-es' +import { ValidatedEndpointStatus } from './declarations' +import type { ValidateCallback, ValidateValue, ValidatedStatusState } from './declarations' + +export const useValidateEndpoint: (value: ValidateValue) => [DebouncedFunc<(validateCallback: ValidateCallback) => Promise>, boolean, ValidatedStatusState] = (value) => { + const [validating, setValidating] = useState(false) + const [validatedStatus, setValidatedStatus] = useState({}) + + const { run } = useDebounceFn(async (validateCallback: ValidateCallback) => { + if (!validateCallback.before(value)) { + setValidating(false) + setValidatedStatus({}) + return + } + setValidating(true) + + if (validateCallback.run) { + const res = await validateCallback?.run(value) + setValidatedStatus( + res.status === 'success' + ? { status: ValidatedEndpointStatus.Success } + : { status: ValidatedEndpointStatus.Error, message: res.message }) + + setValidating(false) + } + }, { wait: 1000 }) + + return [run, validating, validatedStatus] +} diff --git a/web/app/components/datasets/external-api/external-api-modal/Form.tsx b/web/app/components/datasets/external-api/external-api-modal/Form.tsx index e69de29bb2..3a9ea2bc02 100644 --- a/web/app/components/datasets/external-api/external-api-modal/Form.tsx +++ b/web/app/components/datasets/external-api/external-api-modal/Form.tsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +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 + validatingEndpoint: boolean + validatedApiKeySuccess?: boolean + validatingApiKey: boolean + validatedEndpointSuccess?: boolean + formSchemas: FormSchema[] + inputClassName?: string +} + +const Form: FC = React.memo(({ + className, + itemClassName, + fieldLabelClassName, + value, + onChange, + formSchemas, + validatingEndpoint, + validatingApiKey, + validatedApiKeySuccess, + validatedEndpointSuccess, + 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 ( +
+ + handleFormChange(variable, val.target.value)} + required={required} + className={cn(inputClassName)} + /> +
+ ) + } + + return ( +
+ {formSchemas.map(formSchema => renderField(formSchema))} +
+ ) +}) + +export default Form diff --git a/web/app/components/datasets/external-api/external-api-modal/index.tsx b/web/app/components/datasets/external-api/external-api-modal/index.tsx index 243ae1c6d3..b79f81199e 100644 --- a/web/app/components/datasets/external-api/external-api-modal/index.tsx +++ b/web/app/components/datasets/external-api/external-api-modal/index.tsx @@ -1,46 +1,111 @@ 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 { useToastContext } from '@/app/components/base/toast' +import { useValidateApiKey } from '../key-validator/hooks' +import { ValidatedApiKeyStatus } from '../key-validator/declarations' +import { ValidatedEndpointStatus } from '../endpoint-validator/declarations' +import { useValidateEndpoint } from '../endpoint-validator/hooks' +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 ActionButton from '@/app/components/base/action-button' -import Input from '@/app/components/base/input' +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 = { - show: boolean - onHide: () => void + data?: CreateExternalAPIReq + onSave: (formValue: CreateExternalAPIReq) => void + onCancel: () => void + onEdit?: (formValue: CreateExternalAPIReq) => Promise + datasetBindings?: { id: string; name: string }[] + isEditMode: boolean } -const AddExternalAPIModal: FC = ({ show, onHide }) => { +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 = ({ 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({ name: '', endpoint: '', apiKey: '' }) - const isEditMode = true + const [formData, setFormData] = useState({ name: '', settings: { endpoint: '', api_key: '' } }) - const handleInputChange = (e: React.ChangeEvent) => { - const { name, value } = e.target - setFormData({ ...formData, [name]: value }) + useEffect(() => { + if (isEditMode && data) + setFormData(data) + }, [isEditMode, data]) + + const [, validatingApiKey, validatedApiKeyStatusState] = useValidateApiKey(formData.settings.api_key) + const [, validatingEndpoint, validatedEndpointStatusState] = useValidateEndpoint(formData.settings.endpoint) + const hasEmptyInputs = Object.values(formData).includes('') + const handleDataChange = (val: CreateExternalAPIReq) => { + setFormData(val) } - const handleFormSubmit = (e: React.FormEvent) => { - e.preventDefault() - // Handle form submission logic here - console.log('Form Data:', formData) - onHide() + const handleSave = async () => { + try { + setLoading(true) + if (isEditMode && onEdit) { + await onEdit(formData) + 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 ( @@ -51,69 +116,69 @@ const AddExternalAPIModal: FC = ({ show, onHide }) =>
{ - isEditMode ? t('dataset.editExternalAPIFormTitle') : t('dataset.createExternalAPIFormTitle') + isEditMode ? t('dataset.editExternalAPIFormTitle') : t('dataset.createExternalAPI') }
- {isEditMode && ( + {isEditMode && (datasetBindings?.length ?? 0) > 0 && (
{t('dataset.editExternalAPIFormWarning.front')} -  3 {t('dataset.editExternalAPIFormWarning.end')}  +  {datasetBindings?.length} {t('dataset.editExternalAPIFormWarning.end')}  + +
+
{`${datasetBindings?.length} ${t('dataset.editExternalAPITooltipTitle')}`}
+ {datasetBindings?.map(binding => ( +
+ +
{binding.name}
+
+ ))} +
+
+ } + asChild={false} + position='bottom' + > + +
)}
- + -
-
-
- - -
-
- - -
-
- - -
-
-
+
- -
@@ -132,6 +197,16 @@ const AddExternalAPIModal: FC = ({ show, onHide }) => {t('dataset.externalAPIForm.encrypted.end')}
+ {showConfirm && (datasetBindings?.length ?? 0) > 0 && ( + setShowConfirm(false)} + onConfirm={handleSave} + /> + )}
diff --git a/web/app/components/datasets/external-api/external-api-panel/index.tsx b/web/app/components/datasets/external-api/external-api-panel/index.tsx index 881540617e..5eb3218a8d 100644 --- a/web/app/components/datasets/external-api/external-api-panel/index.tsx +++ b/web/app/components/datasets/external-api/external-api-panel/index.tsx @@ -5,23 +5,37 @@ import { RiCloseLine, } from '@remixicon/react' import { useTranslation } from 'react-i18next' +import ExternalKnowledgeAPICard from '../external-knowledge-api-card' import cn from '@/utils/classnames' -// import AddExternalAPIForm from '../create/add-external-api' +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 = ({ onClose, isShow }) => { +const ExternalAPIPanel: React.FC = ({ onClose, isShow, datasetBindings }) => { const { t } = useTranslation() - const { setShowExternalAPIModal } = useModalContext() + const { setShowExternalKnowledgeAPIModal } = useModalContext() + const { externalKnowledgeApiList, mutateExternalKnowledgeApis, isLoading } = useExternalKnowledgeApi() const handleOpenExternalAPIModal = () => { - setShowExternalAPIModal() + setShowExternalKnowledgeAPIModal({ + payload: { name: '', settings: { endpoint: '', api_key: '' } }, + datasetBindings: [], + onSaveCallback: () => { + mutateExternalKnowledgeApis() + }, + onCancelCallback: () => { + mutateExternalKnowledgeApis() + }, + isEditMode: false, + }) } return ( @@ -60,7 +74,15 @@ const ExternalAPIPanel: React.FC = ({ onClose, isShow })
- + {isLoading + ? ( + + ) + : ( + externalKnowledgeApiList.map(api => ( + + )) + )}
diff --git a/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx new file mode 100644 index 0000000000..603b4fe7cb --- /dev/null +++ b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx @@ -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 = ({ 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 ( + <> +
+
+
+ +
{api.name}
+
+
{api.settings.endpoint}
+
+
+ + + + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + +
+
+ {showConfirm && ( + 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 diff --git a/web/app/components/datasets/external-api/key-validator/declarations.ts b/web/app/components/datasets/external-api/key-validator/declarations.ts new file mode 100644 index 0000000000..865bc6a23b --- /dev/null +++ b/web/app/components/datasets/external-api/key-validator/declarations.ts @@ -0,0 +1,42 @@ +import type { Dispatch, SetStateAction } from 'react' + +export enum ValidatedApiKeyStatus { + Success = 'success', + Error = 'error', +} + +export type ValidatedStatusState = { + status?: ValidatedApiKeyStatus + message?: string +} + +export type Status = 'add' | 'fail' | 'success' + +export type ValidateValue = string + +export type ValidateCallback = { + before: (v?: ValidateValue) => boolean | undefined + run?: (v?: ValidateValue) => Promise +} + +export type Form = { + key: string + title: string + placeholder: string + value?: string + validate?: ValidateCallback + handleFocus?: (v: ValidateValue, dispatch: Dispatch>) => void +} + +export type KeyFrom = { + text: string + link: string +} + +export type KeyValidatorProps = { + type: string + title: React.ReactNode + status: Status + forms: Form[] + keyFrom: KeyFrom +} diff --git a/web/app/components/datasets/external-api/key-validator/hooks.ts b/web/app/components/datasets/external-api/key-validator/hooks.ts new file mode 100644 index 0000000000..324ef033ae --- /dev/null +++ b/web/app/components/datasets/external-api/key-validator/hooks.ts @@ -0,0 +1,31 @@ +import { useState } from 'react' +import { useDebounceFn } from 'ahooks' +import type { DebouncedFunc } from 'lodash-es' +import { ValidatedApiKeyStatus } from './declarations' +import type { ValidateCallback, ValidateValue, ValidatedStatusState } from './declarations' + +export const useValidateApiKey: (value: ValidateValue) => [DebouncedFunc<(validateCallback: ValidateCallback) => Promise>, boolean, ValidatedStatusState] = (value) => { + const [validating, setValidating] = useState(false) + const [validatedStatus, setValidatedStatus] = useState({}) + + const { run } = useDebounceFn(async (validateCallback: ValidateCallback) => { + if (!validateCallback.before(value)) { + setValidating(false) + setValidatedStatus({}) + return + } + setValidating(true) + + if (validateCallback.run) { + const res = await validateCallback?.run(value) + setValidatedStatus( + res.status === 'success' + ? { status: ValidatedApiKeyStatus.Success } + : { status: ValidatedApiKeyStatus.Error, message: res.message }) + + setValidating(false) + } + }, { wait: 1000 }) + + return [run, validating, validatedStatus] +} diff --git a/web/app/components/datasets/external-knowledge-base/connector/index.tsx b/web/app/components/datasets/external-knowledge-base/connector/index.tsx new file mode 100644 index 0000000000..7aecadc751 --- /dev/null +++ b/web/app/components/datasets/external-knowledge-base/connector/index.tsx @@ -0,0 +1,20 @@ +'use client' + +import React from 'react' +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 handleConnect = async (formValue: CreateKnowledgeBaseReq) => { + try { + const result = await createExternalKnowledgeBase({ body: formValue }) + } + catch (error) { + console.error('Error creating external knowledge base:', error) + } + } + return +} + +export default ExternalKnowledgeBaseConnector diff --git a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx new file mode 100644 index 0000000000..63fe315b70 --- /dev/null +++ b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx @@ -0,0 +1,47 @@ +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 = ({ external_knowledge_api_id, external_knowledge_id, onChange }: ExternalApiSelectionProps) => { + const { t } = useTranslation() + const { externalKnowledgeApiList } = useExternalKnowledgeApi() + + const apiItems = externalKnowledgeApiList.map(api => ({ + value: api.id, + name: api.name, + })) + + return ( + +
+
+ +
+ onChange({ external_knowledge_id: e.target.value, external_knowledge_api_id })} + placeholder={t('dataset.externalKnowledgeIdPlaceholder') ?? ''} + /> +
+ + ) +} + +export default ExternalApiSelection diff --git a/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx b/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx new file mode 100644 index 0000000000..8cd09aebab --- /dev/null +++ b/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx @@ -0,0 +1,29 @@ +import { RiBookOpenLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' + +const InfoPanel = () => { + const { t } = useTranslation() + + return ( +
+
+
+ +
+

+ + {t('dataset.connectDatasetIntro.title')} + + + {t('dataset.connectDatasetIntro.content')} + + + {t('dataset.connectDatasetIntro.learnMore')} + +

+
+
+ ) +} + +export default InfoPanel diff --git a/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx b/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx new file mode 100644 index 0000000000..edd8554f53 --- /dev/null +++ b/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } 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 = ({ name: initialName, description: initialDescription, onChange }) => { + const { t } = useTranslation() + const [name, setName] = useState(initialName) + const [description, setDescription] = useState(initialDescription) + + useEffect(() => { + const savedName = localStorage.getItem('knowledgeBaseName') + const savedDescription = localStorage.getItem('knowledgeBaseDescription') + + if (savedName) + setName(savedName) + if (savedDescription) + setDescription(savedDescription) + + onChange({ name: savedName || initialName, description: savedDescription || initialDescription }) + }, []) + + const handleNameChange = (e: React.ChangeEvent) => { + const newName = e.target.value + setName(newName) + localStorage.setItem('knowledgeBaseName', newName) + onChange({ name: newName }) + } + + const handleDescriptionChange = (e: React.ChangeEvent) => { + const newDescription = e.target.value + setDescription(newDescription) + localStorage.setItem('knowledgeBaseDescription', newDescription) + onChange({ description: newDescription }) + } + + return ( +
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+ +
+
{t('dataset.learnHowToWriteGoodKnowledgeDescription')}
+
+
+
+
+
+ ) +} + +export const clearKnowledgeBaseInfo = () => { + localStorage.removeItem('knowledgeBaseName') + localStorage.removeItem('knowledgeBaseDescription') +} + +export default KnowledgeBaseInfo diff --git a/web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx b/web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx new file mode 100644 index 0000000000..4b804caef2 --- /dev/null +++ b/web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx @@ -0,0 +1,45 @@ +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' + +type RetrievalSettingsProps = { + topK: number + scoreThreshold: number + onChange: (data: { top_k?: number; score_threshold?: number }) => void +} + +const RetrievalSettings: FC = ({ topK, scoreThreshold, onChange }) => { + const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(false) + const { t } = useTranslation() + return ( +
+
+ +
+
+
+ onChange({ top_k: v })} + enable={true} + /> +
+
+ onChange({ score_threshold: v })} + enable={scoreThresholdEnabled} + hasSwitch={true} + onSwitchChange={(_key, v) => setScoreThresholdEnabled(v)} + /> +
+
+
+ ) +} + +export default RetrievalSettings diff --git a/web/app/components/datasets/external-knowledge-base/create/declarations.ts b/web/app/components/datasets/external-knowledge-base/create/declarations.ts new file mode 100644 index 0000000000..a131cefc04 --- /dev/null +++ b/web/app/components/datasets/external-knowledge-base/create/declarations.ts @@ -0,0 +1,11 @@ +export type CreateKnowledgeBaseReq = { + name: string + description?: string + external_knowledge_api_id: string + provider: 'external' + external_knowledge_id: string + external_retrieval_modal: { + top_k: number + score_threshold: number + } +} diff --git a/web/app/components/datasets/external-knowledge-base/create/index.tsx b/web/app/components/datasets/external-knowledge-base/create/index.tsx new file mode 100644 index 0000000000..3f36d1fefb --- /dev/null +++ b/web/app/components/datasets/external-knowledge-base/create/index.tsx @@ -0,0 +1,110 @@ +'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 +} + +const ExternalKnowledgeBaseCreate: React.FC = ({ onConnect }) => { + const { t } = useTranslation() + const router = useRouter() + const [formData, setFormData] = useState({ + name: '', + description: '', + external_knowledge_api_id: '', + external_knowledge_id: '', + external_retrieval_modal: { + top_k: 2, + score_threshold: 0.5, + }, + provider: 'external', + + }) + + const navBackHandle = useCallback(() => { + router.replace('/datasets') + }, [router]) + + const handleFormChange = (newData: CreateKnowledgeBaseReq) => { + setFormData(newData) + console.log(formData) + } + + const isFormValid = formData.name !== '' + && formData.external_knowledge_api_id !== '' + && formData.external_knowledge_id !== '' + && formData.external_retrieval_modal.top_k !== undefined + && formData.external_retrieval_modal.score_threshold !== undefined + + return ( +
+
+
+
+
+
{t('dataset.connectDataset')}
+ +
+ handleFormChange({ + ...formData, + ...data, + })} + /> + + handleFormChange({ + ...formData, + ...data, + })} + /> + handleFormChange({ + ...formData, + external_retrieval_modal: { + ...formData.external_retrieval_modal, + ...data, + }, + })} + /> +
+ + +
+
+
+ +
+
+ ) +} + +export default ExternalKnowledgeBaseCreate diff --git a/web/context/external-knowledge-api-context.tsx b/web/context/external-knowledge-api-context.tsx new file mode 100644 index 0000000000..5f2d2ff393 --- /dev/null +++ b/web/context/external-knowledge-api-context.tsx @@ -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 + isLoading: boolean +} + +const ExternalKnowledgeApiContext = createContext(undefined) + +export type ExternalKnowledgeApiProviderProps = { + children: ReactNode +} + +export const ExternalKnowledgeApiProvider: FC = ({ children }) => { + const { data, mutate: mutateExternalKnowledgeApis, isLoading } = useSWR( + { url: '/datasets/external-knowledge-api' }, + fetchExternalAPIList, + ) + + const contextValue = useMemo(() => ({ + externalKnowledgeApiList: data?.data || [], + mutateExternalKnowledgeApis, + isLoading, + }), [data, mutateExternalKnowledgeApis, isLoading]) + + return ( + + {children} + + ) +} + +export const useExternalKnowledgeApi = () => { + const context = useContext(ExternalKnowledgeApiContext) + if (context === undefined) + throw new Error('useExternalKnowledgeApi must be used within a ExternalKnowledgeApiProvider') + + return context +} diff --git a/web/context/modal-context.tsx b/web/context/modal-context.tsx index 31e9037fdb..727268a29a 100644 --- a/web/context/modal-context.tsx +++ b/web/context/modal-context.tsx @@ -24,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' @@ -33,7 +34,10 @@ export type ModalState = { 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 = { @@ -53,7 +57,7 @@ export type ModalContextState = { setShowPricingModal: () => void setShowAnnotationFullModal: () => void setShowModelModal: Dispatch | null>> - setShowExternalAPIModal: () => void + setShowExternalKnowledgeAPIModal: Dispatch | null>> setShowModelLoadBalancingModal: Dispatch> setShowModelLoadBalancingEntryModal: Dispatch | null>> } @@ -65,7 +69,7 @@ const ModalContext = createContext({ setShowPricingModal: () => { }, setShowAnnotationFullModal: () => { }, setShowModelModal: () => { }, - setShowExternalAPIModal: () => { }, + setShowExternalKnowledgeAPIModal: () => { }, setShowModelLoadBalancingModal: () => { }, setShowModelLoadBalancingEntryModal: () => { }, }) @@ -89,7 +93,7 @@ export const ModalContextProvider = ({ const [showModerationSettingModal, setShowModerationSettingModal] = useState | null>(null) const [showExternalDataToolModal, setShowExternalDataToolModal] = useState | null>(null) const [showModelModal, setShowModelModal] = useState | null>(null) - const [showExternalAPIModal, setShowExternalAPIModal] = useState(false) + const [showExternalKnowledgeAPIModal, setShowExternalKnowledgeAPIModal] = useState | null>(null) const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState(null) const [showModelLoadBalancingEntryModal, setShowModelLoadBalancingEntryModal] = useState | null>(null) const searchParams = useSearchParams() @@ -126,17 +130,23 @@ export const ModalContextProvider = ({ setShowModelModal(null) }, [showModelModal]) - // const handleCancelExternalApiModal = useCallback(() => { - // setShowExternalAPIModal(null) - // if (showExternalAPIModal?.onCancelCallback) - // showExternalAPIModal.onCancelCallback() - // }, [showExternalAPIModal]) + const handleCancelExternalApiModal = useCallback(() => { + setShowExternalKnowledgeAPIModal(null) + if (showExternalKnowledgeAPIModal?.onCancelCallback) + showExternalKnowledgeAPIModal.onCancelCallback() + }, [showExternalKnowledgeAPIModal]) - // const handleSaveExternalApiModal = useCallback(() => { - // if (showExternalAPIModal?.onSaveCallback) - // showExternalAPIModal.onSaveCallback(null) - // setShowExternalAPIModal(null) - // }, [showExternalAPIModal]) + 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?.() @@ -189,7 +199,7 @@ export const ModalContextProvider = ({ setShowPricingModal: () => setShowPricingModal(true), setShowAnnotationFullModal: () => setShowAnnotationFullModal(true), setShowModelModal, - setShowExternalAPIModal: () => setShowExternalAPIModal(true), + setShowExternalKnowledgeAPIModal, setShowModelLoadBalancingModal, setShowModelLoadBalancingEntryModal, }}> @@ -263,10 +273,14 @@ export const ModalContextProvider = ({ ) } { - !!showExternalAPIModal && ( + !!showExternalKnowledgeAPIModal && ( setShowExternalAPIModal(false)} + data={showExternalKnowledgeAPIModal.payload} + datasetBindings={showExternalKnowledgeAPIModal.datasetBindings ?? []} + onSave={handleSaveExternalApiModal} + onCancel={handleCancelExternalApiModal} + onEdit={handleEditExternalApiModal} + isEditMode={showExternalKnowledgeAPIModal.isEditMode ?? false} /> ) } diff --git a/web/i18n/en-US/dataset.ts b/web/i18n/en-US/dataset.ts index 3572459464..f471cf23c1 100644 --- a/web/i18n/en-US/dataset.ts +++ b/web/i18n/en-US/dataset.ts @@ -2,20 +2,47 @@ const translation = { knowledge: 'Knowledge', 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 API', documentCount: ' docs', wordCount: ' k words', appCount: ' linked apps', createDataset: 'Create Knowledge', - createExternalAPI: 'Add an External API', - createExternalAPIFormTitle: 'Add an External Knowledge API', + 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', }, - connectDataset: 'Connect to an external knowledge base', + 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: 'To connect to an external knowledge base, you need to create an external API first. Please read carefully and refer to learn how to create an external API. 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: @@ -34,6 +61,10 @@ 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', diff --git a/web/i18n/zh-Hans/dataset.ts b/web/i18n/zh-Hans/dataset.ts index 6ea94410bb..3df97e9cf0 100644 --- a/web/i18n/zh-Hans/dataset.ts +++ b/web/i18n/zh-Hans/dataset.ts @@ -2,19 +2,46 @@ const translation = { knowledge: '知识库', externalAPI: '外部 API', externalAPIPanelTitle: '外部知识库 API', + externalKnowledgeId: '外部知识库 ID', + externalKnowledgeName: '外部知识库名称', + externalKnowledgeDescription: '知识库描述', + externalKnowledgeIdPlaceholder: '请输入外部知识库 ID', + externalKnowledgeNamePlaceholder: '请输入外部知识库名称', + externalKnowledgeDescriptionPlaceholder: '描述知识库内容(可选)', + learnHowToWriteGoodKnowledgeDescription: '了解如何编写良好的知识库描述', externalAPIPanelDescription: '外部知识库 API 用于连接到 Dify 之外的知识库并从中检索知识。', externalAPIPanelDocumentation: '了解如何创建外部 API', documentCount: ' 文档', wordCount: ' 千字符', appCount: ' 关联应用', createDataset: '创建知识库', - createExternalAPI: '添加外部 API', - createExternalAPIFormTitle: '添加外部知识库 API', + 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: '要连接到外部知识库,您需要先创建一个外部 API。请仔细阅读并参考如何创建外部 API。然后找到相应的知识 ID 并将其填写在左侧表单中。如果所有信息都正确,点击连接按钮后会自动跳转到知识库的检索测试。', + learnMore: '了解更多', + }, connectDataset: '连接外部知识库', createDatasetIntro: '导入您自己的文本数据或通过 Webhook 实时写入数据以增强 LLM 的上下文。', deleteDatasetConfirmTitle: '要删除知识库吗?', @@ -34,6 +61,10 @@ const translation = { unavailableTip: '由于 embedding 模型不可用,需要配置默认 embedding 模型', datasets: '知识库', datasetsApi: 'API', + externalKnowledgeForm: { + connect: '连接', + cancel: '取消', + }, externalAPIForm: { name: '名称', endpoint: 'API 端点', diff --git a/web/models/datasets.ts b/web/models/datasets.ts index 294eb73af8..fe55a43694 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -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 @@ -42,15 +43,39 @@ export type ExternalAPIItem = { settings: { endpoint: string api_key: string - document_retrieval_setting: { - top_k: number - score_threshold: number - } } + 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 & { id?: string extension?: string diff --git a/web/service/datasets.ts b/web/service/datasets.ts index 812bcd602d..581620e4f5 100644 --- a/web/service/datasets.ts +++ b/web/service/datasets.ts @@ -8,8 +8,11 @@ import type { DocumentDetailResponse, DocumentListResponse, ErrorDocsResponse, + ExternalAPIDeleteResponse, ExternalAPIItem, ExternalAPIListResponse, + ExternalAPIUsage, + ExternalKnowledgeItem, FileIndexingEstimateResponse, HitTestingRecordsResponse, HitTestingResponse, @@ -25,6 +28,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, @@ -84,12 +89,32 @@ export const deleteDataset: Fetcher = (datasetID) => { return del(`/datasets/${datasetID}`) } -export const fetchExternalAPIList: Fetcher = ({ url, params }) => { - return get(url, { params }) +export const fetchExternalAPIList: Fetcher = ({ url }) => { + return get(url) } -export const createExternalAPI: Fetcher = ({ body }) => { - return post('/datasets/api-template', { body }) +export const fetchExternalAPI: Fetcher = ({ apiTemplateId }) => { + return get(`/datasets/external-knowledge-api/${apiTemplateId}`) +} + +export const updateExternalAPI: Fetcher = ({ apiTemplateId, body }) => { + return patch(`/datasets/external-knowledge-api/${apiTemplateId}`, { body }) +} + +export const deleteExternalAPI: Fetcher = ({ apiTemplateId }) => { + return del(`/datasets/external-knowledge-api/${apiTemplateId}`) +} + +export const checkUsageExternalAPI: Fetcher = ({ apiTemplateId }) => { + return get(`/datasets/external-knowledge-api/${apiTemplateId}/use-check`) +} + +export const createExternalAPI: Fetcher = ({ body }) => { + return post('/datasets/external-knowledge-api', { body }) +} + +export const createExternalKnowledgeBase: Fetcher = ({ body }) => { + return post('/datasets/external', { body }) } export const fetchDefaultProcessRule: Fetcher = ({ url }) => { From ff0260e564b7e14b21c1e10722f5347ee6e6b306 Mon Sep 17 00:00:00 2001 From: Yi Date: Thu, 26 Sep 2024 10:23:06 +0800 Subject: [PATCH 3/5] fix: minor issues --- .../external-api/external-api-modal/index.tsx | 12 +++---- .../create/ExternalApiSelection.tsx | 8 ++++- .../create/KnowledgeBaseInfo.tsx | 35 +++---------------- .../external-knowledge-base/create/index.tsx | 8 +++-- 4 files changed, 24 insertions(+), 39 deletions(-) diff --git a/web/app/components/datasets/external-api/external-api-modal/index.tsx b/web/app/components/datasets/external-api/external-api-modal/index.tsx index b79f81199e..09f761cda4 100644 --- a/web/app/components/datasets/external-api/external-api-modal/index.tsx +++ b/web/app/components/datasets/external-api/external-api-modal/index.tsx @@ -130,13 +130,13 @@ const AddExternalAPIModal: FC = ({ data, onSave, onCan
{`${datasetBindings?.length} ${t('dataset.editExternalAPITooltipTitle')}`}
- {datasetBindings?.map(binding => ( -
- -
{binding.name}
-
- ))}
+ {datasetBindings?.map(binding => ( +
+ +
{binding.name}
+
+ ))}
} asChild={false} diff --git a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx index 63fe315b70..b353f6e237 100644 --- a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx @@ -1,3 +1,4 @@ +import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import Select from '@/app/components/base/select' import Input from '@/app/components/base/input' @@ -8,7 +9,7 @@ type ExternalApiSelectionProps = { onChange: (data: { external_knowledge_api_id?: string; external_knowledge_id?: string }) => void } -const ExternalApiSelection = ({ external_knowledge_api_id, external_knowledge_id, onChange }: ExternalApiSelectionProps) => { +const ExternalApiSelection: React.FC = ({ external_knowledge_api_id, external_knowledge_id, onChange }) => { const { t } = useTranslation() const { externalKnowledgeApiList } = useExternalKnowledgeApi() @@ -17,6 +18,11 @@ const ExternalApiSelection = ({ external_knowledge_api_id, external_knowledge_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 (
diff --git a/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx b/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx index edd8554f53..b0a8566a23 100644 --- a/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx @@ -1,43 +1,23 @@ -import React, { useEffect, useState } from 'react' +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 + description?: string onChange: (data: { name?: string; description?: string }) => void } -const KnowledgeBaseInfo: React.FC = ({ name: initialName, description: initialDescription, onChange }) => { +const KnowledgeBaseInfo: React.FC = ({ name, description, onChange }) => { const { t } = useTranslation() - const [name, setName] = useState(initialName) - const [description, setDescription] = useState(initialDescription) - - useEffect(() => { - const savedName = localStorage.getItem('knowledgeBaseName') - const savedDescription = localStorage.getItem('knowledgeBaseDescription') - - if (savedName) - setName(savedName) - if (savedDescription) - setDescription(savedDescription) - - onChange({ name: savedName || initialName, description: savedDescription || initialDescription }) - }, []) const handleNameChange = (e: React.ChangeEvent) => { - const newName = e.target.value - setName(newName) - localStorage.setItem('knowledgeBaseName', newName) - onChange({ name: newName }) + onChange({ name: e.target.value }) } const handleDescriptionChange = (e: React.ChangeEvent) => { - const newDescription = e.target.value - setDescription(newDescription) - localStorage.setItem('knowledgeBaseDescription', newDescription) - onChange({ description: newDescription }) + onChange({ description: e.target.value }) } return ( @@ -77,9 +57,4 @@ const KnowledgeBaseInfo: React.FC = ({ name: initialName ) } -export const clearKnowledgeBaseInfo = () => { - localStorage.removeItem('knowledgeBaseName') - localStorage.removeItem('knowledgeBaseDescription') -} - export default KnowledgeBaseInfo diff --git a/web/app/components/datasets/external-knowledge-base/create/index.tsx b/web/app/components/datasets/external-knowledge-base/create/index.tsx index 3f36d1fefb..ceada10a4e 100644 --- a/web/app/components/datasets/external-knowledge-base/create/index.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/index.tsx @@ -38,7 +38,6 @@ const ExternalKnowledgeBaseCreate: React.FC = const handleFormChange = (newData: CreateKnowledgeBaseReq) => { setFormData(newData) - console.log(formData) } const isFormValid = formData.name !== '' @@ -94,7 +93,12 @@ const ExternalKnowledgeBaseCreate: React.FC = - From 1c7cb3fbc0e46d318f3cb36795fa2822f4d2b998 Mon Sep 17 00:00:00 2001 From: Yi Date: Fri, 27 Sep 2024 00:33:56 +0800 Subject: [PATCH 4/5] feat: external knowledge base --- .../[datasetId]/layout.tsx | 26 ++-- .../(commonLayout)/datasets/DatasetCard.tsx | 8 +- web/app/components/app-sidebar/basic.tsx | 4 +- web/app/components/app-sidebar/index.tsx | 4 +- .../detail/completed/SegmentCard.tsx | 15 ++- .../connector/index.tsx | 14 ++- .../create/KnowledgeBaseInfo.tsx | 8 +- .../create/RetrievalSettings.tsx | 20 ++- .../create/declarations.ts | 2 +- .../external-knowledge-base/create/index.tsx | 22 ++-- .../datasets/hit-testing/hit-detail.tsx | 58 +++++---- .../components/datasets/hit-testing/index.tsx | 109 ++++++++++------ .../modify-external-retrieval-modal.tsx | 65 ++++++++++ .../datasets/hit-testing/textarea.tsx | 90 +++++++++++--- .../datasets/settings/form/index.tsx | 117 +++++++++++++----- .../components/header/dataset-nav/index.tsx | 2 +- web/i18n/en-US/dataset-hit-testing.ts | 3 +- web/i18n/en-US/dataset-settings.ts | 11 +- web/i18n/zh-Hans/dataset-hit-testing.ts | 3 +- web/i18n/zh-Hans/dataset-settings.ts | 6 +- web/models/datasets.ts | 27 ++++ web/service/datasets.ts | 5 + 22 files changed, 470 insertions(+), 149 deletions(-) create mode 100644 web/app/components/datasets/hit-testing/modify-external-retrieval-modal.tsx diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx index e691cc05f6..a58027bcd1 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx @@ -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 = (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 = (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 => : undefined} iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'} diff --git a/web/app/(commonLayout)/datasets/DatasetCard.tsx b/web/app/(commonLayout)/datasets/DatasetCard.tsx index 5542ddd0d6..e3014f08d0 100644 --- a/web/app/(commonLayout)/datasets/DatasetCard.tsx +++ b/web/app/(commonLayout)/datasets/DatasetCard.tsx @@ -33,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(dataset.tags) @@ -40,6 +41,7 @@ const DatasetCard = ({ const [showRenameModal, setShowRenameModal] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [confirmMessage, setConfirmMessage] = useState('') + const isExternalProvider = (provider: string): boolean => provider === EXTERNAL_PROVIDER const detectIsUsedByApp = useCallback(async () => { try { const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id) @@ -113,10 +115,12 @@ const DatasetCard = ({ 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`) }} > - {dataset.provider === 'external' && } + {isExternalProvider(dataset.provider) && }
, } -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) { return (
{icon && icon_background && iconType === 'app' && ( @@ -83,6 +84,7 @@ export default function AppBasic({ icon, icon_background, name, type, hoverTip, }
{type}
+
{isExternal ? 'External' : ''}
}
) diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index 5d5d407dc0..5ee063ad64 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -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} /> )}
diff --git a/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx b/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx index c65b244f6d..f5512838ab 100644 --- a/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx +++ b/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx @@ -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 @@ -48,6 +54,8 @@ type ISegmentCardProps = { const SegmentCard: FC = ({ detail = {}, + contentExternal, + refSource, score, onClick, onChangeSwitch, @@ -88,6 +96,9 @@ const SegmentCard: FC = ({ ) } + if (contentExternal) + return contentExternal + return content } @@ -201,8 +212,8 @@ const SegmentCard: FC = ({
{ + const { notify } = useToastContext() + const [loading, setLoading] = useState(false) + 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' }) + else + throw new Error('Failed to create external knowledge base') } catch (error) { console.error('Error creating external knowledge base:', error) } + setLoading(false) } - return + return } export default ExternalKnowledgeBaseConnector diff --git a/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx b/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx index b0a8566a23..42ddebdfa3 100644 --- a/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx @@ -16,7 +16,7 @@ const KnowledgeBaseInfo: React.FC = ({ name, description onChange({ name: e.target.value }) } - const handleDescriptionChange = (e: React.ChangeEvent) => { + const handleDescriptionChange = (e: React.ChangeEvent) => { onChange({ description: e.target.value }) } @@ -38,11 +38,11 @@ const KnowledgeBaseInfo: React.FC = ({ name, description
- handleDescriptionChange(e)} placeholder={t('dataset.externalKnowledgeDescriptionPlaceholder') ?? ''} - className='flex h-20 p-2 self-stretch items-start' + className='flex h-20 p-2 self-stretch items-start rounded-lg bg-components-input-bg-normal text-components-input-text-placeholder system-sm-regular' />
diff --git a/web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx b/web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx index 4b804caef2..56793bb2fb 100644 --- a/web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx @@ -3,22 +3,32 @@ 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 = ({ topK, scoreThreshold, onChange }) => { +const RetrievalSettings: FC = ({ topK, scoreThreshold, onChange, isInHitTesting = false, isInRetrievalSetting = false }) => { const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(false) const { t } = useTranslation() return ( -
-
+
+ {!isInHitTesting && !isInRetrievalSetting &&
-
-
+
} +
void + loading: boolean } -const ExternalKnowledgeBaseCreate: React.FC = ({ onConnect }) => { +const ExternalKnowledgeBaseCreate: React.FC = ({ onConnect, loading }) => { const { t } = useTranslation() const router = useRouter() const [formData, setFormData] = useState({ @@ -24,7 +25,7 @@ const ExternalKnowledgeBaseCreate: React.FC = description: '', external_knowledge_api_id: '', external_knowledge_id: '', - external_retrieval_modal: { + external_retrieval_model: { top_k: 2, score_threshold: 0.5, }, @@ -43,8 +44,8 @@ const ExternalKnowledgeBaseCreate: React.FC = const isFormValid = formData.name !== '' && formData.external_knowledge_api_id !== '' && formData.external_knowledge_id !== '' - && formData.external_retrieval_modal.top_k !== undefined - && formData.external_retrieval_modal.score_threshold !== undefined + && formData.external_retrieval_model.top_k !== undefined + && formData.external_retrieval_model.score_threshold !== undefined return (
@@ -79,12 +80,12 @@ const ExternalKnowledgeBaseCreate: React.FC = })} /> handleFormChange({ ...formData, - external_retrieval_modal: { - ...formData.external_retrieval_modal, + external_retrieval_model: { + ...formData.external_retrieval_model, ...data, }, })} @@ -98,7 +99,10 @@ const ExternalKnowledgeBaseCreate: React.FC = onClick={() => { onConnect(formData) navBackHandle() - }} disabled={!isFormValid}> + }} + disabled={!isFormValid} + loading={loading} + >
{t('dataset.externalKnowledgeForm.connect')}
diff --git a/web/app/components/datasets/hit-testing/hit-detail.tsx b/web/app/components/datasets/hit-testing/hit-detail.tsx index 70e43176d9..a1c6b10e53 100644 --- a/web/app/components/datasets/hit-testing/hit-detail.tsx +++ b/web/app/components/datasets/hit-testing/hit-detail.tsx @@ -30,36 +30,40 @@ const HitDetail: FC = ({ segInfo }) => { } return ( -
-
-
- -
- - {segInfo?.word_count} {t('datasetDocuments.segment.characters')} - -
- - {segInfo?.hit_count} {t('datasetDocuments.segment.hitCount')} - -
- + segInfo?.id === 'external' + ?
{renderContent()}
-
- {t('datasetDocuments.segment.keywords')} -
-
- {!segInfo?.keywords?.length - ? '-' - : segInfo?.keywords?.map((word, index) => { - return
{word}
- })} +
+ :
+
+
+ +
+ + {segInfo?.word_count} {t('datasetDocuments.segment.characters')} + +
+ + {segInfo?.hit_count} {t('datasetDocuments.segment.hitCount')} + +
+ +
{renderContent()}
+
+ {t('datasetDocuments.segment.keywords')} +
+
+ {!segInfo?.keywords?.length + ? '-' + : segInfo?.keywords?.map((word, index) => { + return
{word}
+ })} +
-
) } diff --git a/web/app/components/datasets/hit-testing/index.tsx b/web/app/components/datasets/hit-testing/index.tsx index 505cd98fa7..cb345f4fc2 100644 --- a/web/app/components/datasets/hit-testing/index.tsx +++ b/web/app/components/datasets/hit-testing/index.tsx @@ -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 = ({ datasetId }: Props) => { const isMobile = media === MediaType.mobile const [hitResult, setHitResult] = useState() // 初始化记录为空数组 + const [externalHitResult, setExternalHitResult] = useState() 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(0) @@ -66,12 +68,50 @@ const HitTesting: FC = ({ 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) => ( + <> +
{t('datasetHitTesting.hit.title')}
+
+
+ {results.map((record, idx) => ( + onClickCard(record)} + /> + ))} +
+
+ + ) + + const renderEmptyState = () => ( +
+
+
+ {t('datasetHitTesting.hit.emptyTip')} +
+
+ ) + useEffect(() => { setShowRightPanel(!isMobile) }, [isMobile, setShowRightPanel]) @@ -86,12 +126,14 @@ const HitTesting: FC = ({ datasetId }: Props) => {