feat: display all pdf pages and add DocumentPreview (#88)

* feat: add DocumentPreview

* feat: display all pdf pages
This commit is contained in:
balibabu
2024-03-01 11:28:58 +08:00
committed by GitHub
parent 3d4315c42a
commit 5fb8989bb1
17 changed files with 813 additions and 43 deletions

View File

@ -48,3 +48,8 @@ export enum LlmModelType {
Image2text = 'image2text',
Speech2text = 'speech2text',
}
export enum KnowledgeSearchParams {
DocumentId = 'doc_id',
KnowledgeId = 'id',
}

View File

@ -1,4 +1,5 @@
import showDeleteConfirm from '@/components/deleting-confirm';
import { KnowledgeSearchParams } from '@/constants/knowledge';
import { IKnowledge, ITenantInfo } from '@/interfaces/database/knowledge';
import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSearchParams, useSelector } from 'umi';
@ -181,3 +182,14 @@ export const useFetchFileThumbnails = (docIds?: Array<string>) => {
return { fileThumbnails, fetchFileThumbnails };
};
export const useGetKnowledgeSearchParams = () => {
const [currentQueryParameters] = useSearchParams();
return {
documentId:
currentQueryParameters.get(KnowledgeSearchParams.DocumentId) || '',
knowledgeId:
currentQueryParameters.get(KnowledgeSearchParams.KnowledgeId) || '',
};
};

View File

@ -0,0 +1,20 @@
import { useSize } from 'ahooks';
import { useCallback, useEffect, useState } from 'react';
export const useDocumentResizeObserver = () => {
const [containerWidth, setContainerWidth] = useState<number>();
const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
const size = useSize(containerRef);
const onResize = useCallback((width?: number) => {
if (width) {
setContainerWidth(width);
}
}, []);
useEffect(() => {
onResize(size?.width);
}, [size?.width, onResize]);
return { containerWidth, setContainerRef };
};

View File

@ -0,0 +1,6 @@
.documentContainer {
width: 100%;
height: calc(100vh - 284px);
overflow-y: auto;
overflow-x: hidden;
}

View File

@ -0,0 +1,75 @@
import { useGetKnowledgeSearchParams } from '@/hooks/knowledgeHook';
import { api_host } from '@/utils/api';
import { useMemo, useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import { useDocumentResizeObserver } from './hooks';
import styles from './index.less';
// type PDFFile = string | File | null;
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
// const options = {
// cMapUrl: '/cmaps/',
// standardFontDataUrl: '/standard_fonts/',
// };
const DocumentPreview = () => {
const [numPages, setNumPages] = useState<number>();
const { documentId } = useGetKnowledgeSearchParams();
// const [file, setFile] = useState<PDFFile>(null);
const { containerWidth, setContainerRef } = useDocumentResizeObserver();
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
setNumPages(numPages);
}
// const handleChange = (e: any) => {
// console.info(e.files);
// setFile(e.target.files[0] || null);
// };
const url = useMemo(() => {
return `${api_host}/document/get/${documentId}`;
}, [documentId]);
// const fetch_document_file = useCallback(async () => {
// const ret: Blob = await getDocumentFile(documentId);
// console.info(ret);
// const f = new File([ret], 'xx.pdf', { type: ret.type });
// setFile(f);
// }, [documentId]);
// useEffect(() => {
// // dispatch({ type: 'kFModel/fetch_document_file', payload: documentId });
// fetch_document_file();
// }, [fetch_document_file]);
return (
<div ref={setContainerRef} className={styles.documentContainer}>
<Document
file={url}
onLoadSuccess={onDocumentLoadSuccess}
// options={options}
>
{Array.from(new Array(numPages), (el, index) => (
<Page
key={`page_${index + 1}`}
pageNumber={index + 1}
width={containerWidth}
/>
))}
</Document>
{/* <input type="file" onChange={handleChange} /> */}
</div>
);
};
export default DocumentPreview;

View File

@ -0,0 +1,9 @@
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { useSelector } from 'umi';
export const useSelectDocumentInfo = () => {
const documentInfo: IKnowledgeFile = useSelector(
(state: any) => state.chunkModel.documentInfo,
);
return documentInfo;
};

View File

@ -23,6 +23,11 @@
}
}
.documentPreview {
width: 40%;
height: 100%;
}
.chunkContainer {
height: calc(100vh - 320px);
overflow: auto;

View File

@ -1,6 +1,6 @@
import { getOneNamespaceEffectsLoading } from '@/utils/storeUtil';
import type { PaginationProps } from 'antd';
import { Divider, Pagination, Space, Spin, message } from 'antd';
import { Divider, Flex, Pagination, Space, Spin, message } from 'antd';
import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSearchParams, useSelector } from 'umi';
import CreatingModal from './components/chunk-creating-modal';
@ -8,6 +8,8 @@ import CreatingModal from './components/chunk-creating-modal';
import { useDeleteChunkByIds } from '@/hooks/knowledgeHook';
import ChunkCard from './components/chunk-card';
import ChunkToolBar from './components/chunk-toolbar';
import DocumentPreview from './components/document-preview';
import { useSelectDocumentInfo } from './hooks';
import styles from './index.less';
import { ChunkModelState } from './model';
@ -33,6 +35,7 @@ const Chunk = () => {
const documentId: string = searchParams.get('doc_id') || '';
const [chunkId, setChunkId] = useState<string | undefined>();
const { removeChunk } = useDeleteChunkByIds();
const documentInfo = useSelectDocumentInfo();
const getChunkList = useCallback(() => {
const payload: PayloadType = {
@ -158,39 +161,51 @@ const Chunk = () => {
switchChunk={switchChunk}
></ChunkToolBar>
<Divider></Divider>
<div className={styles.pageContent}>
<Spin spinning={loading} className={styles.spin} size="large">
<Space
direction="vertical"
size={'middle'}
className={styles.chunkContainer}
>
{data.map((item) => (
<ChunkCard
item={item}
key={item.chunk_id}
editChunk={handleEditChunk}
checked={selectedChunkIds.some((x) => x === item.chunk_id)}
handleCheckboxClick={handleSingleCheckboxClick}
switchChunk={switchChunk}
></ChunkCard>
))}
</Space>
</Spin>
</div>
<div className={styles.pageFooter}>
<Pagination
responsive
showLessItems
showQuickJumper
showSizeChanger
onChange={onPaginationChange}
pageSize={pagination.pageSize}
pageSizeOptions={[10, 30, 60, 90]}
current={pagination.current}
total={total}
/>
</div>
<Flex flex={1} gap={'middle'}>
<Flex flex={1} vertical>
<div className={styles.pageContent}>
<Spin spinning={loading} className={styles.spin} size="large">
<Space
direction="vertical"
size={'middle'}
className={styles.chunkContainer}
>
{data.map((item) => (
<ChunkCard
item={item}
key={item.chunk_id}
editChunk={handleEditChunk}
checked={selectedChunkIds.some(
(x) => x === item.chunk_id,
)}
handleCheckboxClick={handleSingleCheckboxClick}
switchChunk={switchChunk}
></ChunkCard>
))}
</Space>
</Spin>
</div>
<div className={styles.pageFooter}>
<Pagination
responsive
showLessItems
showQuickJumper
showSizeChanger
onChange={onPaginationChange}
pageSize={pagination.pageSize}
pageSizeOptions={[10, 30, 60, 90]}
current={pagination.current}
total={total}
/>
</div>
</Flex>
{documentInfo.type === 'pdf' && (
<section className={styles.documentPreview}>
<DocumentPreview></DocumentPreview>
</section>
)}
</Flex>
</div>
<CreatingModal doc_id={documentId} chunkId={chunkId} />
</>

View File

@ -13,7 +13,7 @@ export interface ChunkModelState extends BaseState {
chunk_id: string;
doc_id: string;
chunkInfo: any;
documentInfo: Partial<IKnowledgeFile>;
documentInfo: IKnowledgeFile;
available?: number;
}
@ -26,7 +26,7 @@ const model: DvaModel<ChunkModelState> = {
chunk_id: '',
doc_id: '',
chunkInfo: {},
documentInfo: {},
documentInfo: {} as IKnowledgeFile,
pagination: {
current: 1,
pageSize: 10,

View File

@ -1,6 +1,6 @@
import { BaseState } from '@/interfaces/common';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import kbService from '@/services/kbService';
import kbService, { getDocumentFile } from '@/services/kbService';
import { message } from 'antd';
import omit from 'lodash/omit';
import pick from 'lodash/pick';
@ -212,6 +212,16 @@ const model: DvaModel<KFModelState> = {
yield put({ type: 'setFileThumbnails', payload: data.data });
}
},
*fetch_document_file({ payload = {} }, { call }) {
const documentId = payload;
try {
const ret = yield call(getDocumentFile, documentId);
console.info('fetch_document_file:', ret);
return ret;
} catch (error) {
console.warn(error);
}
},
},
};
export default model;

View File

@ -29,7 +29,7 @@ const ChunkTitle = ({ item }: { item: ITestingChunk }) => {
<span className={styles.similarityCircle}>
{((item[x.field] as number) * 100).toFixed(2)}%
</span>
<span className={styles.similarityText}>Hybrid Similarity</span>
<span className={styles.similarityText}>{x.label}</span>
</Space>
))}
</Flex>

View File

@ -1,6 +1,7 @@
import api from '@/utils/api';
import registerServer from '@/utils/registerServer';
import request from '@/utils/request';
import pureRequest from 'umi-request';
const {
create_kb,
@ -23,6 +24,7 @@ const {
retrieval_test,
document_rename,
document_run,
get_document_file,
} = api;
const methods = {
@ -113,4 +115,39 @@ const methods = {
const kbService = registerServer<keyof typeof methods>(methods, request);
export const getDocumentFile = (documentId: string) => {
return pureRequest(get_document_file + '/' + documentId, {
responseType: 'blob',
method: 'get',
parseResponse: false,
// getResponse: true,
})
.then((res) => {
const x = res.headers.get('content-disposition');
console.info(res);
console.info(x);
return res.blob();
})
.then((res) => {
// const objectURL = URL.createObjectURL(res);
// let btn = document.createElement('a');
// btn.download = '文件名.pdf';
// btn.href = objectURL;
// btn.click();
// URL.revokeObjectURL(objectURL);
// btn = null;
return res;
})
.catch((err) => {
console.info(err);
});
};
export default kbService;

View File

@ -43,6 +43,7 @@ export default {
document_run: `${api_host}/document/run`,
document_change_parser: `${api_host}/document/change_parser`,
document_thumbnails: `${api_host}/document/thumbnails`,
get_document_file: `${api_host}/document/get`,
setDialog: `${api_host}/dialog/set`,
getDialog: `${api_host}/dialog/get`,

View File

@ -106,7 +106,10 @@ request.interceptors.request.use((url: string, options: any) => {
* 请求response拦截器
* */
request.interceptors.response.use(async (response: any, request) => {
request.interceptors.response.use(async (response: any, options) => {
if (options.responseType === 'blob') {
return response;
}
const data: ResponseType = await response.clone().json();
// response 拦截