加入脑图功能
parent
8ebaddc866
commit
6b329adce8
|
|
@ -11,9 +11,14 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uiw/react-md-editor": "^4.0.8",
|
"@uiw/react-md-editor": "^4.0.8",
|
||||||
|
"antd": "^5.27.3",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"canvg": "^4.0.3",
|
||||||
"jspdf": "^3.0.2",
|
"jspdf": "^3.0.2",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
|
"markmap-common": "^0.18.9",
|
||||||
|
"markmap-lib": "^0.18.12",
|
||||||
|
"markmap-view": "^0.18.12",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FileText, Download, Brain } from 'lucide-react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
import rehypeSanitize from 'rehype-sanitize';
|
||||||
|
|
||||||
|
const MeetingSummary = ({
|
||||||
|
meeting,
|
||||||
|
summaryResult,
|
||||||
|
summaryHistory,
|
||||||
|
isCreator,
|
||||||
|
onOpenSummaryModal,
|
||||||
|
formatDateTime
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const exportToPDF = async () => {
|
||||||
|
try {
|
||||||
|
// 检查是否有总结内容
|
||||||
|
let summaryContent = summaryResult?.content ||
|
||||||
|
meeting?.summary ||
|
||||||
|
(summaryHistory.length > 0 ? summaryHistory[0].content : null);
|
||||||
|
|
||||||
|
if (!summaryContent) {
|
||||||
|
alert('暂无会议总结内容,请先生成AI总结。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先渲染Markdown内容
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.style.display = 'none';
|
||||||
|
document.body.appendChild(tempDiv);
|
||||||
|
|
||||||
|
const ReactMarkdown = (await import('react-markdown')).default;
|
||||||
|
const { createRoot } = await import('react-dom/client');
|
||||||
|
|
||||||
|
const root = createRoot(tempDiv);
|
||||||
|
|
||||||
|
// 渲染Markdown内容并获取HTML
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(ReactMarkdown, {
|
||||||
|
remarkPlugins: [remarkGfm],
|
||||||
|
rehypePlugins: [rehypeRaw, rehypeSanitize],
|
||||||
|
children: summaryContent
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderedHTML = tempDiv.innerHTML;
|
||||||
|
|
||||||
|
// 创建会议信息
|
||||||
|
const meetingTime = formatDateTime(meeting.meeting_time);
|
||||||
|
const attendeesList = meeting.attendees.map(attendee =>
|
||||||
|
typeof attendee === 'string' ? attendee : attendee.caption
|
||||||
|
).join('、');
|
||||||
|
|
||||||
|
// 创建一个隐藏的iframe用于打印
|
||||||
|
const printFrame = document.createElement('iframe');
|
||||||
|
printFrame.style.position = 'fixed';
|
||||||
|
printFrame.style.width = '0';
|
||||||
|
printFrame.style.height = '0';
|
||||||
|
printFrame.style.border = 'none';
|
||||||
|
printFrame.style.left = '-9999px';
|
||||||
|
document.body.appendChild(printFrame);
|
||||||
|
|
||||||
|
// 使用Blob和URL来确保编码正确
|
||||||
|
const htmlContent = `<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<title>${meeting.title || '会议总结'}</title>
|
||||||
|
<style>
|
||||||
|
@charset "UTF-8";
|
||||||
|
@page { size: A4; margin: 20mm; }
|
||||||
|
body {
|
||||||
|
font-family: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #374151;
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 25px 0 15px;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 10px 0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
ul, ol {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 25px;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin: 5px 0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
strong {
|
||||||
|
color: #1e293b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.info-section {
|
||||||
|
background: #f9fafb;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.info-section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.content-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.footer-section {
|
||||||
|
margin-top: 50px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
body { padding: 0; }
|
||||||
|
h1 { page-break-before: avoid; }
|
||||||
|
h2 { page-break-before: avoid; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>${meeting.title || '会议总结'}</h1>
|
||||||
|
<div class="info-section">
|
||||||
|
<h2>会议信息</h2>
|
||||||
|
<p><strong>会议时间:</strong>${meetingTime}</p>
|
||||||
|
<p><strong>创建人:</strong>${meeting.creator_username}</p>
|
||||||
|
<p><strong>参会人数:</strong>${meeting.attendees.length}人</p>
|
||||||
|
<p><strong>参会人员:</strong>${attendeesList}</p>
|
||||||
|
</div>
|
||||||
|
<div class="content-section">
|
||||||
|
<h2>会议摘要</h2>
|
||||||
|
${renderedHTML}
|
||||||
|
</div>
|
||||||
|
<div class="footer-section">
|
||||||
|
<p>导出时间:${new Date().toLocaleString('zh-CN')}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
// 使用Blob创建URL以确保正确的编码
|
||||||
|
const blob = new Blob([htmlContent], { type: 'text/html; charset=UTF-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// 设置iframe的src为blob URL
|
||||||
|
printFrame.src = url;
|
||||||
|
|
||||||
|
// 等待iframe加载完成
|
||||||
|
printFrame.onload = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// 执行打印
|
||||||
|
printFrame.contentWindow.focus();
|
||||||
|
printFrame.contentWindow.print();
|
||||||
|
|
||||||
|
// 清理资源
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
root.unmount();
|
||||||
|
document.body.removeChild(tempDiv);
|
||||||
|
document.body.removeChild(printFrame);
|
||||||
|
}, 2000);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PDF导出失败:', error);
|
||||||
|
alert('PDF导出失败,请重试。');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="meeting-summary">
|
||||||
|
<div className="summary-header">
|
||||||
|
<h2><FileText size={20} /> 摘要内容</h2>
|
||||||
|
{meeting?.summary && (
|
||||||
|
<button
|
||||||
|
className="export-pdf-btn-main"
|
||||||
|
onClick={exportToPDF}
|
||||||
|
title="导出PDF"
|
||||||
|
>
|
||||||
|
<Download size={16} />
|
||||||
|
<span>导出PDF</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="summary-content">
|
||||||
|
{meeting?.summary ? (
|
||||||
|
<div className="markdown-content">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
||||||
|
>
|
||||||
|
{meeting.summary}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="no-summary">
|
||||||
|
<div className="no-summary-content">
|
||||||
|
<FileText size={48} />
|
||||||
|
<h3>暂无会议总结</h3>
|
||||||
|
<p>该会议尚未生成总结内容</p>
|
||||||
|
{isCreator && (
|
||||||
|
<button
|
||||||
|
className="generate-summary-cta"
|
||||||
|
onClick={onOpenSummaryModal}
|
||||||
|
>
|
||||||
|
<Brain size={16} />
|
||||||
|
<span>生成AI总结</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MeetingSummary;
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
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, Download, Loader } from 'lucide-react';
|
||||||
|
|
||||||
|
const MindMap = ({ meetingId, meetingTitle }) => {
|
||||||
|
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]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading || !markdown || !svgRef.current) return;
|
||||||
|
|
||||||
|
const transformer = new Transformer();
|
||||||
|
const { root } = transformer.transform(markdown);
|
||||||
|
|
||||||
|
if (markmapRef.current) {
|
||||||
|
markmapRef.current.setData(root);
|
||||||
|
} else {
|
||||||
|
markmapRef.current = Markmap.create(svgRef.current, null, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
markmapRef.current.fit();
|
||||||
|
|
||||||
|
}, [markdown, loading]);
|
||||||
|
|
||||||
|
const handleExportPDF = async () => {
|
||||||
|
if (!svgRef.current) {
|
||||||
|
alert('思维导图尚未渲染,无法导出。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取SVG元素
|
||||||
|
const svgElement = svgRef.current;
|
||||||
|
const svgHTML = svgElement.outerHTML;
|
||||||
|
|
||||||
|
if (!svgHTML.trim()) {
|
||||||
|
alert('思维导图内容为空,无法导出。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取SVG的尺寸
|
||||||
|
const svgRect = svgElement.getBoundingClientRect();
|
||||||
|
const svgWidth = svgRect.width || 800;
|
||||||
|
const svgHeight = svgRect.height || 600;
|
||||||
|
|
||||||
|
// 创建一个隐藏的iframe用于打印
|
||||||
|
const printFrame = document.createElement('iframe');
|
||||||
|
printFrame.style.position = 'fixed';
|
||||||
|
printFrame.style.width = '0';
|
||||||
|
printFrame.style.height = '0';
|
||||||
|
printFrame.style.border = 'none';
|
||||||
|
printFrame.style.left = '-9999px';
|
||||||
|
document.body.appendChild(printFrame);
|
||||||
|
|
||||||
|
// 创建HTML内容
|
||||||
|
const htmlContent = `<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<title>${meetingTitle || '会议思维导图'}</title>
|
||||||
|
<style>
|
||||||
|
@charset "UTF-8";
|
||||||
|
@page {
|
||||||
|
size: A4 landscape;
|
||||||
|
margin: 15mm;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 90vh;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.mindmap-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
/* 脑图节点样式 */
|
||||||
|
.markmap-node circle {
|
||||||
|
fill: #3b82f6;
|
||||||
|
stroke: #1d4ed8;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.markmap-node text {
|
||||||
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
fill: #1e293b;
|
||||||
|
}
|
||||||
|
.markmap-link {
|
||||||
|
stroke: #6b7280;
|
||||||
|
stroke-width: 2;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
.footer-section {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
body { padding: 0; }
|
||||||
|
h1 { page-break-before: avoid; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>${meetingTitle || '会议思维导图'}</h1>
|
||||||
|
<div class="mindmap-container">
|
||||||
|
${svgHTML}
|
||||||
|
</div>
|
||||||
|
<div class="footer-section">
|
||||||
|
<p>导出时间:${new Date().toLocaleString('zh-CN')}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
// 使用Blob创建URL以确保正确的编码
|
||||||
|
const blob = new Blob([htmlContent], { type: 'text/html; charset=UTF-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// 设置iframe的src为blob URL
|
||||||
|
printFrame.src = url;
|
||||||
|
|
||||||
|
// 等待iframe加载完成
|
||||||
|
printFrame.onload = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
// 执行打印
|
||||||
|
printFrame.contentWindow.focus();
|
||||||
|
printFrame.contentWindow.print();
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Print failed:", e);
|
||||||
|
alert("导出PDF失败,您的浏览器可能阻止了打印操作。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理资源
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(printFrame);
|
||||||
|
}, 2000);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PDF导出失败:', error);
|
||||||
|
alert('PDF导出失败,请重试。');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="mindmap-loading">
|
||||||
|
<Loader className="animate-spin" />
|
||||||
|
<p>正在加载思维导图...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mindmap-container">
|
||||||
|
<div className="mindmap-header">
|
||||||
|
<h3><Brain size={18} /> 思维导图</h3>
|
||||||
|
{hasSummary && (
|
||||||
|
<button onClick={handleExportPDF} className="export-pdf-btn-main" disabled={loading || !!error}>
|
||||||
|
<Download size={16} />
|
||||||
|
<span>导出PDF</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="markmap-render-area">
|
||||||
|
<svg ref={svgRef} style={{ width: '100%', height: '100%' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MindMap;
|
||||||
15
src/main.jsx
15
src/main.jsx
|
|
@ -1,10 +1,13 @@
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client';
|
||||||
import './index.css'
|
import { StyleProvider } from '@ant-design/cssinjs';
|
||||||
import App from './App.jsx'
|
import './index.css';
|
||||||
|
import App from './App.jsx';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<StyleProvider hashPriority="high">
|
||||||
|
<App />
|
||||||
|
</StyleProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
);
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabs */
|
/* Old Tabs styles - can be removed or kept for reference */
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
|
@ -109,6 +109,41 @@
|
||||||
min-height: 60vh;
|
min-height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* New AntD Tabs Styles */
|
||||||
|
.admin-tabs .ant-tabs-nav {
|
||||||
|
padding: 0 2rem;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tabs .ant-tabs-tab {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #475569;
|
||||||
|
padding: 16px 4px;
|
||||||
|
margin: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tabs .ant-tabs-tab .ant-tabs-tab-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tabs .ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tabs .ant-tabs-ink-bar {
|
||||||
|
background: #667eea;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tabs .ant-tabs-content-holder {
|
||||||
|
padding: 2rem;
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.admin-content {
|
.admin-content {
|
||||||
|
|
@ -124,7 +159,11 @@
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content, .admin-tabs .ant-tabs-content-holder {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.admin-tabs .ant-tabs-nav {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { MessageSquare, Settings, Users } from 'lucide-react';
|
import { MessageSquare, Settings, Users } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Tabs } from 'antd';
|
||||||
import UserManagement from '../components/admin/UserManagement';
|
import UserManagement from '../components/admin/UserManagement';
|
||||||
import SystemConfiguration from '../components/admin/SystemConfiguration';
|
import SystemConfiguration from '../components/admin/SystemConfiguration';
|
||||||
import './AdminManagement.css';
|
import './AdminManagement.css';
|
||||||
|
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
const AdminManagement = () => {
|
const AdminManagement = () => {
|
||||||
const [activeTab, setActiveTab] = useState('userManagement');
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleLogoClick = () => {
|
const handleLogoClick = () => {
|
||||||
|
|
@ -26,30 +28,25 @@ const AdminManagement = () => {
|
||||||
</header>
|
</header>
|
||||||
<div className="admin-content">
|
<div className="admin-content">
|
||||||
<div className="admin-wrapper">
|
<div className="admin-wrapper">
|
||||||
<div className="tabs">
|
<Tabs defaultActiveKey="userManagement" className="admin-tabs">
|
||||||
<button
|
<TabPane
|
||||||
className={`tab-btn ${activeTab === 'userManagement' ? 'active' : ''}`}
|
tab={<span><Users size={16} /> 用户管理</span>}
|
||||||
onClick={() => setActiveTab('userManagement')}
|
key="userManagement"
|
||||||
>
|
>
|
||||||
<Users size={18} style={{ marginRight: '0.5rem' }} />
|
<UserManagement />
|
||||||
用户管理
|
</TabPane>
|
||||||
</button>
|
<TabPane
|
||||||
<button
|
tab={<span><Settings size={16} /> 系统配置</span>}
|
||||||
className={`tab-btn ${activeTab === 'systemConfiguration' ? 'active' : ''}`}
|
key="systemConfiguration"
|
||||||
onClick={() => setActiveTab('systemConfiguration')}
|
|
||||||
>
|
>
|
||||||
<Settings size={18} style={{ marginRight: '0.5rem' }} />
|
<SystemConfiguration />
|
||||||
系统配置
|
</TabPane>
|
||||||
</button>
|
</Tabs>
|
||||||
</div>
|
|
||||||
<div className="tab-content">
|
|
||||||
{activeTab === 'userManagement' && <UserManagement />}
|
|
||||||
{activeTab === 'systemConfiguration' && <SystemConfiguration />}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default AdminManagement;
|
export default AdminManagement;
|
||||||
|
|
@ -1817,3 +1817,80 @@
|
||||||
transform: none;
|
transform: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* New styles for Tabs and MindMap */
|
||||||
|
.summary-tabs-section .ant-tabs-nav {
|
||||||
|
margin-bottom: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-tabs-section .ant-tabs-tab {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #475569;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-tabs-section .ant-tabs-tab .ant-tabs-tab-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-tabs-section .ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-tabs-section .ant-tabs-ink-bar {
|
||||||
|
background: #667eea;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #334155;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markmap-render-area {
|
||||||
|
height: 600px;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markmap-render-area svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 600px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-loading .animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,13 @@ import remarkGfm from 'remark-gfm';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
import rehypeSanitize from 'rehype-sanitize';
|
import rehypeSanitize from 'rehype-sanitize';
|
||||||
import { buildApiUrl, API_ENDPOINTS, API_BASE_URL } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS, API_BASE_URL } from '../config/api';
|
||||||
|
import MindMap from '../components/MindMap';
|
||||||
|
import MeetingSummary from '../components/MeetingSummary';
|
||||||
|
import { Tabs } from 'antd';
|
||||||
import './MeetingDetails.css';
|
import './MeetingDetails.css';
|
||||||
|
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
const MeetingDetails = ({ user }) => {
|
const MeetingDetails = ({ user }) => {
|
||||||
const { meeting_id } = useParams();
|
const { meeting_id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -24,6 +29,7 @@ const MeetingDetails = ({ user }) => {
|
||||||
const [audioUrl, setAudioUrl] = useState(null);
|
const [audioUrl, setAudioUrl] = useState(null);
|
||||||
const [audioFileName, setAudioFileName] = useState(null);
|
const [audioFileName, setAudioFileName] = useState(null);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [showSummaryError, setShowSummaryError] = useState(false);
|
||||||
const [showSpeakerEdit, setShowSpeakerEdit] = useState(false);
|
const [showSpeakerEdit, setShowSpeakerEdit] = useState(false);
|
||||||
const [editingSpeakers, setEditingSpeakers] = useState({});
|
const [editingSpeakers, setEditingSpeakers] = useState({});
|
||||||
const [speakerList, setSpeakerList] = useState([]);
|
const [speakerList, setSpeakerList] = useState([]);
|
||||||
|
|
@ -547,8 +553,6 @@ const MeetingDetails = ({ user }) => {
|
||||||
summary: response.data.summary
|
summary: response.data.summary
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log('会议摘要已更新,无loading状态');
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('刷新会议摘要失败:', error);
|
console.error('刷新会议摘要失败:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -625,9 +629,14 @@ const MeetingDetails = ({ user }) => {
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error starting summary generation:', err);
|
console.error('Error starting summary generation:', err);
|
||||||
setError('启动AI总结失败,请重试');
|
// Check for detailed error message from backend
|
||||||
|
const detail = err.response?.data?.detail;
|
||||||
|
const errorMessage = detail || '启动AI总结失败,请重试。';
|
||||||
|
|
||||||
|
setError(errorMessage); // Set the more specific error
|
||||||
|
setSummaryTaskMessage(`生成失败:${errorMessage}`); // Also show it in the modal
|
||||||
|
|
||||||
setSummaryLoading(false);
|
setSummaryLoading(false);
|
||||||
setSummaryTaskMessage('');
|
|
||||||
setSummaryTaskProgress(0);
|
setSummaryTaskProgress(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -699,6 +708,12 @@ const MeetingDetails = ({ user }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const openSummaryModal = async () => {
|
const openSummaryModal = async () => {
|
||||||
|
// Frontend check before opening the modal
|
||||||
|
if (!transcriptionStatus || transcriptionStatus.status !== 'completed') {
|
||||||
|
setShowSummaryError(true);
|
||||||
|
return; // Prevent modal from opening
|
||||||
|
}
|
||||||
|
|
||||||
setShowSummaryModal(true);
|
setShowSummaryModal(true);
|
||||||
setUserPrompt('');
|
setUserPrompt('');
|
||||||
setSummaryResult(null);
|
setSummaryResult(null);
|
||||||
|
|
@ -712,186 +727,7 @@ const MeetingDetails = ({ user }) => {
|
||||||
await refreshMeetingSummary();
|
await refreshMeetingSummary();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportToPDF = async () => {
|
|
||||||
try {
|
|
||||||
// 检查是否有总结内容
|
|
||||||
let summaryContent = summaryResult?.content ||
|
|
||||||
meeting?.summary ||
|
|
||||||
(summaryHistory.length > 0 ? summaryHistory[0].content : null);
|
|
||||||
|
|
||||||
if (!summaryContent) {
|
|
||||||
alert('暂无会议总结内容,请先生成AI总结。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先渲染Markdown内容
|
|
||||||
const tempDiv = document.createElement('div');
|
|
||||||
tempDiv.style.display = 'none';
|
|
||||||
document.body.appendChild(tempDiv);
|
|
||||||
|
|
||||||
const ReactMarkdown = (await import('react-markdown')).default;
|
|
||||||
const { createRoot } = await import('react-dom/client');
|
|
||||||
|
|
||||||
const root = createRoot(tempDiv);
|
|
||||||
|
|
||||||
// 渲染Markdown内容并获取HTML
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
root.render(
|
|
||||||
React.createElement(ReactMarkdown, {
|
|
||||||
remarkPlugins: [remarkGfm],
|
|
||||||
rehypePlugins: [rehypeRaw, rehypeSanitize],
|
|
||||||
children: summaryContent
|
|
||||||
})
|
|
||||||
);
|
|
||||||
setTimeout(resolve, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderedHTML = tempDiv.innerHTML;
|
|
||||||
|
|
||||||
// 创建会议信息
|
|
||||||
const meetingTime = formatDateTime(meeting.meeting_time);
|
|
||||||
const attendeesList = meeting.attendees.map(attendee =>
|
|
||||||
typeof attendee === 'string' ? attendee : attendee.caption
|
|
||||||
).join('、');
|
|
||||||
|
|
||||||
// 创建一个隐藏的iframe用于打印
|
|
||||||
const printFrame = document.createElement('iframe');
|
|
||||||
printFrame.style.position = 'fixed';
|
|
||||||
printFrame.style.width = '0';
|
|
||||||
printFrame.style.height = '0';
|
|
||||||
printFrame.style.border = 'none';
|
|
||||||
printFrame.style.left = '-9999px';
|
|
||||||
document.body.appendChild(printFrame);
|
|
||||||
|
|
||||||
const printDocument = printFrame.contentWindow.document;
|
|
||||||
|
|
||||||
// 使用Blob和URL来确保编码正确
|
|
||||||
const htmlContent = `<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<title>${meeting.title || '会议总结'}</title>
|
|
||||||
<style>
|
|
||||||
@charset "UTF-8";
|
|
||||||
@page { size: A4; margin: 20mm; }
|
|
||||||
body {
|
|
||||||
font-family: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #2563eb;
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
border-bottom: 2px solid #e5e7eb;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
color: #374151;
|
|
||||||
font-size: 18px;
|
|
||||||
margin: 25px 0 15px;
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
color: #1e293b;
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 20px 0 10px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 10px 0;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
ul, ol {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding-left: 25px;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
margin: 5px 0;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
strong {
|
|
||||||
color: #1e293b;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.info-section {
|
|
||||||
background: #f9fafb;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
.info-section h2 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
.content-section {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.footer-section {
|
|
||||||
margin-top: 50px;
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
@media print {
|
|
||||||
body { padding: 0; }
|
|
||||||
h1 { page-break-before: avoid; }
|
|
||||||
h2 { page-break-before: avoid; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>${meeting.title || '会议总结'}</h1>
|
|
||||||
<div class="info-section">
|
|
||||||
<h2>会议信息</h2>
|
|
||||||
<p><strong>会议时间:</strong>${meetingTime}</p>
|
|
||||||
<p><strong>创建人:</strong>${meeting.creator_username}</p>
|
|
||||||
<p><strong>参会人数:</strong>${meeting.attendees.length}人</p>
|
|
||||||
<p><strong>参会人员:</strong>${attendeesList}</p>
|
|
||||||
</div>
|
|
||||||
<div class="content-section">
|
|
||||||
<h2>会议摘要</h2>
|
|
||||||
${renderedHTML}
|
|
||||||
</div>
|
|
||||||
<div class="footer-section">
|
|
||||||
<p>导出时间:${new Date().toLocaleString('zh-CN')}</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
|
|
||||||
// 使用Blob创建URL以确保正确的编码
|
|
||||||
const blob = new Blob([htmlContent], { type: 'text/html; charset=UTF-8' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
// 设置iframe的src为blob URL
|
|
||||||
printFrame.src = url;
|
|
||||||
|
|
||||||
// 等待iframe加载完成
|
|
||||||
printFrame.onload = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
// 执行打印
|
|
||||||
printFrame.contentWindow.focus();
|
|
||||||
printFrame.contentWindow.print();
|
|
||||||
|
|
||||||
// 清理资源
|
|
||||||
setTimeout(() => {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
root.unmount();
|
|
||||||
document.body.removeChild(tempDiv);
|
|
||||||
document.body.removeChild(printFrame);
|
|
||||||
}, 2000);
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('PDF导出失败:', error);
|
|
||||||
alert('PDF导出失败,请重试。');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCreator = meeting && user && String(meeting.creator_id) === String(user.user_id);
|
const isCreator = meeting && user && String(meeting.creator_id) === String(user.user_id);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|
@ -1110,49 +946,22 @@ ${renderedHTML}
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="card-section">
|
<section className="card-section summary-tabs-section">
|
||||||
<div className="summary-header">
|
<Tabs defaultActiveKey="1">
|
||||||
<h2><FileText size={20} /> 会议摘要</h2>
|
<TabPane tab={<span><FileText size={16} /> 会议总结</span>} key="1">
|
||||||
{meeting?.summary && (
|
<MeetingSummary
|
||||||
<button
|
meeting={meeting}
|
||||||
className="export-pdf-btn-main"
|
summaryResult={summaryResult}
|
||||||
onClick={exportToPDF}
|
summaryHistory={summaryHistory}
|
||||||
title="导出PDF"
|
isCreator={isCreator}
|
||||||
>
|
onOpenSummaryModal={openSummaryModal}
|
||||||
<Download size={16} />
|
formatDateTime={formatDateTime}
|
||||||
<span>导出PDF</span>
|
/>
|
||||||
</button>
|
</TabPane>
|
||||||
)}
|
<TabPane tab={<span><Brain size={16} /> 会议脑图</span>} key="2">
|
||||||
</div>
|
<MindMap meetingId={meeting_id} meetingTitle={meeting.title} />
|
||||||
<div className="summary-content">
|
</TabPane>
|
||||||
{meeting?.summary ? (
|
</Tabs>
|
||||||
<div className="markdown-content">
|
|
||||||
<ReactMarkdown
|
|
||||||
remarkPlugins={[remarkGfm]}
|
|
||||||
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
|
||||||
>
|
|
||||||
{meeting.summary}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="no-summary">
|
|
||||||
<div className="no-summary-content">
|
|
||||||
<FileText size={48} />
|
|
||||||
<h3>暂无会议总结</h3>
|
|
||||||
<p>该会议尚未生成总结内容</p>
|
|
||||||
{isCreator && (
|
|
||||||
<button
|
|
||||||
className="generate-summary-cta"
|
|
||||||
onClick={openSummaryModal}
|
|
||||||
>
|
|
||||||
<Brain size={16} />
|
|
||||||
<span>生成AI总结</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1265,7 +1074,25 @@ ${renderedHTML}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Summary Error Modal */}
|
||||||
|
{showSummaryError && (
|
||||||
|
<div className="delete-modal-overlay" onClick={() => setShowSummaryError(false)}>
|
||||||
|
<div className="delete-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h3>操作无法进行</h3>
|
||||||
|
<p>会议转录尚未完成或处理失败,请在转录成功后再生成AI总结。</p>
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button
|
||||||
|
className="btn-cancel"
|
||||||
|
onClick={() => setShowSummaryError(false)}
|
||||||
|
>
|
||||||
|
知道了
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Speaker Tags Edit Modal */}
|
{/* Speaker Tags Edit Modal */}
|
||||||
{showSpeakerEdit && (
|
{showSpeakerEdit && (
|
||||||
<div className="speaker-edit-modal-overlay" onClick={() => setShowSpeakerEdit(false)}>
|
<div className="speaker-edit-modal-overlay" onClick={() => setShowSpeakerEdit(false)}>
|
||||||
|
|
@ -1470,14 +1297,6 @@ ${renderedHTML}
|
||||||
<div className="summary-result-section">
|
<div className="summary-result-section">
|
||||||
<div className="summary-result-header">
|
<div className="summary-result-header">
|
||||||
<h4>最新生成的总结</h4>
|
<h4>最新生成的总结</h4>
|
||||||
<button
|
|
||||||
className="export-pdf-btn"
|
|
||||||
onClick={exportToPDF}
|
|
||||||
title="导出PDF"
|
|
||||||
>
|
|
||||||
<Download size={14} />
|
|
||||||
<span>导出PDF</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="summary-result-content">
|
<div className="summary-result-content">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue