feat(chunk): add pdf preview and locate functionality to chunk results
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>RAG Empowerment System</title>
|
||||
<title>TERES AI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -11,14 +11,13 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@teres/iframe-bridge": "workspace:*",
|
||||
"penpal": "^6.2.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@mui/material": "^7.3.4",
|
||||
"@mui/x-data-grid": "^8.14.0",
|
||||
"@mui/x-date-pickers": "^8.14.0",
|
||||
"@teres/iframe-bridge": "workspace:*",
|
||||
"@xyflow/react": "^12.8.6",
|
||||
"ahooks": "^3.9.5",
|
||||
"axios": "^1.12.2",
|
||||
@@ -29,6 +28,8 @@
|
||||
"jsencrypt": "^3.5.4",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.9.2",
|
||||
"pdfjs-dist": "^5.4.394",
|
||||
"penpal": "^6.2.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.64.0",
|
||||
|
||||
124
pnpm-lock.yaml
generated
124
pnpm-lock.yaml
generated
@@ -59,6 +59,9 @@ importers:
|
||||
loglevel:
|
||||
specifier: ^1.9.2
|
||||
version: 1.9.2
|
||||
pdfjs-dist:
|
||||
specifier: ^5.4.394
|
||||
version: 5.4.394
|
||||
penpal:
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.2
|
||||
@@ -2225,6 +2228,75 @@ packages:
|
||||
peerDependencies:
|
||||
workerize-loader: '*'
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.82':
|
||||
resolution: {integrity: sha512-bvZhN0iI54ouaQOrgJV96H2q7J3ZoufnHf4E1fUaERwW29Rz4rgicohnAg4venwBJZYjGl5Yl3CGmlAl1LZowQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.82':
|
||||
resolution: {integrity: sha512-InuBHKCyuFqhNwNr4gpqazo5Xp6ltKflqOLiROn4hqAS8u21xAHyYCJRgHwd+a5NKmutFTaRWeUIT/vxWbU/iw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.82':
|
||||
resolution: {integrity: sha512-aQGV5Ynn96onSXcuvYb2y7TRXD/t4CL2EGmnGqvLyeJX1JLSNisKQlWN/1bPDDXymZYSdUqbXehj5qzBlOx+RQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.82':
|
||||
resolution: {integrity: sha512-YIUpmHWeHGGRhWitT1KJkgj/JPXPfc9ox8oUoyaGPxolLGPp5AxJkq8wIg8CdFGtutget968dtwmx71m8o3h5g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.82':
|
||||
resolution: {integrity: sha512-AwLzwLBgmvk7kWeUgItOUor/QyG31xqtD26w1tLpf4yE0hiXTGp23yc669aawjB6FzgIkjh1NKaNS52B7/qEBQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.82':
|
||||
resolution: {integrity: sha512-moZWuqepAwWBffdF4JDadt8TgBD02iMhG6I1FHZf8xO20AsIp9rB+p0B8Zma2h2vAF/YMjeFCDmW5un6+zZz9g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.82':
|
||||
resolution: {integrity: sha512-w9++2df2kG9eC9LWYIHIlMLuhIrKGQYfUxs97CwgxYjITeFakIRazI9LYWgVzEc98QZ9x9GQvlicFsrROV59MQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.82':
|
||||
resolution: {integrity: sha512-lZulOPwrRi6hEg/17CaqdwWEUfOlIJuhXxincx1aVzsVOCmyHf+xFq4i6liJl1P+x2v6Iz2Z/H5zHvXJCC7Bwg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.82':
|
||||
resolution: {integrity: sha512-Be9Wf5RTv1w6GXlTph55K3PH3vsAh1Ax4T1FQY1UYM0QfD0yrwGdnJ8/fhqw7dEgMjd59zIbjJQC8C3msbGn5g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.82':
|
||||
resolution: {integrity: sha512-LN/i8VrvxTDmEEK1c10z2cdOTkWT76LlTGtyZe5Kr1sqoSomKeExAjbilnu1+oee5lZUgS5yfZ2LNlVhCeARuw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas@0.1.82':
|
||||
resolution: {integrity: sha512-FGjyUBoF0sl1EenSiE4UV2WYu76q6F9GSYedq5EiOCOyGYoQ/Owulcv6rd7v/tWOpljDDtefXXIaOCJrVKem4w==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@napi-rs/nice-android-arm-eabi@1.1.1':
|
||||
resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -9139,6 +9211,10 @@ packages:
|
||||
worker-loader:
|
||||
optional: true
|
||||
|
||||
pdfjs-dist@5.4.394:
|
||||
resolution: {integrity: sha512-9ariAYGqUJzx+V/1W4jHyiyCep6IZALmDzoaTLZ6VNu8q9LWi1/ukhzHgE2Xsx96AZi0mbZuK4/ttIbqSbLypg==}
|
||||
engines: {node: '>=20.16.0 || >=22.3.0'}
|
||||
|
||||
penpal@6.2.2:
|
||||
resolution: {integrity: sha512-RQD7hTx14/LY7QoS3tQYO3/fzVtwvZI+JeS5udgsu7FPaEDjlvfK9HBcme9/ipzSPKnrxSgacI9PI7154W62YQ==}
|
||||
|
||||
@@ -14104,6 +14180,50 @@ snapshots:
|
||||
dependencies:
|
||||
workerize-loader: 2.0.2(webpack@5.102.1(@swc/core@1.15.1)(esbuild@0.25.10))
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.82':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.82':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.82':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.82':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.82':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.82':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.82':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.82':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.82':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.82':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas@0.1.82':
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 0.1.82
|
||||
'@napi-rs/canvas-darwin-arm64': 0.1.82
|
||||
'@napi-rs/canvas-darwin-x64': 0.1.82
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.82
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.82
|
||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.82
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.82
|
||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.82
|
||||
'@napi-rs/canvas-linux-x64-musl': 0.1.82
|
||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.82
|
||||
optional: true
|
||||
|
||||
'@napi-rs/nice-android-arm-eabi@1.1.1':
|
||||
optional: true
|
||||
|
||||
@@ -22626,6 +22746,10 @@ snapshots:
|
||||
dommatrix: 1.0.3
|
||||
web-streams-polyfill: 3.3.3
|
||||
|
||||
pdfjs-dist@5.4.394:
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.82
|
||||
|
||||
penpal@6.2.2: {}
|
||||
|
||||
performance-now@2.1.0: {}
|
||||
|
||||
@@ -41,7 +41,7 @@ function MaterialUIApp() {
|
||||
}
|
||||
|
||||
function App() {
|
||||
useTitle('RAG Empowerment System');
|
||||
useTitle('TERES AI');
|
||||
|
||||
return (
|
||||
<MaterialUIApp />
|
||||
|
||||
@@ -61,7 +61,7 @@ const Header = () => {
|
||||
borderBottom: '1px solid #E5E5E5',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
{/* <Box
|
||||
sx={{
|
||||
fontSize: '1.2rem',
|
||||
fontWeight: 'bold',
|
||||
@@ -69,7 +69,7 @@ const Header = () => {
|
||||
}}
|
||||
>
|
||||
RAG Empowerment System
|
||||
</Box>
|
||||
</Box> */}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -84,7 +84,7 @@ const Sidebar = () => {
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
T-Systems Enterprise
|
||||
TERES AI
|
||||
</Typography>
|
||||
|
||||
<List>
|
||||
|
||||
@@ -59,10 +59,11 @@ interface ChunkListResultProps {
|
||||
onPageChange: (page: number) => void;
|
||||
onRefresh?: () => void;
|
||||
docName?: string;
|
||||
onLocate?: (chunk: IChunk) => void;
|
||||
}
|
||||
|
||||
function ChunkListResult(props: ChunkListResultProps) {
|
||||
const { doc_id, chunks, total, loading, page, pageSize, onPageChange, onRefresh } = props;
|
||||
const { doc_id, chunks, total, loading, page, pageSize, onPageChange, onRefresh, onLocate } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 选择状态
|
||||
@@ -149,9 +150,12 @@ function ChunkListResult(props: ChunkListResultProps) {
|
||||
}, []);
|
||||
|
||||
// 图片点击放大
|
||||
const handleImageClick = useCallback((imageUrl: string) => {
|
||||
const handleImageClick = useCallback((imageUrl: string, chunk?: IChunk) => {
|
||||
setPreviewImageUrl(imageUrl);
|
||||
setImagePreviewOpen(true);
|
||||
if (chunk) {
|
||||
onLocate?.(chunk);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 编辑chunk
|
||||
@@ -351,7 +355,7 @@ function ChunkListResult(props: ChunkListResultProps) {
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{chunks.map((chunk, index) => (
|
||||
<Grid size={6} key={chunk.chunk_id}>
|
||||
<Grid size={12} key={chunk.chunk_id}>
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={{
|
||||
@@ -383,6 +387,12 @@ function ChunkListResult(props: ChunkListResultProps) {
|
||||
color={chunk.available_int === 1 ? 'success' : 'default'}
|
||||
variant={chunk.available_int === 1 ? 'filled' : 'outlined'}
|
||||
/>
|
||||
{/* 定位到文档位置 */}
|
||||
<Tooltip title={'定位'}>
|
||||
<IconButton size="small" onClick={() => onLocate?.(chunk)}>
|
||||
<ZoomInIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* 主要内容区域 - 左右布局 */}
|
||||
@@ -415,7 +425,7 @@ function ChunkListResult(props: ChunkListResultProps) {
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={() => handleImageClick(`${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`)}
|
||||
onClick={() => handleImageClick(`${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`, chunk)}
|
||||
onMouseEnter={(e) => handleImageHover(e, `${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`)}
|
||||
onMouseLeave={handleImageHoverClose}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
// 使用 pdf.js 在左侧容器中渲染 PDF 页面,支持自由滑动与页码定位
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
// 使用 Vite 的资源 URL 解析,将 pdf.js 的模块 worker 转成可访问 URL
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import pdfjsWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
|
||||
|
||||
// 采用模块 Worker,避免经典 worker 的 MIME/路径问题
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
pdfjsLib.GlobalWorkerOptions.workerPort = new Worker(pdfjsWorkerUrl, { type: 'module' });
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -11,24 +22,31 @@ import {
|
||||
Card,
|
||||
CardContent
|
||||
} from "@mui/material";
|
||||
import { Search as SearchIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
|
||||
import { Search as SearchIcon, Download as DownloadIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useChunkList } from '@/hooks/chunk-hooks';
|
||||
import ChunkListResult from './components/ChunkListResult';
|
||||
import knowledgeService from '@/services/knowledge_service';
|
||||
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||
import type { IKnowledge, IKnowledgeFile, IChunk } from '@/interfaces/database/knowledge';
|
||||
import KnowledgeBreadcrumbs from '@/pages/knowledge/components/KnowledgeBreadcrumbs';
|
||||
|
||||
function ChunkParsedResult() {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const kb_id = searchParams.get('kb_id');
|
||||
const doc_id = searchParams.get('doc_id');
|
||||
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<IKnowledge | null>(null);
|
||||
const [document, setDocument] = useState<IKnowledgeFile | null>(null);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [documentFile, setDocumentFile] = useState<Blob | null>(null);
|
||||
const [fileUrl, setFileUrl] = useState<string>('');
|
||||
const [fileLoading, setFileLoading] = useState(false);
|
||||
const [previewOverrideUrl, setPreviewOverrideUrl] = useState<string>('');
|
||||
const [focusPage, setFocusPage] = useState<number | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const pdfContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [pdfRendered, setPdfRendered] = useState<boolean>(false);
|
||||
|
||||
// 使用chunk列表hook
|
||||
const {
|
||||
@@ -68,6 +86,9 @@ function ChunkParsedResult() {
|
||||
setDocument(docArr[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// 自动加载文档文件用于预览
|
||||
await loadDocumentFile();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch data:', error);
|
||||
}
|
||||
@@ -83,11 +104,223 @@ function ChunkParsedResult() {
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// 处理查看文件
|
||||
const handleViewFile = () => {
|
||||
if (doc_id && kb_id) {
|
||||
// 跳转到文件预览页面
|
||||
navigate(`/chunk/document-preview/${kb_id}/${doc_id}`);
|
||||
// 异步加载文档文件
|
||||
const loadDocumentFile = async () => {
|
||||
if (!doc_id || fileLoading) return;
|
||||
|
||||
try {
|
||||
setFileLoading(true);
|
||||
|
||||
// 取消之前的请求
|
||||
if (abortControllerRef.current) {
|
||||
// abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
// 创建新的AbortController
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
const fileResponse = await knowledgeService.getDocumentFile({
|
||||
doc_id
|
||||
}, {
|
||||
signal: abortControllerRef.current.signal
|
||||
});
|
||||
|
||||
if (fileResponse.data instanceof Blob) {
|
||||
setDocumentFile(fileResponse.data);
|
||||
const url = URL.createObjectURL(fileResponse.data);
|
||||
setFileUrl(url);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err?.name !== 'AbortError' && err?.name !== 'CanceledError') {
|
||||
console.error('获取文档文件失败:', err);
|
||||
}
|
||||
} finally {
|
||||
setFileLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 清理fileUrl
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (fileUrl) {
|
||||
URL.revokeObjectURL(fileUrl);
|
||||
}
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, [fileUrl]);
|
||||
|
||||
// 使用 pdf.js 渲染 PDF 到左侧容器,替代 iframe,保证滚动与定位可用
|
||||
useEffect(() => {
|
||||
const renderPdf = async () => {
|
||||
if (!documentFile || documentFile.type !== 'application/pdf') {
|
||||
// 非 PDF 或无文件,不渲染 PDF
|
||||
setPdfRendered(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const arrayBuffer = await documentFile.arrayBuffer();
|
||||
// 使用模块 Worker 的端口,避免设置 workerSrc 导致的路径/MIME 问题
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const task = pdfjsLib.getDocument({ data: arrayBuffer });
|
||||
const pdfDoc = await task.promise;
|
||||
|
||||
const container = pdfContainerRef.current;
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
// 根据容器宽度自适应缩放
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const containerWidth = Math.max(1, Math.floor(containerRect.width || container.clientWidth || 800));
|
||||
|
||||
for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
const scale = containerWidth / viewport.width;
|
||||
const scaledViewport = page.getViewport({ scale });
|
||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||
|
||||
const canvas = window.document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.style.display = 'block';
|
||||
canvas.style.marginBottom = '12px';
|
||||
canvas.style.width = `${Math.floor(scaledViewport.width)}px`;
|
||||
canvas.style.height = `${Math.floor(scaledViewport.height)}px`;
|
||||
canvas.width = Math.floor(scaledViewport.width * dpr);
|
||||
canvas.height = Math.floor(scaledViewport.height * dpr);
|
||||
|
||||
if (ctx) {
|
||||
// 提升清晰度:将绘制与 CSS 尺寸解耦并使用设备像素比
|
||||
// @ts-ignore
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
// @ts-ignore
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
}
|
||||
// @ts-ignore
|
||||
await page.render({
|
||||
canvasContext: ctx as CanvasRenderingContext2D,
|
||||
viewport: scaledViewport,
|
||||
transform: dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : undefined,
|
||||
}).promise;
|
||||
|
||||
const pageWrapper = window.document.createElement('div');
|
||||
pageWrapper.setAttribute('data-page-index', String(pageNum));
|
||||
pageWrapper.appendChild(canvas);
|
||||
container.appendChild(pageWrapper);
|
||||
}
|
||||
|
||||
setPdfRendered(true);
|
||||
} catch (err) {
|
||||
console.error('渲染 PDF 过程中发生错误: ', err);
|
||||
setPdfRendered(false);
|
||||
}
|
||||
};
|
||||
|
||||
renderPdf();
|
||||
|
||||
// 清理渲染内容,避免内存泄露
|
||||
return () => {
|
||||
const container = pdfContainerRef.current;
|
||||
if (container) container.innerHTML = '';
|
||||
};
|
||||
// 仅在文件或其类型变化时重新渲染
|
||||
}, [documentFile]);
|
||||
|
||||
// 当 focusPage 变化时,滚动到对应页(仅对 PDF 有效)
|
||||
useEffect(() => {
|
||||
if (documentFile?.type !== 'application/pdf' || !pdfRendered || !focusPage) return;
|
||||
const container = pdfContainerRef.current;
|
||||
if (!container) return;
|
||||
const children = Array.from(container.children);
|
||||
const target = children.find((el) => (el as HTMLElement).dataset?.pageIndex === String(focusPage));
|
||||
if (target) {
|
||||
(target as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, [focusPage, pdfRendered, documentFile]);
|
||||
|
||||
// 下载文件
|
||||
const handleDownload = () => {
|
||||
if (fileUrl && window.document) {
|
||||
const link = window.document.createElement('a');
|
||||
link.href = fileUrl;
|
||||
link.download = document?.name || 'document';
|
||||
window.document.body.appendChild(link);
|
||||
link.click();
|
||||
window.document.body.removeChild(link);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理定位:点击某个chunk后联动预览
|
||||
const handleLocate = (chunk: IChunk) => {
|
||||
// 使用 positions 的第一个元素的第一个值作为页码(若存在)
|
||||
const page = Array.isArray(chunk.positions) && chunk.positions.length > 0 && Array.isArray(chunk.positions[0])
|
||||
? Number(chunk.positions[0][0])
|
||||
: null;
|
||||
|
||||
// 保持左侧预览为文档视图,定位仅改变 PDF 页码,不替换为切片图片
|
||||
if (documentFile?.type === 'application/pdf') {
|
||||
setFocusPage(page && !Number.isNaN(page) ? page : null);
|
||||
setPreviewOverrideUrl('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 非 PDF 文档暂不处理页码定位,保持可滚动预览
|
||||
setPreviewOverrideUrl('');
|
||||
setFocusPage(null);
|
||||
};
|
||||
|
||||
// 渲染左侧预览
|
||||
const renderPreview = () => {
|
||||
// 如果有覆盖的图片URL,直接显示图片
|
||||
if (previewOverrideUrl) {
|
||||
return (
|
||||
<Box
|
||||
component="img"
|
||||
src={previewOverrideUrl}
|
||||
alt={document?.name || t('chunkPage.documentPreview')}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'contain', borderRadius: 1 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!documentFile || !fileUrl) return null;
|
||||
|
||||
const fileType = documentFile.type;
|
||||
if (fileType.startsWith('image/')) {
|
||||
return (
|
||||
<Box
|
||||
component="img"
|
||||
src={fileUrl}
|
||||
alt={document?.name || t('chunkPage.documentPreview')}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'contain', borderRadius: 1 }}
|
||||
/>
|
||||
);
|
||||
} else if (fileType === 'application/pdf') {
|
||||
// 使用 pdf.js 渲染的容器,支持滚动与页码定位
|
||||
return (
|
||||
<Box
|
||||
ref={pdfContainerRef}
|
||||
sx={{ width: '100%', height: '100%', overflow: 'auto', borderRadius: 1 }}
|
||||
/>
|
||||
);
|
||||
} else if (fileType.startsWith('text/')) {
|
||||
return (
|
||||
<Box
|
||||
component="iframe"
|
||||
src={fileUrl}
|
||||
sx={{ width: '100%', height: '100%', border: '1px solid', borderColor: 'grey.300', borderRadius: 1 }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
{t('chunkPage.fileTypeNotSupportedPreview')}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,17 +354,18 @@ function ChunkParsedResult() {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{/* 页面标题和文档信息 */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||
{/* 顶部信息与下载 */}
|
||||
<Paper sx={{ p: 3, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
{document?.name || t('chunkPage.documentDetail')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('chunkPage.documentChunkResult')}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{t('chunkPage.viewDocument')} "{document?.name}" {t('chunkPage.allChunkData')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h4" color="primary">
|
||||
@@ -142,19 +376,34 @@ function ChunkParsedResult() {
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<VisibilityIcon />}
|
||||
onClick={handleViewFile}
|
||||
sx={{ ml: 2 }}
|
||||
>
|
||||
{t('chunkPage.viewFile')}
|
||||
{fileUrl && (
|
||||
<Button variant="contained" startIcon={<DownloadIcon />} onClick={handleDownload}>
|
||||
{t('chunkPage.downloadFile')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
{/* 左右布局:左预览,右切片结果 */}
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
{/* 左侧文档预览 */}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Paper sx={{ p: 2, height: '80vh' }}>
|
||||
{/* 文件加载状态 */}
|
||||
{fileLoading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px' }}>
|
||||
<Alert severity="info">{t('chunkPage.loadingFile')}</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{renderPreview()}
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* 右侧切片结果与搜索 */}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Paper sx={{ p: 2, height: '80vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder={t('chunkPage.searchChunkPlaceholder')}
|
||||
@@ -168,9 +417,7 @@ function ChunkParsedResult() {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* Chunk列表结果 */}
|
||||
<Box sx={{ flex: 1, overflow: 'auto', pt: 2 }}>
|
||||
<ChunkListResult
|
||||
doc_id={doc_id}
|
||||
chunks={chunks}
|
||||
@@ -182,8 +429,13 @@ function ChunkParsedResult() {
|
||||
onPageChange={setCurrentPage}
|
||||
onRefresh={refresh}
|
||||
docName={document?.name}
|
||||
onLocate={handleLocate}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ const Login = () => {
|
||||
<Card sx={{ width: '100%' }}>
|
||||
<CardContent sx={{ p: 4 }}>
|
||||
<Typography variant="subtitle1" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||
T-Systems Enterprise RAG Empowerment System
|
||||
TERES AI
|
||||
</Typography>
|
||||
|
||||
{/* 标签页 */}
|
||||
|
||||
Reference in New Issue
Block a user