feat: 优化ASR任务接管逻辑和前端多语言支持

- 在后端实现ASR任务接管逻辑,支持恢复未完成的任务
- 添加前端多语言支持,引入 `react-i18next` 库
-
dev_na
chenhao 2026-03-05 09:36:41 +08:00
parent 11ab76f2ed
commit 0ccf0aa87d
4 changed files with 144 additions and 104 deletions

View File

@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.net.URI; import java.net.URI;
import java.net.URLEncoder; import java.net.URLEncoder;
@ -60,30 +61,27 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
@Override @Override
@Async @Async
public void dispatchTasks(Long meetingId) { public void dispatchTasks(Long meetingId) {
// 尝试获取轮询锁,防止并发执行
String lockKey = RedisKeys.meetingPollingLockKey(meetingId); String lockKey = RedisKeys.meetingPollingLockKey(meetingId);
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES); Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(acquired)) { if (Boolean.FALSE.equals(acquired)) {
log.warn("Meeting {} task is already being processed by another thread", meetingId); log.warn("Meeting {} is already being processed", meetingId);
return; return;
} }
try { try {
log.info("Starting real AI processing for meeting ID: {}", meetingId);
Meeting meeting = meetingMapper.selectById(meetingId); Meeting meeting = meetingMapper.selectById(meetingId);
if (meeting == null) return; if (meeting == null) return;
// 1. 执行 ASR 识别 // 1. 执行 ASR 识别 (含接管逻辑)
String asrText = processAsrTask(meeting); String asrText = processAsrTask(meeting);
// 2. 执行 LLM 总结 // 2. 执行 LLM 总结
processSummaryTask(meeting, asrText); processSummaryTask(meeting, asrText);
// 完成后清除进度
redisTemplate.delete(RedisKeys.meetingProgressKey(meetingId)); redisTemplate.delete(RedisKeys.meetingProgressKey(meetingId));
} catch (Exception e) { } catch (Exception e) {
log.error("Meeting {} AI Task Flow failed", meetingId, e); log.error("Meeting {} AI Task Flow failed", meetingId, e);
updateMeetingStatus(meetingId, 4); // Overall Failed updateMeetingStatus(meetingId, 4);
updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0); updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0);
} finally { } finally {
redisTemplate.delete(lockKey); redisTemplate.delete(lockKey);
@ -95,13 +93,15 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
public void dispatchSummaryTask(Long meetingId) { public void dispatchSummaryTask(Long meetingId) {
Meeting meeting = meetingMapper.selectById(meetingId); Meeting meeting = meetingMapper.selectById(meetingId);
if (meeting == null) return; if (meeting == null) return;
try { try {
// 获取已有转录全文
List<MeetingTranscript> transcripts = transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>() List<MeetingTranscript> transcripts = transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, meetingId) .eq(MeetingTranscript::getMeetingId, meetingId)
.orderByAsc(MeetingTranscript::getStartTime)); .orderByAsc(MeetingTranscript::getStartTime));
if (transcripts.isEmpty()) {
throw new RuntimeException("没有找到可用的转录文本,无法生成总结");
}
String asrText = transcripts.stream() String asrText = transcripts.stream()
.map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent()) .map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent())
.collect(Collectors.joining("\n")); .collect(Collectors.joining("\n"));
@ -110,64 +110,73 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
} catch (Exception e) { } catch (Exception e) {
log.error("Re-summary failed for meeting {}", meetingId, e); log.error("Re-summary failed for meeting {}", meetingId, e);
updateMeetingStatus(meetingId, 4); updateMeetingStatus(meetingId, 4);
updateProgress(meetingId, -1, "总结失败: " + e.getMessage(), 0);
} }
} }
private String processAsrTask(Meeting meeting) throws Exception { private String processAsrTask(Meeting meeting) throws Exception {
updateMeetingStatus(meeting.getId(), 1); // 识别中 updateMeetingStatus(meeting.getId(), 1);
updateProgress(meeting.getId(), 5, "已提交识别请求...", 0);
AiModel asrModel = aiModelService.getById(meeting.getAsrModelId()); AiModel asrModel = aiModelService.getById(meeting.getAsrModelId());
if (asrModel == null) throw new RuntimeException("ASR Model config not found"); if (asrModel == null) throw new RuntimeException("ASR模型配置不存在");
// 构建请求参数并转码 String submitUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/recognition" : asrModel.getBaseUrl() + "/api/tasks/recognition";
Map<String, Object> req = new HashMap<>(); String taskId = null;
String rawAudioUrl = meeting.getAudioUrl(); AiTask taskRecord = null;
String encodedAudioUrl = Arrays.stream(rawAudioUrl.split("/"))
.map(part -> {
try {
return URLEncoder.encode(part, StandardCharsets.UTF_8).replace("+", "%20");
} catch (Exception e) { return part; }
})
.collect(Collectors.joining("/"));
String fullAudioUrl = serverBaseUrl + (encodedAudioUrl.startsWith("/") ? "" : "/") + encodedAudioUrl;
req.put("file_path", fullAudioUrl); // --- 核心:接管逻辑 ---
req.put("use_spk_id", meeting.getUseSpkId() != null && meeting.getUseSpkId() == 1); List<AiTask> existingTasks = this.list(new LambdaQueryWrapper<AiTask>()
.eq(AiTask::getMeetingId, meeting.getId())
.eq(AiTask::getTaskType, "ASR")
.orderByDesc(AiTask::getStartedAt));
// 处理热词权重 if (!existingTasks.isEmpty()) {
List<Map<String, Object>> formattedHotwords = new ArrayList<>(); AiTask lastTask = existingTasks.get(0);
if (meeting.getHotWords() != null && !meeting.getHotWords().isEmpty()) { if (lastTask.getResponseData() != null && lastTask.getResponseData().get("task_id") != null) {
List<HotWord> entities = hotWordService.list(new LambdaQueryWrapper<HotWord>() String potentialTaskId = lastTask.getResponseData().get("task_id").toString();
.eq(HotWord::getTenantId, meeting.getTenantId()) log.info("Attempting to resume ASR task {} for meeting {}", potentialTaskId, meeting.getId());
.in(HotWord::getWord, meeting.getHotWords()));
Map<String, Integer> weightMap = entities.stream().collect(Collectors.toMap(HotWord::getWord, HotWord::getWeight, (v1, v2) -> v1)); // 验证旧 taskId 是否依然有效
for (String w : meeting.getHotWords()) { String checkUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + potentialTaskId : asrModel.getBaseUrl() + "/api/tasks/" + potentialTaskId;
formattedHotwords.add(Map.of("hotword", w, "weight", weightMap.getOrDefault(w, 10) / 10.0)); try {
String checkResp = get(checkUrl);
JsonNode node = objectMapper.readTree(checkResp);
if (node.path("code").asInt() == 200) {
taskId = potentialTaskId;
taskRecord = lastTask;
log.info("Successfully resumed ASR task {}", taskId);
}
} catch (Exception e) {
log.warn("Old task ID {} is invalid, will start a new one", potentialTaskId);
}
} }
} }
req.put("hotwords", formattedHotwords);
AiTask taskRecord = createAiTask(meeting.getId(), "ASR", req); // 如果没有可接管的任务,则发起新任务
if (taskId == null) {
updateProgress(meeting.getId(), 5, "正在提交识别请求...", 0);
Map<String, Object> req = buildAsrRequest(meeting);
taskRecord = createAiTask(meeting.getId(), "ASR", req);
// 提交 String respBody = postJson(submitUrl, req);
String submitUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/recognition" : asrModel.getBaseUrl() + "/api/tasks/recognition"; JsonNode submitNode = objectMapper.readTree(respBody);
String respBody = postJson(submitUrl, req); if (submitNode.path("code").asInt() != 200) {
JsonNode submitNode = objectMapper.readTree(respBody); updateAiTaskFail(taskRecord, "Submission Failed: " + respBody);
if (submitNode.path("code").asInt() != 200) { throw new RuntimeException("ASR引擎拒绝请求: " + submitNode.path("msg").asText());
updateAiTaskFail(taskRecord, "Submission Failed: " + respBody); }
throw new RuntimeException("ASR引擎拒绝请求: " + submitNode.path("msg").asText()); taskId = submitNode.path("data").path("task_id").asText();
taskRecord.setResponseData(Map.of("task_id", taskId));
this.updateById(taskRecord);
} }
String taskId = submitNode.path("data").path("task_id").asText();
String queryUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + taskId : asrModel.getBaseUrl() + "/api/tasks/" + taskId; String queryUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + taskId : asrModel.getBaseUrl() + "/api/tasks/" + taskId;
// 轮询带防护 // 轮询逻辑 (带防卡死防)
JsonNode resultNode = null; JsonNode resultNode = null;
int lastPercent = -1; int lastPercent = -1;
int unchangedCount = 0; int unchangedCount = 0;
for (int i = 0; i < 600; i++) { // Max 20 minutes for (int i = 0; i < 600; i++) {
Thread.sleep(2000); Thread.sleep(2000);
String queryResp = get(queryUrl); String queryResp = get(queryUrl);
JsonNode statusNode = objectMapper.readTree(queryResp); JsonNode statusNode = objectMapper.readTree(queryResp);
@ -177,25 +186,17 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
if ("completed".equalsIgnoreCase(status)) { if ("completed".equalsIgnoreCase(status)) {
resultNode = data.path("result"); resultNode = data.path("result");
updateAiTaskSuccess(taskRecord, statusNode); updateAiTaskSuccess(taskRecord, statusNode);
updateProgress(meeting.getId(), 85, "语音转录完成,准备进行总结...", 0);
break; break;
} else if ("failed".equalsIgnoreCase(status)) { } else if ("failed".equalsIgnoreCase(status)) {
updateAiTaskFail(taskRecord, "ASR reported failure: " + queryResp); updateAiTaskFail(taskRecord, "ASR engine reported failure: " + queryResp);
throw new RuntimeException("ASR处理失败: " + data.path("message").asText()); throw new RuntimeException("ASR引擎处理失败: " + data.path("message").asText());
} else { } else {
// 处理中:同步进度到 Redis
int currentPercent = data.path("percentage").asInt(); int currentPercent = data.path("percentage").asInt();
String message = data.path("message").asText();
int eta = data.path("eta").asInt(0); int eta = data.path("eta").asInt(0);
updateProgress(meeting.getId(), (int)(currentPercent * 0.85), data.path("message").asText(), eta);
// 缩放到 0-85% 范围
int scaledPercent = (int)(currentPercent * 0.85);
updateProgress(meeting.getId(), Math.max(5, scaledPercent), message, eta);
// 防死循环逻辑:如果进度长时间不动且不是 0
if (currentPercent > 0 && currentPercent == lastPercent) { if (currentPercent > 0 && currentPercent == lastPercent) {
unchangedCount++; if (++unchangedCount > 45) throw new RuntimeException("识别任务长时间无进度增长,自动强制超时");
if (unchangedCount > 40) throw new RuntimeException("ASR处理停滞自动超时");
} else { } else {
unchangedCount = 0; unchangedCount = 0;
} }
@ -205,7 +206,40 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
if (resultNode == null) throw new RuntimeException("ASR轮询超时"); if (resultNode == null) throw new RuntimeException("ASR轮询超时");
// 解析并入库 (适配新 Speaker 格式) // 解析并入库 (防御性清理旧数据)
return saveTranscripts(meeting, resultNode);
}
private Map<String, Object> buildAsrRequest(Meeting meeting) {
Map<String, Object> req = new HashMap<>();
String rawAudioUrl = meeting.getAudioUrl();
String encodedAudioUrl = Arrays.stream(rawAudioUrl.split("/"))
.map(part -> {
try { return URLEncoder.encode(part, StandardCharsets.UTF_8).replace("+", "%20"); }
catch (Exception e) { return part; }
})
.collect(Collectors.joining("/"));
req.put("file_path", serverBaseUrl + (encodedAudioUrl.startsWith("/") ? "" : "/") + encodedAudioUrl);
req.put("use_spk_id", meeting.getUseSpkId() != null && meeting.getUseSpkId() == 1);
List<Map<String, Object>> hotwords = new ArrayList<>();
if (meeting.getHotWords() != null && !meeting.getHotWords().isEmpty()) {
List<HotWord> entities = hotWordService.list(new LambdaQueryWrapper<HotWord>()
.eq(HotWord::getTenantId, meeting.getTenantId()).in(HotWord::getWord, meeting.getHotWords()));
Map<String, Integer> weightMap = entities.stream().collect(Collectors.toMap(HotWord::getWord, HotWord::getWeight, (v1, v2) -> v1));
for (String w : meeting.getHotWords()) {
hotwords.add(Map.of("hotword", w, "weight", weightMap.getOrDefault(w, 10) / 10.0));
}
}
req.put("hotwords", hotwords);
return req;
}
@Transactional(rollbackFor = Exception.class)
protected String saveTranscripts(Meeting meeting, JsonNode resultNode) {
// 关键:入库前清理旧记录,防止恢复任务导致数据重复
transcriptMapper.delete(new LambdaQueryWrapper<MeetingTranscript>().eq(MeetingTranscript::getMeetingId, meeting.getId()));
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
JsonNode segments = resultNode.path("segments"); JsonNode segments = resultNode.path("segments");
if (segments.isArray()) { if (segments.isArray()) {
@ -214,12 +248,10 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
MeetingTranscript mt = new MeetingTranscript(); MeetingTranscript mt = new MeetingTranscript();
mt.setMeetingId(meeting.getId()); mt.setMeetingId(meeting.getId());
// 解析 Speaker 对象
JsonNode spkNode = seg.path("speaker"); JsonNode spkNode = seg.path("speaker");
String spkId = spkNode.path("user_id").asText("spk_0"); String spkId = spkNode.path("user_id").asText("spk_0");
String spkName = spkNode.path("name").asText(spkId); String spkName = spkNode.path("name").asText(spkId);
// 用户名称转换逻辑
if (spkId.matches("\\d+")) { if (spkId.matches("\\d+")) {
SysUser user = sysUserMapper.selectById(Long.parseLong(spkId)); SysUser user = sysUserMapper.selectById(Long.parseLong(spkId));
if (user != null) spkName = user.getDisplayName() != null ? user.getDisplayName() : user.getUsername(); if (user != null) spkName = user.getDisplayName() != null ? user.getDisplayName() : user.getUsername();
@ -241,8 +273,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
} }
private void processSummaryTask(Meeting meeting, String asrText) throws Exception { private void processSummaryTask(Meeting meeting, String asrText) throws Exception {
updateMeetingStatus(meeting.getId(), 2); // 总结中 updateMeetingStatus(meeting.getId(), 2);
updateProgress(meeting.getId(), 90, "正在进行 AI 智能总结...", 0); updateProgress(meeting.getId(), 90, "正在生成智能总结纪要...", 0);
AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId()); AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId());
if (llmModel == null) return; if (llmModel == null) return;
@ -252,7 +284,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
req.put("temperature", llmModel.getTemperature()); req.put("temperature", llmModel.getTemperature());
req.put("messages", List.of( req.put("messages", List.of(
Map.of("role", "system", "content", meeting.getPromptContent()), Map.of("role", "system", "content", meeting.getPromptContent()),
Map.of("role", "user", "content", "请总结以下内容:\n" + asrText) Map.of("role", "user", "content", "请总结以下会议内容:\n" + asrText)
)); ));
AiTask taskRecord = createAiTask(meeting.getId(), "SUMMARY", req); AiTask taskRecord = createAiTask(meeting.getId(), "SUMMARY", req);
@ -271,13 +303,13 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
if (response.statusCode() == 200 && respNode.has("choices")) { if (response.statusCode() == 200 && respNode.has("choices")) {
String content = respNode.path("choices").path(0).path("message").path("content").asText(); String content = respNode.path("choices").path(0).path("message").path("content").asText();
meeting.setSummaryContent(content); meeting.setSummaryContent(content);
meeting.setStatus(3); // Finished meeting.setStatus(3);
meetingMapper.updateById(meeting); meetingMapper.updateById(meeting);
updateAiTaskSuccess(taskRecord, respNode); updateAiTaskSuccess(taskRecord, respNode);
updateProgress(meeting.getId(), 100, "分析完成", 0); updateProgress(meeting.getId(), 100, "全流程分析完成", 0);
} else { } else {
updateAiTaskFail(taskRecord, "LLM failed: " + response.body()); updateAiTaskFail(taskRecord, "LLM Summary failed: " + response.body());
throw new RuntimeException("AI总结生成失败"); throw new RuntimeException("AI总结生成异常");
} }
} }
@ -291,53 +323,38 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
redisTemplate.opsForValue().set(RedisKeys.meetingProgressKey(meetingId), redisTemplate.opsForValue().set(RedisKeys.meetingProgressKey(meetingId),
objectMapper.writeValueAsString(progress), 1, TimeUnit.HOURS); objectMapper.writeValueAsString(progress), 1, TimeUnit.HOURS);
} catch (Exception e) { } catch (Exception e) {
log.error("Update progress to redis failed", e); log.error("Redis progress update error", e);
} }
} }
private String postJson(String url, Object body) throws Exception { private String postJson(String url, Object body) throws Exception {
HttpRequest request = HttpRequest.newBuilder() return httpClient.send(HttpRequest.newBuilder().uri(URI.create(url)).header("Content-Type", "application/json")
.uri(URI.create(url)) .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body))).build(),
.header("Content-Type", "application/json") HttpResponse.BodyHandlers.ofString()).body();
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))
.build();
return httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body();
} }
private String get(String url) throws Exception { private String get(String url) throws Exception {
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build(); return httpClient.send(HttpRequest.newBuilder().uri(URI.create(url)).GET().build(), HttpResponse.BodyHandlers.ofString()).body();
return httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body();
} }
private void updateMeetingStatus(Long id, int status) { private void updateMeetingStatus(Long id, int status) {
Meeting m = new Meeting(); Meeting m = new Meeting(); m.setId(id); m.setStatus(status); meetingMapper.updateById(m);
m.setId(id);
m.setStatus(status);
meetingMapper.updateById(m);
} }
private AiTask createAiTask(Long meetingId, String type, Map<String, Object> req) { private AiTask createAiTask(Long meetingId, String type, Map<String, Object> req) {
AiTask task = new AiTask(); AiTask task = new AiTask();
task.setMeetingId(meetingId); task.setMeetingId(meetingId); task.setTaskType(type); task.setStatus(1);
task.setTaskType(type); task.setRequestData(req); task.setStartedAt(LocalDateTime.now());
task.setStatus(1); this.save(task); return task;
task.setRequestData(req);
task.setStartedAt(LocalDateTime.now());
this.save(task);
return task;
} }
private void updateAiTaskSuccess(AiTask task, JsonNode resp) { private void updateAiTaskSuccess(AiTask task, JsonNode resp) {
task.setStatus(2); task.setStatus(2); task.setResponseData(objectMapper.convertValue(resp, Map.class));
task.setResponseData(objectMapper.convertValue(resp, Map.class)); task.setCompletedAt(LocalDateTime.now()); this.updateById(task);
task.setCompletedAt(LocalDateTime.now());
this.updateById(task);
} }
private void updateAiTaskFail(AiTask task, String error) { private void updateAiTaskFail(AiTask task, String error) {
task.setStatus(3); task.setStatus(3); task.setErrorMsg(error);
task.setErrorMsg(error); task.setCompletedAt(LocalDateTime.now()); this.updateById(task);
task.setCompletedAt(LocalDateTime.now());
this.updateById(task);
} }
} }

View File

@ -8,6 +8,7 @@ import {
PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined,
UserOutlined, GlobalOutlined UserOutlined, GlobalOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useDict } from '../../hooks/useDict'; import { useDict } from '../../hooks/useDict';
import { import {
getHotWordPage, getHotWordPage,
@ -23,6 +24,7 @@ const { Option } = Select;
const { Text } = Typography; const { Text } = Typography;
const HotWords: React.FC = () => { const HotWords: React.FC = () => {
const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { items: categories, loading: dictLoading } = useDict('biz_hotword_category'); const { items: categories, loading: dictLoading } = useDict('biz_hotword_category');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);

View File

@ -41,7 +41,7 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
// 格式化剩余时间 (ETA) // 格式化剩余时间 (ETA)
const formatETA = (seconds?: number) => { const formatETA = (seconds?: number) => {
if (!seconds || seconds <= 0) return '即将完成'; if (!seconds || seconds <= 0) return '正在分析中';
if (seconds < 60) return `${seconds}`; if (seconds < 60) return `${seconds}`;
const m = Math.floor(seconds / 60); const m = Math.floor(seconds / 60);
const s = seconds % 60; const s = seconds % 60;
@ -67,7 +67,7 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
<Text strong style={{ fontSize: 18, color: isError ? '#ff4d4f' : '#1890ff', display: 'block', marginBottom: 8 }}> <Text strong style={{ fontSize: 18, color: isError ? '#ff4d4f' : '#1890ff', display: 'block', marginBottom: 8 }}>
{progress?.message || '正在准备计算资源...'} {progress?.message || '正在准备计算资源...'}
</Text> </Text>
<Text type="secondary"> 1-3 </Text> <Text type="secondary"></Text>
</div> </div>
<Divider style={{ margin: '32px 0' }} /> <Divider style={{ margin: '32px 0' }} />
<Row gutter={24}> <Row gutter={24}>
@ -304,7 +304,28 @@ const MeetingDetail: React.FC = () => {
</Col> </Col>
<Col> <Col>
<Space> <Space>
{isOwner && <Button icon={<SyncOutlined />} type="primary" ghost onClick={() => setSummaryVisible(true)}></Button>} {isOwner && meeting.status === 3 && (
<Button
icon={<SyncOutlined />}
type="primary"
ghost
onClick={() => setSummaryVisible(true)}
disabled={actionLoading}
>
</Button>
)}
{isOwner && meeting.status === 2 && (
<Button
icon={<LoadingOutlined />}
type="primary"
ghost
disabled
loading
>
</Button>
)}
<Button icon={<LeftOutlined />} onClick={() => navigate('/meetings')}></Button> <Button icon={<LeftOutlined />} onClick={() => navigate('/meetings')}></Button>
</Space> </Space>
</Col> </Col>

View File

@ -241,10 +241,10 @@ const PromptTemplates: React.FC = () => {
</Space> </Space>
</div> </div>
<div style={{ marginBottom: 12 }}> {/*<div style={{ marginBottom: 12 }}>*/}
<Text strong style={{ fontSize: 16, display: 'block' }} ellipsis={{ tooltip: item.templateName }}>{item.templateName}</Text> {/* <Text strong style={{ fontSize: 16, display: 'block' }} ellipsis={{ tooltip: item.templateName }}>{item.templateName}</Text>*/}
<Text type="secondary" style={{ fontSize: 12 }}>使: {item.usageCount || 0}</Text> {/* <Text type="secondary" style={{ fontSize: 12 }}>使用次数: {item.usageCount || 0}</Text>*/}
</div> {/*</div>*/}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 20, height: 22, overflow: 'hidden' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 20, height: 22, overflow: 'hidden' }}>
{item.tags?.map(tag => { {item.tags?.map(tag => {