feat(chunk): add pdf preview and locate functionality to chunk results
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>RAG Empowerment System</title>
|
<title>TERES AI</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -11,14 +11,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@teres/iframe-bridge": "workspace:*",
|
|
||||||
"penpal": "^6.2.1",
|
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^7.3.4",
|
"@mui/icons-material": "^7.3.4",
|
||||||
"@mui/material": "^7.3.4",
|
"@mui/material": "^7.3.4",
|
||||||
"@mui/x-data-grid": "^8.14.0",
|
"@mui/x-data-grid": "^8.14.0",
|
||||||
"@mui/x-date-pickers": "^8.14.0",
|
"@mui/x-date-pickers": "^8.14.0",
|
||||||
|
"@teres/iframe-bridge": "workspace:*",
|
||||||
"@xyflow/react": "^12.8.6",
|
"@xyflow/react": "^12.8.6",
|
||||||
"ahooks": "^3.9.5",
|
"ahooks": "^3.9.5",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
@@ -29,6 +28,8 @@
|
|||||||
"jsencrypt": "^3.5.4",
|
"jsencrypt": "^3.5.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"loglevel": "^1.9.2",
|
"loglevel": "^1.9.2",
|
||||||
|
"pdfjs-dist": "^5.4.394",
|
||||||
|
"penpal": "^6.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.64.0",
|
"react-hook-form": "^7.64.0",
|
||||||
|
|||||||
124
pnpm-lock.yaml
generated
124
pnpm-lock.yaml
generated
@@ -59,6 +59,9 @@ importers:
|
|||||||
loglevel:
|
loglevel:
|
||||||
specifier: ^1.9.2
|
specifier: ^1.9.2
|
||||||
version: 1.9.2
|
version: 1.9.2
|
||||||
|
pdfjs-dist:
|
||||||
|
specifier: ^5.4.394
|
||||||
|
version: 5.4.394
|
||||||
penpal:
|
penpal:
|
||||||
specifier: ^6.2.1
|
specifier: ^6.2.1
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
@@ -2225,6 +2228,75 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
workerize-loader: '*'
|
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':
|
'@napi-rs/nice-android-arm-eabi@1.1.1':
|
||||||
resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==}
|
resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -9139,6 +9211,10 @@ packages:
|
|||||||
worker-loader:
|
worker-loader:
|
||||||
optional: true
|
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:
|
penpal@6.2.2:
|
||||||
resolution: {integrity: sha512-RQD7hTx14/LY7QoS3tQYO3/fzVtwvZI+JeS5udgsu7FPaEDjlvfK9HBcme9/ipzSPKnrxSgacI9PI7154W62YQ==}
|
resolution: {integrity: sha512-RQD7hTx14/LY7QoS3tQYO3/fzVtwvZI+JeS5udgsu7FPaEDjlvfK9HBcme9/ipzSPKnrxSgacI9PI7154W62YQ==}
|
||||||
|
|
||||||
@@ -14104,6 +14180,50 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
workerize-loader: 2.0.2(webpack@5.102.1(@swc/core@1.15.1)(esbuild@0.25.10))
|
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':
|
'@napi-rs/nice-android-arm-eabi@1.1.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -22626,6 +22746,10 @@ snapshots:
|
|||||||
dommatrix: 1.0.3
|
dommatrix: 1.0.3
|
||||||
web-streams-polyfill: 3.3.3
|
web-streams-polyfill: 3.3.3
|
||||||
|
|
||||||
|
pdfjs-dist@5.4.394:
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas': 0.1.82
|
||||||
|
|
||||||
penpal@6.2.2: {}
|
penpal@6.2.2: {}
|
||||||
|
|
||||||
performance-now@2.1.0: {}
|
performance-now@2.1.0: {}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function MaterialUIApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useTitle('RAG Empowerment System');
|
useTitle('TERES AI');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MaterialUIApp />
|
<MaterialUIApp />
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const Header = () => {
|
|||||||
borderBottom: '1px solid #E5E5E5',
|
borderBottom: '1px solid #E5E5E5',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
{/* <Box
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: '1.2rem',
|
fontSize: '1.2rem',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
@@ -69,7 +69,7 @@ const Header = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
RAG Empowerment System
|
RAG Empowerment System
|
||||||
</Box>
|
</Box> */}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const Sidebar = () => {
|
|||||||
letterSpacing: '0.5px',
|
letterSpacing: '0.5px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
T-Systems Enterprise
|
TERES AI
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
|
|||||||
@@ -59,10 +59,11 @@ interface ChunkListResultProps {
|
|||||||
onPageChange: (page: number) => void;
|
onPageChange: (page: number) => void;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
docName?: string;
|
docName?: string;
|
||||||
|
onLocate?: (chunk: IChunk) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChunkListResult(props: ChunkListResultProps) {
|
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();
|
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);
|
setPreviewImageUrl(imageUrl);
|
||||||
setImagePreviewOpen(true);
|
setImagePreviewOpen(true);
|
||||||
|
if (chunk) {
|
||||||
|
onLocate?.(chunk);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 编辑chunk
|
// 编辑chunk
|
||||||
@@ -351,7 +355,7 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
|
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{chunks.map((chunk, index) => (
|
{chunks.map((chunk, index) => (
|
||||||
<Grid size={6} key={chunk.chunk_id}>
|
<Grid size={12} key={chunk.chunk_id}>
|
||||||
<Card
|
<Card
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -383,6 +387,12 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
color={chunk.available_int === 1 ? 'success' : 'default'}
|
color={chunk.available_int === 1 ? 'success' : 'default'}
|
||||||
variant={chunk.available_int === 1 ? 'filled' : 'outlined'}
|
variant={chunk.available_int === 1 ? 'filled' : 'outlined'}
|
||||||
/>
|
/>
|
||||||
|
{/* 定位到文档位置 */}
|
||||||
|
<Tooltip title={'定位'}>
|
||||||
|
<IconButton size="small" onClick={() => onLocate?.(chunk)}>
|
||||||
|
<ZoomInIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</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}`)}
|
onMouseEnter={(e) => handleImageHover(e, `${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`)}
|
||||||
onMouseLeave={handleImageHoverClose}
|
onMouseLeave={handleImageHoverClose}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
// 使用 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 {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -11,24 +22,31 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardContent
|
CardContent
|
||||||
} from "@mui/material";
|
} 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 { useTranslation } from 'react-i18next';
|
||||||
import { useChunkList } from '@/hooks/chunk-hooks';
|
import { useChunkList } from '@/hooks/chunk-hooks';
|
||||||
import ChunkListResult from './components/ChunkListResult';
|
import ChunkListResult from './components/ChunkListResult';
|
||||||
import knowledgeService from '@/services/knowledge_service';
|
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';
|
import KnowledgeBreadcrumbs from '@/pages/knowledge/components/KnowledgeBreadcrumbs';
|
||||||
|
|
||||||
function ChunkParsedResult() {
|
function ChunkParsedResult() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
|
||||||
const kb_id = searchParams.get('kb_id');
|
const kb_id = searchParams.get('kb_id');
|
||||||
const doc_id = searchParams.get('doc_id');
|
const doc_id = searchParams.get('doc_id');
|
||||||
|
|
||||||
const [knowledgeBase, setKnowledgeBase] = useState<IKnowledge | null>(null);
|
const [knowledgeBase, setKnowledgeBase] = useState<IKnowledge | null>(null);
|
||||||
const [document, setDocument] = useState<IKnowledgeFile | null>(null);
|
const [document, setDocument] = useState<IKnowledgeFile | null>(null);
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
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
|
// 使用chunk列表hook
|
||||||
const {
|
const {
|
||||||
@@ -68,6 +86,9 @@ function ChunkParsedResult() {
|
|||||||
setDocument(docArr[0]);
|
setDocument(docArr[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动加载文档文件用于预览
|
||||||
|
await loadDocumentFile();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch data:', error);
|
console.error('Failed to fetch data:', error);
|
||||||
}
|
}
|
||||||
@@ -83,11 +104,223 @@ function ChunkParsedResult() {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理查看文件
|
// 异步加载文档文件
|
||||||
const handleViewFile = () => {
|
const loadDocumentFile = async () => {
|
||||||
if (doc_id && kb_id) {
|
if (!doc_id || fileLoading) return;
|
||||||
// 跳转到文件预览页面
|
|
||||||
navigate(`/chunk/document-preview/${kb_id}/${doc_id}`);
|
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,68 +354,87 @@ function ChunkParsedResult() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{/* 页面标题和文档信息 */}
|
{/* 顶部信息与下载 */}
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 2 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
|
{document?.name || t('chunkPage.documentDetail')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t('chunkPage.documentChunkResult')}
|
{t('chunkPage.documentChunkResult')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
{t('chunkPage.viewDocument')} "{document?.name}" {t('chunkPage.allChunkData')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Card>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<CardContent sx={{ textAlign: 'center' }}>
|
<Card>
|
||||||
<Typography variant="h4" color="primary">
|
<CardContent sx={{ textAlign: 'center' }}>
|
||||||
{total}
|
<Typography variant="h4" color="primary">
|
||||||
</Typography>
|
{total}
|
||||||
<Typography variant="body2" color="text.secondary">
|
</Typography>
|
||||||
{t('chunkPage.totalChunkCount')}
|
<Typography variant="body2" color="text.secondary">
|
||||||
</Typography>
|
{t('chunkPage.totalChunkCount')}
|
||||||
</CardContent>
|
</Typography>
|
||||||
</Card>
|
</CardContent>
|
||||||
<Button
|
</Card>
|
||||||
variant="outlined"
|
{fileUrl && (
|
||||||
startIcon={<VisibilityIcon />}
|
<Button variant="contained" startIcon={<DownloadIcon />} onClick={handleDownload}>
|
||||||
onClick={handleViewFile}
|
{t('chunkPage.downloadFile')}
|
||||||
sx={{ ml: 2 }}
|
</Button>
|
||||||
>
|
)}
|
||||||
{t('chunkPage.viewFile')}
|
</Box>
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* 搜索框 */}
|
{/* 左右布局:左预览,右切片结果 */}
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
<TextField
|
{/* 左侧文档预览 */}
|
||||||
fullWidth
|
<Box sx={{ flex: 1 }}>
|
||||||
placeholder={t('chunkPage.searchChunkPlaceholder')}
|
<Paper sx={{ p: 2, height: '80vh' }}>
|
||||||
value={searchKeyword}
|
{/* 文件加载状态 */}
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
{fileLoading && (
|
||||||
InputProps={{
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px' }}>
|
||||||
startAdornment: (
|
<Alert severity="info">{t('chunkPage.loadingFile')}</Alert>
|
||||||
<InputAdornment position="start">
|
</Box>
|
||||||
<SearchIcon />
|
)}
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Chunk列表结果 */}
|
{renderPreview()}
|
||||||
<ChunkListResult
|
</Paper>
|
||||||
doc_id={doc_id}
|
</Box>
|
||||||
chunks={chunks}
|
|
||||||
total={total}
|
{/* 右侧切片结果与搜索 */}
|
||||||
loading={loading}
|
<Box sx={{ flex: 1 }}>
|
||||||
error={error}
|
<Paper sx={{ p: 2, height: '80vh', display: 'flex', flexDirection: 'column' }}>
|
||||||
page={currentPage}
|
<TextField
|
||||||
pageSize={pageSize}
|
fullWidth
|
||||||
onPageChange={setCurrentPage}
|
placeholder={t('chunkPage.searchChunkPlaceholder')}
|
||||||
onRefresh={refresh}
|
value={searchKeyword}
|
||||||
docName={document?.name}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
/>
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{ flex: 1, overflow: 'auto', pt: 2 }}>
|
||||||
|
<ChunkListResult
|
||||||
|
doc_id={doc_id}
|
||||||
|
chunks={chunks}
|
||||||
|
total={total}
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
page={currentPage}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
onRefresh={refresh}
|
||||||
|
docName={document?.name}
|
||||||
|
onLocate={handleLocate}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const Login = () => {
|
|||||||
<Card sx={{ width: '100%' }}>
|
<Card sx={{ width: '100%' }}>
|
||||||
<CardContent sx={{ p: 4 }}>
|
<CardContent sx={{ p: 4 }}>
|
||||||
<Typography variant="subtitle1" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
<Typography variant="subtitle1" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
T-Systems Enterprise RAG Empowerment System
|
TERES AI
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* 标签页 */}
|
{/* 标签页 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user