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'; const MindMap = ({ meetingId, meetingTitle, meeting, formatDateTime }) => { 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(); } }, [meetingId]); // 提取关键短语的函数 const extractKeyPhrases = (text) => { // 移除markdown格式 const cleanText = text.replace(/\*\*([^*]+)\*\*/g, '$1'); // 按标点符号分割 const phrases = cleanText.split(/[,。;、:]/); const keyPhrases = []; phrases.forEach(phrase => { const trimmed = phrase.trim(); // 只保留包含重要关键词的短语,且长度适中 if (trimmed.length > 4 && trimmed.length < 40) { const hasKeywords = /(?:项目|收入|问题|产品|团队|开发|验收|成本|功能|市场|合作|资源|计划|目标|业务|投入|效率|协作|管理|分析|讨论|决策|优化|整合)/.test(trimmed); if (hasKeywords) { keyPhrases.push(trimmed); } } }); // 如果没有找到关键短语,至少保留一个总结性的短语 if (keyPhrases.length === 0 && phrases.length > 0) { const firstPhrase = phrases[0].trim(); if (firstPhrase.length > 0 && firstPhrase.length < 50) { keyPhrases.push(firstPhrase); } } // 最多返回3个关键短语 return keyPhrases.slice(0, 3); }; // 预处理markdown,确保格式适合生成思维导图 const preprocessMarkdownForMindMap = (markdown) => { if (!markdown || markdown.trim() === '') return '# 暂无内容'; let processed = markdown.trim(); // 移除分隔线 processed = processed.replace(/^---+$/gm, ''); // 如果没有主标题,添加一个 if (!processed.startsWith('# ')) { processed = `# 会议总结\n\n${processed}`; } const lines = processed.split('\n'); const processedLines = []; let i = 0; while (i < lines.length) { const line = lines[i].trim(); if (line === '') { i++; continue; } // 处理标题行 if (line.match(/^#+\s+/)) { // 清理标题格式,移除粗体和多余符号 let cleanTitle = line.replace(/\*\*([^*]+)\*\*/g, '$1'); // 移除粗体 cleanTitle = cleanTitle.replace(/[*_]/g, ''); // 移除其他markdown符号 processedLines.push(cleanTitle); // 查看下一个非空行 let j = i + 1; while (j < lines.length && lines[j].trim() === '') { j++; } // 如果下一行不是标题、列表或表格,将段落内容转换为列表项 if (j < lines.length) { const nextLine = lines[j].trim(); if (!nextLine.match(/^#+\s+/) && !nextLine.match(/^[-*+]\s+/) && !nextLine.includes('|') && nextLine.length > 0) { // 收集段落内容直到下一个标题、列表或表格 const paragraphLines = []; while (j < lines.length) { const currentLine = lines[j].trim(); if (currentLine === '') { j++; continue; } if (currentLine.match(/^#+\s+/) || currentLine.match(/^[-*+]\s+/) || currentLine.includes('|')) { break; } paragraphLines.push(currentLine); j++; } // 将段落内容转换为列表项,只保留重点内容 if (paragraphLines.length > 0) { const paragraphText = paragraphLines.join(' '); // 提取加粗内容作为重点 const boldMatches = paragraphText.match(/\*\*([^*]+)\*\*/g); if (boldMatches && boldMatches.length > 0) { // 只保留加粗的重点内容 boldMatches.forEach(match => { const cleanText = match.replace(/\*\*/g, '').trim(); if (cleanText.length > 0 && cleanText.length < 50) { // 避免过长的内容 processedLines.push(`- ${cleanText}`); } }); } else { // 如果没有加粗内容,提取关键词汇或短句 const keyPhrases = extractKeyPhrases(paragraphText); keyPhrases.forEach(phrase => { if (phrase.length > 0) { processedLines.push(`- ${phrase}`); } }); } } i = j - 1; // j will be incremented at the end of the loop } } } // 处理列表项 else if (line.match(/^[-*+]\s+/)) { const cleanListItem = line.replace(/\*\*([^*]+)\*\*/g, '$1'); // 移除粗体 processedLines.push(cleanListItem); } // 保持表格原样,让markmap自己处理 else if (line.includes('|')) { processedLines.push(line); } // 处理其他普通段落(如果前面没有被处理) else if (line.length > 0 && !line.match(/\*\*总字数:\d+字\*\*/)) { processedLines.push(`- ${line}`); } i++; } // 清理结果 const result = processedLines .filter(line => line.trim().length > 0) .join('\n'); return result; }; useEffect(() => { if (loading || !markdown || !svgRef.current) return; try { const processedMarkdown = preprocessMarkdownForMindMap(markdown); console.log('原始markdown内容:', markdown); console.log('预处理后的markdown:', processedMarkdown); const transformer = new Transformer(); const { root } = transformer.transform(processedMarkdown); console.log('转换后的思维导图数据:', root); if (markmapRef.current) { markmapRef.current.setData(root); } else { markmapRef.current = Markmap.create(svgRef.current, null, root); } markmapRef.current.fit(); // 延迟一下再次调用fit,确保内容完全渲染 setTimeout(() => { if (markmapRef.current) { markmapRef.current.fit(); } }, 500); } catch (error) { console.error('思维导图渲染失败:', error); 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 = `
会议时间:${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('、')}
` : `会议信息加载中...
`}正在加载思维导图...