加入缓存
parent
c9a304e080
commit
a78561edb0
|
|
@ -607,4 +607,79 @@
|
||||||
padding: 0.6rem 1.2rem;
|
padding: 0.6rem 1.2rem;
|
||||||
font-size: 0.9rem;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -29,6 +29,7 @@ const EditMeeting = ({ user }) => {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [meeting, setMeeting] = useState(null);
|
const [meeting, setMeeting] = useState(null);
|
||||||
const [showUploadArea, setShowUploadArea] = useState(false);
|
const [showUploadArea, setShowUploadArea] = useState(false);
|
||||||
|
const [showUploadConfirm, setShowUploadConfirm] = useState(false);
|
||||||
|
|
||||||
const handleSummaryChange = useCallback((value) => {
|
const handleSummaryChange = useCallback((value) => {
|
||||||
setFormData(prev => ({ ...prev, summary: value || '' }));
|
setFormData(prev => ({ ...prev, summary: value || '' }));
|
||||||
|
|
@ -159,10 +160,10 @@ const EditMeeting = ({ user }) => {
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Upload new audio file
|
|
||||||
const formDataUpload = new FormData();
|
const formDataUpload = new FormData();
|
||||||
formDataUpload.append('audio_file', audioFile);
|
formDataUpload.append('audio_file', audioFile);
|
||||||
formDataUpload.append('meeting_id', meeting_id);
|
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, {
|
const response = await axios.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formDataUpload, {
|
||||||
headers: {
|
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);
|
setAudioFile(null);
|
||||||
setShowUploadArea(false);
|
setShowUploadArea(false);
|
||||||
|
setShowUploadConfirm(false);
|
||||||
// Reset file input
|
// Reset file input
|
||||||
const fileInput = document.getElementById('audio-file');
|
const fileInput = document.getElementById('audio-file');
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('上传音频文件失败,请重试');
|
console.error('Upload error:', err);
|
||||||
|
setError(err.response?.data?.detail || '上传音频文件失败,请重试');
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -511,7 +506,7 @@ const EditMeeting = ({ user }) => {
|
||||||
{audioFile && (
|
{audioFile && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleUploadAudio}
|
onClick={() => setShowUploadConfirm(true)}
|
||||||
className="upload-btn"
|
className="upload-btn"
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
>
|
>
|
||||||
|
|
@ -541,7 +536,6 @@ const EditMeeting = ({ user }) => {
|
||||||
preview="edit"
|
preview="edit"
|
||||||
hideToolbar={false}
|
hideToolbar={false}
|
||||||
toolbarBottom={false}
|
toolbarBottom={false}
|
||||||
visibleDragBar={false}
|
|
||||||
commands={customCommands}
|
commands={customCommands}
|
||||||
extraCommands={customExtraCommands}
|
extraCommands={customExtraCommands}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
|
|
@ -596,6 +590,31 @@ const EditMeeting = ({ user }) => {
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Upload Confirmation Modal */}
|
||||||
|
{showUploadConfirm && (
|
||||||
|
<div className="delete-modal-overlay" onClick={() => setShowUploadConfirm(false)}>
|
||||||
|
<div className="delete-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h3>确认重新上传</h3>
|
||||||
|
<p>重传音频文件将清空已有的会话转录,是否继续?</p>
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button
|
||||||
|
className="btn-cancel"
|
||||||
|
onClick={() => setShowUploadConfirm(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-submit"
|
||||||
|
onClick={handleUploadAudio}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
确定重传
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -330,6 +330,108 @@
|
||||||
backdrop-filter: blur(10px);
|
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 {
|
.player-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -556,7 +658,6 @@
|
||||||
|
|
||||||
.no-summary-content svg {
|
.no-summary-content svg {
|
||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-summary-content h3 {
|
.no-summary-content h3 {
|
||||||
|
|
@ -573,17 +674,31 @@
|
||||||
.generate-summary-cta {
|
.generate-summary-cta {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 12px 20px;
|
padding: 10px 18px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
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 {
|
.generate-summary-cta:hover {
|
||||||
|
|
@ -617,6 +732,32 @@
|
||||||
align-items: center;
|
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,
|
.edit-speakers-btn,
|
||||||
.ai-summary-btn {
|
.ai-summary-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1132,17 +1273,31 @@
|
||||||
.generate-summary-btn {
|
.generate-summary-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 12px 20px;
|
padding: 10px 18px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
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) {
|
.generate-summary-btn:hover:not(:disabled) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||||
import axios from 'axios';
|
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 ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
|
@ -28,6 +28,10 @@ const MeetingDetails = ({ user }) => {
|
||||||
const [editingSpeakers, setEditingSpeakers] = useState({});
|
const [editingSpeakers, setEditingSpeakers] = useState({});
|
||||||
const [speakerList, setSpeakerList] = useState([]);
|
const [speakerList, setSpeakerList] = useState([]);
|
||||||
const [showTranscriptEdit, setShowTranscriptEdit] = useState(false);
|
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 [editingTranscriptIndex, setEditingTranscriptIndex] = useState(-1);
|
||||||
const [editingTranscripts, setEditingTranscripts] = useState({});
|
const [editingTranscripts, setEditingTranscripts] = useState({});
|
||||||
const [currentSubtitle, setCurrentSubtitle] = useState('');
|
const [currentSubtitle, setCurrentSubtitle] = useState('');
|
||||||
|
|
@ -43,7 +47,61 @@ const MeetingDetails = ({ user }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMeetingDetails();
|
fetchMeetingDetails();
|
||||||
|
|
||||||
|
// Cleanup interval on unmount
|
||||||
|
return () => {
|
||||||
|
if (statusCheckInterval) {
|
||||||
|
clearInterval(statusCheckInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [meeting_id]);
|
}, [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 () => {
|
const fetchMeetingDetails = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -58,6 +116,20 @@ const MeetingDetails = ({ user }) => {
|
||||||
const response = await axios.get(`${baseUrl}${detailEndpoint}`);
|
const response = await axios.get(`${baseUrl}${detailEndpoint}`);
|
||||||
setMeeting(response.data);
|
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
|
// Fetch audio file if available
|
||||||
try {
|
try {
|
||||||
const audioResponse = await axios.get(`${baseUrl}${audioEndpoint}`);
|
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);
|
const currentIndex = transcript.findIndex(item => item.segment_id === currentSegment.segment_id);
|
||||||
setCurrentHighlightIndex(currentIndex);
|
setCurrentHighlightIndex(currentIndex);
|
||||||
|
|
||||||
// 滚动到对应的转录条目
|
// 滚动到对应的转录条目(仅在启用自动滚动时)
|
||||||
if (currentIndex !== -1 && transcriptRefs.current[currentIndex]) {
|
if (autoScrollEnabled && currentIndex !== -1 && transcriptRefs.current[currentIndex]) {
|
||||||
transcriptRefs.current[currentIndex].scrollIntoView({
|
transcriptRefs.current[currentIndex].scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'center'
|
block: 'center'
|
||||||
|
|
@ -658,6 +730,53 @@ const MeetingDetails = ({ user }) => {
|
||||||
您的浏览器不支持音频播放。
|
您的浏览器不支持音频播放。
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
|
{/* 转录状态显示 */}
|
||||||
|
{transcriptionStatus && (
|
||||||
|
<div className="transcription-status">
|
||||||
|
<div className="status-content-inline">
|
||||||
|
<div className="status-header-inline">
|
||||||
|
<Brain size={16} />
|
||||||
|
<span>语音转录进度:</span>
|
||||||
|
</div>
|
||||||
|
{transcriptionStatus.status === 'pending' && (
|
||||||
|
<div className="status-pending-inline">
|
||||||
|
<div className="status-indicator pending"></div>
|
||||||
|
<span>等待处理中...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{transcriptionStatus.status === 'processing' && (
|
||||||
|
<div className="status-processing-inline">
|
||||||
|
<div className="status-indicator processing"></div>
|
||||||
|
<span>转录进行中</span>
|
||||||
|
<div className="progress-bar-small">
|
||||||
|
<div
|
||||||
|
className="progress-fill-small"
|
||||||
|
style={{ width: `${transcriptionProgress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span className="progress-text">{transcriptionProgress}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{transcriptionStatus.status === 'completed' && (
|
||||||
|
<div className="status-completed-inline">
|
||||||
|
<div className="status-indicator completed"></div>
|
||||||
|
<span>转录已完成</span>
|
||||||
|
<Sparkles size={14} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{transcriptionStatus.status === 'failed' && (
|
||||||
|
<div className="status-failed-inline">
|
||||||
|
<div className="status-indicator failed"></div>
|
||||||
|
<span>转录失败</span>
|
||||||
|
{transcriptionStatus.error_message && (
|
||||||
|
<span className="error-message-inline">({transcriptionStatus.error_message})</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="player-controls">
|
<div className="player-controls">
|
||||||
<button className="play-button" onClick={handlePlayPause}>
|
<button className="play-button" onClick={handlePlayPause}>
|
||||||
{isPlaying ? <Pause size={24} /> : <Play size={24} />}
|
{isPlaying ? <Pause size={24} /> : <Play size={24} />}
|
||||||
|
|
@ -778,7 +897,17 @@ const MeetingDetails = ({ user }) => {
|
||||||
{/* Transcript Sidebar */}
|
{/* Transcript Sidebar */}
|
||||||
<div className="transcript-sidebar">
|
<div className="transcript-sidebar">
|
||||||
<div className="transcript-header">
|
<div className="transcript-header">
|
||||||
<h3><MessageCircle size={20} /> 对话转录</h3>
|
<h3>
|
||||||
|
<MessageCircle size={20} />
|
||||||
|
对话转录
|
||||||
|
<button
|
||||||
|
className="auto-scroll-btn"
|
||||||
|
onClick={() => setAutoScrollEnabled(!autoScrollEnabled)}
|
||||||
|
title={autoScrollEnabled ? "关闭自动滚动" : "开启自动滚动"}
|
||||||
|
>
|
||||||
|
{autoScrollEnabled ? <Lock size={16} /> : <Unlock size={16} />}
|
||||||
|
</button>
|
||||||
|
</h3>
|
||||||
<div className="transcript-controls">
|
<div className="transcript-controls">
|
||||||
{isCreator && (
|
{isCreator && (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
host: true, // Optional: Allows the server to be accessible externally
|
host: true, // Optional: Allows the server to be accessible externally
|
||||||
port: 5173, // Optional: Specify a port if needed
|
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: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8000', // 后端服务地址
|
target: 'http://localhost:8000', // 后端服务地址
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue