feat:优化会议转录和章节功能
- 更新 `MeetingTranscriptFileServiceImpl`,使用 Markdown 格式导出会议转录 - 在 `MeetingQueryServiceImpl` 中添加会议存在性检查,并调用 `listDisplayChapterAnalysis` - 在 `MeetingCommandServiceImpl` 中更新会议进度信息 - 在前端 `http.ts` 和 `meeting.ts` 中添加 `suppressErrorToast` 选项 - 在 `MeetingVO` 中添加最近一次总结和章节尝试的任务状态及错误信息 - 更新 `MeetingDetail.tsx` 和 `Meetings.tsx`,处理生成失败的提示和展示逻辑 - 在 `MeetingTranscriptChapterService` 和 `MeetingTranscriptChapterServiceImpl` 中添加加载当前章节 Markdown 的方法 - 优化 `PageContainer` 和 `Meetings` 页面布局,改善滚动和内容展示dev_na
parent
1877c64cc2
commit
ccb408ade5
|
|
@ -57,6 +57,18 @@ public class MeetingVO {
|
|||
private String lastUserPrompt;
|
||||
@Schema(description = "分析结果")
|
||||
private Map<String, Object> analysis;
|
||||
@Schema(description = "最近一次总结尝试任务 ID")
|
||||
private Long latestSummaryAttemptTaskId;
|
||||
@Schema(description = "最近一次总结尝试任务状态")
|
||||
private Integer latestSummaryAttemptStatus;
|
||||
@Schema(description = "最近一次总结尝试错误信息")
|
||||
private String latestSummaryAttemptErrorMsg;
|
||||
@Schema(description = "最近一次章节尝试任务 ID")
|
||||
private Long latestChapterAttemptTaskId;
|
||||
@Schema(description = "最近一次章节尝试任务状态")
|
||||
private Integer latestChapterAttemptStatus;
|
||||
@Schema(description = "最近一次章节尝试错误信息")
|
||||
private String latestChapterAttemptErrorMsg;
|
||||
@Schema(description = "会议状态")
|
||||
private Integer status;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ public interface MeetingTranscriptChapterService {
|
|||
|
||||
List<Map<String, Object>> listCurrentChapterAnalysis(Long meetingId);
|
||||
|
||||
List<Map<String, Object>> listDisplayChapterAnalysis(Meeting meeting);
|
||||
|
||||
void invalidateCurrentVersion(Long meetingId);
|
||||
|
||||
MeetingTranscriptChapterVersion importExternalChapters(Meeting meeting, AiTask sourceTask, MeetingTranscriptChapterImportDTO command);
|
||||
|
|
@ -22,4 +24,6 @@ public interface MeetingTranscriptChapterService {
|
|||
MeetingTranscriptSourceVO buildTranscriptSource(Long meetingId);
|
||||
|
||||
MeetingTranscriptChapterVersion getCurrentVersion(Long meetingId);
|
||||
|
||||
String loadCurrentChapterMarkdown(Meeting meeting);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,6 @@ public interface MeetingTranscriptFileService {
|
|||
void initializeTranscriptFileIfAbsent(Long meetingId);
|
||||
|
||||
MeetingTranscriptExportResult exportTranscript(Meeting meeting, MeetingVO meetingDetail);
|
||||
|
||||
String loadTranscriptMarkdown(Meeting meeting, MeetingVO meetingDetail);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -735,6 +735,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
}
|
||||
meeting.setStatus(2);
|
||||
meetingService.updateById(meeting);
|
||||
if ("EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) {
|
||||
updateMeetingProgress(meetingId, 95, "等待外部章节与总结编排...", 0);
|
||||
} else {
|
||||
updateMeetingProgress(meetingId, 85, "重新总结已提交,正在生成章节...", 0);
|
||||
}
|
||||
dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -339,6 +339,7 @@ public class MeetingDomainSupport {
|
|||
vo.setSummaryContent(meetingSummaryFileService.loadSummaryContent(meeting));
|
||||
vo.setAnalysis(meetingSummaryFileService.loadSummaryAnalysis(meeting));
|
||||
vo.setLastUserPrompt(resolveLastSummaryUserPrompt(meeting));
|
||||
fillLatestTaskAttemptInfo(meeting, vo);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -379,6 +380,41 @@ public class MeetingDomainSupport {
|
|||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private void fillLatestTaskAttemptInfo(Meeting meeting, com.imeeting.dto.biz.MeetingVO vo) {
|
||||
AiTask latestSummaryAttempt = resolveLatestTaskAttempt(meeting, "SUMMARY");
|
||||
if (latestSummaryAttempt != null) {
|
||||
vo.setLatestSummaryAttemptTaskId(latestSummaryAttempt.getId());
|
||||
vo.setLatestSummaryAttemptStatus(latestSummaryAttempt.getStatus());
|
||||
vo.setLatestSummaryAttemptErrorMsg(normalizeTaskError(latestSummaryAttempt.getErrorMsg()));
|
||||
}
|
||||
|
||||
AiTask latestChapterAttempt = resolveLatestTaskAttempt(meeting, "CHAPTER");
|
||||
if (latestChapterAttempt != null) {
|
||||
vo.setLatestChapterAttemptTaskId(latestChapterAttempt.getId());
|
||||
vo.setLatestChapterAttemptStatus(latestChapterAttempt.getStatus());
|
||||
vo.setLatestChapterAttemptErrorMsg(normalizeTaskError(latestChapterAttempt.getErrorMsg()));
|
||||
}
|
||||
}
|
||||
|
||||
private AiTask resolveLatestTaskAttempt(Meeting meeting, String taskType) {
|
||||
if (meeting == null || meeting.getId() == null) {
|
||||
return null;
|
||||
}
|
||||
return aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meeting.getId())
|
||||
.eq(AiTask::getTaskType, taskType)
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private String normalizeTaskError(String errorMsg) {
|
||||
if (errorMsg == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = errorMsg.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
private record AudioRelocationPlan(Path sourcePath, Path targetPath, Path backupPath, String relocatedUrl) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,11 @@ public class MeetingQueryServiceImpl implements MeetingQueryService {
|
|||
|
||||
@Override
|
||||
public List<Map<String, Object>> getChapters(Long meetingId) {
|
||||
return meetingTranscriptChapterService.listCurrentChapterAnalysis(meetingId);
|
||||
Meeting meeting = meetingService.getById(meetingId);
|
||||
if (meeting == null) {
|
||||
return List.of();
|
||||
}
|
||||
return meetingTranscriptChapterService.listDisplayChapterAnalysis(meeting);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.imeeting.entity.biz.Meeting;
|
|||
import com.imeeting.entity.biz.MeetingTranscript;
|
||||
import com.imeeting.entity.biz.MeetingTranscriptChapter;
|
||||
import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
|
||||
import com.imeeting.mapper.biz.AiTaskMapper;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptChapterMapper;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptChapterVersionMapper;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
|
|
@ -71,6 +72,7 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
|||
private final MeetingTranscriptMapper transcriptMapper;
|
||||
private final MeetingTranscriptChapterVersionMapper versionMapper;
|
||||
private final MeetingTranscriptChapterMapper chapterMapper;
|
||||
private final AiTaskMapper aiTaskMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private AiModelService aiModelService;
|
||||
|
|
@ -129,10 +131,26 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
|||
if (current == null) {
|
||||
return List.of();
|
||||
}
|
||||
return listVersionChapterAnalysis(meetingId, current.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> listDisplayChapterAnalysis(Meeting meeting) {
|
||||
if (meeting == null || meeting.getId() == null) {
|
||||
return List.of();
|
||||
}
|
||||
MeetingTranscriptChapterVersion displayVersion = resolveDisplayVersion(meeting);
|
||||
if (displayVersion == null) {
|
||||
return List.of();
|
||||
}
|
||||
return listVersionChapterAnalysis(meeting.getId(), displayVersion.getId());
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> listVersionChapterAnalysis(Long meetingId, Long versionId) {
|
||||
List<MeetingTranscript> transcripts = loadRawTranscripts(meetingId);
|
||||
Map<Long, MeetingTranscript> transcriptById = transcripts.stream()
|
||||
.collect(Collectors.toMap(MeetingTranscript::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
|
||||
return loadVersionChapters(current.getId()).stream()
|
||||
return loadVersionChapters(versionId).stream()
|
||||
.map(chapter -> toChapterAnalysis(chapter, transcriptById))
|
||||
.toList();
|
||||
}
|
||||
|
|
@ -200,6 +218,29 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
|||
return findCurrentVersion(meetingId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String loadCurrentChapterMarkdown(Meeting meeting) {
|
||||
if (meeting == null || meeting.getId() == null) {
|
||||
throw new RuntimeException("Meeting not found");
|
||||
}
|
||||
MeetingTranscriptChapterVersion current = findCurrentVersion(meeting.getId());
|
||||
if (current == null) {
|
||||
return "";
|
||||
}
|
||||
List<MeetingTranscript> transcripts = loadRawTranscripts(meeting.getId());
|
||||
Map<Long, MeetingTranscript> transcriptById = transcripts.stream()
|
||||
.collect(Collectors.toMap(MeetingTranscript::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
|
||||
List<MeetingTranscriptChapter> chapters = loadVersionChapters(current.getId());
|
||||
String relativePath = writeCurrentChapterMarkdown(meeting, current, chapters, transcriptById);
|
||||
try {
|
||||
String basePath = uploadPath.endsWith("/") || uploadPath.endsWith("\\") ? uploadPath : uploadPath + "/";
|
||||
Path targetPath = Paths.get(basePath, relativePath.replace("\\", "/"));
|
||||
return Files.exists(targetPath) ? Files.readString(targetPath, StandardCharsets.UTF_8) : "";
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("Failed to load meeting chapter markdown", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private MeetingTranscriptChapterVersion generateInternalVersion(Meeting meeting,
|
||||
AiTask summaryTask,
|
||||
List<MeetingTranscript> transcripts,
|
||||
|
|
@ -397,6 +438,49 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
|||
return version;
|
||||
}
|
||||
|
||||
private MeetingTranscriptChapterVersion resolveDisplayVersion(Meeting meeting) {
|
||||
Long chapterVersionId = extractChapterVersionId(resolveDisplaySummaryTask(meeting));
|
||||
if (chapterVersionId != null) {
|
||||
MeetingTranscriptChapterVersion version = versionMapper.selectById(chapterVersionId);
|
||||
if (version != null && Objects.equals(version.getMeetingId(), meeting.getId())) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
return findCurrentVersion(meeting.getId());
|
||||
}
|
||||
|
||||
private AiTask resolveDisplaySummaryTask(Meeting meeting) {
|
||||
if (meeting.getLatestSummaryTaskId() != null) {
|
||||
AiTask task = aiTaskMapper.selectById(meeting.getLatestSummaryTaskId());
|
||||
if (task != null
|
||||
&& Objects.equals(task.getMeetingId(), meeting.getId())
|
||||
&& "SUMMARY".equals(task.getTaskType())
|
||||
&& Integer.valueOf(2).equals(task.getStatus())) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
return aiTaskMapper.selectOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meeting.getId())
|
||||
.eq(AiTask::getTaskType, "SUMMARY")
|
||||
.eq(AiTask::getStatus, 2)
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private Long extractChapterVersionId(AiTask summaryTask) {
|
||||
if (summaryTask == null || summaryTask.getResponseData() == null) {
|
||||
return null;
|
||||
}
|
||||
Object summarySource = summaryTask.getResponseData().get("summarySource");
|
||||
if (summarySource instanceof Map<?, ?> sourceMap) {
|
||||
Long versionId = longValue(sourceMap.get("chapterVersionId"));
|
||||
if (versionId != null) {
|
||||
return versionId;
|
||||
}
|
||||
}
|
||||
return longValue(summaryTask.getResponseData().get("chapterVersionId"));
|
||||
}
|
||||
|
||||
private MeetingSummarySource buildSummarySource(Meeting meeting,
|
||||
MeetingTranscriptChapterVersion version,
|
||||
List<MeetingTranscript> transcripts,
|
||||
|
|
|
|||
|
|
@ -49,15 +49,16 @@ public class MeetingTranscriptFileServiceImpl implements MeetingTranscriptFileSe
|
|||
if (meeting == null) {
|
||||
return;
|
||||
}
|
||||
writeTranscriptFile(meeting, null);
|
||||
loadTranscriptMarkdown(meeting, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MeetingTranscriptExportResult exportTranscript(Meeting meeting, MeetingVO meetingDetail) {
|
||||
if (meeting == null || meeting.getId() == null) {
|
||||
throw new RuntimeException("会议不存在");
|
||||
throw new RuntimeException("Meeting not found");
|
||||
}
|
||||
byte[] content = writeTranscriptFile(meeting, meetingDetail);
|
||||
String markdown = loadTranscriptMarkdown(meeting, meetingDetail);
|
||||
byte[] content = markdown.getBytes(StandardCharsets.UTF_8);
|
||||
String safeTitle = sanitizeFileName(
|
||||
meetingDetail != null ? meetingDetail.getTitle() : meeting.getTitle(),
|
||||
"meeting-transcript-" + meeting.getId()
|
||||
|
|
@ -65,7 +66,8 @@ public class MeetingTranscriptFileServiceImpl implements MeetingTranscriptFileSe
|
|||
return new MeetingTranscriptExportResult(content, CONTENT_TYPE, safeTitle + "-Transcript.md");
|
||||
}
|
||||
|
||||
private byte[] writeTranscriptFile(Meeting meeting, MeetingVO meetingDetail) {
|
||||
@Override
|
||||
public String loadTranscriptMarkdown(Meeting meeting, MeetingVO meetingDetail) {
|
||||
try {
|
||||
Path transcriptPath = buildTranscriptPath(meeting.getId());
|
||||
Path parent = transcriptPath.getParent();
|
||||
|
|
@ -74,9 +76,9 @@ public class MeetingTranscriptFileServiceImpl implements MeetingTranscriptFileSe
|
|||
}
|
||||
String markdown = buildTranscriptMarkdown(meeting, meetingDetail);
|
||||
Files.writeString(transcriptPath, markdown, StandardCharsets.UTF_8);
|
||||
return markdown.getBytes(StandardCharsets.UTF_8);
|
||||
return markdown;
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException("写入会议转录文件失败", ex);
|
||||
throw new RuntimeException("Failed to write meeting transcript markdown", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ export interface MeetingVO {
|
|||
keyPoints?: Array<{ title?: string; summary?: string; speaker?: string; time?: string }>;
|
||||
todos?: string[];
|
||||
};
|
||||
latestSummaryAttemptTaskId?: number;
|
||||
latestSummaryAttemptStatus?: number;
|
||||
latestSummaryAttemptErrorMsg?: string;
|
||||
latestChapterAttemptTaskId?: number;
|
||||
latestChapterAttemptStatus?: number;
|
||||
latestChapterAttemptErrorMsg?: string;
|
||||
status: number;
|
||||
displayStatus?: number;
|
||||
realtimeSessionStatus?: RealtimeMeetingSessionStatus["status"];
|
||||
|
|
@ -269,11 +275,12 @@ export interface PublicMeetingPreviewVO {
|
|||
chapters?: MeetingChapterVO[];
|
||||
}
|
||||
|
||||
export const getMeetingDetail = (id: number) => {
|
||||
export const getMeetingDetail = (id: number, options?: { suppressErrorToast?: boolean }) => {
|
||||
return http.get<{ code: string; data: MeetingVO; msg: string }>(
|
||||
`/api/biz/meeting/${id}`,
|
||||
{
|
||||
timeout: MEETING_DETAIL_TIMEOUT,
|
||||
suppressErrorToast: options?.suppressErrorToast,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -403,9 +410,12 @@ export interface MeetingProgress {
|
|||
eta?: number;
|
||||
}
|
||||
|
||||
export const getMeetingProgress = (id: number) => {
|
||||
export const getMeetingProgress = (id: number, options?: { suppressErrorToast?: boolean }) => {
|
||||
return http.get<{ code: string; data: MeetingProgress; msg: string }>(
|
||||
`/api/biz/meeting/${id}/progress`
|
||||
`/api/biz/meeting/${id}/progress`,
|
||||
{
|
||||
suppressErrorToast: options?.suppressErrorToast,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import axios from "axios";
|
||||
import { message } from "antd";
|
||||
|
||||
declare module "axios" {
|
||||
interface AxiosRequestConfig {
|
||||
suppressErrorToast?: boolean;
|
||||
}
|
||||
|
||||
interface InternalAxiosRequestConfig {
|
||||
suppressErrorToast?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: "/",
|
||||
timeout: 15000
|
||||
|
|
@ -122,7 +132,9 @@ http.interceptors.response.use(
|
|||
const body = resp.data;
|
||||
if (body && !isApiSuccessCode(body.code)) {
|
||||
const errorMsg = body.msg || "请求失败";
|
||||
if (!resp.config?.suppressErrorToast) {
|
||||
message.error(errorMsg);
|
||||
}
|
||||
const err = new Error(errorMsg);
|
||||
(err as any).code = body.code;
|
||||
(err as any).msg = body.msg;
|
||||
|
|
@ -154,7 +166,9 @@ http.interceptors.response.use(
|
|||
|
||||
const body = error.response?.data;
|
||||
const errorMsg = body?.msg || error.message || "网络异常";
|
||||
if (!originalRequest.suppressErrorToast) {
|
||||
message.error(errorMsg);
|
||||
}
|
||||
|
||||
if (body && body.msg) {
|
||||
const err = new Error(body.msg);
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ const PageContainer: React.FC<PageContainerProps> = ({
|
|||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -359,20 +359,31 @@ const MeetingProgressDisplay: React.FC<{
|
|||
const [progress, setProgress] = useState<MeetingProgress | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let completed = false;
|
||||
|
||||
const fetchProgress = async () => {
|
||||
if (completed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const [progressRes, detailRes] = await Promise.all([
|
||||
getMeetingProgress(meetingId),
|
||||
getMeetingDetail(meetingId),
|
||||
getMeetingProgress(meetingId, { suppressErrorToast: true }),
|
||||
getMeetingDetail(meetingId, { suppressErrorToast: true }),
|
||||
]);
|
||||
|
||||
if (detailRes.data?.data) {
|
||||
onProgressUpdate?.(detailRes.data.data);
|
||||
if (detailRes.data.data.status !== 1 && detailRes.data.data.status !== 2) {
|
||||
completed = true;
|
||||
onComplete();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (progressRes.data?.data) {
|
||||
setProgress(progressRes.data.data);
|
||||
if (progressRes.data.data.percent === 100) {
|
||||
if (progressRes.data.data.percent === 100 || progressRes.data.data.percent < 0) {
|
||||
completed = true;
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
|
@ -383,7 +394,10 @@ const MeetingProgressDisplay: React.FC<{
|
|||
|
||||
fetchProgress();
|
||||
const timer = setInterval(fetchProgress, 3000);
|
||||
return () => clearInterval(timer);
|
||||
return () => {
|
||||
completed = true;
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [meetingId, onComplete, onProgressUpdate]);
|
||||
|
||||
const percent = progress?.percent || 0;
|
||||
|
|
@ -902,6 +916,38 @@ 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(() => {
|
||||
if (!meeting || meeting.status !== 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasFallbackContent = Boolean(meeting.summaryContent) || meetingChapters.length > 0;
|
||||
if (meeting.latestChapterAttemptStatus === 3) {
|
||||
const detail = meeting.latestChapterAttemptErrorMsg || '章节生成失败';
|
||||
return {
|
||||
key: `chapter-${meeting.latestChapterAttemptTaskId ?? 'latest'}`,
|
||||
title: '本次重新总结失败',
|
||||
description: hasFallbackContent
|
||||
? `章节生成失败,当前展示的是上一次成功的摘要和 AI 目录。失败原因:${detail}`
|
||||
: `章节生成失败,且当前没有可展示的历史摘要或 AI 目录。失败原因:${detail}`,
|
||||
hasFallbackContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (meeting.latestSummaryAttemptStatus === 3) {
|
||||
const detail = meeting.latestSummaryAttemptErrorMsg || '总结生成失败';
|
||||
return {
|
||||
key: `summary-${meeting.latestSummaryAttemptTaskId ?? 'latest'}`,
|
||||
title: '本次重新总结失败',
|
||||
description: hasFallbackContent
|
||||
? `总结生成失败,当前展示的是上一次成功的摘要和 AI 目录。失败原因:${detail}`
|
||||
: `总结生成失败,且当前没有可展示的历史摘要或 AI 目录。失败原因:${detail}`,
|
||||
hasFallbackContent,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [meeting, meetingChapters.length]);
|
||||
const emptyTranscriptFailureNotice = useMemo(() => {
|
||||
if (!meeting || meeting.status !== 4 || transcripts.length > 0) {
|
||||
return null;
|
||||
|
|
@ -916,6 +962,24 @@ const MeetingDetail: React.FC = () => {
|
|||
};
|
||||
}, [canRetryTranscription, meeting, transcripts.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!generationFailureNotice) {
|
||||
return;
|
||||
}
|
||||
const acknowledgedKey = `meeting-failure-ack:${generationFailureNotice.key}`;
|
||||
if (sessionStorage.getItem(acknowledgedKey) === '1') {
|
||||
return;
|
||||
}
|
||||
Modal.warning({
|
||||
title: generationFailureNotice.title,
|
||||
content: generationFailureNotice.description,
|
||||
okText: '我知道了',
|
||||
onOk: () => {
|
||||
sessionStorage.setItem(acknowledgedKey, '1');
|
||||
},
|
||||
});
|
||||
}, [generationFailureNotice]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!playbackAudioUrl) {
|
||||
setShowFloatingTranscriptPlayer(false);
|
||||
|
|
@ -1831,11 +1895,26 @@ const MeetingDetail: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{generationFailureNotice && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message={generationFailureNotice.title}
|
||||
description={generationFailureNotice.description}
|
||||
/>
|
||||
)}
|
||||
|
||||
{meeting.status === 2 ? (
|
||||
<div className="summary-progress-shell">
|
||||
<MeetingProgressDisplay
|
||||
meetingId={meeting.id}
|
||||
onComplete={() => fetchData(meeting.id)}
|
||||
onProgressUpdate={(updated) => {
|
||||
if (updated.status !== meeting.status) {
|
||||
void fetchData(updated.id);
|
||||
}
|
||||
}}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -2029,6 +2108,15 @@ const MeetingDetail: React.FC = () => {
|
|||
<div className="transcript-scroll-shell">
|
||||
{workspaceTab === 'catalog' ? (
|
||||
<div className="catalog-list">
|
||||
{generationFailureNotice && !generationFailureNotice.hasFallbackContent && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="AI 目录生成失败"
|
||||
description={generationFailureNotice.description}
|
||||
/>
|
||||
)}
|
||||
{catalogChapterLinks.length ? (
|
||||
catalogChapterLinks.map((chapter, index) => (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -138,10 +138,10 @@ const useMeetingProgress = (meeting: MeetingVO, onComplete?: () => void) => {
|
|||
if (meeting.status !== 1 && meeting.status !== 2) return;
|
||||
const fetchProgress = async () => {
|
||||
try {
|
||||
const res = await getMeetingProgress(meeting.id);
|
||||
const res = await getMeetingProgress(meeting.id, { suppressErrorToast: true });
|
||||
if (res.data && res.data.data) {
|
||||
setProgress(res.data.data);
|
||||
if (res.data.data.percent === 100 && onComplete) {
|
||||
if ((res.data.data.percent === 100 || res.data.data.percent < 0) && onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
|
@ -604,7 +604,12 @@ const Meetings: React.FC = () => {
|
|||
</>
|
||||
}
|
||||
>
|
||||
<Card className="app-page__content-card" style={{ flex: 1, minHeight: 0 }} styles={{ body: { padding: '16px 24px', flex: 1 } }}>
|
||||
<Card
|
||||
className="app-page__content-card"
|
||||
style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
|
||||
styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}
|
||||
>
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px 24px' }}>
|
||||
{displayMode === 'card' ? (
|
||||
<Skeleton loading={loading} active paragraph={{ rows: 10 }}>
|
||||
<List
|
||||
|
|
@ -624,6 +629,7 @@ const Meetings: React.FC = () => {
|
|||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
scroll={{ x: 'max-content' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => handleOpenMeeting(record),
|
||||
style: { cursor: 'pointer' }
|
||||
|
|
@ -631,8 +637,11 @@ const Meetings: React.FC = () => {
|
|||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '0 24px 16px', flexShrink: 0 }}>
|
||||
<AppPagination current={current} pageSize={size} total={total} onChange={(p, s) => { setCurrent(p); setSize(s); }} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<MeetingCreateDrawer
|
||||
|
|
|
|||
Loading…
Reference in New Issue