x修复前端无法自动总结,修复点击说话人修改问题,新增双击编辑文本内容问题
parent
a3ae293d42
commit
cc1817078a
|
|
@ -370,6 +370,12 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren
|
||||||
meeting_data.transcription_status = TranscriptionTaskStatus(**transcription_status_data)
|
meeting_data.transcription_status = TranscriptionTaskStatus(**transcription_status_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Failed to get transcription status for meeting {meeting_id}: {e}")
|
print(f"Warning: Failed to get transcription status for meeting {meeting_id}: {e}")
|
||||||
|
try:
|
||||||
|
llm_status_data = async_meeting_service.get_meeting_llm_status(meeting_id)
|
||||||
|
if llm_status_data:
|
||||||
|
meeting_data.llm_status = TranscriptionTaskStatus(**llm_status_data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Failed to get llm status for meeting {meeting_id}: {e}")
|
||||||
return create_api_response(code="200", message="获取会议详情成功", data=meeting_data)
|
return create_api_response(code="200", message="获取会议详情成功", data=meeting_data)
|
||||||
|
|
||||||
@router.get("/meetings/{meeting_id}/transcript")
|
@router.get("/meetings/{meeting_id}/transcript")
|
||||||
|
|
@ -543,14 +549,21 @@ async def upload_audio(
|
||||||
|
|
||||||
model_code = model_code.strip() if model_code else None
|
model_code = model_code.strip() if model_code else None
|
||||||
|
|
||||||
# 0. 如果没有传入 prompt_id,尝试获取默认模版ID
|
# 0. 如果没有传入 prompt_id,优先使用会议已配置模版,否则回退默认模版
|
||||||
if prompt_id is None:
|
if prompt_id is None:
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor(dictionary=True)
|
||||||
cursor.execute(
|
cursor.execute("SELECT prompt_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||||
"SELECT id FROM prompts WHERE task_type = 'MEETING_TASK' AND is_default = TRUE AND is_active = TRUE LIMIT 1"
|
meeting_row = cursor.fetchone()
|
||||||
)
|
if meeting_row and meeting_row.get('prompt_id') and int(meeting_row['prompt_id']) > 0:
|
||||||
prompt_id = cursor.fetchone()[0]
|
prompt_id = int(meeting_row['prompt_id'])
|
||||||
|
else:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM prompts WHERE task_type = 'MEETING_TASK' AND is_default = TRUE AND is_active = TRUE LIMIT 1"
|
||||||
|
)
|
||||||
|
prompt_row = cursor.fetchone()
|
||||||
|
prompt_id = prompt_row[0] if prompt_row else None
|
||||||
|
|
||||||
# 1. 文件类型验证
|
# 1. 文件类型验证
|
||||||
file_extension = os.path.splitext(audio_file.filename)[1].lower()
|
file_extension = os.path.splitext(audio_file.filename)[1].lower()
|
||||||
|
|
@ -779,12 +792,17 @@ def get_meeting_transcription_status(meeting_id: int, current_user: dict = Depen
|
||||||
return create_api_response(code="500", message=f"Failed to get meeting transcription status: {str(e)}")
|
return create_api_response(code="500", message=f"Failed to get meeting transcription status: {str(e)}")
|
||||||
|
|
||||||
@router.post("/meetings/{meeting_id}/transcription/start")
|
@router.post("/meetings/{meeting_id}/transcription/start")
|
||||||
def start_meeting_transcription(meeting_id: int, current_user: dict = Depends(get_current_user)):
|
def start_meeting_transcription(
|
||||||
|
meeting_id: int,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
current_user: dict = Depends(get_current_user)
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
cursor = connection.cursor(dictionary=True)
|
cursor = connection.cursor(dictionary=True)
|
||||||
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
cursor.execute("SELECT meeting_id, prompt_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||||
if not cursor.fetchone():
|
meeting = cursor.fetchone()
|
||||||
|
if not meeting:
|
||||||
return create_api_response(code="404", message="Meeting not found")
|
return create_api_response(code="404", message="Meeting not found")
|
||||||
cursor.execute("SELECT file_path FROM audio_files WHERE meeting_id = %s LIMIT 1", (meeting_id,))
|
cursor.execute("SELECT file_path FROM audio_files WHERE meeting_id = %s LIMIT 1", (meeting_id,))
|
||||||
audio_file = cursor.fetchone()
|
audio_file = cursor.fetchone()
|
||||||
|
|
@ -796,6 +814,13 @@ def start_meeting_transcription(meeting_id: int, current_user: dict = Depends(ge
|
||||||
"task_id": existing_status['task_id'], "status": existing_status['status']
|
"task_id": existing_status['task_id'], "status": existing_status['status']
|
||||||
})
|
})
|
||||||
task_id = transcription_service.start_transcription(meeting_id, audio_file['file_path'])
|
task_id = transcription_service.start_transcription(meeting_id, audio_file['file_path'])
|
||||||
|
background_tasks.add_task(
|
||||||
|
async_meeting_service.monitor_and_auto_summarize,
|
||||||
|
meeting_id,
|
||||||
|
task_id,
|
||||||
|
meeting.get('prompt_id') if meeting.get('prompt_id') not in (None, 0) else None,
|
||||||
|
None
|
||||||
|
)
|
||||||
return create_api_response(code="200", message="Transcription task started successfully", data={
|
return create_api_response(code="200", message="Transcription task started successfully", data={
|
||||||
"task_id": task_id, "meeting_id": meeting_id
|
"task_id": task_id, "meeting_id": meeting_id
|
||||||
})
|
})
|
||||||
|
|
@ -914,6 +939,12 @@ def generate_meeting_summary_async(meeting_id: int, request: GenerateSummaryRequ
|
||||||
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
return create_api_response(code="404", message="Meeting not found")
|
return create_api_response(code="404", message="Meeting not found")
|
||||||
|
transcription_status = transcription_service.get_meeting_transcription_status(meeting_id)
|
||||||
|
if transcription_status and transcription_status.get('status') in ['pending', 'processing']:
|
||||||
|
return create_api_response(code="409", message="转录进行中,暂不允许重新总结", data={
|
||||||
|
"task_id": transcription_status.get('task_id'),
|
||||||
|
"status": transcription_status.get('status')
|
||||||
|
})
|
||||||
# 传递 prompt_id 和 model_code 参数给服务层
|
# 传递 prompt_id 和 model_code 参数给服务层
|
||||||
task_id = async_meeting_service.start_summary_generation(meeting_id, request.user_prompt, request.prompt_id, request.model_code)
|
task_id = async_meeting_service.start_summary_generation(meeting_id, request.user_prompt, request.prompt_id, request.model_code)
|
||||||
background_tasks.add_task(async_meeting_service._process_task, task_id)
|
background_tasks.add_task(async_meeting_service._process_task, task_id)
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ class Meeting(BaseModel):
|
||||||
audio_duration: Optional[float] = None
|
audio_duration: Optional[float] = None
|
||||||
summary: Optional[str] = None
|
summary: Optional[str] = None
|
||||||
transcription_status: Optional[TranscriptionTaskStatus] = None
|
transcription_status: Optional[TranscriptionTaskStatus] = None
|
||||||
|
llm_status: Optional[TranscriptionTaskStatus] = None
|
||||||
prompt_id: Optional[int] = None
|
prompt_id: Optional[int] = None
|
||||||
prompt_name: Optional[str] = None
|
prompt_name: Optional[str] = None
|
||||||
overall_status: Optional[str] = None
|
overall_status: Optional[str] = None
|
||||||
|
|
@ -125,7 +126,7 @@ class BatchSpeakerTagUpdateRequest(BaseModel):
|
||||||
|
|
||||||
class TranscriptUpdateRequest(BaseModel):
|
class TranscriptUpdateRequest(BaseModel):
|
||||||
segment_id: int
|
segment_id: int
|
||||||
new_text: str
|
text_content: str
|
||||||
|
|
||||||
class BatchTranscriptUpdateRequest(BaseModel):
|
class BatchTranscriptUpdateRequest(BaseModel):
|
||||||
updates: List[TranscriptUpdateRequest]
|
updates: List[TranscriptUpdateRequest]
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,7 @@ class AsyncMeetingService:
|
||||||
'meeting_id': int(task_data.get('meeting_id', 0)),
|
'meeting_id': int(task_data.get('meeting_id', 0)),
|
||||||
'created_at': task_data.get('created_at'),
|
'created_at': task_data.get('created_at'),
|
||||||
'updated_at': task_data.get('updated_at'),
|
'updated_at': task_data.get('updated_at'),
|
||||||
|
'message': task_data.get('message'),
|
||||||
'result': task_data.get('result'),
|
'result': task_data.get('result'),
|
||||||
'error_message': task_data.get('error_message')
|
'error_message': task_data.get('error_message')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
EyeOutlined, FileTextOutlined, PartitionOutlined,
|
EyeOutlined, FileTextOutlined, PartitionOutlined,
|
||||||
SaveOutlined, CloseOutlined,
|
SaveOutlined, CloseOutlined,
|
||||||
StarFilled, RobotOutlined, DownloadOutlined,
|
StarFilled, RobotOutlined, DownloadOutlined,
|
||||||
DownOutlined,
|
DownOutlined, CheckOutlined,
|
||||||
MoreOutlined, AudioOutlined
|
MoreOutlined, AudioOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import MarkdownRenderer from '../components/MarkdownRenderer';
|
import MarkdownRenderer from '../components/MarkdownRenderer';
|
||||||
|
|
@ -65,6 +65,7 @@ const MeetingDetails = ({ user }) => {
|
||||||
const [summaryTaskProgress, setSummaryTaskProgress] = useState(0);
|
const [summaryTaskProgress, setSummaryTaskProgress] = useState(0);
|
||||||
const [summaryTaskMessage, setSummaryTaskMessage] = useState('');
|
const [summaryTaskMessage, setSummaryTaskMessage] = useState('');
|
||||||
const [summaryPollInterval, setSummaryPollInterval] = useState(null);
|
const [summaryPollInterval, setSummaryPollInterval] = useState(null);
|
||||||
|
const [activeSummaryTaskId, setActiveSummaryTaskId] = useState(null);
|
||||||
const [llmModels, setLlmModels] = useState([]);
|
const [llmModels, setLlmModels] = useState([]);
|
||||||
const [selectedModelCode, setSelectedModelCode] = useState(null);
|
const [selectedModelCode, setSelectedModelCode] = useState(null);
|
||||||
|
|
||||||
|
|
@ -87,10 +88,26 @@ const MeetingDetails = ({ user }) => {
|
||||||
// 总结内容编辑(同窗口)
|
// 总结内容编辑(同窗口)
|
||||||
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
||||||
const [editingSummaryContent, setEditingSummaryContent] = useState('');
|
const [editingSummaryContent, setEditingSummaryContent] = useState('');
|
||||||
|
const [inlineSpeakerEdit, setInlineSpeakerEdit] = useState(null);
|
||||||
|
const [inlineSpeakerEditSegmentId, setInlineSpeakerEditSegmentId] = useState(null);
|
||||||
|
const [inlineSpeakerValue, setInlineSpeakerValue] = useState('');
|
||||||
|
const [inlineSegmentEditId, setInlineSegmentEditId] = useState(null);
|
||||||
|
const [inlineSegmentValue, setInlineSegmentValue] = useState('');
|
||||||
|
const [savingInlineEdit, setSavingInlineEdit] = useState(false);
|
||||||
|
|
||||||
const audioRef = useRef(null);
|
const audioRef = useRef(null);
|
||||||
const transcriptRefs = useRef([]);
|
const transcriptRefs = useRef([]);
|
||||||
const isMeetingOwner = user?.user_id === meeting?.creator_id;
|
const isMeetingOwner = user?.user_id === meeting?.creator_id;
|
||||||
|
const hasUploadedAudio = Boolean(audioUrl);
|
||||||
|
const isTranscriptionRunning = ['pending', 'processing'].includes(transcriptionStatus?.status);
|
||||||
|
const summaryDisabledReason = isUploading
|
||||||
|
? '音频上传中,暂不允许重新总结'
|
||||||
|
: !hasUploadedAudio
|
||||||
|
? '请先上传音频后再总结'
|
||||||
|
: isTranscriptionRunning
|
||||||
|
? '转录进行中,完成后会自动总结'
|
||||||
|
: '';
|
||||||
|
const isSummaryActionDisabled = isUploading || !hasUploadedAudio || isTranscriptionRunning || summaryLoading;
|
||||||
|
|
||||||
/* ══════════════════ 数据获取 ══════════════════ */
|
/* ══════════════════ 数据获取 ══════════════════ */
|
||||||
|
|
||||||
|
|
@ -109,6 +126,9 @@ const MeetingDetails = ({ user }) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)));
|
||||||
setMeeting(response.data);
|
setMeeting(response.data);
|
||||||
|
if (response.data.prompt_id) {
|
||||||
|
setSelectedPromptId(response.data.prompt_id);
|
||||||
|
}
|
||||||
setAccessPasswordEnabled(Boolean(response.data.access_password));
|
setAccessPasswordEnabled(Boolean(response.data.access_password));
|
||||||
setAccessPasswordDraft(response.data.access_password || '');
|
setAccessPasswordDraft(response.data.access_password || '');
|
||||||
|
|
||||||
|
|
@ -119,6 +139,17 @@ const MeetingDetails = ({ user }) => {
|
||||||
if (['pending', 'processing'].includes(ts.status)) {
|
if (['pending', 'processing'].includes(ts.status)) {
|
||||||
startStatusPolling(ts.task_id);
|
startStatusPolling(ts.task_id);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setTranscriptionStatus(null);
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -127,6 +158,7 @@ const MeetingDetails = ({ user }) => {
|
||||||
} catch { setAudioUrl(null); }
|
} catch { setAudioUrl(null); }
|
||||||
|
|
||||||
fetchTranscript();
|
fetchTranscript();
|
||||||
|
fetchSummaryHistory();
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载会议详情失败');
|
message.error('加载会议详情失败');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -160,7 +192,13 @@ const MeetingDetails = ({ user }) => {
|
||||||
setTranscriptionProgress(status.progress || 0);
|
setTranscriptionProgress(status.progress || 0);
|
||||||
if (['completed', 'failed', 'error', 'cancelled'].includes(status.status)) {
|
if (['completed', 'failed', 'error', 'cancelled'].includes(status.status)) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
if (status.status === 'completed') fetchTranscript();
|
if (status.status === 'completed') {
|
||||||
|
fetchTranscript();
|
||||||
|
fetchMeetingDetails();
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchSummaryHistory();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch { clearInterval(interval); }
|
} catch { clearInterval(interval); }
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
@ -186,10 +224,66 @@ const MeetingDetails = ({ user }) => {
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startSummaryPolling = (taskId, options = {}) => {
|
||||||
|
const { closeDrawerOnComplete = false } = options;
|
||||||
|
if (!taskId) return;
|
||||||
|
if (summaryPollInterval && activeSummaryTaskId === taskId) return;
|
||||||
|
if (summaryPollInterval) clearInterval(summaryPollInterval);
|
||||||
|
|
||||||
|
setActiveSummaryTaskId(taskId);
|
||||||
|
setSummaryLoading(true);
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
const statusRes = await apiClient.get(buildApiUrl(API_ENDPOINTS.TASKS.SUMMARY_STATUS(taskId)));
|
||||||
|
const status = statusRes.data;
|
||||||
|
setSummaryTaskProgress(status.progress || 0);
|
||||||
|
setSummaryTaskMessage(status.message || '');
|
||||||
|
|
||||||
|
if (status.status === 'completed') {
|
||||||
|
clearInterval(interval);
|
||||||
|
setSummaryPollInterval(null);
|
||||||
|
setActiveSummaryTaskId(null);
|
||||||
|
setSummaryLoading(false);
|
||||||
|
if (closeDrawerOnComplete) {
|
||||||
|
setShowSummaryDrawer(false);
|
||||||
|
}
|
||||||
|
fetchSummaryHistory();
|
||||||
|
fetchMeetingDetails();
|
||||||
|
} else if (status.status === 'failed') {
|
||||||
|
clearInterval(interval);
|
||||||
|
setSummaryPollInterval(null);
|
||||||
|
setActiveSummaryTaskId(null);
|
||||||
|
setSummaryLoading(false);
|
||||||
|
message.error(status.error_message || '生成总结失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
clearInterval(interval);
|
||||||
|
setSummaryPollInterval(null);
|
||||||
|
setActiveSummaryTaskId(null);
|
||||||
|
setSummaryLoading(false);
|
||||||
|
message.error(error?.response?.data?.message || '获取总结状态失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const interval = setInterval(poll, 3000);
|
||||||
|
setSummaryPollInterval(interval);
|
||||||
|
poll();
|
||||||
|
};
|
||||||
|
|
||||||
const fetchSummaryHistory = async () => {
|
const fetchSummaryHistory = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(buildApiUrl(`/api/meetings/${meeting_id}/llm-tasks`));
|
const res = await apiClient.get(buildApiUrl(`/api/meetings/${meeting_id}/llm-tasks`));
|
||||||
setSummaryHistory(res.data.tasks?.filter(t => t.status === 'completed') || []);
|
const tasks = res.data.tasks || [];
|
||||||
|
setSummaryHistory(tasks.filter(t => t.status === 'completed'));
|
||||||
|
const latestRunningTask = tasks.find(t => ['pending', 'processing'].includes(t.status));
|
||||||
|
if (latestRunningTask) {
|
||||||
|
startSummaryPolling(latestRunningTask.task_id);
|
||||||
|
} else if (!activeSummaryTaskId) {
|
||||||
|
setSummaryLoading(false);
|
||||||
|
setSummaryTaskProgress(0);
|
||||||
|
setSummaryTaskMessage('');
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -217,6 +311,12 @@ const MeetingDetails = ({ user }) => {
|
||||||
formData.append('audio_file', file);
|
formData.append('audio_file', file);
|
||||||
formData.append('meeting_id', meeting_id);
|
formData.append('meeting_id', meeting_id);
|
||||||
formData.append('force_replace', 'true');
|
formData.append('force_replace', 'true');
|
||||||
|
if (meeting?.prompt_id) {
|
||||||
|
formData.append('prompt_id', String(meeting.prompt_id));
|
||||||
|
}
|
||||||
|
if (selectedModelCode) {
|
||||||
|
formData.append('model_code', selectedModelCode);
|
||||||
|
}
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
try {
|
try {
|
||||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formData);
|
await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formData);
|
||||||
|
|
@ -264,6 +364,81 @@ const MeetingDetails = ({ user }) => {
|
||||||
document.getElementById('audio-upload-input')?.click();
|
document.getElementById('audio-upload-input')?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startInlineSpeakerEdit = (speakerId, currentTag, segmentId) => {
|
||||||
|
setInlineSpeakerEdit(speakerId);
|
||||||
|
setInlineSpeakerEditSegmentId(`speaker-${speakerId}-${segmentId}`);
|
||||||
|
setInlineSpeakerValue(currentTag || `发言人 ${speakerId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelInlineSpeakerEdit = () => {
|
||||||
|
setInlineSpeakerEdit(null);
|
||||||
|
setInlineSpeakerEditSegmentId(null);
|
||||||
|
setInlineSpeakerValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveInlineSpeakerEdit = async () => {
|
||||||
|
if (inlineSpeakerEdit == null) return;
|
||||||
|
const nextTag = inlineSpeakerValue.trim();
|
||||||
|
if (!nextTag) {
|
||||||
|
message.warning('发言人名称不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSavingInlineEdit(true);
|
||||||
|
try {
|
||||||
|
await apiClient.put(buildApiUrl(`/api/meetings/${meeting_id}/speaker-tags/batch`), {
|
||||||
|
updates: [{ speaker_id: inlineSpeakerEdit, new_tag: nextTag }]
|
||||||
|
});
|
||||||
|
setTranscript(prev => prev.map(item => (
|
||||||
|
item.speaker_id === inlineSpeakerEdit
|
||||||
|
? { ...item, speaker_tag: nextTag }
|
||||||
|
: item
|
||||||
|
)));
|
||||||
|
setSpeakerList(prev => prev.map(item => (
|
||||||
|
item.speaker_id === inlineSpeakerEdit
|
||||||
|
? { ...item, speaker_tag: nextTag }
|
||||||
|
: item
|
||||||
|
)));
|
||||||
|
setEditingSpeakers(prev => ({ ...prev, [inlineSpeakerEdit]: nextTag }));
|
||||||
|
message.success('发言人名称已更新');
|
||||||
|
cancelInlineSpeakerEdit();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error?.response?.data?.message || '更新发言人名称失败');
|
||||||
|
} finally {
|
||||||
|
setSavingInlineEdit(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startInlineSegmentEdit = (segment) => {
|
||||||
|
setInlineSegmentEditId(segment.segment_id);
|
||||||
|
setInlineSegmentValue(segment.text_content || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelInlineSegmentEdit = () => {
|
||||||
|
setInlineSegmentEditId(null);
|
||||||
|
setInlineSegmentValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveInlineSegmentEdit = async () => {
|
||||||
|
if (inlineSegmentEditId == null) return;
|
||||||
|
setSavingInlineEdit(true);
|
||||||
|
try {
|
||||||
|
await apiClient.put(buildApiUrl(`/api/meetings/${meeting_id}/transcript/batch`), {
|
||||||
|
updates: [{ segment_id: inlineSegmentEditId, text_content: inlineSegmentValue }]
|
||||||
|
});
|
||||||
|
setTranscript(prev => prev.map(item => (
|
||||||
|
item.segment_id === inlineSegmentEditId
|
||||||
|
? { ...item, text_content: inlineSegmentValue }
|
||||||
|
: item
|
||||||
|
)));
|
||||||
|
message.success('转录内容已更新');
|
||||||
|
cancelInlineSegmentEdit();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error?.response?.data?.message || '更新转录内容失败');
|
||||||
|
} finally {
|
||||||
|
setSavingInlineEdit(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const changePlaybackRate = (nextRate) => {
|
const changePlaybackRate = (nextRate) => {
|
||||||
setPlaybackRate(nextRate);
|
setPlaybackRate(nextRate);
|
||||||
if (audioRef.current) {
|
if (audioRef.current) {
|
||||||
|
|
@ -298,6 +473,18 @@ const MeetingDetails = ({ user }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateSummary = async () => {
|
const generateSummary = async () => {
|
||||||
|
if (isUploading) {
|
||||||
|
message.warning('音频上传中,暂不允许重新总结');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasUploadedAudio) {
|
||||||
|
message.warning('请先上传音频后再总结');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isTranscriptionRunning) {
|
||||||
|
message.warning('转录进行中,暂不允许重新总结');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSummaryLoading(true);
|
setSummaryLoading(true);
|
||||||
setSummaryTaskProgress(0);
|
setSummaryTaskProgress(0);
|
||||||
try {
|
try {
|
||||||
|
|
@ -306,29 +493,26 @@ const MeetingDetails = ({ user }) => {
|
||||||
prompt_id: selectedPromptId,
|
prompt_id: selectedPromptId,
|
||||||
model_code: selectedModelCode
|
model_code: selectedModelCode
|
||||||
});
|
});
|
||||||
const taskId = res.data.task_id;
|
startSummaryPolling(res.data.task_id, { closeDrawerOnComplete: true });
|
||||||
const interval = setInterval(async () => {
|
} catch (error) {
|
||||||
const statusRes = await apiClient.get(buildApiUrl(API_ENDPOINTS.TASKS.SUMMARY_STATUS(taskId)));
|
message.error(error?.response?.data?.message || '生成总结失败');
|
||||||
const s = statusRes.data;
|
setSummaryLoading(false);
|
||||||
setSummaryTaskProgress(s.progress || 0);
|
}
|
||||||
setSummaryTaskMessage(s.message);
|
|
||||||
if (s.status === 'completed') {
|
|
||||||
clearInterval(interval);
|
|
||||||
setSummaryLoading(false);
|
|
||||||
setShowSummaryDrawer(false);
|
|
||||||
fetchSummaryHistory();
|
|
||||||
fetchMeetingDetails();
|
|
||||||
} else if (s.status === 'failed') {
|
|
||||||
clearInterval(interval);
|
|
||||||
setSummaryLoading(false);
|
|
||||||
message.error('生成总结失败');
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
setSummaryPollInterval(interval);
|
|
||||||
} catch { setSummaryLoading(false); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openSummaryDrawer = () => {
|
const openSummaryDrawer = () => {
|
||||||
|
if (isUploading) {
|
||||||
|
message.warning('音频上传中,暂不允许重新总结');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasUploadedAudio) {
|
||||||
|
message.warning('请先上传音频后再总结');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isTranscriptionRunning) {
|
||||||
|
message.warning('转录进行中,完成后会自动总结');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setShowSummaryDrawer(true);
|
setShowSummaryDrawer(true);
|
||||||
fetchSummaryHistory();
|
fetchSummaryHistory();
|
||||||
};
|
};
|
||||||
|
|
@ -444,7 +628,13 @@ const MeetingDetails = ({ user }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Space style={{ flexShrink: 0 }}>
|
<Space style={{ flexShrink: 0 }}>
|
||||||
<Button icon={<SyncOutlined />} onClick={openSummaryDrawer}>重新总结</Button>
|
<Tooltip title={summaryDisabledReason}>
|
||||||
|
<span>
|
||||||
|
<Button icon={<SyncOutlined />} onClick={openSummaryDrawer} disabled={isSummaryActionDisabled}>
|
||||||
|
重新总结
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
<Button icon={<DownloadOutlined />} onClick={downloadSummaryMd}>下载总结</Button>
|
<Button icon={<DownloadOutlined />} onClick={downloadSummaryMd}>下载总结</Button>
|
||||||
<Tooltip title="分享二维码">
|
<Tooltip title="分享二维码">
|
||||||
<Button icon={<QrcodeOutlined />} onClick={() => setShowQRModal(true)} />
|
<Button icon={<QrcodeOutlined />} onClick={() => setShowQRModal(true)} />
|
||||||
|
|
@ -574,16 +764,78 @@ const MeetingDetails = ({ user }) => {
|
||||||
icon={<UserOutlined />}
|
icon={<UserOutlined />}
|
||||||
style={{ backgroundColor: getSpeakerColor(item.speaker_id), flexShrink: 0 }}
|
style={{ backgroundColor: getSpeakerColor(item.speaker_id), flexShrink: 0 }}
|
||||||
/>
|
/>
|
||||||
<Text
|
{inlineSpeakerEdit === item.speaker_id && inlineSpeakerEditSegmentId === `speaker-${item.speaker_id}-${item.segment_id}` ? (
|
||||||
strong
|
<Space.Compact onClick={(e) => e.stopPropagation()}>
|
||||||
style={{ color: '#1677ff', cursor: 'pointer', fontSize: 13 }}
|
<Input
|
||||||
onClick={e => { e.stopPropagation(); openTranscriptEditDrawer(index); }}
|
size="small"
|
||||||
>
|
autoFocus
|
||||||
{item.speaker_tag || `发言人 ${item.speaker_id}`}
|
value={inlineSpeakerValue}
|
||||||
<EditOutlined style={{ fontSize: 11, marginLeft: 3 }} />
|
onChange={(e) => setInlineSpeakerValue(e.target.value)}
|
||||||
</Text>
|
onPressEnter={saveInlineSpeakerEdit}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
loading={savingInlineEdit}
|
||||||
|
onClick={saveInlineSpeakerEdit}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
disabled={savingInlineEdit}
|
||||||
|
onClick={cancelInlineSpeakerEdit}
|
||||||
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{ color: '#1677ff', cursor: 'pointer', fontSize: 13 }}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startInlineSpeakerEdit(item.speaker_id, item.speaker_tag, item.segment_id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.speaker_tag || `发言人 ${item.speaker_id}`}
|
||||||
|
<EditOutlined style={{ fontSize: 11, marginLeft: 3 }} />
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Text style={{ fontSize: 14, lineHeight: 1.7, color: '#333' }}>{item.text_content}</Text>
|
{inlineSegmentEditId === item.segment_id ? (
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Input.TextArea
|
||||||
|
autoFocus
|
||||||
|
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||||
|
value={inlineSegmentValue}
|
||||||
|
onChange={(e) => setInlineSegmentValue(e.target.value)}
|
||||||
|
onPressEnter={(e) => {
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
saveInlineSegmentEdit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Space style={{ marginTop: 8 }}>
|
||||||
|
<Button size="small" type="primary" icon={<CheckOutlined />} loading={savingInlineEdit} onClick={saveInlineSegmentEdit}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button size="small" icon={<CloseOutlined />} disabled={savingInlineEdit} onClick={cancelInlineSegmentEdit}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
style={{ fontSize: 14, lineHeight: 1.7, color: '#333', cursor: 'text' }}
|
||||||
|
onDoubleClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startInlineSegmentEdit(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.text_content}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ const PromptConfigPage = ({ user }) => {
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: 'config',
|
key: 'config',
|
||||||
label: '提示词配置',
|
label: '系统提示词配置',
|
||||||
children: (
|
children: (
|
||||||
<div className="console-tab-panel">
|
<div className="console-tab-panel">
|
||||||
<div className="console-tab-toolbar">
|
<div className="console-tab-toolbar">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue