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:
chanx
2025-11-25 19:13:00 +08:00
committed by GitHub
parent a793dd2ea8
commit 5d0981d046
38 changed files with 216 additions and 1222 deletions

View File

@ -1,5 +1,7 @@
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';
@ -22,6 +24,7 @@ export const DocPreviewer: React.FC<DocPreviewerProps> = ({
const res = await request(url, {
method: 'GET',
responseType: 'blob',
headers: { [Authorization]: getAuthorization() },
onError: () => {
message.error('Document parsing failed');
console.error('Error loading document:', url);

View File

@ -1,5 +1,6 @@
import { useFetchExcel } from '@/pages/document-viewer/hooks';
// import { useFetchExcel } from '@/pages/document-viewer/hooks';
import classNames from 'classnames';
import { useFetchExcel } from './hooks';
interface ExcelCsvPreviewerProps {
className?: string;

View File

@ -1,9 +1,67 @@
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, useRef, useState } from 'react';
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('');

View File

@ -1,5 +1,7 @@
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';
@ -22,6 +24,7 @@ export const ImagePreviewer: React.FC<ImagePreviewerProps> = ({
const res = await request(url, {
method: 'GET',
responseType: 'blob',
headers: { [Authorization]: getAuthorization() },
onError: () => {
message.error('Failed to load image');
setIsLoading(false);

View File

@ -4,7 +4,7 @@ import CSVFileViewer from './csv-preview';
import { DocPreviewer } from './doc-preview';
import { ExcelCsvPreviewer } from './excel-preview';
import { ImagePreviewer } from './image-preview';
import styles from './index.less';
import { Md } from './md';
import PdfPreviewer, { IProps } from './pdf-preview';
import { PptPreviewer } from './ppt-preview';
import { TxtPreviewer } from './txt-preview';
@ -25,7 +25,7 @@ const Preview = ({
return (
<>
{fileType === 'pdf' && highlights && setWidthAndHeight && (
<section className={styles.documentPreview}>
<section>
<PdfPreviewer
highlights={highlights}
setWidthAndHeight={setWidthAndHeight}
@ -38,7 +38,7 @@ const Preview = ({
<DocPreviewer className={className} url={url} />
</section>
)}
{['txt', 'md'].indexOf(fileType) > -1 && (
{['txt'].indexOf(fileType) > -1 && (
<section>
<TxtPreviewer className={className} url={url} />
</section>
@ -82,6 +82,11 @@ const Preview = ({
<CSVFileViewer className={className} url={url} />
</section>
)}
{['md'].indexOf(fileType) > -1 && (
<section>
<Md className={className} url={url} />
</section>
)}
</>
);
};

View File

@ -1,31 +1,39 @@
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';
import FileError from '../file-error';
interface MdProps {
filePath: string;
// filePath: string;
className?: string;
url: string;
}
const Md: React.FC<MdProps> = ({ filePath }) => {
export const Md: React.FC<MdProps> = ({ url, className }) => {
const [content, setContent] = useState<string>('');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setError(null);
fetch(filePath)
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));
}, [filePath]);
}, [url]);
if (error) return <FileError>{error}</FileError>;
return (
<div style={{ padding: 24, height: '100vh', overflow: 'scroll' }}>
<div
style={{ padding: 4, overflow: 'scroll' }}
className={cn(className, 'markdown-body h-[calc(100vh - 200px)]')}
>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</div>
);

View File

@ -10,13 +10,21 @@ import {
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;
highlights?: IHighlight[];
setWidthAndHeight?: (width: number, height: number) => void;
url: string;
className?: string;
}
const HighlightPopup = ({
comment,
@ -30,7 +38,12 @@ const HighlightPopup = ({
) : null;
// TODO: merge with DocumentPreviewer
const PdfPreview = ({ highlights: state, setWidthAndHeight, url }: IProps) => {
const PdfPreview = ({
highlights: state,
setWidthAndHeight,
url,
className,
}: IProps) => {
// const url = useGetDocumentUrl();
const ref = useRef<(highlight: IHighlight) => void>(() => {});
@ -39,17 +52,22 @@ const PdfPreview = ({ highlights: state, setWidthAndHeight, url }: IProps) => {
const resetHash = () => {};
useEffect(() => {
if (state.length > 0) {
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={`${styles.documentContainer} rounded-[10px] overflow-hidden ${className}`}
>
<PdfLoader
<Loader
url={url}
httpHeaders={httpHeaders}
beforeLoad={
<div className="absolute inset-0 flex items-center justify-center">
<Spin />
@ -63,7 +81,7 @@ const PdfPreview = ({ highlights: state, setWidthAndHeight, url }: IProps) => {
const viewport = page.getViewport({ scale: 1 });
const width = viewport.width;
const height = viewport.height;
setWidthAndHeight(width, height);
setWidthAndHeight?.(width, height);
});
return (
@ -115,11 +133,11 @@ const PdfPreview = ({ highlights: state, setWidthAndHeight, url }: IProps) => {
</Popup>
);
}}
highlights={state}
highlights={state || []}
/>
);
}}
</PdfLoader>
</Loader>
</div>
);
};

View File

@ -148,7 +148,7 @@ export const Images = [
];
// Without FileViewer
export const ExceptiveType = ['xlsx', 'xls', 'pdf', 'docx', ...Images];
export const ExceptiveType = ['xlsx', 'xls', 'pdf', 'docx', 'md', ...Images];
export const SupportedPreviewDocumentTypes = [...ExceptiveType];
//#endregion

View File

@ -1,14 +1,13 @@
import { Input } from '@/components/originui/input';
import { Button } from '@/components/ui/button';
import { SearchInput } from '@/components/ui/input';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Radio } from '@/components/ui/radio';
import { Segmented } from '@/components/ui/segmented';
import { useTranslate } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import { SearchOutlined } from '@ant-design/icons';
import { ListFilter, Plus } from 'lucide-react';
import { useState } from 'react';
import { ChunkTextMode } from '../../constant';
@ -61,46 +60,43 @@ export default ({
};
return (
<div className="flex pr-[25px]">
<div className="flex items-center gap-4 bg-bg-card text-muted-foreground w-fit h-[35px] rounded-md px-4 py-2">
{textSelectOptions.map((option) => (
<div
key={option.value}
className={cn('flex items-center cursor-pointer', {
'text-primary': option.value === textSelectValue,
})}
onClick={() => changeTextSelectValue(option.value)}
>
{option.label}
</div>
))}
</div>
<div className="ml-auto"></div>
<Input
className="bg-bg-card text-muted-foreground"
style={{ width: 200 }}
placeholder={t('search')}
icon={<SearchOutlined />}
onChange={handleInputChange}
value={searchString}
<Segmented
options={textSelectOptions}
value={textSelectValue}
onChange={changeTextSelectValue}
/>
<div className="w-[20px]"></div>
<Popover>
<PopoverTrigger asChild>
<Button className="bg-bg-card text-muted-foreground hover:bg-card">
<ListFilter />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[200px]">
{filterContent}
</PopoverContent>
</Popover>
<div className="w-[20px]"></div>
<Button
onClick={() => createChunk()}
className="bg-bg-card text-primary hover:bg-card"
>
<Plus size={44} />
</Button>
<div className="ml-auto"></div>
<div className="h-8 flex items-center gap-5">
<SearchInput
// style={{ width: 200 }}
placeholder={t('search')}
// icon={<SearchOutlined />}
onChange={handleInputChange}
value={searchString}
/>
<Popover>
<PopoverTrigger asChild>
<Button
variant={'ghost'}
// className="bg-bg-card text-text-secondary hover:bg-card"
>
<ListFilter />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[200px]">
{filterContent}
</PopoverContent>
</Popover>
<Button
variant={'ghost'}
onClick={() => createChunk()}
// className="bg-bg-card text-primary hover:bg-card"
>
<Plus size={44} />
</Button>
</div>
{/* <div className="w-[20px]"></div>
<div className="w-[20px]"></div> */}
</div>
);
};

View File

@ -1,21 +0,0 @@
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-[24px]">{name}</h2>
<div className="text-[#979AAB] pt-[5px]">
Size{sizeName} Uploaded Time{dateStr}
</div>
</div>
);
};

View File

@ -1,25 +0,0 @@
import { useFetchExcel } from '@/pages/document-viewer/hooks';
import classNames from 'classnames';
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>
);
};

View File

@ -1,55 +0,0 @@
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { api_host } from '@/utils/api';
import { useSize } from 'ahooks';
import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types';
import { useCallback, useEffect, useMemo, 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: CustomTextRenderer = useCallback(
(textItem) => {
return highlightPattern(textItem.str, searchText, textItem.pageNumber);
},
[searchText],
);
return textRenderer;
};
export const useGetDocumentUrl = () => {
const { documentId } = useGetKnowledgeSearchParams();
const url = useMemo(() => {
return `${api_host}/document/get/${documentId}`;
}, [documentId]);
return url;
};

View File

@ -1,74 +0,0 @@
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 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 = useCallback(async () => {
setIsLoading(true);
const res = await request(url, {
method: 'GET',
responseType: 'blob',
onError: () => {
message.error('Failed to load image');
setIsLoading(false);
},
});
const objectUrl = URL.createObjectURL(res.data);
setImageSrc(objectUrl);
setIsLoading(false);
}, [url]);
useEffect(() => {
if (url) {
fetchImage();
}
}, [url, fetchImage]);
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>
);
};

View File

@ -7,7 +7,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ChunkCard from './components/chunk-card';
import CreatingModal from './components/chunk-creating-modal';
import DocumentPreview from './components/document-preview';
import {
useChangeChunkTextMode,
useDeleteChunkByIds,
@ -18,8 +17,11 @@ import {
import ChunkResultBar from './components/chunk-result-bar';
import CheckboxSets from './components/chunk-result-bar/checkbox-sets';
import DocumentHeader from './components/document-preview/document-header';
// import DocumentHeader from './components/document-preview/document-header';
import DocumentPreview from '@/components/document-preview';
import DocumentHeader from '@/components/document-preview/document-header';
import { useGetDocumentUrl } from '@/components/document-preview/hooks';
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
@ -40,7 +42,6 @@ import {
useNavigatePage,
} from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { useGetDocumentUrl } from './components/document-preview/hooks';
import styles from './index.less';
const Chunk = () => {
@ -74,7 +75,7 @@ const Chunk = () => {
} = useUpdateChunk();
const { navigateToDataFile, getQueryString, navigateToDatasetList } =
useNavigatePage();
const fileUrl = useGetDocumentUrl();
const fileUrl = useGetDocumentUrl(false);
useEffect(() => {
setChunkList(data);
}, [data]);

View File

@ -1,114 +0,0 @@
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;

View File

@ -1,70 +0,0 @@
import message from '@/components/ui/message';
import { Spin } from '@/components/ui/spin';
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',
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>
);
};

View File

@ -1,60 +0,0 @@
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import api, { api_host } from '@/utils/api';
import { useSize } from 'ahooks';
import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useGetPipelineResultSearchParams } from '../../hooks';
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: CustomTextRenderer = useCallback(
(textItem) => {
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;
};

View File

@ -1,13 +0,0 @@
.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);
}
}

View File

@ -1,67 +0,0 @@
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 PdfPreviewer, { IProps } from './pdf-preview';
import { PptPreviewer } from './ppt-preview';
import { TxtPreviewer } from './txt-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', 'md'].indexOf(fileType) > -1 && (
<section>
<TxtPreviewer className={className} url={url} />
</section>
)}
{['visual'].indexOf(fileType) > -1 && (
<section>
<ImagePreviewer 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>
)}
</>
);
};
export default memo(Preview);

View File

@ -1,127 +0,0 @@
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 styles from './index.less';
export interface IProps {
highlights: IHighlight[];
setWidthAndHeight: (width: number, height: number) => void;
url: 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 }: IProps) => {
// const url = useGetDocumentUrl();
const ref = useRef<(highlight: IHighlight) => void>(() => {});
const error = useCatchDocumentError(url);
const resetHash = () => {};
useEffect(() => {
if (state.length > 0) {
ref?.current(state[0]);
}
}, [state]);
return (
<div
className={`${styles.documentContainer} rounded-[10px] overflow-hidden `}
>
<PdfLoader
url={url}
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}
/>
);
}}
</PdfLoader>
</div>
);
};
export default memo(PdfPreview);

View File

@ -1,70 +0,0 @@
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>
);
};

View File

@ -1,56 +0,0 @@
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>
);
};

View File

@ -1,7 +1,7 @@
import DocumentPreview from '@/components/document-preview';
import { useFetchNextChunkList } from '@/hooks/use-chunk-request';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import DocumentPreview from './components/document-preview';
import {
useFetchPipelineFileLogDetail,
useFetchPipelineResult,
@ -13,8 +13,9 @@ import {
useTimelineDataFlow,
} from './hooks';
import DocumentHeader from './components/document-preview/document-header';
import DocumentHeader from '@/components/document-preview/document-header';
import { useGetDocumentUrl } from '@/components/document-preview/hooks';
import { TimelineNode } from '@/components/originui/timeline';
import { PageHeader } from '@/components/page-header';
import Spotlight from '@/components/spotlight';
@ -32,7 +33,6 @@ import { AgentCategory } from '@/constants/agent';
import { Images } from '@/constants/common';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { useGetDocumentUrl } from './components/document-preview/hooks';
import TimelineDataFlow from './components/time-line';
import { TimelineNodeType } from './constant';
import styles from './index.less';
@ -76,13 +76,14 @@ const Chunk = () => {
const fileType = useMemo(() => {
if (isAgent) {
return Images.some((x) => x === documentExtension)
? 'visual'
? documentInfo?.name.split('.').pop() || 'visual'
: documentExtension;
}
switch (documentInfo?.type) {
case 'doc':
return documentInfo?.name.split('.').pop() || 'doc';
case 'visual':
return documentInfo?.name.split('.').pop() || 'visual';
case 'docx':
case 'txt':
case 'md':

View File

@ -1,282 +0,0 @@
// Copyright (c) 2017 PlanGrid, Inc.
.docxViewerWrapper {
overflow-y: scroll;
height: 100%;
width: 100%;
.box {
width: 100%;
height: 100%;
}
:global(.document-container) {
padding: 30px;
width: 700px;
background: rgba(255, 255, 255, 0.1);
margin: auto;
}
html,
bodyaddress,
blockquote,
body,
dd,
div,
dl,
dt,
fieldset,
form,
frame,
frameset,
h1,
h2,
h3,
h4,
h5,
h6,
noframes,
ol,
p,
ul,
center,
dir,
hr,
menu,
pre {
display: block;
unicode-bidi: embed;
}
li {
display: list-item;
list-style-type: disc;
}
head {
display: none;
}
table {
display: table;
}
img {
width: 100%;
}
tr {
display: table-row;
}
thead {
display: table-header-group;
}
tbody {
display: table-row-group;
}
tfoot {
display: table-footer-group;
}
col {
display: table-column;
}
colgroup {
display: table-column-group;
}
th {
display: table-cell;
}
td {
display: table-cell;
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
padding: 0.2em 0.5em;
}
caption {
display: table-caption;
}
th {
font-weight: bolder;
text-align: center;
}
caption {
text-align: center;
}
body {
margin: 8px;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
h2 {
font-size: 1.5em;
margin: 0.75em 0;
}
h3 {
font-size: 1.17em;
margin: 0.83em 0;
}
h4,
p,
blockquote,
ul,
fieldset,
form,
ol,
dl,
dir,
menu {
margin: 1.12em 0;
}
h5 {
font-size: 0.83em;
margin: 1.5em 0;
}
h6 {
font-size: 0.75em;
margin: 1.67em 0;
}
h1,
h2,
h3,
h4,
h5,
h6,
b,
strong {
font-weight: bolder;
}
blockquote {
margin-left: 40px;
margin-right: 40px;
}
i,
cite,
em,
var,
address {
font-style: italic;
}
pre,
tt,
code,
kbd,
samp {
font-family: monospace;
}
pre {
white-space: pre;
}
button,
textarea,
input,
select {
display: inline-block;
}
big {
font-size: 1.17em;
}
small,
sub,
sup {
font-size: 0.83em;
}
sub {
vertical-align: sub;
}
sup {
vertical-align: super;
}
table {
border-spacing: 2px;
}
thead,
tbody,
tfoot {
vertical-align: middle;
}
td,
th,
tr {
vertical-align: inherit;
}
s,
strike,
del {
text-decoration: line-through;
}
hr {
border: 1px inset;
}
ol,
ul,
dir,
menu,
dd {
margin-left: 40px;
}
ol {
list-style-type: decimal;
}
ol ul,
ol ul,
ul ol,
ul ol,
ul ul,
ul ul,
ol ol,
ol ol {
margin-top: 0;
margin-bottom: 0;
}
u,
ins {
text-decoration: underline;
}
br:before {
content: '\A';
white-space: pre-line;
}
center {
text-align: center;
}
:link,
:visited {
text-decoration: underline;
}
:focus {
outline: thin dotted invert;
}
/* Begin bidirectionality settings (do not change) */
BDO[DIR='ltr'] {
direction: ltr;
unicode-bidi: bidi-override;
}
BDO[DIR='rtl'] {
direction: rtl;
unicode-bidi: bidi-override;
}
*[DIR='ltr'] {
direction: ltr;
unicode-bidi: embed;
}
*[DIR='rtl'] {
direction: rtl;
unicode-bidi: embed;
}
@media print {
h1 {
page-break-before: always;
}
h1,
h2,
h3,
h4,
h5,
h6 {
page-break-after: avoid;
}
ul,
ol,
dl {
page-break-before: avoid;
}
}
}

View File

@ -1,25 +0,0 @@
import { Spin } from 'antd';
import FileError from '../file-error';
import { useFetchDocx } from '../hooks';
import styles from './index.less';
const Docx = ({ filePath }: { filePath: string }) => {
const { succeed, containerRef, error } = useFetchDocx(filePath);
return (
<>
{succeed ? (
<section className={styles.docxViewerWrapper}>
<div id="docx" ref={containerRef} className={styles.box}>
<Spin />
</div>
</section>
) : (
<FileError>{error}</FileError>
)}
</>
);
};
export default Docx;

View File

@ -1,19 +0,0 @@
import '@js-preview/excel/lib/index.css';
import FileError from '../file-error';
import { useFetchExcel } from '../hooks';
const Excel = ({ filePath }: { filePath: string }) => {
const { status, containerRef, error } = useFetchExcel(filePath);
return (
<div
id="excel"
ref={containerRef}
style={{ height: '100%', width: '100%' }}
>
{status || <FileError>{error}</FileError>}
</div>
);
};
export default Excel;

View File

@ -1,4 +0,0 @@
.errorWrapper {
width: 100%;
height: 100%;
}

View File

@ -1,18 +1,18 @@
import { Alert, Flex } from 'antd';
import { useTranslate } from '@/hooks/common-hooks';
import React from 'react';
import styles from './index.less';
const FileError = ({ children }: React.PropsWithChildren) => {
const { t } = useTranslate('fileManager');
return (
<Flex align="center" justify="center" className={styles.errorWrapper}>
<Alert
type="error"
message={<h2>{children || t('fileError')}</h2>}
></Alert>
</Flex>
<div className="flex items-center justify-center min-h-screen">
<div className="bg-state-error-5 border border-state-error rounded-lg p-4 shadow-sm">
<div className="flex ml-3">
<div className="text-white font-medium">
{children || t('fileError')}
</div>
</div>
</div>
</div>
);
};

View File

@ -1,16 +1,22 @@
import { Images } from '@/constants/common';
import { api_host } from '@/utils/api';
import { Flex } from 'antd';
// import { Flex } from 'antd';
import { useParams, useSearchParams } from 'umi';
import Docx from './docx';
import Excel from './excel';
import Image from './image';
import Md from './md';
import Pdf from './pdf';
import Text from './text';
// import Docx from './docx';
// import Excel from './excel';
// import Image from './image';
// import Md from './md';
// import Pdf from './pdf';
// import Text from './text';
import { DocPreviewer } from '@/components/document-preview/doc-preview';
import { ExcelCsvPreviewer } from '@/components/document-preview/excel-preview';
import { ImagePreviewer } from '@/components/document-preview/image-preview';
import Md from '@/components/document-preview/md';
import PdfPreview from '@/components/document-preview/pdf-preview';
import { TxtPreviewer } from '@/components/document-preview/txt-preview';
import { previewHtmlFile } from '@/utils/file-util';
import styles from './index.less';
// import styles from './index.less';
// TODO: The interface returns an incorrect content-type for the SVG.
@ -20,6 +26,7 @@ const DocumentViewer = () => {
const ext = currentQueryParameters.get('ext');
const prefix = currentQueryParameters.get('prefix');
const api = `${api_host}/${prefix || 'file'}/get/${documentId}`;
// request.head
if (ext === 'html' && documentId) {
previewHtmlFile(documentId);
@ -27,19 +34,24 @@ const DocumentViewer = () => {
}
return (
<section className={styles.viewerWrapper}>
<section className="w-full h-full">
{Images.includes(ext!) && (
<Flex className={styles.image} align="center" justify="center">
<Image src={api} preview={false}></Image>
</Flex>
<div className="flex w-full h-full items-center justify-center">
{/* <Image src={api} preview={false}></Image> */}
<ImagePreviewer className="w-full !h-dvh p-5" url={api} />
</div>
)}
{ext === 'md' && <Md filePath={api}></Md>}
{ext === 'txt' && <Text filePath={api}></Text>}
{ext === 'md' && <Md url={api} className="!h-dvh p-5"></Md>}
{ext === 'txt' && <TxtPreviewer url={api}></TxtPreviewer>}
{ext === 'pdf' && <Pdf url={api}></Pdf>}
{(ext === 'xlsx' || ext === 'xls') && <Excel filePath={api}></Excel>}
{ext === 'pdf' && (
<PdfPreview url={api} className="!h-dvh p-5"></PdfPreview>
)}
{(ext === 'xlsx' || ext === 'xls') && (
<ExcelCsvPreviewer url={api}></ExcelCsvPreviewer>
)}
{ext === 'docx' && <Docx filePath={api}></Docx>}
{ext === 'docx' && <DocPreviewer url={api}></DocPreviewer>}
</section>
);
};

View File

@ -1,32 +0,0 @@
import React, { useEffect, useState } from 'react';
import FileError from '../file-error';
interface TxtProps {
filePath: string;
}
const Md: React.FC<TxtProps> = ({ filePath }) => {
const [content, setContent] = useState<string>('');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setError(null);
fetch(filePath)
.then((res) => {
if (!res.ok) throw new Error('Failed to fetch text file');
return res.text();
})
.then((text) => setContent(text))
.catch((err) => setError(err.message));
}, [filePath]);
if (error) return <FileError>{error}</FileError>;
return (
<div style={{ padding: 24, height: '100vh', overflow: 'scroll' }}>
{content}
</div>
);
};
export default Md;

View File

@ -1,3 +1,4 @@
import DocumentPreview from '@/components/document-preview';
import { FileIcon } from '@/components/icon-font';
import { Modal } from '@/components/ui/modal/modal';
import {
@ -7,7 +8,6 @@ import {
import { IModalProps } from '@/interfaces/common';
import { IReferenceChunk } from '@/interfaces/database/chat';
import { IChunk } from '@/interfaces/database/knowledge';
import DocumentPreview from '@/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview';
import { useEffect, useState } from 'react';
interface IProps extends IModalProps<any> {

View File

@ -45,21 +45,23 @@ export const useListDataSource = () => {
const updatedDataSourceTemplates = useMemo(() => {
const categorizedData = categorizeDataBySource(list || []);
let sourcelist: Array<IDataSorceInfo & { list: Array<IDataSourceBase> }> =
let sourceList: Array<IDataSorceInfo & { list: Array<IDataSourceBase> }> =
[];
Object.keys(categorizedData).forEach((key: string) => {
const k = key as DataSourceKey;
sourcelist.push({
id: k,
name: DataSourceInfo[k].name,
description: DataSourceInfo[k].description,
icon: DataSourceInfo[k].icon,
list: categorizedData[k] || [],
});
if (DataSourceInfo[k]) {
sourceList.push({
id: k,
name: DataSourceInfo[k].name,
description: DataSourceInfo[k].description,
icon: DataSourceInfo[k].icon,
list: categorizedData[k] || [],
});
}
});
console.log('🚀 ~ useListDataSource ~ sourcelist:', sourcelist);
return sourcelist;
console.log('🚀 ~ useListDataSource ~ sourceList:', sourceList);
return sourceList;
}, [list]);
return { list, categorizedList: updatedDataSourceTemplates, isFetching };