mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
feat: display all pdf pages and add DocumentPreview (#88)
* feat: add DocumentPreview * feat: display all pdf pages
This commit is contained in:
@ -48,3 +48,8 @@ export enum LlmModelType {
|
||||
Image2text = 'image2text',
|
||||
Speech2text = 'speech2text',
|
||||
}
|
||||
|
||||
export enum KnowledgeSearchParams {
|
||||
DocumentId = 'doc_id',
|
||||
KnowledgeId = 'id',
|
||||
}
|
||||
|
||||
@ -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) || '',
|
||||
};
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
.documentContainer {
|
||||
width: 100%;
|
||||
height: calc(100vh - 284px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
};
|
||||
@ -23,6 +23,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.documentPreview {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chunkContainer {
|
||||
height: calc(100vh - 320px);
|
||||
overflow: auto;
|
||||
|
||||
@ -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} />
|
||||
</>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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 拦截
|
||||
|
||||
|
||||
Reference in New Issue
Block a user