Sync frontend project from dev-yjp branch
- Updated web folder with latest frontend code - Added new components: CreateContentModal, CreateContentModalExample - Added new hook: useBreadcrumbManager - Updated knowledge base components and views - Updated i18n translations - Various bug fixes and improvements
This commit is contained in:
6
web/.gitignore
vendored
6
web/.gitignore
vendored
@@ -24,3 +24,9 @@ dist-ssr
|
||||
*.sw?
|
||||
|
||||
package-lock.json
|
||||
|
||||
# 文档和截图(不上传到仓库)
|
||||
操作说明.md
|
||||
记忆熊系统功能使用说明.md
|
||||
截图清单.md
|
||||
images/
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
// 文档分块预览
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 152 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 184 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 189 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB |
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 29</title>
|
||||
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="红熊空间-应用管理" transform="translate(-24, -249)" stroke="#212332">
|
||||
<g id="记忆库" transform="translate(12, 241)">
|
||||
<g id="编组-29" transform="translate(12, 8)">
|
||||
<g id="编组-30" transform="translate(1.5, 2)">
|
||||
<path d="M5.15208739,12 L3.96504286,11.9873871 C3.14287934,11.9786512 2.48098017,11.3096817 2.48098017,10.4874718 L2.48098017,8.17573134 L2.48098017,8.17573134 L0.701703053,7.96314675 C0.482349244,7.93693878 0.325773604,7.73787163 0.351981572,7.51851782 C0.360586526,7.44649662 0.388612869,7.37817062 0.433061291,7.3208519 L1.79815052,5.56049306 L1.79815052,5.56049306 L1.79815052,5.43150782 C1.79815052,2.43176888 4.30576994,0 7.39907526,0 C10.4923806,0 13,2.43176888 13,5.43150782" id="路径" stroke-linecap="round"></path>
|
||||
<path d="M8,6 L11,6 C11.5522847,6 12,6.44771525 12,7 L12,11 C12,11.5522847 11.5522847,12 11,12 L8,12 C7.44771525,12 7,11.5522847 7,11 L7,7 C7,6.44771525 7.44771525,6 8,6 Z" id="矩形"></path>
|
||||
<line x1="7" y1="8" x2="12" y2="8" id="路径-26"></line>
|
||||
<line x1="7" y1="10" x2="12" y2="10" id="路径-26备份"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -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<EmptyProps> = ({
|
||||
url,
|
||||
size = 200,
|
||||
title,
|
||||
isNeedSubTitle = true,
|
||||
subTitle,
|
||||
className = '',
|
||||
}) => {
|
||||
@@ -22,12 +20,12 @@ const Empty: FC<EmptyProps> = ({
|
||||
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 (
|
||||
<div className={`rb:flex rb:items-center rb:justify-center rb:flex-col ${className}`}>
|
||||
<img src={url || emptyIcon} alt="404" style={{ width: `${width}px`, height: `${height}px` }} />
|
||||
{title && <div className="rb:mt-2 rb:leading-5">{title}</div>}
|
||||
{curSubTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-4 rb:text-[12px] rb:text-[#A8A9AA]`}>{subTitle}</div>}
|
||||
{title && <div className="rb:mt-[8px] rb:leading-[20px]">{title}</div>}
|
||||
{subTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-[16px] rb:text-[#5B6167]`}>{subTitle}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<SettingModalRef>(null)
|
||||
const userInfoModalRef = useRef<UserInfoModalRef>(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 = () => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
248
web/src/hooks/useBreadcrumbManager.ts
Normal file
248
web/src/hooks/useBreadcrumbManager.ts
Normal file
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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]);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '红熊记忆科学',
|
||||
|
||||
@@ -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') || '[]'
|
||||
|
||||
@@ -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<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
{chatList.length === 0
|
||||
? <Empty
|
||||
url={DebuggingEmpty}
|
||||
size={[300, 200]}
|
||||
title={t('application.debuggingEmpty')}
|
||||
subTitle={t('application.debuggingEmptyDesc')}
|
||||
className="rb:h-full"
|
||||
@@ -272,7 +271,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
</div>
|
||||
}
|
||||
{!chat.list || chat.list.length === 0
|
||||
? <Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} className="rb:h-full" size={[240, 200]} />
|
||||
? <Empty url={ChatIcon} title={t('application.chatEmpty')} className="rb:h-full" />
|
||||
: (
|
||||
<div ref={el => scrollContainerRefs.current[index] = el} className={clsx(`rb:relative rb:overflow-y-auto rb:overflow-x-hidden`, {
|
||||
'rb:h-[calc(100vh-186px)]': isCluster,
|
||||
|
||||
@@ -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<number>(stepIndexMap[initialStepKey]);
|
||||
const tableRef = useRef<TableRef>(null);
|
||||
|
||||
|
||||
const [data, setData] = useState<KnowledgeBaseDocumentData[]>([]);
|
||||
const [chunkData, setChunkData] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const [rechunkFileIds, setRechunkFileIds] = useState<string[]>(initialFileIds);
|
||||
const [curSelectedFileId, setCurSelectedFileId] = useState<number>(-1);
|
||||
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
|
||||
|
||||
const [pollingLoading, setPollingLoading] = useState<boolean>(false);
|
||||
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const [delimiter, setDelimiter] = useState<string | undefined>(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 {
|
||||
|
||||
@@ -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<KnowledgeBaseDocumentData | null>(null);
|
||||
const [chunkList, setChunkList] = useState<RecallTestData[]>([]);
|
||||
@@ -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`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<string | undefined>(undefined);
|
||||
const recallTestDrawerRef = useRef<RecallTestDrawerRef>(null);
|
||||
const createFolderModalRef = useRef<CreateFolderModalRef>(null);
|
||||
const createImageDataset = useRef<CreateImageModalRef>(null)
|
||||
const createImageDataset = useRef<CreateSetModalRef>(null)
|
||||
const createContentModalRef = useRef<CreateSetModalRef>(null);
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseListItem | null>(null);
|
||||
const [folder, setFolder] = useState<FolderFormData | null>({
|
||||
kb_id:knowledgeBaseId ?? '',
|
||||
@@ -56,47 +60,47 @@ const Private: FC = () => {
|
||||
});
|
||||
const [query, setQuery] = useState<Record<string, unknown>>({
|
||||
orderby: 'created_at',
|
||||
desc: true,
|
||||
desc: true
|
||||
});
|
||||
const modalRef = useRef<CreateModalRef>(null)
|
||||
const shareModalRef = useRef<ShareModalRef>(null);
|
||||
const datasetModalRef = useRef<CreateDatasetModalRef>(null);
|
||||
const [folderTreeRefreshKey, setFolderTreeRefreshKey] = useState(0);
|
||||
const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu();
|
||||
const [folderPath, setFolderPath] = useState<Array<{ id: string; name: string }>>([]);
|
||||
const [autoExpandPath, setAutoExpandPath] = useState<Array<{ id: string; name: string }>>([]);
|
||||
|
||||
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<BreadcrumbItem[]>([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||
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<BreadcrumbItem[]>([]);
|
||||
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: <img src={blankIcon} alt="Custome Text" style={{ width: 16, height: 16 }} />,
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -601,6 +683,10 @@ const Private: FC = () => {
|
||||
ref={createFolderModalRef}
|
||||
refreshTable={refreshDirectoryTree}
|
||||
/>
|
||||
<CreateContentModal
|
||||
ref={createContentModalRef}
|
||||
refreshTable={handleRefreshTable}
|
||||
/>
|
||||
<CreateModal
|
||||
ref={modalRef}
|
||||
refreshTable={handleRefreshTable}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useRef, type FC } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { KnowledgeBaseListItem, RecallTestDrawerRef } from '@/views/KnowledgeBase/types';
|
||||
import RecallTest from '../components/RecallTest';
|
||||
@@ -15,17 +15,22 @@ import kbModelIcon from '@/assets/images/knowledgeBase/kb-model.png';
|
||||
import kbHistoryIcon from '@/assets/images/knowledgeBase/kb-history.png';
|
||||
import { getKnowledgeBaseDetail } from '@/api/knowledgeBase';
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import { useMenu } from '@/store/menu';
|
||||
import { useBreadcrumbManager, type BreadcrumbItem } from '@/hooks/useBreadcrumbManager';
|
||||
|
||||
const Share: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams<{ knowledgeBaseId: string }>();
|
||||
const location = useLocation();
|
||||
const knowledgeBaseId = params.knowledgeBaseId;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseListItem | null>(null);
|
||||
const recallTestRef = useRef<RecallTestDrawerRef>(null);
|
||||
const [infoItems, setInfoItems] = useState<InfoItem[]>([]);
|
||||
const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu();
|
||||
const [knowledgeBaseFolderPath, setKnowledgeBaseFolderPath] = useState<BreadcrumbItem[]>([]);
|
||||
|
||||
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');
|
||||
|
||||
104
web/src/views/KnowledgeBase/components/CreateContentModal.tsx
Normal file
104
web/src/views/KnowledgeBase/components/CreateContentModal.tsx
Normal file
@@ -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<CreateSetModalRef, CreateSetMoealRefProps>(
|
||||
({ refreshTable }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ContentFormData>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [kbId, setKbId] = useState<string>('');
|
||||
const [parentId, setParentId] = useState<string>('');
|
||||
|
||||
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 (
|
||||
<RbModal
|
||||
title={t('knowledgeBase.createContent')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.create')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
width={600}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="title"
|
||||
label={t('knowledgeBase.title')}
|
||||
rules={[{ required: true, message: t('knowledgeBase.pleaseEnterTitle') }]}
|
||||
>
|
||||
<Input placeholder={t('knowledgeBase.pleaseEnterTitle')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="content"
|
||||
label={t('knowledgeBase.content')}
|
||||
rules={[{ required: true, message: t('knowledgeBase.pleaseEnterContent') }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={t('knowledgeBase.pleaseEnterContent')}
|
||||
rows={8}
|
||||
showCount
|
||||
maxLength={5000}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default CreateContentModal;
|
||||
@@ -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<CreateContentModalRef>(null);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
// 打开弹窗,传入知识库ID和父级ID
|
||||
createContentModalRef.current?.handleOpen('kb_123', 'parent_456');
|
||||
};
|
||||
|
||||
const handleRefreshTable = () => {
|
||||
console.log('刷新表格数据');
|
||||
// 这里可以添加刷新表格的逻辑
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button type="primary" onClick={handleOpenModal}>
|
||||
创建内容
|
||||
</Button>
|
||||
|
||||
<CreateContentModal
|
||||
ref={createContentModalRef}
|
||||
refreshTable={handleRefreshTable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateContentModalExample;
|
||||
@@ -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<CreateImageModalRef, CreateImageMoealRefProps>(
|
||||
const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>(
|
||||
({ refreshTable }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
@@ -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<FolderTreeProps> = ({
|
||||
onRootLoad,
|
||||
onFolderPathChange,
|
||||
selectedKeys,
|
||||
autoExpandPath,
|
||||
}) => {
|
||||
const [treeData, setTreeData] = useState<TreeNodeData[]>([]);
|
||||
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
||||
const [autoExpandInProgress, setAutoExpandInProgress] = useState(false);
|
||||
|
||||
// 更新树节点数据的辅助函数
|
||||
const updateTreeData = (nodes: TreeNodeData[], key: Key, children: TreeNodeData[]): TreeNodeData[] => {
|
||||
@@ -370,6 +375,109 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
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<FolderTreeProps> = ({
|
||||
|
||||
return (
|
||||
<DirectoryTree
|
||||
key={refreshKey} // 添加key确保refreshKey变化时重新渲染整个组件
|
||||
multiple={multiple}
|
||||
className={className}
|
||||
style={style}
|
||||
onSelect={handleSelect}
|
||||
onExpand={onExpand}
|
||||
onExpand={handleExpand}
|
||||
expandedKeys={expandedKeys}
|
||||
loadData={onLoadData}
|
||||
treeData={treeNodes}
|
||||
selectedKeys={selectedKeys}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState, useRef, useMemo, type FC } from 'react';
|
||||
import { useEffect, useState, useRef, useMemo, useCallback, type FC } from 'react';
|
||||
import { Row, Col, Button, Dropdown, Modal, message, Tooltip } from 'antd'
|
||||
import type { MenuProps } from 'antd';
|
||||
import { EllipsisOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
@@ -18,7 +18,8 @@ import Empty from '@/components/Empty'
|
||||
import { getKnowledgeBaseList, getModelList, getModelTypeList, deleteKnowledgeBase, getKnowledgeBaseTypeList } from '@/api/knowledgeBase'
|
||||
const { confirm } = Modal;
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import { useMenu } from '@/store/menu';
|
||||
|
||||
import { useBreadcrumbManager, type BreadcrumbItem } from '@/hooks/useBreadcrumbManager';
|
||||
|
||||
type ModelMenuInfo = {
|
||||
menu: NonNullable<MenuProps['items']>;
|
||||
@@ -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<KnowledgeBaseListItem[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
@@ -42,10 +44,29 @@ const KnowledgeBaseManagement: FC = () => {
|
||||
const modelListCache = useRef<Record<string, string>>({});
|
||||
const modalRef = useRef<CreateModalRef>(null)
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const processedStateRef = useRef<any>(null);
|
||||
|
||||
// 使用 menu store 管理面包屑
|
||||
const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu();
|
||||
const [folderPath, setFolderPath] = useState<Array<{ id: string; name: string }>>([]);
|
||||
// 使用面包屑管理 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<BreadcrumbItem[]>([]);
|
||||
|
||||
|
||||
// 生成下拉菜单项(根据当前 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 (
|
||||
<>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
<Empty
|
||||
url={AnalysisEmptyIcon}
|
||||
className="rb:h-full"
|
||||
title={t('memoryConversation.memoryConversationAnalysisEmpty')}
|
||||
subTitle={t('memoryConversation.memoryConversationAnalysisEmptySubTitle')}
|
||||
size={[270, 170]}
|
||||
/>
|
||||
: <Space size={12} direction="vertical" style={{width: '100%'}}>
|
||||
{logs.map((log, logIndex) => (
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user