-
{userInfo?.caption}
-
{userInfo?.email}
-
加入时间:{formatDate(userInfo?.created_at)}
-
-
-
-
知识库管理
-
+ {/* 左侧列:用户卡片和知识库入口 */}
+
+
+
+
+
+
+
{userInfo?.caption}
+
{userInfo?.email}
+
加入时间:{formatDate(userInfo?.created_at)}
+
+ {/* 知识库入口卡片 */}
+
+
+
+
+
+
+
+
+
{/* 统一的统计卡片 */}
@@ -301,7 +312,6 @@ const Dashboard = ({ user, onLogout }) => {
onTagClick={handleTagClick}
selectedTags={selectedTags}
/>
-
diff --git a/src/pages/EditKnowledgeBase.css b/src/pages/EditKnowledgeBase.css
new file mode 100644
index 0000000..bc56191
--- /dev/null
+++ b/src/pages/EditKnowledgeBase.css
@@ -0,0 +1,259 @@
+/* Edit Knowledge Base Page */
+.edit-kb-page {
+ background-color: #f8fafc;
+ min-height: 100vh;
+ padding: 2rem;
+}
+
+.edit-header {
+ max-width: 800px;
+ margin: 0 auto 1.5rem auto;
+}
+
+.back-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #475569;
+ text-decoration: none;
+ font-weight: 500;
+ transition: color 0.3s ease;
+}
+
+.back-link:hover {
+ color: #667eea;
+}
+
+.edit-content {
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.edit-card {
+ background: white;
+ border-radius: 16px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+}
+
+.edit-card-header {
+ padding: 2rem;
+ background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
+ border-bottom: 1px solid #e2e8f0;
+ text-align: center;
+}
+
+.edit-card-header h1 {
+ font-size: 2rem;
+ font-weight: 700;
+ color: #1e293b;
+ margin: 0 0 0.5rem 0;
+}
+
+.edit-card-header p {
+ color: #64748b;
+ margin: 0;
+ font-size: 1rem;
+}
+
+.edit-form {
+ padding: 2rem;
+}
+
+.form-group {
+ margin-bottom: 2rem;
+}
+
+.form-group label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+ color: #334155;
+ margin-bottom: 0.75rem;
+ font-size: 0.95rem;
+}
+
+.form-group input[type="text"] {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: 2px solid #e2e8f0;
+ border-radius: 8px;
+ font-size: 1rem;
+ transition: all 0.3s ease;
+ box-sizing: border-box;
+}
+
+.form-group input:focus {
+ outline: none;
+ border-color: #667eea;
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+/* Content Section */
+.content-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0.25rem;
+}
+
+.markdown-hint {
+ margin-top: 0.5rem;
+ color: #64748b;
+}
+
+/* Error Message */
+.error-message {
+ background: #fef2f2;
+ color: #ef4444;
+ padding: 0.75rem 1rem;
+ border-radius: 6px;
+ border: 1px solid #fecaca;
+ font-size: 0.9rem;
+ margin-bottom: 1rem;
+}
+
+/* Loading State */
+.loading-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 50vh;
+ color: #64748b;
+}
+
+.loading-spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid #e2e8f0;
+ border-top: 3px solid #667eea;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: 1rem;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Form Actions */
+.form-actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+ padding-top: 2rem;
+ border-top: 1px solid #e2e8f0;
+}
+
+.btn-cancel {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.75rem 1.5rem;
+ background: #f1f5f9;
+ color: #475569;
+ text-decoration: none;
+ border-radius: 8px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.btn-cancel:hover {
+ background: #e2e8f0;
+}
+
+.btn-submit {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 2rem;
+ background: linear-gradient(135deg, #10b981, #059669);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+}
+
+.btn-submit:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
+}
+
+.btn-submit:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* Markdown Editor */
+.markdown-editor-container {
+ margin-top: 0.5rem;
+}
+
+.markdown-editor-container .w-md-editor {
+ background-color: white;
+}
+
+.markdown-editor-container .w-md-editor-text-input,
+.markdown-editor-container .w-md-editor-text-textarea,
+.markdown-editor-container .w-md-editor-text {
+ font-size: 0.9rem !important;
+ line-height: 1.6 !important;
+ caret-color: #667eea !important;
+}
+
+.markdown-editor-container .w-md-editor-text-input {
+ resize: none !important;
+}
+
+.markdown-editor-container .w-md-editor-text-textarea {
+ resize: none !important;
+ outline: none !important;
+ border: none !important;
+ box-shadow: none !important;
+}
+
+.markdown-editor-container .w-md-editor-toolbar {
+ background-color: #f8fafc;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.markdown-editor-container .w-md-editor-toolbar button {
+ color: #64748b;
+ text-align: left;
+}
+
+.markdown-editor-container .w-md-editor-toolbar button:hover {
+ background-color: #e2e8f0;
+ color: #334155;
+ text-align: left;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .edit-kb-page {
+ padding: 1rem;
+ }
+
+ .edit-card-header {
+ padding: 1.5rem;
+ }
+
+ .edit-form {
+ padding: 1.5rem;
+ }
+
+ .form-actions {
+ flex-direction: column;
+ }
+
+ .content-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+ }
+}
diff --git a/src/pages/EditKnowledgeBase.jsx b/src/pages/EditKnowledgeBase.jsx
new file mode 100644
index 0000000..2fa506c
--- /dev/null
+++ b/src/pages/EditKnowledgeBase.jsx
@@ -0,0 +1,251 @@
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import { Link, useNavigate, useParams } from 'react-router-dom';
+import apiClient from '../utils/apiClient';
+import { ArrowLeft, FileText, Tag, Save } from 'lucide-react';
+import MDEditor, * as commands from '@uiw/react-md-editor';
+import '@uiw/react-md-editor/markdown-editor.css';
+import { buildApiUrl, API_ENDPOINTS } from '../config/api';
+import TagEditor from '../components/TagEditor';
+import './EditKnowledgeBase.css';
+
+const EditKnowledgeBase = ({ user }) => {
+ const navigate = useNavigate();
+ const { kb_id } = useParams();
+ const [formData, setFormData] = useState({
+ title: '',
+ content: '',
+ tags: ''
+ });
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+ const [error, setError] = useState('');
+ const [kb, setKb] = useState(null);
+
+ const handleContentChange = useCallback((value) => {
+ setFormData(prev => ({ ...prev, content: value || '' }));
+ }, []);
+
+ useEffect(() => {
+ fetchKbData();
+ }, [kb_id]);
+
+ const fetchKbData = async () => {
+ try {
+ const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kb_id)));
+ const kbData = response.data;
+
+ // Check if current user is the creator
+ if (kbData.creator_id !== user.user_id) {
+ navigate('/knowledge-base');
+ return;
+ }
+
+ setKb(kbData);
+ setFormData({
+ title: kbData.title,
+ content: kbData.content || '',
+ tags: kbData.tags ? kbData.tags.map(t => t.name).join(', ') : ''
+ });
+ } catch (err) {
+ setError('无法加载知识库信息');
+ console.error('Error fetching knowledge base:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!formData.title.trim()) {
+ setError('请输入知识库标题');
+ return;
+ }
+
+ setIsSaving(true);
+ setError('');
+
+ try {
+ const updateData = {
+ title: formData.title,
+ content: formData.content,
+ tags: formData.tags
+ };
+
+ await apiClient.put(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.UPDATE(kb_id)), updateData);
+ navigate('/knowledge-base');
+ } catch (err) {
+ setError(err.response?.data?.message || '更新知识库失败,请重试');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ // 自定义工具栏命令配置
+ const customCommands = [
+ commands.bold,
+ commands.italic,
+ commands.strikethrough,
+ commands.hr,
+ commands.group([
+ commands.title1,
+ commands.title2,
+ commands.title3,
+ commands.title4,
+ commands.title5,
+ commands.title6,
+ ], {
+ name: 'title',
+ groupName: 'title',
+ buttonProps: { 'aria-label': '插入标题', title: '插入标题' }
+ }),
+ commands.divider,
+ commands.link,
+ commands.quote,
+ commands.code,
+ commands.codeBlock,
+ commands.image,
+ commands.divider,
+ commands.unorderedListCommand,
+ commands.orderedListCommand,
+ commands.checkedListCommand,
+ ];
+
+ // 右侧额外命令(预览、全屏等)
+ const customExtraCommands = [
+ commands.codeEdit,
+ commands.codeLive,
+ commands.codePreview,
+ commands.divider,
+ commands.fullscreen,
+ ];
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ 编辑知识库
+ 修改知识库标题、标签和内容摘要
+
+
+
+
+
+
+ );
+};
+
+export default EditKnowledgeBase;
diff --git a/src/pages/KnowledgeBasePage.css b/src/pages/KnowledgeBasePage.css
index 6bf904d..f02f2f0 100644
--- a/src/pages/KnowledgeBasePage.css
+++ b/src/pages/KnowledgeBasePage.css
@@ -1,18 +1,20 @@
-
+/* 整体页面布局 */
.kb-management-page {
min-height: 100vh;
background: #f8fafc;
+ display: flex;
+ flex-direction: column;
}
+/* 顶部Header */
.kb-header {
background: white;
border-bottom: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- margin-bottom: 0;
}
.kb-header .header-content {
- max-width: 1200px;
+ max-width: 1400px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
@@ -41,101 +43,53 @@
font-weight: 600;
}
-.kb-content {
- max-width: 1200px;
+/* 主布局区 - 左右分栏 */
+.kb-layout {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+ max-width: 1400px;
margin: 0 auto;
- padding: 2rem;
+ width: 100%;
+}
+
+/* 左侧边栏 */
+.kb-sidebar {
+ width: 320px;
+ background: white;
+ border-right: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
- gap: 2rem;
-}
-
-.kb-wrapper {
- background: white;
- border-radius: 16px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
- overflow: hidden;
-}
-
-.kb-tabs .ant-tabs-nav {
- padding: 0 2rem;
- margin-bottom: 0 !important;
- border-bottom: 1px solid #e2e8f0;
-}
-
-.kb-tabs .ant-tabs-tab {
- font-size: 1rem;
- color: #475569;
- padding: 16px 4px;
- margin: 0 16px;
-}
-
-.kb-tabs .ant-tabs-tab .ant-tabs-tab-btn {
- display: flex;
- align-items: center;
- gap: 8px;
- font-weight: 500;
-}
-
-.kb-tabs .ant-tabs-tab-active .ant-tabs-tab-btn {
- color: #667eea;
-}
-
-.kb-tabs .ant-tabs-ink-bar {
- background: #667eea;
- height: 3px;
-}
-
-.kb-tabs .ant-tabs-content-holder {
- padding: 2rem;
- min-height: 60vh;
-}
-
-/* Knowledge Base List Styles */
-.kb-list {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
- gap: 1.5rem;
- margin-bottom: 2rem;
-}
-
-.kb-card {
- background: white;
- border: 1px solid #e2e8f0;
- border-radius: 12px;
- padding: 1.5rem;
transition: all 0.3s ease;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
-.kb-card:hover {
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- transform: translateY(-2px);
+.kb-sidebar.collapsed {
+ width: 60px;
}
-.kb-card-header {
+.sidebar-header {
+ padding: 1.5rem 1rem;
+ border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 1rem;
+ align-items: center;
}
-.kb-title {
+.sidebar-header h2 {
margin: 0;
- font-size: 1.25rem;
+ font-size: 1.125rem;
font-weight: 600;
color: #1e293b;
- flex: 1;
- word-break: break-word;
}
-.kb-actions {
+.sidebar-actions {
display: flex;
gap: 0.5rem;
- flex-shrink: 0;
+ align-items: center;
}
-.kb-action-btn {
+.btn-new-kb,
+.btn-toggle-sidebar {
padding: 0.5rem;
border: none;
border-radius: 6px;
@@ -148,135 +102,463 @@
justify-content: center;
}
-.kb-action-btn:hover {
+.btn-new-kb:hover {
background: #e2e8f0;
+ color: #667eea;
}
-.view-btn:hover {
- background: #dbeafe;
- color: #3b82f6;
-}
-
-.delete-btn:hover {
- background: #fee2e2;
- color: #ef4444;
-}
-
-.kb-card-body {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.kb-meta {
- display: flex;
- flex-wrap: wrap;
- gap: 1rem;
- color: #64748b;
- font-size: 0.875rem;
-}
-
-.kb-meta-item {
- display: flex;
- align-items: center;
- gap: 0.25rem;
-}
-
-.kb-prompt {
- padding: 0.75rem;
- background: #f8fafc;
- border-left: 3px solid #667eea;
- border-radius: 4px;
- font-size: 0.875rem;
- color: #475569;
-}
-
-.kb-content-preview {
- color: #64748b;
- font-size: 0.875rem;
- line-height: 1.6;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 3;
- -webkit-box-orient: vertical;
-}
-
-/* Empty State */
-.empty-state {
- text-align: center;
- padding: 3rem 2rem;
- color: #94a3b8;
-}
-
-.empty-state p {
- margin: 0.5rem 0;
- font-size: 1rem;
-}
-
-.empty-hint {
- font-size: 0.875rem;
- color: #cbd5e1;
-}
-
-/* Generation Section */
-.kb-generation-section {
- margin-top: 2rem;
- padding-top: 2rem;
- border-top: 2px solid #e2e8f0;
-}
-
-.kb-generation-section h2 {
- margin-bottom: 1.5rem;
- font-size: 1.25rem;
- font-weight: 600;
+.btn-toggle-sidebar:hover {
+ background: #e2e8f0;
color: #1e293b;
}
-.generation-form {
- display: flex;
- flex-direction: column;
- gap: 1rem;
+/* 知识库列表 */
+.kb-list-sidebar {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0.5rem;
}
-.kb-title-display {
- padding: 0.75rem 1rem;
- background: #f8fafc;
+.kb-list-item {
+ padding: 1rem;
+ margin-bottom: 0.5rem;
border-radius: 8px;
- font-size: 0.95rem;
- color: #475569;
-}
-
-.generation-actions {
- display: flex;
- gap: 1rem;
- align-items: flex-end;
-}
-
-.prompt-input-container {
- flex-grow: 1;
- display: flex;
- gap: 0.5rem;
- align-items: flex-end;
-}
-
-.add-meeting-btn {
- padding: 0.75rem;
- border-radius: 8px;
- border: 1px solid #e2e8f0;
- background: #f8fafc;
cursor: pointer;
transition: all 0.2s ease;
- flex-shrink: 0;
+ border: 1px solid transparent;
+}
+
+.kb-list-item:hover {
+ background: #f8fafc;
+ border-color: #e2e8f0;
+}
+
+.kb-list-item.active {
+ background: #eff6ff;
+ border-color: #667eea;
+}
+
+.kb-list-item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 0.5rem;
+}
+
+.kb-list-item-header h3 {
+ margin: 0;
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: #1e293b;
+ flex: 1;
+ word-break: break-word;
+}
+
+.kb-item-actions {
+ display: flex;
+ gap: 0.25rem;
+ align-items: center;
+}
+
+.btn-edit-kb,
+.btn-delete-kb {
+ padding: 0.25rem;
+ border: none;
+ border-radius: 4px;
+ background: transparent;
+ color: #94a3b8;
+ cursor: pointer;
+ transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
-.add-meeting-btn:hover {
- background: #667eea;
+.btn-edit-kb:hover {
+ background: #dbeafe;
+ color: #3b82f6;
+}
+
+.btn-delete-kb {
+ padding: 0.25rem;
+ border: none;
+ border-radius: 4px;
+ background: transparent;
+ color: #94a3b8;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.btn-delete-kb:hover {
+ background: #fee2e2;
+ color: #ef4444;
+}
+
+.kb-list-item-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ font-size: 0.75rem;
+ color: #64748b;
+}
+
+.kb-list-item-meta .meta-item {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+/* 右侧详情区 */
+.kb-detail-area {
+ flex: 1;
+ overflow-y: auto;
+ padding: 2rem;
+ background: #f8fafc;
+}
+
+.kb-detail-header {
+ background: white;
+ padding: 2rem;
+ border-radius: 12px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ margin-bottom: 1.5rem;
+}
+
+.kb-header-title {
+ margin-bottom: 1.5rem;
+}
+
+.kb-detail-header h1 {
+ font-size: 1.75rem;
+ font-weight: 600;
+ color: #1e293b;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+ line-height: 1.2;
+}
+
+.kb-detail-header h1 .inline-title-tags {
+ margin: 0;
+}
+
+.kb-detail-header h1 .inline-title-tags .tag-item {
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
- border-color: #667eea;
+ font-size: 12px;
+ padding: 4px 10px;
+ border-radius: 14px;
+}
+
+.kb-detail-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1.5rem;
+ color: #64748b;
+ font-size: 0.9rem;
+}
+
+.kb-detail-meta .meta-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+/* 数据源区域 */
+.kb-sources-section {
+ background: white;
+ padding: 1.5rem;
+ border-radius: 12px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ margin-bottom: 1.5rem;
+}
+
+.kb-sources-section h3 {
+ margin: 0 0 1rem 0;
+ font-size: 1rem;
+ font-weight: 600;
+ color: #1e293b;
+}
+
+.source-meetings-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.source-meeting-link {
+ display: inline-block;
+ padding: 0.5rem 0.75rem;
+ background: #f1f5f9;
+ border-radius: 6px;
+ color: #667eea;
+ text-decoration: none;
+ font-size: 0.9rem;
+ transition: all 0.2s ease;
+ border-left: 3px solid #667eea;
+}
+
+.source-meeting-link:hover {
+ background: #e2e8f0;
+ transform: translateX(4px);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+/* 用户提示词区域 */
+.kb-prompt-section {
+ background: #f8fafc;
+ padding: 1rem;
+ border-left: 3px solid #667eea;
+ border-radius: 6px;
+ margin-bottom: 1.5rem;
+ font-size: 0.9rem;
+ color: #475569;
+}
+
+/* 内容Tabs区域 */
+.kb-content-tabs {
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+}
+
+.kb-content-tabs .ant-tabs-nav {
+ margin: 0;
+ padding: 0 1.5rem;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.kb-content-tabs .ant-tabs-tab {
+ font-size: 1rem;
+ color: #475569;
+ padding: 16px 4px;
+ margin: 0 16px;
+}
+
+.kb-content-tabs .ant-tabs-tab .ant-tabs-tab-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 500;
+}
+
+.kb-content-tabs .ant-tabs-tab-active .ant-tabs-tab-btn {
+ color: #667eea;
+}
+
+.kb-content-tabs .ant-tabs-ink-bar {
+ background: #667eea;
+ height: 3px;
+}
+
+.kb-content-tabs .ant-tabs-content-holder {
+ padding: 2rem;
+}
+
+/* Markdown内容区 */
+.kb-content-markdown {
+ line-height: 1.8;
+ color: #475569;
+}
+
+.kb-content-markdown h1 {
+ color: #1e293b;
+ font-size: 1.75rem;
+ margin-top: 2rem;
+ margin-bottom: 1rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid #e2e8f0;
+}
+
+.kb-content-markdown h2 {
+ color: #1e293b;
+ font-size: 1.5rem;
+ margin-top: 1.5rem;
+ margin-bottom: 0.75rem;
+}
+
+.kb-content-markdown h3 {
+ color: #1e293b;
+ font-size: 1.25rem;
+ margin-top: 1.25rem;
+ margin-bottom: 0.5rem;
+}
+
+.kb-content-markdown p {
+ margin-bottom: 1rem;
+}
+
+.kb-content-markdown ul,
+.kb-content-markdown ol {
+ margin-left: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.kb-content-markdown li {
+ margin-bottom: 0.5rem;
+}
+
+.kb-content-markdown code {
+ background: #f1f5f9;
+ padding: 0.2rem 0.4rem;
+ border-radius: 4px;
+ font-size: 0.9em;
+ font-family: 'Courier New', monospace;
+}
+
+.kb-content-markdown pre {
+ background: #1e293b;
+ color: #e2e8f0;
+ padding: 1rem;
+ border-radius: 8px;
+ overflow-x: auto;
+ margin-bottom: 1rem;
+}
+
+.kb-content-markdown pre code {
+ background: none;
+ padding: 0;
+ color: inherit;
+}
+
+.kb-content-markdown table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 1rem;
+}
+
+.kb-content-markdown th,
+.kb-content-markdown td {
+ border: 1px solid #e2e8f0;
+ padding: 0.5rem;
+ text-align: left;
+}
+
+.kb-content-markdown th {
+ background: #f8fafc;
+ font-weight: 600;
+}
+
+.kb-content-markdown blockquote {
+ border-left: 4px solid #667eea;
+ padding-left: 1rem;
+ margin-left: 0;
+ color: #64748b;
+ font-style: italic;
+}
+
+/* 空状态 */
+.kb-empty-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 60vh;
+ color: #94a3b8;
+}
+
+.kb-empty-placeholder p {
+ margin-top: 1rem;
+ font-size: 1.125rem;
+}
+
+.empty-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 3rem;
+ color: #94a3b8;
+ font-size: 1rem;
+}
+
+.empty-state {
+ text-align: center;
+ padding: 2rem;
+ color: #94a3b8;
+}
+
+/* 新增知识库弹窗 */
+.create-kb-modal {
+ max-width: 700px;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ backdrop-filter: blur(2px);
+}
+
+.modal-content {
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ width: 90%;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem 2rem;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.modal-header h2 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #1e293b;
+}
+
+.close-btn {
+ padding: 0.5rem;
+ border: none;
+ border-radius: 6px;
+ background: #f1f5f9;
+ color: #64748b;
+ cursor: pointer;
+ font-size: 1.5rem;
+ line-height: 1;
+ transition: all 0.2s ease;
+}
+
+.close-btn:hover {
+ background: #e2e8f0;
+ color: #1e293b;
+}
+
+.modal-body {
+ padding: 2rem;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.form-group {
+ margin-bottom: 1.5rem;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ color: #1e293b;
}
.kb-prompt-input {
@@ -297,141 +579,14 @@
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
-.generate-kb-btn {
- align-self: flex-end;
- padding: 0.75rem 1.5rem;
- border-radius: 8px;
- border: none;
- background: #667eea;
- color: white;
- cursor: pointer;
- font-weight: 500;
- transition: all 0.2s ease;
- white-space: nowrap;
- flex-shrink: 0;
-}
-
-.generate-kb-btn:hover:not(:disabled) {
- background: #5568d3;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
-}
-
-.generate-kb-btn:disabled {
- background: #cbd5e1;
- cursor: not-allowed;
-}
-
-.selected-meetings-info {
- padding: 0.5rem 1rem;
- background: #dbeafe;
- color: #1e40af;
- border-radius: 6px;
- font-size: 0.875rem;
- font-weight: 500;
-}
-
-/* Modal Styles */
-.modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- backdrop-filter: blur(2px);
-}
-
-.modal-content {
- background: white;
- padding: 2rem;
- border-radius: 12px;
- width: 90%;
- max-width: 800px;
- max-height: 80vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
-}
-
-.detail-modal {
- max-width: 900px;
-}
-
-.modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1.5rem;
- padding-bottom: 1rem;
- border-bottom: 1px solid #e2e8f0;
-}
-
-.modal-header h2 {
- margin: 0;
- font-size: 1.5rem;
- font-weight: 600;
- color: #1e293b;
-}
-
-.close-btn {
- padding: 0.5rem;
- border: none;
- border-radius: 6px;
- background: #f1f5f9;
- color: #64748b;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.close-btn:hover {
- background: #e2e8f0;
- color: #1e293b;
-}
-
-.tag-cloud-wrapper {
- flex-shrink: 0;
- padding-bottom: 1rem;
- border-bottom: 1px solid #e2e8f0;
- margin-bottom: 1rem;
-}
-
-/* Search Wrapper Styles */
-.search-wrapper {
- flex-shrink: 0;
- padding-bottom: 1rem;
- border-bottom: 1px solid #e2e8f0;
- margin-bottom: 1rem;
-}
-
-.search-input-container {
- position: relative;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.search-icon {
- position: absolute;
- left: 12px;
- color: #94a3b8;
- pointer-events: none;
-}
-
.search-input {
- flex: 1;
- padding: 0.75rem 0.75rem 0.75rem 2.5rem;
+ width: 100%;
+ padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 0.95rem;
- transition: all 0.2s ease;
+ margin-bottom: 1rem;
+ transition: border-color 0.2s ease;
}
.search-input:focus {
@@ -440,30 +595,12 @@
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
-.clear-search-btn {
- position: absolute;
- right: 8px;
- padding: 0.25rem;
- border: none;
- background: transparent;
- color: #94a3b8;
- cursor: pointer;
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
-}
-
-.clear-search-btn:hover {
- background: #f1f5f9;
- color: #64748b;
-}
-
.meeting-list {
- flex-grow: 1;
+ max-height: 300px;
overflow-y: auto;
- padding-right: 0.5rem;
+ border: 1px solid #e2e8f0;
+ border-radius: 8px;
+ padding: 0.5rem;
}
.meeting-item {
@@ -492,12 +629,6 @@
cursor: pointer;
}
-.meeting-item input[type="radio"] {
- width: 18px;
- height: 18px;
- cursor: pointer;
-}
-
.meeting-item label {
cursor: pointer;
flex: 1;
@@ -505,101 +636,60 @@
color: #475569;
}
-.modal-actions {
- margin-top: 1.5rem;
- padding-top: 1rem;
- border-top: 1px solid #e2e8f0;
- text-align: right;
+.selected-meetings-info {
+ padding: 0.75rem 1rem;
+ background: #dbeafe;
+ color: #1e40af;
+ border-radius: 6px;
+ font-size: 0.875rem;
+ font-weight: 500;
}
-.modal-actions button {
+.modal-actions {
+ padding: 1rem 2rem;
+ border-top: 1px solid #e2e8f0;
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.btn-cancel,
+.btn-primary,
+.btn-delete {
padding: 0.75rem 1.5rem;
border-radius: 8px;
border: none;
- background: #667eea;
- color: white;
- cursor: pointer;
font-weight: 500;
+ cursor: pointer;
transition: all 0.2s ease;
}
-.modal-actions button:hover {
- background: #5568d3;
-}
-
-/* Detail Modal Styles */
-.detail-content {
- overflow-y: auto;
- flex-grow: 1;
-}
-
-.detail-meta {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
- padding: 1rem;
- background: #f8fafc;
- border-radius: 8px;
- margin-bottom: 1.5rem;
-}
-
-.detail-meta-item {
- font-size: 0.95rem;
- color: #475569;
-}
-
-.detail-meta-item strong {
- color: #1e293b;
- margin-right: 0.5rem;
-}
-
-.detail-body {
- padding: 1rem 0;
-}
-
-.detail-body h3 {
- margin: 0 0 1rem 0;
- font-size: 1.125rem;
- font-weight: 600;
- color: #1e293b;
-}
-
-.kb-content-full {
- line-height: 1.8;
- color: #475569;
- white-space: pre-wrap;
- word-wrap: break-word;
- padding: 1rem;
- background: #f8fafc;
- border-radius: 8px;
- max-height: 400px;
- overflow-y: auto;
-}
-
-/* Scrollbar Styles */
-.meeting-list::-webkit-scrollbar,
-.kb-content-full::-webkit-scrollbar {
- width: 6px;
-}
-
-.meeting-list::-webkit-scrollbar-track,
-.kb-content-full::-webkit-scrollbar-track {
+.btn-cancel {
background: #f1f5f9;
- border-radius: 3px;
+ color: #475569;
}
-.meeting-list::-webkit-scrollbar-thumb,
-.kb-content-full::-webkit-scrollbar-thumb {
+.btn-cancel:hover {
+ background: #e2e8f0;
+}
+
+.btn-primary {
+ background: #667eea;
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: #5568d3;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.btn-primary:disabled {
background: #cbd5e1;
- border-radius: 3px;
+ cursor: not-allowed;
}
-.meeting-list::-webkit-scrollbar-thumb:hover,
-.kb-content-full::-webkit-scrollbar-thumb:hover {
- background: #94a3b8;
-}
-
-/* Delete Modal Styles - consistent with MeetingTimeline */
+/* 删除确认弹窗 */
.delete-modal-overlay {
position: fixed;
top: 0;
@@ -634,34 +724,6 @@
line-height: 1.6;
}
-.delete-modal .modal-actions {
- display: flex;
- gap: 1rem;
- justify-content: flex-end;
- margin-top: 0;
- padding-top: 0;
- border-top: none;
-}
-
-.btn-cancel, .btn-delete {
- padding: 0.5rem 1rem;
- border-radius: 6px;
- font-size: 0.9rem;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
- border: none;
-}
-
-.btn-cancel {
- background: #f1f5f9;
- color: #475569;
-}
-
-.btn-cancel:hover {
- background: #e2e8f0;
-}
-
.btn-delete {
background: #ef4444;
color: white;
@@ -671,30 +733,36 @@
background: #dc2626;
}
-/* Source Meetings List Styles */
-.source-meetings-list {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
- margin-top: 0.5rem;
+/* 脑图组件 */
+.knowledge-mindmap {
+ padding: 2rem;
+ text-align: center;
+ color: #94a3b8;
}
-.source-meeting-link {
- display: inline-block;
- padding: 0.5rem 0.75rem;
+/* 滚动条样式 */
+.kb-list-sidebar::-webkit-scrollbar,
+.kb-detail-area::-webkit-scrollbar,
+.meeting-list::-webkit-scrollbar {
+ width: 6px;
+}
+
+.kb-list-sidebar::-webkit-scrollbar-track,
+.kb-detail-area::-webkit-scrollbar-track,
+.meeting-list::-webkit-scrollbar-track {
background: #f1f5f9;
- border-radius: 6px;
- color: #667eea;
- text-decoration: none;
- font-size: 0.9rem;
- transition: all 0.2s ease;
- border-left: 3px solid #667eea;
+ border-radius: 3px;
}
-.source-meeting-link:hover {
- background: #e2e8f0;
- transform: translateX(4px);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+.kb-list-sidebar::-webkit-scrollbar-thumb,
+.kb-detail-area::-webkit-scrollbar-thumb,
+.meeting-list::-webkit-scrollbar-thumb {
+ background: #cbd5e1;
+ border-radius: 3px;
}
-
+.kb-list-sidebar::-webkit-scrollbar-thumb:hover,
+.kb-detail-area::-webkit-scrollbar-thumb:hover,
+.meeting-list::-webkit-scrollbar-thumb:hover {
+ background: #94a3b8;
+}
diff --git a/src/pages/KnowledgeBasePage.jsx b/src/pages/KnowledgeBasePage.jsx
index 70ed4b0..98a98d8 100644
--- a/src/pages/KnowledgeBasePage.jsx
+++ b/src/pages/KnowledgeBasePage.jsx
@@ -1,20 +1,406 @@
-import React from 'react';
-import { MessageSquare, Book, Users } from 'lucide-react';
+import React, { useState, useEffect } from 'react';
+import { MessageSquare, ChevronLeft, ChevronRight, Plus, Calendar, Database, Trash2, Edit, FileText, Image } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
-import { Tabs } from 'antd';
-import PersonalKnowledgeBase from '../components/knowledgebase/PersonalKnowledgeBase';
-import SharedKnowledgeBase from '../components/knowledgebase/SharedKnowledgeBase';
+import apiClient from '../utils/apiClient';
+import { buildApiUrl, API_ENDPOINTS } from '../config/api';
+import ContentViewer from '../components/ContentViewer';
+import TagDisplay from '../components/TagDisplay';
+import remarkGfm from 'remark-gfm';
+import rehypeRaw from 'rehype-raw';
+import rehypeSanitize from 'rehype-sanitize';
+import html2canvas from 'html2canvas';
import './KnowledgeBasePage.css';
-const { TabPane } = Tabs;
-
const KnowledgeBasePage = ({ user }) => {
const navigate = useNavigate();
+ const [kbs, setKbs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedKb, setSelectedKb] = useState(null);
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+ const [showCreateForm, setShowCreateForm] = useState(false);
+ const [meetings, setMeetings] = useState([]);
+ const [selectedMeetings, setSelectedMeetings] = useState([]);
+ const [userPrompt, setUserPrompt] = useState('');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [generating, setGenerating] = useState(false);
+ const [taskId, setTaskId] = useState(null);
+ const [progress, setProgress] = useState(0);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [deletingKb, setDeletingKb] = useState(null);
+
+ useEffect(() => {
+ fetchAllKbs();
+ fetchMeetings();
+ }, []);
+
+ useEffect(() => {
+ if (taskId) {
+ const interval = setInterval(() => {
+ apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.TASK_STATUS(taskId)))
+ .then(response => {
+ const { status, progress } = response.data;
+ setProgress(progress || 0);
+ if (status === 'completed') {
+ clearInterval(interval);
+ setTaskId(null);
+ setGenerating(false);
+ setProgress(0);
+ setUserPrompt('');
+ setSelectedMeetings([]);
+ setShowCreateForm(false);
+ fetchAllKbs();
+ } else if (status === 'failed') {
+ clearInterval(interval);
+ setTaskId(null);
+ setGenerating(false);
+ setProgress(0);
+ alert('知识库生成失败,请稍后重试');
+ }
+ })
+ .catch(error => {
+ console.error("Error fetching task status:", error);
+ clearInterval(interval);
+ setTaskId(null);
+ setGenerating(false);
+ setProgress(0);
+ });
+ }, 2000);
+ return () => clearInterval(interval);
+ }
+ }, [taskId]);
+
+ const fetchAllKbs = () => {
+ setLoading(true);
+ // 获取所有知识库(个人和共享)
+ apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.LIST))
+ .then(response => {
+ setKbs(response.data.kbs);
+ // 如果有知识库且没有选中,默认选中第一个
+ if (response.data.kbs.length > 0 && !selectedKb) {
+ loadKbDetail(response.data.kbs[0].kb_id);
+ }
+ setLoading(false);
+ })
+ .catch(error => {
+ console.error("Error fetching knowledge bases:", error);
+ setLoading(false);
+ });
+ };
+
+ const fetchMeetings = () => {
+ apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST, { user_id: user.user_id }))
+ .then(response => {
+ setMeetings(response.data);
+ })
+ .catch(error => {
+ console.error("Error fetching meetings:", error);
+ });
+ };
+
+ const loadKbDetail = async (kbId) => {
+ try {
+ const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kbId)));
+ setSelectedKb(response.data);
+ } catch (error) {
+ console.error("Error loading knowledge base detail:", error);
+ }
+ };
+
+ const handleKbSelect = (kb) => {
+ loadKbDetail(kb.kb_id);
+ };
+
+ const handleGenerate = async () => {
+ if (!selectedMeetings || selectedMeetings.length === 0) {
+ alert('请至少选择一个会议');
+ return;
+ }
+
+ setGenerating(true);
+ try {
+ const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.CREATE), {
+ user_prompt: userPrompt,
+ source_meeting_ids: selectedMeetings.join(','),
+ is_shared: false
+ });
+ setTaskId(response.data.task_id);
+ } catch (error) {
+ console.error("Error creating knowledge base:", error);
+ setGenerating(false);
+ }
+ };
+
+ const toggleMeetingSelection = (meetingId) => {
+ setSelectedMeetings(prev =>
+ prev.includes(meetingId)
+ ? prev.filter(id => id !== meetingId)
+ : [...prev, meetingId]
+ );
+ };
+
+ const filteredMeetings = (meetings || []).filter(meeting => {
+ if (!searchQuery) return true;
+ return meeting.title.toLowerCase().includes(searchQuery.toLowerCase());
+ });
+
+ const handleDelete = async (kb) => {
+ setDeletingKb(kb);
+ setShowDeleteConfirm(true);
+ };
+
+ const confirmDelete = async () => {
+ try {
+ await apiClient.delete(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DELETE(deletingKb.kb_id)));
+ setShowDeleteConfirm(false);
+ setDeletingKb(null);
+ // 如果删除的是当前选中的,清除选中
+ if (selectedKb && selectedKb.kb_id === deletingKb.kb_id) {
+ setSelectedKb(null);
+ }
+ fetchAllKbs();
+ } catch (error) {
+ console.error("Error deleting knowledge base:", error);
+ alert('删除失败,请稍后重试');
+ setShowDeleteConfirm(false);
+ setDeletingKb(null);
+ }
+ };
+
+ const formatDate = (dateString) => {
+ const date = new Date(dateString);
+ return date.toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
const handleLogoClick = () => {
navigate('/dashboard');
};
+ // 导出知识库内容为图片
+ const exportSummaryToImage = async () => {
+ try {
+ if (!selectedKb?.content) {
+ alert('暂无知识库内容,请稍后再试。');
+ return;
+ }
+
+ // 创建完整的导出内容
+ const exportContainer = document.createElement('div');
+ exportContainer.style.cssText = `
+ position: fixed;
+ top: -10000px;
+ left: -10000px;
+ width: 800px;
+ background: white;
+ padding: 40px;
+ font-family: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", sans-serif;
+ line-height: 1.6;
+ color: #333;
+ `;
+
+ const createdAt = formatDate(selectedKb.created_at);
+ const sourceMeetings = selectedKb.source_meetings?.map(m => m.title).join('、') || '无';
+
+ exportContainer.innerHTML = `
+
+
+ ${selectedKb.title || '知识库'}
+
+
+
+
+ 📋 知识库信息
+
+
创建时间:${createdAt}
+
创建者:${selectedKb.created_by_name || '未知'}
+
数据源数量:${selectedKb.source_meetings?.length || 0}个
+ ${selectedKb.user_prompt ? `
用户提示词:${selectedKb.user_prompt}
` : ''}
+
+
+
+
+
+ 导出时间:${new Date().toLocaleString('zh-CN')}
+
+
+ `;
+
+ document.body.appendChild(exportContainer);
+
+ // 渲染Markdown内容
+ const tempDiv = document.createElement('div');
+ tempDiv.style.display = 'none';
+ document.body.appendChild(tempDiv);
+
+ const ReactMarkdownModule = (await import('react-markdown')).default;
+ const { createRoot } = await import('react-dom/client');
+
+ const root = createRoot(tempDiv);
+
+ await new Promise((resolve) => {
+ root.render(
+ React.createElement(ReactMarkdownModule, {
+ remarkPlugins: [remarkGfm],
+ rehypePlugins: [rehypeRaw, rehypeSanitize],
+ children: selectedKb.content
+ })
+ );
+ setTimeout(resolve, 200);
+ });
+
+ // 获取渲染后的HTML并添加样式
+ const renderedHTML = tempDiv.innerHTML;
+ const summaryContentDiv = exportContainer.querySelector('#summary-content');
+ summaryContentDiv.innerHTML = renderedHTML;
+
+ // 为渲染后的内容添加样式
+ const styles = `
+
+ `;
+
+ exportContainer.insertAdjacentHTML('afterbegin', styles);
+
+ // 等待图片和样式加载
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ // 使用html2canvas生成图片
+ const canvas = await html2canvas(exportContainer, {
+ width: 880,
+ height: exportContainer.scrollHeight + 80,
+ scale: 2,
+ useCORS: true,
+ allowTaint: true,
+ backgroundColor: '#ffffff',
+ scrollX: 0,
+ scrollY: 0
+ });
+
+ // 创建下载链接
+ const link = document.createElement('a');
+ link.download = `${selectedKb.title || '知识库'}_内容_${new Date().toISOString().slice(0, 10)}.png`;
+ link.href = canvas.toDataURL('image/png', 1.0);
+
+ // 触发下载
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ // 清理DOM
+ root.unmount();
+ document.body.removeChild(tempDiv);
+ document.body.removeChild(exportContainer);
+
+ } catch (error) {
+ console.error('图片导出失败:', error);
+ alert('图片导出失败,请重试。');
+ }
+ };
+
+ // 导出思维导图为图片
+ const exportMindMapToImage = async () => {
+ try {
+ if (!selectedKb?.content) {
+ alert('暂无内容,无法导出思维导图。');
+ return;
+ }
+
+ // 查找SVG元素
+ const svgElement = document.querySelector('.markmap-render-area svg');
+ if (!svgElement) {
+ alert('未找到思维导图,请先切换到脑图标签页。');
+ return;
+ }
+
+ // 使用html2canvas导出SVG
+ const mindmapContainer = svgElement.parentElement;
+
+ const canvas = await html2canvas(mindmapContainer, {
+ scale: 2,
+ useCORS: true,
+ allowTaint: true,
+ backgroundColor: '#ffffff',
+ scrollX: 0,
+ scrollY: 0
+ });
+
+ // 创建下载链接
+ const link = document.createElement('a');
+ link.download = `${selectedKb.title || '知识库'}_思维导图_${new Date().toISOString().slice(0, 10)}.png`;
+ link.href = canvas.toDataURL('image/png', 1.0);
+
+ // 触发下载
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ } catch (error) {
+ console.error('思维导图导出失败:', error);
+ alert('思维导图导出失败,请重试。');
+ }
+ };
+
+ const isCreator = selectedKb && user && String(selectedKb.creator_id) === String(user.user_id);
+
+ if (loading) {
+ return
Loading...
;
+ }
+
return (
@@ -26,26 +412,281 @@ const KnowledgeBasePage = ({ user }) => {
知识库管理
-
-
-
- 个人知识库}
- key="personal"
- >
-
-
- 共享知识库}
- key="shared"
- >
-
-
-
+
+
+ {/* 左侧知识库列表 */}
+
+
+ {!sidebarCollapsed &&
知识库列表
}
+
+ {!sidebarCollapsed && (
+
+ )}
+
+
+
+
+ {!sidebarCollapsed && (
+
+ {kbs.length === 0 ? (
+
+ ) : (
+ kbs.map(kb => (
+
handleKbSelect(kb)}
+ >
+
+
{kb.title}
+ {isCreator && (
+
+
+
+
+ )}
+
+
+
+
+ {formatDate(kb.created_at)}
+
+
+
+ {kb.source_meeting_count || 0} 个数据源
+
+
+
+ ))
+ )}
+
+ )}
+
+
+ {/* 右侧详情区 */}
+
+ {selectedKb ? (
+ <>
+
+
+
+ {selectedKb.title}
+ {selectedKb.tags && selectedKb.tags.length > 0 && (
+ tag.name)}
+ size="medium"
+ showIcon={true}
+ className="inline-title-tags"
+ />
+ )}
+
+
+
+ {selectedKb.created_by_name && (
+
+ 创建者: {selectedKb.created_by_name}
+
+ )}
+
+
+ {formatDate(selectedKb.created_at)}
+
+ {selectedKb.source_meetings && selectedKb.source_meetings.length > 0 && (
+
+
+ {selectedKb.source_meetings.length} 个数据源
+
+ )}
+
+
+
+ {/* 用户提示词 */}
+ {selectedKb.user_prompt && (
+
+ 用户提示词: {selectedKb.user_prompt}
+
+ )}
+
+ {/* 数据源列表 */}
+ {selectedKb.source_meetings && selectedKb.source_meetings.length > 0 && (
+
+ )}
+
+ {/* 内容区域 - Tabs */}
+
+
+
+ 导出图片
+
+ )
+ }
+ mindmapActions={
+ selectedKb.content && (
+
+ )
+ }
+ />
+
+ >
+ ) : (
+
+ )}
+
+ {/* 新增知识库表单弹窗 */}
+ {showCreateForm && (
+
+
+
+
新增知识库
+
+
+
+
+
+
+
+
+
setSearchQuery(e.target.value)}
+ className="search-input"
+ />
+
+ {filteredMeetings.length === 0 ? (
+
+ ) : (
+ filteredMeetings.map(meeting => (
+
toggleMeetingSelection(meeting.meeting_id)}
+ >
+ toggleMeetingSelection(meeting.meeting_id)}
+ />
+
+
+ ))
+ )}
+
+
+ {selectedMeetings.length > 0 && (
+
+ 已选择 {selectedMeetings.length} 个会议
+
+ )}
+
+
+
+
+
+
+
+ )}
+
+ {/* 删除确认弹窗 */}
+ {showDeleteConfirm && deletingKb && (
+
{ setShowDeleteConfirm(false); setDeletingKb(null); }}>
+
e.stopPropagation()}>
+
确认删除
+
确定要删除知识库条目 "{deletingKb.title}" 吗?此操作无法撤销。
+
+
+
+
+
+
+ )}
);
};
-export default KnowledgeBasePage;
\ No newline at end of file
+export default KnowledgeBasePage;
diff --git a/src/pages/MeetingDetails.css b/src/pages/MeetingDetails.css
index b0b1842..997d9e5 100644
--- a/src/pages/MeetingDetails.css
+++ b/src/pages/MeetingDetails.css
@@ -982,6 +982,18 @@
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3);
}
+.export-btn {
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ color: white;
+ box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
+}
+
+.export-btn:hover {
+ background: linear-gradient(135deg, #5a67d8, #6b46c1);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
+ color: white;
+}
/* Delete Modal */
diff --git a/src/pages/MeetingDetails.jsx b/src/pages/MeetingDetails.jsx
index d5e23d7..bb7867e 100644
--- a/src/pages/MeetingDetails.jsx
+++ b/src/pages/MeetingDetails.jsx
@@ -1,16 +1,16 @@
import React, { useState, useEffect, useRef } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import apiClient from '../utils/apiClient';
-import { ArrowLeft, Clock, Users, FileText, User, Calendar, Play, Pause, Volume2, MessageCircle, Edit, Trash2, Settings, Save, X, Edit3, Brain, Sparkles, Download, ArrowDown, RefreshCw, RefreshCwOff } from 'lucide-react';
+import { ArrowLeft, Clock, Users, User, Calendar, Play, Pause, Volume2, MessageCircle, Edit, Trash2, Settings, Save, X, Edit3, Brain, Sparkles, Download, ArrowDown, RefreshCw, RefreshCwOff, Image } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
-import { buildApiUrl, API_ENDPOINTS, API_BASE_URL } from '../config/api';
-import MindMap from '../components/MindMap';
-import MeetingSummary from '../components/MeetingSummary';
+import { buildApiUrl, API_ENDPOINTS } from '../config/api';
+import ContentViewer from '../components/ContentViewer';
import TagDisplay from '../components/TagDisplay';
import { Tabs } from 'antd';
+import html2canvas from 'html2canvas';
import './MeetingDetails.css';
const { TabPane } = Tabs;
@@ -726,6 +726,222 @@ const MeetingDetails = ({ user }) => {
}
};
+ // 导出会议总结为图片
+ const exportSummaryToImage = async () => {
+ try {
+ if (!meeting?.summary) {
+ alert('暂无会议总结内容,请先生成AI总结。');
+ return;
+ }
+
+ // 创建完整的导出内容
+ const exportContainer = document.createElement('div');
+ exportContainer.style.cssText = `
+ position: fixed;
+ top: -10000px;
+ left: -10000px;
+ width: 800px;
+ background: white;
+ padding: 40px;
+ font-family: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", sans-serif;
+ line-height: 1.6;
+ color: #333;
+ `;
+
+ const meetingTime = formatDateTime(meeting.meeting_time);
+ const attendeesList = meeting.attendees.map(attendee =>
+ typeof attendee === 'string' ? attendee : attendee.caption
+ ).join('、');
+
+ exportContainer.innerHTML = `
+
+
+ ${meeting.title || '会议总结'}
+
+
+
+
+ 📋 会议信息
+
+
会议时间:${meetingTime}
+
创建人:${meeting.creator_username}
+
参会人数:${meeting.attendees.length}人
+
参会人员:${attendeesList}
+
+
+
+
+
+ 导出时间:${new Date().toLocaleString('zh-CN')}
+
+
+ `;
+
+ document.body.appendChild(exportContainer);
+
+ // 渲染Markdown内容
+ const tempDiv = document.createElement('div');
+ tempDiv.style.display = 'none';
+ document.body.appendChild(tempDiv);
+
+ const ReactMarkdownModule = (await import('react-markdown')).default;
+ const { createRoot } = await import('react-dom/client');
+
+ const root = createRoot(tempDiv);
+
+ await new Promise((resolve) => {
+ root.render(
+ React.createElement(ReactMarkdownModule, {
+ remarkPlugins: [remarkGfm],
+ rehypePlugins: [rehypeRaw, rehypeSanitize],
+ children: meeting.summary
+ })
+ );
+ setTimeout(resolve, 200);
+ });
+
+ // 获取渲染后的HTML并添加样式
+ const renderedHTML = tempDiv.innerHTML;
+ const summaryContentDiv = exportContainer.querySelector('#summary-content');
+ summaryContentDiv.innerHTML = renderedHTML;
+
+ // 为渲染后的内容添加样式
+ const styles = `
+
+ `;
+
+ exportContainer.insertAdjacentHTML('afterbegin', styles);
+
+ // 等待图片和样式加载
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ // 使用html2canvas生成图片
+ const canvas = await html2canvas(exportContainer, {
+ width: 880,
+ height: exportContainer.scrollHeight + 80,
+ scale: 2,
+ useCORS: true,
+ allowTaint: true,
+ backgroundColor: '#ffffff',
+ scrollX: 0,
+ scrollY: 0
+ });
+
+ // 创建下载链接
+ const link = document.createElement('a');
+ link.download = `${meeting.title || '会议总结'}_总结_${new Date().toISOString().slice(0, 10)}.png`;
+ link.href = canvas.toDataURL('image/png', 1.0);
+
+ // 触发下载
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ // 清理DOM
+ root.unmount();
+ document.body.removeChild(tempDiv);
+ document.body.removeChild(exportContainer);
+
+ } catch (error) {
+ console.error('图片导出失败:', error);
+ alert('图片导出失败,请重试。');
+ }
+ };
+
+ // 导出思维导图为图片
+ const exportMindMapToImage = async () => {
+ try {
+ if (!meeting?.summary) {
+ alert('暂无内容,无法导出思维导图。');
+ return;
+ }
+
+ // 查找SVG元素
+ const svgElement = document.querySelector('.markmap-render-area svg');
+ if (!svgElement) {
+ alert('未找到思维导图,请先切换到脑图标签页。');
+ return;
+ }
+
+ // 使用html2canvas导出SVG
+ const mindmapContainer = svgElement.parentElement;
+
+ const canvas = await html2canvas(mindmapContainer, {
+ scale: 2,
+ useCORS: true,
+ allowTaint: true,
+ backgroundColor: '#ffffff',
+ scrollX: 0,
+ scrollY: 0
+ });
+
+ // 创建下载链接
+ const link = document.createElement('a');
+ link.download = `${meeting.title || '会议'}_思维导图_${new Date().toISOString().slice(0, 10)}.png`;
+ link.href = canvas.toDataURL('image/png', 1.0);
+
+ // 触发下载
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ } catch (error) {
+ console.error('思维导图导出失败:', error);
+ alert('思维导图导出失败,请重试。');
+ }
+ };
+
const isCreator = meeting && user && String(meeting.creator_id) === String(user.user_id);
if (loading) {
@@ -957,26 +1173,35 @@ const MeetingDetails = ({ user }) => {
-
- 会议总结} key="1">
-
-
- 会议脑图} key="2">
-
-
-
+
+
+ 导出图片
+
+ )
+ }
+ mindmapActions={
+ meeting.summary && (
+
+ )
+ }
+ />
diff --git a/src/components/admin/PromptManagement.css b/src/pages/admin/PromptManagement.css
similarity index 100%
rename from src/components/admin/PromptManagement.css
rename to src/pages/admin/PromptManagement.css
diff --git a/src/components/admin/PromptManagement.jsx b/src/pages/admin/PromptManagement.jsx
similarity index 98%
rename from src/components/admin/PromptManagement.jsx
rename to src/pages/admin/PromptManagement.jsx
index 59974d0..0e130b4 100644
--- a/src/components/admin/PromptManagement.jsx
+++ b/src/pages/admin/PromptManagement.jsx
@@ -3,7 +3,7 @@ import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import { Plus, MoreVertical, Edit, Trash2, BookText, Tag, FileText } from 'lucide-react';
import './PromptManagement.css';
-import TagEditor from '../TagEditor'; // Reusing the TagEditor component
+import TagEditor from '../../components/TagEditor'; // Reusing the TagEditor component
const PromptManagement = () => {
const [prompts, setPrompts] = useState([]);
diff --git a/src/components/admin/SystemConfiguration.css b/src/pages/admin/SystemConfiguration.css
similarity index 100%
rename from src/components/admin/SystemConfiguration.css
rename to src/pages/admin/SystemConfiguration.css
diff --git a/src/components/admin/SystemConfiguration.jsx b/src/pages/admin/SystemConfiguration.jsx
similarity index 100%
rename from src/components/admin/SystemConfiguration.jsx
rename to src/pages/admin/SystemConfiguration.jsx
diff --git a/src/components/admin/UserManagement.css b/src/pages/admin/UserManagement.css
similarity index 100%
rename from src/components/admin/UserManagement.css
rename to src/pages/admin/UserManagement.css
diff --git a/src/components/admin/UserManagement.jsx b/src/pages/admin/UserManagement.jsx
similarity index 100%
rename from src/components/admin/UserManagement.jsx
rename to src/pages/admin/UserManagement.jsx