Merge remote-tracking branch 'origin/codex/dev' into codex/dev
commit
3c2ac639b4
|
|
@ -262,7 +262,8 @@ def _build_task_status_model(task_record: Optional[Dict[str, Any]]) -> Optional[
|
|||
return TranscriptionTaskStatus(
|
||||
task_id=task_record.get('task_id'),
|
||||
status=task_record.get('status', 'pending') or 'pending',
|
||||
progress=int(task_record.get('progress') or 0)
|
||||
progress=int(task_record.get('progress') or 0),
|
||||
message=task_record.get('message')
|
||||
)
|
||||
|
||||
def _verify_meeting_owner(cursor, meeting_id: int, current_user_id: int):
|
||||
|
|
@ -499,8 +500,8 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren
|
|||
attendees_data = _load_attendees_map(cursor, [meeting_id]).get(meeting_id, [])
|
||||
attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data]
|
||||
tags = _load_tags_map(cursor, [meeting]).get(meeting_id, [])
|
||||
transcription_task = _load_latest_task_record(cursor, meeting_id, 'transcription')
|
||||
llm_task = _load_latest_task_record(cursor, meeting_id, 'llm')
|
||||
transcription_task = transcription_service.get_meeting_transcription_status(meeting_id)
|
||||
llm_task = async_meeting_service.get_meeting_llm_status(meeting_id)
|
||||
overall_status = _build_meeting_overall_status(transcription_task, llm_task)
|
||||
cursor.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import uuid
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pathlib import Path
|
||||
|
|
@ -96,7 +97,7 @@ class AsyncMeetingService:
|
|||
|
||||
# 3. 构建提示词
|
||||
self._update_task_status_in_redis(task_id, 'processing', 40, message="准备AI提示词...")
|
||||
full_prompt = self._build_prompt(transcript_text, user_prompt, prompt_id)
|
||||
full_prompt = self._build_prompt(meeting_id, transcript_text, user_prompt, prompt_id)
|
||||
|
||||
# 4. 调用LLM API(支持指定模型)
|
||||
self._update_task_status_in_redis(task_id, 'processing', 50, message="AI正在分析会议内容...")
|
||||
|
|
@ -345,23 +346,128 @@ class AsyncMeetingService:
|
|||
print(f"获取会议转录内容错误: {e}")
|
||||
return ""
|
||||
|
||||
def _build_prompt(self, transcript_text: str, user_prompt: str, prompt_id: Optional[int] = None) -> str:
|
||||
def _get_meeting_prompt_context(self, meeting_id: int) -> Dict[str, Any]:
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT m.title, m.meeting_time, u.caption AS creator_name
|
||||
FROM meetings m
|
||||
LEFT JOIN sys_users u ON m.user_id = u.user_id
|
||||
WHERE m.meeting_id = %s
|
||||
""",
|
||||
(meeting_id,)
|
||||
)
|
||||
meeting = cursor.fetchone()
|
||||
|
||||
if not meeting:
|
||||
cursor.close()
|
||||
return {}
|
||||
|
||||
meeting_time = meeting.get('meeting_time')
|
||||
meeting_time_text = ''
|
||||
if isinstance(meeting_time, datetime):
|
||||
meeting_time_text = meeting_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
elif meeting_time:
|
||||
meeting_time_text = str(meeting_time)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT u.caption
|
||||
FROM attendees a
|
||||
JOIN sys_users u ON a.user_id = u.user_id
|
||||
WHERE a.meeting_id = %s
|
||||
ORDER BY u.caption ASC
|
||||
""",
|
||||
(meeting_id,)
|
||||
)
|
||||
attendee_rows = cursor.fetchall()
|
||||
attendee_names = [row.get('caption') for row in attendee_rows if row.get('caption')]
|
||||
attendee_text = '、'.join(attendee_names)
|
||||
cursor.close()
|
||||
|
||||
return {
|
||||
'title': meeting.get('title') or '',
|
||||
'meeting_time': meeting_time_text,
|
||||
'meeting_time_value': meeting_time,
|
||||
'creator_name': meeting.get('creator_name') or '',
|
||||
'attendees': attendee_text
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"获取会议提示词上下文错误: {e}")
|
||||
return {}
|
||||
|
||||
def _format_meeting_time_value(self, meeting_time_value: Any, custom_format: Optional[str] = None) -> str:
|
||||
default_format = '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
if isinstance(meeting_time_value, datetime):
|
||||
try:
|
||||
return meeting_time_value.strftime(custom_format or default_format)
|
||||
except Exception:
|
||||
return meeting_time_value.strftime(default_format)
|
||||
|
||||
if meeting_time_value:
|
||||
meeting_time_text = str(meeting_time_value)
|
||||
if custom_format:
|
||||
try:
|
||||
parsed_time = datetime.fromisoformat(meeting_time_text.replace('Z', '+00:00'))
|
||||
return parsed_time.strftime(custom_format)
|
||||
except Exception:
|
||||
return meeting_time_text
|
||||
return meeting_time_text
|
||||
|
||||
return ''
|
||||
|
||||
def _apply_prompt_variables(self, template: str, variables: Dict[str, Any]) -> str:
|
||||
if not template:
|
||||
return template
|
||||
|
||||
rendered = re.sub(
|
||||
r"\{\{\s*meeting_time\s*:\s*([^{}]+?)\s*\}\}",
|
||||
lambda match: self._format_meeting_time_value(
|
||||
variables.get('meeting_time_value'),
|
||||
match.group(1).strip()
|
||||
),
|
||||
template
|
||||
)
|
||||
|
||||
for key, value in variables.items():
|
||||
if key == 'meeting_time_value':
|
||||
continue
|
||||
rendered = rendered.replace(f"{{{{{key}}}}}", value or '')
|
||||
rendered = rendered.replace(f"{{{{ {key} }}}}", value or '')
|
||||
return rendered
|
||||
|
||||
def _build_prompt(self, meeting_id: int, transcript_text: str, user_prompt: str, prompt_id: Optional[int] = None) -> str:
|
||||
"""
|
||||
构建完整的提示词
|
||||
使用数据库中配置的MEETING_TASK提示词模板
|
||||
|
||||
Args:
|
||||
meeting_id: 会议ID
|
||||
transcript_text: 会议转录文本
|
||||
user_prompt: 用户额外提示词
|
||||
prompt_id: 可选的提示词模版ID,如果不指定则使用默认模版
|
||||
"""
|
||||
# 从数据库获取会议任务的提示词模板(支持指定prompt_id)
|
||||
system_prompt = self.llm_service.get_task_prompt('MEETING_TASK', prompt_id=prompt_id)
|
||||
meeting_context = self._get_meeting_prompt_context(meeting_id)
|
||||
prompt_variables = {
|
||||
'meeting_id': str(meeting_id),
|
||||
'meeting_title': meeting_context.get('title', ''),
|
||||
'meeting_time': meeting_context.get('meeting_time', ''),
|
||||
'meeting_creator': meeting_context.get('creator_name', ''),
|
||||
'meeting_attendees': meeting_context.get('attendees', ''),
|
||||
'meeting_time_value': meeting_context.get('meeting_time_value')
|
||||
}
|
||||
system_prompt = self._apply_prompt_variables(system_prompt, prompt_variables)
|
||||
rendered_user_prompt = self._apply_prompt_variables(user_prompt, prompt_variables) if user_prompt else ''
|
||||
|
||||
prompt = f"{system_prompt}\n\n"
|
||||
|
||||
if user_prompt:
|
||||
prompt += f"用户额外要求:{user_prompt}\n\n"
|
||||
if rendered_user_prompt:
|
||||
prompt += f"用户额外要求:{rendered_user_prompt}\n\n"
|
||||
|
||||
prompt += f"会议转录内容:\n{transcript_text}\n\n请根据以上内容生成会议总结:"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
# 会议总结模板变量说明
|
||||
|
||||
## 概述
|
||||
|
||||
会议总结提示词模板支持使用变量占位符。只有在模板内容中显式写入变量时,系统才会替换对应值;未使用的变量不会自动追加到提示词中。
|
||||
|
||||
## 支持的变量
|
||||
|
||||
### 1. 会议 ID
|
||||
|
||||
```text
|
||||
{{meeting_id}}
|
||||
```
|
||||
|
||||
输出当前会议的唯一 ID。
|
||||
|
||||
### 2. 会议标题
|
||||
|
||||
```text
|
||||
{{meeting_title}}
|
||||
```
|
||||
|
||||
输出当前会议标题。
|
||||
|
||||
### 3. 会议时间
|
||||
|
||||
默认格式输出:
|
||||
|
||||
```text
|
||||
{{meeting_time}}
|
||||
```
|
||||
|
||||
默认格式示例:
|
||||
|
||||
```text
|
||||
2026-04-07 14:30:00
|
||||
```
|
||||
|
||||
自定义格式输出:
|
||||
|
||||
```text
|
||||
{{meeting_time:%Y年%m月%d日 %H:%M}}
|
||||
```
|
||||
|
||||
自定义格式示例:
|
||||
|
||||
```text
|
||||
2026年04月07日 14:30
|
||||
```
|
||||
|
||||
如果未设置自定义格式,则使用默认格式。若自定义格式无法解析,系统会回退到默认时间值。
|
||||
|
||||
### 4. 会议创建人
|
||||
|
||||
```text
|
||||
{{meeting_creator}}
|
||||
```
|
||||
|
||||
输出会议创建人名称。
|
||||
|
||||
### 5. 参会人员
|
||||
|
||||
```text
|
||||
{{meeting_attendees}}
|
||||
```
|
||||
|
||||
输出参会人员名称列表,多个名字使用 `、` 连接。
|
||||
|
||||
## 可直接复制的变量列表
|
||||
|
||||
```text
|
||||
{{meeting_id}}
|
||||
{{meeting_title}}
|
||||
{{meeting_time}}
|
||||
{{meeting_time:%Y年%m月%d日 %H:%M}}
|
||||
{{meeting_creator}}
|
||||
{{meeting_attendees}}
|
||||
```
|
||||
|
||||
## 模板示例
|
||||
|
||||
### 示例一:正式会议纪要
|
||||
|
||||
```text
|
||||
请根据会议转录内容生成正式会议纪要。
|
||||
|
||||
会议标题:{{meeting_title}}
|
||||
会议时间:{{meeting_time:%Y年%m月%d日 %H:%M}}
|
||||
创建人:{{meeting_creator}}
|
||||
参会人员:{{meeting_attendees}}
|
||||
|
||||
请输出:
|
||||
1. 会议背景
|
||||
2. 核心讨论
|
||||
3. 决议事项
|
||||
4. 待办事项
|
||||
```
|
||||
|
||||
### 示例二:简洁总结
|
||||
|
||||
```text
|
||||
请用简洁风格总结本次会议。
|
||||
会议:{{meeting_title}}
|
||||
时间:{{meeting_time}}
|
||||
```
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 变量必须使用双大括号包裹
|
||||
- 仅当模板中写入变量时,系统才会替换对应内容
|
||||
- `meeting_time` 支持自定义时间格式
|
||||
- 未填写的字段可能返回空字符串
|
||||
|
|
@ -109,6 +109,8 @@ const MeetingDetails = ({ user }) => {
|
|||
const [editDrawerOpen, setEditDrawerOpen] = useState(false);
|
||||
const [showQRModal, setShowQRModal] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [uploadStatusMessage, setUploadStatusMessage] = useState('');
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
const [accessPasswordEnabled, setAccessPasswordEnabled] = useState(false);
|
||||
const [accessPasswordDraft, setAccessPasswordDraft] = useState('');
|
||||
|
|
@ -133,11 +135,16 @@ const MeetingDetails = ({ user }) => {
|
|||
const transcriptRefs = useRef([]);
|
||||
const statusCheckIntervalRef = useRef(null);
|
||||
const summaryPollIntervalRef = useRef(null);
|
||||
const summaryBootstrapTimeoutRef = useRef(null);
|
||||
const activeSummaryTaskIdRef = useRef(null);
|
||||
const isMeetingOwner = user?.user_id === meeting?.creator_id;
|
||||
const creatorName = meeting?.creator_username || '未知创建人';
|
||||
const hasUploadedAudio = Boolean(audioUrl);
|
||||
const isTranscriptionRunning = ['pending', 'processing'].includes(transcriptionStatus?.status);
|
||||
const isSummaryRunning = summaryLoading;
|
||||
const displayUploadProgress = Math.max(0, Math.min(uploadProgress, 100));
|
||||
const displayTranscriptionProgress = Math.max(0, Math.min(transcriptionProgress, 100));
|
||||
const displaySummaryProgress = Math.max(0, Math.min(summaryTaskProgress, 100));
|
||||
const summaryDisabledReason = isUploading
|
||||
? '音频上传中,暂不允许重新总结'
|
||||
: !hasUploadedAudio
|
||||
|
|
@ -155,6 +162,7 @@ const MeetingDetails = ({ user }) => {
|
|||
return () => {
|
||||
if (statusCheckIntervalRef.current) clearInterval(statusCheckIntervalRef.current);
|
||||
if (summaryPollIntervalRef.current) clearInterval(summaryPollIntervalRef.current);
|
||||
if (summaryBootstrapTimeoutRef.current) clearTimeout(summaryBootstrapTimeoutRef.current);
|
||||
};
|
||||
}, [meeting_id]);
|
||||
|
||||
|
|
@ -194,25 +202,27 @@ const MeetingDetails = ({ user }) => {
|
|||
transcriptRefs.current[currentHighlightIndex]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}, [currentHighlightIndex, transcriptVisibleCount]);
|
||||
|
||||
const fetchMeetingDetails = async (options = {}) => {
|
||||
const { showPageLoading = true } = options;
|
||||
try {
|
||||
if (showPageLoading) {
|
||||
setLoading(true);
|
||||
const clearSummaryBootstrapPolling = () => {
|
||||
if (summaryBootstrapTimeoutRef.current) {
|
||||
clearTimeout(summaryBootstrapTimeoutRef.current);
|
||||
summaryBootstrapTimeoutRef.current = null;
|
||||
}
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)));
|
||||
setMeeting(response.data);
|
||||
if (response.data.prompt_id) {
|
||||
setSelectedPromptId(response.data.prompt_id);
|
||||
}
|
||||
setAccessPasswordEnabled(Boolean(response.data.access_password));
|
||||
setAccessPasswordDraft(response.data.access_password || '');
|
||||
};
|
||||
|
||||
if (response.data.transcription_status) {
|
||||
const ts = response.data.transcription_status;
|
||||
const applyMeetingDetailsState = (meetingData, options = {}) => {
|
||||
const { allowSummaryBootstrap = true } = options;
|
||||
setMeeting(meetingData);
|
||||
if (meetingData.prompt_id) {
|
||||
setSelectedPromptId(meetingData.prompt_id);
|
||||
}
|
||||
setAccessPasswordEnabled(Boolean(meetingData.access_password));
|
||||
setAccessPasswordDraft(meetingData.access_password || '');
|
||||
|
||||
if (meetingData.transcription_status) {
|
||||
const ts = meetingData.transcription_status;
|
||||
setTranscriptionStatus(ts);
|
||||
setTranscriptionProgress(ts.progress || 0);
|
||||
if (['pending', 'processing'].includes(ts.status)) {
|
||||
if (['pending', 'processing'].includes(ts.status) && ts.task_id) {
|
||||
startStatusPolling(ts.task_id);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -220,24 +230,87 @@ const MeetingDetails = ({ user }) => {
|
|||
setTranscriptionProgress(0);
|
||||
}
|
||||
|
||||
if (response.data.llm_status) {
|
||||
setSummaryTaskProgress(response.data.llm_status.progress || 0);
|
||||
setSummaryTaskMessage(response.data.llm_status.message || '');
|
||||
if (['pending', 'processing'].includes(response.data.llm_status.status)) {
|
||||
startSummaryPolling(response.data.llm_status.task_id);
|
||||
if (meetingData.llm_status) {
|
||||
const llmStatus = meetingData.llm_status;
|
||||
clearSummaryBootstrapPolling();
|
||||
setSummaryTaskProgress(llmStatus.progress || 0);
|
||||
setSummaryTaskMessage(
|
||||
llmStatus.message
|
||||
|| (llmStatus.status === 'processing'
|
||||
? 'AI 正在分析会议内容...'
|
||||
: llmStatus.status === 'pending'
|
||||
? 'AI 总结任务排队中...'
|
||||
: '')
|
||||
);
|
||||
if (['pending', 'processing'].includes(llmStatus.status) && llmStatus.task_id) {
|
||||
startSummaryPolling(llmStatus.task_id);
|
||||
} else {
|
||||
setSummaryLoading(false);
|
||||
}
|
||||
} else if (meetingData.transcription_status?.status === 'completed' && !meetingData.summary) {
|
||||
if (!activeSummaryTaskIdRef.current) {
|
||||
setSummaryLoading(true);
|
||||
setSummaryTaskProgress(0);
|
||||
setSummaryTaskMessage('转录完成,正在启动 AI 分析...');
|
||||
}
|
||||
if (allowSummaryBootstrap) {
|
||||
scheduleSummaryBootstrapPolling();
|
||||
}
|
||||
} else {
|
||||
clearSummaryBootstrapPolling();
|
||||
if (!activeSummaryTaskIdRef.current) {
|
||||
setSummaryLoading(false);
|
||||
setSummaryTaskProgress(0);
|
||||
setSummaryTaskMessage('');
|
||||
}
|
||||
}
|
||||
|
||||
const hasAudio = Boolean(response.data.audio_file_path && String(response.data.audio_file_path).length > 5);
|
||||
setAudioUrl(hasAudio ? buildApiUrl(`${API_ENDPOINTS.MEETINGS.AUDIO(meeting_id)}/stream`) : null);
|
||||
const hasAudioFile = Boolean(meetingData.audio_file_path && String(meetingData.audio_file_path).length > 5);
|
||||
setAudioUrl(hasAudioFile ? buildApiUrl(`${API_ENDPOINTS.MEETINGS.AUDIO(meeting_id)}/stream`) : null);
|
||||
};
|
||||
|
||||
const scheduleSummaryBootstrapPolling = (attempt = 0) => {
|
||||
if (summaryPollIntervalRef.current || activeSummaryTaskIdRef.current) {
|
||||
return;
|
||||
}
|
||||
clearSummaryBootstrapPolling();
|
||||
if (attempt >= 10) {
|
||||
setSummaryLoading(false);
|
||||
setSummaryTaskMessage('');
|
||||
return;
|
||||
}
|
||||
summaryBootstrapTimeoutRef.current = setTimeout(async () => {
|
||||
summaryBootstrapTimeoutRef.current = null;
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)));
|
||||
const meetingData = response.data;
|
||||
applyMeetingDetailsState(meetingData, { allowSummaryBootstrap: false });
|
||||
if (meetingData.llm_status || meetingData.summary) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
if (attempt >= 9) {
|
||||
setSummaryLoading(false);
|
||||
setSummaryTaskMessage('');
|
||||
return;
|
||||
}
|
||||
}
|
||||
scheduleSummaryBootstrapPolling(attempt + 1);
|
||||
}, attempt === 0 ? 1200 : 2000);
|
||||
};
|
||||
|
||||
const fetchMeetingDetails = async (options = {}) => {
|
||||
const { showPageLoading = true } = options;
|
||||
try {
|
||||
if (showPageLoading) {
|
||||
setLoading(true);
|
||||
}
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)));
|
||||
applyMeetingDetailsState(response.data);
|
||||
return response.data;
|
||||
} catch {
|
||||
message.error('加载会议详情失败');
|
||||
return null;
|
||||
} finally {
|
||||
if (showPageLoading) {
|
||||
setLoading(false);
|
||||
|
|
@ -285,6 +358,7 @@ const MeetingDetails = ({ user }) => {
|
|||
const status = res.data;
|
||||
setTranscriptionStatus(status);
|
||||
setTranscriptionProgress(status.progress || 0);
|
||||
setMeeting(prev => (prev ? { ...prev, transcription_status: status } : prev));
|
||||
if (['completed', 'failed', 'error', 'cancelled'].includes(status.status)) {
|
||||
clearInterval(interval);
|
||||
statusCheckIntervalRef.current = null;
|
||||
|
|
@ -350,7 +424,8 @@ const MeetingDetails = ({ user }) => {
|
|||
const statusRes = await apiClient.get(buildApiUrl(API_ENDPOINTS.TASKS.SUMMARY_STATUS(taskId)));
|
||||
const status = statusRes.data;
|
||||
setSummaryTaskProgress(status.progress || 0);
|
||||
setSummaryTaskMessage(status.message || '');
|
||||
setSummaryTaskMessage(status.message || 'AI 正在分析会议内容...');
|
||||
setMeeting(prev => (prev ? { ...prev, llm_status: status } : prev));
|
||||
|
||||
if (status.status === 'completed') {
|
||||
clearInterval(interval);
|
||||
|
|
@ -426,16 +501,33 @@ const MeetingDetails = ({ user }) => {
|
|||
formData.append('model_code', selectedModelCode);
|
||||
}
|
||||
setIsUploading(true);
|
||||
setUploadProgress(0);
|
||||
setUploadStatusMessage('正在上传音频文件...');
|
||||
try {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formData);
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formData, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
setUploadProgress(Math.min(100, Math.round((progressEvent.loaded * 100) / progressEvent.total)));
|
||||
}
|
||||
setUploadStatusMessage('正在上传音频文件...');
|
||||
}
|
||||
});
|
||||
setUploadProgress(100);
|
||||
setUploadStatusMessage('上传完成,正在启动转录任务...');
|
||||
message.success('音频上传成功');
|
||||
setTranscript([]);
|
||||
setSpeakerList([]);
|
||||
setEditingSpeakers({});
|
||||
fetchMeetingDetails({ showPageLoading: false });
|
||||
fetchTranscript();
|
||||
} catch { message.error('上传失败'); }
|
||||
finally { setIsUploading(false); }
|
||||
await fetchMeetingDetails({ showPageLoading: false });
|
||||
await fetchTranscript();
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.detail || '上传失败');
|
||||
throw error;
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
setUploadProgress(0);
|
||||
setUploadStatusMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
const saveAccessPassword = async () => {
|
||||
|
|
@ -729,7 +821,7 @@ const MeetingDetails = ({ user }) => {
|
|||
];
|
||||
|
||||
const audioMoreMenuItems = [
|
||||
{ key: 'transcribe', icon: <AudioOutlined />, label: '智能转录', disabled: !audioUrl || transcriptionStatus?.status === 'processing', onClick: handleStartTranscription },
|
||||
{ key: 'transcribe', icon: <AudioOutlined />, label: '智能转录', disabled: !audioUrl || isUploading || transcriptionStatus?.status === 'processing', onClick: handleStartTranscription },
|
||||
{ key: 'upload', icon: <UploadOutlined />, label: isUploading ? '上传中...' : '上传音频', disabled: isUploading, onClick: openAudioUploadPicker },
|
||||
];
|
||||
|
||||
|
|
@ -795,14 +887,56 @@ const MeetingDetails = ({ user }) => {
|
|||
</div>
|
||||
</Card>
|
||||
|
||||
{isUploading && (
|
||||
<Card
|
||||
variant="borderless"
|
||||
className="console-surface"
|
||||
style={{ marginBottom: 16, border: '1px solid #91caff', background: 'linear-gradient(135deg, #f0f9ff 0%, #ffffff 100%)' }}
|
||||
styles={{ body: { padding: '14px 24px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||
<Space size={10}>
|
||||
<UploadOutlined style={{ color: '#1677ff', fontSize: 16 }} />
|
||||
<div>
|
||||
<Text strong style={{ display: 'block' }}>音频上传中</Text>
|
||||
<Text type="secondary">{uploadStatusMessage || '正在上传音频文件...'}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
<Text strong style={{ color: '#1677ff' }}>{displayUploadProgress}%</Text>
|
||||
</div>
|
||||
<Progress percent={displayUploadProgress} status="active" strokeColor={{ from: '#69b1ff', to: '#1677ff' }} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ── 转录进度条 ── */}
|
||||
{transcriptionStatus && ['pending', 'processing'].includes(transcriptionStatus.status) && (
|
||||
<Card variant="borderless" className="console-surface" style={{ marginBottom: 16 }} styles={{ body: { padding: '12px 24px' } }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<Text><SyncOutlined spin style={{ marginRight: 8 }} />正在转录中...</Text>
|
||||
<Text>{transcriptionProgress}%</Text>
|
||||
<Text>{displayTranscriptionProgress}%</Text>
|
||||
</div>
|
||||
<Progress percent={transcriptionProgress} status="active" size="small" />
|
||||
<Progress percent={displayTranscriptionProgress} status="active" size="small" />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{isSummaryRunning && (
|
||||
<Card
|
||||
variant="borderless"
|
||||
className="console-surface"
|
||||
style={{ marginBottom: 16, border: '1px solid #d3adf7', background: 'linear-gradient(135deg, #f9f0ff 0%, #ffffff 100%)' }}
|
||||
styles={{ body: { padding: '14px 24px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||
<Space size={10}>
|
||||
<RobotOutlined style={{ color: '#722ed1', fontSize: 16 }} />
|
||||
<div>
|
||||
<Text strong style={{ display: 'block' }}>AI 正在分析会议内容</Text>
|
||||
<Text type="secondary">{summaryTaskMessage || '正在生成会议总结,请稍候...'}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
<Text strong style={{ color: '#722ed1' }}>{displaySummaryProgress}%</Text>
|
||||
</div>
|
||||
<Progress percent={displaySummaryProgress} status="active" strokeColor={{ from: '#b37feb', to: '#722ed1' }} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
|
|
@ -810,7 +944,14 @@ const MeetingDetails = ({ user }) => {
|
|||
<Upload
|
||||
id="audio-upload-input"
|
||||
showUploadList={false}
|
||||
customRequest={({ file }) => handleUploadAudio(file)}
|
||||
customRequest={async ({ file, onSuccess, onError }) => {
|
||||
try {
|
||||
await handleUploadAudio(file);
|
||||
onSuccess?.({}, file);
|
||||
} catch (error) {
|
||||
onError?.(error);
|
||||
}
|
||||
}}
|
||||
style={{ display: 'none' }}
|
||||
>
|
||||
<span />
|
||||
|
|
@ -941,18 +1082,6 @@ const MeetingDetails = ({ user }) => {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* 总结生成中进度条 */}
|
||||
{summaryLoading && (
|
||||
<div style={{ padding: '0 16px 16px', flexShrink: 0 }}>
|
||||
<Card variant="borderless" style={{ borderRadius: 10, background: '#f6f8fa' }} styles={{ body: { padding: '12px 16px' } }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<Text><SyncOutlined spin style={{ marginRight: 6 }} />{summaryTaskMessage || 'AI 正在思考中...'}</Text>
|
||||
<Text>{summaryTaskProgress}%</Text>
|
||||
</div>
|
||||
<Progress percent={summaryTaskProgress} status="active" size="small" />
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue