diff --git a/index.html b/index.html index 199ae81..f979027 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - RAG Empowerment System + TERES AI
diff --git a/package.json b/package.json index dfdd45e..08d5566 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 641044d..3815f8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/App.tsx b/src/App.tsx index 3d88310..c9efbcf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,7 +41,7 @@ function MaterialUIApp() { } function App() { - useTitle('RAG Empowerment System'); + useTitle('TERES AI'); return ( diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index b86334c..3d37496 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -61,7 +61,7 @@ const Header = () => { borderBottom: '1px solid #E5E5E5', }} > - { }} > RAG Empowerment System - + */} { letterSpacing: '0.5px', }} > - T-Systems Enterprise + TERES AI diff --git a/src/pages/chunk/components/ChunkListResult.tsx b/src/pages/chunk/components/ChunkListResult.tsx index ec4511b..4ee281d 100644 --- a/src/pages/chunk/components/ChunkListResult.tsx +++ b/src/pages/chunk/components/ChunkListResult.tsx @@ -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) { {chunks.map((chunk, index) => ( - + + {/* 定位到文档位置 */} + + onLocate?.(chunk)}> + + + {/* 主要内容区域 - 左右布局 */} @@ -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} > diff --git a/src/pages/chunk/parsed-result.tsx b/src/pages/chunk/parsed-result.tsx index 6fa8fca..311ec26 100644 --- a/src/pages/chunk/parsed-result.tsx +++ b/src/pages/chunk/parsed-result.tsx @@ -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(null); const [document, setDocument] = useState(null); const [searchKeyword, setSearchKeyword] = useState(''); + const [documentFile, setDocumentFile] = useState(null); + const [fileUrl, setFileUrl] = useState(''); + const [fileLoading, setFileLoading] = useState(false); + const [previewOverrideUrl, setPreviewOverrideUrl] = useState(''); + const [focusPage, setFocusPage] = useState(null); + const abortControllerRef = useRef(null); + const pdfContainerRef = useRef(null); + const [pdfRendered, setPdfRendered] = useState(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 ( + + ); + } + if (!documentFile || !fileUrl) return null; + + const fileType = documentFile.type; + if (fileType.startsWith('image/')) { + return ( + + ); + } else if (fileType === 'application/pdf') { + // 使用 pdf.js 渲染的容器,支持滚动与页码定位 + return ( + + ); + } else if (fileType.startsWith('text/')) { + return ( + + ); + } else { + return ( + + {t('chunkPage.fileTypeNotSupportedPreview')} + + ); } }; @@ -121,68 +354,87 @@ function ChunkParsedResult() { }, ]} /> - {/* 页面标题和文档信息 */} - - + {/* 顶部信息与下载 */} + + + {document?.name || t('chunkPage.documentDetail')} + + {t('chunkPage.documentChunkResult')} - - {t('chunkPage.viewDocument')} "{document?.name}" {t('chunkPage.allChunkData')} - - - - - {total} - - - {t('chunkPage.totalChunkCount')} - - - - + + + + + {total} + + + {t('chunkPage.totalChunkCount')} + + + + {fileUrl && ( + + )} + - {/* 搜索框 */} - - handleSearch(e.target.value)} - InputProps={{ - startAdornment: ( - - - - ), - }} - /> - + {/* 左右布局:左预览,右切片结果 */} + + {/* 左侧文档预览 */} + + + {/* 文件加载状态 */} + {fileLoading && ( + + {t('chunkPage.loadingFile')} + + )} - {/* Chunk列表结果 */} - + {renderPreview()} + + + + {/* 右侧切片结果与搜索 */} + + + handleSearch(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + + + + ); } diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx index 2126bc7..a045dcc 100644 --- a/src/pages/login/Login.tsx +++ b/src/pages/login/Login.tsx @@ -73,7 +73,7 @@ const Login = () => { - T-Systems Enterprise RAG Empowerment System + TERES AI {/* 标签页 */}