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'); // 移除粗体 processedLines.push(cleanTitle); // 获取标题级别 const titleLevel = (line.match(/^#+/) || [''])[0].length; } // 处理现有列表项(有序和无序) - 保持其原始结构 else if (line.match(/^\s*([-*+]|\d+\.)\s+/)) { // 只移除加粗格式,保留原始行,包括缩进和列表标记 const cleanedLine = line.replace(/\*\*([^*]+)\*\*/g, '$1'); processedLines.push(cleanedLine); } // 将区块引用转换为列表项 else if (line.startsWith('>')) { const content = line.replace(/^>+\s*/, ''); processedLines.push(`- ${content}`); } // 保持表格原样,让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('=== 思维导图数据调试 ==='); console.log('原始markdown内容:'); console.log(markdown); console.log('预处理后的markdown:'); console.log(processedMarkdown); const transformer = new Transformer(); const { root } = transformer.transform(processedMarkdown); 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('、')}
` : `会议信息加载中...
`}正在加载思维导图...