diff --git a/src/pages/EditMeeting.css b/src/pages/EditMeeting.css index 04985d3..704c8cc 100644 --- a/src/pages/EditMeeting.css +++ b/src/pages/EditMeeting.css @@ -607,4 +607,79 @@ padding: 0.6rem 1.2rem; font-size: 0.9rem; } +} + +/* Upload Confirmation Modal */ +.delete-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.delete-modal { + background: white; + border-radius: 12px; + padding: 2rem; + max-width: 400px; + width: 90%; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.delete-modal h3 { + margin: 0 0 1rem 0; + color: #1e293b; + font-size: 1.25rem; +} + +.delete-modal p { + margin: 0 0 2rem 0; + color: #64748b; + line-height: 1.6; +} + +.modal-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; +} + +.modal-actions .btn-cancel, .modal-actions .btn-submit { + padding: 0.5rem 1rem; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + border: none; +} + +.modal-actions .btn-cancel { + background: #f1f5f9; + color: #475569; +} + +.modal-actions .btn-cancel:hover { + background: #e2e8f0; +} + +.modal-actions .btn-submit { + background: #3b82f6; + color: white; +} + +.modal-actions .btn-submit:hover:not(:disabled) { + background: #2563eb; +} + +.modal-actions .btn-submit:disabled { + opacity: 0.7; + cursor: not-allowed; +} } \ No newline at end of file diff --git a/src/pages/EditMeeting.jsx b/src/pages/EditMeeting.jsx index d93cde8..c902449 100644 --- a/src/pages/EditMeeting.jsx +++ b/src/pages/EditMeeting.jsx @@ -29,6 +29,7 @@ const EditMeeting = ({ user }) => { const [error, setError] = useState(''); const [meeting, setMeeting] = useState(null); const [showUploadArea, setShowUploadArea] = useState(false); + const [showUploadConfirm, setShowUploadConfirm] = useState(false); const handleSummaryChange = useCallback((value) => { setFormData(prev => ({ ...prev, summary: value || '' })); @@ -159,10 +160,10 @@ const EditMeeting = ({ user }) => { setError(''); try { - // Upload new audio file const formDataUpload = new FormData(); formDataUpload.append('audio_file', audioFile); formDataUpload.append('meeting_id', meeting_id); + formDataUpload.append('force_replace', 'true'); // Always force replace in edit mode const response = await axios.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formDataUpload, { headers: { @@ -170,22 +171,16 @@ const EditMeeting = ({ user }) => { }, }); - // Update summary with new AI analysis result - if (response.data.summary) { - setFormData(prev => ({ - ...prev, - summary: response.data.summary - })); - } - setAudioFile(null); setShowUploadArea(false); + setShowUploadConfirm(false); // Reset file input const fileInput = document.getElementById('audio-file'); if (fileInput) fileInput.value = ''; } catch (err) { - setError('上传音频文件失败,请重试'); + console.error('Upload error:', err); + setError(err.response?.data?.detail || '上传音频文件失败,请重试'); } finally { setIsUploading(false); } @@ -511,7 +506,7 @@ const EditMeeting = ({ user }) => { {audioFile && ( + + + + + )} ); }; diff --git a/src/pages/MeetingDetails.css b/src/pages/MeetingDetails.css index 140ff22..1ff9533 100644 --- a/src/pages/MeetingDetails.css +++ b/src/pages/MeetingDetails.css @@ -330,6 +330,108 @@ backdrop-filter: blur(10px); } +/* 转录状态显示样式 - 一行显示 */ +.transcription-status { + margin: 1rem 0; + padding: 0.75rem 1rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.status-content-inline { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.status-header-inline { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.9); + flex-shrink: 0; +} + +.status-pending-inline, +.status-processing-inline, +.status-completed-inline, +.status-failed-inline { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; +} + +.status-processing-inline { + gap: 0.75rem; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + animation: pulse 2s infinite; +} + +.status-indicator.pending { + background-color: #fbbf24; +} + +.status-indicator.processing { + background-color: #3b82f6; +} + +.status-indicator.completed { + background-color: #10b981; + animation: none; +} + +.status-indicator.failed { + background-color: #ef4444; + animation: none; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.progress-bar-small { + width: 80px; + height: 4px; + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; + overflow: hidden; +} + +.progress-fill-small { + height: 100%; + background: linear-gradient(90deg, #3b82f6, #1d4ed8); + border-radius: 2px; + transition: width 0.3s ease; +} + +.progress-text { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.7); + min-width: 28px; + text-align: right; +} + +.error-message-inline { + font-size: 0.75rem; + color: #fca5a5; + margin-left: 0.25rem; +} + .player-controls { display: flex; align-items: center; @@ -556,7 +658,6 @@ .no-summary-content svg { color: #cbd5e1; - margin-bottom: 1rem; } .no-summary-content h3 { @@ -573,17 +674,31 @@ .generate-summary-cta { display: inline-flex; align-items: center; - gap: 8px; + justify-content: center; + gap: 6px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; - padding: 12px 20px; + padding: 10px 18px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s ease; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); + min-height: 40px; + text-align: left; + vertical-align: middle; +} + +.generate-summary-cta svg { + flex-shrink: 0; + display: block; +} + +.generate-summary-cta span { + display: block; + line-height: 1.2; } .generate-summary-cta:hover { @@ -617,6 +732,32 @@ align-items: center; } +.transcript-header h3 { + display: flex; + align-items: center; + gap: 12px; +} + +.auto-scroll-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + border: none; + border-radius: 4px; + cursor: pointer; + transition: opacity 0.2s; + background: transparent; + color: #64748b; + min-width: 24px; + height: 24px; + margin-left: auto; +} + +.auto-scroll-btn:hover { + opacity: 0.7; +} + .edit-speakers-btn, .ai-summary-btn { display: flex; @@ -1132,17 +1273,31 @@ .generate-summary-btn { display: flex; align-items: center; - gap: 8px; + justify-content: center; + gap: 6px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; - padding: 12px 20px; + padding: 10px 18px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s ease; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); + min-height: 40px; + text-align: left; + vertical-align: middle; +} + +.generate-summary-btn svg { + flex-shrink: 0; + display: block; +} + +.generate-summary-btn span { + display: block; + line-height: 1.2; } .generate-summary-btn:hover:not(:disabled) { diff --git a/src/pages/MeetingDetails.jsx b/src/pages/MeetingDetails.jsx index 4cf1dd4..c330bb3 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, Settings, Save, X, Edit3, Brain, Sparkles, Download } from 'lucide-react'; +import { ArrowLeft, Clock, Users, FileText, User, Calendar, Play, Pause, Volume2, MessageCircle, Edit, Trash2, Settings, Save, X, Edit3, Brain, Sparkles, Download, ArrowDown, Lock, Unlock } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; @@ -28,6 +28,10 @@ const MeetingDetails = ({ user }) => { 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(''); @@ -43,7 +47,61 @@ const MeetingDetails = ({ user }) => { useEffect(() => { fetchMeetingDetails(); + + // Cleanup interval on unmount + return () => { + if (statusCheckInterval) { + clearInterval(statusCheckInterval); + } + }; }, [meeting_id]); + + // Cleanup interval when status changes + useEffect(() => { + if (transcriptionStatus && !['pending', 'processing'].includes(transcriptionStatus.status)) { + if (statusCheckInterval) { + clearInterval(statusCheckInterval); + setStatusCheckInterval(null); + } + } + }, [transcriptionStatus, statusCheckInterval]); + + const startStatusPolling = (taskId) => { + // Clear existing interval + if (statusCheckInterval) { + clearInterval(statusCheckInterval); + } + + // Poll every 3 seconds + const interval = setInterval(async () => { + try { + const baseUrl = ""; + const statusResponse = await axios.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 meeting details to get updated transcript + if (status.status === 'completed') { + fetchMeetingDetails(); + } + } + } 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 { @@ -58,6 +116,20 @@ const MeetingDetails = ({ user }) => { const response = await axios.get(`${baseUrl}${detailEndpoint}`); setMeeting(response.data); + // Handle transcription status from meeting details + if (response.data.transcription_status) { + setTranscriptionStatus(response.data.transcription_status); + setTranscriptionProgress(response.data.transcription_status.progress || 0); + + // If transcription is in progress, start polling for updates + if (['pending', 'processing'].includes(response.data.transcription_status.status)) { + startStatusPolling(response.data.transcription_status.task_id); + } + } else { + setTranscriptionStatus(null); + setTranscriptionProgress(0); + } + // Fetch audio file if available try { const audioResponse = await axios.get(`${baseUrl}${audioEndpoint}`); @@ -168,8 +240,8 @@ const MeetingDetails = ({ user }) => { const currentIndex = transcript.findIndex(item => item.segment_id === currentSegment.segment_id); setCurrentHighlightIndex(currentIndex); - // 滚动到对应的转录条目 - if (currentIndex !== -1 && transcriptRefs.current[currentIndex]) { + // 滚动到对应的转录条目(仅在启用自动滚动时) + if (autoScrollEnabled && currentIndex !== -1 && transcriptRefs.current[currentIndex]) { transcriptRefs.current[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' @@ -658,6 +730,53 @@ const MeetingDetails = ({ user }) => { 您的浏览器不支持音频播放。 + {/* 转录状态显示 */} + {transcriptionStatus && ( +
+
+
+ + 语音转录进度: +
+ {transcriptionStatus.status === 'pending' && ( +
+
+ 等待处理中... +
+ )} + {transcriptionStatus.status === 'processing' && ( +
+
+ 转录进行中 +
+
+
+ {transcriptionProgress}% +
+ )} + {transcriptionStatus.status === 'completed' && ( +
+
+ 转录已完成 + +
+ )} + {transcriptionStatus.status === 'failed' && ( +
+
+ 转录失败 + {transcriptionStatus.error_message && ( + ({transcriptionStatus.error_message}) + )} +
+ )} +
+
+ )} +
+
{isCreator && ( <> diff --git a/vite.config.js b/vite.config.js index 024bdad..c778882 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,7 +7,7 @@ export default defineConfig({ server: { host: true, // Optional: Allows the server to be accessible externally port: 5173, // Optional: Specify a port if needed - allowedHosts: ['6fc3f0b0.r3.cpolar.cn'], // Add the problematic hostname here + allowedHosts: ['imeeting.unisspace.com'], // Add the problematic hostname here proxy: { '/api': { target: 'http://localhost:8000', // 后端服务地址