import React, { useState, useEffect, useRef } from 'react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import apiClient from '../utils/apiClient'; import { ArrowLeft, Clock, Users, FileText, User, Calendar, Play, Pause, Volume2, MessageCircle, Edit, Trash2, Settings, Save, X, Edit3, Brain, Sparkles, Download, ArrowDown, RefreshCw, RefreshCwOff } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import rehypeSanitize from 'rehype-sanitize'; import { buildApiUrl, API_ENDPOINTS, API_BASE_URL } from '../config/api'; import MindMap from '../components/MindMap'; import MeetingSummary from '../components/MeetingSummary'; import { Tabs } from 'antd'; import './MeetingDetails.css'; const { TabPane } = Tabs; const MeetingDetails = ({ user }) => { const { meeting_id } = useParams(); const navigate = useNavigate(); const [meeting, setMeeting] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolume] = useState(1); const [transcript, setTranscript] = useState([]); const [showTranscript, setShowTranscript] = useState(true); const [audioUrl, setAudioUrl] = useState(null); const [audioFileName, setAudioFileName] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showSummaryError, setShowSummaryError] = useState(false); const [showSpeakerEdit, setShowSpeakerEdit] = useState(false); const [editingSpeakers, setEditingSpeakers] = useState({}); const [speakerList, setSpeakerList] = useState([]); const [showTranscriptEdit, setShowTranscriptEdit] = useState(false); const [transcriptionStatus, setTranscriptionStatus] = useState(null); const [transcriptionProgress, setTranscriptionProgress] = useState(0); const [statusCheckInterval, setStatusCheckInterval] = useState(null); const [autoScrollEnabled, setAutoScrollEnabled] = useState(false); // 控制自动滚动 const [editingTranscriptIndex, setEditingTranscriptIndex] = useState(-1); const [editingTranscripts, setEditingTranscripts] = useState({}); const [currentSubtitle, setCurrentSubtitle] = useState(''); const [currentSpeaker, setCurrentSpeaker] = useState(''); const [showSummaryModal, setShowSummaryModal] = useState(false); const [summaryLoading, setSummaryLoading] = useState(false); const [summaryResult, setSummaryResult] = useState(null); const [userPrompt, setUserPrompt] = useState(''); const [summaryHistory, setSummaryHistory] = useState([]); const [currentHighlightIndex, setCurrentHighlightIndex] = useState(-1); const [summaryTaskId, setSummaryTaskId] = useState(null); const [summaryTaskStatus, setSummaryTaskStatus] = useState(null); const [summaryTaskProgress, setSummaryTaskProgress] = useState(0); const [summaryTaskMessage, setSummaryTaskMessage] = useState(''); const [summaryPollInterval, setSummaryPollInterval] = useState(null); const audioRef = useRef(null); const transcriptRefs = useRef([]); useEffect(() => { fetchMeetingDetails(); // Cleanup interval on unmount return () => { if (statusCheckInterval) { console.log('组件卸载,清理转录状态轮询定时器'); clearInterval(statusCheckInterval); setStatusCheckInterval(null); } if (summaryPollInterval) { console.log('组件卸载,清理总结任务轮询定时器'); clearInterval(summaryPollInterval); setSummaryPollInterval(null); } }; }, [meeting_id]); // Cleanup interval when status changes useEffect(() => { if (transcriptionStatus) { // 如果转录已完成、失败或取消,清除轮询 if (['completed', 'failed', 'error', 'cancelled'].includes(transcriptionStatus.status)) { if (statusCheckInterval) { clearInterval(statusCheckInterval); setStatusCheckInterval(null); } } } }, [transcriptionStatus, statusCheckInterval]); const refreshTranscriptData = async () => { try { const baseUrl = ""; const transcriptEndpoint = API_ENDPOINTS?.MEETINGS?.TRANSCRIPT?.(meeting_id) || `/api/meetings/${meeting_id}/transcript`; // 只刷新转录数据,不显示loading const transcriptResponse = await apiClient.get(`${baseUrl}${transcriptEndpoint}`); setTranscript(transcriptResponse.data); // 更新发言人列表 const allSpeakerIds = transcriptResponse.data .map(item => item.speaker_id) .filter(speakerId => speakerId !== null && speakerId !== undefined); const uniqueSpeakers = [...new Set(allSpeakerIds)] .map(speakerId => { const segment = transcriptResponse.data.find(item => item.speaker_id === speakerId); return { speaker_id: speakerId, speaker_tag: segment ? (segment.speaker_tag || `发言人 ${speakerId}`) : `发言人 ${speakerId}` }; }) .sort((a, b) => a.speaker_id - b.speaker_id); setSpeakerList(uniqueSpeakers); console.log('转录数据已刷新,无loading状态'); } catch (error) { console.error('刷新转录数据失败:', error); } }; const startStatusPolling = (taskId) => { // Clear existing interval if (statusCheckInterval) { clearInterval(statusCheckInterval); setStatusCheckInterval(null); } // Poll every 3 seconds const interval = setInterval(async () => { try { const baseUrl = ""; const statusResponse = await apiClient.get(`${baseUrl}/api/transcription/tasks/${taskId}/status`); const status = statusResponse.data; setTranscriptionStatus(status); setTranscriptionProgress(status.progress || 0); // Stop polling if task is completed or failed if (['completed', 'failed', 'error', 'cancelled'].includes(status.status)) { clearInterval(interval); setStatusCheckInterval(null); // Refresh transcript data only if completed successfully if (status.status === 'completed') { console.log('转录完成,刷新转录数据(无loading)'); await refreshTranscriptData(); } else { console.log('转录失败或取消,状态:', status.status); } // 再次确保清除状态 setTranscriptionStatus(status); } } catch (error) { console.error('Failed to fetch transcription status:', error); // Clear interval on error to prevent endless polling clearInterval(interval); setStatusCheckInterval(null); } }, 3000); setStatusCheckInterval(interval); }; const fetchMeetingDetails = async () => { try { setLoading(true); // Fallback URL construction in case config fails const baseUrl = "" const detailEndpoint = API_ENDPOINTS?.MEETINGS?.DETAIL?.(meeting_id) || `/api/meetings/${meeting_id}`; const audioEndpoint = API_ENDPOINTS?.MEETINGS?.AUDIO?.(meeting_id) || `/api/meetings/${meeting_id}/audio`; const transcriptEndpoint = API_ENDPOINTS?.MEETINGS?.TRANSCRIPT?.(meeting_id) || `/api/meetings/${meeting_id}/transcript`; const response = await apiClient.get(`${baseUrl}${detailEndpoint}`); setMeeting(response.data); // Handle transcription status from meeting details if (response.data.transcription_status) { const newStatus = response.data.transcription_status; setTranscriptionStatus(newStatus); setTranscriptionProgress(newStatus.progress || 0); // If transcription is in progress, start polling for updates // 但只有当前没有在轮询时才启动新的轮询 if (['pending', 'processing'].includes(newStatus.status)) { if (!statusCheckInterval) { console.log('转录进行中,开始轮询状态'); startStatusPolling(newStatus.task_id); } } else { // 如果转录已完成,确保清除任何现有的轮询 console.log('转录已完成或失败,状态:', newStatus.status); if (statusCheckInterval) { clearInterval(statusCheckInterval); setStatusCheckInterval(null); } } } else { setTranscriptionStatus(null); setTranscriptionProgress(0); // 清除轮询 if (statusCheckInterval) { clearInterval(statusCheckInterval); setStatusCheckInterval(null); } } // Fetch audio file if available try { const audioResponse = await apiClient.get(`${baseUrl}${audioEndpoint}`); // Construct URL using uploads path and relative path from database setAudioUrl(`${baseUrl}${audioResponse.data.file_path}`); setAudioFileName(audioResponse.data.file_name); } catch (audioError) { console.warn('No audio file available:', audioError); setAudioUrl(null); setAudioFileName(null); } // Fetch transcript segments from database try { const transcriptResponse = await apiClient.get(`${baseUrl}${transcriptEndpoint}`); setTranscript(transcriptResponse.data); console.log('First transcript item:', transcriptResponse.data[0]); // 现在使用speaker_id字段进行分组 const allSpeakerIds = transcriptResponse.data .map(item => item.speaker_id) .filter(speakerId => speakerId !== null && speakerId !== undefined); console.log('Extracted speaker IDs:', allSpeakerIds); const uniqueSpeakers = [...new Set(allSpeakerIds)] .map(speakerId => { const segment = transcriptResponse.data.find(item => item.speaker_id === speakerId); return { speaker_id: speakerId, speaker_tag: segment ? (segment.speaker_tag || `发言人 ${speakerId}`) : `发言人 ${speakerId}` }; }) .sort((a, b) => a.speaker_id - b.speaker_id); // 按speaker_id数值排序 console.log('Final unique speakers:', uniqueSpeakers); setSpeakerList(uniqueSpeakers); // 初始化编辑状态 const initialEditingState = {}; uniqueSpeakers.forEach(speaker => { initialEditingState[speaker.speaker_id] = speaker.speaker_tag; }); setEditingSpeakers(initialEditingState); } catch (transcriptError) { console.warn('No transcript data available:', transcriptError); setTranscript([]); setSpeakerList([]); } } catch (err) { console.error('Error fetching meeting details:', err); setError('无法加载会议详情,请稍后重试。'); } finally { setLoading(false); } }; const formatDateTime = (dateTimeString) => { if (!dateTimeString) return '时间待定'; const date = new Date(dateTimeString); return date.toLocaleString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const formatTime = (seconds) => { const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hours > 0) { // 超过60分钟显示小时:分钟:秒格式 return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } else { // 60分钟内显示分钟:秒格式 return `${mins}:${secs.toString().padStart(2, '0')}`; } }; const handlePlayPause = () => { if (audioRef.current) { if (isPlaying) { audioRef.current.pause(); } else { audioRef.current.play(); } setIsPlaying(!isPlaying); } }; const handleTimeUpdate = () => { if (audioRef.current) { const currentTime = audioRef.current.currentTime; setCurrentTime(currentTime); // 更新字幕显示 updateSubtitle(currentTime); } }; const updateSubtitle = (currentTime) => { const currentTimeMs = currentTime * 1000; const currentSegment = transcript.find(item => currentTimeMs >= item.start_time_ms && currentTimeMs <= item.end_time_ms ); if (currentSegment) { setCurrentSubtitle(currentSegment.text_content); // 确保使用 speaker_tag 来保持一致性 setCurrentSpeaker(currentSegment.speaker_tag || `发言人 ${currentSegment.speaker_id}`); // 找到当前segment在transcript数组中的索引 const currentIndex = transcript.findIndex(item => item.segment_id === currentSegment.segment_id); setCurrentHighlightIndex(currentIndex); // 滚动到对应的转录条目(仅在启用自动滚动时) if (autoScrollEnabled && currentIndex !== -1 && transcriptRefs.current[currentIndex]) { transcriptRefs.current[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); } } else { setCurrentSubtitle(''); setCurrentSpeaker(''); setCurrentHighlightIndex(-1); } }; const handleLoadedMetadata = () => { if (audioRef.current) { setDuration(audioRef.current.duration); } }; const handleSeek = (e) => { if (!audioRef.current || !duration) return; const rect = e.currentTarget.getBoundingClientRect(); const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const seekTime = percent * duration; audioRef.current.currentTime = seekTime; setCurrentTime(seekTime); }; const handleProgressMouseDown = (e) => { e.preventDefault(); const handleMouseMove = (moveEvent) => { handleSeek(moveEvent); }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); handleSeek(e); }; const handleVolumeChange = (e) => { const newVolume = parseFloat(e.target.value); setVolume(newVolume); if (audioRef.current) { audioRef.current.volume = newVolume; } }; const jumpToTime = (timestamp) => { if (audioRef.current) { audioRef.current.currentTime = timestamp; setCurrentTime(timestamp); audioRef.current.play(); setIsPlaying(true); } }; const handleDeleteMeeting = async () => { try { await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meeting_id))); navigate('/dashboard'); } catch (err) { console.error('Error deleting meeting:', err); setError('删除会议失败,请重试'); } }; const handleSpeakerTagUpdate = async (speakerId, newTag) => { try { const baseUrl = ""; await apiClient.put(`${baseUrl}/api/meetings/${meeting_id}/speaker-tags`, { speaker_id: speakerId, new_tag: newTag }); // 更新本地状态 setTranscript(prev => prev.map(item => item.speaker_id === speakerId ? { ...item, speaker_tag: newTag } : item )); setSpeakerList(prev => prev.map(speaker => speaker.speaker_id === speakerId ? { ...speaker, speaker_tag: newTag } : speaker )); } catch (err) { console.error('Error updating speaker tag:', err); setError('更新发言人标签失败,请重试'); } }; const handleBatchSpeakerUpdate = async () => { try { const baseUrl = ""; const updates = Object.entries(editingSpeakers).map(([speakerId, newTag]) => ({ speaker_id: parseInt(speakerId), // 确保传递整数类型 new_tag: newTag })); await apiClient.put(`${baseUrl}/api/meetings/${meeting_id}/speaker-tags/batch`, { updates: updates }); // 更新本地状态 setTranscript(prev => prev.map(item => { const newTag = editingSpeakers[item.speaker_id]; return newTag ? { ...item, speaker_tag: newTag } : item; })); setSpeakerList(prev => prev.map(speaker => ({ ...speaker, speaker_tag: editingSpeakers[speaker.speaker_id] || speaker.speaker_tag }))); setShowSpeakerEdit(false); } catch (err) { console.error('Error batch updating speaker tags:', err); setError('批量更新发言人标签失败,请重试'); } }; const handleEditingSpeakerChange = (speakerId, newTag) => { setEditingSpeakers(prev => ({ ...prev, [speakerId]: newTag })); }; const handleSpeakerEditOpen = () => { console.log('Opening speaker edit modal'); console.log('Current transcript:', transcript); console.log('Current speakerList:', speakerList); console.log('Current editingSpeakers:', editingSpeakers); setShowSpeakerEdit(true); }; const handleTranscriptEdit = (index) => { setEditingTranscriptIndex(index); // 获取前一条、当前条、后一条的数据 const editItems = []; if (index > 0) editItems.push({ ...transcript[index - 1], originalIndex: index - 1 }); editItems.push({ ...transcript[index], originalIndex: index }); if (index < transcript.length - 1) editItems.push({ ...transcript[index + 1], originalIndex: index + 1 }); // 初始化编辑状态 const initialEditState = {}; editItems.forEach(item => { initialEditState[item.originalIndex] = item.text_content; }); setEditingTranscripts(initialEditState); setShowTranscriptEdit(true); }; const handleTranscriptTextChange = (index, newText) => { setEditingTranscripts(prev => ({ ...prev, [index]: newText })); }; const handleSaveTranscriptEdits = async () => { try { const baseUrl = ""; const updates = Object.entries(editingTranscripts).map(([index, text_content]) => ({ segment_id: transcript[index].segment_id, text_content: text_content })); await apiClient.put(`${baseUrl}/api/meetings/${meeting_id}/transcript/batch`, { updates: updates }); // 更新本地状态 setTranscript(prev => prev.map((item, idx) => { const newText = editingTranscripts[idx]; return newText !== undefined ? { ...item, text_content: newText } : item; })); setShowTranscriptEdit(false); setEditingTranscripts({}); setEditingTranscriptIndex(-1); } catch (err) { console.error('Error updating transcript:', err); setError('更新转录内容失败,请重试'); } }; const getEditingItems = () => { if (editingTranscriptIndex === -1) return []; const items = []; const index = editingTranscriptIndex; if (index > 0) items.push({ ...transcript[index - 1], originalIndex: index - 1, position: 'prev' }); items.push({ ...transcript[index], originalIndex: index, position: 'current' }); if (index < transcript.length - 1) items.push({ ...transcript[index + 1], originalIndex: index + 1, position: 'next' }); return items; }; const refreshMeetingSummary = async () => { try { const baseUrl = ""; const detailEndpoint = API_ENDPOINTS?.MEETINGS?.DETAIL?.(meeting_id) || `/api/meetings/${meeting_id}`; // 只获取会议详情中的summary字段,不显示loading const response = await apiClient.get(`${baseUrl}${detailEndpoint}`); // 只更新summary相关的字段,保持其他数据不变 setMeeting(prevMeeting => ({ ...prevMeeting, summary: response.data.summary })); } catch (error) { console.error('刷新会议摘要失败:', error); } }; // AI总结相关函数 - 使用异步API const generateSummary = async () => { if (summaryLoading) return; setSummaryLoading(true); setSummaryTaskProgress(0); setSummaryTaskMessage('正在启动AI分析...'); setSummaryTaskStatus('pending'); try { const baseUrl = ""; // 使用异步API const response = await apiClient.post(`${baseUrl}/api/meetings/${meeting_id}/generate-summary-async`, { user_prompt: userPrompt }); const taskId = response.data.task_id; setSummaryTaskId(taskId); // 开始轮询任务状态 const interval = setInterval(async () => { try { const statusResponse = await apiClient.get(`${baseUrl}/api/llm-tasks/${taskId}/status`); const status = statusResponse.data; setSummaryTaskStatus(status.status); setSummaryTaskProgress(status.progress || 0); setSummaryTaskMessage(status.message || '处理中...'); if (status.status === 'completed') { clearInterval(interval); setSummaryPollInterval(null); // 设置结果 setSummaryResult({ content: status.result, task_id: taskId }); // 刷新总结历史(包含所有任务) await fetchSummaryHistory(); // 刷新会议摘要 await refreshMeetingSummary(); setSummaryLoading(false); setSummaryTaskMessage('AI总结生成成功!'); // 3秒后清除成功消息 setTimeout(() => { setSummaryTaskMessage(''); setSummaryTaskProgress(0); }, 3000); } else if (status.status === 'failed') { clearInterval(interval); setSummaryPollInterval(null); setSummaryLoading(false); setError(status.error_message || '生成AI总结失败'); setSummaryTaskMessage('生成失败:' + (status.error_message || '未知错误')); } } catch (err) { console.error('Error polling task status:', err); // 继续轮询,不中断 } }, 3000); // 每3秒查询一次 setSummaryPollInterval(interval); } catch (err) { console.error('Error starting summary generation:', err); // Check for detailed error message from backend const detail = err.response?.data?.detail; const errorMessage = detail || '启动AI总结失败,请重试。'; setError(errorMessage); // Set the more specific error setSummaryTaskMessage(`生成失败:${errorMessage}`); // Also show it in the modal setSummaryLoading(false); setSummaryTaskProgress(0); } }; const fetchSummaryHistory = async () => { try { const baseUrl = ""; // 获取所有LLM任务历史(包含进度和状态) const tasksResponse = await apiClient.get(`${baseUrl}/api/meetings/${meeting_id}/llm-tasks`); const tasks = tasksResponse.data.tasks || []; // 转换为历史记录格式,包含任务信息 const summaries = tasks .filter(task => task.status === 'completed' && task.result) .map(task => ({ id: task.task_id, content: task.result, user_prompt: task.user_prompt, created_at: task.created_at, task_info: { task_id: task.task_id, status: task.status, progress: task.progress } })); setSummaryHistory(summaries); // 如果有进行中的任务,恢复轮询 const runningTask = tasks.find(task => ['pending', 'processing'].includes(task.status)); if (runningTask && !summaryPollInterval) { setSummaryTaskId(runningTask.task_id); setSummaryTaskStatus(runningTask.status); setSummaryTaskProgress(runningTask.progress || 0); setSummaryLoading(true); // 恢复轮询 const interval = setInterval(async () => { try { const statusResponse = await apiClient.get(`${baseUrl}/api/llm-tasks/${runningTask.task_id}/status`); const status = statusResponse.data; setSummaryTaskStatus(status.status); setSummaryTaskProgress(status.progress || 0); setSummaryTaskMessage(status.message || '处理中...'); if (['completed', 'failed'].includes(status.status)) { clearInterval(interval); setSummaryPollInterval(null); setSummaryLoading(false); if (status.status === 'completed') { await fetchSummaryHistory(); await refreshMeetingSummary(); } } } catch (err) { console.error('Error polling task status:', err); } }, 3000); setSummaryPollInterval(interval); } } catch (err) { console.error('Error fetching summary history:', err); setSummaryHistory([]); } }; const openSummaryModal = async () => { // Frontend check before opening the modal if (!transcriptionStatus || transcriptionStatus.status !== 'completed') { setShowSummaryError(true); return; // Prevent modal from opening } setShowSummaryModal(true); setUserPrompt(''); setSummaryResult(null); await fetchSummaryHistory(); }; const closeSummaryModal = async () => { setShowSummaryModal(false); // 关闭弹窗时只刷新摘要部分,避免整页刷新 if (summaryResult) { await refreshMeetingSummary(); } }; const isCreator = meeting && user && String(meeting.creator_id) === String(user.user_id); if (loading) { return

加载中...

; } if (error) { return

{error}

返回首页
; } if (!meeting) { return

未找到会议信息。

返回首页
; } return (
返回首页 {isCreator && (
编辑会议
)}

{meeting.title}

会议日期: {formatDateTime(meeting.meeting_time).split(' ')[0]}
会议时间: {formatDateTime(meeting.meeting_time).split(' ')[1]}
创建人: {meeting.creator_username}
参会人数: {meeting.attendees.length}

参会人员

{meeting.attendees.map((attendee, index) => (
{typeof attendee === 'string' ? attendee : attendee.caption}
))}
{/* Audio Player Section */}

会议录音

{audioUrl ? (
{audioFileName && (
{audioFileName}
)} {/* 转录状态显示 */} {transcriptionStatus && (
语音转录进度:
{transcriptionStatus.status === 'pending' && (
等待处理中...
)} {transcriptionStatus.status === 'processing' && (
转录进行中
{transcriptionProgress}%
)} {transcriptionStatus.status === 'completed' && (
转录已完成
)} {transcriptionStatus.status === 'failed' && (
转录失败 {transcriptionStatus.error_message && ( ({transcriptionStatus.error_message}) )}
)}
)}
{formatTime(currentTime)}
{formatTime(duration)}
{/* 分割线 */}
{/* 动态字幕显示 */}
{currentSubtitle ? (
{currentSpeaker}
{currentSubtitle}
) : (
播放音频时将在此处显示实时字幕
)}
) : (

该会议没有录音文件

录音功能可能未开启或录音文件丢失

)}
会议总结} key="1"> 会议脑图} key="2">
{/* Transcript Sidebar */}

对话转录 setAutoScrollEnabled(!autoScrollEnabled)} title={autoScrollEnabled ? "关闭同步滚动" : "开启同步滚动"} > {autoScrollEnabled ? : }

{isCreator && ( <> )}
{transcript.map((item, index) => (
transcriptRefs.current[index] = el} className={`transcript-item ${currentHighlightIndex === index ? 'active' : ''}`} >
jumpToTime(item.start_time_ms / 1000)} title="跳转到此时间点播放" > {item.speaker_tag}
jumpToTime(item.start_time_ms / 1000)} title="跳转到此时间点播放" > {formatTime(item.start_time_ms / 1000)} {isCreator && ( )}
jumpToTime(item.start_time_ms / 1000)} title="跳转到此时间点播放" > {item.text_content}
))}
{/* Delete Confirmation Modal */} {showDeleteConfirm && (
setShowDeleteConfirm(false)}>
e.stopPropagation()}>

确认删除

确定要删除会议 "{meeting.title}" 吗?此操作无法撤销。

)} {/* Summary Error Modal */} {showSummaryError && (
setShowSummaryError(false)}>
e.stopPropagation()}>

操作无法进行

会议转录尚未完成或处理失败,请在转录成功后再生成AI总结。

)} {/* Speaker Tags Edit Modal */} {showSpeakerEdit && (
setShowSpeakerEdit(false)}>
e.stopPropagation()}>

编辑发言人标签

根据AI识别的发言人ID,为每个发言人设置自定义标签:

{speakerList.length > 0 ? ( speakerList.map((speaker) => { const segmentCount = transcript.filter(item => item.speaker_id === speaker.speaker_id).length; return (
({segmentCount} 条发言)
handleEditingSpeakerChange(speaker.speaker_id, e.target.value)} className="speaker-tag-input" placeholder="输入发言人姓名或标签" />
); }) ) : (

暂无发言人数据,请检查转录内容是否正确加载。

)}
)} {/* Transcript Edit Modal */} {showTranscriptEdit && (
setShowTranscriptEdit(false)}>
e.stopPropagation()}>

编辑转录内容

修改选中转录条目及其上下文内容:

{getEditingItems().map((item) => (
{item.speaker_tag} {formatTime(item.start_time_ms / 1000)} {item.position === 'current' && ( 当前编辑 )}