Compare commits
2 Commits
5a0a9ef2a1
...
91bf724ac0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91bf724ac0 | ||
|
|
0b97991a36 |
4
.env
4
.env
@@ -1,5 +1,5 @@
|
|||||||
VITE_API_BASE_URL = http://150.158.121.95
|
# VITE_API_BASE_URL = http://150.158.121.95
|
||||||
# VITE_API_BASE_URL = http://154.9.253.114
|
VITE_API_BASE_URL = http://154.9.253.114
|
||||||
|
|
||||||
VITE_RSA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
|
VITE_RSA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB
|
||||||
|
|||||||
@@ -11,7 +11,17 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Alert,
|
Alert,
|
||||||
|
Divider,
|
||||||
|
Avatar,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
Image as ImageIcon,
|
||||||
|
TextSnippet as TextIcon,
|
||||||
|
Visibility as VisibilityIcon,
|
||||||
|
VisibilityOff as VisibilityOffIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
import type { IChunk } from '@/interfaces/database/knowledge';
|
import type { IChunk } from '@/interfaces/database/knowledge';
|
||||||
|
|
||||||
interface ChunkListResultProps {
|
interface ChunkListResultProps {
|
||||||
@@ -115,60 +125,157 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={3}>
|
||||||
{chunks.map((chunk, index) => (
|
{chunks.map((chunk, index) => (
|
||||||
<Grid size={12} key={chunk.chunk_id}>
|
<Grid size={12} key={chunk.chunk_id}>
|
||||||
<Card variant="outlined">
|
<Card
|
||||||
<CardContent>
|
variant="outlined"
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
sx={{
|
||||||
<Typography variant="subtitle1" fontWeight="bold">
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: 3,
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
},
|
||||||
|
border: chunk.available_int === 1 ? '2px solid' : '1px solid',
|
||||||
|
borderColor: chunk.available_int === 1 ? 'success.main' : 'grey.300',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 3 }}>
|
||||||
|
{/* 头部信息 */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
bgcolor: chunk.available_int === 1 ? 'success.main' : 'grey.400',
|
||||||
|
mr: 2,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextIcon />
|
||||||
|
</Avatar>
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Typography variant="h6" fontWeight="bold" color="text.primary">
|
||||||
Chunk #{((page - 1) * pageSize) + index + 1}
|
Chunk #{((page - 1) * pageSize) + index + 1}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction="row" spacing={1}>
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
ID: {chunk.chunk_id}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
{chunk.image_id && (
|
||||||
|
<Tooltip title="包含图片">
|
||||||
|
<Avatar sx={{ bgcolor: 'info.main', width: 32, height: 32 }}>
|
||||||
|
<ImageIcon fontSize="small" />
|
||||||
|
</Avatar>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Chip
|
<Chip
|
||||||
|
icon={chunk.available_int === 1 ? <VisibilityIcon /> : <VisibilityOffIcon />}
|
||||||
label={chunk.available_int === 1 ? '已启用' : '未启用'}
|
label={chunk.available_int === 1 ? '已启用' : '未启用'}
|
||||||
size="small"
|
size="small"
|
||||||
color={chunk.available_int === 1 ? 'success' : 'default'}
|
color={chunk.available_int === 1 ? 'success' : 'default'}
|
||||||
|
variant={chunk.available_int === 1 ? 'filled' : 'outlined'}
|
||||||
/>
|
/>
|
||||||
{chunk.image_id && (
|
|
||||||
<Chip
|
|
||||||
label="包含图片"
|
|
||||||
size="small"
|
|
||||||
color="info"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
|
内容预览
|
||||||
|
</Typography>
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
maxHeight: '200px',
|
||||||
|
overflow: 'auto',
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
borderRadius: 2,
|
||||||
|
'&::-webkit-scrollbar': {
|
||||||
|
width: '6px',
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-track': {
|
||||||
|
backgroundColor: 'grey.100',
|
||||||
|
borderRadius: '3px',
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
backgroundColor: 'grey.400',
|
||||||
|
borderRadius: '3px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
mb: 2,
|
|
||||||
maxHeight: '200px',
|
|
||||||
overflow: 'auto',
|
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
backgroundColor: 'grey.50',
|
lineHeight: 1.6,
|
||||||
p: 2,
|
color: 'text.primary',
|
||||||
borderRadius: 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{chunk.content_with_weight || '无内容'}
|
{chunk.content_with_weight || '无内容'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 图片显示区域 */}
|
||||||
|
{chunk.image_id && (
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
|
相关图片
|
||||||
|
</Typography>
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={`${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`}
|
||||||
|
alt="Chunk相关图片"
|
||||||
|
sx={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '300px',
|
||||||
|
borderRadius: 1,
|
||||||
|
objectFit: 'contain',
|
||||||
|
boxShadow: 1,
|
||||||
|
}}
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 关键词区域 */}
|
||||||
|
{((chunk.important_kwd ?? []).length > 0 || (chunk.question_kwd ?? []).length > 0 || (chunk.tag_kwd ?? []).length > 0) && (
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
关键词信息
|
||||||
|
</Typography>
|
||||||
|
|
||||||
{chunk.important_kwd && chunk.important_kwd.length > 0 && (
|
{chunk.important_kwd && chunk.important_kwd.length > 0 && (
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
重要关键词:
|
重要关键词
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||||
{chunk.important_kwd.map((keyword, kwdIndex) => (
|
{chunk.important_kwd.map((keyword, kwdIndex) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={kwdIndex}
|
key={kwdIndex}
|
||||||
label={keyword}
|
label={keyword}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="filled"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
sx={{ fontWeight: 'medium' }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -176,18 +283,19 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{chunk.question_kwd && chunk.question_kwd.length > 0 && (
|
{chunk.question_kwd && chunk.question_kwd.length > 0 && (
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
问题关键词:
|
问题关键词
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||||
{chunk.question_kwd.map((keyword, kwdIndex) => (
|
{chunk.question_kwd.map((keyword, kwdIndex) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={kwdIndex}
|
key={kwdIndex}
|
||||||
label={keyword}
|
label={keyword}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="filled"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
sx={{ fontWeight: 'medium' }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -195,29 +303,37 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{chunk.tag_kwd && chunk.tag_kwd.length > 0 && (
|
{chunk.tag_kwd && chunk.tag_kwd.length > 0 && (
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
标签关键词:
|
标签关键词
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||||
{chunk.tag_kwd.map((keyword, kwdIndex) => (
|
{chunk.tag_kwd.map((keyword, kwdIndex) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={kwdIndex}
|
key={kwdIndex}
|
||||||
label={keyword}
|
label={keyword}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="filled"
|
||||||
color="info"
|
color="info"
|
||||||
|
sx={{ fontWeight: 'medium' }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 位置信息 */}
|
||||||
{chunk.positions && chunk.positions.length > 0 && (
|
{chunk.positions && chunk.positions.length > 0 && (
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mt: 2, pt: 2, borderTop: '1px solid', borderColor: 'grey.200' }}>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Chip
|
||||||
位置信息: {chunk.positions.length} 个位置点
|
label={`位置信息: ${chunk.positions.length} 个位置点`}
|
||||||
</Typography>
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="default"
|
||||||
|
sx={{ fontWeight: 'medium' }}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
309
src/pages/chunk/document-preview.tsx
Normal file
309
src/pages/chunk/document-preview.tsx
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
CircularProgress,
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Breadcrumbs,
|
||||||
|
Link,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardMedia,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
ArrowBack as ArrowBackIcon,
|
||||||
|
Download as DownloadIcon,
|
||||||
|
Visibility as VisibilityIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import knowledgeService from '@/services/knowledge_service';
|
||||||
|
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||||
|
|
||||||
|
interface DocumentPreviewProps {}
|
||||||
|
|
||||||
|
function DocumentPreview(props: DocumentPreviewProps) {
|
||||||
|
const { kb_id, doc_id } = useParams<{ kb_id: string; doc_id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [kb, setKb] = useState<IKnowledge | null>(null);
|
||||||
|
const [documentObj, setDocumentObj] = useState<IKnowledgeFile | null>(null);
|
||||||
|
const [documentFile, setDocumentFile] = useState<Blob | null>(null);
|
||||||
|
const [fileUrl, setFileUrl] = useState<string>('');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [fileLoading, setFileLoading] = useState(false);
|
||||||
|
|
||||||
|
// 获取知识库和文档信息
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
if (!kb_id || !doc_id) {
|
||||||
|
setError('缺少必要的参数');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// 并行获取知识库信息和文档详情
|
||||||
|
const [kbResponse, docResponse] = await Promise.all([
|
||||||
|
knowledgeService.getKnowledgeDetail({ kb_id }),
|
||||||
|
knowledgeService.getDocumentInfos({ doc_ids: [doc_id] })
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (kbResponse.data.data) {
|
||||||
|
setKb(kbResponse.data.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docResponse.data.data?.length > 0) {
|
||||||
|
setDocumentObj(docResponse.data.data[0]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取数据失败:', err);
|
||||||
|
setError('获取数据失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, [kb_id, doc_id]);
|
||||||
|
|
||||||
|
// 异步加载文档文件
|
||||||
|
const loadDocumentFile = async () => {
|
||||||
|
if (!doc_id || fileLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setFileLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const fileResponse = await knowledgeService.getDocumentFile({ doc_id });
|
||||||
|
|
||||||
|
if (fileResponse.data instanceof Blob) {
|
||||||
|
setDocumentFile(fileResponse.data);
|
||||||
|
const url = URL.createObjectURL(fileResponse.data);
|
||||||
|
setFileUrl(url);
|
||||||
|
} else {
|
||||||
|
setError('文件格式不支持预览');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取文档文件失败:', err);
|
||||||
|
setError('获取文档文件失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
setFileLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理文件URL
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (fileUrl) {
|
||||||
|
URL.revokeObjectURL(fileUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [fileUrl]);
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (fileUrl && window.document) {
|
||||||
|
const link = window.document.createElement('a');
|
||||||
|
link.href = fileUrl;
|
||||||
|
link.download = documentObj?.name || 'document';
|
||||||
|
window.document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
window.document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const handleGoBack = () => {
|
||||||
|
navigate(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染文件预览
|
||||||
|
const renderFilePreview = () => {
|
||||||
|
if (!documentFile || !fileUrl) return null;
|
||||||
|
|
||||||
|
const fileType = documentFile.type;
|
||||||
|
|
||||||
|
if (fileType.startsWith('image/')) {
|
||||||
|
return (
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
image={fileUrl}
|
||||||
|
alt={documentObj?.name || '文档预览'}
|
||||||
|
sx={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '80vh',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (fileType === 'application/pdf') {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component="iframe"
|
||||||
|
src={fileUrl}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '80vh',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (fileType.startsWith('text/')) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component="iframe"
|
||||||
|
src={fileUrl}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '80vh',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'grey.300',
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Alert severity="info" sx={{ mt: 2 }}>
|
||||||
|
此文件类型不支持在线预览,请下载后查看。
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '400px' }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error && !documentObj) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<Alert severity="error">{error}</Alert>
|
||||||
|
<Button
|
||||||
|
startIcon={<ArrowBackIcon />}
|
||||||
|
onClick={handleGoBack}
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
>
|
||||||
|
返回
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
{/* 面包屑导航 */}
|
||||||
|
<Breadcrumbs sx={{ mb: 2 }}>
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
variant="body2"
|
||||||
|
onClick={() => navigate('/knowledge')}
|
||||||
|
sx={{ textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
知识库
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
variant="body2"
|
||||||
|
onClick={() => navigate(`/knowledge/${kb_id}`)}
|
||||||
|
sx={{ textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
{kb?.name || '知识库详情'}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
variant="body2"
|
||||||
|
onClick={() => navigate(`/knowledge/${kb_id}/document/${doc_id}/chunks`)}
|
||||||
|
sx={{ textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
文档分块
|
||||||
|
</Link>
|
||||||
|
<Typography color="text.primary">文件预览</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
{/* 页面标题和操作按钮 */}
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
文件预览
|
||||||
|
</Typography>
|
||||||
|
{documentObj && (
|
||||||
|
<Typography variant="subtitle1" color="text.secondary">
|
||||||
|
{documentObj.name}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
startIcon={<ArrowBackIcon />}
|
||||||
|
onClick={handleGoBack}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
返回
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!fileUrl && (
|
||||||
|
<Button
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
onClick={loadDocumentFile}
|
||||||
|
variant="contained"
|
||||||
|
disabled={fileLoading}
|
||||||
|
>
|
||||||
|
{fileLoading ? '加载中...' : '预览文件'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fileUrl && (
|
||||||
|
<Button
|
||||||
|
startIcon={<DownloadIcon />}
|
||||||
|
onClick={handleDownload}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
下载文件
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 文件加载状态 */}
|
||||||
|
{fileLoading && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px' }}>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography sx={{ ml: 2 }}>正在加载文件...</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 文件预览区域 */}
|
||||||
|
{fileUrl && (
|
||||||
|
<Paper elevation={1} sx={{ p: 2 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
{renderFilePreview()}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocumentPreview;
|
||||||
@@ -9,16 +9,13 @@ import {
|
|||||||
InputAdornment,
|
InputAdornment,
|
||||||
Paper,
|
Paper,
|
||||||
Alert,
|
Alert,
|
||||||
Card,
|
Button
|
||||||
CardContent,
|
|
||||||
CardMedia
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Search as SearchIcon, ArrowBack as ArrowBackIcon } from '@mui/icons-material';
|
import { Search as SearchIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
|
||||||
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 } from '@/interfaces/database/knowledge';
|
||||||
import type { IDocumentInfo } from '@/interfaces/database/document';
|
|
||||||
|
|
||||||
function ChunkParsedResult() {
|
function ChunkParsedResult() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@@ -28,8 +25,6 @@ function ChunkParsedResult() {
|
|||||||
|
|
||||||
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 [documentFile, setDocumentFile] = useState<Blob | null>(null);
|
|
||||||
const [fileUrl, setFileUrl] = useState<string>('');
|
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
|
|
||||||
// 使用chunk列表hook
|
// 使用chunk列表hook
|
||||||
@@ -69,22 +64,14 @@ function ChunkParsedResult() {
|
|||||||
setDocument(docArr[0]);
|
setDocument(docArr[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取文档文件
|
|
||||||
const fileResponse = await knowledgeService.getDocumentFile({ doc_id });
|
|
||||||
if (fileResponse.data) {
|
|
||||||
// 处理二进制文件数据
|
|
||||||
setDocumentFile(fileResponse.data);
|
|
||||||
|
|
||||||
// 创建文件URL用于预览
|
|
||||||
const url = URL.createObjectURL(fileResponse.data);
|
|
||||||
setFileUrl(url);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch data:', error);
|
console.error('Failed to fetch data:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, [kb_id, doc_id]);
|
||||||
|
|
||||||
// 处理搜索
|
// 处理搜索
|
||||||
const handleSearch = (keyword: string) => {
|
const handleSearch = (keyword: string) => {
|
||||||
setSearchKeyword(keyword);
|
setSearchKeyword(keyword);
|
||||||
@@ -92,92 +79,13 @@ function ChunkParsedResult() {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
// 处理查看文件
|
||||||
|
const handleViewFile = () => {
|
||||||
// 清理函数,释放URL对象
|
if (doc_id && kb_id) {
|
||||||
return () => {
|
// 跳转到文件预览页面
|
||||||
if (fileUrl) {
|
navigate(`/chunk/document-preview/${kb_id}/${doc_id}`);
|
||||||
URL.revokeObjectURL(fileUrl);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [kb_id, doc_id]);
|
|
||||||
|
|
||||||
// 渲染文件预览组件
|
|
||||||
const renderFilePreview = () => {
|
|
||||||
if (!document || !fileUrl) return null;
|
|
||||||
|
|
||||||
const fileExtension = document.name?.split('.').pop()?.toLowerCase();
|
|
||||||
|
|
||||||
// 图片文件预览
|
|
||||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(fileExtension || '')) {
|
|
||||||
return (
|
|
||||||
<Card sx={{ mb: 3 }}>
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
文件预览
|
|
||||||
</Typography>
|
|
||||||
<CardMedia
|
|
||||||
component="img"
|
|
||||||
sx={{
|
|
||||||
maxHeight: 400,
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: '1px solid #e0e0e0',
|
|
||||||
borderRadius: 1
|
|
||||||
}}
|
|
||||||
image={fileUrl}
|
|
||||||
alt={document.name}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// PDF文件预览
|
|
||||||
if (fileExtension === 'pdf') {
|
|
||||||
return (
|
|
||||||
<Card sx={{ mb: 3 }}>
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
PDF预览
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ height: 600, border: '1px solid #e0e0e0', borderRadius: 1 }}>
|
|
||||||
<iframe
|
|
||||||
src={fileUrl}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
style={{ border: 'none' }}
|
|
||||||
title={document.name}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他文件类型显示下载链接
|
|
||||||
return (
|
|
||||||
<Card sx={{ mb: 3 }}>
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
文件信息
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
||||||
文件名: {document.name}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
||||||
文件类型: {fileExtension?.toUpperCase() || '未知'}
|
|
||||||
</Typography>
|
|
||||||
<Link
|
|
||||||
href={fileUrl}
|
|
||||||
download={document.name}
|
|
||||||
sx={{ mt: 2, display: 'inline-block' }}
|
|
||||||
>
|
|
||||||
下载文件
|
|
||||||
</Link>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!kb_id || !doc_id) {
|
if (!kb_id || !doc_id) {
|
||||||
return (
|
return (
|
||||||
@@ -220,27 +128,35 @@ function ChunkParsedResult() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</Box>
|
</Box>
|
||||||
|
{/* 页面标题和文档信息 */}
|
||||||
{/* 页面标题 */}
|
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||||
|
<Box>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
文档Chunk解析结果
|
文档Chunk解析结果
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" color="text.secondary">
|
<Typography variant="body1" color="text.secondary">
|
||||||
查看文档 "{document?.name}" 的所有chunk数据
|
查看文档 "{document?.name}" 的所有chunk数据
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
onClick={handleViewFile}
|
||||||
|
sx={{ ml: 2 }}
|
||||||
|
>
|
||||||
|
查看文件
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* 文件预览 */}
|
|
||||||
{renderFilePreview()}
|
|
||||||
|
|
||||||
{/* 搜索框 */}
|
{/* 搜索框 */}
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder="搜索chunk内容..."
|
placeholder="搜索chunk内容..."
|
||||||
value={searchKeyword}
|
value={searchKeyword}
|
||||||
// onChange={(e) => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { KnowledgeBaseList, KnowledgeBaseCreate, KnowledgeBaseDetail, KnowledgeB
|
|||||||
import MCP from '../pages/MCP';
|
import MCP from '../pages/MCP';
|
||||||
import FormFieldTest from '../pages/FormFieldTest';
|
import FormFieldTest from '../pages/FormFieldTest';
|
||||||
import ChunkParsedResult from '@/pages/chunk/parsed-result';
|
import ChunkParsedResult from '@/pages/chunk/parsed-result';
|
||||||
|
import DocumentPreview from '@/pages/chunk/document-preview';
|
||||||
|
|
||||||
const AppRoutes = () => {
|
const AppRoutes = () => {
|
||||||
return (
|
return (
|
||||||
@@ -38,6 +39,8 @@ const AppRoutes = () => {
|
|||||||
{/* 处理chunk相关路由 需要传入 kb_id doc_id */}
|
{/* 处理chunk相关路由 需要传入 kb_id doc_id */}
|
||||||
<Route path="chunk">
|
<Route path="chunk">
|
||||||
<Route path="parsed-result" element={<ChunkParsedResult />} />
|
<Route path="parsed-result" element={<ChunkParsedResult />} />
|
||||||
|
{/* 文档预览页面路由 */}
|
||||||
|
<Route path="/document-preview/:kb_id/:doc_id" element={<DocumentPreview />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* 处理未匹配的路由 */}
|
{/* 处理未匹配的路由 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user