import React, { useState, useEffect, useRef } from 'react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import axios from 'axios'; import { ArrowLeft, Clock, Users, FileText, User, Calendar, Play, Pause, Volume2, MessageCircle, Edit, Trash2, Settings, Save, X } 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 './MeetingDetails.css'; 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 [showSpeakerEdit, setShowSpeakerEdit] = useState(false); const [editingSpeakers, setEditingSpeakers] = useState({}); const [speakerList, setSpeakerList] = useState([]); const audioRef = useRef(null); useEffect(() => { fetchMeetingDetails(); }, [meeting_id]); 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 axios.get(`${baseUrl}${detailEndpoint}`); setMeeting(response.data); // Fetch audio file if available try { const audioResponse = await axios.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 axios.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 mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 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) { setCurrentTime(audioRef.current.currentTime); } }; 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 axios.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 axios.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 axios.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 isCreator = meeting && user && String(meeting.creator_id) === String(user.user_id); if (loading) { return
加载中...
{error}
返回首页未找到会议信息。
返回首页该会议没有录音文件
录音功能可能未开启或录音文件丢失
确定要删除会议 "{meeting.title}" 吗?此操作无法撤销。
根据AI识别的发言人ID,为每个发言人设置自定义标签:
暂无发言人数据,请检查转录内容是否正确加载。