refactor: 优化会议创建按钮逻辑和生成进度显示
- 简化 `Meetings.tsx` 中的会议创建按钮逻辑 - 在 `MeetingDetail.tsx` 中添加 `MeetingStateNotice` 类型,并更新生成进度显示 - 优化生成失败提示和展示逻辑,增加对历史内容的支持 - 更新相关组件以支持新的生成进度和状态显示dev_na
parent
7d08234919
commit
7989b6aa11
|
|
@ -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<MeetingProgress | null>(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 (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: '12px 14px',
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${isError ? '#ffccc7' : '#d6dbff'}`,
|
||||
background: isError ? '#fff2f0' : '#f7f8ff',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ color: isError ? '#cf1322' : '#4f46e5' }}>
|
||||
{progress?.message || '正在生成新版总结...'}
|
||||
</Text>
|
||||
<Text strong style={{ color: isError ? '#cf1322' : '#4f46e5', whiteSpace: 'nowrap' }}>
|
||||
{isError ? '失败' : `${percent}%`}
|
||||
</Text>
|
||||
</div>
|
||||
<Progress
|
||||
percent={isError ? 100 : percent}
|
||||
status={isError ? 'exception' : percent === 100 ? 'success' : 'active'}
|
||||
strokeColor={isError ? '#ff4d4f' : '#6c73ff'}
|
||||
showInfo={false}
|
||||
size="small"
|
||||
style={{ marginBottom: 6 }}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{`预计剩余:${isError ? '--' : formatEta(progress?.eta)}`}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -778,6 +832,8 @@ const MeetingDetail: React.FC = () => {
|
|||
const transcriptSectionRef = useRef<HTMLDivElement>(null);
|
||||
const [showFloatingTranscriptPlayer, setShowFloatingTranscriptPlayer] = useState(false);
|
||||
const [floatingTranscriptPlayerLayout, setFloatingTranscriptPlayerLayout] = useState<{ left: number; width: number } | null>(null);
|
||||
const [generationProgress, setGenerationProgress] = useState<MeetingProgress | null>(null);
|
||||
const autoOpenedCatalogAttemptRef = useRef<string | null>(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<MeetingStateNotice | null>(() => {
|
||||
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<MeetingStateNotice | null>(() => {
|
||||
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<MeetingStateNotice | null>(() => {
|
||||
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 = () => {
|
|||
正在总结
|
||||
</Button>
|
||||
)}
|
||||
{(playbackAudioUrl || transcripts.length > 0 || (meeting.status === 3 && !!meeting.summaryContent)) && (
|
||||
{(playbackAudioUrl || transcripts.length > 0 || hasSummaryContent) && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
|
|
@ -1795,7 +1946,7 @@ const MeetingDetail: React.FC = () => {
|
|||
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}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Row gutter={24} style={{ height: '100%' }}>
|
||||
<Col xs={24} xl={13} style={{ height: '100%' }}>
|
||||
<div className="detail-side-column detail-left-column">
|
||||
{(generationFailureNotice || emptyTranscriptFailureNotice || meeting.audioSaveStatus === 'FAILED') && (
|
||||
<Alert
|
||||
type="warning"
|
||||
type={generationFailureNotice?.type || 'warning'}
|
||||
showIcon
|
||||
style={{ marginBottom: 16, borderRadius: 12 }}
|
||||
message={
|
||||
|
|
@ -1883,21 +2036,48 @@ const MeetingDetail: React.FC = () => {
|
|||
)}
|
||||
<Card className="left-flow-card summary-panel" variant="borderless">
|
||||
|
||||
{meeting.status === 2 ? (
|
||||
{meeting.status === 2 && !hasSummaryContent ? (
|
||||
<div className="summary-progress-shell">
|
||||
<MeetingProgressDisplay
|
||||
meetingId={meeting.id}
|
||||
onComplete={() => 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
|
||||
/>
|
||||
</div>
|
||||
) : meeting.summaryContent ? (
|
||||
) : hasSummaryContent ? (
|
||||
<div className="summary-content-box">
|
||||
{summaryPanelNotice && (
|
||||
<Alert
|
||||
type={summaryPanelNotice.type}
|
||||
showIcon
|
||||
style={{ marginBottom: 20, borderRadius: 14 }}
|
||||
message={summaryPanelNotice.title}
|
||||
description={
|
||||
<div>
|
||||
<div>{summaryPanelNotice.description}</div>
|
||||
{meeting.status === 2 ? (
|
||||
<MeetingProgressDisplay
|
||||
meetingId={meeting.id}
|
||||
onComplete={() => fetchData(meeting.id)}
|
||||
onProgressUpdate={(updated) => {
|
||||
if (updated.status === 2 || updated.status !== meeting.status) {
|
||||
void fetchData(updated.id);
|
||||
}
|
||||
}}
|
||||
onProgressChange={setGenerationProgress}
|
||||
inline
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* Keywords placed between title and overview */}
|
||||
<div className="summary-section" style={{ marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
|
|
@ -2078,13 +2258,13 @@ const MeetingDetail: React.FC = () => {
|
|||
<div className="transcript-scroll-shell">
|
||||
{workspaceTab === 'catalog' ? (
|
||||
<div className="catalog-list">
|
||||
{generationFailureNotice && !generationFailureNotice.hasFallbackContent && (
|
||||
{catalogPanelNotice && (
|
||||
<Alert
|
||||
type="warning"
|
||||
type={catalogPanelNotice.type}
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="AI 目录生成失败"
|
||||
description={generationFailureNotice.description}
|
||||
description={catalogPanelNotice.description}
|
||||
/>
|
||||
)}
|
||||
{catalogChapterLinks.length ? (
|
||||
|
|
@ -2118,6 +2298,14 @@ const MeetingDetail: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div> ))
|
||||
) : meeting.status === 2 ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="AI 目录生成中"
|
||||
description={generationProgress?.message || '正在生成 AI 目录,章节完成后会自动展示。'}
|
||||
/>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无 AI 目录" />
|
||||
)}
|
||||
|
|
@ -2170,6 +2358,7 @@ const MeetingDetail: React.FC = () => {
|
|||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -594,55 +594,20 @@ const Meetings: React.FC = () => {
|
|||
<Radio.Button value="list"><UnorderedListOutlined /></Radio.Button>
|
||||
</Radio.Group>
|
||||
{configLoaded && (
|
||||
<>
|
||||
{!createConfig.offlineEnabled && !createConfig.realtimeEnabled ? (
|
||||
<Button type="primary" icon={<PlusOutlined />} disabled>
|
||||
会议创建已关闭
|
||||
</Button>
|
||||
) : createConfig.offlineEnabled && createConfig.realtimeEnabled ? (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "upload",
|
||||
icon: <CloudUploadOutlined />,
|
||||
label: "上传录音",
|
||||
onClick: () => {
|
||||
setCreateDrawerType("upload");
|
||||
setCreateDrawerVisible(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "realtime",
|
||||
icon: <AudioOutlined />,
|
||||
label: "实时会议",
|
||||
onClick: () => {
|
||||
setCreateDrawerType("realtime");
|
||||
setCreateDrawerVisible(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
新建会议
|
||||
</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
disabled={!createConfig.offlineEnabled && !createConfig.realtimeEnabled}
|
||||
onClick={() => {
|
||||
if (createConfig.offlineEnabled || createConfig.realtimeEnabled) {
|
||||
setCreateDrawerType(createConfig.offlineEnabled ? "upload" : "realtime");
|
||||
setCreateDrawerVisible(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{createConfig.offlineEnabled ? "上传录音" : "实时会议"}
|
||||
新建会议
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
toolbar={
|
||||
|
|
|
|||
Loading…
Reference in New Issue