| | |
| | | 'use client' |
| | | import type { FC } from 'react' |
| | | import React, { useCallback, useEffect, useMemo, useState } from 'react' |
| | | import useSWR from 'swr' |
| | | import { useTranslation } from 'react-i18next' |
| | | import { useRouter } from 'next/navigation' |
| | | import { useDebounce, useDebounceFn } from 'ahooks' |
| | | import { groupBy } from 'lodash-es' |
| | | import { groupBy, omit } from 'lodash-es' |
| | | import { PlusIcon } from '@heroicons/react/24/solid' |
| | | import { RiDraftLine, RiExternalLinkLine } from '@remixicon/react' |
| | | import { RiExternalLinkLine } from '@remixicon/react' |
| | | import AutoDisabledDocument from '../common/document-status-with-action/auto-disabled-document' |
| | | import List from './list' |
| | | import s from './style.module.css' |
| | |
| | | import Button from '@/app/components/base/button' |
| | | import Input from '@/app/components/base/input' |
| | | import { get } from '@/service/base' |
| | | import { createDocument } from '@/service/datasets' |
| | | import { createDocument, fetchDocuments } from '@/service/datasets' |
| | | import { useDatasetDetailContext } from '@/context/dataset-detail' |
| | | import { NotionPageSelectorModal } from '@/app/components/base/notion-page-selector' |
| | | import type { NotionPage } from '@/models/common' |
| | | import type { CreateDocumentReq } from '@/models/datasets' |
| | | import { DataSourceType, ProcessMode } from '@/models/datasets' |
| | | import { DataSourceType } from '@/models/datasets' |
| | | import IndexFailed from '@/app/components/datasets/common/document-status-with-action/index-failed' |
| | | import { useProviderContext } from '@/context/provider-context' |
| | | import cn from '@/utils/classnames' |
| | | import { useDocumentList, useInvalidDocumentDetailKey, useInvalidDocumentList } from '@/service/knowledge/use-document' |
| | | import { useInvalidDocumentDetailKey } from '@/service/knowledge/use-document' |
| | | import { useInvalid } from '@/service/use-base' |
| | | import { useChildSegmentListKey, useSegmentListKey } from '@/service/knowledge/use-segment' |
| | | import useEditDocumentMetadata from '../metadata/hooks/use-edit-dataset-metadata' |
| | | import DatasetMetadataDrawer from '../metadata/metadata-dataset/dataset-metadata-drawer' |
| | | import StatusWithAction from '../common/document-status-with-action/status-with-action' |
| | | import { LanguagesSupported } from '@/i18n/language' |
| | | import { getLocaleOnClient } from '@/i18n' |
| | | |
| | | const FolderPlusIcon = ({ className }: React.SVGProps<SVGElement>) => { |
| | | return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}> |
| | |
| | | <div className={s.emptySymbolIconWrapper}> |
| | | {type === 'upload' ? <FolderPlusIcon /> : <NotionIcon />} |
| | | </div> |
| | | <span className={s.emptyTitle}>{t('datasetDocuments.list.empty.title')}<ThreeDotsIcon className='relative -left-1.5 -top-3 inline' /></span> |
| | | <span className={s.emptyTitle}>{t('datasetDocuments.list.empty.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span> |
| | | <div className={s.emptyTip}> |
| | | {t(`datasetDocuments.list.empty.${type}.tip`)} |
| | | </div> |
| | |
| | | } |
| | | |
| | | export const fetcher = (url: string) => get(url, {}, {}) |
| | | const DEFAULT_LIMIT = 10 |
| | | const DEFAULT_LIMIT = 15 |
| | | |
| | | const Documents: FC<IDocumentsProps> = ({ datasetId }) => { |
| | | const { t } = useTranslation() |
| | |
| | | const isDataSourceWeb = dataset?.data_source_type === DataSourceType.WEB |
| | | const isDataSourceFile = dataset?.data_source_type === DataSourceType.FILE |
| | | const embeddingAvailable = !!dataset?.embedding_available |
| | | const locale = getLocaleOnClient() |
| | | |
| | | const debouncedSearchValue = useDebounce(searchValue, { wait: 500 }) |
| | | |
| | | const { data: documentsRes, isFetching: isListLoading } = useDocumentList({ |
| | | const query = useMemo(() => { |
| | | return { page: currPage + 1, limit, keyword: debouncedSearchValue, fetch: isDataSourceNotion ? true : '' } |
| | | }, [currPage, debouncedSearchValue, isDataSourceNotion, limit]) |
| | | |
| | | const { data: documentsRes, mutate, isLoading: isListLoading } = useSWR( |
| | | { |
| | | action: 'fetchDocuments', |
| | | datasetId, |
| | | query: { |
| | | page: currPage + 1, |
| | | limit, |
| | | keyword: debouncedSearchValue, |
| | | params: query, |
| | | }, |
| | | refetchInterval: (isDataSourceNotion && timerCanRun) ? 2500 : 0, |
| | | }) |
| | | apiParams => fetchDocuments(omit(apiParams, 'action')), |
| | | { refreshInterval: (isDataSourceNotion && timerCanRun) ? 2500 : 0 }, |
| | | ) |
| | | |
| | | const invalidDocumentList = useInvalidDocumentList(datasetId) |
| | | |
| | | const [isMuting, setIsMuting] = useState(false) |
| | | useEffect(() => { |
| | | if (documentsRes) { |
| | | const totalPages = Math.ceil(documentsRes.total / limit) |
| | | if (totalPages < currPage + 1) |
| | | setCurrPage(totalPages === 0 ? 0 : totalPages - 1) |
| | | } |
| | | // eslint-disable-next-line react-hooks/exhaustive-deps |
| | | }, [documentsRes]) |
| | | if (!isListLoading && isMuting) |
| | | setIsMuting(false) |
| | | }, [isListLoading, isMuting]) |
| | | |
| | | const invalidDocumentDetail = useInvalidDocumentDetailKey() |
| | | const invalidChunkList = useInvalid(useSegmentListKey) |
| | | const invalidChildChunkList = useInvalid(useChildSegmentListKey) |
| | | |
| | | const handleUpdate = useCallback(() => { |
| | | invalidDocumentList() |
| | | setIsMuting(true) |
| | | mutate() |
| | | invalidDocumentDetail() |
| | | setTimeout(() => { |
| | | invalidChunkList() |
| | |
| | | router.push(`/datasets/${datasetId}/documents/create`) |
| | | } |
| | | |
| | | const isLoading = isListLoading // !documentsRes && !error |
| | | |
| | | const handleSaveNotionPageSelected = async (selectedPages: NotionPage[]) => { |
| | | const workspacesMap = groupBy(selectedPages, 'workspace_id') |
| | | const workspaces = Object.keys(workspacesMap).map((workspaceId) => { |
| | |
| | | indexing_technique: dataset?.indexing_technique, |
| | | process_rule: { |
| | | rules: {}, |
| | | mode: ProcessMode.general, |
| | | mode: 'automatic', |
| | | }, |
| | | } as CreateDocumentReq |
| | | |
| | |
| | | datasetId, |
| | | body: params, |
| | | }) |
| | | invalidDocumentList() |
| | | mutate() |
| | | setTimerCanRun(true) |
| | | // mutateDatasetIndexingStatus(undefined, { revalidate: true }) |
| | | setNotionPageSelectorModalVisible(false) |
| | |
| | | handleSearch() |
| | | } |
| | | |
| | | const { |
| | | isShowEditModal: isShowEditMetadataModal, |
| | | showEditModal: showEditMetadataModal, |
| | | hideEditModal: hideEditMetadataModal, |
| | | datasetMetaData, |
| | | handleAddMetaData, |
| | | handleRename, |
| | | handleDeleteMetaData, |
| | | builtInEnabled, |
| | | setBuiltInEnabled, |
| | | builtInMetaData, |
| | | } = useEditDocumentMetadata({ |
| | | datasetId, |
| | | dataset, |
| | | onUpdateDocList: invalidDocumentList, |
| | | }) |
| | | |
| | | return ( |
| | | <div className='flex h-full flex-col overflow-y-auto'> |
| | | <div className='flex flex-col h-full overflow-y-auto'> |
| | | <div className='flex flex-col justify-center gap-1 px-6 pt-4'> |
| | | <h1 className='text-base font-semibold text-text-primary'>{t('datasetDocuments.list.title')}</h1> |
| | | <div className='flex items-center space-x-0.5 text-sm font-normal text-text-tertiary'> |
| | | <div className='flex items-center text-sm font-normal text-text-tertiary space-x-0.5'> |
| | | <span>{t('datasetDocuments.list.desc')}</span> |
| | | <a |
| | | className='flex items-center text-text-accent' |
| | | target='_blank' |
| | | href={ |
| | | locale === LanguagesSupported[1] |
| | | ? 'https://docs.dify.ai/zh-hans/guides/knowledge-base/integrate-knowledge-within-application' |
| | | : 'https://docs.dify.ai/en/guides/knowledge-base/integrate-knowledge-within-application' |
| | | } |
| | | > |
| | | href='https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'> |
| | | <span>{t('datasetDocuments.list.learnMore')}</span> |
| | | <RiExternalLinkLine className='h-3 w-3' /> |
| | | <RiExternalLinkLine className='w-3 h-3' /> |
| | | </a> |
| | | </div> |
| | | </div> |
| | | <div className='flex flex-1 flex-col px-6 py-4'> |
| | | <div className='flex flex-wrap items-center justify-between'> |
| | | <div className='flex flex-col px-6 py-4 flex-1'> |
| | | <div className='flex items-center justify-between flex-wrap'> |
| | | <Input |
| | | showLeftIcon |
| | | showClearIcon |
| | |
| | | onChange={e => handleInputChange(e.target.value)} |
| | | onClear={() => handleInputChange('')} |
| | | /> |
| | | <div className='flex !h-8 items-center justify-center gap-2'> |
| | | <div className='flex gap-2 justify-center items-center !h-8'> |
| | | {!isFreePlan && <AutoDisabledDocument datasetId={datasetId} />} |
| | | <IndexFailed datasetId={datasetId} /> |
| | | {!embeddingAvailable && <StatusWithAction type='warning' description={t('dataset.embeddingModelNotAvailable')} />} |
| | | {embeddingAvailable && ( |
| | | <Button variant='secondary' className='shrink-0' onClick={showEditMetadataModal}> |
| | | <RiDraftLine className='mr-1 size-4' /> |
| | | {t('dataset.metadata.metadata')} |
| | | </Button> |
| | | )} |
| | | {isShowEditMetadataModal && ( |
| | | <DatasetMetadataDrawer |
| | | userMetadata={datasetMetaData || []} |
| | | onClose={hideEditMetadataModal} |
| | | onAdd={handleAddMetaData} |
| | | onRename={handleRename} |
| | | onRemove={handleDeleteMetaData} |
| | | builtInMetadata={builtInMetaData || []} |
| | | isBuiltInEnabled={!!builtInEnabled} |
| | | onIsBuiltInEnabledChange={setBuiltInEnabled} |
| | | /> |
| | | )} |
| | | {embeddingAvailable && ( |
| | | <Button variant='primary' onClick={routeToDocCreate} className='shrink-0'> |
| | | <PlusIcon className={cn('mr-2 h-4 w-4 stroke-current')} /> |
| | | <PlusIcon className={cn('h-4 w-4 mr-2 stroke-current')} /> |
| | | {isDataSourceNotion && t('datasetDocuments.list.addPages')} |
| | | {isDataSourceWeb && t('datasetDocuments.list.addUrl')} |
| | | {(!dataset?.data_source_type || isDataSourceFile) && t('datasetDocuments.list.addFile')} |
| | |
| | | )} |
| | | </div> |
| | | </div> |
| | | {isListLoading |
| | | {(isLoading && !isMuting) |
| | | ? <Loading type='app' /> |
| | | : total > 0 |
| | | ? <List |
| | |
| | | current: currPage, |
| | | onChange: setCurrPage, |
| | | }} |
| | | onManageMetadata={showEditMetadataModal} |
| | | /> |
| | | : <EmptyElement canAdd={embeddingAvailable} onClick={routeToDocCreate} type={isDataSourceNotion ? 'sync' : 'upload'} /> |
| | | } |