feat(knowledge): add chunk management and document processing features
This commit is contained in:
0
src/constants/document.ts
Normal file
0
src/constants/document.ts
Normal file
107
src/hooks/chunk-hooks.ts
Normal file
107
src/hooks/chunk-hooks.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import knowledgeService from '@/services/knowledge_service';
|
||||||
|
import type { IChunk, IChunkListResult } from '@/interfaces/database/knowledge';
|
||||||
|
import type { IFetchChunkListRequestBody } from '@/interfaces/request/knowledge';
|
||||||
|
|
||||||
|
// Chunk列表Hook状态接口
|
||||||
|
export interface UseChunkListState {
|
||||||
|
chunks: IChunk[];
|
||||||
|
total: number;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
keywords: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk列表Hook返回值接口
|
||||||
|
export interface UseChunkListReturn extends UseChunkListState {
|
||||||
|
fetchChunks: (params?: IFetchChunkListRequestBody) => Promise<void>;
|
||||||
|
setKeywords: (keywords: string) => void;
|
||||||
|
setCurrentPage: (page: number) => void;
|
||||||
|
setPageSize: (size: number) => void;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk列表数据管理Hook
|
||||||
|
* 支持关键词搜索、分页等功能
|
||||||
|
*/
|
||||||
|
export const useChunkList = (
|
||||||
|
docId: string,
|
||||||
|
initialParams?: IFetchChunkListRequestBody
|
||||||
|
): UseChunkListReturn => {
|
||||||
|
const [chunks, setChunks] = useState<IChunk[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [currentPage, setCurrentPage] = useState(initialParams?.page || 1);
|
||||||
|
const [pageSize, setPageSize] = useState(initialParams?.size || 10);
|
||||||
|
const [keywords, setKeywords] = useState(initialParams?.keywords || '');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取chunk列表
|
||||||
|
*/
|
||||||
|
const fetchChunks = useCallback(async (params?: IFetchChunkListRequestBody) => {
|
||||||
|
if (!docId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// 合并参数
|
||||||
|
const queryParams = {
|
||||||
|
doc_id: docId,
|
||||||
|
keywords: params?.keywords ?? keywords,
|
||||||
|
page: params?.page ?? currentPage,
|
||||||
|
size: params?.size ?? pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await knowledgeService.getChunkList(queryParams);
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
const data: IChunkListResult = response.data.data;
|
||||||
|
setChunks(data.chunks || []);
|
||||||
|
setTotal(data.total || 0);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '获取chunk列表失败');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage = err.response?.data?.message || err.message || '获取chunk列表失败';
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error('Failed to fetch chunks:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [docId, keywords, currentPage, pageSize]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新数据
|
||||||
|
*/
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
await fetchChunks();
|
||||||
|
}, [fetchChunks]);
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (docId) {
|
||||||
|
fetchChunks();
|
||||||
|
}
|
||||||
|
}, [docId, fetchChunks]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
chunks,
|
||||||
|
total,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
keywords,
|
||||||
|
fetchChunks,
|
||||||
|
setKeywords,
|
||||||
|
setCurrentPage,
|
||||||
|
setPageSize,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|||||||
import knowledgeService from '@/services/knowledge_service';
|
import knowledgeService from '@/services/knowledge_service';
|
||||||
import type { IKnowledgeFile } from '@/interfaces/database/knowledge';
|
import type { IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||||
import type { IFetchKnowledgeListRequestParams, IFetchDocumentListRequestBody } from '@/interfaces/request/knowledge';
|
import type { IFetchKnowledgeListRequestParams, IFetchDocumentListRequestBody } from '@/interfaces/request/knowledge';
|
||||||
|
import type { IDocumentInfoFilter } from '@/interfaces/database/document';
|
||||||
|
|
||||||
// 文档列表Hook状态接口
|
// 文档列表Hook状态接口
|
||||||
export interface UseDocumentListState {
|
export interface UseDocumentListState {
|
||||||
@@ -17,7 +18,7 @@ export interface UseDocumentListState {
|
|||||||
// 文档列表Hook返回值接口
|
// 文档列表Hook返回值接口
|
||||||
export interface UseDocumentListReturn extends UseDocumentListState {
|
export interface UseDocumentListReturn extends UseDocumentListState {
|
||||||
fetchDocuments: (params?: IFetchKnowledgeListRequestParams) => Promise<void>;
|
fetchDocuments: (params?: IFetchKnowledgeListRequestParams) => Promise<void>;
|
||||||
fetchDocumentsFilter: () => Promise<void>;
|
fetchDocumentsFilter: () => Promise<IDocumentInfoFilter | undefined>;
|
||||||
setKeywords: (keywords: string) => void;
|
setKeywords: (keywords: string) => void;
|
||||||
setCurrentPage: (page: number) => void;
|
setCurrentPage: (page: number) => void;
|
||||||
setPageSize: (size: number) => void;
|
setPageSize: (size: number) => void;
|
||||||
@@ -107,7 +108,8 @@ export const useDocumentList = (
|
|||||||
kb_id: kbId,
|
kb_id: kbId,
|
||||||
});
|
});
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
const data = response.data.data;
|
const data: IDocumentInfoFilter = response.data.data;
|
||||||
|
return data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.data.message || '获取文档过滤器失败');
|
throw new Error(response.data.message || '获取文档过滤器失败');
|
||||||
}
|
}
|
||||||
@@ -324,7 +326,7 @@ export const useDocumentOperations = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await knowledgeService.changeDocumentStatus({ doc_id: docIds, status });
|
const response = await knowledgeService.changeDocumentStatus({ doc_ids: docIds, status });
|
||||||
|
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
@@ -349,7 +351,7 @@ export const useDocumentOperations = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await knowledgeService.runDocument({ doc_id: docIds });
|
const response = await knowledgeService.runDocument({ doc_ids: docIds, run: 1, delete: false });
|
||||||
|
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
@@ -366,6 +368,31 @@ export const useDocumentOperations = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消文档处理
|
||||||
|
*/
|
||||||
|
const cancelRunDocuments = useCallback(async (docIds: string[]) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const response = await knowledgeService.runDocument({ doc_ids: docIds, run: 2, delete: false });
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '取消文档处理失败');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage = err.response?.data?.message || err.message || '取消文档处理失败';
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error('Failed to cancel documents:', err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除错误状态
|
* 清除错误状态
|
||||||
*/
|
*/
|
||||||
@@ -383,6 +410,7 @@ export const useDocumentOperations = () => {
|
|||||||
createDocument,
|
createDocument,
|
||||||
changeDocumentStatus,
|
changeDocumentStatus,
|
||||||
runDocuments,
|
runDocuments,
|
||||||
|
cancelRunDocuments,
|
||||||
clearError,
|
clearError,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -435,7 +463,7 @@ export const useDocumentBatchOperations = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await knowledgeService.changeDocumentStatus({ doc_id: docIds, status });
|
const response = await knowledgeService.changeDocumentStatus({ doc_ids: docIds, status });
|
||||||
|
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
@@ -460,7 +488,7 @@ export const useDocumentBatchOperations = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await knowledgeService.runDocument({ doc_id: docIds });
|
const response = await knowledgeService.runDocument({ doc_ids: docIds, run: 1, delete: false });
|
||||||
|
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RunningStatus } from '@/constants/knowledge';
|
import type { RunningStatus } from '@/constants/knowledge';
|
||||||
|
import type { IDocumentInfo } from './document';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -426,6 +427,15 @@ export interface INextTestingResult {
|
|||||||
isRuned?: boolean;
|
isRuned?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IChunkListResult {
|
||||||
|
/** 文档块列表 */
|
||||||
|
chunks: IChunk[];
|
||||||
|
/** 文档信息列表 */
|
||||||
|
doc: IDocumentInfo[]
|
||||||
|
/** 总匹配数量 */
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重命名标签类型
|
* 重命名标签类型
|
||||||
* 用于标签重命名操作
|
* 用于标签重命名操作
|
||||||
|
|||||||
@@ -16,3 +16,19 @@ export interface IDocumentMetaRequestBody {
|
|||||||
documentId: string;
|
documentId: string;
|
||||||
meta: string; // json format string
|
meta: string; // json format string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export const RUNNING_STATUS_KEYS = Object.freeze({
|
||||||
|
// UNSTART: '0', // need to run
|
||||||
|
// RUNNING: '1', // need to cancel
|
||||||
|
// CANCEL: '2', // need to refresh
|
||||||
|
// DONE: '3', // need to refresh
|
||||||
|
// FAIL: '4', // need to refresh
|
||||||
|
// } as const)
|
||||||
|
|
||||||
|
export interface IRunDocumentRequestBody {
|
||||||
|
doc_ids: Array<string | number> | string | number;
|
||||||
|
// running status 1 run 2 cancel - operations
|
||||||
|
run: number;
|
||||||
|
delete: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IPaginationRequestBody } from './base';
|
import { type IPaginationRequestBody } from './base';
|
||||||
|
|
||||||
export interface IFileListRequestBody extends IPaginationRequestBody {
|
export interface IFileListRequestBody extends IPaginationRequestBody {
|
||||||
parent_id?: string; // folder id
|
parent_id?: string; // folder id
|
||||||
|
|||||||
@@ -49,3 +49,11 @@ export interface IFetchDocumentListRequestBody {
|
|||||||
suffix?: string[];
|
suffix?: string[];
|
||||||
run_status?: string[];
|
run_status?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface IFetchChunkListRequestBody {
|
||||||
|
doc_id?: string;
|
||||||
|
keywords?: string;
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|||||||
247
src/pages/chunk/components/ChunkListResult.tsx
Normal file
247
src/pages/chunk/components/ChunkListResult.tsx
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Chip,
|
||||||
|
Stack,
|
||||||
|
Pagination,
|
||||||
|
CircularProgress,
|
||||||
|
Alert,
|
||||||
|
} from '@mui/material';
|
||||||
|
import type { IChunk } from '@/interfaces/database/knowledge';
|
||||||
|
|
||||||
|
interface ChunkListResultProps {
|
||||||
|
chunks: IChunk[];
|
||||||
|
total: number;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
docName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChunkListResult(props: ChunkListResultProps) {
|
||||||
|
const { chunks, total, loading, error, page, pageSize, onPageChange, docName } = props;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
||||||
|
正在加载chunk数据...
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Paper sx={{ p: 3 }}>
|
||||||
|
<Alert severity="error">
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chunks || chunks.length === 0) {
|
||||||
|
return (
|
||||||
|
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
暂无chunk数据
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
|
该文档还没有生成chunk数据,请检查文档是否已完成解析
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(total / pageSize);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* Chunk结果概览 */}
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
文档Chunk详情
|
||||||
|
</Typography>
|
||||||
|
{docName && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
文档名称: {docName}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h4" color="primary">
|
||||||
|
{total}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
总Chunk数量
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h4" color="secondary">
|
||||||
|
{chunks.filter(chunk => chunk.available_int === 1).length}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
已启用Chunk
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Chunk列表 */}
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Chunk列表 (第 {page} 页,共 {totalPages} 页)
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
共 {total} 个chunk
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{chunks.map((chunk, index) => (
|
||||||
|
<Grid size={12} key={chunk.chunk_id}>
|
||||||
|
<Card variant="outlined">
|
||||||
|
<CardContent>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold">
|
||||||
|
Chunk #{((page - 1) * pageSize) + index + 1}
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<Chip
|
||||||
|
label={chunk.available_int === 1 ? '已启用' : '未启用'}
|
||||||
|
size="small"
|
||||||
|
color={chunk.available_int === 1 ? 'success' : 'default'}
|
||||||
|
/>
|
||||||
|
{chunk.image_id && (
|
||||||
|
<Chip
|
||||||
|
label="包含图片"
|
||||||
|
size="small"
|
||||||
|
color="info"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
maxHeight: '200px',
|
||||||
|
overflow: 'auto',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chunk.content_with_weight || '无内容'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{chunk.important_kwd && chunk.important_kwd.length > 0 && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
|
重要关键词:
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||||
|
{chunk.important_kwd.map((keyword, kwdIndex) => (
|
||||||
|
<Chip
|
||||||
|
key={kwdIndex}
|
||||||
|
label={keyword}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{chunk.question_kwd && chunk.question_kwd.length > 0 && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
|
问题关键词:
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||||
|
{chunk.question_kwd.map((keyword, kwdIndex) => (
|
||||||
|
<Chip
|
||||||
|
key={kwdIndex}
|
||||||
|
label={keyword}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{chunk.tag_kwd && chunk.tag_kwd.length > 0 && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
|
标签关键词:
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||||
|
{chunk.tag_kwd.map((keyword, kwdIndex) => (
|
||||||
|
<Chip
|
||||||
|
key={kwdIndex}
|
||||||
|
label={keyword}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="info"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{chunk.positions && chunk.positions.length > 0 && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
位置信息: {chunk.positions.length} 个位置点
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* 分页控件 */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
|
||||||
|
<Pagination
|
||||||
|
count={totalPages}
|
||||||
|
page={page}
|
||||||
|
onChange={(_, newPage) => onPageChange(newPage)}
|
||||||
|
color="primary"
|
||||||
|
showFirstButton
|
||||||
|
showLastButton
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChunkListResult;
|
||||||
269
src/pages/chunk/parsed-result.tsx
Normal file
269
src/pages/chunk/parsed-result.tsx
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Breadcrumbs,
|
||||||
|
Link,
|
||||||
|
TextField,
|
||||||
|
InputAdornment,
|
||||||
|
Paper,
|
||||||
|
Alert,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardMedia
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Search as SearchIcon, ArrowBack as ArrowBackIcon } from '@mui/icons-material';
|
||||||
|
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 { IDocumentInfo } from '@/interfaces/database/document';
|
||||||
|
|
||||||
|
function ChunkParsedResult() {
|
||||||
|
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 [documentFile, setDocumentFile] = useState<Blob | null>(null);
|
||||||
|
const [fileUrl, setFileUrl] = useState<string>('');
|
||||||
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
|
|
||||||
|
// 使用chunk列表hook
|
||||||
|
const {
|
||||||
|
chunks,
|
||||||
|
total,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
setCurrentPage,
|
||||||
|
setKeywords,
|
||||||
|
refresh
|
||||||
|
} = useChunkList(doc_id || '', {
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
keywords: searchKeyword
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取知识库和文档信息
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
if (!kb_id || !doc_id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取知识库信息
|
||||||
|
const kbResponse = await knowledgeService.getKnowledgeDetail({ kb_id });
|
||||||
|
if (kbResponse.data.code === 0) {
|
||||||
|
setKnowledgeBase(kbResponse.data.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文档信息
|
||||||
|
const docResponse = await knowledgeService.getDocumentInfos({ doc_ids: [doc_id] });
|
||||||
|
if (docResponse.data.code === 0) {
|
||||||
|
const docArr: IKnowledgeFile[] = docResponse.data.data;
|
||||||
|
if (docArr.length > 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) {
|
||||||
|
console.error('Failed to fetch data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = (keyword: string) => {
|
||||||
|
setSearchKeyword(keyword);
|
||||||
|
setKeywords(keyword);
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
// 清理函数,释放URL对象
|
||||||
|
return () => {
|
||||||
|
if (fileUrl) {
|
||||||
|
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) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<Alert severity="error">
|
||||||
|
缺少必要的参数:知识库ID或文档ID
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
{/* 面包屑导航 */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Link
|
||||||
|
color="inherit"
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate('/knowledge');
|
||||||
|
}}
|
||||||
|
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
|
||||||
|
>
|
||||||
|
知识库列表
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
color="inherit"
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(`/knowledge/${kb_id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{knowledgeBase?.name || '知识库详情'}
|
||||||
|
</Link>
|
||||||
|
<Typography color="text.primary">
|
||||||
|
{document?.name || '文档Chunk详情'}
|
||||||
|
</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
文档Chunk解析结果
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
查看文档 "{document?.name}" 的所有chunk数据
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* 文件预览 */}
|
||||||
|
{renderFilePreview()}
|
||||||
|
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
placeholder="搜索chunk内容..."
|
||||||
|
value={searchKeyword}
|
||||||
|
// onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Chunk列表结果 */}
|
||||||
|
<ChunkListResult
|
||||||
|
chunks={chunks}
|
||||||
|
total={total}
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
page={currentPage}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
docName={document?.name}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChunkParsedResult;
|
||||||
684
src/pages/knowledge/components/DocumentListComponent.tsx
Normal file
684
src/pages/knowledge/components/DocumentListComponent.tsx
Normal file
@@ -0,0 +1,684 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
Chip,
|
||||||
|
IconButton,
|
||||||
|
TextField,
|
||||||
|
InputAdornment,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Tooltip,
|
||||||
|
Button,
|
||||||
|
Stack,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
OutlinedInput,
|
||||||
|
type SelectChangeEvent,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
LinearProgress,
|
||||||
|
CircularProgress,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
Search as SearchIcon,
|
||||||
|
FileUpload as FileUploadIcon,
|
||||||
|
Delete as DeleteIcon,
|
||||||
|
PlayArrow as PlayIcon,
|
||||||
|
Stop as StopIcon,
|
||||||
|
Edit as EditIcon,
|
||||||
|
Visibility as ViewIcon,
|
||||||
|
CheckCircle as EnableIcon,
|
||||||
|
Cancel as DisableIcon,
|
||||||
|
Clear as ClearIcon,
|
||||||
|
MoreVert as MoreVertIcon,
|
||||||
|
PictureAsPdf as PdfIcon,
|
||||||
|
Description as DocIcon,
|
||||||
|
Image as ImageIcon,
|
||||||
|
VideoFile as VideoIcon,
|
||||||
|
AudioFile as AudioIcon,
|
||||||
|
Refresh as RefreshIcon,
|
||||||
|
InsertDriveFile as FileIcon,
|
||||||
|
Upload as UploadIcon,
|
||||||
|
BugReportOutlined as ProcessIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import { DataGrid, type GridColDef, type GridRowSelectionModel } from '@mui/x-data-grid';
|
||||||
|
import { zhCN, enUS } from '@mui/x-data-grid/locales';
|
||||||
|
import type { IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||||
|
import type { IDocumentInfoFilter } from '@/interfaces/database/document';
|
||||||
|
import { RUNNING_STATUS_KEYS, type RunningStatus } from '@/constants/knowledge';
|
||||||
|
import { LanguageAbbreviation } from '@/locales';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
|
||||||
|
interface DocumentListComponentProps {
|
||||||
|
files: IKnowledgeFile[];
|
||||||
|
loading: boolean;
|
||||||
|
searchKeyword: string;
|
||||||
|
onSearchChange: (keyword: string) => void;
|
||||||
|
onReparse: (fileIds: string[]) => void;
|
||||||
|
onDelete: (fileIds: string[]) => void;
|
||||||
|
onUpload: () => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
rowSelectionModel: GridRowSelectionModel;
|
||||||
|
onRowSelectionModelChange: (model: GridRowSelectionModel) => void;
|
||||||
|
// 分页相关props
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
onPageSizeChange: (pageSize: number) => void;
|
||||||
|
// 筛选器相关props
|
||||||
|
documentFilter?: IDocumentInfoFilter;
|
||||||
|
onFetchFilter?: () => Promise<IDocumentInfoFilter | undefined>;
|
||||||
|
// 新增操作功能props
|
||||||
|
onRename: (fileId: string, newName: string) => void;
|
||||||
|
onChangeStatus: (fileIds: string[], status: string) => void;
|
||||||
|
onCancelRun: (fileIds: string[]) => void;
|
||||||
|
onViewDetails?: (file: IKnowledgeFile) => void;
|
||||||
|
onViewProcessDetails?: (file: IKnowledgeFile) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRunStatusLabel = (status: string) => {
|
||||||
|
const statusLabels = {
|
||||||
|
[RUNNING_STATUS_KEYS.UNSTART]: '未开始',
|
||||||
|
[RUNNING_STATUS_KEYS.RUNNING]: '运行中',
|
||||||
|
[RUNNING_STATUS_KEYS.CANCEL]: '已取消',
|
||||||
|
[RUNNING_STATUS_KEYS.DONE]: '完成',
|
||||||
|
[RUNNING_STATUS_KEYS.FAIL]: '失败',
|
||||||
|
};
|
||||||
|
return statusLabels[status as keyof typeof statusLabels] || '未知';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileIcon = (type: string, suffix?: string) => {
|
||||||
|
const fileTypeArr = ['pdf', 'doc', 'docx', 'jpg', 'jpeg', 'png', 'gif', 'mp4', 'avi', 'mov', 'mp3', 'wav'];
|
||||||
|
const index = fileTypeArr.indexOf(type.toLowerCase());
|
||||||
|
if (index === -1) {
|
||||||
|
// type 找不到就用 suffix
|
||||||
|
const suffixIndex = fileTypeArr.indexOf(suffix?.toLowerCase() || '');
|
||||||
|
if (suffixIndex !== -1) {
|
||||||
|
return getFileIcon(fileTypeArr[suffixIndex]);
|
||||||
|
}
|
||||||
|
return <FileIcon />;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case 'pdf': return <PdfIcon color="error" />;
|
||||||
|
case 'doc':
|
||||||
|
case 'docx': return <DocIcon color="primary" />;
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'png':
|
||||||
|
case 'gif': return <ImageIcon color="success" />;
|
||||||
|
case 'mp4':
|
||||||
|
case 'avi':
|
||||||
|
case 'mov': return <VideoIcon color="secondary" />;
|
||||||
|
case 'mp3':
|
||||||
|
case 'wav': return <AudioIcon color="warning" />;
|
||||||
|
default: return <FileIcon />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusChip = (status: string) => {
|
||||||
|
return <Chip label={status === '1' ? '启用' : '禁用'}
|
||||||
|
color={status === '1' ? 'success' : 'error'}
|
||||||
|
size="small" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRunStatusChip = (run: RunningStatus, progress: number) => {
|
||||||
|
const statusConfig = {
|
||||||
|
[RUNNING_STATUS_KEYS.UNSTART]: { label: '未开始', color: 'default' as const },
|
||||||
|
[RUNNING_STATUS_KEYS.RUNNING]: { label: `解析中`, color: 'info' as const },
|
||||||
|
[RUNNING_STATUS_KEYS.CANCEL]: { label: '已取消', color: 'warning' as const },
|
||||||
|
[RUNNING_STATUS_KEYS.DONE]: { label: '完成', color: 'success' as const },
|
||||||
|
[RUNNING_STATUS_KEYS.FAIL]: { label: '失败', color: 'error' as const },
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[run] || { label: '未知', color: 'default' as const };
|
||||||
|
|
||||||
|
if (run === RUNNING_STATUS_KEYS.RUNNING) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<CircularProgress size={16} />
|
||||||
|
<Box sx={{ minWidth: 80 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{progress}%
|
||||||
|
</Typography>
|
||||||
|
<LinearProgress variant="determinate" value={progress} sx={{ height: 4, borderRadius: 2 }} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Chip label={config.label} color={config.color} size="small" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
||||||
|
files,
|
||||||
|
loading,
|
||||||
|
searchKeyword,
|
||||||
|
onSearchChange,
|
||||||
|
onReparse,
|
||||||
|
onDelete,
|
||||||
|
onUpload,
|
||||||
|
onRefresh,
|
||||||
|
rowSelectionModel,
|
||||||
|
onRowSelectionModelChange,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
onPageChange,
|
||||||
|
onPageSizeChange,
|
||||||
|
documentFilter,
|
||||||
|
onFetchFilter,
|
||||||
|
onRename,
|
||||||
|
onChangeStatus,
|
||||||
|
onCancelRun,
|
||||||
|
onViewDetails,
|
||||||
|
onViewProcessDetails
|
||||||
|
}) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
|
// 菜单出现的时候选中的 file; selectedFileId 会清空, ref用于上一次的保存状态
|
||||||
|
const selectedFileIdRef = useRef<string>('');
|
||||||
|
const selectedFileRef = useRef<IKnowledgeFile | undefined>(undefined);
|
||||||
|
|
||||||
|
const [selectedFileId, setSelectedFileId] = useState<string>('');
|
||||||
|
const [selectedFile, setSelectedFile] = useState<IKnowledgeFile | undefined>(undefined);
|
||||||
|
|
||||||
|
const [filter, setFilter] = useState<IDocumentInfoFilter | undefined>(documentFilter);
|
||||||
|
const [selectedRunStatus, setSelectedRunStatus] = useState<string[]>([]);
|
||||||
|
const [selectedSuffix, setSelectedSuffix] = useState<string[]>([]);
|
||||||
|
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
||||||
|
const [newFileName, setNewFileName] = useState('');
|
||||||
|
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
// 根据当前语言获取DataGrid的localeText
|
||||||
|
const getDataGridLocale = () => {
|
||||||
|
const currentLanguage = i18n.language;
|
||||||
|
return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取筛选器数据
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchFilter = async () => {
|
||||||
|
try {
|
||||||
|
const filterData = await onFetchFilter?.();
|
||||||
|
setFilter(filterData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch document filter:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!filter) {
|
||||||
|
fetchFilter();
|
||||||
|
}
|
||||||
|
}, [onFetchFilter, filter]);
|
||||||
|
|
||||||
|
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, file: IKnowledgeFile) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
setSelectedFileId(file.id);
|
||||||
|
setSelectedFile(file);
|
||||||
|
selectedFileIdRef.current = file.id;
|
||||||
|
selectedFileRef.current = file;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setSelectedFileId('');
|
||||||
|
setSelectedFile(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReparse = () => {
|
||||||
|
if (selectedFileId) {
|
||||||
|
onReparse([selectedFileId]);
|
||||||
|
}
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (selectedFileId) {
|
||||||
|
onDelete([selectedFileId]);
|
||||||
|
}
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = () => {
|
||||||
|
if (selectedFileRef.current) {
|
||||||
|
setNewFileName(selectedFileRef.current.name);
|
||||||
|
setRenameDialogOpen(true);
|
||||||
|
}
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRenameConfirm = () => {
|
||||||
|
console.log('selectedFileId', selectedFileIdRef.current);
|
||||||
|
console.log('newFileName', newFileName);
|
||||||
|
|
||||||
|
if (selectedFileIdRef.current && newFileName.trim()) {
|
||||||
|
onRename(selectedFileIdRef.current, newFileName.trim());
|
||||||
|
setRenameDialogOpen(false);
|
||||||
|
setNewFileName('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeStatus = (status: string) => {
|
||||||
|
if (selectedFileId) {
|
||||||
|
onChangeStatus([selectedFileId], status);
|
||||||
|
}
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelRun = () => {
|
||||||
|
if (selectedFileId) {
|
||||||
|
onCancelRun([selectedFileId]);
|
||||||
|
}
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewProcessDetails = (file?: IKnowledgeFile) => {
|
||||||
|
if (file && onViewProcessDetails) {
|
||||||
|
onViewProcessDetails?.(file);
|
||||||
|
}
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewDetails = (file?: IKnowledgeFile) => {
|
||||||
|
if (file && onViewDetails) {
|
||||||
|
onViewDetails?.(file);
|
||||||
|
}
|
||||||
|
handleMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRunStatusChange = (event: SelectChangeEvent<string[]>) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setSelectedRunStatus(typeof value === 'string' ? value.split(',') : value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuffixChange = (event: SelectChangeEvent<string[]>) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setSelectedSuffix(typeof value === 'string' ? value.split(',') : value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理分页变化
|
||||||
|
const handlePaginationModelChange = (model: { page: number; pageSize: number }) => {
|
||||||
|
if (model.page !== page - 1) { // DataGrid的page是0-based,我们的是1-based
|
||||||
|
onPageChange(model.page + 1);
|
||||||
|
}
|
||||||
|
if (model.pageSize !== pageSize) {
|
||||||
|
onPageSizeChange(model.pageSize);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DataGrid 列定义
|
||||||
|
const columns: GridColDef[] = [
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
headerName: '文件名',
|
||||||
|
flex: 2,
|
||||||
|
minWidth: 200,
|
||||||
|
cellClassName: 'grid-center-cell',
|
||||||
|
renderCell: (params) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
":hover": { color: 'primary.main', fontWeight: 'bold' }
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('查看详情:', params.row);
|
||||||
|
if (onViewDetails) {
|
||||||
|
onViewDetails(params.row);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title={`查看 ${params.value} 的详情`}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
{getFileIcon(params.row.type, params.row.suffix)}
|
||||||
|
<Typography variant="body2" noWrap>{params.value}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
headerName: '类型',
|
||||||
|
flex: 0.5,
|
||||||
|
minWidth: 80,
|
||||||
|
renderCell: (params) => (
|
||||||
|
<Chip label={params.value.toUpperCase()} size="small" variant="outlined" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'size',
|
||||||
|
headerName: '大小',
|
||||||
|
flex: 0.5,
|
||||||
|
minWidth: 80,
|
||||||
|
renderCell: (params) => formatFileSize(params.value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'chunk_num',
|
||||||
|
headerName: '分块数',
|
||||||
|
flex: 0.5,
|
||||||
|
minWidth: 80,
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
headerName: '状态',
|
||||||
|
flex: 0.8,
|
||||||
|
minWidth: 100,
|
||||||
|
renderCell: (params) => getStatusChip(params.value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'run',
|
||||||
|
headerName: '解析状态',
|
||||||
|
flex: 0.8,
|
||||||
|
minWidth: 100,
|
||||||
|
renderCell: (params) => getRunStatusChip(params.value, params.row.progress),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'create_time',
|
||||||
|
headerName: '上传时间',
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 140,
|
||||||
|
valueFormatter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
headerName: '操作',
|
||||||
|
flex: 1.2,
|
||||||
|
minWidth: 200,
|
||||||
|
sortable: false,
|
||||||
|
cellClassName: 'grid-center-cell',
|
||||||
|
renderCell: (params) => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: 1,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'action.hover',
|
||||||
|
color: 'primary.main'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('查看详情:', params.row);
|
||||||
|
handleViewDetails(params.row);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ViewIcon fontSize="small" />
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: 1,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'action.hover',
|
||||||
|
color: 'primary.main'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('查看解析详情:', params.row);
|
||||||
|
handleViewProcessDetails(params.row);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProcessIcon fontSize="small" />
|
||||||
|
</Box>
|
||||||
|
<Tooltip title="更多操作">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => handleMenuClick(e, params.row)}
|
||||||
|
>
|
||||||
|
<MoreVertIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 筛选器 */}
|
||||||
|
{filter && (
|
||||||
|
<Paper sx={{ p: 2, mb: 2 }}>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2 }}>筛选器</Typography>
|
||||||
|
<Stack direction="row" spacing={2} alignItems="center">
|
||||||
|
{/* 运行状态筛选 */}
|
||||||
|
<FormControl sx={{ minWidth: 200 }}>
|
||||||
|
<InputLabel>运行状态</InputLabel>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
value={selectedRunStatus}
|
||||||
|
onChange={handleRunStatusChange}
|
||||||
|
input={<OutlinedInput label="运行状态" />}
|
||||||
|
renderValue={(selected) => (
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||||
|
{selected.map((value) => (
|
||||||
|
<Chip key={value} label={getRunStatusLabel(value)} size="small" />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Object.entries(filter.run_status).map(([status, count]) => (
|
||||||
|
<MenuItem key={status} value={status}>
|
||||||
|
{getRunStatusLabel(status)} ({count})
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{/* 文件类型筛选 */}
|
||||||
|
<FormControl sx={{ minWidth: 200 }}>
|
||||||
|
<InputLabel>文件类型</InputLabel>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
value={selectedSuffix}
|
||||||
|
onChange={handleSuffixChange}
|
||||||
|
input={<OutlinedInput label="文件类型" />}
|
||||||
|
renderValue={(selected) => (
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||||
|
{selected.map((value) => (
|
||||||
|
<Chip key={value} label={value.toUpperCase()} size="small" />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Object.entries(filter.suffix).map(([suffix, count]) => (
|
||||||
|
<MenuItem key={suffix} value={suffix}>
|
||||||
|
{suffix.toUpperCase()} ({count})
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button variant="outlined" onClick={() => {
|
||||||
|
setSelectedRunStatus([]);
|
||||||
|
setSelectedSuffix([]);
|
||||||
|
}}>
|
||||||
|
清除筛选
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 文件操作栏 */}
|
||||||
|
<Paper sx={{ p: 2, mb: 2 }}>
|
||||||
|
<Stack direction="row" spacing={2} alignItems="center" justifyContent="space-between">
|
||||||
|
<TextField
|
||||||
|
placeholder="搜索文件..."
|
||||||
|
value={searchKeyword}
|
||||||
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
sx={{ minWidth: 300 }}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<UploadIcon />}
|
||||||
|
onClick={onUpload}
|
||||||
|
>
|
||||||
|
上传文件
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{rowSelectionModel.ids.size > 0 && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
onClick={() => onReparse(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
|
||||||
|
>
|
||||||
|
重新解析 ({rowSelectionModel.ids.size})
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
onClick={() => onDelete(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
|
||||||
|
>
|
||||||
|
删除 ({rowSelectionModel.ids.size})
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IconButton onClick={onRefresh}>
|
||||||
|
<RefreshIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* 文件列表 */}
|
||||||
|
<Paper sx={{ height: 600, width: '100%' }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={files}
|
||||||
|
columns={columns}
|
||||||
|
loading={loading}
|
||||||
|
checkboxSelection
|
||||||
|
disableRowSelectionOnClick
|
||||||
|
rowSelectionModel={rowSelectionModel}
|
||||||
|
onRowSelectionModelChange={onRowSelectionModelChange}
|
||||||
|
pageSizeOptions={[10, 25, 50, 100]}
|
||||||
|
paginationMode="server"
|
||||||
|
rowCount={total}
|
||||||
|
paginationModel={{
|
||||||
|
page: page - 1,
|
||||||
|
pageSize: pageSize,
|
||||||
|
}}
|
||||||
|
onPaginationModelChange={handlePaginationModelChange}
|
||||||
|
localeText={getDataGridLocale().components.MuiDataGrid.defaultProps.localeText}
|
||||||
|
sx={{
|
||||||
|
'& .MuiDataGrid-cell:focus': {
|
||||||
|
outline: 'none',
|
||||||
|
},
|
||||||
|
'& .MuiDataGrid-row:hover': {
|
||||||
|
backgroundColor: 'action.hover',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* 右键菜单 */}
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={() => handleViewDetails(selectedFile)}>
|
||||||
|
<ViewIcon sx={{ mr: 1 }} />
|
||||||
|
<Typography>查看详情</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleRename}>
|
||||||
|
<EditIcon sx={{ mr: 1 }} />
|
||||||
|
<Typography>重命名</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleReparse}>
|
||||||
|
<PlayIcon sx={{ mr: 1 }} />
|
||||||
|
<Typography>重新解析</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
{selectedFile?.run === RUNNING_STATUS_KEYS.RUNNING && (
|
||||||
|
<MenuItem onClick={handleCancelRun}>
|
||||||
|
<StopIcon sx={{ mr: 1 }} />
|
||||||
|
<Typography>取消运行</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{selectedFile?.status === '1' ? (
|
||||||
|
<MenuItem onClick={() => handleChangeStatus('0')}>
|
||||||
|
<DisableIcon sx={{ mr: 1 }} />
|
||||||
|
<Typography>禁用</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
|
<MenuItem onClick={() => handleChangeStatus('1')}>
|
||||||
|
<EnableIcon sx={{ mr: 1 }} />
|
||||||
|
<Typography>启用</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem onClick={handleDelete}>
|
||||||
|
<DeleteIcon sx={{ mr: 1 }} color="error" />
|
||||||
|
<Typography color="error">删除</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
{/* 重命名对话框 */}
|
||||||
|
<Dialog open={renameDialogOpen} onClose={() => setRenameDialogOpen(false)}>
|
||||||
|
<DialogTitle>重命名文件</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label="文件名"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={newFileName}
|
||||||
|
onChange={(e) => setNewFileName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setRenameDialogOpen(false)}>取消</Button>
|
||||||
|
<Button onClick={handleRenameConfirm} variant="contained">确认</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentListComponent;
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Paper,
|
|
||||||
Chip,
|
|
||||||
IconButton,
|
|
||||||
TextField,
|
|
||||||
InputAdornment,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Tooltip,
|
|
||||||
Button,
|
|
||||||
Stack,
|
|
||||||
} from '@mui/material';
|
|
||||||
import {
|
|
||||||
Search as SearchIcon,
|
|
||||||
MoreVert as MoreVertIcon,
|
|
||||||
InsertDriveFile as FileIcon,
|
|
||||||
PictureAsPdf as PdfIcon,
|
|
||||||
Description as DocIcon,
|
|
||||||
Image as ImageIcon,
|
|
||||||
VideoFile as VideoIcon,
|
|
||||||
AudioFile as AudioIcon,
|
|
||||||
Upload as UploadIcon,
|
|
||||||
Refresh as RefreshIcon,
|
|
||||||
Delete as DeleteIcon,
|
|
||||||
} from '@mui/icons-material';
|
|
||||||
import { DataGrid, type GridColDef, type GridRowSelectionModel } from '@mui/x-data-grid';
|
|
||||||
import { zhCN, enUS } from '@mui/x-data-grid/locales';
|
|
||||||
import type { IKnowledgeFile } from '@/interfaces/database/knowledge';
|
|
||||||
import { RUNNING_STATUS_KEYS, type RunningStatus } from '@/constants/knowledge';
|
|
||||||
import { LanguageAbbreviation } from '@/locales';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
|
|
||||||
interface FileListComponentProps {
|
|
||||||
files: IKnowledgeFile[];
|
|
||||||
loading: boolean;
|
|
||||||
searchKeyword: string;
|
|
||||||
onSearchChange: (keyword: string) => void;
|
|
||||||
onReparse: (fileIds: string[]) => void;
|
|
||||||
onDelete: (fileIds: string[]) => void;
|
|
||||||
onUpload: () => void;
|
|
||||||
onRefresh: () => void;
|
|
||||||
rowSelectionModel: GridRowSelectionModel;
|
|
||||||
onRowSelectionModelChange: (model: GridRowSelectionModel) => void;
|
|
||||||
// 分页相关props
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
onPageChange: (page: number) => void;
|
|
||||||
onPageSizeChange: (pageSize: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFileIcon = (type: string) => {
|
|
||||||
switch (type.toLowerCase()) {
|
|
||||||
case 'pdf': return <PdfIcon color="error" />;
|
|
||||||
case 'doc':
|
|
||||||
case 'docx': return <DocIcon color="primary" />;
|
|
||||||
case 'jpg':
|
|
||||||
case 'jpeg':
|
|
||||||
case 'png':
|
|
||||||
case 'gif': return <ImageIcon color="success" />;
|
|
||||||
case 'mp4':
|
|
||||||
case 'avi':
|
|
||||||
case 'mov': return <VideoIcon color="secondary" />;
|
|
||||||
case 'mp3':
|
|
||||||
case 'wav': return <AudioIcon color="warning" />;
|
|
||||||
default: return <FileIcon />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
|
||||||
if (bytes === 0) return '0 Bytes';
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusChip = (status: string) => {
|
|
||||||
return <Chip label={status === '1' ? '启用' : '禁用'}
|
|
||||||
color={status === '1' ? 'success' : 'error'}
|
|
||||||
size="small" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRunStatusChip = (run: RunningStatus, progress: number) => {
|
|
||||||
const statusConfig = {
|
|
||||||
[RUNNING_STATUS_KEYS.UNSTART]: { label: '未开始', color: 'default' as const },
|
|
||||||
[RUNNING_STATUS_KEYS.RUNNING]: { label: `解析中 ${progress}%`, color: 'info' as const },
|
|
||||||
[RUNNING_STATUS_KEYS.CANCEL]: { label: '已取消', color: 'warning' as const },
|
|
||||||
[RUNNING_STATUS_KEYS.DONE]: { label: '完成', color: 'success' as const },
|
|
||||||
[RUNNING_STATUS_KEYS.FAIL]: { label: '失败', color: 'error' as const },
|
|
||||||
};
|
|
||||||
|
|
||||||
const config = statusConfig[run] || { label: '未知', color: 'default' as const };
|
|
||||||
return <Chip label={config.label} color={config.color} size="small" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FileListComponent: React.FC<FileListComponentProps> = ({
|
|
||||||
files,
|
|
||||||
loading,
|
|
||||||
searchKeyword,
|
|
||||||
onSearchChange,
|
|
||||||
onReparse,
|
|
||||||
onDelete,
|
|
||||||
onUpload,
|
|
||||||
onRefresh,
|
|
||||||
rowSelectionModel,
|
|
||||||
onRowSelectionModelChange,
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
onPageChange,
|
|
||||||
onPageSizeChange,
|
|
||||||
}) => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
||||||
const [selectedFileId, setSelectedFileId] = useState<string>('');
|
|
||||||
|
|
||||||
const { i18n } = useTranslation();
|
|
||||||
|
|
||||||
// 根据当前语言获取DataGrid的localeText
|
|
||||||
const getDataGridLocale = () => {
|
|
||||||
const currentLanguage = i18n.language;
|
|
||||||
return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, fileId: string) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
setSelectedFileId(fileId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMenuClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
setSelectedFileId('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReparse = () => {
|
|
||||||
if (selectedFileId) {
|
|
||||||
onReparse([selectedFileId]);
|
|
||||||
}
|
|
||||||
handleMenuClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
if (selectedFileId) {
|
|
||||||
onDelete([selectedFileId]);
|
|
||||||
}
|
|
||||||
handleMenuClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理分页变化
|
|
||||||
const handlePaginationModelChange = (model: { page: number; pageSize: number }) => {
|
|
||||||
if (model.page !== page - 1) { // DataGrid的page是0-based,我们的是1-based
|
|
||||||
onPageChange(model.page + 1);
|
|
||||||
}
|
|
||||||
if (model.pageSize !== pageSize) {
|
|
||||||
onPageSizeChange(model.pageSize);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// DataGrid 列定义
|
|
||||||
const columns: GridColDef[] = [
|
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
headerName: '文件名',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 100,
|
|
||||||
maxWidth: 300,
|
|
||||||
cellClassName: 'grid-center-cell',
|
|
||||||
renderCell: (params) => (
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
{getFileIcon(params.row.type)}
|
|
||||||
<Tooltip title={params.value}>
|
|
||||||
<Typography variant="body2">{params.value}</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'type',
|
|
||||||
headerName: '类型',
|
|
||||||
width: 100,
|
|
||||||
renderCell: (params) => (
|
|
||||||
<Chip label={params.value.toUpperCase()} size="small" variant="outlined" />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'size',
|
|
||||||
headerName: '大小',
|
|
||||||
width: 120,
|
|
||||||
renderCell: (params) => formatFileSize(params.value),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'chunk_num',
|
|
||||||
headerName: '分块数',
|
|
||||||
width: 100,
|
|
||||||
type: 'number',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'status',
|
|
||||||
headerName: '状态',
|
|
||||||
width: 120,
|
|
||||||
renderCell: (params) => getStatusChip(params.value),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'run',
|
|
||||||
headerName: '解析状态',
|
|
||||||
width: 120,
|
|
||||||
renderCell: (params) => getRunStatusChip(params.value, params.row.progress),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'create_time',
|
|
||||||
headerName: '上传时间',
|
|
||||||
width: 160,
|
|
||||||
valueFormatter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'actions',
|
|
||||||
headerName: '操作',
|
|
||||||
width: 80,
|
|
||||||
sortable: false,
|
|
||||||
renderCell: (params) => (
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={(e) => handleMenuClick(e, params.row.id)}
|
|
||||||
>
|
|
||||||
<MoreVertIcon />
|
|
||||||
</IconButton>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
{/* 文件操作栏 */}
|
|
||||||
<Paper sx={{ p: 2, mb: 2 }}>
|
|
||||||
<Stack direction="row" spacing={2} alignItems="center" justifyContent="space-between">
|
|
||||||
<TextField
|
|
||||||
placeholder="搜索文件..."
|
|
||||||
value={searchKeyword}
|
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
|
||||||
InputProps={{
|
|
||||||
startAdornment: (
|
|
||||||
<InputAdornment position="start">
|
|
||||||
<SearchIcon />
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
sx={{ minWidth: 300 }}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Stack direction="row" spacing={1}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
startIcon={<UploadIcon />}
|
|
||||||
onClick={onUpload}
|
|
||||||
>
|
|
||||||
上传文件
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{rowSelectionModel.ids.size > 0 && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<RefreshIcon />}
|
|
||||||
onClick={() => onReparse(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
|
|
||||||
>
|
|
||||||
重新解析 ({rowSelectionModel.ids.size})
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
startIcon={<DeleteIcon />}
|
|
||||||
onClick={() => onDelete(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
|
|
||||||
>
|
|
||||||
删除 ({rowSelectionModel.ids.size})
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<IconButton onClick={onRefresh}>
|
|
||||||
<RefreshIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* 文件列表 */}
|
|
||||||
<Paper sx={{ height: 600, width: '100%' }}>
|
|
||||||
<DataGrid
|
|
||||||
rows={files}
|
|
||||||
columns={columns}
|
|
||||||
loading={loading}
|
|
||||||
checkboxSelection
|
|
||||||
disableRowSelectionOnClick
|
|
||||||
rowSelectionModel={rowSelectionModel}
|
|
||||||
onRowSelectionModelChange={onRowSelectionModelChange}
|
|
||||||
pageSizeOptions={[10, 25, 50, 100]}
|
|
||||||
paginationMode="server"
|
|
||||||
rowCount={total}
|
|
||||||
paginationModel={{
|
|
||||||
page: page - 1,
|
|
||||||
pageSize: pageSize,
|
|
||||||
}}
|
|
||||||
onPaginationModelChange={handlePaginationModelChange}
|
|
||||||
localeText={getDataGridLocale().components.MuiDataGrid.defaultProps.localeText}
|
|
||||||
sx={{
|
|
||||||
'& .MuiDataGrid-cell:focus': {
|
|
||||||
outline: 'none',
|
|
||||||
},
|
|
||||||
'& .MuiDataGrid-row:hover': {
|
|
||||||
backgroundColor: 'action.hover',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* 右边菜单 */}
|
|
||||||
<Menu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
open={Boolean(anchorEl)}
|
|
||||||
onClose={handleMenuClose}
|
|
||||||
>
|
|
||||||
<MenuItem onClick={handleReparse}>
|
|
||||||
<Typography>重新解析</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={handleDelete}>
|
|
||||||
<Typography color="error">删除</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileListComponent;
|
|
||||||
@@ -29,7 +29,8 @@ interface TestChunkResultProps {
|
|||||||
selectedDocIds: string[];
|
selectedDocIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function TestChunkResult({ result, page, pageSize, onDocumentFilter, selectedDocIds }: TestChunkResultProps) {
|
function TestChunkResult(props: TestChunkResultProps) {
|
||||||
|
const { result, loading, page, pageSize, onDocumentFilter, selectedDocIds } = props;
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -14,16 +14,22 @@ import {
|
|||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Link,
|
Link,
|
||||||
Stack,
|
Stack,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Divider,
|
||||||
|
Chip,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { type GridRowSelectionModel } from '@mui/x-data-grid';
|
import { type GridRowSelectionModel } from '@mui/x-data-grid';
|
||||||
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 { IDocumentInfoFilter } from '@/interfaces/database/document';
|
||||||
import FileUploadDialog from '@/components/FileUploadDialog';
|
import FileUploadDialog from '@/components/FileUploadDialog';
|
||||||
import KnowledgeInfoCard from './components/KnowledgeInfoCard';
|
import KnowledgeInfoCard from './components/KnowledgeInfoCard';
|
||||||
import FileListComponent from './components/FileListComponent';
|
import DocumentListComponent from './components/DocumentListComponent';
|
||||||
import FloatingActionButtons from './components/FloatingActionButtons';
|
import FloatingActionButtons from './components/FloatingActionButtons';
|
||||||
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
|
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
|
||||||
import { useDocumentList, useDocumentOperations } from '@/hooks/document-hooks';
|
import { useDocumentList, useDocumentOperations } from '@/hooks/document-hooks';
|
||||||
|
import { RUNNING_STATUS_KEYS } from '@/constants/knowledge';
|
||||||
|
|
||||||
function KnowledgeBaseDetail() {
|
function KnowledgeBaseDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@@ -42,6 +48,12 @@ function KnowledgeBaseDetail() {
|
|||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [testingDialogOpen, setTestingDialogOpen] = useState(false);
|
const [testingDialogOpen, setTestingDialogOpen] = useState(false);
|
||||||
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
||||||
|
const [processDetailsDialogOpen, setProcessDetailsDialogOpen] = useState(false);
|
||||||
|
const [selectedFileDetails, setSelectedFileDetails] = useState<IKnowledgeFile | null>(null);
|
||||||
|
|
||||||
|
// 轮询相关状态
|
||||||
|
const pollingIntervalRef = useRef<any>(null);
|
||||||
|
const [isPolling, setIsPolling] = useState(false);
|
||||||
|
|
||||||
// 使用新的document hooks
|
// 使用新的document hooks
|
||||||
const {
|
const {
|
||||||
@@ -61,6 +73,9 @@ function KnowledgeBaseDetail() {
|
|||||||
uploadDocuments,
|
uploadDocuments,
|
||||||
deleteDocuments,
|
deleteDocuments,
|
||||||
runDocuments,
|
runDocuments,
|
||||||
|
renameDocument,
|
||||||
|
changeDocumentStatus,
|
||||||
|
cancelRunDocuments,
|
||||||
loading: operationLoading,
|
loading: operationLoading,
|
||||||
error: operationError,
|
error: operationError,
|
||||||
} = useDocumentOperations();
|
} = useDocumentOperations();
|
||||||
@@ -119,11 +134,98 @@ function KnowledgeBaseDetail() {
|
|||||||
try {
|
try {
|
||||||
await runDocuments(docIds);
|
await runDocuments(docIds);
|
||||||
refreshFiles(); // 刷新列表
|
refreshFiles(); // 刷新列表
|
||||||
|
startPolling(); // 开始轮询
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.message || err.message || '重新解析失败');
|
setError(err.response?.data?.message || err.message || '重新解析失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 重命名文件
|
||||||
|
const handleRename = async (fileId: string, newName: string) => {
|
||||||
|
try {
|
||||||
|
await renameDocument(fileId, newName);
|
||||||
|
refreshFiles();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.response?.data?.message || err.message || '重命名失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更改文件状态
|
||||||
|
const handleChangeStatus = async (fileIds: string[], status: string) => {
|
||||||
|
try {
|
||||||
|
await changeDocumentStatus(fileIds, status);
|
||||||
|
refreshFiles();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.response?.data?.message || err.message || '更改状态失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消运行
|
||||||
|
const handleCancelRun = async (fileIds: string[]) => {
|
||||||
|
try {
|
||||||
|
await cancelRunDocuments(fileIds);
|
||||||
|
refreshFiles();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.response?.data?.message || err.message || '取消运行失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleViewDetails = (file: IKnowledgeFile) => {
|
||||||
|
console.log("查看详情:", file);
|
||||||
|
|
||||||
|
navigate(`/chunk/parsed-result?kb_id=${id}&doc_id=${file.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看解析详情
|
||||||
|
const handleViewProcessDetails = (file: IKnowledgeFile) => {
|
||||||
|
console.log("查看解析详情:", file);
|
||||||
|
setSelectedFileDetails(file);
|
||||||
|
setProcessDetailsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始轮询
|
||||||
|
const startPolling = () => {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPolling(true);
|
||||||
|
pollingIntervalRef.current = setInterval(() => {
|
||||||
|
refreshFiles();
|
||||||
|
}, 3000); // 每3秒轮询一次
|
||||||
|
};
|
||||||
|
|
||||||
|
// 停止轮询
|
||||||
|
const stopPolling = () => {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
setIsPolling(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查是否有运行中的文档
|
||||||
|
const hasRunningDocuments = files.some(file => file.run === RUNNING_STATUS_KEYS.RUNNING);
|
||||||
|
|
||||||
|
// 根据运行状态自动管理轮询
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasRunningDocuments && !isPolling) {
|
||||||
|
startPolling();
|
||||||
|
} else if (!hasRunningDocuments && isPolling) {
|
||||||
|
stopPolling();
|
||||||
|
}
|
||||||
|
}, [hasRunningDocuments, isPolling]);
|
||||||
|
|
||||||
|
// 组件卸载时清理轮询
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchKnowledgeDetail();
|
fetchKnowledgeDetail();
|
||||||
@@ -166,6 +268,10 @@ function KnowledgeBaseDetail() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchDocumentsFilter(): Promise<IDocumentInfoFilter | undefined> {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
{/* 面包屑导航 */}
|
{/* 面包屑导航 */}
|
||||||
@@ -175,7 +281,7 @@ function KnowledgeBaseDetail() {
|
|||||||
<KnowledgeInfoCard knowledgeBase={knowledgeBase} />
|
<KnowledgeInfoCard knowledgeBase={knowledgeBase} />
|
||||||
|
|
||||||
{/* 文件列表组件 */}
|
{/* 文件列表组件 */}
|
||||||
<FileListComponent
|
<DocumentListComponent
|
||||||
files={files}
|
files={files}
|
||||||
loading={filesLoading}
|
loading={filesLoading}
|
||||||
searchKeyword={searchKeyword}
|
searchKeyword={searchKeyword}
|
||||||
@@ -183,7 +289,6 @@ function KnowledgeBaseDetail() {
|
|||||||
onReparse={(fileIds) => handleReparse(fileIds)}
|
onReparse={(fileIds) => handleReparse(fileIds)}
|
||||||
onDelete={(fileIds) => {
|
onDelete={(fileIds) => {
|
||||||
console.log('删除文件:', fileIds);
|
console.log('删除文件:', fileIds);
|
||||||
|
|
||||||
setRowSelectionModel({
|
setRowSelectionModel({
|
||||||
type: 'include',
|
type: 'include',
|
||||||
ids: new Set(fileIds)
|
ids: new Set(fileIds)
|
||||||
@@ -205,6 +310,11 @@ function KnowledgeBaseDetail() {
|
|||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
onPageChange={setCurrentPage}
|
onPageChange={setCurrentPage}
|
||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
|
onRename={handleRename}
|
||||||
|
onChangeStatus={handleChangeStatus}
|
||||||
|
onCancelRun={handleCancelRun}
|
||||||
|
onViewDetails={handleViewDetails}
|
||||||
|
onViewProcessDetails={handleViewProcessDetails}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 浮动操作按钮 */}
|
{/* 浮动操作按钮 */}
|
||||||
@@ -319,6 +429,137 @@ function KnowledgeBaseDetail() {
|
|||||||
<Button variant="contained">保存</Button>
|
<Button variant="contained">保存</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 文档详情对话框 */}
|
||||||
|
<Dialog
|
||||||
|
open={processDetailsDialogOpen}
|
||||||
|
onClose={() => setProcessDetailsDialogOpen(false)}
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>文档处理详情</DialogTitle>
|
||||||
|
<DialogContent sx={{ p: 3 }}>
|
||||||
|
{selectedFileDetails && (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
{/* 基本信息卡片 */}
|
||||||
|
<Card variant="outlined">
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom color="primary">
|
||||||
|
基本信息
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
文件名
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||||
|
{selectedFileDetails.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
解析器ID
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{selectedFileDetails.parser_id || '未指定'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 处理状态卡片 */}
|
||||||
|
<Card variant="outlined">
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom color="primary">
|
||||||
|
处理状态
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
开始时间
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{selectedFileDetails.process_begin_at || '未开始'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
处理时长
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{selectedFileDetails.process_duration ? `${selectedFileDetails.process_duration}秒` : '未完成'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
进度
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Chip
|
||||||
|
label={`${100*(selectedFileDetails.progress || 0)}%`}
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={100*(selectedFileDetails.progress || 0)}
|
||||||
|
sx={{ flexGrow: 1, height: 8, borderRadius: 4 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 处理详情卡片 */}
|
||||||
|
{selectedFileDetails.progress_msg && (
|
||||||
|
<Card variant="outlined">
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom color="primary">
|
||||||
|
处理详情
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxHeight: 300,
|
||||||
|
overflow: 'auto',
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
borderRadius: 1,
|
||||||
|
p: 2,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'grey.200',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
component="pre"
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
margin: 0,
|
||||||
|
color: 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedFileDetails.progress_msg}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||||
|
<Button onClick={() => setProcessDetailsDialogOpen(false)} variant="contained">
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,12 @@ function KnowledgeBaseTesting() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 处理测试提交
|
// 处理测试提交
|
||||||
|
|
||||||
const handleTestSubmit = async (data: TestFormData) => {
|
const handleTestSubmit = async (data: TestFormData) => {
|
||||||
|
return handleTestSubmitFunc(data, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTestSubmitFunc = async (data: TestFormData, withSelectedDocs: boolean = false) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
setTesting(true);
|
setTesting(true);
|
||||||
@@ -127,14 +132,16 @@ function KnowledgeBaseTesting() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果有选择的文档,添加到请求中
|
// 如果有选择的文档,添加到请求中
|
||||||
if (data.doc_ids && data.doc_ids.length > 0) {
|
if (withSelectedDocs) {
|
||||||
requestBody.doc_ids = data.doc_ids;
|
const doc_ids = data.doc_ids || [];
|
||||||
|
if (doc_ids.length > 0) {
|
||||||
|
requestBody.doc_ids = doc_ids;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (selectedDocIds.length > 0) {
|
if (selectedDocIds.length > 0) {
|
||||||
requestBody.doc_ids = selectedDocIds;
|
requestBody.doc_ids = selectedDocIds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.cross_languages && data.cross_languages.length > 0) {
|
if (data.cross_languages && data.cross_languages.length > 0) {
|
||||||
requestBody.cross_languages = data.cross_languages;
|
requestBody.cross_languages = data.cross_languages;
|
||||||
}
|
}
|
||||||
@@ -209,7 +216,7 @@ function KnowledgeBaseTesting() {
|
|||||||
const handleDocumentFilter = (docIds: string[]) => {
|
const handleDocumentFilter = (docIds: string[]) => {
|
||||||
setSelectedDocIds(docIds);
|
setSelectedDocIds(docIds);
|
||||||
setValue('doc_ids', docIds);
|
setValue('doc_ids', docIds);
|
||||||
handleTestSubmit(getValues());
|
handleTestSubmitFunc(getValues(), true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 返回详情页
|
// 返回详情页
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ModelsResources from '../pages/ModelsResources';
|
|||||||
import { KnowledgeBaseList, KnowledgeBaseCreate, KnowledgeBaseDetail, KnowledgeBaseSetting, KnowledgeBaseTesting } from '../pages/knowledge';
|
import { KnowledgeBaseList, KnowledgeBaseCreate, KnowledgeBaseDetail, KnowledgeBaseSetting, KnowledgeBaseTesting } from '../pages/knowledge';
|
||||||
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';
|
||||||
|
|
||||||
const AppRoutes = () => {
|
const AppRoutes = () => {
|
||||||
return (
|
return (
|
||||||
@@ -34,6 +35,10 @@ const AppRoutes = () => {
|
|||||||
<Route path="mcp" element={<MCP />} />
|
<Route path="mcp" element={<MCP />} />
|
||||||
<Route path="form-field-test" element={<FormFieldTest />} />
|
<Route path="form-field-test" element={<FormFieldTest />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
{/* 处理chunk相关路由 需要传入 kb_id doc_id */}
|
||||||
|
<Route path="chunk">
|
||||||
|
<Route path="parsed-result" element={<ChunkParsedResult />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* 处理未匹配的路由 */}
|
{/* 处理未匹配的路由 */}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
IFetchKnowledgeListRequestParams,
|
IFetchKnowledgeListRequestParams,
|
||||||
IFetchDocumentListRequestBody,
|
IFetchDocumentListRequestBody,
|
||||||
ITestRetrievalRequestBody,
|
ITestRetrievalRequestBody,
|
||||||
|
IFetchChunkListRequestBody,
|
||||||
} from '@/interfaces/request/knowledge';
|
} from '@/interfaces/request/knowledge';
|
||||||
import type {
|
import type {
|
||||||
IKnowledge,
|
IKnowledge,
|
||||||
@@ -12,8 +13,10 @@ import type {
|
|||||||
IChunk,
|
IChunk,
|
||||||
IRenameTag,
|
IRenameTag,
|
||||||
IParserConfig,
|
IParserConfig,
|
||||||
|
IKnowledgeFileParserConfig,
|
||||||
} from '@/interfaces/database/knowledge';
|
} from '@/interfaces/database/knowledge';
|
||||||
import type { GridRowSelectionModel } from '@mui/x-data-grid';
|
import type { GridRowSelectionModel } from '@mui/x-data-grid';
|
||||||
|
import type { IRunDocumentRequestBody } from '@/interfaces/request/document';
|
||||||
|
|
||||||
// 知识库相关API服务
|
// 知识库相关API服务
|
||||||
const knowledgeService = {
|
const knowledgeService = {
|
||||||
@@ -110,18 +113,21 @@ const knowledgeService = {
|
|||||||
return request.delete(`${api.document_delete}/${doc_id}`);
|
return request.delete(`${api.document_delete}/${doc_id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更改文档状态
|
/**
|
||||||
changeDocumentStatus: (data: { doc_id: string | Array<string | number>; status: string }) => {
|
* 更改文档状态
|
||||||
|
* @param data 文档ID列表和状态 status 0 禁用 1 启用
|
||||||
|
*/
|
||||||
|
changeDocumentStatus: (data: { doc_ids: Array<string | number>; status: string | number }) => {
|
||||||
return post(api.document_change_status, data);
|
return post(api.document_change_status, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 运行文档处理
|
// 运行文档处理
|
||||||
runDocument: (data: { doc_id: string | Array<string | number>}) => {
|
runDocument: (data: IRunDocumentRequestBody) => {
|
||||||
return post(api.document_run, data);
|
return post(api.document_run, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更改文档解析器配置
|
// 更改文档解析器配置
|
||||||
changeDocumentParser: (data: { doc_id: string; parser_config: IParserConfig }) => {
|
changeDocumentParser: (data: { doc_id: string; parser_config: IKnowledgeFileParserConfig }) => {
|
||||||
return post(api.document_change_parser, data);
|
return post(api.document_change_parser, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -132,11 +138,13 @@ const knowledgeService = {
|
|||||||
|
|
||||||
// 获取文档文件
|
// 获取文档文件
|
||||||
getDocumentFile: (params: { doc_id: string }) => {
|
getDocumentFile: (params: { doc_id: string }) => {
|
||||||
return request.get(api.get_document_file, { params });
|
return request.get(`${api.get_document_file}/${params.doc_id}`, {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取文档信息
|
// 获取文档信息
|
||||||
getDocumentInfos: (data: { doc_id: string | Array<string | number> }) => {
|
getDocumentInfos: (data: { doc_ids: string | Array<string | number> }) => {
|
||||||
return post(api.document_infos, data);
|
return post(api.document_infos, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -158,7 +166,7 @@ const knowledgeService = {
|
|||||||
// ===== 分块管理 =====
|
// ===== 分块管理 =====
|
||||||
|
|
||||||
// 获取分块列表
|
// 获取分块列表
|
||||||
getChunkList: (data: any) => {
|
getChunkList: (data: IFetchChunkListRequestBody) => {
|
||||||
return post(api.chunk_list, data);
|
return post(api.chunk_list, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ request.interceptors.response.use(
|
|||||||
if (status === 413 || status === 504) {
|
if (status === 413 || status === 504) {
|
||||||
snackbar.error(RetcodeMessage[status as ResultCode]);
|
snackbar.error(RetcodeMessage[status as ResultCode]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理blob类型响应
|
// 处理blob类型响应
|
||||||
if (response.config.responseType === 'blob') {
|
if (response.config.responseType === 'blob') {
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
Reference in New Issue
Block a user