Compare commits
5 Commits
79ee33be7c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa42853d41 | ||
|
|
70926de445 | ||
|
|
898c0988db | ||
|
|
79d0cf6f3b | ||
|
|
c992c04c7c |
@@ -6,18 +6,28 @@ import tseslint from 'typescript-eslint'
|
|||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist', 'rag_web_core']),
|
globalIgnores(['dist', 'ragflow_web']),
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
rules: {},
|
||||||
extends: [
|
extends: [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
// tseslint.configs.recommended,
|
||||||
reactHooks.configs['recommended-latest'],
|
reactHooks.configs['recommended-latest'],
|
||||||
reactRefresh.configs.vite,
|
reactRefresh.configs.vite,
|
||||||
],
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
|
parser: tseslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': tseslint.plugin,
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -38,7 +38,9 @@
|
|||||||
"react-is": "18.3.1",
|
"react-is": "18.3.1",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"monaco-editor": "^0.52.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
|||||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
|||||||
'@emotion/styled':
|
'@emotion/styled':
|
||||||
specifier: ^11.14.1
|
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)
|
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':
|
'@mui/icons-material':
|
||||||
specifier: ^7.3.4
|
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)
|
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:
|
loglevel:
|
||||||
specifier: ^1.9.2
|
specifier: ^1.9.2
|
||||||
version: 1.9.2
|
version: 1.9.2
|
||||||
|
monaco-editor:
|
||||||
|
specifier: ^0.52.2
|
||||||
|
version: 0.52.2
|
||||||
pdfjs-dist:
|
pdfjs-dist:
|
||||||
specifier: ^5.4.394
|
specifier: ^5.4.394
|
||||||
version: 5.4.394
|
version: 5.4.394
|
||||||
@@ -8785,6 +8791,9 @@ packages:
|
|||||||
moment@2.30.1:
|
moment@2.30.1:
|
||||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
||||||
|
|
||||||
|
monaco-editor@0.52.2:
|
||||||
|
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||||
|
|
||||||
monaco-editor@0.54.0:
|
monaco-editor@0.54.0:
|
||||||
resolution: {integrity: sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==}
|
resolution: {integrity: sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==}
|
||||||
|
|
||||||
@@ -14016,6 +14025,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
state-local: 1.0.7
|
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)':
|
'@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:
|
dependencies:
|
||||||
'@monaco-editor/loader': 1.6.1
|
'@monaco-editor/loader': 1.6.1
|
||||||
@@ -22253,6 +22269,8 @@ snapshots:
|
|||||||
|
|
||||||
moment@2.30.1: {}
|
moment@2.30.1: {}
|
||||||
|
|
||||||
|
monaco-editor@0.52.2: {}
|
||||||
|
|
||||||
monaco-editor@0.54.0:
|
monaco-editor@0.54.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
dompurify: 3.1.7
|
dompurify: 3.1.7
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
yes: 'Yes',
|
yes: 'Yes',
|
||||||
no: 'No',
|
no: 'No',
|
||||||
total: 'Total',
|
total: 'Total',
|
||||||
|
know: 'Known',
|
||||||
rename: 'Rename',
|
rename: 'Rename',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
@@ -106,6 +107,7 @@ export default {
|
|||||||
noMoreData: `That's all. Nothing more.`,
|
noMoreData: `That's all. Nothing more.`,
|
||||||
},
|
},
|
||||||
knowledgeDetails: {
|
knowledgeDetails: {
|
||||||
|
pleaseAddEmbeddingModel: 'Please add embedding model and LLM in the model provider first, then set them in "Set Default Model".',
|
||||||
localUpload: 'Local Upload',
|
localUpload: 'Local Upload',
|
||||||
fileSize: 'File Size',
|
fileSize: 'File Size',
|
||||||
fileType: 'File Type',
|
fileType: 'File Type',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
yes: '是',
|
yes: '是',
|
||||||
no: '否',
|
no: '否',
|
||||||
total: '总共',
|
total: '总共',
|
||||||
|
know: '知道了',
|
||||||
rename: '重命名',
|
rename: '重命名',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
@@ -99,6 +100,7 @@ export default {
|
|||||||
noMoreData: '没有更多数据了',
|
noMoreData: '没有更多数据了',
|
||||||
},
|
},
|
||||||
knowledgeDetails: {
|
knowledgeDetails: {
|
||||||
|
pleaseAddEmbeddingModel: '请先在模型提供商中添加嵌入模型和LLM,然后在"设置默认模型"中设置它们。',
|
||||||
localUpload: '本地上传',
|
localUpload: '本地上传',
|
||||||
fileSize: '文件大小',
|
fileSize: '文件大小',
|
||||||
fileType: '文件类型',
|
fileType: '文件类型',
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import agentService from '@/services/agent_service';
|
import agentService from '@/services/agent_service';
|
||||||
|
import userService from '@/services/user_service';
|
||||||
import type { IAgent, IGraph } from '@/interfaces/database/agent';
|
import type { IAgent, IGraph } from '@/interfaces/database/agent';
|
||||||
import type { IAgentPaginationParams, IAgentCreateRequestBody, IAgentSettingRequestBody } from '@/interfaces/request/agent';
|
import type { IAgentPaginationParams, IAgentCreateRequestBody, IAgentSettingRequestBody } from '@/interfaces/request/agent';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSnackbar } from '@/hooks/useSnackbar';
|
import { useSnackbar } from '@/hooks/useSnackbar';
|
||||||
|
import { useDialog } from '@/hooks/useDialog';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
export interface UseAgentListState {
|
export interface UseAgentListState {
|
||||||
agents: IAgent[];
|
agents: IAgent[];
|
||||||
@@ -39,6 +43,28 @@ export const useAgentList = (initialParams?: IAgentPaginationParams) => {
|
|||||||
const [currentPage, setCurrentPage] = useState(initialParams?.page || 1);
|
const [currentPage, setCurrentPage] = useState(initialParams?.page || 1);
|
||||||
const [pageSize, setPageSize] = useState(initialParams?.page_size || 10);
|
const [pageSize, setPageSize] = useState(initialParams?.page_size || 10);
|
||||||
const [keywords, setKeywords] = useState(initialParams?.keywords || '');
|
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) => {
|
const fetchAgentList = useCallback(async (params?: IAgentPaginationParams) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -81,6 +107,7 @@ export const useAgentList = (initialParams?: IAgentPaginationParams) => {
|
|||||||
currentPage,
|
currentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
keywords,
|
keywords,
|
||||||
|
checkDefaultModel,
|
||||||
fetchAgents: fetchAgentList,
|
fetchAgents: fetchAgentList,
|
||||||
setKeywords,
|
setKeywords,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
|
|||||||
@@ -279,6 +279,10 @@ export interface IKnowledgeFile {
|
|||||||
name: string;
|
name: string;
|
||||||
/** 解析器ID */
|
/** 解析器ID */
|
||||||
parser_id: string;
|
parser_id: string;
|
||||||
|
/** 流水线ID,可选 */
|
||||||
|
pipeline_id?: string;
|
||||||
|
/** 流水线名称,可选 */
|
||||||
|
pipeline_name?: string;
|
||||||
/** 处理开始时间,可选 */
|
/** 处理开始时间,可选 */
|
||||||
process_begin_at?: any;
|
process_begin_at?: any;
|
||||||
/** 处理持续时间 */
|
/** 处理持续时间 */
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default {
|
|||||||
total: 'Total',
|
total: 'Total',
|
||||||
rename: 'Rename',
|
rename: 'Rename',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
|
know: 'Known',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
namePlaceholder: 'Please input name',
|
namePlaceholder: 'Please input name',
|
||||||
next: 'Next',
|
next: 'Next',
|
||||||
@@ -425,6 +426,7 @@ export default {
|
|||||||
paginationInfo: 'Total {{total}} knowledge bases, page {{current}} of {{totalPages}}',
|
paginationInfo: 'Total {{total}} knowledge bases, page {{current}} of {{totalPages}}',
|
||||||
},
|
},
|
||||||
knowledgeDetails: {
|
knowledgeDetails: {
|
||||||
|
pleaseAddEmbeddingModel: 'Please add embedding model and LLM in the model provider first, then set them in "Set Default Model".',
|
||||||
fileSize: 'File Size',
|
fileSize: 'File Size',
|
||||||
fileType: 'File Type',
|
fileType: 'File Type',
|
||||||
uploadedBy: 'Uploaded by',
|
uploadedBy: 'Uploaded by',
|
||||||
@@ -599,6 +601,7 @@ export default {
|
|||||||
redo: 'Do you want to clear the existing {{chunkNum}} chunks?',
|
redo: 'Do you want to clear the existing {{chunkNum}} chunks?',
|
||||||
setMetaData: 'Set Meta Data',
|
setMetaData: 'Set Meta Data',
|
||||||
pleaseInputJson: 'Please enter JSON',
|
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>
|
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>
|
<p>Examples:</p>
|
||||||
<b>The meta data is:</b><br>
|
<b>The meta data is:</b><br>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default {
|
|||||||
cancel: '否',
|
cancel: '否',
|
||||||
total: '总共',
|
total: '总共',
|
||||||
rename: '重命名',
|
rename: '重命名',
|
||||||
|
know: '知道了',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
namePlaceholder: '请输入名称',
|
namePlaceholder: '请输入名称',
|
||||||
@@ -423,6 +424,7 @@ export default {
|
|||||||
paginationInfo: '共 {{total}} 个知识库,第 {{current}} 页,共 {{totalPages}} 页',
|
paginationInfo: '共 {{total}} 个知识库,第 {{current}} 页,共 {{totalPages}} 页',
|
||||||
},
|
},
|
||||||
knowledgeDetails: {
|
knowledgeDetails: {
|
||||||
|
pleaseAddEmbeddingModel: '请先在模型提供商中添加嵌入模型和LLM,然后在"设置默认模型"中设置它们。',
|
||||||
fileSize: '文件大小',
|
fileSize: '文件大小',
|
||||||
fileType: '文件类型',
|
fileType: '文件类型',
|
||||||
uploadedBy: '创建者',
|
uploadedBy: '创建者',
|
||||||
@@ -593,6 +595,7 @@ export default {
|
|||||||
redo: '是否清空已有 {{chunkNum}}个 chunk?',
|
redo: '是否清空已有 {{chunkNum}}个 chunk?',
|
||||||
setMetaData: '设置元数据',
|
setMetaData: '设置元数据',
|
||||||
pleaseInputJson: '请输入JSON',
|
pleaseInputJson: '请输入JSON',
|
||||||
|
invalidJson: '无效的JSON格式',
|
||||||
documentMetaTips: `<p>元数据为 Json 格式(不可搜索)。如果提示中包含此文档的任何块,它将被添加到 LLM 的提示中。</p>
|
documentMetaTips: `<p>元数据为 Json 格式(不可搜索)。如果提示中包含此文档的任何块,它将被添加到 LLM 的提示中。</p>
|
||||||
<p>示例:</p>
|
<p>示例:</p>
|
||||||
<b>元数据为:</b><br>
|
<b>元数据为:</b><br>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import EditAgentDialog from './components/EditAgentDialog';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDialog } from '@/hooks/useDialog';
|
import { useDialog } from '@/hooks/useDialog';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
function AgentListPage() {
|
function AgentListPage() {
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
@@ -31,6 +30,7 @@ function AgentListPage() {
|
|||||||
pageSize,
|
pageSize,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
setKeywords,
|
setKeywords,
|
||||||
|
checkDefaultModel,
|
||||||
refresh,
|
refresh,
|
||||||
} = useAgentList({ page: 1, page_size: 10 });
|
} = useAgentList({ page: 1, page_size: 10 });
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@ function AgentListPage() {
|
|||||||
const ops = useAgentOperations();
|
const ops = useAgentOperations();
|
||||||
|
|
||||||
const totalPages = useMemo(() => {
|
const totalPages = useMemo(() => {
|
||||||
return Math.ceil((agents?.length || 0) / pageSize) || 1;
|
return Math.ceil((total || 0) / pageSize) || 1;
|
||||||
}, [agents, pageSize]);
|
}, [total, pageSize]);
|
||||||
|
|
||||||
const currentPageData = useMemo(() => {
|
const currentPageData = useMemo(() => {
|
||||||
const startIndex = (currentPage - 1) * pageSize;
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
@@ -108,7 +108,7 @@ function AgentListPage() {
|
|||||||
onCreateAgent={() => setCreateOpen(true)}
|
onCreateAgent={() => setCreateOpen(true)}
|
||||||
onEdit={(agent) => { setEditTarget(agent); setEditOpen(true); }}
|
onEdit={(agent) => { setEditTarget(agent); setEditOpen(true); }}
|
||||||
onView={(agent) => {
|
onView={(agent) => {
|
||||||
navigate(`/route-ragflow/agent/${agent.id}`);
|
checkDefaultModel(agent.id);
|
||||||
}}
|
}}
|
||||||
onDelete={async (agent) => {
|
onDelete={async (agent) => {
|
||||||
const confirmed = await dialog.confirm({
|
const confirmed = await dialog.confirm({
|
||||||
|
|||||||
@@ -60,10 +60,12 @@ interface ChunkListResultProps {
|
|||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
docName?: string;
|
docName?: string;
|
||||||
onLocate?: (chunk: IChunk) => void;
|
onLocate?: (chunk: IChunk) => void;
|
||||||
|
selectedChunkId?: string;
|
||||||
|
onSelect?: (chunk: IChunk) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChunkListResult(props: ChunkListResultProps) {
|
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();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// 选择状态
|
// 选择状态
|
||||||
@@ -363,14 +365,13 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
'&:hover': {
|
'&:hover': {
|
||||||
boxShadow: 2,
|
boxShadow: 2,
|
||||||
},
|
},
|
||||||
border: selectedChunks.includes(chunk.chunk_id) ? '2px solid' : '1px solid',
|
border: (selectedChunks.includes(chunk.chunk_id) || selectedChunkId === chunk.chunk_id) ? '2px solid' : '1px solid',
|
||||||
borderColor: selectedChunks.includes(chunk.chunk_id)
|
borderColor: (selectedChunks.includes(chunk.chunk_id) || selectedChunkId === chunk.chunk_id)
|
||||||
? 'primary.main'
|
? 'primary.main'
|
||||||
: chunk.available_int === 1
|
: (chunk.available_int === 1 ? 'success.light' : 'grey.300'),
|
||||||
? 'success.light'
|
backgroundColor: (selectedChunks.includes(chunk.chunk_id) || selectedChunkId === chunk.chunk_id) ? 'action.selected' : 'background.paper',
|
||||||
: 'grey.300',
|
|
||||||
backgroundColor: selectedChunks.includes(chunk.chunk_id) ? 'action.selected' : 'background.paper',
|
|
||||||
}}
|
}}
|
||||||
|
onClick={() => onSelect?.(chunk)}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ p: 2 }}>
|
<CardContent sx={{ p: 2 }}>
|
||||||
{/* 头部操作区域 */}
|
{/* 头部操作区域 */}
|
||||||
@@ -389,7 +390,7 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
/>
|
/>
|
||||||
{/* 定位到文档位置 */}
|
{/* 定位到文档位置 */}
|
||||||
<Tooltip title={'定位'}>
|
<Tooltip title={'定位'}>
|
||||||
<IconButton size="small" onClick={() => onLocate?.(chunk)}>
|
<IconButton size="small" onClick={() => { onSelect?.(chunk); onLocate?.(chunk); }}>
|
||||||
<ZoomInIcon />
|
<ZoomInIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</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}`)}
|
onMouseEnter={(e) => handleImageHover(e, `${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`)}
|
||||||
onMouseLeave={handleImageHoverClose}
|
onMouseLeave={handleImageHoverClose}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -37,17 +37,7 @@ function ChunkParsedResult() {
|
|||||||
const kb_id = searchParams.get('kb_id');
|
const kb_id = searchParams.get('kb_id');
|
||||||
const doc_id = searchParams.get('doc_id');
|
const doc_id = searchParams.get('doc_id');
|
||||||
|
|
||||||
const [knowledgeBase, setKnowledgeBase] = useState<IKnowledge | null>(null);
|
|
||||||
const [document, setDocument] = useState<IKnowledgeFile | null>(null);
|
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
const [documentFile, setDocumentFile] = useState<Blob | null>(null);
|
|
||||||
const [fileUrl, setFileUrl] = useState<string>('');
|
|
||||||
const [fileLoading, setFileLoading] = useState(false);
|
|
||||||
const [previewOverrideUrl, setPreviewOverrideUrl] = useState<string>('');
|
|
||||||
const [focusPage, setFocusPage] = useState<number | null>(null);
|
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
|
||||||
const pdfContainerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const [pdfRendered, setPdfRendered] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// 使用chunk列表hook
|
// 使用chunk列表hook
|
||||||
const {
|
const {
|
||||||
@@ -66,6 +56,19 @@ function ChunkParsedResult() {
|
|||||||
keywords: searchKeyword
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -204,6 +207,10 @@ function ChunkParsedResult() {
|
|||||||
|
|
||||||
const pageWrapper = window.document.createElement('div');
|
const pageWrapper = window.document.createElement('div');
|
||||||
pageWrapper.setAttribute('data-page-index', String(pageNum));
|
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);
|
pageWrapper.appendChild(canvas);
|
||||||
container.appendChild(pageWrapper);
|
container.appendChild(pageWrapper);
|
||||||
}
|
}
|
||||||
@@ -278,6 +285,7 @@ function ChunkParsedResult() {
|
|||||||
if (documentFile?.type === 'application/pdf') {
|
if (documentFile?.type === 'application/pdf') {
|
||||||
setFocusPage(page && !Number.isNaN(page) ? page : null);
|
setFocusPage(page && !Number.isNaN(page) ? page : null);
|
||||||
setPreviewOverrideUrl('');
|
setPreviewOverrideUrl('');
|
||||||
|
setSelectedChunkId(chunk.chunk_id || '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +294,107 @@ function ChunkParsedResult() {
|
|||||||
setFocusPage(null);
|
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 = () => {
|
const renderPreview = () => {
|
||||||
// 如果有覆盖的图片URL,直接显示图片
|
// 如果有覆盖的图片URL,直接显示图片
|
||||||
@@ -442,6 +551,11 @@ function ChunkParsedResult() {
|
|||||||
onRefresh={refresh}
|
onRefresh={refresh}
|
||||||
docName={document?.name}
|
docName={document?.name}
|
||||||
onLocate={handleLocate}
|
onLocate={handleLocate}
|
||||||
|
selectedChunkId={selectedChunkId}
|
||||||
|
onSelect={(chunk) => {
|
||||||
|
setSelectedChunkId(chunk.chunk_id || '');
|
||||||
|
handleLocate(chunk);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ import dayjs from 'dayjs';
|
|||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
import { LanguageAbbreviation } from '@/constants/common';
|
import { LanguageAbbreviation } from '@/constants/common';
|
||||||
import knowledgeService from '@/services/knowledge_service';
|
import knowledgeService from '@/services/knowledge_service';
|
||||||
|
import ParserContextMenu from './ParserContextMenu';
|
||||||
|
import DocumentParserDialog from './DocumentParserDialog';
|
||||||
|
import DocumentMetadataDialog from './DocumentMetadataDialog';
|
||||||
|
|
||||||
|
|
||||||
interface DocumentListComponentProps {
|
interface DocumentListComponentProps {
|
||||||
@@ -215,6 +218,15 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
|||||||
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
||||||
const [newFileName, setNewFileName] = useState('');
|
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();
|
const { i18n, t } = useTranslation();
|
||||||
|
|
||||||
// 根据当前语言获取DataGrid的localeText
|
// 根据当前语言获取DataGrid的localeText
|
||||||
@@ -346,6 +358,19 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
|||||||
setSelectedSuffix(typeof value === 'string' ? value.split(',') : value);
|
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 countSelected = (model: GridRowSelectionModel, totalCount: number): number => {
|
||||||
const size = model.ids.size ?? 0;
|
const size = model.ids.size ?? 0;
|
||||||
@@ -380,7 +405,7 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
|||||||
field: 'name',
|
field: 'name',
|
||||||
headerName: t('knowledge.fileName'),
|
headerName: t('knowledge.fileName'),
|
||||||
flex: 2,
|
flex: 2,
|
||||||
minWidth: 200,
|
minWidth: 120,
|
||||||
cellClassName: 'grid-center-cell',
|
cellClassName: 'grid-center-cell',
|
||||||
renderCell: (params) => (
|
renderCell: (params) => (
|
||||||
<Box
|
<Box
|
||||||
@@ -408,6 +433,13 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
|||||||
</Box>
|
</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',
|
field: 'type',
|
||||||
headerName: t('knowledge.type'),
|
headerName: t('knowledge.type'),
|
||||||
@@ -446,11 +478,15 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
|||||||
renderCell: (params) => getRunStatusChip(params.value, params.row.progress),
|
renderCell: (params) => getRunStatusChip(params.value, params.row.progress),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'create_time',
|
field: 'parser_id',
|
||||||
headerName: t('knowledge.uploadTime'),
|
headerName: t('knowledge.parser'),
|
||||||
flex: 1,
|
flex: 0.8,
|
||||||
minWidth: 140,
|
minWidth: 100,
|
||||||
valueFormatter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
|
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',
|
field: 'actions',
|
||||||
@@ -680,6 +716,23 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
|||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* Parser 列上下文菜单 */}
|
||||||
|
<ParserContextMenu
|
||||||
|
anchorEl={parserMenuAnchor}
|
||||||
|
open={Boolean(parserMenuAnchor)}
|
||||||
|
onClose={handleCloseParserMenu}
|
||||||
|
onOpenParser={() => {
|
||||||
|
setDialogFile(parserMenuFile);
|
||||||
|
setParserDialogOpen(true);
|
||||||
|
handleCloseParserMenu();
|
||||||
|
}}
|
||||||
|
onOpenMetadata={() => {
|
||||||
|
setDialogFile(parserMenuFile);
|
||||||
|
setMetadataDialogOpen(true);
|
||||||
|
handleCloseParserMenu();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 菜单 */}
|
{/* 菜单 */}
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
@@ -746,6 +799,22 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
|
|||||||
<Button onClick={handleRenameConfirm} variant="contained">{t('common.confirm')}</Button>
|
<Button onClick={handleRenameConfirm} variant="contained">{t('common.confirm')}</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
115
src/pages/knowledge/components/DocumentMetadataDialog.tsx
Normal file
115
src/pages/knowledge/components/DocumentMetadataDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
128
src/pages/knowledge/components/DocumentParseForm.tsx
Normal file
128
src/pages/knowledge/components/DocumentParseForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
100
src/pages/knowledge/components/DocumentParserDialog.tsx
Normal file
100
src/pages/knowledge/components/DocumentParserDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -52,9 +52,9 @@ const KnowledgeInfoCard: React.FC<KnowledgeInfoCardProps> = ({ knowledgeBase })
|
|||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t('knowledge.updateTime')}: {dayjs(knowledgeBase.update_time).format('YYYY-MM-DD HH:mm:ss')}
|
{t('knowledge.updateTime')}: {dayjs(knowledgeBase.update_time).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
{/* <Typography variant="body2" color="text.secondary"> */}
|
||||||
{t('knowledge.language')}: {knowledgeBase.language || 'English'}
|
{/* {t('knowledge.language')}: {knowledgeBase.language || 'English'} */}
|
||||||
</Typography>
|
{/* </Typography> */}
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t('knowledge.permission')}: {knowledgeBase.permission}
|
{t('knowledge.permission')}: {knowledgeBase.permission}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
32
src/pages/knowledge/components/ParserContextMenu.tsx
Normal file
32
src/pages/knowledge/components/ParserContextMenu.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -106,7 +106,7 @@ const knowledgeService = {
|
|||||||
|
|
||||||
// 删除文档
|
// 删除文档
|
||||||
removeDocument: (data: { doc_id: string | Array<string | number> }) => {
|
removeDocument: (data: { doc_id: string | Array<string | number> }) => {
|
||||||
return post(api.document_rm, data);
|
return request.post(api.document_rm, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除文档(DELETE方法)
|
// 删除文档(DELETE方法)
|
||||||
@@ -119,17 +119,26 @@ const knowledgeService = {
|
|||||||
* @param data 文档ID列表和状态 status 0 禁用 1 启用
|
* @param data 文档ID列表和状态 status 0 禁用 1 启用
|
||||||
*/
|
*/
|
||||||
changeDocumentStatus: (data: { doc_ids: Array<string | number>; status: string | number }) => {
|
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) => {
|
runDocument: (data: IRunDocumentRequestBody) => {
|
||||||
return post(api.document_run, data);
|
return request.post(api.document_run, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更改文档解析器配置
|
// 更改文档解析器配置(兼容可选的 parser_id 与 pipeline_id 字段)
|
||||||
changeDocumentParser: (data: { doc_id: string; parser_config: IKnowledgeFileParserConfig }) => {
|
changeDocumentParser: (data: { doc_id: string; parser_config: IKnowledgeFileParserConfig; parser_id?: string; pipeline_id?: string }) => {
|
||||||
return post(api.document_change_parser, data);
|
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取文档缩略图
|
// 获取文档缩略图
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
],
|
],
|
||||||
// exclude rag_web_core/**/*
|
// exclude rag_web_core/**/*
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"rag_web_core/**"
|
"ragflow_web/**"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user