diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index e0436d6f5c..5794852a49 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -5,7 +5,6 @@ import { usePathname } from 'next/navigation' import { useTranslation } from 'react-i18next' import type { RemixiconComponentType } from '@remixicon/react' import { - RiAttachmentLine, RiEqualizer2Fill, RiEqualizer2Line, RiFileTextFill, @@ -14,7 +13,6 @@ import { RiFocus2Line, } from '@remixicon/react' import { RiInformation2Line } from '@remixicon/react' -import classNames from '@/utils/classnames' import type { RelatedAppResponse } from '@/models/datasets' import AppSideBar from '@/app/components/app-sidebar' import Loading from '@/app/components/base/loading' @@ -22,7 +20,6 @@ import DatasetDetailContext from '@/context/dataset-detail' import { DataSourceType } from '@/models/datasets' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { useStore } from '@/app/components/app/store' -import { useDocLink } from '@/context/i18n' import { useAppContext } from '@/context/app-context' import Tooltip from '@/app/components/base/tooltip' import LinkedAppsPanel from '@/app/components/base/linked-apps-panel' @@ -49,7 +46,6 @@ const ExtraInfo = React.memo(({ expand, }: IExtraInfoProps) => { const { t } = useTranslation() - const docLink = useDocLink() const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0 const relatedAppsTotal = relatedApps?.data?.length || 0 @@ -57,7 +53,7 @@ const ExtraInfo = React.memo(({ return ( <> {!expand && ( -
+
{documentCount ?? '--'} @@ -74,7 +70,7 @@ const ExtraInfo = React.memo(({ {relatedAppsTotal ?? '--'}
)} - - {expand && ( -
- {relatedAppsTotal ?? '--'} - -
- )} ) }) diff --git a/web/app/components/app-sidebar/dataset-info.tsx b/web/app/components/app-sidebar/dataset-info.tsx deleted file mode 100644 index 3db8789722..0000000000 --- a/web/app/components/app-sidebar/dataset-info.tsx +++ /dev/null @@ -1,94 +0,0 @@ -'use client' -import type { FC } from 'react' -import React from 'react' -import { useTranslation } from 'react-i18next' -import AppIcon from '../base/app-icon' -import Effect from '../base/effect' -import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' -import type { DataSet } from '@/models/datasets' -import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets' -import { useKnowledge } from '@/hooks/use-knowledge' -import Badge from '../base/badge' -import cn from '@/utils/classnames' - -type Props = { - expand: boolean - extraInfo?: React.ReactNode -} - -const DatasetInfo: FC = ({ - expand, - extraInfo, -}) => { - const { t } = useTranslation() - const dataset = useDatasetDetailContextWithSelector(state => state.dataset) as DataSet - const iconInfo = dataset.icon_info || { - icon: '📙', - icon_type: 'emoji', - icon_background: '#FFF4ED', - icon_url: '', - } - const isExternalProvider = dataset.provider === 'external' - const { formatIndexingTechniqueAndMethod } = useKnowledge() - const chunkingModeIcon = dataset.doc_form ? DOC_FORM_ICON_WITH_BG[dataset.doc_form] : React.Fragment - const Icon = isExternalProvider ? DOC_FORM_ICON_WITH_BG.external : chunkingModeIcon - - return ( -
- {expand && ( - <> - -
-
- - {(dataset.doc_form || isExternalProvider) && ( -
- -
- )} -
- <> -
-
- {dataset.name} -
-
- {isExternalProvider && t('dataset.externalTag')} - {!isExternalProvider && dataset.doc_form && dataset.indexing_technique && ( -
- {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)} - {formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)} -
- )} -
-
-

- {dataset.description} -

- -
- - )} - {!expand && ( - - )} - {extraInfo} -
- ) -} -export default React.memo(DatasetInfo) diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx new file mode 100644 index 0000000000..bd73c8c182 --- /dev/null +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -0,0 +1,45 @@ +import React, { useCallback, useState } from 'react' +import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem' +import ActionButton from '../../base/action-button' +import { RiMoreFill } from '@remixicon/react' +import cn from '@/utils/classnames' +import Menu from './menu' + +type DropDownProps = { + expand: boolean +} + +const DropDown = ({ + expand, +}: DropDownProps) => { + const [open, setOpen] = useState(false) + + const handleTrigger = useCallback(() => { + setOpen(prev => !prev) + }, []) + + return ( + + + + + + + + + + + ) +} + +export default React.memo(DropDown) diff --git a/web/app/components/app-sidebar/dataset-info/index.tsx b/web/app/components/app-sidebar/dataset-info/index.tsx new file mode 100644 index 0000000000..3d9ebecf0a --- /dev/null +++ b/web/app/components/app-sidebar/dataset-info/index.tsx @@ -0,0 +1,88 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import AppIcon from '../../base/app-icon' +import Effect from '../../base/effect' +import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' +import type { DataSet } from '@/models/datasets' +import { DOC_FORM_TEXT } from '@/models/datasets' +import { useKnowledge } from '@/hooks/use-knowledge' +import cn from '@/utils/classnames' +import Dropdown from './dropdown' + +type DatasetInfoProps = { + expand: boolean +} + +const DatasetInfo: FC = ({ + expand, +}) => { + const { t } = useTranslation() + const dataset = useDatasetDetailContextWithSelector(state => state.dataset) as DataSet + const iconInfo = dataset.icon_info || { + icon: '📙', + icon_type: 'emoji', + icon_background: '#FFF4ED', + icon_url: '', + } + const isExternalProvider = dataset.provider === 'external' + const { formatIndexingTechniqueAndMethod } = useKnowledge() + + return ( +
+ {expand && ( + <> + +
+
+ + +
+
+
+ {dataset.name} +
+
+ {isExternalProvider && t('dataset.externalTag')} + {!isExternalProvider && dataset.doc_form && dataset.indexing_technique && ( +
+ {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)} + {formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)} +
+ )} +
+
+ {!!dataset.description && ( +

+ {dataset.description} +

+ )} +
+ + )} + {!expand && ( +
+ + +
+ )} +
+ ) +} +export default React.memo(DatasetInfo) diff --git a/web/app/components/app-sidebar/dataset-info/menu-item.tsx b/web/app/components/app-sidebar/dataset-info/menu-item.tsx new file mode 100644 index 0000000000..d81d78ee14 --- /dev/null +++ b/web/app/components/app-sidebar/dataset-info/menu-item.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import type { RemixiconComponentType } from '@remixicon/react' + +type MenuItemProps = { + name: string + Icon: RemixiconComponentType + handleClick?: () => void +} + +const MenuItem = ({ + Icon, + name, + handleClick, +}: MenuItemProps) => { + return ( +
{ + handleClick?.() + }} + > + + {name} +
+ ) +} + +export default React.memo(MenuItem) diff --git a/web/app/components/app-sidebar/dataset-info/menu.tsx b/web/app/components/app-sidebar/dataset-info/menu.tsx new file mode 100644 index 0000000000..f075ca5887 --- /dev/null +++ b/web/app/components/app-sidebar/dataset-info/menu.tsx @@ -0,0 +1,21 @@ +import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' +import React from 'react' +import { useTranslation } from 'react-i18next' +import MenuItem from './menu-item' +import { RiEditLine } from '@remixicon/react' +import { noop } from 'lodash-es' + +const Menu = () => { + const { t } = useTranslation() + const dataset = useDatasetDetailContextWithSelector(state => state.dataset) + + return ( +
+
+ +
+
+ ) +} + +export default React.memo(Menu) diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index 6d25a13051..cc30797b9e 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -82,25 +82,22 @@ const AppDetailNav = ({ return (
{iconType === 'app' && ( )} {iconType !== 'app' && ( - + )}
@@ -141,6 +138,7 @@ const AppDetailNav = ({ ) })} + {iconType !== 'app' && extraInfo && extraInfo(appSidebarExpand)}
) } diff --git a/web/app/components/datasets/list/dataset-card/index.tsx b/web/app/components/datasets/list/dataset-card/index.tsx index 7d02642ebc..8af05e7aa4 100644 --- a/web/app/components/datasets/list/dataset-card/index.tsx +++ b/web/app/components/datasets/list/dataset-card/index.tsx @@ -22,6 +22,7 @@ import Operations from './operations' import AppIcon from '@/app/components/base/app-icon' import CornerLabel from '@/app/components/base/corner-label' import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets' +import { useExportPipelineDSL } from '@/service/use-pipeline' const EXTERNAL_PROVIDER = 'external' @@ -45,6 +46,7 @@ const DatasetCard = ({ const [showRenameModal, setShowRenameModal] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [confirmMessage, setConfirmMessage] = useState('') + const [exporting, setExporting] = useState(false) const isExternalProvider = useMemo(() => { return dataset.provider === EXTERNAL_PROVIDER @@ -81,6 +83,36 @@ const DatasetCard = ({ return dayjs(time * 1_000).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow() }, [language]) + const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL() + + const handleExportPipeline = useCallback(async (include = false) => { + const { pipeline_id, name } = dataset + if (!pipeline_id) + return + + if (exporting) + return + + try { + setExporting(true) + const { data } = await exportPipelineConfig({ + pipelineId: pipeline_id, + include, + }) + const a = document.createElement('a') + const file = new Blob([data], { type: 'application/yaml' }) + a.href = URL.createObjectURL(file) + a.download = `${name}.yml` + a.click() + } + catch { + Toast.notify({ type: 'error', message: t('app.exportFailed') }) + } + finally { + setExporting(false) + } + }, [dataset, exportPipelineConfig, exporting, t]) + const detectIsUsedByApp = useCallback(async () => { try { const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id) @@ -234,6 +266,7 @@ const DatasetCard = ({ setShowRenameModal(true) }} detectIsUsedByApp={detectIsUsedByApp} + handleExportPipeline={handleExportPipeline} /> } className={'z-20 min-w-[186px]'} diff --git a/web/app/components/datasets/list/dataset-card/operation-item.tsx b/web/app/components/datasets/list/dataset-card/operation-item.tsx new file mode 100644 index 0000000000..4a7e7e0cf6 --- /dev/null +++ b/web/app/components/datasets/list/dataset-card/operation-item.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import type { RemixiconComponentType } from '@remixicon/react' + +type OperationItemProps = { + Icon: RemixiconComponentType + name: string + handleClick?: (e: React.MouseEvent) => void +} + +const OperationItem = ({ + Icon, + name, + handleClick, +}: OperationItemProps) => { + return ( +
+ + + {name} + +
+ ) +} + +export default React.memo(OperationItem) diff --git a/web/app/components/datasets/list/dataset-card/operations.tsx b/web/app/components/datasets/list/dataset-card/operations.tsx index 3d24ebbac5..2901ebc98d 100644 --- a/web/app/components/datasets/list/dataset-card/operations.tsx +++ b/web/app/components/datasets/list/dataset-card/operations.tsx @@ -1,17 +1,20 @@ import Divider from '@/app/components/base/divider' import React from 'react' import { useTranslation } from 'react-i18next' -import { RiDeleteBinLine, RiEditLine } from '@remixicon/react' +import { RiDeleteBinLine, RiEditLine, RiFileDownloadLine } from '@remixicon/react' +import OperationItem from './operation-item' type OperationsProps = { showDelete: boolean openRenameModal: () => void + handleExportPipeline: () => void detectIsUsedByApp: () => void } const Operations = ({ showDelete, openRenameModal, + handleExportPipeline, detectIsUsedByApp, }: OperationsProps) => { const { t } = useTranslation() @@ -22,6 +25,12 @@ const Operations = ({ openRenameModal() } + const onClickExport = async (e: React.MouseEvent) => { + e.stopPropagation() + e.preventDefault() + handleExportPipeline() + } + const onClickDelete = async (e: React.MouseEvent) => { e.stopPropagation() e.preventDefault() @@ -31,59 +40,26 @@ const Operations = ({ return (
-
- - - {t('common.operation.edit')} - -
- {/*
{ console.log('duplicate') }} - > - - - {t('common.operation.duplicate')} - -
*/} + +
- {/* -
-
{ console.log('Export') }} - > - - - Export Solution - -
-
{ console.log('Import') }} - > - - - Import Solution - -
-
*/} {showDelete && ( <>
-
- - - {t('common.operation.delete')} - -
+
)} diff --git a/web/i18n/en-US/dataset-pipeline.ts b/web/i18n/en-US/dataset-pipeline.ts index af0510ebf9..6fe3d38202 100644 --- a/web/i18n/en-US/dataset-pipeline.ts +++ b/web/i18n/en-US/dataset-pipeline.ts @@ -29,6 +29,7 @@ const translation = { dataSource: 'Data Source', saveAndProcess: 'Save & Process', preview: 'Preview', + exportPipeline: 'Export Pipeline', }, knowledgeNameAndIcon: 'Knowledge name & icon', knowledgeNameAndIconPlaceholder: 'Please enter the name of the Knowledge Base', diff --git a/web/i18n/zh-Hans/dataset-pipeline.ts b/web/i18n/zh-Hans/dataset-pipeline.ts index cd1b3a5a89..8471282694 100644 --- a/web/i18n/zh-Hans/dataset-pipeline.ts +++ b/web/i18n/zh-Hans/dataset-pipeline.ts @@ -29,6 +29,7 @@ const translation = { dataSource: '数据源', saveAndProcess: '保存并处理', preview: '预览', + exportPipeline: '导出 pipeline', }, knowledgeNameAndIcon: '知识库名称和图标', knowledgeNameAndIconPlaceholder: '请输入知识库名称',