mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-24 23:46:52 +08:00
Refactoring: Integrating the file preview component (#11523)
### What problem does this PR solve? Refactoring: Integrating the file preview component ### Type of change - [x] Refactoring
This commit is contained in:
114
web/src/components/document-preview/csv-preview.tsx
Normal file
114
web/src/components/document-preview/csv-preview.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { Spin } from '@/components/ui/spin';
|
||||
import request from '@/utils/request';
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface CSVData {
|
||||
rows: string[][];
|
||||
headers: string[];
|
||||
}
|
||||
|
||||
interface FileViewerProps {
|
||||
className?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const CSVFileViewer: React.FC<FileViewerProps> = ({ url }) => {
|
||||
const [data, setData] = useState<CSVData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// const url = useGetDocumentUrl();
|
||||
const parseCSV = (csvText: string): CSVData => {
|
||||
console.log('Parsing CSV data:', csvText);
|
||||
const lines = csvText.split('\n');
|
||||
const headers = lines[0].split(',').map((header) => header.trim());
|
||||
const rows = lines
|
||||
.slice(1)
|
||||
.map((line) => line.split(',').map((cell) => cell.trim()));
|
||||
|
||||
return { headers, rows };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadCSV = async () => {
|
||||
try {
|
||||
const res = await request(url, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
onError: () => {
|
||||
message.error('file load failed');
|
||||
setIsLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
// parse CSV file
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(res.data);
|
||||
reader.onload = () => {
|
||||
const parsedData = parseCSV(reader.result as string);
|
||||
console.log('file loaded successfully', reader.result);
|
||||
setData(parsedData);
|
||||
};
|
||||
} catch (error) {
|
||||
message.error('CSV file parse failed');
|
||||
console.error('Error loading CSV file:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadCSV();
|
||||
|
||||
return () => {
|
||||
setData(null);
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classNames(
|
||||
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md',
|
||||
'overflow-auto max-h-[80vh] p-2',
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Spin />
|
||||
</div>
|
||||
) : data ? (
|
||||
<table className="min-w-full divide-y divide-border-normal">
|
||||
<thead className="bg-background-header-bar">
|
||||
<tr>
|
||||
{data.headers.map((header, index) => (
|
||||
<th
|
||||
key={`header-${index}`}
|
||||
className="px-6 py-3 text-left text-sm font-medium text-text-primary"
|
||||
>
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-background-paper divide-y divide-border-normal">
|
||||
{data.rows.map((row, rowIndex) => (
|
||||
<tr key={`row-${rowIndex}`}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td
|
||||
key={`cell-${rowIndex}-${cellIndex}`}
|
||||
className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary"
|
||||
>
|
||||
{cell || '-'}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CSVFileViewer;
|
||||
73
web/src/components/document-preview/doc-preview.tsx
Normal file
73
web/src/components/document-preview/doc-preview.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { Spin } from '@/components/ui/spin';
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
import request from '@/utils/request';
|
||||
import classNames from 'classnames';
|
||||
import mammoth from 'mammoth';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface DocPreviewerProps {
|
||||
className?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const DocPreviewer: React.FC<DocPreviewerProps> = ({
|
||||
className,
|
||||
url,
|
||||
}) => {
|
||||
// const url = useGetDocumentUrl();
|
||||
const [htmlContent, setHtmlContent] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const fetchDocument = async () => {
|
||||
setLoading(true);
|
||||
const res = await request(url, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: { [Authorization]: getAuthorization() },
|
||||
onError: () => {
|
||||
message.error('Document parsing failed');
|
||||
console.error('Error loading document:', url);
|
||||
},
|
||||
});
|
||||
try {
|
||||
const arrayBuffer = await res.data.arrayBuffer();
|
||||
const result = await mammoth.convertToHtml(
|
||||
{ arrayBuffer },
|
||||
{ includeDefaultStyleMap: true },
|
||||
);
|
||||
|
||||
const styledContent = result.value
|
||||
.replace(/<p>/g, '<p class="mb-2">')
|
||||
.replace(/<h(\d)>/g, '<h$1 class="font-semibold mt-4 mb-2">');
|
||||
|
||||
setHtmlContent(styledContent);
|
||||
} catch (err) {
|
||||
message.error('Document parsing failed');
|
||||
console.error('Error parsing document:', err);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
fetchDocument();
|
||||
}
|
||||
}, [url]);
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && <div dangerouslySetInnerHTML={{ __html: htmlContent }} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
21
web/src/components/document-preview/document-header.tsx
Normal file
21
web/src/components/document-preview/document-header.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { formatBytes } from '@/utils/file-util';
|
||||
|
||||
type Props = {
|
||||
size: number;
|
||||
name: string;
|
||||
create_date: string;
|
||||
};
|
||||
|
||||
export default ({ size, name, create_date }: Props) => {
|
||||
const sizeName = formatBytes(size);
|
||||
const dateStr = formatDate(create_date);
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-[16px]">{name}</h2>
|
||||
<div className="text-text-secondary text-[12px] pt-[5px]">
|
||||
Size:{sizeName} Uploaded Time:{dateStr}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
26
web/src/components/document-preview/excel-preview.tsx
Normal file
26
web/src/components/document-preview/excel-preview.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
// import { useFetchExcel } from '@/pages/document-viewer/hooks';
|
||||
import classNames from 'classnames';
|
||||
import { useFetchExcel } from './hooks';
|
||||
|
||||
interface ExcelCsvPreviewerProps {
|
||||
className?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const ExcelCsvPreviewer: React.FC<ExcelCsvPreviewerProps> = ({
|
||||
className,
|
||||
url,
|
||||
}) => {
|
||||
// const url = useGetDocumentUrl();
|
||||
const { containerRef } = useFetchExcel(url);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classNames(
|
||||
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md excel-csv-previewer',
|
||||
className,
|
||||
)}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
167
web/src/components/document-preview/hooks.ts
Normal file
167
web/src/components/document-preview/hooks.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
|
||||
import { useGetPipelineResultSearchParams } from '@/pages/dataflow-result/hooks';
|
||||
import api, { api_host } from '@/utils/api';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
import jsPreviewExcel from '@js-preview/excel';
|
||||
import { useSize } from 'ahooks';
|
||||
import axios from 'axios';
|
||||
import mammoth from 'mammoth';
|
||||
import { useCallback, useEffect, useMemo, useRef, 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 };
|
||||
};
|
||||
|
||||
function highlightPattern(text: string, pattern: string, pageNumber: number) {
|
||||
if (pageNumber === 2) {
|
||||
return `<mark>${text}</mark>`;
|
||||
}
|
||||
if (text.trim() !== '' && pattern.match(text)) {
|
||||
// return pattern.replace(text, (value) => `<mark>${value}</mark>`);
|
||||
return `<mark>${text}</mark>`;
|
||||
}
|
||||
return text.replace(pattern, (value) => `<mark>${value}</mark>`);
|
||||
}
|
||||
|
||||
export const useHighlightText = (searchText: string = '') => {
|
||||
const textRenderer = useCallback(
|
||||
(textItem: any) => {
|
||||
return highlightPattern(textItem.str, searchText, textItem.pageNumber);
|
||||
},
|
||||
[searchText],
|
||||
);
|
||||
|
||||
return textRenderer;
|
||||
};
|
||||
|
||||
export const useGetDocumentUrl = (isAgent: boolean) => {
|
||||
const { documentId } = useGetKnowledgeSearchParams();
|
||||
const { createdBy, documentId: id } = useGetPipelineResultSearchParams();
|
||||
|
||||
const url = useMemo(() => {
|
||||
if (isAgent) {
|
||||
return api.downloadFile + `?id=${id}&created_by=${createdBy}`;
|
||||
}
|
||||
return `${api_host}/document/get/${documentId}`;
|
||||
}, [createdBy, documentId, id, isAgent]);
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
export const useCatchError = (api: string) => {
|
||||
const [error, setError] = useState('');
|
||||
const fetchDocument = useCallback(async () => {
|
||||
const ret = await axios.get(api);
|
||||
const { data } = ret;
|
||||
if (!(data instanceof ArrayBuffer) && data.code !== 0) {
|
||||
setError(data.message);
|
||||
}
|
||||
return ret;
|
||||
}, [api]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocument();
|
||||
}, [fetchDocument]);
|
||||
|
||||
return { fetchDocument, error };
|
||||
};
|
||||
|
||||
export const useFetchDocument = () => {
|
||||
const fetchDocument = useCallback(async (api: string) => {
|
||||
const ret = await axios.get(api, {
|
||||
headers: {
|
||||
[Authorization]: getAuthorization(),
|
||||
},
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
return ret;
|
||||
}, []);
|
||||
|
||||
return { fetchDocument };
|
||||
};
|
||||
|
||||
export const useFetchExcel = (filePath: string) => {
|
||||
const [status, setStatus] = useState(true);
|
||||
const { fetchDocument } = useFetchDocument();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { error } = useCatchError(filePath);
|
||||
|
||||
const fetchDocumentAsync = useCallback(async () => {
|
||||
let myExcelPreviewer;
|
||||
if (containerRef.current) {
|
||||
myExcelPreviewer = jsPreviewExcel.init(containerRef.current);
|
||||
}
|
||||
const jsonFile = await fetchDocument(filePath);
|
||||
myExcelPreviewer
|
||||
?.preview(jsonFile.data)
|
||||
.then(() => {
|
||||
console.log('succeed');
|
||||
setStatus(true);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('failed', e);
|
||||
myExcelPreviewer.destroy();
|
||||
setStatus(false);
|
||||
});
|
||||
}, [filePath, fetchDocument]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocumentAsync();
|
||||
}, [fetchDocumentAsync]);
|
||||
|
||||
return { status, containerRef, error };
|
||||
};
|
||||
|
||||
export const useFetchDocx = (filePath: string) => {
|
||||
const [succeed, setSucceed] = useState(true);
|
||||
const [error, setError] = useState<string>();
|
||||
const { fetchDocument } = useFetchDocument();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchDocumentAsync = useCallback(async () => {
|
||||
try {
|
||||
const jsonFile = await fetchDocument(filePath);
|
||||
mammoth
|
||||
.convertToHtml(
|
||||
{ arrayBuffer: jsonFile.data },
|
||||
{ includeDefaultStyleMap: true },
|
||||
)
|
||||
.then((result) => {
|
||||
setSucceed(true);
|
||||
const docEl = document.createElement('div');
|
||||
docEl.className = 'document-container';
|
||||
docEl.innerHTML = result.value;
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
container.innerHTML = docEl.outerHTML;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setSucceed(false);
|
||||
});
|
||||
} catch (error: any) {
|
||||
setError(error.toString());
|
||||
}
|
||||
}, [filePath, fetchDocument]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocumentAsync();
|
||||
}, [fetchDocumentAsync]);
|
||||
|
||||
return { succeed, containerRef, error };
|
||||
};
|
||||
76
web/src/components/document-preview/image-preview.tsx
Normal file
76
web/src/components/document-preview/image-preview.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { Spin } from '@/components/ui/spin';
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
import request from '@/utils/request';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface ImagePreviewerProps {
|
||||
className?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const ImagePreviewer: React.FC<ImagePreviewerProps> = ({
|
||||
className,
|
||||
url,
|
||||
}) => {
|
||||
// const url = useGetDocumentUrl();
|
||||
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
const fetchImage = async () => {
|
||||
setIsLoading(true);
|
||||
const res = await request(url, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: { [Authorization]: getAuthorization() },
|
||||
onError: () => {
|
||||
message.error('Failed to load image');
|
||||
setIsLoading(false);
|
||||
},
|
||||
});
|
||||
const objectUrl = URL.createObjectURL(res.data);
|
||||
setImageSrc(objectUrl);
|
||||
setIsLoading(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
fetchImage();
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (imageSrc) {
|
||||
URL.revokeObjectURL(imageSrc);
|
||||
}
|
||||
};
|
||||
}, [imageSrc]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md image-previewer',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && imageSrc && (
|
||||
<div className="max-h-[80vh] overflow-auto p-2">
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={'image'}
|
||||
className="w-full h-auto max-w-full object-contain"
|
||||
onLoad={() => URL.revokeObjectURL(imageSrc!)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
13
web/src/components/document-preview/index.less
Normal file
13
web/src/components/document-preview/index.less
Normal file
@ -0,0 +1,13 @@
|
||||
.documentContainer {
|
||||
width: 100%;
|
||||
// height: calc(100vh - 284px);
|
||||
height: calc(100vh - 180px);
|
||||
position: relative;
|
||||
:global(.PdfHighlighter) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
:global(.Highlight--scrolledTo .Highlight__part) {
|
||||
overflow-x: hidden;
|
||||
background-color: rgba(255, 226, 143, 1);
|
||||
}
|
||||
}
|
||||
93
web/src/components/document-preview/index.tsx
Normal file
93
web/src/components/document-preview/index.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import CSVFileViewer from './csv-preview';
|
||||
import { DocPreviewer } from './doc-preview';
|
||||
import { ExcelCsvPreviewer } from './excel-preview';
|
||||
import { ImagePreviewer } from './image-preview';
|
||||
import { Md } from './md';
|
||||
import PdfPreviewer, { IProps } from './pdf-preview';
|
||||
import { PptPreviewer } from './ppt-preview';
|
||||
import { TxtPreviewer } from './txt-preview';
|
||||
import { VideoPreviewer } from './video-preview';
|
||||
|
||||
type PreviewProps = {
|
||||
fileType: string;
|
||||
className?: string;
|
||||
url: string;
|
||||
};
|
||||
const Preview = ({
|
||||
fileType,
|
||||
className,
|
||||
highlights,
|
||||
setWidthAndHeight,
|
||||
url,
|
||||
}: PreviewProps & Partial<IProps>) => {
|
||||
return (
|
||||
<>
|
||||
{fileType === 'pdf' && highlights && setWidthAndHeight && (
|
||||
<section>
|
||||
<PdfPreviewer
|
||||
highlights={highlights}
|
||||
setWidthAndHeight={setWidthAndHeight}
|
||||
url={url}
|
||||
></PdfPreviewer>
|
||||
</section>
|
||||
)}
|
||||
{['doc', 'docx'].indexOf(fileType) > -1 && (
|
||||
<section>
|
||||
<DocPreviewer className={className} url={url} />
|
||||
</section>
|
||||
)}
|
||||
{['txt'].indexOf(fileType) > -1 && (
|
||||
<section>
|
||||
<TxtPreviewer className={className} url={url} />
|
||||
</section>
|
||||
)}
|
||||
{['jpg', 'png', 'gif', 'jpeg', 'svg', 'bmp', 'ico', 'tif'].indexOf(
|
||||
fileType,
|
||||
) > -1 && (
|
||||
<section>
|
||||
<ImagePreviewer className={className} url={url} />
|
||||
</section>
|
||||
)}
|
||||
{[
|
||||
'mp4',
|
||||
'avi',
|
||||
'mov',
|
||||
'mkv',
|
||||
'wmv',
|
||||
'flv',
|
||||
'mpeg',
|
||||
'mpg',
|
||||
'asf',
|
||||
'rm',
|
||||
'rmvb',
|
||||
].indexOf(fileType) > -1 && (
|
||||
<section>
|
||||
<VideoPreviewer className={className} url={url} />
|
||||
</section>
|
||||
)}
|
||||
{['pptx'].indexOf(fileType) > -1 && (
|
||||
<section>
|
||||
<PptPreviewer className={className} url={url} />
|
||||
</section>
|
||||
)}
|
||||
{['xlsx'].indexOf(fileType) > -1 && (
|
||||
<section>
|
||||
<ExcelCsvPreviewer className={className} url={url} />
|
||||
</section>
|
||||
)}
|
||||
{['csv'].indexOf(fileType) > -1 && (
|
||||
<section>
|
||||
<CSVFileViewer className={className} url={url} />
|
||||
</section>
|
||||
)}
|
||||
{['md'].indexOf(fileType) > -1 && (
|
||||
<section>
|
||||
<Md className={className} url={url} />
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default memo(Preview);
|
||||
42
web/src/components/document-preview/md/index.tsx
Normal file
42
web/src/components/document-preview/md/index.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import { cn } from '@/lib/utils';
|
||||
import FileError from '@/pages/document-viewer/file-error';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
interface MdProps {
|
||||
// filePath: string;
|
||||
className?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const Md: React.FC<MdProps> = ({ url, className }) => {
|
||||
const [content, setContent] = useState<string>('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setError(null);
|
||||
fetch(url, { headers: { [Authorization]: getAuthorization() } })
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('Failed to fetch markdown file');
|
||||
return res.text();
|
||||
})
|
||||
.then((text) => setContent(text))
|
||||
.catch((err) => setError(err.message));
|
||||
}, [url]);
|
||||
|
||||
if (error) return <FileError>{error}</FileError>;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ padding: 4, overflow: 'scroll' }}
|
||||
className={cn(className, 'markdown-body h-[calc(100vh - 200px)]')}
|
||||
>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Md;
|
||||
145
web/src/components/document-preview/pdf-preview.tsx
Normal file
145
web/src/components/document-preview/pdf-preview.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import {
|
||||
AreaHighlight,
|
||||
Highlight,
|
||||
IHighlight,
|
||||
PdfHighlighter,
|
||||
PdfLoader,
|
||||
Popup,
|
||||
} from 'react-pdf-highlighter';
|
||||
|
||||
import { useCatchDocumentError } from '@/components/pdf-previewer/hooks';
|
||||
import { Spin } from '@/components/ui/spin';
|
||||
// import FileError from '@/pages/document-viewer/file-error';
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import FileError from '@/pages/document-viewer/file-error';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
import styles from './index.less';
|
||||
type PdfLoaderProps = React.ComponentProps<typeof PdfLoader> & {
|
||||
httpHeaders?: Record<string, string>;
|
||||
};
|
||||
|
||||
const Loader = PdfLoader as React.ComponentType<PdfLoaderProps>;
|
||||
export interface IProps {
|
||||
highlights?: IHighlight[];
|
||||
setWidthAndHeight?: (width: number, height: number) => void;
|
||||
url: string;
|
||||
className?: string;
|
||||
}
|
||||
const HighlightPopup = ({
|
||||
comment,
|
||||
}: {
|
||||
comment: { text: string; emoji: string };
|
||||
}) =>
|
||||
comment.text ? (
|
||||
<div className="Highlight__popup">
|
||||
{comment.emoji} {comment.text}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// TODO: merge with DocumentPreviewer
|
||||
const PdfPreview = ({
|
||||
highlights: state,
|
||||
setWidthAndHeight,
|
||||
url,
|
||||
className,
|
||||
}: IProps) => {
|
||||
// const url = useGetDocumentUrl();
|
||||
|
||||
const ref = useRef<(highlight: IHighlight) => void>(() => {});
|
||||
const error = useCatchDocumentError(url);
|
||||
|
||||
const resetHash = () => {};
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.length && state?.length > 0) {
|
||||
ref?.current(state[0]);
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
const httpHeaders = {
|
||||
[Authorization]: getAuthorization(),
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.documentContainer} rounded-[10px] overflow-hidden ${className}`}
|
||||
>
|
||||
<Loader
|
||||
url={url}
|
||||
httpHeaders={httpHeaders}
|
||||
beforeLoad={
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Spin />
|
||||
</div>
|
||||
}
|
||||
workerSrc="/pdfjs-dist/pdf.worker.min.js"
|
||||
errorMessage={<FileError>{error}</FileError>}
|
||||
>
|
||||
{(pdfDocument) => {
|
||||
pdfDocument.getPage(1).then((page) => {
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
const width = viewport.width;
|
||||
const height = viewport.height;
|
||||
setWidthAndHeight?.(width, height);
|
||||
});
|
||||
|
||||
return (
|
||||
<PdfHighlighter
|
||||
pdfDocument={pdfDocument}
|
||||
enableAreaSelection={(event) => event.altKey}
|
||||
onScrollChange={resetHash}
|
||||
scrollRef={(scrollTo) => {
|
||||
ref.current = scrollTo;
|
||||
}}
|
||||
onSelectionFinished={() => null}
|
||||
highlightTransform={(
|
||||
highlight,
|
||||
index,
|
||||
setTip,
|
||||
hideTip,
|
||||
viewportToScaled,
|
||||
screenshot,
|
||||
isScrolledTo,
|
||||
) => {
|
||||
const isTextHighlight = !Boolean(
|
||||
highlight.content && highlight.content.image,
|
||||
);
|
||||
|
||||
const component = isTextHighlight ? (
|
||||
<Highlight
|
||||
isScrolledTo={isScrolledTo}
|
||||
position={highlight.position}
|
||||
comment={highlight.comment}
|
||||
/>
|
||||
) : (
|
||||
<AreaHighlight
|
||||
isScrolledTo={isScrolledTo}
|
||||
highlight={highlight}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
popupContent={<HighlightPopup {...highlight} />}
|
||||
onMouseOver={(popupContent) =>
|
||||
setTip(highlight, () => popupContent)
|
||||
}
|
||||
onMouseOut={hideTip}
|
||||
key={index}
|
||||
>
|
||||
{component}
|
||||
</Popup>
|
||||
);
|
||||
}}
|
||||
highlights={state || []}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PdfPreview);
|
||||
70
web/src/components/document-preview/ppt-preview.tsx
Normal file
70
web/src/components/document-preview/ppt-preview.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import message from '@/components/ui/message';
|
||||
import request from '@/utils/request';
|
||||
import classNames from 'classnames';
|
||||
import { init } from 'pptx-preview';
|
||||
import { useEffect, useRef } from 'react';
|
||||
interface PptPreviewerProps {
|
||||
className?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const PptPreviewer: React.FC<PptPreviewerProps> = ({
|
||||
className,
|
||||
url,
|
||||
}) => {
|
||||
// const url = useGetDocumentUrl();
|
||||
const wrapper = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const fetchDocument = async () => {
|
||||
const res = await request(url, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
onError: () => {
|
||||
message.error('Document parsing failed');
|
||||
console.error('Error loading document:', url);
|
||||
},
|
||||
});
|
||||
console.log(res);
|
||||
try {
|
||||
const arrayBuffer = await res.data.arrayBuffer();
|
||||
|
||||
if (containerRef.current) {
|
||||
let width = 500;
|
||||
let height = 900;
|
||||
if (containerRef.current) {
|
||||
width = containerRef.current.clientWidth - 50;
|
||||
height = containerRef.current.clientHeight - 50;
|
||||
}
|
||||
let pptxPrviewer = init(containerRef.current, {
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
pptxPrviewer.preview(arrayBuffer);
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('ppt parse failed');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
fetchDocument();
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classNames(
|
||||
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md ppt-previewer',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="overflow-auto p-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div ref={wrapper} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
56
web/src/components/document-preview/txt-preview.tsx
Normal file
56
web/src/components/document-preview/txt-preview.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { Spin } from '@/components/ui/spin';
|
||||
import request from '@/utils/request';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type TxtPreviewerProps = { className?: string; url: string };
|
||||
export const TxtPreviewer = ({ className, url }: TxtPreviewerProps) => {
|
||||
// const url = useGetDocumentUrl();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<string>('');
|
||||
const fetchTxt = async () => {
|
||||
setLoading(true);
|
||||
const res = await request(url, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
onError: (err: any) => {
|
||||
message.error('Failed to load file');
|
||||
console.error('Error loading file:', err);
|
||||
},
|
||||
});
|
||||
// blob to string
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(res.data);
|
||||
reader.onload = () => {
|
||||
setData(reader.result as string);
|
||||
setLoading(false);
|
||||
console.log('file loaded successfully', reader.result);
|
||||
};
|
||||
console.log('file data:', res);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
fetchTxt();
|
||||
} else {
|
||||
setLoading(false);
|
||||
setData('');
|
||||
}
|
||||
}, [url]);
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && <pre className="whitespace-pre-wrap p-2 ">{data}</pre>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
74
web/src/components/document-preview/video-preview.tsx
Normal file
74
web/src/components/document-preview/video-preview.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { Spin } from '@/components/ui/spin';
|
||||
import request from '@/utils/request';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
interface VideoPreviewerProps {
|
||||
className?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const VideoPreviewer: React.FC<VideoPreviewerProps> = ({
|
||||
className,
|
||||
url,
|
||||
}) => {
|
||||
// const url = useGetDocumentUrl();
|
||||
const [videoSrc, setVideoSrc] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
const fetchVideo = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
const res = await request(url, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
onError: () => {
|
||||
message.error('Failed to load video');
|
||||
setIsLoading(false);
|
||||
},
|
||||
});
|
||||
const objectUrl = URL.createObjectURL(res.data);
|
||||
setVideoSrc(objectUrl);
|
||||
setIsLoading(false);
|
||||
}, [url]);
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
fetchVideo();
|
||||
}
|
||||
}, [url, fetchVideo]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (videoSrc) {
|
||||
URL.revokeObjectURL(videoSrc);
|
||||
}
|
||||
};
|
||||
}, [videoSrc]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md video-previewer',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && videoSrc && (
|
||||
<div className="max-h-[80vh] overflow-auto p-2">
|
||||
<video
|
||||
src={videoSrc}
|
||||
controls
|
||||
className="w-full h-auto max-w-full object-contain"
|
||||
onLoadedData={() => URL.revokeObjectURL(videoSrc!)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user