diff --git a/src/pages/MeetingDetails.css b/src/pages/MeetingDetails.css index 485458c..30b9835 100644 --- a/src/pages/MeetingDetails.css +++ b/src/pages/MeetingDetails.css @@ -404,6 +404,30 @@ align-items: center; } +.transcript-controls { + display: flex; + gap: 8px; + align-items: center; +} + +.edit-speakers-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + background: #007bff; + color: white; + border: none; + border-radius: 4px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s; +} + +.edit-speakers-btn:hover { + background: #0056b3; +} + .transcript-header h3 { margin: 0; color: #334155; @@ -453,7 +477,21 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 0.5rem; + margin-bottom: 8px; +} + +.transcript-item-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.clickable { + cursor: pointer; +} + +.clickable:hover { + opacity: 0.7; } .speaker-name { @@ -620,6 +658,197 @@ background: #dc2626; } +/* Speaker Edit Modal Styles */ +.speaker-edit-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.speaker-edit-modal { + background: white; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + max-width: 600px; + width: 90%; + max-height: 80vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid #e2e8f0; + background: #f8fafc; +} + +.modal-header h3 { + margin: 0; + color: #1e293b; + font-size: 1.2rem; + font-weight: 600; +} + +.close-btn { + background: none; + border: none; + color: #64748b; + cursor: pointer; + padding: 8px; + border-radius: 6px; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.close-btn:hover { + background: #e2e8f0; + color: #1e293b; +} + +.speaker-edit-content { + flex: 1; + overflow-y: auto; + padding: 20px 24px; +} + +.modal-description { + color: #64748b; + margin-bottom: 24px; + font-size: 14px; + line-height: 1.5; +} + +.speaker-list { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 8px; +} + +.no-speakers-message { + text-align: center; + padding: 40px 20px; + color: #64748b; +} + +.no-speakers-message p { + margin: 0; + font-size: 14px; +} + +.speaker-edit-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + background: #f8fafc; + border-radius: 8px; + border: 1px solid #e2e8f0; + transition: all 0.2s ease; +} + +.speaker-edit-item:hover { + background: #f1f5f9; + border-color: #cbd5e1; +} + +.speaker-info { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 160px; +} + +.speaker-id { + font-weight: 600; + color: #1e293b; + font-size: 15px; +} + +.segment-count { + font-size: 13px; + color: #64748b; +} + +.speaker-tag-input { + flex: 1; + padding: 10px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + margin-left: 20px; + min-width: 220px; + transition: all 0.2s ease; +} + +.speaker-tag-input:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + background: #fff; +} + +.modal-actions { + padding: 20px 24px; + border-top: 1px solid #e2e8f0; + background: #f8fafc; + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.btn-save { + display: flex; + align-items: center; + gap: 8px; + background: #10b981; + color: white; + border: none; + padding: 12px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(16, 185, 129, 0.2); +} + +.btn-save:hover { + background: #059669; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3); +} + +.btn-cancel { + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid #d1d5db; + background: white; + color: #374151; +} + +.btn-cancel:hover { + background: #f9fafb; + border-color: #9ca3af; +} + /* Responsive Design */ @media (max-width: 1200px) { .details-layout { @@ -671,4 +900,42 @@ .volume-control { order: 4; } + + .speaker-edit-modal { + max-width: 95%; + max-height: 90vh; + margin: 20px; + } + + .speaker-edit-item { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .speaker-tag-input { + margin-left: 0; + min-width: auto; + width: 100%; + } + + .transcript-controls { + flex-direction: column; + gap: 8px; + } + + .edit-speakers-btn { + font-size: 12px; + padding: 6px 10px; + } + + .modal-actions { + flex-direction: column; + gap: 8px; + } + + .btn-save, .btn-cancel { + width: 100%; + justify-content: center; + } } diff --git a/src/pages/MeetingDetails.jsx b/src/pages/MeetingDetails.jsx index 56cde5f..7ebd341 100644 --- a/src/pages/MeetingDetails.jsx +++ b/src/pages/MeetingDetails.jsx @@ -1,7 +1,7 @@ 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 } from 'lucide-react'; +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'; @@ -24,6 +24,9 @@ const MeetingDetails = ({ user }) => { 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(() => { @@ -59,9 +62,40 @@ const MeetingDetails = ({ user }) => { 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); @@ -164,6 +198,79 @@ const MeetingDetails = ({ user }) => { } }; + 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) { @@ -334,12 +441,24 @@ const MeetingDetails = ({ user }) => {
根据AI识别的发言人ID,为每个发言人设置自定义标签:
+ +暂无发言人数据,请检查转录内容是否正确加载。
+