diff --git a/frontend/src/pages/business/MeetingDetail.tsx b/frontend/src/pages/business/MeetingDetail.tsx index b13421c..d8516de 100644 --- a/frontend/src/pages/business/MeetingDetail.tsx +++ b/frontend/src/pages/business/MeetingDetail.tsx @@ -96,6 +96,14 @@ type ChapterTranscriptLink = { firstTranscriptStartTime: number | null; }; +type MeetingStateNotice = { + title: string; + description: string; + type: 'info' | 'warning'; + hasFallbackContent: boolean; + scope: 'summary' | 'catalog' | 'global'; +}; + const ANALYSIS_EMPTY: MeetingAnalysis = { overview: '', keywords: [], @@ -355,8 +363,10 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => void; onProgressUpdate?: (meeting: MeetingVO) => void; + onProgressChange?: (progress: MeetingProgress | null) => void; compact?: boolean; -}> = ({ meetingId, onComplete, onProgressUpdate, compact }) => { + inline?: boolean; +}> = ({ meetingId, onComplete, onProgressUpdate, onProgressChange, compact, inline }) => { const [progress, setProgress] = useState(null); useEffect(() => { @@ -382,8 +392,10 @@ const MeetingProgressDisplay: React.FC<{ } if (progressRes.data?.data) { - setProgress(progressRes.data.data); - if (progressRes.data.data.percent === 100 || progressRes.data.data.percent < 0) { + const nextProgress = progressRes.data.data; + setProgress(nextProgress); + onProgressChange?.(nextProgress); + if (nextProgress.percent === 100 || nextProgress.percent < 0) { completed = true; onComplete(); } @@ -399,7 +411,7 @@ const MeetingProgressDisplay: React.FC<{ completed = true; clearInterval(timer); }; - }, [meetingId, onComplete, onProgressUpdate]); + }, [meetingId, onComplete, onProgressChange, onProgressUpdate]); const percent = progress?.percent || 0; const isError = percent < 0; @@ -412,6 +424,48 @@ const MeetingProgressDisplay: React.FC<{ return remainSeconds > 0 ? `${minutes} 分 ${remainSeconds} 秒` : `${minutes} 分钟`; }; + if (inline) { + return ( +
+
+ + {progress?.message || '正在生成新版总结...'} + + + {isError ? '失败' : `${percent}%`} + +
+ + + {`预计剩余:${isError ? '--' : formatEta(progress?.eta)}`} + +
+ ); + } + if (compact) { return (
{ const transcriptSectionRef = useRef(null); const [showFloatingTranscriptPlayer, setShowFloatingTranscriptPlayer] = useState(false); const [floatingTranscriptPlayerLayout, setFloatingTranscriptPlayerLayout] = useState<{ left: number; width: number } | null>(null); + const [generationProgress, setGenerationProgress] = useState(null); + const autoOpenedCatalogAttemptRef = useRef(null); const fetchData = useCallback(async (meetingId: number) => { try { @@ -917,12 +973,49 @@ const MeetingDetail: React.FC = () => { const canRetrySummary = isOwner && transcripts.length > 0 && meeting?.status !== 1 && meeting?.status !== 2; const canRetryTranscription = isOwner && meeting?.status === 4 && transcripts.length === 0 && !!meeting?.audioUrl; - const generationFailureNotice = useMemo(() => { + const hasSummaryContent = Boolean(meeting?.summaryContent?.trim()); + const hasCatalogContent = catalogChapterLinks.length > 0; + const generationFailureNotice = useMemo(() => { if (!meeting || meeting.status !== 4) { return null; } - const hasFallbackContent = Boolean(meeting.summaryContent) || meetingChapters.length > 0; + const hasFallbackContent = hasSummaryContent || hasCatalogContent; + if (meeting.latestChapterAttemptStatus === 3) { + const detail = meeting.latestChapterAttemptErrorMsg || '章节生成失败'; + return { + title: hasFallbackContent ? '历史内容仍可查看' : '本次 AI 目录生成失败', + description: hasFallbackContent + ? `最近一次 AI 目录生成失败,当前展示的是最近一次成功生成的内容。失败原因:${detail}` + : `AI 目录生成失败,当前没有可展示的历史目录或总结内容。失败原因:${detail}`, + type: hasFallbackContent ? 'info' : 'warning', + hasFallbackContent, + scope: 'catalog', + }; + } + + if (meeting.latestSummaryAttemptStatus === 3) { + const detail = meeting.latestSummaryAttemptErrorMsg || '总结生成失败'; + return { + title: hasFallbackContent ? '历史总结可用' : '本次总结生成失败', + description: hasFallbackContent + ? `最近一次重新总结失败,当前展示的是最近一次成功生成的总结内容。失败原因:${detail}` + : `总结生成失败,当前没有可展示的历史总结内容。失败原因:${detail}`, + type: hasFallbackContent ? 'info' : 'warning', + hasFallbackContent, + scope: 'summary', + }; + } + + return { + title: hasFallbackContent ? '历史内容仍可查看' : '会议处理异常', + description: hasFallbackContent + ? '最近一次处理未成功,当前展示的是最近一次成功生成的内容。你可以继续查看,或重新发起识别/总结。' + : '会议在处理中遇到问题,暂时没有可展示的总结内容。你可以重新发起识别或总结。', + type: hasFallbackContent ? 'info' : 'warning', + hasFallbackContent, + scope: 'global', + }; if (meeting.latestChapterAttemptStatus === 3) { const detail = meeting.latestChapterAttemptErrorMsg || '章节生成失败'; return { @@ -953,7 +1046,37 @@ const MeetingDetail: React.FC = () => { description: '会议在处理过程中遇到了问题。您可以尝试重新发起识别或总结。', hasFallbackContent, }; - }, [meeting, meetingChapters.length]); + }, [hasCatalogContent, hasSummaryContent, meeting]); + const summaryPanelNotice = useMemo(() => { + if (!meeting || !hasSummaryContent) { + return null; + } + if (meeting.status === 2) { + return { + title: '正在生成新版总结', + description: '当前展示的是上一版已成功生成的总结,系统会在新版完成后自动替换。', + type: 'info', + hasFallbackContent: true, + scope: 'summary', + }; + } + return null; + }, [generationFailureNotice, hasSummaryContent, meeting]); + const catalogPanelNotice = useMemo(() => { + if (!generationFailureNotice || generationFailureNotice.scope !== 'catalog') { + return null; + } + if (generationFailureNotice.hasFallbackContent && hasCatalogContent) { + return { + title: '当前展示的是历史成功目录', + description: generationFailureNotice.description, + type: generationFailureNotice.type, + hasFallbackContent: true, + scope: 'catalog', + }; + } + return generationFailureNotice; + }, [generationFailureNotice, hasCatalogContent]); const emptyTranscriptFailureNotice = useMemo(() => { if (!meeting || meeting.status !== 4 || transcripts.length > 0) { return null; @@ -968,6 +1091,34 @@ const MeetingDetail: React.FC = () => { }; }, [canRetryTranscription, meeting, transcripts.length]); + useEffect(() => { + if (meeting?.status !== 1 && meeting?.status !== 2) { + setGenerationProgress(null); + autoOpenedCatalogAttemptRef.current = null; + } + }, [meeting?.id, meeting?.status]); + + useEffect(() => { + const attemptKey = String(meeting?.latestChapterAttemptTaskId ?? meeting?.latestSummaryAttemptTaskId ?? ''); + if (meeting?.status !== 2 || !attemptKey) { + return; + } + if ((generationProgress?.percent ?? 0) < 88 || catalogChapterLinks.length === 0) { + return; + } + if (autoOpenedCatalogAttemptRef.current === attemptKey) { + return; + } + autoOpenedCatalogAttemptRef.current = attemptKey; + setWorkspaceTab('catalog'); + }, [ + catalogChapterLinks.length, + generationProgress?.percent, + meeting?.latestChapterAttemptTaskId, + meeting?.latestSummaryAttemptTaskId, + meeting?.status, + ]); + useEffect(() => { if (!playbackAudioUrl) { setShowFloatingTranscriptPlayer(false); @@ -1778,7 +1929,7 @@ const MeetingDetail: React.FC = () => { 正在总结 )} - {(playbackAudioUrl || transcripts.length > 0 || (meeting.status === 3 && !!meeting.summaryContent)) && ( + {(playbackAudioUrl || transcripts.length > 0 || hasSummaryContent) && ( { onClick: handleDownloadTranscript, disabled: downloadLoading === 'transcript', }] : []), - ...(meeting.status === 3 && !!meeting.summaryContent ? [ + ...(hasSummaryContent ? [ { key: 'pdf', label: '下载 PDF', @@ -1848,14 +1999,16 @@ const MeetingDetail: React.FC = () => { void fetchData(updated.id); } }} + onProgressChange={setGenerationProgress} /> ) : ( - + <> +
{(generationFailureNotice || emptyTranscriptFailureNotice || meeting.audioSaveStatus === 'FAILED') && ( { )} - {meeting.status === 2 ? ( + {meeting.status === 2 && !hasSummaryContent ? (
fetchData(meeting.id)} onProgressUpdate={(updated) => { - if (updated.status !== meeting.status) { + if (updated.status === 2 || updated.status !== meeting.status) { void fetchData(updated.id); } }} + onProgressChange={setGenerationProgress} compact />
- ) : meeting.summaryContent ? ( + ) : hasSummaryContent ? (
+ {summaryPanelNotice && ( + +
{summaryPanelNotice.description}
+ {meeting.status === 2 ? ( + fetchData(meeting.id)} + onProgressUpdate={(updated) => { + if (updated.status === 2 || updated.status !== meeting.status) { + void fetchData(updated.id); + } + }} + onProgressChange={setGenerationProgress} + inline + /> + ) : null} +
+ } + /> + )} {/* Keywords placed between title and overview */}
@@ -2078,13 +2258,13 @@ const MeetingDetail: React.FC = () => {
{workspaceTab === 'catalog' ? (
- {generationFailureNotice && !generationFailureNotice.hasFallbackContent && ( + {catalogPanelNotice && ( )} {catalogChapterLinks.length ? ( @@ -2118,6 +2298,14 @@ const MeetingDetail: React.FC = () => {
)) + ) : meeting.status === 2 ? ( + ) : ( )} @@ -2169,7 +2357,8 @@ const MeetingDetail: React.FC = () => { )}
- + + )}
diff --git a/frontend/src/pages/business/Meetings.tsx b/frontend/src/pages/business/Meetings.tsx index bc6c1fd..481c28a 100644 --- a/frontend/src/pages/business/Meetings.tsx +++ b/frontend/src/pages/business/Meetings.tsx @@ -594,54 +594,19 @@ const Meetings: React.FC = () => { {configLoaded && ( - <> - {!createConfig.offlineEnabled && !createConfig.realtimeEnabled ? ( - - ) : createConfig.offlineEnabled && createConfig.realtimeEnabled ? ( - , - label: "上传录音", - onClick: () => { - setCreateDrawerType("upload"); - setCreateDrawerVisible(true); - }, - }, - { - key: "realtime", - icon: , - label: "实时会议", - onClick: () => { - setCreateDrawerType("realtime"); - setCreateDrawerVisible(true); - }, - }, - ], - }} - placement="bottomRight" - > - - - ) : ( - - )} - + )} }