diff --git a/web/.gitignore b/web/.gitignore index b398d222..0de8ef71 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -24,3 +24,9 @@ dist-ssr *.sw? package-lock.json + +# 文档和截图(不上传到仓库) +操作说明.md +记忆熊系统功能使用说明.md +截图清单.md +images/ \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx index c255f522..800b2a47 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -27,12 +27,19 @@ import 'dayjs/locale/en' import 'dayjs/locale/zh-cn' import 'dayjs/plugin/timezone' import 'dayjs/plugin/utc' +import { cookieUtils } from './utils/request'; function App() { const { t } = useTranslation(); const { locale, language, timeZone } = useI18n() + useEffect(() => { + const authToken = cookieUtils.get('authToken') + if (!authToken && !window.location.hash.includes('#/login')) { + window.location.href = `/#/login`; + } + }, []) useEffect(() => { document.title = t('memoryBear') diff --git a/web/src/api/knowledgeBase.ts b/web/src/api/knowledgeBase.ts index 9791ee8d..60f374a2 100644 --- a/web/src/api/knowledgeBase.ts +++ b/web/src/api/knowledgeBase.ts @@ -200,7 +200,7 @@ export const deleteFile = async (id: string) => { // 获取文档列表 export const getDocumentList = async (query: PathQuery) => { - const response = await request.get(`${apiPrefix}/documents/${query.kb_id}/${query.parent_id}/documents`, query); + const response = await request.get(`${apiPrefix}/documents/${query.kb_id}/documents`, query); return response as KnowledgeBaseDocumentData[]; }; // 文档详情 @@ -213,6 +213,11 @@ export const createDocument = async (data: KnowledgeBaseDocumentData) => { const response = await request.post(`${apiPrefix}/documents/document`, data); return response as KnowledgeBaseDocumentData; }; +// 自定义文档上传并创建 +export const createDocumentAndUpload = async ( data: any, params: PathQuery) => { + const response = await request.post(`${apiPrefix}/files/customtext`, data, { params } ); + return response as any; +}; // 更新文档 export const updateDocument = async (id: string, data: KnowledgeBaseDocumentData) => { const response = await request.put(`${apiPrefix}/documents/${id}`, data); @@ -223,9 +228,9 @@ export const deleteDocument = async (id: string) => { const response = await request.delete(`${apiPrefix}/documents/${id}`); return response; }; -// 文档解析 -export const parseDocument = async (id: string) => { - const response = await request.post(`${apiPrefix}/documents/${id}/chunks`); +// 文档解析 / 分块 +export const parseDocument = async (id: string, data: any) => { + const response = await request.post(`${apiPrefix}/documents/${id}/chunks`, data); return response as any; }; // 文档分块预览 diff --git a/web/src/assets/images/application/chat.png b/web/src/assets/images/application/chat.png deleted file mode 100644 index a8bb40fa..00000000 Binary files a/web/src/assets/images/application/chat.png and /dev/null differ diff --git a/web/src/assets/images/application/debuggingEmpty.png b/web/src/assets/images/application/debuggingEmpty.png deleted file mode 100644 index 0879d4e3..00000000 Binary files a/web/src/assets/images/application/debuggingEmpty.png and /dev/null differ diff --git a/web/src/assets/images/conversation/analysisEmpty.png b/web/src/assets/images/conversation/analysisEmpty.png deleted file mode 100644 index 6d497f31..00000000 Binary files a/web/src/assets/images/conversation/analysisEmpty.png and /dev/null differ diff --git a/web/src/assets/images/empty/404.png b/web/src/assets/images/empty/404.png deleted file mode 100644 index 3a4c684d..00000000 Binary files a/web/src/assets/images/empty/404.png and /dev/null differ diff --git a/web/src/assets/images/empty/noPermission.png b/web/src/assets/images/empty/noPermission.png deleted file mode 100644 index cacac721..00000000 Binary files a/web/src/assets/images/empty/noPermission.png and /dev/null differ diff --git a/web/src/assets/images/menu/userMemory_active.svg b/web/src/assets/images/menu/userMemory_active.svg deleted file mode 100644 index 554dc0bc..00000000 --- a/web/src/assets/images/menu/userMemory_active.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - 编组 29 - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/components/Empty/index.tsx b/web/src/components/Empty/index.tsx index 53f52941..3aa2bb50 100644 --- a/web/src/components/Empty/index.tsx +++ b/web/src/components/Empty/index.tsx @@ -6,7 +6,6 @@ interface EmptyProps { url?: string; size?: number | number[]; title?: string; - isNeedSubTitle?: boolean; subTitle?: string; className?: string; } @@ -14,7 +13,6 @@ const Empty: FC = ({ url, size = 200, title, - isNeedSubTitle = true, subTitle, className = '', }) => { @@ -22,12 +20,12 @@ const Empty: FC = ({ const width = Array.isArray(size) ? size[0] : size ? size : url ? 200 : 88; const height = Array.isArray(size) ? size[1] : size ? size : url ? 200 : 88; - const curSubTitle = isNeedSubTitle ? (subTitle || t('empty.tableEmpty')) : null; + subTitle = subTitle || t('empty.tableEmpty'); return (
404 - {title &&
{title}
} - {curSubTitle &&
{subTitle}
} + {title &&
{title}
} + {subTitle &&
{subTitle}
}
); } diff --git a/web/src/components/Header/index.tsx b/web/src/components/Header/index.tsx index 6ce5484e..9aeeab6b 100644 --- a/web/src/components/Header/index.tsx +++ b/web/src/components/Header/index.tsx @@ -3,6 +3,7 @@ import { Layout, Dropdown, Space, Breadcrumb } from 'antd'; import type { MenuProps, BreadcrumbProps } from 'antd'; import { UserOutlined, LogoutOutlined, SettingOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; import { useUser } from '@/store/user'; import { useMenu } from '@/store/menu'; import styles from './index.module.css' @@ -12,12 +13,35 @@ const { Header } = Layout; const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { const { t } = useTranslation(); + const location = useLocation(); const settingModalRef = useRef(null) const userInfoModalRef = useRef(null) const { user, logout } = useUser(); const { allBreadcrumbs } = useMenu(); - const breadcrumbs = allBreadcrumbs[source] || []; + + // 根据当前路由动态选择面包屑源 + const getBreadcrumbSource = () => { + const pathname = location.pathname; + + // 知识库列表页面使用默认的 space 面包屑 + if (pathname === '/knowledge-base') { + return 'space'; + } + + // 知识库详情相关页面使用独立的面包屑 + if (pathname.includes('/knowledge-base/') && pathname !== '/knowledge-base') { + return 'space-detail'; + } + + // 其他页面使用传入的 source + return source; + }; + + const breadcrumbSource = getBreadcrumbSource(); + const breadcrumbs = allBreadcrumbs[breadcrumbSource] || []; + + // 处理退出登录 const handleLogout = () => { diff --git a/web/src/components/Layout/AuthLayout.tsx b/web/src/components/Layout/AuthLayout.tsx index a969298d..c53be942 100644 --- a/web/src/components/Layout/AuthLayout.tsx +++ b/web/src/components/Layout/AuthLayout.tsx @@ -6,7 +6,6 @@ import { useNavigationBreadcrumbs } from '@/hooks/useNavigationBreadcrumbs'; import AppHeader from '@/components/Header'; import Sider from '@/components/SiderMenu' import { useUser } from '@/store/user'; -import { cookieUtils } from '@/utils/request'; const { Content } = Layout; @@ -19,12 +18,7 @@ const AuthLayout: FC = () => { // 自动更新面包屑导航 useNavigationBreadcrumbs('manage'); useEffect(() => { - const authToken = cookieUtils.get('authToken') - if (!authToken && !window.location.hash.includes('#/login')) { - window.location.href = `/#/login`; - } else { - getUserInfo() - } + getUserInfo() }, []); return ( diff --git a/web/src/components/Layout/AuthSpaceLayout.tsx b/web/src/components/Layout/AuthSpaceLayout.tsx index 17ee0bac..acc2cdb0 100644 --- a/web/src/components/Layout/AuthSpaceLayout.tsx +++ b/web/src/components/Layout/AuthSpaceLayout.tsx @@ -6,7 +6,6 @@ import { useNavigationBreadcrumbs } from '@/hooks/useNavigationBreadcrumbs'; import AppHeader from '@/components/Header'; import Sider from '@/components/SiderMenu'; import { useUser } from '@/store/user'; -import { cookieUtils } from '@/utils/request'; const { Content } = Layout; @@ -19,13 +18,8 @@ const AuthSpaceLayout: FC = () => { // 自动更新面包屑导航 useNavigationBreadcrumbs('space'); useEffect(() => { - const authToken = cookieUtils.get('authToken') - if (!authToken && !window.location.hash.includes('#/login')) { - window.location.href = `/#/login`; - } else { - getUserInfo() - getStorageType() - } + getUserInfo() + getStorageType() }, []); return ( diff --git a/web/src/hooks/useBreadcrumbManager.ts b/web/src/hooks/useBreadcrumbManager.ts new file mode 100644 index 00000000..bd18f4f8 --- /dev/null +++ b/web/src/hooks/useBreadcrumbManager.ts @@ -0,0 +1,248 @@ +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useMenu } from '@/store/menu'; +import type { MenuItem } from '@/store/menu'; + +export interface BreadcrumbItem { + id: string; + name: string; + type?: 'knowledgeBase' | 'folder' | 'document'; +} + +export interface BreadcrumbPath { + knowledgeBaseFolderPath: BreadcrumbItem[]; // 知识库文件夹路径 + knowledgeBase?: BreadcrumbItem; // 知识库信息 + documentFolderPath: BreadcrumbItem[]; // 文档文件夹路径 + document?: BreadcrumbItem; // 文档信息 +} + +export interface BreadcrumbOptions { + onKnowledgeBaseMenuClick?: () => void; + onKnowledgeBaseFolderClick?: (folderId: string, folderPath: BreadcrumbItem[]) => void; + // 新增:区分面包屑类型 + breadcrumbType?: 'list' | 'detail'; +} + +export const useBreadcrumbManager = (options?: BreadcrumbOptions) => { + const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); + const navigate = useNavigate(); + + const updateBreadcrumbs = useCallback((breadcrumbPath: BreadcrumbPath) => { + const breadcrumbType = options?.breadcrumbType || 'list'; + + // 获取基础面包屑,对于详情页面,使用列表页面的基础面包屑作为起点 + const baseBreadcrumbs = breadcrumbType === 'list' + ? (allBreadcrumbs['space'] || []) + : (allBreadcrumbs['space'] || []); // 详情页面也从 space 获取基础面包屑 + + // 只保留知识库菜单项之前的面包屑 + const knowledgeBaseMenuIndex = baseBreadcrumbs.findIndex(item => item.path === '/knowledge-base'); + const filteredBaseBreadcrumbs = knowledgeBaseMenuIndex >= 0 + ? baseBreadcrumbs.slice(0, knowledgeBaseMenuIndex + 1) + : baseBreadcrumbs; + + // 给"知识库管理"添加点击事件 + const breadcrumbsWithClick = filteredBaseBreadcrumbs.map((item) => { + if (item.path === '/knowledge-base') { + return { + ...item, + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + + if (options?.onKnowledgeBaseMenuClick) { + // 如果提供了回调函数,执行回调 + options.onKnowledgeBaseMenuClick(); + } else if (breadcrumbType === 'detail') { + // 知识库详情页面:没有回调函数时,返回到知识库列表页面 + navigate('/knowledge-base', { + state: { + resetToRoot: true, + } + }); + } + return false; + }, + }; + } + return item; + }); + + let customBreadcrumbs: MenuItem[] = [...breadcrumbsWithClick]; + + if (breadcrumbType === 'list') { + // 知识库列表页面:只显示知识库文件夹路径 + customBreadcrumbs = [ + ...breadcrumbsWithClick, + ...breadcrumbPath.knowledgeBaseFolderPath.map((folder, index) => ({ + id: 0, + parent: 0, + code: null, + label: folder.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + + // 如果有回调函数,直接调用回调函数来更新状态 + if (options?.onKnowledgeBaseFolderClick) { + options.onKnowledgeBaseFolderClick(folder.id, breadcrumbPath.knowledgeBaseFolderPath.slice(0, index + 1)); + } else { + // 否则使用导航(兜底逻辑) + navigate('/knowledge-base', { + state: { + navigateToFolder: folder.id, + folderPath: breadcrumbPath.knowledgeBaseFolderPath.slice(0, index + 1) + } + }); + } + return false; + }, + })), + ]; + } else { + // 知识库详情页面:显示知识库名称 + 文档文件夹路径 + 文档名称 + customBreadcrumbs = [ + ...breadcrumbsWithClick, + + // 添加知识库名称 + ...(breadcrumbPath.knowledgeBase ? [{ + id: 0, + parent: 0, + code: null, + label: breadcrumbPath.knowledgeBase.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + // 返回到知识库详情页的根目录 + const navigationState = { + fromKnowledgeBaseList: true, + knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath, + resetToRoot: true, // 添加重置到根目录的标志 + refresh: true, // 添加刷新标志 + timestamp: Date.now(), // 添加时间戳确保状态变化 + }; + navigate(`/knowledge-base/${breadcrumbPath.knowledgeBase!.id}/private`, { + state: navigationState, + replace: true // 使用 replace 避免历史记录堆积 + }); + return false; + }, + }] : []), + + // 添加文档文件夹路径 + ...breadcrumbPath.documentFolderPath.map((folder, index) => ({ + id: 0, + parent: 0, + code: null, + label: folder.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + // 返回到知识库详情页的对应文件夹 + const navigationState = { + fromKnowledgeBaseList: true, + knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath, + navigateToDocumentFolder: folder.id, + documentFolderPath: breadcrumbPath.documentFolderPath.slice(0, index + 1), + refresh: true, // 添加刷新标志 + timestamp: Date.now(), // 添加时间戳确保状态变化 + }; + navigate(`/knowledge-base/${breadcrumbPath.knowledgeBase!.id}/private`, { + state: navigationState, + replace: true // 使用 replace 避免历史记录堆积 + }); + return false; + }, + })), + + // 添加文档名称(如果存在) + ...(breadcrumbPath.document ? [{ + id: 0, + parent: 0, + code: null, + label: breadcrumbPath.document.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + // 文档名称不可点击 + }] : []), + ]; + } + + // 根据面包屑类型使用不同的键,实现独立的面包屑路径 + const breadcrumbKey = breadcrumbType === 'list' ? 'space' : 'space-detail'; + + + + setCustomBreadcrumbs(customBreadcrumbs, breadcrumbKey); + }, [setCustomBreadcrumbs, navigate, options?.breadcrumbType, options?.onKnowledgeBaseMenuClick, options?.onKnowledgeBaseFolderClick]); + + return { + updateBreadcrumbs, + }; +}; \ No newline at end of file diff --git a/web/src/hooks/useRouteGuard.ts b/web/src/hooks/useRouteGuard.ts index 23c31ec5..0bf6d485 100644 --- a/web/src/hooks/useRouteGuard.ts +++ b/web/src/hooks/useRouteGuard.ts @@ -11,8 +11,10 @@ export const checkAuthStatus = (): boolean => { // 递归检查路由是否存在于菜单数据中 export const checkRoutePermission = (menus: MenuItem[], currentPath: string): boolean => { - // 首页默认有权限 - if (currentPath === '/' || currentPath.includes('knowledge-detail')) return true; + // 首页和知识库相关页面默认有权限 + if (currentPath === '/' || currentPath.includes('knowledge-detail') || currentPath.includes('knowledge-base')) { + return true; + } for (const menu of menus) { // 检查当前菜单的path是否匹配 @@ -26,6 +28,7 @@ export const checkRoutePermission = (menus: MenuItem[], currentPath: string): bo } } } + return false; }; @@ -52,7 +55,7 @@ export const useRouteGuard = (source: 'space' | 'manage') => { const hasPermission = checkRoutePermission(menus, location.pathname); if (!hasPermission) { // 无权限访问该路由,重定向到无权限页面 - // navigate('/not-found', { replace: true }); + // navigate('/no-permission', { replace: true }); } } }, [navigate, location.pathname, location.search, location.hash, menus]); diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 41347c66..503f54f0 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -479,11 +479,18 @@ export const en = { noDataSets: 'No datasets yet, click the button below or drag files to create.', createEmptyDataSet: '+ Empty Dataset', createImageDataSet: '+ Image Dataset', + createContent: 'Create Content', + title: 'Title', + content: 'Content', + pleaseEnterTitle: 'Please enter title', + pleaseEnterContent: 'Please enter content', + // createImageDataSet: '+ Image Dataset', dragFilesHere: 'Drag files here to upload', createImport: 'Create/Import', textDataSet: 'Text Dataset', imageDataSet: 'Image Dataset', blankDataset: 'Blank Dataset', + customTextDataset: 'Custom Text Dataset', text: 'Text', search: 'Search', image: 'Image', @@ -1172,8 +1179,6 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re quickReply: 'Quick Reply', web_search: 'Online search', memory: 'Memory', - memoryConversationAnalysisEmpty: 'There is currently no dialogue analysis content available', - memoryConversationAnalysisEmptySubTitle: 'After entering your user ID, click on "Send" to view the conversation memory' }, login: { title: 'Red Bear Memory Science', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 7aeaed03..1f9b315e 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -110,6 +110,11 @@ export const zh = { noDataSets: '暂无数据集,点击下方按钮或拖拽文件创建。', createEmptyDataSet: '+ 空白数据集', createImageDataSet: '+ 图片数据集', + createContent: '创建内容', + title: '标题', + content: '内容', + pleaseEnterTitle: '请输入标题', + pleaseEnterContent: '请输入内容', dragFilesHere: '拖拽文件到此处上传', downloadOriginal: '下载原始内容', createImport: '新建/导入', @@ -117,6 +122,7 @@ export const zh = { imageDataSet: '图片数据集', blankDataset: '空白数据集', emptyDataSet: '空白数据集', + customTextDataset: '自定义文本数据集', text: '文本', search: '搜索', image: '图片', @@ -1242,8 +1248,6 @@ export const zh = { startANewConversation: '开始新对话', normalReply: '正常回复', quickReply: '快速回复', - memoryConversationAnalysisEmpty: '当前没有可用的对话分析内容', - memoryConversationAnalysisEmptySubTitle: '输入用户ID后,单击“发送”查看对话记忆' }, login: { title: '红熊记忆科学', diff --git a/web/src/store/menu.ts b/web/src/store/menu.ts index 7e725921..f6a66077 100644 --- a/web/src/store/menu.ts +++ b/web/src/store/menu.ts @@ -32,7 +32,7 @@ interface MenuState { allBreadcrumbs: Record<'space' | 'manage' | string, MenuItem[]>; loadMenus: (source: 'space' | 'manage') => void; updateBreadcrumbs: (keyPath: string[], source: 'space' | 'manage') => void; - setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: 'space' | 'manage') => void; + setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: string) => void; } const initBreadcrumbs = localStorage.getItem('breadcrumbs') || '[]' diff --git a/web/src/views/ApplicationConfig/components/Chat.tsx b/web/src/views/ApplicationConfig/components/Chat.tsx index 9a70b5f2..8adb642a 100644 --- a/web/src/views/ApplicationConfig/components/Chat.tsx +++ b/web/src/views/ApplicationConfig/components/Chat.tsx @@ -2,9 +2,9 @@ import { type FC, useRef, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import clsx from 'clsx' import { Input, Form } from 'antd' -import ChatIcon from '@/assets/images/application/chat.png' +import ChatIcon from '@/assets/images/application/chat.svg' import ChatSendIcon from '@/assets/images/application/chatSend.svg' -import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png' +import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.svg' import type { ChatItem, ChatData, Config } from '../types' import { runCompare, draftRun } from '@/api/application' import Empty from '@/components/Empty' @@ -240,7 +240,6 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc {chatList.length === 0 ? = ({ chatList, data, updateChatList, handleSave, sourc } {!chat.list || chat.list.length === 0 - ? + ? : (
scrollContainerRefs.current[index] = el} className={clsx(`rb:relative rb:overflow-y-auto rb:overflow-x-hidden`, { 'rb:h-[calc(100vh-186px)]': isCluster, diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx index 92059e7e..7139230d 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx @@ -8,15 +8,14 @@ import type { UploadFileResponse,KnowledgeBaseDocumentData } from '@/views/Knowl import type { ColumnsType } from 'antd/es/table'; import UploadFiles from '@/components/Upload/UploadFiles'; import type { UploadRequestOption } from 'rc-upload/lib/interface'; -import { uploadFile, getDocumentList, previewDocumentChunk, parseDocument, updateDocument, deleteDocument } from '@/api/knowledgeBase'; +import { uploadFile, getDocumentList, parseDocument, updateDocument, deleteDocument } from '@/api/knowledgeBase'; import exitIcon from '@/assets/images/knowledgeBase/exit.png'; -import { NoData } from '../components/noData'; -import noDataIcon from '@/assets/images/knowledgeBase/noData.png'; + import SliderInput from '@/components/SliderInput'; import DelimiterSelector from '../components/DelimiterSelector'; const { confirm } = Modal const { TextArea } = Input; -import styles from '../index.module.css'; + const style: React.CSSProperties = { display: 'flex', gap: 16, @@ -71,12 +70,11 @@ const CreateDataset = () => { const initialFileIds = locationState.fileIds ?? (locationState.fileId ? [locationState.fileId] : []); const [current, setCurrent] = useState(stepIndexMap[initialStepKey]); const tableRef = useRef(null); + + const [data, setData] = useState([]); - const [chunkData, setChunkData] = useState([]); - const [total, setTotal] = useState(0); const [rechunkFileIds, setRechunkFileIds] = useState(initialFileIds); - const [curSelectedFileId, setCurSelectedFileId] = useState(-1); - const [previewLoading, setPreviewLoading] = useState(false); + const [pollingLoading, setPollingLoading] = useState(false); const pollingTimerRef = useRef | null>(null); const [delimiter, setDelimiter] = useState(undefined); @@ -121,6 +119,7 @@ const CreateDataset = () => { layout_recognize:'DeepDOC', delimiter: delimiter, chunk_token_num: blockSize, + auto_question: processingMethod === 'directBlock' ? 0 : 1, } } updateDocument(id, params) @@ -145,7 +144,7 @@ const CreateDataset = () => { }); return; } - debugger + // 显示确认弹框 confirm({ @@ -168,7 +167,7 @@ const CreateDataset = () => { const startProcessing = (autoReturnToList: boolean) => { // 触发文档解析 rechunkFileIds.map((id) => { - parseDocument(id); + parseDocument(id, {}); }); // 开启 loading @@ -276,21 +275,7 @@ const CreateDataset = () => { onError?.(error as Error); }); }; - // 点击文件 预览分块 - const handlePreview = async(item: KnowledgeBaseDocumentData, index: number) => { - setCurSelectedFileId(index); - setPreviewLoading(true); - try{ - const res = await previewDocumentChunk(knowledgeBaseId ?? '', item.id ?? ''); - setChunkData(res.items || []); - setTotal(res.page.total || 0); - console.log('res', res); - }catch(error) { - console.log('error', error); - } finally { - setPreviewLoading(false); - } - } + // 轮询检查文档处理状态 // autoReturn: 是否在所有文档完成时自动返回列表页 @@ -346,6 +331,8 @@ const CreateDataset = () => { state: { refresh: true, timestamp: Date.now(), // 添加时间戳确保每次都是新的 state + // 保持返回到原来的文档文件夹位置 + navigateToDocumentFolder: parentId !== knowledgeBaseId ? parentId : undefined, }, }); } else { diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx index 1994c456..b96dff75 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx @@ -4,11 +4,12 @@ * @Author: yujiangping * @Date: 2025-11-15 16:13:47 * @LastEditors: yujiangping - * @LastEditTime: 2025-11-29 19:46:46 + * @LastEditTime: 2025-12-12 20:02:05 */ import { useEffect, useState, useRef, type FC } from 'react'; import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { useBreadcrumbManager, type BreadcrumbPath } from '@/hooks/useBreadcrumbManager'; import { Button, Spin, message, Switch } from 'antd'; import { getDocumentDetail, getDocumentChunkList, downloadFile, updateDocument, updateDocumentChunk, createDocumentChunk } from '@/api/knowledgeBase'; import type { KnowledgeBaseDocumentData, RecallTestData } from '@/views/KnowledgeBase/types'; @@ -25,7 +26,18 @@ const DocumentDetails: FC = () => { const navigate = useNavigate(); const { knowledgeBaseId } = useParams<{ knowledgeBaseId: string }>(); const location = useLocation(); - const { documentId, parentId: locationParentId } = location.state as { documentId: string; parentId?: string }; + const { updateBreadcrumbs } = useBreadcrumbManager({ + breadcrumbType: 'detail' + }); + const { + documentId, + parentId: locationParentId, + breadcrumbPath + } = location.state as { + documentId: string; + parentId?: string; + breadcrumbPath?: BreadcrumbPath; + }; const [loading, setLoading] = useState(false); const [document, setDocument] = useState(null); const [chunkList, setChunkList] = useState([]); @@ -44,6 +56,13 @@ const DocumentDetails: FC = () => { } }, [documentId]); + // 更新面包屑 + useEffect(() => { + if (breadcrumbPath) { + updateBreadcrumbs(breadcrumbPath); + } + }, [breadcrumbPath, updateBreadcrumbs]); + // 当文档加载完成且 progress === 1 时,加载分块列表 useEffect(() => { if (document && document.progress === 1 && !isManualRefreshRef.current) { @@ -179,7 +198,18 @@ const DocumentDetails: FC = () => { }; const handleBack = () => { - if (knowledgeBaseId) { + if (knowledgeBaseId && breadcrumbPath) { + // 返回到知识库详情页,并传递面包屑信息以恢复状态 + const navigationState = { + fromKnowledgeBaseList: true, + knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath, + navigateToDocumentFolder: locationParentId, + documentFolderPath: breadcrumbPath.documentFolderPath, + timestamp: Date.now(), // 添加时间戳确保状态变化 + }; + navigate(`/knowledge-base/${knowledgeBaseId}/private`, { state: navigationState }); + } else if (knowledgeBaseId) { + // 降级处理:直接跳转到知识库详情页 navigate(`/knowledge-base/${knowledgeBaseId}/private`); } }; diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx index 7c43b79f..96b1af9d 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState, useRef, type FC } from 'react'; +import { useEffect, useState, useRef, useCallback, type FC } from 'react'; import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Switch, Button, Dropdown, Space, Modal, message } from 'antd'; @@ -12,26 +12,29 @@ import { MoreOutlined } from '@ant-design/icons'; import folderIcon from '@/assets/images/knowledgeBase/folder.png'; import textIcon from '@/assets/images/knowledgeBase/text.png'; import editIcon from '@/assets/images/knowledgeBase/edit.png'; +import blankIcon from '@/assets/images/knowledgeBase/blankDocument.png'; import { getKnowledgeBaseDetail, deleteDocument, downloadFile, updateKnowledgeBase } from '@/api/knowledgeBase'; -import type { - CreateModalRef, - KnowledgeBaseListItem, - RecallTestDrawerRef, - CreateFolderModalRef, - CreateImageModalRef, - ShareModalRef, - CreateDatasetModalRef,FolderFormData, - KnowledgeBaseDocumentData +import { + type CreateModalRef, + type KnowledgeBaseListItem, + type RecallTestDrawerRef, + type CreateFolderModalRef, + type CreateSetModalRef, + type ShareModalRef, + type CreateDatasetModalRef,type FolderFormData, + type KnowledgeBaseDocumentData, } from '@/views/KnowledgeBase/types'; import RecallTestDrawer from '../components/RecallTestDrawer'; import CreateFolderModal from '../components/CreateFolderModal'; +import CreateContentModal from '../components/CreateContentModal'; import CreateModal from '../components/CreateModal'; import ShareModal from '../components/ShareModal'; import CreateDatasetModal from '../components/CreateDatasetModal'; import CreateImageDataset from '../components/CreateImageDataset'; import FolderTree, { type TreeNodeData } from '../components/FolderTree'; import { formatDateTime } from '@/utils/format'; -import { useMenu } from '@/store/menu'; + +import { useBreadcrumbManager, type BreadcrumbItem } from '@/hooks/useBreadcrumbManager'; import './Private.css' const { confirm } = Modal // 树节点数据类型 @@ -48,7 +51,8 @@ const Private: FC = () => { const [tableApi, setTableApi] = useState(undefined); const recallTestDrawerRef = useRef(null); const createFolderModalRef = useRef(null); - const createImageDataset = useRef(null) + const createImageDataset = useRef(null) + const createContentModalRef = useRef(null); const [knowledgeBase, setKnowledgeBase] = useState(null); const [folder, setFolder] = useState({ kb_id:knowledgeBaseId ?? '', @@ -56,47 +60,47 @@ const Private: FC = () => { }); const [query, setQuery] = useState>({ orderby: 'created_at', - desc: true, + desc: true }); const modalRef = useRef(null) const shareModalRef = useRef(null); const datasetModalRef = useRef(null); const [folderTreeRefreshKey, setFolderTreeRefreshKey] = useState(0); - const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); - const [folderPath, setFolderPath] = useState>([]); + const [autoExpandPath, setAutoExpandPath] = useState>([]); + + const { updateBreadcrumbs } = useBreadcrumbManager({ + breadcrumbType: 'detail', + // 不提供 onKnowledgeBaseMenuClick,让它使用默认的导航行为(返回列表页面) + onKnowledgeBaseFolderClick: useCallback((folderId: string, folderPath: Array<{ id: string; name: string }>) => { + // 点击文件夹面包屑时,导航到对应文件夹 + setParentId(folderId); + setFolderPath(folderPath); + setSelectedKeys([folderId]); + setFolder({ + kb_id: knowledgeBaseId ?? '', + parent_id: folderId + }); + + // 确保query对象发生变化,触发表格刷新 + setQuery({ + orderby: 'created_at', + desc: true, + parent_id: folderId, + _timestamp: Date.now() + }); + + // 确保API URL正确设置 + setTableApi(`/documents/${knowledgeBaseId}/documents`); + + // 手动触发表格刷新,确保数据更新 + setTimeout(() => { + tableRef.current?.loadData(); + }, 100); + }, [knowledgeBaseId]) + }); + const [folderPath, setFolderPath] = useState([]); const [selectedKeys, setSelectedKeys] = useState([]); - useEffect(() => { - if (knowledgeBaseId) { - let url = `/documents/${knowledgeBaseId}/${parentId}/documents`; - setTableApi(url); - fetchKnowledgeBaseDetail(knowledgeBaseId); - } - }, [knowledgeBaseId]); - - // 更新面包屑 - useEffect(() => { - if (knowledgeBase) { - updateBreadcrumbs(); - } - }, [knowledgeBase, folderPath]); - - // 监听 tableApi 变化,自动刷新表格数据 - useEffect(() => { - if (tableApi) { - tableRef.current?.loadData(); - } - }, [tableApi]); - - // 监听 location state 变化,如果有 refresh 标志则刷新列表 - useEffect(() => { - const state = location.state as { refresh?: boolean; timestamp?: number } | null; - if (state?.refresh) { - tableRef.current?.loadData(); - // 清除 state,避免重复刷新 - navigate(location.pathname, { replace: true, state: {} }); - } - }, [location.state]); - + const [knowledgeBaseFolderPath, setKnowledgeBaseFolderPath] = useState([]); const fetchKnowledgeBaseDetail = async (id: string) => { setLoading(true); try { @@ -109,110 +113,160 @@ const Private: FC = () => { } }; - // 更新面包屑,包含知识库名称和文件夹路径 - const updateBreadcrumbs = () => { - if (!knowledgeBase) return; - - const baseBreadcrumbs = allBreadcrumbs['space'] || []; - // 只保留知识库菜单项之前的面包屑 - const knowledgeBaseMenuIndex = baseBreadcrumbs.findIndex(item => item.path === '/knowledge-base'); - const filteredBaseBreadcrumbs = knowledgeBaseMenuIndex >= 0 - ? baseBreadcrumbs.slice(0, knowledgeBaseMenuIndex + 1) - : baseBreadcrumbs; - - const customBreadcrumbs = [ - ...filteredBaseBreadcrumbs, - { - id: 0, - parent: 0, - code: null, - label: knowledgeBase.name, - i18nKey: null, - path: null, - enable: true, - display: true, - level: 0, - sort: 0, - icon: null, - iconActive: null, - menuDesc: null, - deleted: null, - updateTime: 0, - new_: null, - keepAlive: false, - master: null, - disposable: false, - appSystem: null, - subs: [], - onClick: (e?: React.MouseEvent) => { - // 阻止默认行为和事件冒泡 - e?.preventDefault(); - e?.stopPropagation(); - // 点击知识库名称,回到根目录 - setParentId(knowledgeBaseId); - setFolder({ - kb_id: knowledgeBaseId ?? '', - parent_id: knowledgeBaseId ?? '' - }); - setTableApi(`/documents/${knowledgeBaseId}/${knowledgeBaseId}/documents`); - setFolderPath([]); - setSelectedKeys([knowledgeBaseId ?? '']); - return false; - }, - }, - ...folderPath.map((folder, index) => ({ - id: 0, - parent: 0, - code: null, - label: folder.name, - i18nKey: null, - path: null, - enable: true, - display: true, - level: 0, - sort: 0, - icon: null, - iconActive: null, - menuDesc: null, - deleted: null, - updateTime: 0, - new_: null, - keepAlive: false, - master: null, - disposable: false, - appSystem: null, - subs: [], - onClick: (e?: React.MouseEvent) => { - // 阻止默认行为和事件冒泡 - e?.preventDefault(); - e?.stopPropagation(); - // 点击文件夹,回到该文件夹层级 - setParentId(folder.id); - setFolder({ - kb_id: knowledgeBaseId ?? '', - parent_id: folder.id - }); - setTableApi(`/documents/${knowledgeBaseId}/${folder.id}/documents`); - // 更新文件夹路径,只保留到当前点击的文件夹 - setFolderPath(folderPath.slice(0, index + 1)); - setSelectedKeys([folder.id]); - return false; - }, - })), - ]; + useEffect(() => { + if (knowledgeBaseId) { + let url = `/documents/${knowledgeBaseId}/documents`; + setTableApi(url); + fetchKnowledgeBaseDetail(knowledgeBaseId); + } + }, [knowledgeBaseId]); - setCustomBreadcrumbs(customBreadcrumbs, 'space'); - }; + // 更新面包屑 + useEffect(() => { + if (knowledgeBase) { + updateBreadcrumbs({ + knowledgeBaseFolderPath, + knowledgeBase: { + id: knowledgeBase.id, + name: knowledgeBase.name, + type: 'knowledgeBase' + }, + documentFolderPath: folderPath, + }); + } + }, [knowledgeBase, knowledgeBaseFolderPath, folderPath, updateBreadcrumbs]); + + // 监听 tableApi 变化,自动刷新表格数据 + useEffect(() => { + if (tableApi) { + tableRef.current?.loadData(); + } + }, [tableApi]); + + // 监听 query 变化,确保表格数据更新 + useEffect(() => { + if (tableApi && query._timestamp) { + // 当 query 中有 _timestamp 时,说明是通过面包屑或其他方式触发的更新 + tableRef.current?.loadData(); + } + }, [query._timestamp, tableApi]); + + // 监听 location state 变化 + useEffect(() => { + const state = location.state as { + refresh?: boolean; + timestamp?: number; + fromKnowledgeBaseList?: boolean; + knowledgeBaseFolderPath?: BreadcrumbItem[]; + parentId?: string; + navigateToDocumentFolder?: string; + documentFolderPath?: BreadcrumbItem[]; + resetToRoot?: boolean; + } | null; + + if (state?.refresh) { + tableRef.current?.loadData(); + // 清除 state,避免重复刷新 + navigate(location.pathname, { replace: true, state: {} }); + } + + // 如果是从知识库列表页跳转过来的,设置知识库文件夹路径 + if (state?.fromKnowledgeBaseList && state?.knowledgeBaseFolderPath) { + setKnowledgeBaseFolderPath(state.knowledgeBaseFolderPath); + } + + // 如果需要重置到根目录(回到初始状态) + if (state?.resetToRoot) { + // 重置所有状态到初始状态,和页面初始化保持一致 + setParentId(knowledgeBaseId); + setFolderPath([]); + setSelectedKeys([]); + setFolder({ + kb_id: knowledgeBaseId ?? '', + parent_id: knowledgeBaseId ?? '' + }); + setQuery({ + orderby: 'created_at', + desc: true, + _timestamp: Date.now() // 添加时间戳确保query对象发生变化,触发API调用 + }); + + // 重新设置API URL + const rootUrl = `/documents/${knowledgeBaseId}/documents`; + setTableApi(rootUrl); + + // 清除自动展开路径 + setAutoExpandPath([]); + + // 刷新文件夹树(简单的刷新,不需要复杂的重置逻辑) + setFolderTreeRefreshKey((prev) => prev + 1); + + // 清除 state,避免重复处理 + navigate(location.pathname, { replace: true, state: {} }); + } + + // 如果是从文档详情页返回,恢复文档文件夹路径 + if (state?.navigateToDocumentFolder && state?.documentFolderPath) { + setFolderPath(state.documentFolderPath); + setParentId(state.navigateToDocumentFolder); + setFolder({ + kb_id: knowledgeBaseId ?? '', + parent_id: state.navigateToDocumentFolder + }); + setQuery(prevQuery => ({ + ...prevQuery, + parent_id: state.navigateToDocumentFolder, + _timestamp: Date.now() + })); + setTableApi(`/documents/${knowledgeBaseId}/documents`); + setSelectedKeys([state.navigateToDocumentFolder]); + + // 设置自动展开路径,让FolderTree自动展开到对应位置 + setAutoExpandPath(state.documentFolderPath); + + // 手动触发表格刷新 + setTimeout(() => { + tableRef.current?.loadData(); + }, 100); + + // 清除自动展开路径,避免重复触发(延迟清除,确保FolderTree处理完成) + setTimeout(() => { + setAutoExpandPath([]); + }, 2000); + } + }, [location.state, knowledgeBaseId, navigate, location.pathname]); // 处理树节点选择 const onSelect = (keys: React.Key[]) => { - if (!keys.length) return; + if (!keys.length) { + // 如果没有选中任何节点,回到根目录(初始状态) + setParentId(knowledgeBaseId); + setFolder({ + kb_id: knowledgeBaseId ?? '', + parent_id: knowledgeBaseId ?? '' + }); + setQuery({ + orderby: 'created_at', + desc: true, + _timestamp: Date.now() // 添加时间戳确保query对象发生变化 + }); + setSelectedKeys([]); + return; + } + if (!folder) return; + const f = { ...folder, parent_id: String(keys[0]), } - let url = `/documents/${knowledgeBaseId}/${String(keys[0])}/documents`; + setQuery({ + ...query, + parent_id: String(keys[0]), + _timestamp: Date.now() // 添加时间戳确保query对象发生变化 + }) + let url = `/documents/${knowledgeBaseId}/documents`; + setTableApi(url); setParentId(String(keys[0])) setFolder(f) @@ -253,6 +307,15 @@ const Private: FC = () => { datasetModalRef?.current?.handleOpen(knowledgeBase?.id,folder?.parent_id ?? knowledgeBase?.id ?? ''); }, }, + { + key: '8', + icon: Custome Text, + label: t('knowledgeBase.customTextDataset'), + onClick: () => { + createContentModalRef?.current?.handleOpen(knowledgeBase?.id ?? '', folder?.parent_id ?? knowledgeBase?.id ?? ''); + // handleCreate('folder'); // 传入 type: 'folder' + }, + }, // 暂时未实现 // { // key: '3', @@ -413,6 +476,21 @@ const Private: FC = () => { state: { documentId: document.id, parentId: parentId ?? knowledgeBaseId, + // 传递面包屑信息 + breadcrumbPath: { + knowledgeBaseFolderPath, + knowledgeBase: { + id: knowledgeBase?.id || knowledgeBaseId, + name: knowledgeBase?.name || '', + type: 'knowledgeBase' + }, + documentFolderPath: folderPath, + document: { + id: document.id, + name: document.file_name || '', + type: 'document' + } + } }, }); } @@ -486,7 +564,9 @@ const Private: FC = () => { } const refreshDirectoryTree = async () => { // 先刷新知识库详情,确保数据是最新的 - await fetchKnowledgeBaseDetail(knowledgeBase.id); + if (knowledgeBase?.id) { + await fetchKnowledgeBaseDetail(knowledgeBase.id); + } // 添加短暂延迟,确保后端数据已经完全更新 await new Promise(resolve => setTimeout(resolve, 300)); // 然后刷新文件夹树 @@ -501,6 +581,7 @@ const Private: FC = () => { } const handleRootTreeLoad = (nodes: TreeNodeData[] | null) => { if (!nodes || nodes.length === 0) { + // 如果没有节点,设置folder为null(这会隐藏FolderTree) setFolder(null); } else { // 如果有节点且 folder 为 null,重新设置 folder @@ -545,6 +626,7 @@ const Private: FC = () => { onRootLoad={handleRootTreeLoad} onFolderPathChange={handleFolderPathChange} selectedKeys={selectedKeys} + autoExpandPath={autoExpandPath} />
)} @@ -601,6 +683,10 @@ const Private: FC = () => { ref={createFolderModalRef} refreshTable={refreshDirectoryTree} /> + { const { t } = useTranslation(); const params = useParams<{ knowledgeBaseId: string }>(); + const location = useLocation(); const knowledgeBaseId = params.knowledgeBaseId; const [loading, setLoading] = useState(false); const [knowledgeBase, setKnowledgeBase] = useState(null); const recallTestRef = useRef(null); const [infoItems, setInfoItems] = useState([]); - const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); + const [knowledgeBaseFolderPath, setKnowledgeBaseFolderPath] = useState([]); + + const { updateBreadcrumbs } = useBreadcrumbManager({ + breadcrumbType: 'detail' + }); useEffect(() => { console.log('Share.tsx - useParams result:', params); console.log('Share.tsx - knowledgeBaseId:', knowledgeBaseId); @@ -46,9 +51,30 @@ const Share: FC = () => { // 更新面包屑 useEffect(() => { if (knowledgeBase) { - updateBreadcrumbs(); + updateBreadcrumbs({ + knowledgeBaseFolderPath, + knowledgeBase: { + id: knowledgeBase.id, + name: knowledgeBase.name, + type: 'knowledgeBase' + }, + documentFolderPath: [], + }); } - }, [knowledgeBase]); + }, [knowledgeBase, knowledgeBaseFolderPath, updateBreadcrumbs]); + + // 监听 location state 变化 + useEffect(() => { + const state = location.state as { + fromKnowledgeBaseList?: boolean; + knowledgeBaseFolderPath?: BreadcrumbItem[]; + } | null; + + // 如果是从知识库列表页跳转过来的,设置知识库文件夹路径 + if (state?.fromKnowledgeBaseList && state?.knowledgeBaseFolderPath) { + setKnowledgeBaseFolderPath(state.knowledgeBaseFolderPath); + } + }, [location.state]); const formatInfoItems = (data: KnowledgeBaseListItem): InfoItem[] => { const items: InfoItem[] = [ { @@ -112,46 +138,7 @@ const Share: FC = () => { }); }; - // 更新面包屑,包含知识库名称 - const updateBreadcrumbs = () => { - if (!knowledgeBase) return; - - const baseBreadcrumbs = allBreadcrumbs['space'] || []; - // 只保留知识库菜单项之前的面包屑 - const knowledgeBaseMenuIndex = baseBreadcrumbs.findIndex(item => item.path === '/knowledge-base'); - const filteredBaseBreadcrumbs = knowledgeBaseMenuIndex >= 0 - ? baseBreadcrumbs.slice(0, knowledgeBaseMenuIndex + 1) - : baseBreadcrumbs; - - const customBreadcrumbs = [ - ...filteredBaseBreadcrumbs, - { - id: 0, - parent: 0, - code: null, - label: knowledgeBase.name, - i18nKey: null, - path: null, - enable: true, - display: true, - level: 0, - sort: 0, - icon: null, - iconActive: null, - menuDesc: null, - deleted: null, - updateTime: 0, - new_: null, - keepAlive: false, - master: null, - disposable: false, - appSystem: null, - subs: [], - }, - ]; - setCustomBreadcrumbs(customBreadcrumbs, 'space'); - }; // const handleBack = () => { // navigate('/knowledge-base'); diff --git a/web/src/views/KnowledgeBase/components/CreateContentModal.tsx b/web/src/views/KnowledgeBase/components/CreateContentModal.tsx new file mode 100644 index 00000000..5ad0b4f8 --- /dev/null +++ b/web/src/views/KnowledgeBase/components/CreateContentModal.tsx @@ -0,0 +1,104 @@ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Form, Input } from 'antd'; +import { useTranslation } from 'react-i18next'; +import RbModal from '@/components/RbModal'; +import { createDocumentAndUpload } from '@/api/knowledgeBase' +import type { CreateSetModalRef,CreateSetMoealRefProps } from '../types' +interface ContentFormData { + title: string; + content: string; +} + +const CreateContentModal = forwardRef( + ({ refreshTable }, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [kbId, setKbId] = useState(''); + const [parentId, setParentId] = useState(''); + + const handleClose = () => { + form.resetFields(); + setLoading(false); + setVisible(false); + setKbId(''); + setParentId(''); + }; + + const handleOpen = (kb_id: string, parent_id: string) => { + setKbId(kb_id); + setParentId(parent_id); + form.resetFields(); + setVisible(true); + }; + + const handleSave = async () => { + try { + const values = await form.validateFields(); + setLoading(true); + + // TODO: 这里需要调用相应的API来保存内容 + const params = { + // ...values, + kb_id: kbId, + parent_id: parentId, + }; + + // 模拟API调用 + await new Promise(resolve => setTimeout(resolve, 1000)); + await createDocumentAndUpload(values, params) + if (refreshTable) { + await refreshTable(); + } + + handleClose(); + } catch (err) { + console.error('创建内容失败:', err); + } finally { + setLoading(false); + } + }; + + useImperativeHandle(ref, () => ({ + handleOpen, + })); + + return ( + +
+ + + + + + + +
+
+ ); + } +); + +export default CreateContentModal; \ No newline at end of file diff --git a/web/src/views/KnowledgeBase/components/CreateContentModalExample.tsx b/web/src/views/KnowledgeBase/components/CreateContentModalExample.tsx new file mode 100644 index 00000000..7434e559 --- /dev/null +++ b/web/src/views/KnowledgeBase/components/CreateContentModalExample.tsx @@ -0,0 +1,34 @@ +import { useRef } from 'react'; +import { Button } from 'antd'; +import CreateContentModal from './CreateContentModal'; +import type { CreateContentModalRef } from '../types'; + +// 使用示例组件 +const CreateContentModalExample = () => { + const createContentModalRef = useRef(null); + + const handleOpenModal = () => { + // 打开弹窗,传入知识库ID和父级ID + createContentModalRef.current?.handleOpen('kb_123', 'parent_456'); + }; + + const handleRefreshTable = () => { + console.log('刷新表格数据'); + // 这里可以添加刷新表格的逻辑 + }; + + return ( +
+ + + +
+ ); +}; + +export default CreateContentModalExample; \ No newline at end of file diff --git a/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx b/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx index 4f40da7f..8f57b009 100644 --- a/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx +++ b/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx @@ -2,7 +2,7 @@ import { forwardRef, useImperativeHandle, useState, useRef } from 'react'; import { Form, Input } from 'antd'; import { useTranslation } from 'react-i18next'; import type { UploadFile } from 'antd'; -import type { CreateImageModalRef, CreateImageMoealRefProps,UploadFileResponse } from '@/views/KnowledgeBase/types'; +import type { CreateSetModalRef, CreateSetMoealRefProps, UploadFileResponse } from '@/views/KnowledgeBase/types'; import type { UploadRequestOption } from 'rc-upload/lib/interface'; import RbModal from '@/components/RbModal'; import UploadFiles from '@/components/Upload/UploadFiles'; @@ -13,7 +13,7 @@ interface ImageDatasetFormData { images: UploadFile[]; } -const CreateImageDataset = forwardRef( +const CreateImageDataset = forwardRef( ({ refreshTable }, ref) => { const { t } = useTranslation(); const [visible, setVisible] = useState(false); diff --git a/web/src/views/KnowledgeBase/components/FolderTree.tsx b/web/src/views/KnowledgeBase/components/FolderTree.tsx index 4a5a288c..299e4cf3 100644 --- a/web/src/views/KnowledgeBase/components/FolderTree.tsx +++ b/web/src/views/KnowledgeBase/components/FolderTree.tsx @@ -60,6 +60,8 @@ interface FolderTreeProps { onRootLoad?: (nodes: TreeNodeData[] | null) => void; onFolderPathChange?: (path: Array<{ id: string; name: string }>) => void; selectedKeys?: React.Key[]; + // 新增:自动展开到指定路径 + autoExpandPath?: Array<{ id: string; name: string }>; } const renderIcon = (icon?: string) => { @@ -275,8 +277,11 @@ const FolderTree: FC = ({ onRootLoad, onFolderPathChange, selectedKeys, + autoExpandPath, }) => { const [treeData, setTreeData] = useState([]); + const [expandedKeys, setExpandedKeys] = useState([]); + const [autoExpandInProgress, setAutoExpandInProgress] = useState(false); // 更新树节点数据的辅助函数 const updateTreeData = (nodes: TreeNodeData[], key: Key, children: TreeNodeData[]): TreeNodeData[] => { @@ -370,6 +375,109 @@ const FolderTree: FC = ({ return null; }; + // 查找节点的辅助函数 + const findNodeInTree = (nodes: TreeNodeData[], key: string): TreeNodeData | null => { + for (const node of nodes) { + if (String(node.key) === key) { + return node; + } + if (node.children) { + const found = findNodeInTree(node.children, key); + if (found) return found; + } + } + return null; + }; + + // 渐进式自动展开到指定路径 + useEffect(() => { + if (!autoExpandPath || autoExpandPath.length === 0 || autoExpandInProgress || treeData.length === 0) { + return; + } + + const expandToPath = async () => { + setAutoExpandInProgress(true); + + try { + const keysToExpand: React.Key[] = []; + let currentTreeData = treeData; + + // 逐级展开,从第一级开始(跳过根节点,因为根节点已经加载) + for (let i = 0; i < autoExpandPath.length - 1; i++) { + const nodeKey = autoExpandPath[i].id; + keysToExpand.push(nodeKey); + + // 查找当前节点 + const targetNode = findNodeInTree(currentTreeData, nodeKey); + + if (targetNode && targetNode.children === undefined) { + // 如果子节点未加载,先加载 + try { + console.log(`自动展开:加载节点 ${nodeKey} 的子节点`); + const children = await buildTreeNodes(knowledgeBaseId, nodeKey); + + // 更新树数据 + setTreeData((prevData) => { + const newData = updateTreeData(prevData, nodeKey, children); + currentTreeData = newData; // 更新当前引用 + return newData; + }); + + // 等待状态更新完成 + await new Promise(resolve => setTimeout(resolve, 150)); + + } catch (error) { + console.error(`自动展开时加载节点 ${nodeKey} 失败:`, error); + // 加载失败时停止展开 + break; + } + } + } + + // 设置展开的节点 + setExpandedKeys(keysToExpand); + + // 选中最后一个节点(目标文件夹) + const targetKey = autoExpandPath[autoExpandPath.length - 1]?.id; + if (targetKey) { + console.log(`自动展开:选中目标节点 ${targetKey}`); + // 延迟选中,确保展开动画完成 + setTimeout(() => { + if (onSelect) { + onSelect([targetKey], { + selected: true, + selectedNodes: [], + node: {} as any, + event: 'select', + nativeEvent: new MouseEvent('click') + }); + } + }, 200); + } + + } catch (error) { + console.error('自动展开路径失败:', error); + } finally { + // 延迟重置标志,确保展开过程完全完成 + setTimeout(() => { + setAutoExpandInProgress(false); + }, 500); + } + }; + + // 延迟执行,确保树数据已经加载完成 + const timer = setTimeout(expandToPath, 300); + return () => clearTimeout(timer); + }, [autoExpandPath, treeData.length, knowledgeBaseId, onSelect, autoExpandInProgress]); + + // 处理展开事件 + const handleExpand: TreeProps['onExpand'] = (expandedKeys, info) => { + setExpandedKeys(expandedKeys); + if (onExpand) { + onExpand(expandedKeys, info); + } + }; + // 处理选择事件,计算并传递路径 const handleSelect: TreeProps['onSelect'] = (selectedKeys, info) => { if (selectedKeys.length > 0) { @@ -391,11 +499,13 @@ const FolderTree: FC = ({ return ( ; @@ -28,6 +29,7 @@ type ModelMenuInfo = { const KnowledgeBaseManagement: FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); + const location = useLocation(); const [loading, setLoading] = useState(false); const [data, setData] = useState([]) const [page, setPage] = useState(1) @@ -42,10 +44,29 @@ const KnowledgeBaseManagement: FC = () => { const modelListCache = useRef>({}); const modalRef = useRef(null) const [messageApi, contextHolder] = message.useMessage(); + const processedStateRef = useRef(null); - // 使用 menu store 管理面包屑 - const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); - const [folderPath, setFolderPath] = useState>([]); + // 使用面包屑管理 Hook + const { updateBreadcrumbs } = useBreadcrumbManager({ + breadcrumbType: 'list', + onKnowledgeBaseMenuClick: useCallback(() => { + // 返回根目录 + setFolderPath([]); + setQuery((prev) => ({ + ...prev, + parent_id: undefined, + })); + }, []), + onKnowledgeBaseFolderClick: useCallback((folderId: string, folderPath: Array<{ id: string; name: string }>) => { + // 直接更新文件夹路径和查询状态 + setFolderPath(folderPath); + setQuery((prev) => ({ + ...prev, + parent_id: folderId, + })); + }, []) + }); + const [folderPath, setFolderPath] = useState([]); // 生成下拉菜单项(根据当前 item) @@ -134,7 +155,7 @@ const KnowledgeBaseManagement: FC = () => { handleCreate(type); }, })); - }, [knowledgeBaseTypes, t, folderPath, query]); + }, [knowledgeBaseTypes, t]); const typeToFieldKey = (type: string) => { const normalized = (type || '').toLowerCase(); switch (normalized) { @@ -371,90 +392,72 @@ const KnowledgeBaseManagement: FC = () => { })); return; } - // 根据权限类型跳转到不同的详情页 - if (knowledgeBase.permission_id === 'Private' || knowledgeBase.permission_id === 'private') { - navigate(`/knowledge-base/${knowledgeBase.id}/private`) + // 跳转时传递当前的文件夹路径信息 + const navigationState = { + fromKnowledgeBaseList: true, + knowledgeBaseFolderPath: folderPath, + parentId: query.parent_id, + timestamp: Date.now(), // 添加时间戳确保每次跳转状态都不同 + }; + const targetPath = knowledgeBase.permission_id === 'Private' || knowledgeBase.permission_id === 'private' + ? `/knowledge-base/${knowledgeBase.id}/private` + : `/knowledge-base/${knowledgeBase.id}/share`; + + // 检查是否是相同路径跳转 + const currentPath = location.pathname; + + if (currentPath === targetPath) { + // 如果是相同路径,使用replace并强制刷新状态 + navigate(targetPath, { + state: navigationState, + replace: true + }); } else { - navigate(`/knowledge-base/${knowledgeBase.id}/share`) + // 不同路径,正常跳转 + navigate(targetPath, { state: navigationState }); } } - // 更新面包屑的函数 - const updateBreadcrumbs = () => { - const baseBreadcrumbs = allBreadcrumbs['space'] || []; - // 只保留知识库菜单项之前的面包屑 - const knowledgeBaseMenuIndex = baseBreadcrumbs.findIndex(item => item.path === '/knowledge-base'); - const filteredBaseBreadcrumbs = knowledgeBaseMenuIndex >= 0 - ? baseBreadcrumbs.slice(0, knowledgeBaseMenuIndex + 1) - : baseBreadcrumbs; - - // 给"知识库管理"添加点击事件,返回根目录 - const breadcrumbsWithClick = filteredBaseBreadcrumbs.map((item) => { - if (item.path === '/knowledge-base') { - return { - ...item, - onClick: (e?: React.MouseEvent) => { - e?.preventDefault(); - e?.stopPropagation(); - // 返回根目录 - setFolderPath([]); - setQuery((prev) => ({ - ...prev, - parent_id: undefined, - })); - return false; - }, - }; - } - return item; - }); - - const customBreadcrumbs = [ - ...breadcrumbsWithClick, - ...folderPath.map((folder, index) => ({ - id: 0, - parent: 0, - code: null, - label: folder.name, - i18nKey: null, - path: null, - enable: true, - display: true, - level: 0, - sort: 0, - icon: null, - iconActive: null, - menuDesc: null, - deleted: null, - updateTime: 0, - new_: null, - keepAlive: false, - master: null, - disposable: false, - appSystem: null, - subs: [], - onClick: (e?: React.MouseEvent) => { - e?.preventDefault(); - e?.stopPropagation(); - // 点击文件夹,回到该文件夹层级 - const newFolderPath = folderPath.slice(0, index + 1); - setFolderPath(newFolderPath); - setQuery((prev) => ({ - ...prev, - parent_id: folder.id, - })); - return false; - }, - })), - ]; - - setCustomBreadcrumbs(customBreadcrumbs, 'space'); - }; - // 更新面包屑 useEffect(() => { - updateBreadcrumbs(); - }, [folderPath]); + updateBreadcrumbs({ + knowledgeBaseFolderPath: folderPath, + documentFolderPath: [], + }); + }, [folderPath, updateBreadcrumbs]); + + // 处理从详情页返回的导航 + useEffect(() => { + const state = location.state as { + navigateToFolder?: string; + folderPath?: Array<{ id: string; name: string }>; + resetToRoot?: boolean; + } | null; + + // 避免重复处理相同的状态 + if (state && state !== processedStateRef.current) { + processedStateRef.current = state; + + if (state.resetToRoot) { + // 重置到根目录 + setFolderPath([]); + setQuery((prev) => ({ + ...prev, + parent_id: undefined, + })); + } else if (state?.navigateToFolder && state?.folderPath) { + // 恢复文件夹路径和查询状态 + setFolderPath(state.folderPath); + setQuery((prev) => ({ + ...prev, + parent_id: state.navigateToFolder, + })); + } + + // 不清除 state,避免干扰后续导航 + // 使用 processedStateRef 来避免重复处理相同的 state + } + }, [location.state, navigate]); useEffect(() => { fetchModelTypes(); @@ -465,7 +468,7 @@ const KnowledgeBaseManagement: FC = () => { if (modelTypes.length) { fetchData(1, false); } - }, [modelTypes, query]) + }, [modelTypes, query.parent_id, query.keywords, query.orderby, query.desc]) return ( <> diff --git a/web/src/views/KnowledgeBase/types.ts b/web/src/views/KnowledgeBase/types.ts index 04c66ac0..f4a6ed56 100644 --- a/web/src/views/KnowledgeBase/types.ts +++ b/web/src/views/KnowledgeBase/types.ts @@ -146,11 +146,19 @@ export interface CreateFolderModalRefProps{ refreshTable?: () => void; } -//他建图片数据集 -export interface CreateImageModalRef{ - handleOpen: (kb_id:string,parent_id:string) => void; +//创建图片数据集 / 创建自定义文本数据集 +export interface CreateSetModalRef{ + handleOpen: (kb_id:string, parent_id:string) => void; } -export interface CreateImageMoealRefProps{ +export interface CreateSetMoealRefProps{ + refreshTable?: () => void; +} + +// 创建内容 +export interface CreateContentModalRef { + handleOpen: (kb_id: string, parent_id: string) => void; +} +export interface CreateContentModalRefProps { refreshTable?: () => void; } diff --git a/web/src/views/MemoryConversation/index.tsx b/web/src/views/MemoryConversation/index.tsx index c92044cc..43c81cc6 100644 --- a/web/src/views/MemoryConversation/index.tsx +++ b/web/src/views/MemoryConversation/index.tsx @@ -4,7 +4,7 @@ import { Col, Row, App, Skeleton, Space, Select } from 'antd' import clsx from 'clsx' import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg' -import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.png' +import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.svg' import Card from './components/Card' import Chat from './components/Chat' import { readService, getUserMemoryList } from '@/api/memory' @@ -139,9 +139,6 @@ const MemoryConversation: FC = () => { : {logs.map((log, logIndex) => ( diff --git a/web/src/views/NoPermission/index.tsx b/web/src/views/NoPermission/index.tsx index ac547fd6..a14e505c 100644 --- a/web/src/views/NoPermission/index.tsx +++ b/web/src/views/NoPermission/index.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import noPermission from '@/assets/images/empty/noPermission.png'; +import noPermission from '@/assets/images/empty/noPermission.svg'; import Empty from '@/components/Empty'; const NoPermission = () => { diff --git a/web/src/views/NotFound/index.tsx b/web/src/views/NotFound/index.tsx index 6d879167..4a7fbbe6 100644 --- a/web/src/views/NotFound/index.tsx +++ b/web/src/views/NotFound/index.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import notFoundImg from '@/assets/images/empty/404.png'; +import notFoundImg from '@/assets/images/empty/404.svg'; import Empty from '@/components/Empty'; const NotFound = () => { diff --git a/web/vite.config.ts b/web/vite.config.ts index 7181389f..e2a484fb 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,3 +1,11 @@ +/* + * @Description: + * @Version: 0.0.1 + * @Author: yujiangping + * @Date: 2025-12-03 15:40:49 + * @LastEditors: yujiangping + * @LastEditTime: 2025-12-12 14:09:00 + */ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { resolve } from 'path' @@ -11,7 +19,10 @@ export default defineConfig({ proxy: { // 主要API代理,支持 /api 和 /api/* 格式 '/api': { - target: 'http://0.0.0.0:5173', // 后端服务地址 + // target: 'http://0.0.0.0:5173', // 后端服务地址 + // target: 'http://119.45.181.55:8000', + target: 'https://devmemorybear.redbearai.com/', // 开发服务器 + // target: 'https://memorybear.redbearai.com', // 测试服务器 changeOrigin: true, // 匹配所有以/api开头的请求,包括/api/token