Compare commits

...

7 Commits

Author SHA1 Message Date
guangfei.zhao
fa42853d41 feat(chunk): add chunk selection and highlighting in PDF viewer 2025-11-25 14:46:25 +08:00
Zhu,JW
70926de445 Merge branch 'main' of https://code.deep-pilot.chat/AI_POC/TERES_web_frontend 2025-11-24 11:22:12 +08:00
Zhu,JW
898c0988db fix(agent): redirect to blank page when navigating home from model page 2025-11-24 11:22:03 +08:00
guangfei.zhao
79d0cf6f3b refactor(knowledge): remove language display from KnowledgeInfoCard 2025-11-20 14:12:15 +08:00
guangfei.zhao
c992c04c7c feat(knowledge): add document parser and metadata management 2025-11-20 14:00:30 +08:00
guangfei.zhao
79ee33be7c fix(knowledge): ensure proper id handling in document operations
refactor(teams): update user deletion to use user_id instead of id
refactor(knowledge): optimize data grid locale handling with useCallback
style(knowledge): adjust similarity display format in test chunks
refactor(knowledge): improve document selection logic and typing
2025-11-19 17:44:12 +08:00
guangfei.zhao
83df8a7373 feat(auth): enhance password change flow and error handling
- Return response from useProfileSetting hook after password change
2025-11-19 17:43:45 +08:00
31 changed files with 783 additions and 88 deletions

View File

@@ -6,18 +6,28 @@ import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist', 'rag_web_core']),
globalIgnores(['dist', 'ragflow_web']),
{
files: ['**/*.{ts,tsx}'],
rules: {},
extends: [
js.configs.recommended,
tseslint.configs.recommended,
// tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parser: tseslint.parser,
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
plugins: {
'@typescript-eslint': tseslint.plugin,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
},
])

View File

@@ -38,7 +38,9 @@
"react-is": "18.3.1",
"react-router-dom": "^7.9.4",
"uuid": "^13.0.0",
"zustand": "^5.0.8"
"zustand": "^5.0.8",
"@monaco-editor/react": "^4.6.0",
"monaco-editor": "^0.52.2"
},
"devDependencies": {
"@eslint/js": "^9.36.0",

18
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@emotion/styled':
specifier: ^11.14.1
version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1)
'@monaco-editor/react':
specifier: ^4.6.0
version: 4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@mui/icons-material':
specifier: ^7.3.4
version: 7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@19.2.2)(react@18.3.1)
@@ -59,6 +62,9 @@ importers:
loglevel:
specifier: ^1.9.2
version: 1.9.2
monaco-editor:
specifier: ^0.52.2
version: 0.52.2
pdfjs-dist:
specifier: ^5.4.394
version: 5.4.394
@@ -8785,6 +8791,9 @@ packages:
moment@2.30.1:
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
monaco-editor@0.52.2:
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
monaco-editor@0.54.0:
resolution: {integrity: sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==}
@@ -14016,6 +14025,13 @@ snapshots:
dependencies:
state-local: 1.0.7
'@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@monaco-editor/loader': 1.6.1
monaco-editor: 0.52.2
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@monaco-editor/react@4.7.0(monaco-editor@0.54.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@monaco-editor/loader': 1.6.1
@@ -22253,6 +22269,8 @@ snapshots:
moment@2.30.1: {}
monaco-editor@0.52.2: {}
monaco-editor@0.54.0:
dependencies:
dompurify: 3.1.7

View File

@@ -11,6 +11,7 @@ export default {
yes: 'Yes',
no: 'No',
total: 'Total',
know: 'Known',
rename: 'Rename',
name: 'Name',
save: 'Save',
@@ -106,6 +107,7 @@ export default {
noMoreData: `That's all. Nothing more.`,
},
knowledgeDetails: {
pleaseAddEmbeddingModel: 'Please add embedding model and LLM in the model provider first, then set them in "Set Default Model".',
localUpload: 'Local Upload',
fileSize: 'File Size',
fileType: 'File Type',

View File

@@ -11,6 +11,7 @@ export default {
yes: '是',
no: '否',
total: '总共',
know: '知道了',
rename: '重命名',
name: '名称',
save: '保存',
@@ -99,6 +100,7 @@ export default {
noMoreData: '没有更多数据了',
},
knowledgeDetails: {
pleaseAddEmbeddingModel: '请先在模型提供商中添加嵌入模型和LLM然后在"设置默认模型"中设置它们。',
localUpload: '本地上传',
fileSize: '文件大小',
fileType: '文件类型',

View File

@@ -120,7 +120,7 @@ const DialogComponent: React.FC<DialogComponentProps> = ({ dialog, onClose }) =>
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
{getDialogIcon()}
<Typography variant="h6" component="span">
{config.title || t('dialog.defaultTitle')}
{config.title || t('dialog.confirm')}
</Typography>
</Box>
<IconButton

View File

@@ -1,10 +1,14 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import agentService from '@/services/agent_service';
import userService from '@/services/user_service';
import type { IAgent, IGraph } from '@/interfaces/database/agent';
import type { IAgentPaginationParams, IAgentCreateRequestBody, IAgentSettingRequestBody } from '@/interfaces/request/agent';
import logger from '@/utils/logger';
import { useTranslation } from 'react-i18next';
import { useSnackbar } from '@/hooks/useSnackbar';
import { useDialog } from '@/hooks/useDialog';
import { useNavigate } from 'react-router-dom';
import { isEmpty } from 'lodash';
export interface UseAgentListState {
agents: IAgent[];
@@ -39,6 +43,28 @@ export const useAgentList = (initialParams?: IAgentPaginationParams) => {
const [currentPage, setCurrentPage] = useState(initialParams?.page || 1);
const [pageSize, setPageSize] = useState(initialParams?.page_size || 10);
const [keywords, setKeywords] = useState(initialParams?.keywords || '');
const { showMessage } = useSnackbar();
const dialog = useDialog();
const navigate = useNavigate();
const { t } = useTranslation();
// 查询是否有默认的模型
const checkDefaultModel = useCallback(async (id: string) => {
const { data: res } = await userService.getTenantInfo();
if (res.code === 0) {
const { data } = res;
if (isEmpty(data.embd_id) || isEmpty(data.llm_id)) {
await dialog.warning({
title: t('common.warn'),
content: t('knowledgeDetails.pleaseAddEmbeddingModel'),
confirmText: t('common.know')
});
navigate(`/models`);
} else {
navigate(`/route-ragflow/agent/${id}`);
}
}
}, [dialog, navigate, showMessage]);
const fetchAgentList = useCallback(async (params?: IAgentPaginationParams) => {
setLoading(true);
@@ -81,6 +107,7 @@ export const useAgentList = (initialParams?: IAgentPaginationParams) => {
currentPage,
pageSize,
keywords,
checkDefaultModel,
fetchAgents: fetchAgentList,
setKeywords,
setCurrentPage,

View File

@@ -394,12 +394,12 @@ export const useLoginForm = () => {
password: {
required: t('login.passwordPlaceholder'),
minLength: {
value: 8,
value: 6,
message: t('setting.passwordMinLength')
}
},
confirmPassword: {
required: t('confirmPasswordRequired'),
required: t('login.confirmPasswordRequired'),
validate: (value: string) => {
const pwd = registerForm.getValues('password');
return value === pwd || t('setting.passwordMismatch');

View File

@@ -42,6 +42,7 @@ export function useProfileSetting() {
password: oldPassword,
new_password: newPassword,
});
return res;
} catch (error) {
throw error;
}

View File

@@ -279,6 +279,10 @@ export interface IKnowledgeFile {
name: string;
/** 解析器ID */
parser_id: string;
/** 流水线ID可选 */
pipeline_id?: string;
/** 流水线名称,可选 */
pipeline_name?: string;
/** 处理开始时间,可选 */
process_begin_at?: any;
/** 处理持续时间 */

View File

@@ -15,6 +15,7 @@ export default {
total: 'Total',
rename: 'Rename',
name: 'Name',
know: 'Known',
save: 'Save',
namePlaceholder: 'Please input name',
next: 'Next',
@@ -262,6 +263,11 @@ export default {
emailInvalid: 'Invalid email address',
passwordLabel: 'Password',
passwordPlaceholder: 'Please input password',
confirmPasswordRequired: 'Please confirm your password',
confirmPassword: 'Confirm password',
confirmPasswordMessage: 'Please confirm your password!',
confirmPasswordNonMatchMessage:
'The confirm password that you entered do not match!',
rememberMe: 'Remember me',
signInTip: 'Dont have an account?',
signUpTip: 'Already have an account?',
@@ -420,6 +426,7 @@ export default {
paginationInfo: 'Total {{total}} knowledge bases, page {{current}} of {{totalPages}}',
},
knowledgeDetails: {
pleaseAddEmbeddingModel: 'Please add embedding model and LLM in the model provider first, then set them in "Set Default Model".',
fileSize: 'File Size',
fileType: 'File Type',
uploadedBy: 'Uploaded by',
@@ -594,6 +601,7 @@ export default {
redo: 'Do you want to clear the existing {{chunkNum}} chunks?',
setMetaData: 'Set Meta Data',
pleaseInputJson: 'Please enter JSON',
invalidJson: 'Invalid JSON format',
documentMetaTips: `<p>The meta data is in Json format(it's not searchable). It will be added into prompt for LLM if any chunks of this document are included in the prompt.</p>
<p>Examples:</p>
<b>The meta data is:</b><br>
@@ -942,7 +950,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
serverTypeRequired: 'Please select server type',
testConnection: 'Test Connection',
testing: 'Testing...',
connectionSuccess: 'Connection successful! Found {count} tools',
connectionSuccess: 'Connection successful! Found {{count}} tools',
availableTools: 'Available Tools',
connectionFailed: 'Connection failed',
testFailed: 'Test failed',

View File

@@ -13,6 +13,7 @@ export default {
cancel: '否',
total: '总共',
rename: '重命名',
know: '知道了',
name: '名称',
save: '保存',
namePlaceholder: '请输入名称',
@@ -244,6 +245,11 @@ export default {
emailInvalid: '无效的邮箱地址',
passwordLabel: '密码',
passwordPlaceholder: '请输入密码',
confirmPasswordRequired: '请确认您的密码',
confirmPassword: '确认密码',
confirmPasswordMessage: '请确认您的密码!',
confirmPasswordNonMatchMessage:
'确认密码与密码不匹配!',
rememberMe: '记住我',
signInTip: '没有帐户?',
signUpTip: '已经有帐户?',
@@ -418,6 +424,7 @@ export default {
paginationInfo: '共 {{total}} 个知识库,第 {{current}} 页,共 {{totalPages}} 页',
},
knowledgeDetails: {
pleaseAddEmbeddingModel: '请先在模型提供商中添加嵌入模型和LLM然后在"设置默认模型"中设置它们。',
fileSize: '文件大小',
fileType: '文件类型',
uploadedBy: '创建者',
@@ -588,6 +595,7 @@ export default {
redo: '是否清空已有 {{chunkNum}}个 chunk',
setMetaData: '设置元数据',
pleaseInputJson: '请输入JSON',
invalidJson: '无效的JSON格式',
documentMetaTips: `<p>元数据为 Json 格式(不可搜索)。如果提示中包含此文档的任何块,它将被添加到 LLM 的提示中。</p>
<p>示例:</p>
<b>元数据为:</b><br>
@@ -941,7 +949,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
serverTypeRequired: '请选择服务器类型',
testConnection: '测试连接',
testing: '测试中...',
connectionSuccess: '连接成功!发现 {count} 个工具',
connectionSuccess: '连接成功!发现 {{count}} 个工具',
availableTools: '可用工具',
connectionFailed: '连接失败',
testFailed: '测试失败',

View File

@@ -17,7 +17,6 @@ import EditAgentDialog from './components/EditAgentDialog';
import { useTranslation } from 'react-i18next';
import { useDialog } from '@/hooks/useDialog';
import { useNavigate } from 'react-router-dom';
function AgentListPage() {
const [searchValue, setSearchValue] = useState('');
const [createOpen, setCreateOpen] = useState(false);
@@ -31,6 +30,7 @@ function AgentListPage() {
pageSize,
setCurrentPage,
setKeywords,
checkDefaultModel,
refresh,
} = useAgentList({ page: 1, page_size: 10 });
@@ -40,8 +40,8 @@ function AgentListPage() {
const ops = useAgentOperations();
const totalPages = useMemo(() => {
return Math.ceil((agents?.length || 0) / pageSize) || 1;
}, [agents, pageSize]);
return Math.ceil((total || 0) / pageSize) || 1;
}, [total, pageSize]);
const currentPageData = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
@@ -108,7 +108,7 @@ function AgentListPage() {
onCreateAgent={() => setCreateOpen(true)}
onEdit={(agent) => { setEditTarget(agent); setEditOpen(true); }}
onView={(agent) => {
navigate(`/route-ragflow/agent/${agent.id}`);
checkDefaultModel(agent.id);
}}
onDelete={async (agent) => {
const confirmed = await dialog.confirm({

View File

@@ -60,10 +60,12 @@ interface ChunkListResultProps {
onRefresh?: () => void;
docName?: string;
onLocate?: (chunk: IChunk) => void;
selectedChunkId?: string;
onSelect?: (chunk: IChunk) => void;
}
function ChunkListResult(props: ChunkListResultProps) {
const { doc_id, chunks, total, loading, page, pageSize, onPageChange, onRefresh, onLocate } = props;
const { doc_id, chunks, total, loading, page, pageSize, onPageChange, onRefresh, onLocate, selectedChunkId, onSelect } = props;
const { t } = useTranslation();
// 选择状态
@@ -363,14 +365,13 @@ function ChunkListResult(props: ChunkListResultProps) {
'&:hover': {
boxShadow: 2,
},
border: selectedChunks.includes(chunk.chunk_id) ? '2px solid' : '1px solid',
borderColor: selectedChunks.includes(chunk.chunk_id)
? 'primary.main'
: chunk.available_int === 1
? 'success.light'
: 'grey.300',
backgroundColor: selectedChunks.includes(chunk.chunk_id) ? 'action.selected' : 'background.paper',
border: (selectedChunks.includes(chunk.chunk_id) || selectedChunkId === chunk.chunk_id) ? '2px solid' : '1px solid',
borderColor: (selectedChunks.includes(chunk.chunk_id) || selectedChunkId === chunk.chunk_id)
? 'primary.main'
: (chunk.available_int === 1 ? 'success.light' : 'grey.300'),
backgroundColor: (selectedChunks.includes(chunk.chunk_id) || selectedChunkId === chunk.chunk_id) ? 'action.selected' : 'background.paper',
}}
onClick={() => onSelect?.(chunk)}
>
<CardContent sx={{ p: 2 }}>
{/* 头部操作区域 */}
@@ -389,7 +390,7 @@ function ChunkListResult(props: ChunkListResultProps) {
/>
{/* 定位到文档位置 */}
<Tooltip title={'定位'}>
<IconButton size="small" onClick={() => onLocate?.(chunk)}>
<IconButton size="small" onClick={() => { onSelect?.(chunk); onLocate?.(chunk); }}>
<ZoomInIcon />
</IconButton>
</Tooltip>
@@ -425,7 +426,7 @@ function ChunkListResult(props: ChunkListResultProps) {
}
}
}}
onClick={() => handleImageClick(`${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`, chunk)}
onClick={() => { onSelect?.(chunk); handleImageClick(`${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`, chunk); }}
onMouseEnter={(e) => handleImageHover(e, `${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`)}
onMouseLeave={handleImageHoverClose}
>
@@ -752,4 +753,4 @@ function ChunkListResult(props: ChunkListResultProps) {
);
}
export default ChunkListResult;
export default ChunkListResult;

View File

@@ -37,17 +37,7 @@ function ChunkParsedResult() {
const kb_id = searchParams.get('kb_id');
const doc_id = searchParams.get('doc_id');
const [knowledgeBase, setKnowledgeBase] = useState<IKnowledge | null>(null);
const [document, setDocument] = useState<IKnowledgeFile | null>(null);
const [searchKeyword, setSearchKeyword] = useState('');
const [documentFile, setDocumentFile] = useState<Blob | null>(null);
const [fileUrl, setFileUrl] = useState<string>('');
const [fileLoading, setFileLoading] = useState(false);
const [previewOverrideUrl, setPreviewOverrideUrl] = useState<string>('');
const [focusPage, setFocusPage] = useState<number | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const pdfContainerRef = useRef<HTMLDivElement | null>(null);
const [pdfRendered, setPdfRendered] = useState<boolean>(false);
// 使用chunk列表hook
const {
@@ -66,6 +56,19 @@ function ChunkParsedResult() {
keywords: searchKeyword
});
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 [fileLoading, setFileLoading] = useState(false);
const [previewOverrideUrl, setPreviewOverrideUrl] = useState<string>('');
const [focusPage, setFocusPage] = useState<number | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const pdfContainerRef = useRef<HTMLDivElement | null>(null);
const [pdfRendered, setPdfRendered] = useState<boolean>(false);
const [selectedChunkId, setSelectedChunkId] = useState<string>('');
const selectedChunk = chunks.find((c) => c.chunk_id === selectedChunkId) || null;
// 获取知识库和文档信息
useEffect(() => {
const fetchData = async () => {
@@ -204,6 +207,10 @@ function ChunkParsedResult() {
const pageWrapper = window.document.createElement('div');
pageWrapper.setAttribute('data-page-index', String(pageNum));
pageWrapper.setAttribute('data-scale', String(scale));
pageWrapper.setAttribute('data-viewport-width', String(viewport.width));
pageWrapper.setAttribute('data-viewport-height', String(viewport.height));
pageWrapper.style.position = 'relative';
pageWrapper.appendChild(canvas);
container.appendChild(pageWrapper);
}
@@ -278,6 +285,7 @@ function ChunkParsedResult() {
if (documentFile?.type === 'application/pdf') {
setFocusPage(page && !Number.isNaN(page) ? page : null);
setPreviewOverrideUrl('');
setSelectedChunkId(chunk.chunk_id || '');
return;
}
@@ -286,6 +294,107 @@ function ChunkParsedResult() {
setFocusPage(null);
};
useEffect(() => {
if (documentFile?.type !== 'application/pdf' || !pdfRendered) return;
const container = pdfContainerRef.current;
if (!container) return;
Array.from(container.querySelectorAll('.pdf-highlight-layer')).forEach((el) => el.remove());
const chunk = selectedChunk;
const positions: any[] = (chunk?.positions || []) as any[];
if (!Array.isArray(positions) || positions.length === 0) return;
// 1) 将位置按页分组
const pageMap = new Map<number, Array<{x1:number;x2:number;y1:number;y2:number}>>();
positions.forEach((pos) => {
if (!Array.isArray(pos) || pos.length < 5) return;
const p = Number(pos[0]);
const x1 = Number(pos[1]);
const x2 = Number(pos[2]);
const y1 = Number(pos[3]);
const y2 = Number(pos[4]);
const list = pageMap.get(p) || [];
list.push({ x1, x2, y1, y2 });
pageMap.set(p, list);
});
// 2) 设置容差与行距阈值:用于合并同一段落的多行
const XTOL = 2; // x范围容差像素
const GAP_TOL = 8; // 行间距阈值,像素
const quant = (v: number) => Math.round(v / XTOL) * XTOL;
// 3) 遍历每页:按量化后的 x1/x2 分桶,再按 y1 合并相邻行
pageMap.forEach((segList, pageNumber) => {
const pageWrapper = container.querySelector(`[data-page-index="${pageNumber}"]`) as HTMLElement | null;
if (!pageWrapper) return;
const scale = Number(pageWrapper.getAttribute('data-scale') || '1');
const canvas = pageWrapper.querySelector('canvas') as HTMLCanvasElement | null;
if (!canvas) return;
const cssWidth = canvas.clientWidth;
const cssHeight = canvas.clientHeight;
// 分桶:相同(或近似)列宽的文本认为是一段
const buckets = new Map<string, Array<{x1:number;x2:number;y1:number;y2:number}>>();
segList.forEach(s => {
const key = `${quant(s.x1)}-${quant(s.x2)}`;
const arr = buckets.get(key) || [];
arr.push(s);
buckets.set(key, arr);
});
const layer = window.document.createElement('div');
layer.className = 'pdf-highlight-layer';
layer.style.position = 'absolute';
layer.style.left = '0px';
layer.style.top = '0px';
layer.style.width = `${cssWidth}px`;
layer.style.height = `${cssHeight}px`;
layer.style.pointerEvents = 'none';
buckets.forEach((bucketSegs) => {
const segs = bucketSegs.slice().sort((a,b) => a.y1 - b.y1);
const merged: Array<{x1:number;x2:number;y1:number;y2:number}> = [];
segs.forEach(seg => {
const last = merged[merged.length - 1];
if (!last) {
merged.push({ ...seg });
return;
}
const similarWidth = Math.abs(seg.x1 - last.x1) <= XTOL && Math.abs(seg.x2 - last.x2) <= XTOL;
const contiguous = seg.y1 <= last.y2 + GAP_TOL; // 上一行的下边缘到当前行的上边缘间隔很小
if (similarWidth && contiguous) {
last.y2 = Math.max(last.y2, seg.y2);
} else {
merged.push({ ...seg });
}
});
merged.forEach(m => {
const left = m.x1 * scale;
const width = (m.x2 - m.x1) * scale;
const top = m.y1 * scale;
const height = (m.y2 - m.y1) * scale;
const rect = window.document.createElement('div');
rect.style.position = 'absolute';
rect.style.left = `${left}px`;
rect.style.top = `${top}px`;
rect.style.width = `${Math.max(0, width)}px`;
rect.style.height = `${Math.max(0, height)}px`;
rect.style.background = 'rgba(255, 230, 0, 0.30)';
rect.style.border = '1px solid rgba(255, 193, 7, 0.75)';
rect.style.borderRadius = '2px';
rect.style.pointerEvents = 'none';
layer.appendChild(rect);
});
});
pageWrapper.appendChild(layer);
});
}, [selectedChunk, pdfRendered, documentFile]);
// 渲染左侧预览
const renderPreview = () => {
// 如果有覆盖的图片URL直接显示图片
@@ -442,6 +551,11 @@ function ChunkParsedResult() {
onRefresh={refresh}
docName={document?.name}
onLocate={handleLocate}
selectedChunkId={selectedChunkId}
onSelect={(chunk) => {
setSelectedChunkId(chunk.chunk_id || '');
handleLocate(chunk);
}}
/>
</Box>
</Paper>
@@ -451,4 +565,4 @@ function ChunkParsedResult() {
);
}
export default ChunkParsedResult;
export default ChunkParsedResult;

View File

@@ -49,7 +49,7 @@ import {
BugReportOutlined as ProcessIcon,
Download as DownloadIcon,
} from '@mui/icons-material';
import { DataGrid, type GridColDef, type GridRowSelectionModel } from '@mui/x-data-grid';
import { DataGrid, type GridColDef, type GridRowSelectionModel, type GridRowId } 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';
@@ -58,6 +58,9 @@ import dayjs from 'dayjs';
import logger from '@/utils/logger';
import { LanguageAbbreviation } from '@/constants/common';
import knowledgeService from '@/services/knowledge_service';
import ParserContextMenu from './ParserContextMenu';
import DocumentParserDialog from './DocumentParserDialog';
import DocumentMetadataDialog from './DocumentMetadataDialog';
interface DocumentListComponentProps {
@@ -147,7 +150,7 @@ const getStatusChip = (status: string) => {
};
const getRunStatusChip = (run: RunningStatus, progress: number) => {
const statusConfig = {
const statusConfig = {
[RunningStatus.UNSTART]: { label: translate('knowledge.runStatus.unstart'), color: 'default' as const },
[RunningStatus.RUNNING]: { label: translate('knowledge.runStatus.parsing'), color: 'info' as const },
[RunningStatus.CANCEL]: { label: translate('knowledge.runStatus.cancel'), color: 'warning' as const },
@@ -163,7 +166,7 @@ const getRunStatusChip = (run: RunningStatus, progress: number) => {
<CircularProgress size={16} />
<Box sx={{ minWidth: 80 }}>
<Typography variant="caption" color="text.secondary">
{(progress*100).toFixed(2)}%
{(progress * 100).toFixed(2)}%
</Typography>
<LinearProgress variant="determinate" value={progress} sx={{ height: 4, borderRadius: 2 }} />
</Box>
@@ -215,13 +218,22 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [newFileName, setNewFileName] = useState('');
// parser 列右键菜单与对话框状态
const [parserMenuAnchor, setParserMenuAnchor] = useState<null | HTMLElement>(null);
const [parserMenuFile, setParserMenuFile] = useState<IKnowledgeFile | undefined>(undefined);
const [parserDialogOpen, setParserDialogOpen] = useState(false);
const [metadataDialogOpen, setMetadataDialogOpen] = useState(false);
// 打开对话框时使用的文件,避免关闭菜单时清空导致对话框拿不到文件
const [dialogFile, setDialogFile] = useState<IKnowledgeFile | undefined>(undefined);
// 解析与元数据对话框提交状态由子组件内部管理
const { i18n, t } = useTranslation();
// 根据当前语言获取DataGrid的localeText
const getDataGridLocale = () => {
const getDataGridLocale = useCallback(() => {
const currentLanguage = i18n.language;
return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS;
};
}, [i18n]);
const handleFilterSubmit = useCallback(() => {
const filter = {
@@ -346,6 +358,37 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
setSelectedSuffix(typeof value === 'string' ? value.split(',') : value);
};
// 打开 parser 列的上下文菜单
const handleOpenParserMenu = (event: React.MouseEvent<HTMLElement>, file: IKnowledgeFile) => {
event.stopPropagation();
event.preventDefault();
setParserMenuAnchor(event.currentTarget);
setParserMenuFile(file);
};
const handleCloseParserMenu = () => {
setParserMenuAnchor(null);
setParserMenuFile(undefined);
};
// 选中数量计算(强类型)
const countSelected = (model: GridRowSelectionModel, totalCount: number): number => {
const size = model.ids.size ?? 0;
return model.type === 'exclude' ? Math.max(totalCount - size, 0) : size;
};
// 获取当前页有效选中 ID 列表(强类型)
const getSelectedIdsOnCurrentPage = (model: GridRowSelectionModel, currentFiles: IKnowledgeFile[]): string[] => {
if (model.type === 'include') {
return Array.from(model.ids).map((id) => id.toString());
}
const excluded = model.ids;
return currentFiles
.map((f) => f.id)
.filter((id) => !excluded.has(id))
.map((id) => id.toString());
};
// 处理分页变化
const handlePaginationModelChange = (model: { page: number; pageSize: number }) => {
if (model.page !== page - 1) { // DataGrid的page是0-based我们的是1-based
@@ -362,7 +405,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
field: 'name',
headerName: t('knowledge.fileName'),
flex: 2,
minWidth: 200,
minWidth: 120,
cellClassName: 'grid-center-cell',
renderCell: (params) => (
<Box
@@ -390,6 +433,13 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
</Box>
),
},
{
field: 'create_time',
headerName: t('knowledge.uploadTime'),
flex: 1,
minWidth: 140,
valueFormatter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
},
{
field: 'type',
headerName: t('knowledge.type'),
@@ -428,11 +478,15 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
renderCell: (params) => getRunStatusChip(params.value, params.row.progress),
},
{
field: 'create_time',
headerName: t('knowledge.uploadTime'),
flex: 1,
minWidth: 140,
valueFormatter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
field: 'parser_id',
headerName: t('knowledge.parser'),
flex: 0.8,
minWidth: 100,
renderCell: (params) => (
<Box onContextMenu={(e) => handleOpenParserMenu(e, params.row)} onClick={(e) => handleOpenParserMenu(e, params.row)}>
<Chip label={params.value.toUpperCase()} size="small" variant="outlined" />
</Box>
),
},
{
field: 'actions',
@@ -603,23 +657,22 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
{t('knowledge.uploadFile')}
</Button>
{rowSelectionModel.ids.size > 0 && (
{countSelected(rowSelectionModel, total) > 0 && (
<>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={() => onReparse(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
onClick={() => onReparse(getSelectedIdsOnCurrentPage(rowSelectionModel, files))}
>
{t('knowledge.reparse')} ({rowSelectionModel.ids.size})
{t('knowledge.reparse')} ({countSelected(rowSelectionModel, total)})
</Button>
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={() => onDelete(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
onClick={() => onDelete(getSelectedIdsOnCurrentPage(rowSelectionModel, files))}
>
{t('common.delete')} ({rowSelectionModel.ids.size})
{t('common.delete')} ({countSelected(rowSelectionModel, total)})
</Button>
</>
)}
@@ -640,7 +693,9 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
checkboxSelection
disableRowSelectionOnClick
rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={onRowSelectionModelChange}
onRowSelectionModelChange={(model: GridRowSelectionModel) => {
onRowSelectionModelChange(model);
}}
pageSizeOptions={[10, 25, 50, 100]}
paginationMode="server"
rowCount={total}
@@ -661,6 +716,23 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
/>
</Paper>
{/* Parser 列上下文菜单 */}
<ParserContextMenu
anchorEl={parserMenuAnchor}
open={Boolean(parserMenuAnchor)}
onClose={handleCloseParserMenu}
onOpenParser={() => {
setDialogFile(parserMenuFile);
setParserDialogOpen(true);
handleCloseParserMenu();
}}
onOpenMetadata={() => {
setDialogFile(parserMenuFile);
setMetadataDialogOpen(true);
handleCloseParserMenu();
}}
/>
{/* 菜单 */}
<Menu
anchorEl={anchorEl}
@@ -727,6 +799,22 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
<Button onClick={handleRenameConfirm} variant="contained">{t('common.confirm')}</Button>
</DialogActions>
</Dialog>
{/* Document Parser Dialog */}
<DocumentParserDialog
open={parserDialogOpen}
file={dialogFile}
onClose={() => { setParserDialogOpen(false); setDialogFile(undefined); }}
onSuccess={() => { setParserDialogOpen(false); setDialogFile(undefined); onRefresh(); }}
/>
{/* Document Metadata Dialog */}
<DocumentMetadataDialog
open={metadataDialogOpen}
file={dialogFile}
onClose={() => { setMetadataDialogOpen(false); setDialogFile(undefined); }}
onSuccess={() => { setMetadataDialogOpen(false); setDialogFile(undefined); onRefresh(); }}
/>
</Box>
);
};

View File

@@ -0,0 +1,115 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
import type { IKnowledgeFile } from '@/interfaces/database/knowledge';
import knowledgeService from '@/services/knowledge_service';
import logger from '@/utils/logger';
import Editor from '@monaco-editor/react';
interface DocumentMetadataDialogProps {
open: boolean;
file?: IKnowledgeFile;
onClose: () => void;
onSubmittingChange?: (submitting: boolean) => void;
onSuccess?: () => void;
}
export default function DocumentMetadataDialog({
open,
file,
onClose,
onSubmittingChange,
onSuccess,
}: DocumentMetadataDialogProps) {
const { t } = useTranslation();
const [value, setValue] = useState('');
const [error, setError] = useState<string | null>(null);
// 初始值:将 file.meta_fields 作为格式化 JSON 预填
const initialJson = useMemo(() => {
const meta = (file as any)?.meta_fields;
if (meta && typeof meta === 'object') {
try {
return JSON.stringify(meta, null, 2);
} catch {
return '';
}
}
return '';
}, [file]);
useEffect(() => {
// 每次打开针对不同文件重置输入为当前文件的 meta_fields
if (open) {
setValue(initialJson);
setError(null);
}
}, [open, file?.id, initialJson]);
const validateJson = (text: string) => {
try {
if (!text.trim()) {
setError(null);
return {};
}
const parsed = JSON.parse(text);
setError(null);
return parsed;
} catch (e: any) {
setError(e?.message || 'Invalid JSON');
return null;
}
};
const handleSubmit = async () => {
if (!file) return;
try {
onSubmittingChange?.(true);
const parsed = validateJson(value);
if (parsed === null) {
return;
}
const metaStr = JSON.stringify(parsed);
await knowledgeService.setDocumentMetaData({ doc_id: file.id, meta: metaStr });
onSuccess?.();
onClose();
} catch (err) {
logger.error('设置元数据失败', err);
} finally {
onSubmittingChange?.(false);
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{t('knowledgeDetails.setMetaData')}</DialogTitle>
<DialogContent dividers>
<Editor
height="240px"
language="json"
theme="vs-dark"
value={value}
onChange={(val) => setValue(val || '')}
options={{
minimap: { enabled: false },
automaticLayout: true,
formatOnPaste: true,
formatOnType: true,
tabSize: 2,
}}
/>
{error ? (
<Typography variant="caption" color="error" sx={{ mt: 1, display: 'block' }}>
{t('knowledgeDetails.invalidJson')}: {error}
</Typography>
) : null}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t('common.cancel')}</Button>
<Button variant="contained" onClick={handleSubmit} disabled={!!error}>
{t('common.confirm')}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,128 @@
import React, { useEffect, useState } from 'react';
import { Box, Card, CardContent } from '@mui/material';
import { useFormContext, useWatch, type UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { AutoKeywordsItem, AutoQuestionsItem, BasicConfigItems, ChunkTokenNumberItem, DelimiterItem, PipelineSelectorItem } from '../configuration/common-items';
import { ChunkMethodItem } from '../configuration';
import { RadioFormField } from '@/components/FormField';
import { DocumentParserType, ParseType } from '@/constants/knowledge';
import logger from '@/utils/logger';
function ParserConfigurationItems({ parser_id }: { parser_id: DocumentParserType }) {
logger.info('ParserConfigurationItems ---- parser_id', parser_id);
const chunkNum_parserArr = [DocumentParserType.Naive]
const auto_parserArr = [DocumentParserType.Naive, DocumentParserType.Manual, DocumentParserType.Paper, DocumentParserType.Book, DocumentParserType.Laws, DocumentParserType.Presentation, DocumentParserType.One]
const itemsArr = []
if (chunkNum_parserArr.includes(parser_id)) {
const item = (
<Card sx={{ mb: 2, px: 2 }}>
<CardContent>
{/* 建议文本块大小 */}
<ChunkTokenNumberItem />
{/* 文本分段标识符 */}
<DelimiterItem />
</CardContent>
</Card>
)
itemsArr.push(item)
}
if (auto_parserArr.includes(parser_id)) {
const item = (
<Card sx={{ mb: 2, px: 2 }}>
<CardContent>
{/* 自动关键词提取 */}
<AutoKeywordsItem />
{/* 自动问题提取 */}
<AutoQuestionsItem />
</CardContent>
</Card>
)
itemsArr.push(item)
}
return (
<Box>
{itemsArr}
</Box>
)
}
interface DocumentParseFormProps {
form?: UseFormReturn;
}
export default function DocumentParseForm({
form: propForm,
}: DocumentParseFormProps = {}) {
const { t } = useTranslation();
// 允许从 FormProvider 获取 form也允许通过 props 传入
let contextForm: UseFormReturn | null = null;
try {
contextForm = useFormContext();
} catch (err) {
contextForm = null;
}
const form = propForm || contextForm || null;
if (!form) {
return (
<Box sx={{ p: 2, textAlign: 'center' }}>
{t('form.formConfigError')}
</Box>
);
}
const { control } = form;
// 同步 pipeline_id 选择影响解析模式默认值
const pipeline_id = useWatch({ control, name: 'pipeline_id' });
// 同步 parser_id 选择影响解析模式默认值
const parser_id = useWatch({ control, name: 'parser_id' });
const [parseType, setParseType] = useState<ParseType>(ParseType.BuildIn);
useEffect(() => {
setParseType(pipeline_id ? ParseType.Pipeline : ParseType.BuildIn);
}, [pipeline_id]);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Card sx={{ px: 3 }}>
<CardContent>
<RadioFormField
name="parseType"
label={t('knowledgeConfiguration.parseType')}
defaultValue={parseType}
options={[
{ value: ParseType.BuildIn, label: 'Built-in' },
{ value: ParseType.Pipeline, label: 'Pipeline' },
]}
onChangeValue={(v) => setParseType(v as ParseType)}
/>
{/* 基于模式内置显示切片方法Pipeline 显示选择器 */}
{parseType === ParseType.BuildIn ? (
<ChunkMethodItem />
) : (
<PipelineSelectorItem />
)}
</CardContent>
</Card>
{/* 动态配置内容:始终渲染,内部按 parseType 控制基础配置显示 */}
{
parseType === ParseType.BuildIn &&
<ParserConfigurationItems parser_id={parser_id} />
}
</Box>
);
}

View File

@@ -0,0 +1,100 @@
import React, { useEffect } from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useForm, FormProvider } from 'react-hook-form';
import type { IKnowledgeFile, IKnowledgeFileParserConfig } from '@/interfaces/database/knowledge';
import knowledgeService from '@/services/knowledge_service';
import logger from '@/utils/logger';
import DocumentParseForm from './DocumentParseForm';
interface DocumentParserDialogProps {
open: boolean;
file?: IKnowledgeFile;
onClose: () => void;
onSubmittingChange?: (submitting: boolean) => void;
onSuccess?: () => void;
}
export default function DocumentParserDialog({
open,
file,
onClose,
onSubmittingChange,
onSuccess,
}: DocumentParserDialogProps) {
const { t } = useTranslation();
logger.info('DocumentParserDialog 组件渲染', { open, file });
// 统一构建默认表单值(与 BasicConfigItems 字段保持一致)
const buildDefaultValues = (f?: IKnowledgeFile) => {
const cfg: Partial<IKnowledgeFileParserConfig> = f?.parser_config || {};
const defaultValues: IKnowledgeFile = {
parser_id: f?.parser_id ?? '',
pipeline_id: f?.pipeline_id ?? '',
pipeline_name: f?.pipeline_name ?? '',
parser_config: cfg,
} as any;
return defaultValues;
};
const parserFormMethods = useForm({
defaultValues: buildDefaultValues(file),
});
// 当对话框打开或文件变更时,重置表单为最新数据;关闭时清空到默认值
useEffect(() => {
if (open && file) {
parserFormMethods.reset(buildDefaultValues(file));
}
if (!open) {
parserFormMethods.reset(buildDefaultValues(undefined));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, file]);
const handleSubmit = async (data: {
parser_id: string;
pipeline_id?: string;
parser_config: IKnowledgeFileParserConfig;
}) => {
try {
onSubmittingChange?.(true);
if (!file) return; // 无文件时不提交
await knowledgeService.changeDocumentParser({
doc_id: file.id,
parser_config: data.parser_config,
parser_id: data.parser_id,
pipeline_id: data.pipeline_id || '',
});
onSuccess?.();
onClose();
} catch (err) {
logger.error('更新文档解析器配置失败', err);
} finally {
onSubmittingChange?.(false);
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>{t('knowledgeDetails.dataPipeline')}</DialogTitle>
<DialogContent dividers>
{file ? (
<FormProvider {...parserFormMethods}>
<DocumentParseForm
// @ts-ignore
form={parserFormMethods}
/>
</FormProvider>
) : null}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t('common.cancel')}</Button>
<Button variant="contained" onClick={parserFormMethods.handleSubmit(handleSubmit)}>
{t('common.save')}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -52,9 +52,9 @@ const KnowledgeInfoCard: React.FC<KnowledgeInfoCardProps> = ({ knowledgeBase })
<Typography variant="body2" color="text.secondary">
{t('knowledge.updateTime')}: {dayjs(knowledgeBase.update_time).format('YYYY-MM-DD HH:mm:ss')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('knowledge.language')}: {knowledgeBase.language || 'English'}
</Typography>
{/* <Typography variant="body2" color="text.secondary"> */}
{/* {t('knowledge.language')}: {knowledgeBase.language || 'English'} */}
{/* </Typography> */}
<Typography variant="body2" color="text.secondary">
{t('knowledge.permission')}: {knowledgeBase.permission}
</Typography>

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { Menu, MenuItem, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
interface ParserContextMenuProps {
anchorEl: HTMLElement | null;
open: boolean;
onClose: () => void;
onOpenParser: () => void;
onOpenMetadata: () => void;
}
export default function ParserContextMenu({
anchorEl,
open,
onClose,
onOpenParser,
onOpenMetadata,
}: ParserContextMenuProps) {
const { t } = useTranslation();
return (
<Menu anchorEl={anchorEl} open={open} onClose={onClose}>
<MenuItem onClick={() => { onOpenParser(); }}>
<Typography>{t('knowledgeDetails.dataPipeline')}</Typography>
</MenuItem>
<MenuItem onClick={() => { onOpenMetadata(); }}>
<Typography>{t('knowledgeDetails.setMetaData')}</Typography>
</MenuItem>
</Menu>
);
}

View File

@@ -162,20 +162,20 @@ function TestChunkResult(props: TestChunkResultProps) {
</Typography>
<Stack direction="row" spacing={1}>
<Chip
label={t('knowledge.similarity', { value: (chunk.similarity * 100).toFixed(1) })}
label={`${t('knowledge.similarity')}: ${(chunk.similarity).toFixed(0)}`}
size="small"
color="primary"
/>
{chunk.vector_similarity !== undefined && (
<Chip
label={t('knowledge.vectorSimilarity', { value: (chunk.vector_similarity * 100).toFixed(1) })}
label={`${t('knowledge.vectorSimilarity')}: ${(chunk.vector_similarity).toFixed(2)}`}
size="small"
variant="outlined"
/>
)}
{chunk.term_similarity !== undefined && (
<Chip
label={t('knowledge.termSimilarity', { value: (chunk.term_similarity * 100).toFixed(1) })}
label={`${t('knowledge.termSimilarity')}: ${(chunk.term_similarity).toFixed(0)}`}
size="small"
variant="outlined"
/>

View File

@@ -134,7 +134,7 @@ function KnowledgeBaseDetail() {
// 删除文件
const handleDeleteFiles = async () => {
try {
await deleteDocuments(Array.from(rowSelectionModel.ids) as string[]);
await deleteDocuments(Array.from(rowSelectionModel.ids).map((id) => id.toString()));
setDeleteDialogOpen(false);
setRowSelectionModel({
type: 'include',

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Box, Grid, Paper, Typography, IconButton, TextField, Tabs, Tab, Fab, Avatar, Chip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Card, CardContent, Divider } from '@mui/material';
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
import {
@@ -10,7 +10,6 @@ import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import { useKnowledgeOverview, useKnowledgeDetail } from '@/hooks/knowledge-hooks';
import logger from '@/utils/logger';
import i18n from '@/locales';
import { enUS, zhCN } from '@mui/x-data-grid/locales';
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
import { LanguageAbbreviation } from '@/constants/common';
@@ -38,14 +37,14 @@ interface KnowledgeLogsPageProps {
}
function KnowledgeLogsPage({ embedded = false, kbId: kbIdProp }: KnowledgeLogsPageProps) {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const [paginationModel, setPaginationModel] = React.useState({ page: 0, pageSize: 50 });
// 根据当前语言获取DataGrid的localeText
const getDataGridLocale = () => {
const getDataGridLocale = useCallback(() => {
const currentLanguage = i18n.language;
return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS;
};
}, [i18n]);
// 路由参数与数据Hook
const { id: kbIdParam = '' } = useParams();

View File

@@ -224,7 +224,7 @@ const Login = () => {
fullWidth
id="register-confirm-password"
type={showConfirmPassword ? 'text' : 'password'}
placeholder={t('confirmPassword')}
placeholder={t('login.confirmPassword')}
autoComplete="new-password"
{...registerForm.register('confirmPassword', registerValidation.confirmPassword)}
error={!!registerForm.formState.errors.confirmPassword}

View File

@@ -20,7 +20,8 @@ import logger from '@/utils/logger';
interface ChangePasswordDialogProps {
open: boolean;
onClose: () => void;
changeUserPassword: (data: { password: string; new_password: string }) => Promise<void>;
onSuccess: () => void;
changeUserPassword: (data: { password: string; new_password: string }) => Promise<any>;
}
interface PasswordFormData {
@@ -32,7 +33,7 @@ interface PasswordFormData {
/**
* 修改密码对话框
*/
function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePasswordDialogProps) {
function ChangePasswordDialog({ open, onClose, onSuccess, changeUserPassword }: ChangePasswordDialogProps) {
const { t } = useTranslation();
const { showMessage } = useSnackbar();
@@ -133,20 +134,19 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
setLoading(true);
try {
await changeUserPassword({
const res = await changeUserPassword({
password: formData.currentPassword,
new_password: formData.newPassword
});
logger.info('修改密码成功:', res);
showMessage.success(t('setting.passwordChangeSuccess'));
handleClose();
// delay 1000 ms
setTimeout(() => onSuccess(), 1000);
} catch (error: any) {
logger.error('修改密码失败:', error);
if (error.response?.status === 400) {
showMessage.error(t('setting.currentPasswordIncorrect'));
} else {
showMessage.error(t('setting.passwordChangeError'));
}
} finally {
setLoading(false);
}
@@ -217,7 +217,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
value={formData.newPassword}
onChange={handleInputChange('newPassword')}
error={!!errors.newPassword}
helperText={errors.newPassword || '密码长度至少6位'}
helperText={errors.newPassword}
InputProps={{
endAdornment: (
<InputAdornment position="end">

View File

@@ -7,11 +7,14 @@ import ChangePasswordDialog from "./components/ChangePasswordDialog";
import { useProfileSetting } from "@/hooks/setting-hooks";
import logger from "@/utils/logger";
import { useUserData } from "@/hooks/useUserData";
import { useAuth } from '@/hooks/login-hooks';
import { useNavigate } from "react-router-dom";
function ProfileSetting() {
const { t } = useTranslation();
const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting();
const { refreshUserData } = useUserData();
const { logout } = useAuth();
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
logger.debug('userInfo', userInfo);
@@ -66,6 +69,10 @@ function ProfileSetting() {
<ChangePasswordDialog
open={passwordDialogOpen}
onClose={handleClosePasswordDialog}
onSuccess={() => {
// 使用 useAuth 的 logout确保清除本地存储令牌并跳转登录
logout();
}}
changeUserPassword={changeUserPasswordFunc}
/>
</Box>

View File

@@ -34,6 +34,7 @@ import {
ExitToApp as ExitToAppIcon
} from '@mui/icons-material';
import { useTeamSetting } from '@/hooks/setting-hooks';
import { useDialog } from '@/hooks/useDialog';
interface TenantUser {
id: string;
@@ -88,8 +89,15 @@ function TeamsSetting() {
}
};
const dialog = useDialog();
const handleDeleteUser = async (userId: string) => {
await deleteUser(userId);
dialog.confirm({
content: t('setting.sureDelete'),
onConfirm: async () => {
await deleteUser(userId);
}
});
};
const handleAgreeTenant = async (tenantId: string) => {
@@ -170,7 +178,7 @@ function TeamsSetting() {
<IconButton
size="small"
color="error"
onClick={() => handleDeleteUser(user.id)}
onClick={() => handleDeleteUser(user.user_id)}
>
<DeleteIcon />
</IconButton>

View File

@@ -106,7 +106,7 @@ const knowledgeService = {
// 删除文档
removeDocument: (data: { doc_id: string | Array<string | number> }) => {
return post(api.document_rm, data);
return request.post(api.document_rm, data);
},
// 删除文档DELETE方法
@@ -119,17 +119,26 @@ const knowledgeService = {
* @param data 文档ID列表和状态 status 0 禁用 1 启用
*/
changeDocumentStatus: (data: { doc_ids: Array<string | number>; status: string | number }) => {
return post(api.document_change_status, data);
return request.post(api.document_change_status, data);
},
// 运行文档处理
runDocument: (data: IRunDocumentRequestBody) => {
return post(api.document_run, data);
return request.post(api.document_run, data);
},
// 更改文档解析器配置
changeDocumentParser: (data: { doc_id: string; parser_config: IKnowledgeFileParserConfig }) => {
return post(api.document_change_parser, data);
// 更改文档解析器配置(兼容可选的 parser_id 与 pipeline_id 字段)
changeDocumentParser: (data: { doc_id: string; parser_config: IKnowledgeFileParserConfig; parser_id?: string; pipeline_id?: string }) => {
return request.post(api.document_change_parser, data);
},
/**
* 设置文档元数据
* @param data 文档ID和元数据字符串
* @param data.meta json string 文档元数据
*/
setDocumentMetaData: (data: { doc_id: string; meta: string }) => {
return request.post(api.setMeta, data);
},
// 获取文档缩略图

View File

@@ -184,17 +184,29 @@ request.interceptors.response.use(
});
const { status, data } = error.response || {};
if (status == 401) {
logger.info('401', data)
if (status == 401 || status == 400) {
logger.info('401 || 400', data)
const detail = data['detail']
if (detail) {
const error = new CustomError(detail || i18n.t('message.requestError'));
if (detail.includes('not registered')) {
if (status == 401) {
const arr = ['not registered', 'Password error', 'Email and password do not match']
const redirectArr = ['Invalid or expired token', 'No Authorization']
if (arr.some(item => detail.includes(item))) {
error.code = data?.code || -1;
error.response = data;
snackbar.error(detail);
} else if (redirectArr.some(item => detail.includes(item))) {
redirectToLogin();
} else {
error.code = data?.code || -1;
error.response = data;
snackbar.error(detail);
}
} else {
error.code = data?.code || -1;
error.response = data;
snackbar.error(detail);
} else {
redirectToLogin();
}
return Promise.reject(error);
}

View File

@@ -7,6 +7,6 @@
],
// exclude rag_web_core/**/*
"exclude": [
"rag_web_core/**"
"ragflow_web/**"
]
}