diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java index 3c94b24..bf42695 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryPromptAssembler.java @@ -92,19 +92,19 @@ public class MeetingSummaryPromptAssembler { .append("{\n") .append(" \"summaryContent\": \"完整会议纪要正文,使用 markdown\",\n") .append(" \"analysis\": {\n") - .append(" \"overview\": \"会议概览\",\n") +// .append(" \"overview\": \"会议概览\",\n") .append(" \"keywords\": [\"关键词1\", \"关键词2\"],\n") - .append(" \"chapters\": [{\"time\":\"00:00\",\"title\":\"章节标题\",\"summary\":\"章节摘要\"}],\n") - .append(" \"speakerSummaries\": [{\"speaker\":\"发言人\",\"summary\":\"观点总结\"}],\n") - .append(" \"keyPoints\": [{\"title\":\"关键点\",\"summary\":\"具体说明\",\"speaker\":\"发言人\"}],\n") - .append(" \"todos\": [\"待办事项1\", \"待办事项2\"]\n") +// .append(" \"chapters\": [{\"time\":\"00:00\",\"title\":\"章节标题\",\"summary\":\"章节摘要\"}],\n") +// .append(" \"speakerSummaries\": [{\"speaker\":\"发言人\",\"summary\":\"观点总结\"}],\n") +// .append(" \"keyPoints\": [{\"title\":\"关键点\",\"summary\":\"具体说明\",\"speaker\":\"发言人\"}],\n") +// .append(" \"todos\": [\"待办事项1\", \"待办事项2\"]\n") .append(" }\n") .append("}\n") .append("要求:\n") .append("1. `summaryContent` 必须优先遵循模板提示词中的结构、标题层级、章节顺序和写作风格。\n") - .append("2. `analysis` 必须基于完整转写内容生成,不得脱离上下文。\n") - .append("3. 若无待办事项,`todos` 返回空数组。\n") - .append("4. 仅输出 JSON。\n") + .append("2. `analysis.keywords` 必须基于完整转写内容生成,不得脱离上下文。并且在转录中能找到对应的原文\n") +// .append("3. 若无待办事项,`todos` 返回空数组。\n") + .append("3. 仅输出 JSON。\n") .append("\n") .append("会议转写如下:\n") .append(asrText == null ? "" : asrText); diff --git a/frontend/src/pages/business/MeetingDetail.tsx b/frontend/src/pages/business/MeetingDetail.tsx index 4779e99..58ca265 100644 --- a/frontend/src/pages/business/MeetingDetail.tsx +++ b/frontend/src/pages/business/MeetingDetail.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { Alert, Avatar, Breadcrumb, Button, Card, Checkbox, Col, Divider, Drawer, Empty, Form, Input, List, Modal, Popover, Progress, QRCode, Row, Select, Skeleton, Space, Switch, Tag, Typography, App, Dropdown } from 'antd'; +import { Alert, Avatar, Button, Card, Col, Divider, Drawer, Empty, Form, Input, List, Modal, Popover, Progress, QRCode, Row, Select, Skeleton, Space, Switch, Tag, Typography, App, Dropdown } from 'antd'; import { AudioOutlined, CaretRightFilled, @@ -9,17 +9,16 @@ import { DownloadOutlined, EditOutlined, FastForwardOutlined, + FileTextOutlined, LeftOutlined, LinkOutlined, LoadingOutlined, PauseOutlined, - QrcodeOutlined, RobotOutlined, SyncOutlined, UserOutlined, PlusOutlined, CheckCircleFilled, - EllipsisOutlined, FilePdfOutlined, FileWordOutlined, ShareAltOutlined, @@ -50,6 +49,7 @@ import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt'; import { listUsers } from '../../api'; import { useDict } from '../../hooks/useDict'; import { SysUser } from '../../types'; +import PageHeader from '../../components/shared/PageHeader'; const { Title, Text } = Typography; const { Option } = Select; @@ -651,11 +651,11 @@ const MeetingDetail: React.FC = () => { const [loading, setLoading] = useState(true); const [editVisible, setEditVisible] = useState(false); const [summaryVisible, setSummaryVisible] = useState(false); + const [summaryRecordVisible, setSummaryRecordVisible] = useState(false); const [actionLoading, setActionLoading] = useState(false); const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | 'transcript' | null>(null); const [isEditingSummary, setIsEditingSummary] = useState(false); const [summaryDraft, setSummaryDraft] = useState(''); - const [summaryTab, setSummaryTab] = useState<'chapters' | 'speakers' | 'actions' | 'todos'>('chapters'); const [expandKeywords, setExpandKeywords] = useState(false); const [expandSummary, setExpandSummary] = useState(false); const [selectedKeywords, setSelectedKeywords] = useState([]); @@ -711,6 +711,20 @@ const MeetingDetail: React.FC = () => { analysis.todos.length ); const visibleKeywords = expandKeywords ? analysis.keywords : analysis.keywords.slice(0, 9); + const meetingTags = useMemo( + () => (meeting?.tags?.split(',').map((item) => item.trim()).filter(Boolean) || []), + [meeting?.tags], + ); + const discussionItems = useMemo(() => { + if (analysis.keyPoints.length) { + return analysis.keyPoints; + } + return analysis.chapters.map((item) => ({ + title: item.title, + summary: item.summary, + time: item.time, + })); + }, [analysis.chapters, analysis.keyPoints]); const speakerLabelMap = useMemo( () => new Map(speakerLabels.map((item) => [item.itemValue, item.itemLabel])), [speakerLabels], @@ -1453,124 +1467,113 @@ const MeetingDetail: React.FC = () => { } return ( -
- navigate('/meetings')}>会议中心 }, - { title: '会议详情' } - ]} +
+ +
+ +
+
+
+ {meeting.title} + {isOwner && ( + + )} +
+
+ + {dayjs(meeting.meetingTime).format('YYYY-MM-DD HH:mm')} + + {meeting.participants || '未指定'} +
+
+
+ )} + extra={( + + + {canRetrySummary && ( + + )} + {canRetryTranscription && ( + + )} + {isOwner && meeting.status === 2 && ( + + )} + {(playbackAudioUrl || transcripts.length > 0 || (meeting.status === 3 && !!meeting.summaryContent)) && ( + , + onClick: handleDownloadAudio, + }] : []), + ...(transcripts.length > 0 ? [{ + key: 'transcript', + label: '下载转录 MD', + icon: , + onClick: handleDownloadTranscript, + disabled: downloadLoading === 'transcript', + }] : []), + ...(meeting.status === 3 && !!meeting.summaryContent ? [ + { + key: 'pdf', + label: '下载 PDF', + icon: , + onClick: () => handleDownloadSummary('pdf'), + disabled: downloadLoading === 'pdf', + }, + { + key: 'word', + label: '下载 Word', + icon: , + onClick: () => handleDownloadSummary('word'), + disabled: downloadLoading === 'word', + }, + ] : []), + ], + }} + placement="bottomRight" + > + + + )} + {shareQrContent ? ( + + + + ) : null} + + )} /> - - - - - - {meeting.title} - {isOwner && ( - <EditOutlined style={{ fontSize: 16, cursor: 'pointer', color: '#1890ff', marginLeft: 8 }} onClick={handleEditMeeting} /> - )} - - }> - - {dayjs(meeting.meetingTime).format('YYYY-MM-DD HH:mm')} - - - {meeting.tags?.split(',').filter(Boolean).map((tag) => ( - {tag} - ))} - - - {meeting.participants || '未指定'} - - - - - - - {canRetrySummary && ( - - )} - {canRetryTranscription && ( - - )} - {isOwner && meeting.status === 2 && ( - - )} - {(playbackAudioUrl || transcripts.length > 0 || (meeting.status === 3 && !!meeting.summaryContent)) && ( - , - onClick: handleDownloadAudio, - }] : []), - ...(transcripts.length > 0 && !(meeting.status === 3 && !!meeting.summaryContent) ? [{ - key: 'transcript', - label: '下载转录 MD', - icon: , - onClick: handleDownloadTranscript, - disabled: downloadLoading === 'transcript' - }] : []), - ...(meeting.status === 3 && !!meeting.summaryContent ? [ - { - key: 'transcript', - label: '下载转录 MD', - icon: , - }, - { - key: 'pdf', - label: '下载 PDF', - icon: , - onClick: () => handleDownloadSummary('pdf'), - disabled: downloadLoading === 'pdf' - }, - { - key: 'word', - label: '下载 Word', - icon: , - onClick: () => handleDownloadSummary('word'), - disabled: downloadLoading === 'word' - } - ] : []) - ] - }} - placement="bottomRight" - > - - - )} - {shareQrContent ? ( - - - - ) : null} - - - - - -
+
{meeting.status === 1 ? ( { /> ) : ( - -
- {!emptyTranscriptFailureNotice && ( + +
- 智能速览 + AI 智能总结
-
- {hasAnalysis ? '已生成' : '待生成'} -
-
- -
-
- 关键词 - {isOwner && analysis.keywords.length > 0 && ( - - )} + ) : null} + {meeting.summaryContent && isOwner ? ( + + ) : null}
- {analysis.keywords.length ? ( - <> -
- {visibleKeywords.map((tag) => { - const isSelected = selectedKeywords.includes(tag); - return ( -
isOwner && handleKeywordToggle(tag, !isSelected)} - > - {tag} - {isOwner && isSelected && } -
- ); - })} -
- {analysis.keywords.length > 9 && ( - - )} - - ) : ( - 暂无关键词 - )}
-
-
- 全文概要 + {meeting.status === 2 ? ( +
+ fetchData(meeting.id)} + compact + />
- {analysis.overview ? ( - <> -
220 ? 'summary-copy summary-fade' : 'summary-copy'}> - {analysis.overview} -
- {analysis.overview.length > 220 && ( - - )} - - ) : ( - 暂无概要 - )} -
- -
-
- - - - + ) : meeting.summaryContent ? ( +
+
+ {meeting.summaryContent} +
- - {summaryTab === 'chapters' && ( -
- {analysis.chapters.length ? ( - analysis.chapters.map((item, index) => ( -
-
{item.time || '--:--'}
-
- {item.title || `章节 ${index + 1}`} - {item.summary || '暂无章节描述'} -
+ ) : false ? ( + <> +
+
会议概述
+ {analysis.overview ? ( + <> +
220 ? 'summary-copy summary-fade' : 'summary-copy'}> + {analysis.overview}
- )) + {analysis.overview.length > 220 && ( + + )} + ) : ( - + 暂无概述 )}
- )} - {summaryTab === 'speakers' && ( -
- {analysis.speakerSummaries.length ? ( - analysis.speakerSummaries.map((item, index) => ( -
-
-
{(item.speaker || '发').slice(0, 1)}
-
{item.speaker || `发言人${index + 1}`}
-
-
-
发言概述
-
{item.summary || '暂无发言总结'}
-
-
- )) - ) : ( - - )} -
- )} - - {summaryTab === 'actions' && ( -
- {analysis.keyPoints.length ? ( - analysis.keyPoints.map((item, index) => ( -
-
{String(index + 1).padStart(2, '0')}
-
- {item.title || `要点 ${index + 1}`} - {item.summary || '暂无要点说明'} - {(item.speaker || item.time) && ( -
- {item.speaker ? {item.speaker} : null} - {item.time ? {item.time} : null} +
+
主要讨论点
+ {discussionItems.length ? ( +
+ {discussionItems.map((item, index) => ( +
+
+
+
+ {item.title || `讨论点 ${index + 1}`} + {(item.speaker || item.time) && ( +
+ {item.speaker ? {item.speaker} : null} + {item.time ? {item.time} : null} +
+ )}
- )} +
{item.summary || '暂无讨论摘要'}
+
-
- )) + ))} +
) : ( - + 暂无主要讨论点 )}
- )} - {summaryTab === 'todos' && ( -
- {analysis.todos.length ? ( - analysis.todos.map((item, index) => ( -
- - {item} -
- )) - ) : ( - - )} -
- )} -
+ {analysis.todos.length ? ( +
+
待办事项
+
+ {analysis.todos.map((item, index) => ( +
+ + {item} +
+ ))} +
+
+ ) : null} + + ) : ( +
+ +
+ )} - )} {!emptyTranscriptFailureNotice && (
@@ -1823,71 +1766,119 @@ const MeetingDetail: React.FC = () => {
- {!emptyTranscriptFailureNotice && ( - -
- AI 总结} - extra={ - meeting.summaryContent && isOwner && ( - - {isEditingSummary ? ( - <> - - - - ) : ( - - )} - - ) - } - style={{ height: '100%' }} - styles={{ body: { padding: 24, height: '100%', overflowY: 'auto', overflowX: 'hidden', minWidth: 0 } }} - > -
- {meeting.status === 2 ? ( -
- fetchData(meeting.id)} - compact - /> -
- ) : meeting.summaryContent ? ( - isEditingSummary ? ( - setSummaryDraft(event.target.value)} - style={{ height: '100%', resize: 'none' }} - /> - ) : ( -
- {meeting.summaryContent} -
- ) - ) : ( -
- -
+ +
+ {( +
+ + {playbackAudioUrl && ( + )} -
- + +
+
关键词
+
+
+ {(analysis.keywords.length ? visibleKeywords : meetingTags).length ? ( + (analysis.keywords.length ? visibleKeywords : meetingTags).map((tag) => { + const isSelected = selectedKeywords.includes(tag); + return ( +
isOwner && analysis.keywords.length && handleKeywordToggle(tag, !isSelected)} + > + #{tag} + {isOwner && isSelected && } +
+ ); + }) + ) : ( + 暂无关键词 + )} +
+
+ {analysis.keywords.length > 9 ? ( + + ) : null} + {isOwner && analysis.keywords.length > 0 ? ( + + ) : null} +
+
+
+ +
+ +
+ +
+ {emptyTranscriptFailureNotice && ( +
+
当前没有可展示的转录内容
+
{emptyTranscriptFailureNotice.description}
+
+ )} + {meeting.audioSaveStatus === 'FAILED' && ( + + )} + { + const nextStartTime = transcripts[index + 1]?.startTime || Infinity; + const isActive = (audioCurrentTime * 1000) >= item.startTime && (audioCurrentTime * 1000) < nextStartTime; + + return ( + + ); + }} + locale={{ emptyText: meeting.status < 3 ? '识别任务进行中...' : '暂无数据' }} + /> +
+ +
+ )}
- )} )}
@@ -1923,6 +1914,82 @@ const MeetingDetail: React.FC = () => { )}