diff --git a/web/.gitignore b/web/.gitignore index 0de8ef71..b398d222 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -24,9 +24,3 @@ dist-ssr *.sw? package-lock.json - -# 文档和截图(不上传到仓库) -操作说明.md -记忆熊系统功能使用说明.md -截图清单.md -images/ \ No newline at end of file diff --git a/web/README.md b/web/README.md new file mode 100644 index 00000000..7ce73358 --- /dev/null +++ b/web/README.md @@ -0,0 +1,91 @@ +# Memory Bear 前端项目 + +基于 React + TypeScript + Vite + Ant Design 构建的知识库管理系统前端应用。 + +## 技术栈 + +- **框架**: React 18 + TypeScript +- **构建工具**: Vite +- **UI 组件库**: Ant Design 5 +- **样式**: Tailwind CSS 4 +- **路由**: React Router 6 +- **状态管理**: Zustand +- **国际化**: i18next +- **图表**: ECharts +- **其他**: React Markdown + +## 环境要求 + +- Node.js >= 20.19+, 22.12+ +- npm 或 yarn + +## 安装 + +```bash +# 克隆项目 +git clone + +# 进入项目目录 +cd memory-bear-font-end + +# 安装依赖 +npm install +``` + +## 运行 + +### 开发环境 + +```bash +npm run dev +``` + +启动后访问: `http://localhost:5173` + +### 生产构建 + +```bash +npm run build +``` + +构建产物输出到 `dist` 目录。 + +### 预览构建结果 + +```bash +npm run preview +``` + +## 代码检查 + +```bash +npm run lint +``` + +## 项目结构 + +``` +src/ +├── api/ # API 接口 +├── assets/ # 静态资源 +├── components/ # 公共组件 +├── hooks/ # 自定义 Hooks +├── i18n/ # 国际化配置 +├── routes/ # 路由配置 +├── store/ # 状态管理 +├── styles/ # 全局样式 +├── utils/ # 工具函数 +├── views/ # 页面视图 +├── App.tsx # 应用入口组件 +└── main.tsx # 应用入口文件 +``` + +## 配置说明 + +- 开发服务器默认监听 `0.0.0.0:5173` +- API 代理配置在 `vite.config.ts` 中 +- 路径别名 `@` 指向 `src` 目录 + +## License + +Private diff --git a/web/i18n-comparison-report.md b/web/i18n-comparison-report.md new file mode 100644 index 00000000..07e59b22 --- /dev/null +++ b/web/i18n-comparison-report.md @@ -0,0 +1,205 @@ +# i18n 中英文对比报告 + +## 📊 统计概览 + +- **中文键总数**: 1136 +- **英文键总数**: 1052 +- **中文缺失**: 27 个键 +- **英文缺失**: 111 个键 + +--- + +## ❌ 英文缺失的翻译(111个) + +### 1. Application 模块 (3个) +- `application.cluster` - 集群 +- `application.clusterDesc` - 创建Agent集群 +- `application.fullAmount` - 全量 + +### 2. Role 角色管理模块 (15个) +- `role.roleManagement` - 角色管理 +- `role.roleId` - 角色ID +- `role.roleName` - 角色名称 +- `role.roleCode` - 角色编码 +- `role.description` - 角色描述 +- `role.status` - 状态 +- `role.enabled` - 已启用 +- `role.disabled` - 已停用 +- `role.createTime` - 创建时间 +- `role.createRole` - 新建角色 +- `role.editRole` - 编辑角色 +- `role.roleTemplate` - 角色模板 +- `role.emptyTemplate` - 空模板 +- `role.adminTemplate` - 管理员模板 +- `role.userTemplate` - 用户模板 +- `role.confirmDelete` - 确定要删除这个角色吗? +- `role.createSuccess` - 角色创建成功 +- `role.updateSuccess` - 角色更新成功 +- `role.deleteSuccess` - 角色删除成功 +- `role.createFailed` - 角色创建失败 +- `role.updateFailed` - 角色更新失败 +- `role.deleteFailed` - 角色删除失败 + +### 3. Tenant 租户管理模块 (20个) +- `tenant.tenantId` - 租户ID +- `tenant.tenantName` - 租户名称 +- `tenant.contactPerson` - 联系人 +- `tenant.contactInfo` - 联系方式 +- `tenant.status` - 状态 +- `tenant.enabled` - 启用 +- `tenant.disabled` - 禁用 +- `tenant.expiryDate` - 到期时间 +- `tenant.createTenant` - 新增租户 +- `tenant.editTenant` - 编辑租户 +- `tenant.searchPlaceholder` - 搜索租户ID、名称、联系人或联系方式 +- `tenant.confirmDelete` - 确定要删除该租户吗? +- `tenant.confirmBatchDelete` - 确定要批量删除选中的租户吗? +- `tenant.fetchFailed` - 获取租户数据失败 +- `tenant.batchEnableSuccess` - 批量启用成功 +- `tenant.batchEnableFailed` - 批量启用失败 +- `tenant.batchDisableSuccess` - 批量停用成功 +- `tenant.batchDisableFailed` - 批量停用失败 +- `tenant.exportSuccess` - 导出成功 +- `tenant.batchDeleteSuccess` - 批量删除成功 +- `tenant.batchDeleteFailed` - 批量删除失败 +- `tenant.saveFailed` - 保存失败 +- `tenant.batchImport` - 批量导入 + +### 4. User 用户管理模块 (13个) +- `user.tenantName` - 所属租户 +- `user.password` - 密码 +- `user.expiryDate` - 有效期 +- `user.expiryDateDue` - 有效期至 +- `user.batchImport` - 批量导入 +- `user.batchImportUser` - 批量导入用户 +- `user.downloadTemplate` - 下载导入模板 +- `user.templateDownloadSuccess` - 模板下载成功 +- `user.startImport` - 开始导入 +- `user.batchImportSuccess` - 批量导入成功 +- `user.importFailed` - 导入失败,请检查文件格式 +- `user.noFileSelected` - 请选择要导入的文件 +- `user.onlyXlsxOrCsv` - 只能上传 .xlsx 或 .csv 格式的文件 +- `user.reselect` - 重新选择 +- `user.noFileSelectedTip` - 未选择任何文件 +- `user.downloadTemplateTip` - 请下载模板,填写用户信息后上传。 + +### 5. Product 产品管理模块 (13个) +- `product.applicationManagement` - 应用管理 +- `product.createApplication` - 创建应用 +- `product.applicationName` - 应用名称 +- `product.applicationIcon` - 应用图标 +- `product.applicationNameRequired` - 请输入应用名称 +- `product.associationStatus` - 关联状态 +- `product.associated` - 已关联 +- `product.notAssociated` - 未关联 +- `product.unassociate` - 解除关联 +- `product.unassociateSuccess` - 解除关联成功 +- `product.unassociateFailed` - 解除关联失败 +- `product.viewKey` - 查看KEY +- `product.viewStats` - 查看统计 +- `product.disableSuccess` - 停用成功 +- `product.enableSuccess` - 启用成功 +- `product.operationFailed` - 操作失败 + +### 6. 其他模块 (47个) +- `count` - 计数: {{count}} +- `increment` - 增加 +- `decrement` - 减少 +- `reset` - 重置 +- `switchLanguage` - 切换语言 +- `home.title` - 首页 +- `home.welcome` - 欢迎使用我们的带单页路由的 React 应用! +- `home.counterCard` - 计数器演示 +- `home.aboutCard` - 关于我们 +- `home.workflowCard` - 工作流编辑器 +- `home.websocketDemoCard` - WebSocket 演示 +- `home.sseDemoCard` - SSE演示 +- `workflow.title` - 工作流编辑器 +- `workflow.description` - 拖拽节点创建连接,构建您的工作流程。点击节点可进行配置。 +- `workflow.addNode` - 添加节点 +- `workflow.deleteNode` - 删除选中 +- `workflow.saveWorkflow` - 保存工作流 +- `workflow.startNode` - 触发节点 +- `workflow.conditionNode` - 条件判断 +- `workflow.actionNode` - 执行动作 +- `workflow.endNode` - 结束节点 +- `workflow.newNode` - 新节点 +- `workflow.node` - 节点 +- `workflow.nodesCreated` - 已创建节点 +- `workflow.loadingNodes` - 正在加载节点 {{progress}}% +- `workflow.loadingFailed` - 加载节点失败 +- `workflow.create5kNodes` - 创建5000节点 +- `workflow.create10kNodes` - 创建10000节点 +- `notFound.title` - 页面未找到 +- `notFound.description` - 请求的页面不存在。 +- `notFound.backToHome` - 返回首页 + +--- + +## ✅ 中文缺失的翻译(27个) + +### 1. Common 通用模块 (1个) +- `common.operateSuccess` - Operation successful + +### 2. KnowledgeBase 知识库模块 (3个) +- `knowledgeBase.models` - Model +- `knowledgeBase.owner` - Owner +- `knowledgeBase.operation` - Operation + +### 3. Application 应用模块 (15个) +- `application.multi_agent` - Cluster +- `application.multi_agentDesc` - Create an Agent Cluster +- `application.current` - Current +- `application.versionName` - Version Name +- `application.versionNameTip` - Version number format: v[major version number].[next version number].[revision number] (e.g. v1.3.0) +- `application.agentName` - Agent Name +- `application.roleType` - Role Type +- `application.coordinator` - Coordinator +- `application.analyzer` - Analyzer +- `application.executor` - Executor +- `application.reviewer` - Reviewer +- `application.updateSubAgent` - Update Sub Agent +- `application.subAgentMaxLength` - Sub Agent maximum {{maxLength}} +- `application.capabilities` - Capabilities + +### 4. Space 空间模块 (5个) +- `space.storageType` - Storage Type +- `space.rag` - RAG storage +- `space.ragDesc` - Based on vector retrieval, suitable for document Q&A and semantic search +- `space.neo4j` - Graph storage +- `space.neo4jDesc` - Based on knowledge graph, suitable for relational reasoning and path query + +### 5. MemoryExtractionEngine 记忆提取引擎模块 (4个) +- `memoryExtractionEngine.coreEntitiesAfterDedup` - Core entities after deduplication +- `memoryExtractionEngine.extractRelationalTriples` - Extracted relational triples (partial) +- `memoryExtractionEngine.extractRelationalTriplesDesc` - There are a total of {{count}} segments with clear semantic boundaries +- `memoryExtractionEngine.theEffectOfEntityDisambiguationLLMDriven` - The effect of entity disambiguation (LLM driven) + +--- + +## 🎯 建议 + +### 优先级 1 - 核心功能模块(需要立即补充) +1. **Role 角色管理** - 完整模块缺失(15个键) +2. **Tenant 租户管理** - 完整模块缺失(20个键) +3. **Product 产品管理** - 完整模块缺失(13个键) +4. **User 用户管理扩展** - 批量导入功能缺失(13个键) + +### 优先级 2 - 功能增强(建议补充) +1. **Application 应用模块** - 多代理相关功能(15个键) +2. **Space 空间模块** - 存储类型配置(5个键) +3. **MemoryExtractionEngine** - 实体去重相关(4个键) + +### 优先级 3 - 演示/测试功能(可选) +1. **Home/Workflow/NotFound** - 演示页面(30个键) +2. **通用计数器功能** - 测试功能(5个键) + +--- + +## 📝 下一步行动 + +1. **补充英文翻译**: 优先补充 Role、Tenant、Product、User 模块的英文翻译 +2. **补充中文翻译**: 补充 Application、Space、MemoryExtractionEngine 模块的中文翻译 +3. **清理无用翻译**: 如果 Home/Workflow 等演示功能不再使用,可以考虑从中文文件中移除 +4. **建立翻译规范**: 建议建立翻译键的命名规范和审查流程,避免未来出现遗漏 + diff --git a/web/public/auto-imports.d.ts b/web/public/auto-imports.d.ts index 61cb2733..62f88140 100644 --- a/web/public/auto-imports.d.ts +++ b/web/public/auto-imports.d.ts @@ -6,31 +6,22 @@ // biome-ignore lint: disable export {} declare global { - const Activity: typeof import('react').Activity - const Fragment: typeof import('react').Fragment const Link: typeof import('react-router-dom').Link const NavLink: typeof import('react-router-dom').NavLink const Navigate: typeof import('react-router-dom').Navigate const Outlet: typeof import('react-router-dom').Outlet const Route: typeof import('react-router-dom').Route const Routes: typeof import('react-router-dom').Routes - const Suspense: typeof import('react').Suspense - const cache: typeof import('react').cache - const cacheSignal: typeof import('react').cacheSignal - const createContext: typeof import('react').createContext const createRef: typeof import('react').createRef const forwardRef: typeof import('react').forwardRef const lazy: typeof import('react').lazy const memo: typeof import('react').memo const startTransition: typeof import('react').startTransition - const use: typeof import('react').use - const useActionState: typeof import('react').useActionState const useCallback: typeof import('react').useCallback const useContext: typeof import('react').useContext const useDebugValue: typeof import('react').useDebugValue const useDeferredValue: typeof import('react').useDeferredValue const useEffect: typeof import('react').useEffect - const useEffectEvent: typeof import('react').useEffectEvent const useHref: typeof import('react-router-dom').useHref const useId: typeof import('react').useId const useImperativeHandle: typeof import('react').useImperativeHandle @@ -42,7 +33,6 @@ declare global { const useMemo: typeof import('react').useMemo const useNavigate: typeof import('react-router-dom').useNavigate const useNavigationType: typeof import('react-router-dom').useNavigationType - const useOptimistic: typeof import('react').useOptimistic const useOutlet: typeof import('react-router-dom').useOutlet const useOutletContext: typeof import('react-router-dom').useOutletContext const useParams: typeof import('react-router-dom').useParams diff --git a/web/src/App.tsx b/web/src/App.tsx index 8ed03f96..c255f522 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -28,6 +28,8 @@ import 'dayjs/locale/zh-cn' import 'dayjs/plugin/timezone' import 'dayjs/plugin/utc' + + function App() { const { t } = useTranslation(); const { locale, language, timeZone } = useI18n() diff --git a/web/src/views/KnowledgeBase/service.ts b/web/src/api/knowledgeBase.ts similarity index 98% rename from web/src/views/KnowledgeBase/service.ts rename to web/src/api/knowledgeBase.ts index 23857d41..9791ee8d 100644 --- a/web/src/views/KnowledgeBase/service.ts +++ b/web/src/api/knowledgeBase.ts @@ -1,4 +1,4 @@ -import { request } from "@/utils/request"; +import { request, cookieUtils } from "@/utils/request"; import type { AxiosProgressEvent } from "axios"; import type { ShareRequestParams, @@ -15,7 +15,7 @@ import type { KnowledgeBaseDocumentData, KnowledgeBaseListResponse, KnowledgeBaseShareListResponse, -} from "./types"; +} from "@/views/KnowledgeBase/types"; const apiPrefix = ''; @@ -151,7 +151,7 @@ export const uploadFile = async (data: FormData, options?: UploadFileOptions) => // 下载文件 export const downloadFile = async (fileId: string, fileName?: string) => { - const token = localStorage.getItem('token'); + const token = cookieUtils.get('authToken'); const url = `${apiPrefix}/files/${fileId}`; try { @@ -277,4 +277,4 @@ export const createDocumentChunk = async (kb_id:string, document_id:string, data export const getRetrievalModeType = async () => { const response = await request.get(`${apiPrefix}/chunks/retrieve_type`); return response as any; -}; \ No newline at end of file +}; diff --git a/web/src/api/user.ts b/web/src/api/user.ts index 912ea537..3ff03386 100644 --- a/web/src/api/user.ts +++ b/web/src/api/user.ts @@ -1,5 +1,6 @@ import { request } from '@/utils/request' import type { CreateModalData } from '@/views/UserManagement/types' +import { cookieUtils } from '@/utils/request' // 用户信息 export const getUsers = () => { @@ -15,7 +16,7 @@ export const login = (data: { email: string; password: string; invite?: string; // 刷新token export const refreshTokenUrl = '/refresh' export const refreshToken = () => { - return request.post(refreshTokenUrl, { refresh_token: localStorage.getItem('refresh_token') }) + return request.post(refreshTokenUrl, { refresh_token: cookieUtils.get('refreshToken') }) } // 重置密码 export const changePassword = (data: { user_id: string; new_password: string }) => { diff --git a/web/src/assets/images/application/chat.png b/web/src/assets/images/application/chat.png new file mode 100644 index 00000000..a8bb40fa Binary files /dev/null and b/web/src/assets/images/application/chat.png differ diff --git a/web/src/assets/images/application/chat.svg b/web/src/assets/images/application/chat.svg deleted file mode 100644 index 9cdf744f..00000000 --- a/web/src/assets/images/application/chat.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - 常见问题 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/assets/images/application/debuggingEmpty.png b/web/src/assets/images/application/debuggingEmpty.png new file mode 100644 index 00000000..0879d4e3 Binary files /dev/null and b/web/src/assets/images/application/debuggingEmpty.png differ diff --git a/web/src/assets/images/application/debuggingEmpty.svg b/web/src/assets/images/application/debuggingEmpty.svg deleted file mode 100644 index 11f38f03..00000000 --- a/web/src/assets/images/application/debuggingEmpty.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - 分类 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/assets/images/conversation/analysisEmpty.png b/web/src/assets/images/conversation/analysisEmpty.png new file mode 100644 index 00000000..6d497f31 Binary files /dev/null and b/web/src/assets/images/conversation/analysisEmpty.png differ diff --git a/web/src/assets/images/empty/404.png b/web/src/assets/images/empty/404.png new file mode 100644 index 00000000..3a4c684d Binary files /dev/null and b/web/src/assets/images/empty/404.png differ diff --git a/web/src/assets/images/empty/404.svg b/web/src/assets/images/empty/404.svg deleted file mode 100644 index 96c7f605..00000000 --- a/web/src/assets/images/empty/404.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - 404 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/assets/images/empty/noPermission.png b/web/src/assets/images/empty/noPermission.png new file mode 100644 index 00000000..cacac721 Binary files /dev/null and b/web/src/assets/images/empty/noPermission.png differ diff --git a/web/src/assets/images/empty/noPermission.svg b/web/src/assets/images/empty/noPermission.svg deleted file mode 100644 index 7ba9396a..00000000 --- a/web/src/assets/images/empty/noPermission.svg +++ /dev/null @@ -1,62 +0,0 @@ - - - 订单 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/assets/images/knowledgeBase/blankImage.png b/web/src/assets/images/knowledgeBase/blankImage.png index b108f627..3aa59b88 100644 Binary files a/web/src/assets/images/knowledgeBase/blankImage.png and b/web/src/assets/images/knowledgeBase/blankImage.png differ diff --git a/web/src/assets/images/knowledgeBase/noData.png b/web/src/assets/images/knowledgeBase/noData.png index 96af1243..c36100e8 100644 Binary files a/web/src/assets/images/knowledgeBase/noData.png and b/web/src/assets/images/knowledgeBase/noData.png differ diff --git a/web/src/assets/images/menu/userMemory_active.svg b/web/src/assets/images/menu/userMemory_active.svg new file mode 100644 index 00000000..554dc0bc --- /dev/null +++ b/web/src/assets/images/menu/userMemory_active.svg @@ -0,0 +1,18 @@ + + + 编组 29 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/DocumentPreview/index.tsx b/web/src/components/DocumentPreview/index.tsx index b60a7ed9..404d6e50 100644 --- a/web/src/components/DocumentPreview/index.tsx +++ b/web/src/components/DocumentPreview/index.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, type FC } from 'react'; import { Spin, Alert, Button } from 'antd'; import { ReloadOutlined } from '@ant-design/icons'; import RbMarkdown from '../Markdown'; +import { cookieUtils } from '@/utils/request' type PreviewMode = 'office' | 'google'; @@ -156,7 +157,7 @@ const DocumentPreview: FC = ({ const response = await fetch(requestUrl, { credentials: 'include', // 包含认证信息 headers: { - 'Authorization': `Bearer ${localStorage.getItem('token') || ''}`, + 'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`, }, }); diff --git a/web/src/components/Empty/index.tsx b/web/src/components/Empty/index.tsx index 3aa2bb50..53f52941 100644 --- a/web/src/components/Empty/index.tsx +++ b/web/src/components/Empty/index.tsx @@ -6,6 +6,7 @@ interface EmptyProps { url?: string; size?: number | number[]; title?: string; + isNeedSubTitle?: boolean; subTitle?: string; className?: string; } @@ -13,6 +14,7 @@ const Empty: FC = ({ url, size = 200, title, + isNeedSubTitle = true, subTitle, className = '', }) => { @@ -20,12 +22,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; - subTitle = subTitle || t('empty.tableEmpty'); + const curSubTitle = isNeedSubTitle ? (subTitle || t('empty.tableEmpty')) : null; return (
404 - {title &&
{title}
} - {subTitle &&
{subTitle}
} + {title &&
{title}
} + {curSubTitle &&
{subTitle}
}
); } diff --git a/web/src/components/Header/index.tsx b/web/src/components/Header/index.tsx index 5ae59eb0..6ce5484e 100644 --- a/web/src/components/Header/index.tsx +++ b/web/src/components/Header/index.tsx @@ -66,10 +66,30 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { }, ]; const formatBreadcrumbNames = () => { - return breadcrumbs.map((menu, index) => ({ - title: menu.i18nKey ? t(menu.i18nKey) : menu.label, - path: index === breadcrumbs.length - 1 ? undefined : menu.path - })) + return breadcrumbs.map((menu, index) => { + const item: any = { + title: menu.i18nKey ? t(menu.i18nKey) : menu.label, + }; + + // 如果是最后一项,不设置 path + if (index === breadcrumbs.length - 1) { + return item; + } + + // 如果有自定义 onClick,使用 onClick 并设置 href 为 '#' 以显示手型光标 + if ((menu as any).onClick) { + item.onClick = (e: React.MouseEvent) => { + e.preventDefault(); + (menu as any).onClick(e); + }; + item.href = '#'; + } else if (menu.path && menu.path !== '#') { + // 只有当 path 不是 '#' 时才设置 path + item.path = menu.path; + } + + return item; + }); } return (
diff --git a/web/src/components/Layout/AuthLayout.tsx b/web/src/components/Layout/AuthLayout.tsx index c53be942..a969298d 100644 --- a/web/src/components/Layout/AuthLayout.tsx +++ b/web/src/components/Layout/AuthLayout.tsx @@ -6,6 +6,7 @@ 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; @@ -18,7 +19,12 @@ const AuthLayout: FC = () => { // 自动更新面包屑导航 useNavigationBreadcrumbs('manage'); useEffect(() => { - getUserInfo() + const authToken = cookieUtils.get('authToken') + if (!authToken && !window.location.hash.includes('#/login')) { + window.location.href = `/#/login`; + } else { + getUserInfo() + } }, []); return ( diff --git a/web/src/components/Layout/AuthSpaceLayout.tsx b/web/src/components/Layout/AuthSpaceLayout.tsx index acc2cdb0..17ee0bac 100644 --- a/web/src/components/Layout/AuthSpaceLayout.tsx +++ b/web/src/components/Layout/AuthSpaceLayout.tsx @@ -6,6 +6,7 @@ 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; @@ -18,8 +19,13 @@ const AuthSpaceLayout: FC = () => { // 自动更新面包屑导航 useNavigationBreadcrumbs('space'); useEffect(() => { - getUserInfo() - getStorageType() + const authToken = cookieUtils.get('authToken') + if (!authToken && !window.location.hash.includes('#/login')) { + window.location.href = `/#/login`; + } else { + getUserInfo() + getStorageType() + } }, []); return ( diff --git a/web/src/components/Upload/UploadFiles.tsx b/web/src/components/Upload/UploadFiles.tsx index b577d18f..aedd0e17 100644 --- a/web/src/components/Upload/UploadFiles.tsx +++ b/web/src/components/Upload/UploadFiles.tsx @@ -6,6 +6,7 @@ import type { UploadProps, UploadFile } from 'antd'; import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface'; import CloudUploadOutlined from '@/assets/images/CloudUploadOutlined.png' import { useTranslation } from 'react-i18next'; +import { cookieUtils } from '@/utils/request' const { confirm } = Modal; const { Dragger } = Upload; @@ -219,7 +220,7 @@ const UploadFiles = forwardRef(({ fileList, beforeUpload, headers: { - authorization: localStorage.getItem('token') || '', + authorization: cookieUtils.get('authToken') || '', }, onRemove: handleRemove, onChange: handleChange, diff --git a/web/src/components/Upload/UploadImages.tsx b/web/src/components/Upload/UploadImages.tsx index 1e77ad3f..2006ea09 100644 --- a/web/src/components/Upload/UploadImages.tsx +++ b/web/src/components/Upload/UploadImages.tsx @@ -5,6 +5,7 @@ import type { GetProp, UploadFile, UploadProps } from 'antd'; import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface'; import { useTranslation } from 'react-i18next'; import PlusIcon from '@/assets/images/plus.svg' +import { cookieUtils } from '@/utils/request' const { confirm } = Modal; @@ -166,7 +167,7 @@ const UploadImages = forwardRef(({ fileList, beforeUpload, headers: { - authorization: localStorage.getItem('token') || '', + authorization: cookieUtils.get('authToken') || '', }, onPreview: handlePreview, onRemove: handleRemove, diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 184c34cd..41347c66 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -403,6 +403,7 @@ export const en = { apiKeyName: 'API Key Name', }, knowledgeBase: { + home: 'Home', selectSpace: 'Please select a workspace.', preview:'Preview', pleaseUploadFileFirst: 'Please upload file first', @@ -1171,6 +1172,8 @@ 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 8dc17275..7aeaed03 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -35,6 +35,7 @@ export const zh = { userMemoryDetail: '用户记忆详情', }, knowledgeBase: { + home: '首页', selectSpace: '请选择空间', preview:'预览', pleaseUploadFileFirst: '请先上传文件', @@ -1241,6 +1242,8 @@ 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 0d35a932..7e725921 100644 --- a/web/src/store/menu.ts +++ b/web/src/store/menu.ts @@ -23,6 +23,7 @@ export interface MenuItem { disposable?: boolean; appSystem?: string | null; subs: MenuItem[] | null; + onClick?: (e?: React.MouseEvent) => void | boolean; } interface MenuState { collapsed: boolean; diff --git a/web/src/store/user.ts b/web/src/store/user.ts index 86615812..4d8fab35 100644 --- a/web/src/store/user.ts +++ b/web/src/store/user.ts @@ -3,6 +3,7 @@ import { clearAuthData } from '@/utils/auth'; import type { User } from '@/views/UserManagement/types' import { getUsers, refreshToken, logout } from '@/api/user' import { getWorkspaceStorageType } from '@/api/workspaces'; +import { cookieUtils } from '@/utils/request' export interface LoginInfo { access_token: string; @@ -21,17 +22,18 @@ export interface UserState { logout: () => void; getStorageType: () => void; } + export const useUser = create((set, get) => ({ user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || '{}') as User : {} as User, loginInfo: {} as LoginInfo, storageType: null, updateLoginInfo: (values: LoginInfo) => { - localStorage.setItem('token', values.access_token); - localStorage.setItem('refresh_token', values.refresh_token); + cookieUtils.set('authToken', values.access_token); + cookieUtils.set('refreshToken', values.refresh_token); set({ loginInfo: values }); }, getUserInfo: async (flag?: boolean) => { - if (!localStorage.getItem('token')) { + if (!cookieUtils.get('authToken')) { return } const localUser = JSON.parse(localStorage.getItem('user') || '{}') as User; @@ -70,7 +72,7 @@ export const useUser = create((set, get) => ({ refreshToken() .then((res) => { const response = res as { refresh_token: string } - localStorage.setItem('token', response.refresh_token); + cookieUtils.set('authToken', response.refresh_token); }) .catch((err) => { console.error('Failed to refresh token:', err) diff --git a/web/src/utils/auth.ts b/web/src/utils/auth.ts index 07aec086..b7c72527 100644 --- a/web/src/utils/auth.ts +++ b/web/src/utils/auth.ts @@ -1,5 +1,7 @@ +import { cookieUtils } from './request' export const clearAuthData = () => { console.log("Clearing auth data and redirecting to login"); sessionStorage.clear(); localStorage.clear() + cookieUtils.clear(); } \ No newline at end of file diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 663be056..647b30f6 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -27,6 +27,7 @@ interface data { const service = axios.create({ baseURL: '/api', // 与vite.config.ts中的代理配置对应 // timeout: 10000, // 请求超时时间 + withCredentials: false, headers: { 'Content-Type': 'application/json' }, @@ -46,11 +47,12 @@ let requests: RequestQueueItem[] = []; service.interceptors.request.use( (config) => { if (!config.headers.Authorization) { - const token = localStorage.getItem('token'); + const token = cookieUtils.get('authToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } } + config.headers.Cookie = undefined return config; }, (error) => { @@ -63,7 +65,7 @@ service.interceptors.request.use( // 刷新token的函数 const tokenRefresh = async (): Promise => { try { - const refresh_token = localStorage.getItem('refresh_token'); + const refresh_token = cookieUtils.get('refreshToken'); if (window.location.hash.includes('#/invite-register')) { throw new Error(i18n.t('common.refreshTokenNotExist')); } @@ -73,7 +75,7 @@ const tokenRefresh = async (): Promise => { // 使用原生axios调用refresh接口,避免触发拦截器导致的循环调用 const response: any = await refreshToken(); const newToken = response.access_token; - localStorage.setItem('token', newToken); + cookieUtils.set('authToken', newToken); return newToken; } catch (error) { // 如果refresh接口也返回401,则退出登录 @@ -274,6 +276,7 @@ export const request = { headers: { 'Content-Type': 'multipart/form-data', }, + withCredentials: false, ...config }); }, @@ -295,4 +298,38 @@ export const request = { }; + +// 获取父级域名 +const getParentDomain = () => { + const hostname = window.location.hostname + const parts = hostname.split('.') + return parts.length > 2 ? `.${parts.slice(-2).join('.')}` : hostname +} + +// Cookie操作工具 +export const cookieUtils = { + set: (name: string, value: string, domain = getParentDomain()) => { + document.cookie = `${name}=${value}; domain=${domain}; path=/; secure; samesite=strict` + }, + get: (name: string) => { + const value = `; ${document.cookie}` + const parts = value.split(`; ${name}=`) + return parts.length === 2 ? parts.pop()?.split(';').shift() : null + }, + remove: (name: string, domain = getParentDomain()) => { + document.cookie = `${name}=; domain=${domain}; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT` + }, + clear: (domain = getParentDomain()) => { + document.cookie.split(';').forEach(cookie => { + const eqPos = cookie.indexOf('='); + const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + if (name) { + document.cookie = `${name}=; domain=${domain}; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`; + document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`; + } + }); + }, +} + + export default service; \ No newline at end of file diff --git a/web/src/utils/stream.ts b/web/src/utils/stream.ts index e5a65c7b..2c827c2f 100644 --- a/web/src/utils/stream.ts +++ b/web/src/utils/stream.ts @@ -1,10 +1,11 @@ import { message } from 'antd'; import i18n from '@/i18n' +import { cookieUtils } from './request' const API_PREFIX = '/api' export const handleSSE = async (url: string, data: any, onMessage?: (data: string) => void, config = {}) => { try { - const token = localStorage.getItem('token'); + const token = cookieUtils.get('authToken'); const response = await fetch(`${API_PREFIX}${url}`, { method: 'POST', headers: { diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index 4a5c9daa..dc600f1f 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -24,7 +24,7 @@ import { saveAgentConfig } from '@/api/application' import Knowledge from './components/Knowledge' import VariableList from './components/VariableList' import { getApplicationConfig } from '@/api/application' -import { getKnowledgeBaseList } from '@/views/KnowledgeBase/service' +import { getKnowledgeBaseList } from '@/api/knowledgeBase' import { memoryConfigListUrl } from '@/api/memory' import CustomSelect from '@/components/CustomSelect' diff --git a/web/src/views/ApplicationConfig/Cluster.tsx b/web/src/views/ApplicationConfig/Cluster.tsx index 29212269..66e8f5a9 100644 --- a/web/src/views/ApplicationConfig/Cluster.tsx +++ b/web/src/views/ApplicationConfig/Cluster.tsx @@ -44,12 +44,22 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { priority: 1, })) } - console.log('params', params) - form.validateFields().then(() => { - saveMultiAgentConfig(id as string, params).then(() => { - if (flag) { - message.success(t('common.saveSuccess')) - } + + return new Promise((resolve, reject) => { + form.validateFields().then(() => { + saveMultiAgentConfig(id as string, params) + .then(() => { + if (flag) { + message.success(t('common.saveSuccess')) + } + resolve(true) + }) + .catch(error => { + reject(error) + }) + }) + .catch(error => { + reject(error) }) }) } diff --git a/web/src/views/ApplicationConfig/components/Chat.tsx b/web/src/views/ApplicationConfig/components/Chat.tsx index ad9cd60b..9a70b5f2 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.svg' +import ChatIcon from '@/assets/images/application/chat.png' import ChatSendIcon from '@/assets/images/application/chatSend.svg' -import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.svg' +import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png' import type { ChatItem, ChatData, Config } from '../types' import { runCompare, draftRun } from '@/api/application' import Empty from '@/components/Empty' @@ -114,6 +114,7 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc if (index === targetIndex) { return { ...item, + conversation_id: parsed.conversation_id, list: item.list?.map((msg, msgIndex) => { if (msgIndex === item.list!.length - 1 && msg.role === 'answer') { return { ...msg, content: msg.content + parsed.content }; @@ -126,10 +127,6 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc })) } } - - if (parsed.conversation_id) { - setConversationId(parsed.conversation_id); - } } else if (line.startsWith('data:') && (isCluster && currentEvent === 'message')) { const jsonData = line.substring(5).trim(); const parsed = JSON.parse(jsonData); @@ -175,10 +172,6 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc })) } } - - if (parsed.conversation_id) { - setConversationId(parsed.conversation_id); - } } else if (line.startsWith('data:') && (isCluster && currentEvent === 'model_end')) { const jsonData = line.substring(5).trim(); const parsed = JSON.parse(jsonData); @@ -247,6 +240,7 @@ 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/ApplicationConfig/components/KnowledgeListModal.tsx b/web/src/views/ApplicationConfig/components/KnowledgeListModal.tsx index 2145c8f4..0c7b47b2 100644 --- a/web/src/views/ApplicationConfig/components/KnowledgeListModal.tsx +++ b/web/src/views/ApplicationConfig/components/KnowledgeListModal.tsx @@ -5,7 +5,7 @@ import clsx from 'clsx' import type { KnowledgeModalRef, KnowledgeBase } from '../types' import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types' import RbModal from '@/components/RbModal' -import { getKnowledgeBaseList } from '@/views/KnowledgeBase/service' +import { getKnowledgeBaseList } from '@/api/knowledgeBase' import SearchInput from '@/components/SearchInput' import Empty from '@/components/Empty' import { formatDateTime } from '@/utils/format'; diff --git a/web/src/views/ForgettingEngine/components/LineChart.tsx b/web/src/views/ForgettingEngine/components/LineChart.tsx index e699813d..c6a0dfc0 100644 --- a/web/src/views/ForgettingEngine/components/LineChart.tsx +++ b/web/src/views/ForgettingEngine/components/LineChart.tsx @@ -1,4 +1,4 @@ -import { type FC, useRef, useEffect, useState } from 'react' +import { type FC, useRef, useEffect, useState, useMemo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import ReactEcharts from 'echarts-for-react'; import type { ConfigForm } from '../types' @@ -30,6 +30,7 @@ const LineChart: FC = ({ config }) => { const { t } = useTranslation() const chartRef = useRef(null); const debounceRef = useRef() + const resizeScheduledRef = useRef(false) const xAxisData = [1, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60] const [initialData, setInitialData] = useState([]) const [currentData, setCurrentData] = useState({ @@ -38,7 +39,7 @@ const LineChart: FC = ({ config }) => { data: [], config: {} }) - const seriesData = [ + const seriesData = useMemo(() => [ { ...SeriesConfig, name: `${t('forgettingEngine.quicklyForget')}(λ_time=0.3)`, @@ -51,56 +52,77 @@ const LineChart: FC = ({ config }) => { data: [], config: {lambda_mem: 1, lambda_time: 0.3, offset: 0.2} } - ] + ], [t]) useEffect(() => { getInitData() }, []) + + useEffect(() => { + const handleResize = () => { + if (chartRef.current && !resizeScheduledRef.current) { + resizeScheduledRef.current = true + requestAnimationFrame(() => { + chartRef.current?.getEchartsInstance().resize(); + resizeScheduledRef.current = false + }); + } + } + + const resizeObserver = new ResizeObserver(handleResize) + const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement + if (chartElement) { + resizeObserver.observe(chartElement) + } + + return () => { + resizeObserver.disconnect() + } + }, [initialData]) useEffect(() => { if (config) { clearTimeout(debounceRef.current) debounceRef.current = setTimeout(() => { getCaculateData(config) - }, 500) + }, 300) } return () => { - console.log('clearTimeout') clearTimeout(debounceRef.current) } }, [config]) // 快速遗忘:lambda_mem=0.3,lambda_time=1,offset=0.05; // 慢速遗忘:lambda_mem=1,lambda_time=0.3,offset=0.2 - const getInitData = () => { + const getInitData = useCallback(() => { const list = seriesData.map(item => ({ ...item, data: formatData(item.config) })) setInitialData(list) - } + }, [seriesData]) - const calculateSeriesData = (days: number, data: ConfigForm) => { - const { offset, lambda_time, lambda_mem } = data; - const S = 1 + const calculateSeriesData = useCallback((days: number, data: ConfigForm) => { + const offset = Number(data.offset) + const lambda_time = Number(data.lambda_time) + const lambda_mem = Number(data.lambda_mem) // R = offset + (1 - offset) × e^(-λtime × t / (λmem × S)) - return (Number(offset) + (1 - Number(offset)) * Math.exp(-Number(lambda_time) * days / (Number(lambda_mem) * S))).toFixed(4) - } - const formatData = (data: ConfigForm) => { + return +(offset + (1 - offset) * Math.exp(-lambda_time * days / lambda_mem)).toFixed(4) + }, []) + const formatData = useCallback((data: ConfigForm) => { return xAxisData.map(days => Number(calculateSeriesData(days, data))) - } + }, [calculateSeriesData]) - const getCaculateData = (data: ConfigForm) => { + const getCaculateData = useCallback((data: ConfigForm) => { if (!data) { return } - console.log('getCaculateData', data) - setCurrentData({ - ...currentData, + setCurrentData(prev => ({ + ...prev, config: data, name: `${t('forgettingEngine.currentConfig')}(λ_time=${data.lambda_time})`, data: xAxisData.map(days => Number(calculateSeriesData(days, data))) - }) - } + })) + }, [t, calculateSeriesData]) return ( <> @@ -175,17 +197,6 @@ const LineChart: FC = ({ config }) => { opts={{ renderer: 'canvas' }} notMerge={true} lazyUpdate={true} - onEvents={{ - // 图表渲染完成后再次调整大小,确保宽度正确 - // 使用 setTimeout 避免在主渲染过程中调用 resize - rendered: () => { - if (chartRef.current) { - setTimeout(() => { - chartRef.current?.getEchartsInstance().resize(); - }, 0); - } - } - }} /> )} diff --git a/web/src/views/ForgettingEngine/types.ts b/web/src/views/ForgettingEngine/types.ts index 0a0bfb45..6313f88d 100644 --- a/web/src/views/ForgettingEngine/types.ts +++ b/web/src/views/ForgettingEngine/types.ts @@ -39,10 +39,12 @@ export interface CurveRecord { } export interface ConfigForm { + config_id?: string; statement_granularity?: string; include_dialogue_context?: boolean; max_context?: string; lambda_time: string | number; lambda_mem: string | number; offset: string | number; + [key: string]: any; } \ No newline at end of file diff --git a/web/src/views/Home/components/LineCard.tsx b/web/src/views/Home/components/LineCard.tsx index f6885d19..e343dd26 100644 --- a/web/src/views/Home/components/LineCard.tsx +++ b/web/src/views/Home/components/LineCard.tsx @@ -156,17 +156,6 @@ const LineCard: FC = ({ chartData, limit, onChange, type, classNa opts={{ renderer: 'canvas' }} notMerge={true} lazyUpdate={true} - onEvents={{ - // 图表渲染完成后再次调整大小,确保宽度正确 - // 使用 setTimeout 避免在主渲染过程中调用 resize - rendered: () => { - if (chartRef.current) { - setTimeout(() => { - chartRef.current?.getEchartsInstance().resize(); - }, 0); - } - } - }} /> ) : } diff --git a/web/src/views/Home/components/PieCard.tsx b/web/src/views/Home/components/PieCard.tsx index 76f78177..5a437268 100644 --- a/web/src/views/Home/components/PieCard.tsx +++ b/web/src/views/Home/components/PieCard.tsx @@ -1,4 +1,4 @@ -import { type FC, useRef } from 'react' +import { type FC, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import ReactEcharts from 'echarts-for-react'; import Card from './Card' @@ -14,6 +14,29 @@ const Colors = ['#155EEF', '#31E8FF', '#AD88FF', '#FFB048', '#4DA8FF', '#03BDFF' const PieCard: FC = ({ chartData, loading }) => { const { t } = useTranslation() const chartRef = useRef(null); + const resizeScheduledRef = useRef(false) + + useEffect(() => { + const handleResize = () => { + if (chartRef.current && !resizeScheduledRef.current) { + resizeScheduledRef.current = true + requestAnimationFrame(() => { + chartRef.current?.getEchartsInstance().resize(); + resizeScheduledRef.current = false + }); + } + } + + const resizeObserver = new ResizeObserver(handleResize) + const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement + if (chartElement) { + resizeObserver.observe(chartElement) + } + + return () => { + resizeObserver.disconnect() + } + }, [chartData]) return ( = ({ chartData, loading }) => { style={{ height: '265px', width: '100%', minWidth: '400px' }} notMerge={true} lazyUpdate={true} - onEvents={{ - // 图表渲染完成后再次调整大小,确保宽度正确 - // 使用 setTimeout 避免在主渲染过程中调用 resize - rendered: () => { - if (chartRef.current) { - setTimeout(() => { - chartRef.current?.getEchartsInstance().resize(); - }, 0); - } - } - }} /> } diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx index 44bd1d2f..92059e7e 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx @@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import Table, { type TableRef } from '@/components/Table' import type { AnyObject } from 'antd/es/_util/type'; -import type { UploadFileResponse,KnowledgeBaseDocumentData } from '../types'; +import type { UploadFileResponse,KnowledgeBaseDocumentData } from '@/views/KnowledgeBase/types'; 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 '../service'; +import { uploadFile, getDocumentList, previewDocumentChunk, 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'; @@ -227,7 +227,7 @@ const CreateDataset = () => { return ( - {value === 1 ? 'Completed' : 'Processing'} + {value === 1 ? t('knowledgeBase.completed') : value === 0 ? t('knowledgeBase.pending') : t('knowledgeBase.processing')} ); } diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx index 207bf673..1994c456 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/DocumentDetails.tsx @@ -10,8 +10,8 @@ import { useEffect, useState, useRef, type FC } from 'react'; import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Button, Spin, message, Switch } from 'antd'; -import { getDocumentDetail, getDocumentChunkList, downloadFile, updateDocument, updateDocumentChunk, createDocumentChunk } from '../service'; -import type { KnowledgeBaseDocumentData, RecallTestData } from '../types'; +import { getDocumentDetail, getDocumentChunkList, downloadFile, updateDocument, updateDocumentChunk, createDocumentChunk } from '@/api/knowledgeBase'; +import type { KnowledgeBaseDocumentData, RecallTestData } from '@/views/KnowledgeBase/types'; import { formatDateTime } from '@/utils/format'; import InfoPanel, { type InfoItem } from '../components/InfoPanel'; import RecallTestResult from '../components/RecallTestResult'; diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx index 571f8ed0..7c43b79f 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx @@ -12,7 +12,7 @@ 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 { getKnowledgeBaseDetail, deleteDocument, downloadFile, updateKnowledgeBase } from '../service'; +import { getKnowledgeBaseDetail, deleteDocument, downloadFile, updateKnowledgeBase } from '@/api/knowledgeBase'; import type { CreateModalRef, KnowledgeBaseListItem, @@ -22,7 +22,7 @@ import type { ShareModalRef, CreateDatasetModalRef,FolderFormData, KnowledgeBaseDocumentData -} from '../types'; +} from '@/views/KnowledgeBase/types'; import RecallTestDrawer from '../components/RecallTestDrawer'; import CreateFolderModal from '../components/CreateFolderModal'; import CreateModal from '../components/CreateModal'; @@ -64,6 +64,7 @@ const Private: FC = () => { const [folderTreeRefreshKey, setFolderTreeRefreshKey] = useState(0); const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); const [folderPath, setFolderPath] = useState>([]); + const [selectedKeys, setSelectedKeys] = useState([]); useEffect(() => { if (knowledgeBaseId) { let url = `/documents/${knowledgeBaseId}/${parentId}/documents`; @@ -143,8 +144,23 @@ const Private: FC = () => { 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) => ({ + ...folderPath.map((folder, index) => ({ id: 0, parent: 0, code: null, @@ -166,6 +182,22 @@ const Private: FC = () => { 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; + }, })), ]; @@ -173,17 +205,18 @@ const Private: FC = () => { }; // 处理树节点选择 - const onSelect = (selectedKeys: React.Key[]) => { - if (!selectedKeys.length) return; + const onSelect = (keys: React.Key[]) => { + if (!keys.length) return; if (!folder) return; const f = { ...folder, - parent_id: String(selectedKeys[0]), + parent_id: String(keys[0]), } - let url = `/documents/${knowledgeBaseId}/${String(selectedKeys[0])}/documents`; + let url = `/documents/${knowledgeBaseId}/${String(keys[0])}/documents`; setTableApi(url); - setParentId(String(selectedKeys[0])) + setParentId(String(keys[0])) setFolder(f) + setSelectedKeys(keys) }; // 处理文件夹路径变化 @@ -511,6 +544,7 @@ const Private: FC = () => { refreshKey={folderTreeRefreshKey} onRootLoad={handleRootTreeLoad} onFolderPathChange={handleFolderPathChange} + selectedKeys={selectedKeys} />
)} diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/Share.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/Share.tsx index 307a370f..8398bb50 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/Share.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/Share.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef, type FC } from 'react'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import type { KnowledgeBaseListItem, RecallTestDrawerRef } from '../types'; +import type { KnowledgeBaseListItem, RecallTestDrawerRef } from '@/views/KnowledgeBase/types'; import RecallTest from '../components/RecallTest'; import InfoPanel, { type InfoItem } from '../components/InfoPanel'; import shareUserIcon from '@/assets/images/knowledgeBase/share-user.png'; @@ -13,8 +13,9 @@ import kbSizeIcon from '@/assets/images/knowledgeBase/kb-size.png'; import kbModelIcon from '@/assets/images/knowledgeBase/kb-model.png'; import kbHistoryIcon from '@/assets/images/knowledgeBase/kb-history.png'; -import { getKnowledgeBaseDetail } from '../service'; +import { getKnowledgeBaseDetail } from '@/api/knowledgeBase'; import { formatDateTime } from '@/utils/format'; +import { useMenu } from '@/store/menu'; const Share: FC = () => { const { t } = useTranslation(); @@ -24,6 +25,7 @@ const Share: FC = () => { const [knowledgeBase, setKnowledgeBase] = useState(null); const recallTestRef = useRef(null); const [infoItems, setInfoItems] = useState([]); + const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); useEffect(() => { console.log('Share.tsx - useParams result:', params); console.log('Share.tsx - knowledgeBaseId:', knowledgeBaseId); @@ -40,6 +42,13 @@ const Share: FC = () => { console.warn('Share.tsx - knowledgeBaseId is undefined or empty'); } }, [knowledgeBaseId]); + + // 更新面包屑 + useEffect(() => { + if (knowledgeBase) { + updateBreadcrumbs(); + } + }, [knowledgeBase]); const formatInfoItems = (data: KnowledgeBaseListItem): InfoItem[] => { const items: InfoItem[] = [ { @@ -103,6 +112,47 @@ 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/CreateDatasetModal.tsx b/web/src/views/KnowledgeBase/components/CreateDatasetModal.tsx index 9b2b080d..b362d6ae 100644 --- a/web/src/views/KnowledgeBase/components/CreateDatasetModal.tsx +++ b/web/src/views/KnowledgeBase/components/CreateDatasetModal.tsx @@ -11,7 +11,7 @@ import type { RadioChangeEvent } from 'antd'; import { Flex, Radio } from 'antd'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import type { CreateDatasetModalRef, CreateDatasetModalRefProps} from '../types'; +import type { CreateDatasetModalRef, CreateDatasetModalRefProps} from '@/views/KnowledgeBase/types'; import RbModal from '@/components/RbModal' const style: React.CSSProperties = { display: 'flex', diff --git a/web/src/views/KnowledgeBase/components/CreateFolderModal.tsx b/web/src/views/KnowledgeBase/components/CreateFolderModal.tsx index 81520e94..906c1f77 100644 --- a/web/src/views/KnowledgeBase/components/CreateFolderModal.tsx +++ b/web/src/views/KnowledgeBase/components/CreateFolderModal.tsx @@ -1,9 +1,9 @@ import { forwardRef, useImperativeHandle, useState } from 'react'; import { Form, Input } from 'antd'; import { useTranslation } from 'react-i18next'; -import type { FolderFormData, KnowledgeBaseFormData, CreateFolderModalRef, CreateFolderModalRefProps } from '../types'; +import type { FolderFormData, KnowledgeBaseFormData, CreateFolderModalRef, CreateFolderModalRefProps } from '@/views/KnowledgeBase/types'; import RbModal from '@/components/RbModal' -import { createFolder, updateKnowledgeBase } from '../service'; +import { createFolder, updateKnowledgeBase } from '@/api/knowledgeBase'; const CreateFolderModal = forwardRef(({ refreshTable }, ref) => { diff --git a/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx b/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx index a12ee394..4f40da7f 100644 --- a/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx +++ b/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx @@ -2,11 +2,11 @@ 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 '../types'; +import type { CreateImageModalRef, CreateImageMoealRefProps,UploadFileResponse } from '@/views/KnowledgeBase/types'; import type { UploadRequestOption } from 'rc-upload/lib/interface'; import RbModal from '@/components/RbModal'; import UploadFiles from '@/components/Upload/UploadFiles'; -import { uploadFile } from '../service'; +import { uploadFile } from '@/api/knowledgeBase'; interface ImageDatasetFormData { name: string; diff --git a/web/src/views/KnowledgeBase/components/CreateModal.tsx b/web/src/views/KnowledgeBase/components/CreateModal.tsx index 3e882696..0ce25430 100644 --- a/web/src/views/KnowledgeBase/components/CreateModal.tsx +++ b/web/src/views/KnowledgeBase/components/CreateModal.tsx @@ -1,8 +1,8 @@ import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'; import { Form, Input, Select, Modal } from 'antd'; import { useTranslation } from 'react-i18next'; -import type { KnowledgeBaseListItem, KnowledgeBaseFormData, CreateModalRef, CreateModalRefProps } from '../types'; -import { getModelTypeList, getModelList, createKnowledgeBase, updateKnowledgeBase } from '../service' +import type { KnowledgeBaseListItem, KnowledgeBaseFormData, CreateModalRef, CreateModalRefProps } from '@/views/KnowledgeBase/types'; +import { getModelTypeList, getModelList, createKnowledgeBase, updateKnowledgeBase } from '@/api/knowledgeBase' import RbModal from '@/components/RbModal' const { TextArea } = Input; const { confirm } = Modal @@ -15,7 +15,7 @@ const CreateModal = forwardRef(({ const [modelTypeList, setModelTypeList] = useState([]); const [modelOptionsByType, setModelOptionsByType] = useState>({}); const [datasets, setDatasets] = useState(null); - const [currentType, setCurrentType] = useState('General'); // 保存当前 type + const [currentType, setCurrentType] = useState<'General' | 'Web' | 'Third-party' | 'Folder'>('General'); const [form] = Form.useForm(); const [loading, setLoading] = useState(false) @@ -102,7 +102,7 @@ const CreateModal = forwardRef(({ const handleOpen = (record?: KnowledgeBaseListItem | null, type?: string) => { setDatasets(record || null); const nextType = type || currentType; - setCurrentType(nextType); + setCurrentType(nextType as any); setBaseFields(record || null, nextType); getTypeList(record || null); setVisible(true); diff --git a/web/src/views/KnowledgeBase/components/FolderTree.tsx b/web/src/views/KnowledgeBase/components/FolderTree.tsx index dad92411..4a5a288c 100644 --- a/web/src/views/KnowledgeBase/components/FolderTree.tsx +++ b/web/src/views/KnowledgeBase/components/FolderTree.tsx @@ -8,7 +8,7 @@ import textIcon from '@/assets/images/knowledgeBase/text.png'; import imageIcon from '@/assets/images/knowledgeBase/image.png'; import datasetsIcon from '@/assets/images/knowledgeBase/datasets.png'; import switcherIcon from '@/assets/images/knowledgeBase/switcher.png'; -import { getFolderList } from '../service'; +import { getFolderList } from '@/api/knowledgeBase'; const { DirectoryTree } = Tree; @@ -59,6 +59,7 @@ interface FolderTreeProps { refreshKey?: number; onRootLoad?: (nodes: TreeNodeData[] | null) => void; onFolderPathChange?: (path: Array<{ id: string; name: string }>) => void; + selectedKeys?: React.Key[]; } const renderIcon = (icon?: string) => { @@ -273,6 +274,7 @@ const FolderTree: FC = ({ refreshKey = 0, onRootLoad, onFolderPathChange, + selectedKeys, }) => { const [treeData, setTreeData] = useState([]); @@ -396,6 +398,7 @@ const FolderTree: FC = ({ onExpand={onExpand} loadData={onLoadData} treeData={treeNodes} + selectedKeys={selectedKeys} /> ); }; diff --git a/web/src/views/KnowledgeBase/components/RecallTest.tsx b/web/src/views/KnowledgeBase/components/RecallTest.tsx index ddfe7386..81332a11 100644 --- a/web/src/views/KnowledgeBase/components/RecallTest.tsx +++ b/web/src/views/KnowledgeBase/components/RecallTest.tsx @@ -2,10 +2,10 @@ import { forwardRef, useImperativeHandle, useState, useEffect } from 'react'; import { Form, Input, Select, Button, InputNumber } from 'antd'; import { useTranslation } from 'react-i18next'; -import type { RecallTestDrawerRef, RecallTestData, RecallTestParams } from '../types'; +import type { RecallTestDrawerRef, RecallTestData, RecallTestParams } from '@/views/KnowledgeBase/types'; // import refreshIcon from '@/assets/images/knowledgeBase/refresh-blue.png'; import RecallTestResult from './RecallTestResult'; -import { reChunks, getRetrievalModeType } from '../service'; +import { reChunks, getRetrievalModeType } from '@/api/knowledgeBase'; import { hybrid } from 'react-syntax-highlighter/dist/esm/styles/hljs'; const { TextArea } = Input; diff --git a/web/src/views/KnowledgeBase/components/RecallTestDrawer.tsx b/web/src/views/KnowledgeBase/components/RecallTestDrawer.tsx index 2ce28251..3ad82b9b 100644 --- a/web/src/views/KnowledgeBase/components/RecallTestDrawer.tsx +++ b/web/src/views/KnowledgeBase/components/RecallTestDrawer.tsx @@ -2,7 +2,7 @@ import { forwardRef, useImperativeHandle, useState, useRef, useLayoutEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import RbDrawer from '@/components/RbDrawer'; -import type { RecallTestDrawerRef } from '../types'; +import type { RecallTestDrawerRef } from '@/views/KnowledgeBase/types'; import RecallTest from './RecallTest'; const RecallTestDrawer = forwardRef(({},ref) => { diff --git a/web/src/views/KnowledgeBase/components/RecallTestResult.tsx b/web/src/views/KnowledgeBase/components/RecallTestResult.tsx index 9cb0a910..1475d790 100644 --- a/web/src/views/KnowledgeBase/components/RecallTestResult.tsx +++ b/web/src/views/KnowledgeBase/components/RecallTestResult.tsx @@ -9,7 +9,7 @@ import { FileOutlined, FieldTimeOutlined, EditOutlined } from '@ant-design/icons'; import { Skeleton } from 'antd'; import { useTranslation } from 'react-i18next'; -import type { RecallTestData } from '../types'; +import type { RecallTestData } from '@/views/KnowledgeBase/types'; import { NoData } from './noData'; import { formatDateTime } from '@/utils/format'; import InfiniteScroll from 'react-infinite-scroll-component'; diff --git a/web/src/views/KnowledgeBase/components/ShareModal.tsx b/web/src/views/KnowledgeBase/components/ShareModal.tsx index 4a769f5d..f37d669b 100644 --- a/web/src/views/KnowledgeBase/components/ShareModal.tsx +++ b/web/src/views/KnowledgeBase/components/ShareModal.tsx @@ -10,14 +10,14 @@ import { forwardRef, useImperativeHandle, useState, useRef } from 'react'; import { Switch } from 'antd'; import { useTranslation } from 'react-i18next'; import { message } from 'antd'; -import type { ShareModalRef, ShareModalRefProps, KnowledgeBase} from '../types'; +import type { ShareModalRef, ShareModalRefProps, KnowledgeBase} from '@/views/KnowledgeBase/types'; import RbModal from '@/components/RbModal' // import betchControlIcon from '@/assets/images/knowledgeBase/betch-control.png'; import kbIcon from '@/assets/images/knowledgeBase/knowledge-management.png'; // import robotIcon from '@/assets/images/knowledgeBase/robot.png'; -import { updateKnowledgeBase, getWorkspaceAuthorizationList } from '../service'; +import { updateKnowledgeBase, getWorkspaceAuthorizationList } from '@/api/knowledgeBase'; import { NoData } from './noData'; -import type { ListQuery, ShareSpaceModalRef } from '../types'; +import type { ListQuery, ShareSpaceModalRef } from '@/views/KnowledgeBase/types'; import { formatDateTime } from '@/utils/format'; import ShareSpaceModal from './ShareSpaceModal' const ShareModal = forwardRef(({ handleShare: onShare }, ref) => { diff --git a/web/src/views/KnowledgeBase/components/ShareSpaceModal.tsx b/web/src/views/KnowledgeBase/components/ShareSpaceModal.tsx index 899f6ec4..60cfd81d 100644 --- a/web/src/views/KnowledgeBase/components/ShareSpaceModal.tsx +++ b/web/src/views/KnowledgeBase/components/ShareSpaceModal.tsx @@ -10,14 +10,14 @@ import { forwardRef, useImperativeHandle, useState } from 'react'; import { Switch } from 'antd'; import { useTranslation } from 'react-i18next'; import { message } from 'antd'; -import type { ShareModalRef, ShareModalRefProps, KnowledgeBase} from '../types'; +import type { ShareModalRef, ShareModalRefProps, KnowledgeBase} from '@/views/KnowledgeBase/types'; import RbModal from '@/components/RbModal' // import betchControlIcon from '@/assets/images/knowledgeBase/betch-control.png'; import kbIcon from '@/assets/images/knowledgeBase/knowledge-management.png'; // import robotIcon from '@/assets/images/knowledgeBase/robot.png'; -import { getSpaceList, shareKnowledgeBase } from '../service'; +import { getSpaceList, shareKnowledgeBase } from '@/api/knowledgeBase'; import { NoData } from './noData'; -import type { SpaceItem } from '../types'; +import type { SpaceItem } from '@/views/KnowledgeBase/types'; import { formatDateTime } from '@/utils/format'; const ShareModal = forwardRef(({ handleShare: onShare }, ref) => { const { t } = useTranslation(); diff --git a/web/src/views/KnowledgeBase/datasets.tsx b/web/src/views/KnowledgeBase/datasets.tsx index b03e2da5..e5e6d79e 100644 --- a/web/src/views/KnowledgeBase/datasets.tsx +++ b/web/src/views/KnowledgeBase/datasets.tsx @@ -5,7 +5,7 @@ import { Button } from 'antd'; import { ArrowLeftOutlined } from '@ant-design/icons'; import { request } from '@/utils/request'; -import type { KnowledgeBase } from './types'; +import type { KnowledgeBase } from '@/views/KnowledgeBase/types'; const Datasets: FC = () => { const { t } = useTranslation(); diff --git a/web/src/views/KnowledgeBase/index.tsx b/web/src/views/KnowledgeBase/index.tsx index d17bcad9..f8f10c0e 100644 --- a/web/src/views/KnowledgeBase/index.tsx +++ b/web/src/views/KnowledgeBase/index.tsx @@ -10,14 +10,16 @@ import folderIcon from '@/assets/images/knowledgeBase/folder.png'; import generalIcon from '@/assets/images/knowledgeBase/datasets.png'; import webIcon from '@/assets/images/knowledgeBase/general.png'; import tpIcon from '@/assets/images/knowledgeBase/text.png'; -import type { KnowledgeBaseListItem, CreateModalRef, KnowledgeBaseListResponse, ListQuery } from './types' +import type { KnowledgeBaseListItem, CreateModalRef, KnowledgeBaseListResponse, ListQuery } from '@/views/KnowledgeBase/types' import CreateModal from './components/CreateModal' import RbCard from '@/components/RbCard' import SearchInput from '@/components/SearchInput' import Empty from '@/components/Empty' -import { getKnowledgeBaseList, getModelList, getModelTypeList, deleteKnowledgeBase, getKnowledgeBaseTypeList } from './service' +import { getKnowledgeBaseList, getModelList, getModelTypeList, deleteKnowledgeBase, getKnowledgeBaseTypeList } from '@/api/knowledgeBase' const { confirm } = Modal; import InfiniteScroll from 'react-infinite-scroll-component'; +import { useMenu } from '@/store/menu'; + type ModelMenuInfo = { menu: NonNullable; summary: string[]; @@ -41,6 +43,10 @@ const KnowledgeBaseManagement: FC = () => { const modalRef = useRef(null) const [messageApi, contextHolder] = message.useMessage(); + // 使用 menu store 管理面包屑 + const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); + const [folderPath, setFolderPath] = useState>([]); + // 生成下拉菜单项(根据当前 item) const getOptMenuItems = (item: KnowledgeBaseListItem): MenuProps['items'] => { @@ -105,7 +111,17 @@ const KnowledgeBaseManagement: FC = () => { // 处理创建 const handleCreate = (type?: string) => { - modalRef?.current?.handleOpen(null, type) + // 如果在文件夹内,使用 folderPath 的最后一项作为 parent_id + // 这样更可靠,因为 folderPath 是直接管理的状态 + const currentParentId = folderPath.length > 0 + ? folderPath[folderPath.length - 1].id + : query.parent_id; // 降级使用 query.parent_id + + const record = currentParentId ? { + parent_id: currentParentId as string, + } as KnowledgeBaseListItem : null; + + modalRef?.current?.handleOpen(record, type) } // 动态生成 createItems @@ -118,7 +134,7 @@ const KnowledgeBaseManagement: FC = () => { handleCreate(type); }, })); - }, [knowledgeBaseTypes, t]); + }, [knowledgeBaseTypes, t, folderPath, query]); const typeToFieldKey = (type: string) => { const normalized = (type || '').toLowerCase(); switch (normalized) { @@ -176,7 +192,7 @@ const KnowledgeBaseManagement: FC = () => { const fetchKnowledgeBaseTypes = async () => { try { let types = await getKnowledgeBaseTypeList(); - types = types.filter(type => (type === 'General' )); //|| type === 'Folder' + types = types.filter(type => (type === 'General' || type === 'Folder' )); // //暂时未实现 ,过滤掉未实现 setKnowledgeBaseTypes(types); } catch (error) { @@ -337,6 +353,25 @@ const KnowledgeBaseManagement: FC = () => { }; // 处理跳转详情 const handleToDetail = (knowledgeBase: KnowledgeBaseListItem) => { + // 如果是 Folder 类型,刷新当前页面,显示该文件夹下的知识库列表 + if (knowledgeBase.type === 'Folder' || knowledgeBase.type === 'folder') { + // 添加到文件夹路径 + const newFolderPath = [ + ...folderPath, + { + id: knowledgeBase.id, + name: knowledgeBase.name, + }, + ]; + setFolderPath(newFolderPath); + + setQuery((prev) => ({ + ...prev, + parent_id: knowledgeBase.id, + })); + return; + } + // 根据权限类型跳转到不同的详情页 if (knowledgeBase.permission_id === 'Private' || knowledgeBase.permission_id === 'private') { navigate(`/knowledge-base/${knowledgeBase.id}/private`) @@ -344,6 +379,83 @@ const KnowledgeBaseManagement: FC = () => { navigate(`/knowledge-base/${knowledgeBase.id}/share`) } } + // 更新面包屑的函数 + 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]); + useEffect(() => { fetchModelTypes(); fetchKnowledgeBaseTypes(); diff --git a/web/src/views/KnowledgeBase/types.ts b/web/src/views/KnowledgeBase/types.ts index 1d235e22..04c66ac0 100644 --- a/web/src/views/KnowledgeBase/types.ts +++ b/web/src/views/KnowledgeBase/types.ts @@ -359,4 +359,4 @@ export interface ShareSpaceModalRef{ export interface ShareSpaceModalRefProps { handleShare?: () => void; -} \ No newline at end of file +} diff --git a/web/src/views/MemoryConversation/index.tsx b/web/src/views/MemoryConversation/index.tsx index 43c81cc6..c92044cc 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.svg' +import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.png' import Card from './components/Card' import Chat from './components/Chat' import { readService, getUserMemoryList } from '@/api/memory' @@ -139,6 +139,9 @@ const MemoryConversation: FC = () => { : {logs.map((log, logIndex) => ( diff --git a/web/src/views/NoPermission/index.tsx b/web/src/views/NoPermission/index.tsx index a14e505c..ac547fd6 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.svg'; +import noPermission from '@/assets/images/empty/noPermission.png'; import Empty from '@/components/Empty'; const NoPermission = () => { diff --git a/web/src/views/NotFound/index.tsx b/web/src/views/NotFound/index.tsx index 4a7fbbe6..6d879167 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.svg'; +import notFoundImg from '@/assets/images/empty/404.png'; import Empty from '@/components/Empty'; const NotFound = () => { diff --git a/web/src/views/SpaceManagement/components/SpaceModal.tsx b/web/src/views/SpaceManagement/components/SpaceModal.tsx index 7b4e5115..8596cceb 100644 --- a/web/src/views/SpaceManagement/components/SpaceModal.tsx +++ b/web/src/views/SpaceManagement/components/SpaceModal.tsx @@ -1,13 +1,14 @@ -import { forwardRef, useImperativeHandle, useState } from 'react'; -import { Form, Input, App } from 'antd'; +import { forwardRef, useImperativeHandle, useState, useEffect } from 'react'; +import { Form, Input, App, Select } from 'antd'; import { useTranslation } from 'react-i18next'; import type { SpaceModalData, SpaceModalRef, Space } from '../types' import RbModal from '@/components/RbModal' import { createWorkspace } from '@/api/workspaces' import RadioGroupCard from '@/components/RadioGroupCard' -import { getModelListUrl } from '@/api/models' +import { getModelListUrl, getModelList } from '@/api/models' import CustomSelect from '@/components/CustomSelect' +import type { Model } from '@/views/ModelManagement/types' const FormItem = Form.Item; @@ -28,6 +29,7 @@ const SpaceModal = forwardRef(({ const [form] = Form.useForm(); const [loading, setLoading] = useState(false) const [editVo, setEditVo] = useState(null) + const [modelList, setModelList] = useState([]) const values = Form.useWatch([], form); @@ -73,6 +75,21 @@ const SpaceModal = forwardRef(({ }); } + useEffect(() => { + getModels() + }, []) + + const getModels = () => { + const requests = [getModelList({ type: 'llm', pagesize: 100, page: 1 }), getModelList({ type: 'chat', pagesize: 100, page: 1 })] + Promise.all(requests) + .then(responses => { + const [chatRes, modelRes] = responses as { items: Model[] }[] + const chatList = chatRes.items || [] + const modelList = modelRes.items || [] + setModelList([...chatList, ...modelList]) + }) + } + // 暴露给父组件的方法 useImperativeHandle(ref, () => ({ handleOpen, @@ -104,13 +121,13 @@ const SpaceModal = forwardRef(({ name="llm" rules={[{ required: true, message: t('common.pleaseSelect') }]} > - { const { id } = useParams() const chartRef = useRef(null); + const resizeScheduledRef = useRef(false) const [loading, setLoading] = useState(false) const [data, setData] = useState>>([]) @@ -30,6 +31,29 @@ const PieCard: FC = () => { }) } + + useEffect(() => { + const handleResize = () => { + if (chartRef.current && !resizeScheduledRef.current) { + resizeScheduledRef.current = true + requestAnimationFrame(() => { + chartRef.current?.getEchartsInstance().resize(); + resizeScheduledRef.current = false + }); + } + } + + const resizeObserver = new ResizeObserver(handleResize) + const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement + if (chartElement) { + resizeObserver.observe(chartElement) + } + + return () => { + resizeObserver.disconnect() + } + }, [data]) + return ( <> {loading @@ -108,17 +132,6 @@ const PieCard: FC = () => { style={{ height: '340px', width: '100%' }} notMerge={true} lazyUpdate={true} - onEvents={{ - // 图表渲染完成后再次调整大小,确保宽度正确 - // 使用 setTimeout 避免在主渲染过程中调用 resize - rendered: () => { - if (chartRef.current) { - setTimeout(() => { - chartRef.current?.getEchartsInstance().resize(); - }, 0); - } - } - }} /> } diff --git a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx index bec0e851..93412274 100644 --- a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx +++ b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx @@ -23,6 +23,7 @@ const RelationshipNetwork:FC = () => { const { t } = useTranslation() const { id } = useParams() const chartRef = useRef(null) + const resizeScheduledRef = useRef(false) const [nodes, setNodes] = useState([]) const [links, setLinks] = useState([]) const [categories, setCategories] = useState<{ name: string }[]>([]) @@ -83,6 +84,28 @@ const RelationshipNetwork:FC = () => { setNodes(uniqueNodes) }) } + + useEffect(() => { + const handleResize = () => { + if (chartRef.current && !resizeScheduledRef.current) { + resizeScheduledRef.current = true + requestAnimationFrame(() => { + chartRef.current?.getEchartsInstance().resize(); + resizeScheduledRef.current = false + }); + } + } + + const resizeObserver = new ResizeObserver(handleResize) + const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement + if (chartElement) { + resizeObserver.observe(chartElement) + } + + return () => { + resizeObserver.disconnect() + } + }, [nodes]) return ( <> {/* 关系网络 */} @@ -147,15 +170,6 @@ const RelationshipNetwork:FC = () => { notMerge={false} lazyUpdate={true} onEvents={{ - // 图表渲染完成后再次调整大小,确保宽度正确 - // 使用 setTimeout 避免在主渲染过程中调用 resize - rendered: () => { - if (chartRef.current) { - setTimeout(() => { - chartRef.current?.getEchartsInstance().resize(); - }, 0); - } - }, // 节点点击事件处理 click: (params: { dataType: string; data: Node }) => { if (params.dataType === 'node') { diff --git a/web/vite.config.ts b/web/vite.config.ts index 08054ec9..7181389f 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ proxy: { // 主要API代理,支持 /api 和 /api/* 格式 '/api': { - target: 'http://127.0.0.1:8000', // 后端服务地址 + target: 'http://0.0.0.0:5173', // 后端服务地址 changeOrigin: true, // 匹配所有以/api开头的请求,包括/api/token