From 39be510299539e37c539bb1c58e8654817515c79 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Thu, 16 Oct 2025 17:15:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 4 + src/components/ContentViewer.css | 165 +++ src/components/ContentViewer.jsx | 90 ++ src/components/MindMap.jsx | 225 +--- .../knowledgebase/PersonalKnowledgeBase.jsx | 369 ------ .../knowledgebase/SharedKnowledgeBase.jsx | 157 --- src/pages/AdminManagement.css | 4 +- src/pages/AdminManagement.jsx | 6 +- src/pages/Dashboard.css | 105 +- src/pages/Dashboard.jsx | 38 +- src/pages/EditKnowledgeBase.css | 259 +++++ src/pages/EditKnowledgeBase.jsx | 251 ++++ src/pages/KnowledgeBasePage.css | 1020 +++++++++-------- src/pages/KnowledgeBasePage.jsx | 689 ++++++++++- src/pages/MeetingDetails.css | 12 + src/pages/MeetingDetails.jsx | 273 ++++- .../admin/PromptManagement.css | 0 .../admin/PromptManagement.jsx | 2 +- .../admin/SystemConfiguration.css | 0 .../admin/SystemConfiguration.jsx | 0 .../admin/UserManagement.css | 0 .../admin/UserManagement.jsx | 0 22 files changed, 2412 insertions(+), 1257 deletions(-) create mode 100644 src/components/ContentViewer.css create mode 100644 src/components/ContentViewer.jsx delete mode 100644 src/components/knowledgebase/PersonalKnowledgeBase.jsx delete mode 100644 src/components/knowledgebase/SharedKnowledgeBase.jsx create mode 100644 src/pages/EditKnowledgeBase.css create mode 100644 src/pages/EditKnowledgeBase.jsx rename src/{components => pages}/admin/PromptManagement.css (100%) rename src/{components => pages}/admin/PromptManagement.jsx (98%) rename src/{components => pages}/admin/SystemConfiguration.css (100%) rename src/{components => pages}/admin/SystemConfiguration.jsx (100%) rename src/{components => pages}/admin/UserManagement.css (100%) rename src/{components => pages}/admin/UserManagement.jsx (100%) diff --git a/src/App.jsx b/src/App.jsx index ced3745..30081c1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,6 +9,7 @@ import CreateMeeting from './pages/CreateMeeting'; import EditMeeting from './pages/EditMeeting'; import AdminManagement from './pages/AdminManagement'; import KnowledgeBasePage from './pages/KnowledgeBasePage'; +import EditKnowledgeBase from './pages/EditKnowledgeBase'; import './App.css'; function App() { @@ -85,6 +86,9 @@ function App() { : } /> + : + } /> diff --git a/src/components/ContentViewer.css b/src/components/ContentViewer.css new file mode 100644 index 0000000..0fd3d04 --- /dev/null +++ b/src/components/ContentViewer.css @@ -0,0 +1,165 @@ +/* Content Viewer Component */ +.content-viewer { + background: white; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.content-viewer .ant-tabs-nav { + margin: 0; + padding: 0 1.5rem; + border-bottom: 1px solid #e2e8f0; +} + +.content-viewer .ant-tabs-tab { + font-size: 1rem; + color: #475569; + padding: 16px 4px; + margin: 0 16px; +} + +.content-viewer .ant-tabs-tab .ant-tabs-tab-btn { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} + +.content-viewer .ant-tabs-tab-active .ant-tabs-tab-btn { + color: #667eea; +} + +.content-viewer .ant-tabs-ink-bar { + background: #667eea; + height: 3px; +} + +.content-viewer .ant-tabs-content-holder { + padding: 2rem; +} + +/* Tab Header with Actions */ +.tab-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.tab-header h2 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: #1e293b; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.tab-actions { + display: flex; + gap: 0.5rem; +} + +/* Content Markdown Area */ +.content-markdown { + line-height: 1.8; + color: #475569; +} + +.content-markdown h1 { + color: #1e293b; + font-size: 1.75rem; + margin-top: 2rem; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid #e2e8f0; +} + +.content-markdown h2 { + color: #1e293b; + font-size: 1.5rem; + margin-top: 1.5rem; + margin-bottom: 0.75rem; +} + +.content-markdown h3 { + color: #1e293b; + font-size: 1.25rem; + margin-top: 1.25rem; + margin-bottom: 0.5rem; +} + +.content-markdown p { + margin-bottom: 1rem; +} + +.content-markdown ul, +.content-markdown ol { + margin-left: 1.5rem; + margin-bottom: 1rem; +} + +.content-markdown li { + margin-bottom: 0.5rem; +} + +.content-markdown code { + background: #f1f5f9; + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-size: 0.9em; + font-family: 'Courier New', monospace; +} + +.content-markdown pre { + background: #1e293b; + color: #e2e8f0; + padding: 1rem; + border-radius: 8px; + overflow-x: auto; + margin-bottom: 1rem; +} + +.content-markdown pre code { + background: none; + padding: 0; + color: inherit; +} + +.content-markdown table { + width: 100%; + border-collapse: collapse; + margin-bottom: 1rem; +} + +.content-markdown th, +.content-markdown td { + border: 1px solid #e2e8f0; + padding: 0.5rem; + text-align: left; +} + +.content-markdown th { + background: #f8fafc; + font-weight: 600; +} + +.content-markdown blockquote { + border-left: 4px solid #667eea; + padding-left: 1rem; + margin-left: 0; + color: #64748b; + font-style: italic; +} + +/* Empty State */ +.empty-content { + display: flex; + align-items: center; + justify-content: center; + padding: 3rem; + color: #94a3b8; + font-size: 1rem; +} diff --git a/src/components/ContentViewer.jsx b/src/components/ContentViewer.jsx new file mode 100644 index 0000000..3a08b39 --- /dev/null +++ b/src/components/ContentViewer.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { Tabs } from 'antd'; +import { FileText, Brain } from 'lucide-react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeRaw from 'rehype-raw'; +import rehypeSanitize from 'rehype-sanitize'; +import MindMap from './MindMap'; +import './ContentViewer.css'; + +const { TabPane } = Tabs; + +/** + * ContentViewer - 纯展示组件,用于显示Markdown内容和脑图 + * + * 设计原则: + * 1. 组件只负责纯展示,不处理数据获取 + * 2. 父组件负责数据准备和导出功能 + * 3. 通过props传入已准备好的content + * + * @param {Object} props + * @param {string} props.content - Markdown格式的内容(必须由父组件准备好) + * @param {string} props.title - 标题(用于脑图显示) + * @param {string} props.emptyMessage - 内容为空时的提示消息 + * @param {React.ReactNode} props.summaryActions - 总结tab的额外操作按钮(如导出) + * @param {React.ReactNode} props.mindmapActions - 脑图tab的额外操作按钮(如导出) + */ +const ContentViewer = ({ + content, + title, + emptyMessage = '暂无内容', + summaryActions, + mindmapActions +}) => { + return ( +
+ + + 摘要 + + } + key="content" + > +
+

AI总结

+ {summaryActions &&
{summaryActions}
} +
+
+ {content ? ( + + {content} + + ) : ( +
{emptyMessage}
+ )} +
+
+ + + 脑图 + + } + key="mindmap" + > +
+

思维导图

+ {mindmapActions &&
{mindmapActions}
} +
+ {content ? ( + + ) : ( +
等待内容生成后查看脑图
+ )} +
+
+
+ ); +}; + +export default ContentViewer; diff --git a/src/components/MindMap.jsx b/src/components/MindMap.jsx index 197661d..a459e22 100644 --- a/src/components/MindMap.jsx +++ b/src/components/MindMap.jsx @@ -1,49 +1,37 @@ import React, { useState, useEffect, useRef } from 'react'; import { Transformer } from 'markmap-lib'; import { Markmap } from 'markmap-view'; -import apiClient from '../utils/apiClient'; -import { API_ENDPOINTS } from '../config/api'; -import { Brain, Image, Loader } from 'lucide-react'; -import html2canvas from 'html2canvas'; +import { Loader } from 'lucide-react'; -const MindMap = ({ meetingId, meetingTitle, meeting, formatDateTime }) => { +/** + * MindMap - 纯展示组件,用于渲染Markdown内容的思维导图 + * + * 设计原则: + * 1. 组件只负责渲染脑图,不处理数据获取 + * 2. 不包含导出功能,导出由父组件处理 + * 3. 通过props传入已准备好的content + * + * @param {Object} props + * @param {string} props.content - Markdown格式的内容(必须由父组件准备好) + * @param {string} props.title - 标题(用于显示) + */ +const MindMap = ({ content, title }) => { const [markdown, setMarkdown] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - const [hasSummary, setHasSummary] = useState(false); const svgRef = useRef(null); const markmapRef = useRef(null); useEffect(() => { - const fetchSummary = async () => { - try { - setLoading(true); - const endpoint = API_ENDPOINTS.MEETINGS.DETAIL(meetingId); - const response = await apiClient.get(endpoint); - const summary = response.data?.summary; - - if (summary) { - setMarkdown(summary); - setHasSummary(true); - } else { - setMarkdown('# 暂无会议总结\n\n请先生成AI总结,才能查看思维导图。'); - setHasSummary(false); - } - } catch (err) { - console.error('Failed to fetch summary for mind map:', err); - setError('无法加载会议总结内容。'); - setMarkdown('# 加载失败\n\n无法加载会议总结内容,请稍后重试。'); - setHasSummary(false); - } finally { - setLoading(false); - } - }; - - if (meetingId) { - fetchSummary(); + if (content) { + setMarkdown(content); + setLoading(false); + } else { + setMarkdown('# 暂无内容\n\n等待内容生成后查看思维导图。'); + setLoading(false); } - }, [meetingId]); + }, [content]); // 提取关键短语的函数 const extractKeyPhrases = (text) => { @@ -78,7 +66,7 @@ const MindMap = ({ meetingId, meetingTitle, meeting, formatDateTime }) => { }; // 预处理markdown,确保格式适合生成思维导图 - const preprocessMarkdownForMindMap = (markdown) => { + const preprocessMarkdownForMindMap = (markdown, rootTitle) => { if (!markdown || markdown.trim() === '') return '# 暂无内容'; let processed = markdown.trim(); @@ -86,17 +74,25 @@ const MindMap = ({ meetingId, meetingTitle, meeting, formatDateTime }) => { // 移除分隔线 processed = processed.replace(/^---+$/gm, ''); - // 如果没有主标题,添加一个 - if (!processed.startsWith('# ')) { - processed = `# 会议总结\n\n${processed}`; + // 检查是否有主标题,如果有就替换为rootTitle,如果没有就添加 + const lines = processed.split('\n'); + const firstLine = lines[0].trim(); + + if (firstLine.match(/^#\s+/)) { + // 如果第一行是主标题,替换为rootTitle + lines[0] = `# ${rootTitle || '内容总结'}`; + processed = lines.join('\n'); + } else { + // 如果没有主标题,添加一个 + processed = `# ${rootTitle || '内容总结'}\n\n${processed}`; } - const lines = processed.split('\n'); const processedLines = []; + const contentLines = processed.split('\n'); let i = 0; - while (i < lines.length) { - const line = lines[i].trim(); + while (i < contentLines.length) { + const line = contentLines[i].trim(); if (line === '') { i++; @@ -147,7 +143,7 @@ const MindMap = ({ meetingId, meetingTitle, meeting, formatDateTime }) => { if (loading || !markdown || !svgRef.current) return; try { - const processedMarkdown = preprocessMarkdownForMindMap(markdown); + const processedMarkdown = preprocessMarkdownForMindMap(markdown, title); console.log('=== 思维导图数据调试 ==='); console.log('原始markdown内容:'); console.log(markdown); @@ -177,135 +173,7 @@ const MindMap = ({ meetingId, meetingTitle, meeting, formatDateTime }) => { setError('思维导图渲染失败'); } - }, [markdown, loading]); - - const handleExportImage = async () => { - if (!svgRef.current || !hasSummary) { - alert('思维导图尚未渲染或无总结内容,无法导出。'); - return; - } - - try { - // 获取SVG元素 - const svgElement = svgRef.current; - - // 获取SVG的viewBox或实际内容边界 - const svgBBox = svgElement.querySelector('g')?.getBBox() || { width: 800, height: 600 }; - const svgRect = svgElement.getBoundingClientRect(); - - // 使用更保守的尺寸计算,确保内容完整 - const contentWidth = Math.max(svgBBox.width, svgRect.width, 800); - const contentHeight = Math.max(svgBBox.height, svgRect.height, 400); - - // 添加足够的边距 - const containerWidth = contentWidth + 200; // 左右各100px边距 - const containerHeight = contentHeight + 200; // 上下各100px边距 - - // 创建导出容器 - const exportContainer = document.createElement('div'); - exportContainer.style.cssText = ` - position: fixed; - top: -10000px; - left: -10000px; - width: ${containerWidth}px; - background: white; - padding: 40px; - font-family: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", sans-serif; - `; - - // 克隆SVG元素 - const svgClone = svgElement.cloneNode(true); - svgClone.style.width = `${contentWidth}px`; - svgClone.style.height = `${contentHeight}px`; - svgClone.style.display = 'block'; - - exportContainer.innerHTML = ` -
-

- ${meetingTitle || '会议思维导图'} -

- -
-

- 📋 会议信息 -

- ${meeting ? ` -

会议时间:${formatDateTime ? formatDateTime(meeting.meeting_time) : new Date(meeting.meeting_time).toLocaleString('zh-CN')}

-

创建人:${meeting.creator_username}

-

参会人数:${meeting.attendees.length}人

-

参会人员:${meeting.attendees.map(attendee => - typeof attendee === 'string' ? attendee : attendee.caption - ).join('、')}

- ` : ` -

会议信息加载中...

- `} -
- -
-

- 🧠 思维导图 -

-
-
-
- -
- 导出时间:${new Date().toLocaleString('zh-CN')} -
-
- `; - - // 添加SVG到容器 - const mapContainer = exportContainer.querySelector('#mindmap-wrapper'); - mapContainer.appendChild(svgClone); - - document.body.appendChild(exportContainer); - - // 等待渲染完成 - await new Promise(resolve => setTimeout(resolve, 500)); - - // 使用html2canvas生成图片 - const canvas = await html2canvas(exportContainer, { - width: containerWidth + 80, // 加上padding - height: exportContainer.offsetHeight, - scale: 2, // 高分辨率 - useCORS: true, - allowTaint: true, - backgroundColor: '#ffffff', - scrollX: 0, - scrollY: 0, - logging: false - }); - - // 创建下载链接 - const link = document.createElement('a'); - link.download = `${meetingTitle || '思维导图'}_脑图_${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 - document.body.removeChild(exportContainer); - - } catch (error) { - console.error('图片导出失败:', error); - alert('图片导出失败,请重试。'); - } - }; + }, [markdown, loading, title]); if (loading) { return ( @@ -316,17 +184,16 @@ const MindMap = ({ meetingId, meetingTitle, meeting, formatDateTime }) => { ); } + if (error) { + return ( +
+

{error}

+
+ ); + } + return (
-
-

思维导图

- {hasSummary && ( - - )} -
diff --git a/src/components/knowledgebase/PersonalKnowledgeBase.jsx b/src/components/knowledgebase/PersonalKnowledgeBase.jsx deleted file mode 100644 index 60b5568..0000000 --- a/src/components/knowledgebase/PersonalKnowledgeBase.jsx +++ /dev/null @@ -1,369 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import apiClient from '../../utils/apiClient'; -import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; -import { Plus, X, Eye, Trash2, Calendar, Database, Search } from 'lucide-react'; -import { Link } from 'react-router-dom'; - -const PersonalKnowledgeBase = ({ user }) => { - const [kbs, setKbs] = useState([]); - const [loading, setLoading] = useState(true); - const [showModal, setShowModal] = 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 [selectedKb, setSelectedKb] = useState(null); - const [showDetailModal, setShowDetailModal] = useState(false); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const [deletingKb, setDeletingKb] = useState(null); - - useEffect(() => { - fetchPersonalKbs(); - 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); - // Reset form - setUserPrompt(''); - setSelectedMeetings([]); - setShowModal(false); - // Refresh the list - fetchPersonalKbs(); - } 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); // 缩短轮询间隔到2秒 - return () => clearInterval(interval); - } - }, [taskId]); - - const fetchPersonalKbs = () => { - setLoading(true); - apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.LIST, { is_shared: false })) - .then(response => { - setKbs(response.data.kbs); - setLoading(false); - }) - .catch(error => { - console.error("Error fetching personal knowledge bases:", error); - setLoading(false); - }); - }; - - const fetchTags = () => { - // Tags functionality removed - replaced with search - }; - - 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 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 (kbId) => { - setDeletingKb(kbs.find(kb => kb.kb_id === kbId)); - setShowDeleteConfirm(true); - }; - - const confirmDelete = async () => { - try { - await apiClient.delete(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DELETE(deletingKb.kb_id))); - setShowDeleteConfirm(false); - setDeletingKb(null); - fetchPersonalKbs(); // Refresh the list - } catch (error) { - console.error("Error deleting knowledge base:", error); - alert('删除失败,请稍后重试'); - setShowDeleteConfirm(false); - setDeletingKb(null); - } - }; - - const handleViewDetail = async (kbId) => { - try { - const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kbId))); - setSelectedKb(response.data); - setShowDetailModal(true); - } catch (error) { - console.error("Error fetching knowledge base detail:", error); - alert('获取详情失败,请稍后重试'); - } - }; - - 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' - }); - }; - - if (loading) { - return
Loading...
; - } - - return ( -
-
- {kbs.length === 0 ? ( -
-

暂无知识库条目

-
- ) : ( - kbs.map(kb => ( -
-
-

{kb.title}

-
- - -
-
-
-
- - - {formatDate(kb.created_at)} - - - - {kb.source_meeting_count || 0} 个数据源 - -
- {kb.user_prompt && ( -
- 提示词: {kb.user_prompt} -
- )} -
- {kb.content ? kb.content.substring(0, 200) + '...' : '内容生成中...'} -
-
-
- )) - )} -
- -
-
-
-
- -