main
mula.liu 2025-08-29 16:37:55 +08:00
parent a78561edb0
commit ff2eeee5a3
9 changed files with 215 additions and 50 deletions

View File

@ -1,5 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import apiClient from './utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from './config/api';
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage';
import Dashboard from './pages/Dashboard'; import Dashboard from './pages/Dashboard';
import MeetingDetails from './pages/MeetingDetails'; import MeetingDetails from './pages/MeetingDetails';
@ -33,9 +35,18 @@ function App() {
localStorage.setItem('iMeetingUser', JSON.stringify(userData)); localStorage.setItem('iMeetingUser', JSON.stringify(userData));
}; };
const handleLogout = () => { const handleLogout = async () => {
setUser(null); try {
localStorage.removeItem('iMeetingUser'); // APItoken
await apiClient.post(buildApiUrl(API_ENDPOINTS.AUTH.LOGOUT));
} catch (error) {
console.error('Logout API error:', error);
// 使API
} finally {
//
setUser(null);
localStorage.removeItem('iMeetingUser');
}
}; };
if (isLoading) { if (isLoading) {

View File

@ -3,7 +3,11 @@ const API_CONFIG = {
BASE_URL: "", BASE_URL: "",
ENDPOINTS: { ENDPOINTS: {
AUTH: { AUTH: {
LOGIN: '/api/auth/login' LOGIN: '/api/auth/login',
LOGOUT: '/api/auth/logout',
LOGOUT_ALL: '/api/auth/logout-all',
ME: '/api/auth/me',
REFRESH: '/api/auth/refresh'
}, },
USERS: { USERS: {
LIST: '/api/users', LIST: '/api/users',

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios'; import apiClient from '../utils/apiClient';
import { ArrowLeft, Upload, Users, Calendar, FileText, X, User, Plus } from 'lucide-react'; import { ArrowLeft, Upload, Users, Calendar, FileText, X, User, Plus } from 'lucide-react';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import DateTimePicker from '../components/DateTimePicker'; import DateTimePicker from '../components/DateTimePicker';
@ -26,7 +26,7 @@ const CreateMeeting = ({ user }) => {
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
const response = await axios.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)); const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST));
setAvailableUsers(response.data.filter(u => u.user_id !== user.user_id)); setAvailableUsers(response.data.filter(u => u.user_id !== user.user_id));
} catch (err) { } catch (err) {
console.error('Error fetching users:', err); console.error('Error fetching users:', err);
@ -100,7 +100,7 @@ const CreateMeeting = ({ user }) => {
attendee_ids: formData.attendees.map(a => a.user_id) attendee_ids: formData.attendees.map(a => a.user_id)
}; };
const response = await axios.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), meetingData); const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), meetingData);
const meetingId = response.data.meeting_id; const meetingId = response.data.meeting_id;
// Upload audio file if provided // Upload audio file if provided
@ -109,7 +109,7 @@ const CreateMeeting = ({ user }) => {
formDataUpload.append('audio_file', audioFile); formDataUpload.append('audio_file', audioFile);
formDataUpload.append('meeting_id', meetingId); formDataUpload.append('meeting_id', meetingId);
await axios.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formDataUpload, { await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formDataUpload, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus } from 'lucide-react'; import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus } from 'lucide-react';
import axios from 'axios'; import apiClient from '../utils/apiClient';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import MeetingTimeline from '../components/MeetingTimeline'; import MeetingTimeline from '../components/MeetingTimeline';
@ -21,11 +21,11 @@ const Dashboard = ({ user, onLogout }) => {
setLoading(true); setLoading(true);
console.log('Fetching user data for user_id:', user.user_id); console.log('Fetching user data for user_id:', user.user_id);
const userResponse = await axios.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id))); const userResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
console.log('User response:', userResponse.data); console.log('User response:', userResponse.data);
setUserInfo(userResponse.data); setUserInfo(userResponse.data);
const meetingsResponse = await axios.get(buildApiUrl(`${API_ENDPOINTS.MEETINGS.LIST}?user_id=${user.user_id}`)); const meetingsResponse = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.MEETINGS.LIST}?user_id=${user.user_id}`));
console.log('Meetings response:', meetingsResponse.data); console.log('Meetings response:', meetingsResponse.data);
setMeetings(meetingsResponse.data); setMeetings(meetingsResponse.data);
@ -39,9 +39,9 @@ const Dashboard = ({ user, onLogout }) => {
const handleDeleteMeeting = async (meetingId) => { const handleDeleteMeeting = async (meetingId) => {
try { try {
await axios.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId))); await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId)));
// Refresh meetings list // Refresh meetings list
const meetingsResponse = await axios.get(buildApiUrl(`${API_ENDPOINTS.MEETINGS.LIST}?user_id=${user.user_id}`)); const meetingsResponse = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.MEETINGS.LIST}?user_id=${user.user_id}`));
setMeetings(meetingsResponse.data); setMeetings(meetingsResponse.data);
} catch (err) { } catch (err) {
console.error('Error deleting meeting:', err); console.error('Error deleting meeting:', err);

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom'; import { Link, useNavigate, useParams } from 'react-router-dom';
import axios from 'axios'; import apiClient from '../utils/apiClient';
import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus, Image } from 'lucide-react'; import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus, Image } from 'lucide-react';
import MDEditor, * as commands from '@uiw/react-md-editor'; import MDEditor, * as commands from '@uiw/react-md-editor';
import '@uiw/react-md-editor/markdown-editor.css'; import '@uiw/react-md-editor/markdown-editor.css';
@ -42,7 +42,7 @@ const EditMeeting = ({ user }) => {
const fetchMeetingData = async () => { const fetchMeetingData = async () => {
try { try {
const response = await axios.get(buildApiUrl(API_ENDPOINTS.MEETINGS.EDIT(meeting_id))); const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.EDIT(meeting_id)));
const meetingData = response.data; const meetingData = response.data;
// Check if current user is the creator // Check if current user is the creator
@ -68,7 +68,7 @@ const EditMeeting = ({ user }) => {
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
const response = await axios.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)); const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST));
setAvailableUsers(response.data.filter(u => u.user_id !== user.user_id)); setAvailableUsers(response.data.filter(u => u.user_id !== user.user_id));
} catch (err) { } catch (err) {
console.error('Error fetching users:', err); console.error('Error fetching users:', err);
@ -141,7 +141,7 @@ const EditMeeting = ({ user }) => {
attendee_ids: formData.attendees.map(a => a.user_id) attendee_ids: formData.attendees.map(a => a.user_id)
}; };
await axios.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meeting_id)), updateData); await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meeting_id)), updateData);
navigate(`/meetings/${meeting_id}`); navigate(`/meetings/${meeting_id}`);
} catch (err) { } catch (err) {
setError(err.response?.data?.detail || '更新会议失败,请重试'); setError(err.response?.data?.detail || '更新会议失败,请重试');
@ -165,7 +165,7 @@ const EditMeeting = ({ user }) => {
formDataUpload.append('meeting_id', meeting_id); formDataUpload.append('meeting_id', meeting_id);
formDataUpload.append('force_replace', 'true'); // Always force replace in edit mode 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 apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formDataUpload, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
@ -209,7 +209,7 @@ const EditMeeting = ({ user }) => {
const formData = new FormData(); const formData = new FormData();
formData.append('image_file', file); formData.append('image_file', file);
const response = await axios.post( const response = await apiClient.post(
buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_IMAGE(meeting_id)), buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_IMAGE(meeting_id)),
formData, formData,
{ {

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Brain, Users, Calendar, TrendingUp, X, User, Lock } from 'lucide-react'; import { Brain, Users, Calendar, TrendingUp, X, User, Lock } from 'lucide-react';
import axios from 'axios'; import apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import './HomePage.css'; import './HomePage.css';
@ -16,7 +16,7 @@ const HomePage = ({ onLogin }) => {
setLoginError(''); setLoginError('');
try { try {
const response = await axios.post(buildApiUrl(API_ENDPOINTS.AUTH.LOGIN), loginForm); const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.AUTH.LOGIN), loginForm);
onLogin(response.data); onLogin(response.data);
setShowLoginModal(false); setShowLoginModal(false);
} catch (error) { } catch (error) {

View File

@ -738,6 +738,22 @@
gap: 12px; gap: 12px;
} }
.sync-scroll-icon {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: auto;
color: #64748b;
cursor: pointer;
transition: all 0.2s ease;
padding: 2px;
}
.sync-scroll-icon:hover {
color: #3b82f6;
transform: scale(1.1);
}
.auto-scroll-btn { .auto-scroll-btn {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -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 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, Lock, Unlock } from 'lucide-react'; 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 ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
@ -51,32 +51,70 @@ const MeetingDetails = ({ user }) => {
// Cleanup interval on unmount // Cleanup interval on unmount
return () => { return () => {
if (statusCheckInterval) { if (statusCheckInterval) {
console.log('组件卸载,清理转录状态轮询定时器');
clearInterval(statusCheckInterval); clearInterval(statusCheckInterval);
setStatusCheckInterval(null);
} }
}; };
}, [meeting_id]); }, [meeting_id]);
// Cleanup interval when status changes // Cleanup interval when status changes
useEffect(() => { useEffect(() => {
if (transcriptionStatus && !['pending', 'processing'].includes(transcriptionStatus.status)) { if (transcriptionStatus) {
if (statusCheckInterval) { //
clearInterval(statusCheckInterval); if (['completed', 'failed', 'error', 'cancelled'].includes(transcriptionStatus.status)) {
setStatusCheckInterval(null); if (statusCheckInterval) {
clearInterval(statusCheckInterval);
setStatusCheckInterval(null);
}
} }
} }
}, [transcriptionStatus, statusCheckInterval]); }, [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) => { const startStatusPolling = (taskId) => {
// Clear existing interval // Clear existing interval
if (statusCheckInterval) { if (statusCheckInterval) {
clearInterval(statusCheckInterval); clearInterval(statusCheckInterval);
setStatusCheckInterval(null);
} }
// Poll every 3 seconds // Poll every 3 seconds
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
const baseUrl = ""; const baseUrl = "";
const statusResponse = await axios.get(`${baseUrl}/api/transcription/tasks/${taskId}/status`); const statusResponse = await apiClient.get(`${baseUrl}/api/transcription/tasks/${taskId}/status`);
const status = statusResponse.data; const status = statusResponse.data;
setTranscriptionStatus(status); setTranscriptionStatus(status);
@ -87,10 +125,16 @@ const MeetingDetails = ({ user }) => {
clearInterval(interval); clearInterval(interval);
setStatusCheckInterval(null); setStatusCheckInterval(null);
// Refresh meeting details to get updated transcript // Refresh transcript data only if completed successfully
if (status.status === 'completed') { if (status.status === 'completed') {
fetchMeetingDetails(); console.log('转录完成刷新转录数据无loading');
await refreshTranscriptData();
} else {
console.log('转录失败或取消,状态:', status.status);
} }
//
setTranscriptionStatus(status);
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch transcription status:', error); console.error('Failed to fetch transcription status:', error);
@ -113,26 +157,43 @@ const MeetingDetails = ({ user }) => {
const audioEndpoint = API_ENDPOINTS?.MEETINGS?.AUDIO?.(meeting_id) || `/api/meetings/${meeting_id}/audio`; 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 transcriptEndpoint = API_ENDPOINTS?.MEETINGS?.TRANSCRIPT?.(meeting_id) || `/api/meetings/${meeting_id}/transcript`;
const response = await axios.get(`${baseUrl}${detailEndpoint}`); const response = await apiClient.get(`${baseUrl}${detailEndpoint}`);
setMeeting(response.data); setMeeting(response.data);
// Handle transcription status from meeting details // Handle transcription status from meeting details
if (response.data.transcription_status) { if (response.data.transcription_status) {
setTranscriptionStatus(response.data.transcription_status); const newStatus = response.data.transcription_status;
setTranscriptionProgress(response.data.transcription_status.progress || 0); setTranscriptionStatus(newStatus);
setTranscriptionProgress(newStatus.progress || 0);
// If transcription is in progress, start polling for updates // 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); 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 { } else {
setTranscriptionStatus(null); setTranscriptionStatus(null);
setTranscriptionProgress(0); setTranscriptionProgress(0);
//
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
setStatusCheckInterval(null);
}
} }
// Fetch audio file if available // Fetch audio file if available
try { try {
const audioResponse = await axios.get(`${baseUrl}${audioEndpoint}`); const audioResponse = await apiClient.get(`${baseUrl}${audioEndpoint}`);
// Construct URL using uploads path and relative path from database // Construct URL using uploads path and relative path from database
setAudioUrl(`${baseUrl}${audioResponse.data.file_path}`); setAudioUrl(`${baseUrl}${audioResponse.data.file_path}`);
setAudioFileName(audioResponse.data.file_name); setAudioFileName(audioResponse.data.file_name);
@ -144,7 +205,7 @@ const MeetingDetails = ({ user }) => {
// Fetch transcript segments from database // Fetch transcript segments from database
try { try {
const transcriptResponse = await axios.get(`${baseUrl}${transcriptEndpoint}`); const transcriptResponse = await apiClient.get(`${baseUrl}${transcriptEndpoint}`);
setTranscript(transcriptResponse.data); setTranscript(transcriptResponse.data);
console.log('First transcript item:', transcriptResponse.data[0]); console.log('First transcript item:', transcriptResponse.data[0]);
@ -307,7 +368,7 @@ const MeetingDetails = ({ user }) => {
const handleDeleteMeeting = async () => { const handleDeleteMeeting = async () => {
try { try {
await axios.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meeting_id))); await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meeting_id)));
navigate('/dashboard'); navigate('/dashboard');
} catch (err) { } catch (err) {
console.error('Error deleting meeting:', err); console.error('Error deleting meeting:', err);
@ -318,7 +379,7 @@ const MeetingDetails = ({ user }) => {
const handleSpeakerTagUpdate = async (speakerId, newTag) => { const handleSpeakerTagUpdate = async (speakerId, newTag) => {
try { try {
const baseUrl = ""; const baseUrl = "";
await axios.put(`${baseUrl}/api/meetings/${meeting_id}/speaker-tags`, { await apiClient.put(`${baseUrl}/api/meetings/${meeting_id}/speaker-tags`, {
speaker_id: speakerId, speaker_id: speakerId,
new_tag: newTag new_tag: newTag
}); });
@ -350,7 +411,7 @@ const MeetingDetails = ({ user }) => {
new_tag: newTag new_tag: newTag
})); }));
await axios.put(`${baseUrl}/api/meetings/${meeting_id}/speaker-tags/batch`, { await apiClient.put(`${baseUrl}/api/meetings/${meeting_id}/speaker-tags/batch`, {
updates: updates updates: updates
}); });
@ -421,7 +482,7 @@ const MeetingDetails = ({ user }) => {
text_content: text_content text_content: text_content
})); }));
await axios.put(`${baseUrl}/api/meetings/${meeting_id}/transcript/batch`, { await apiClient.put(`${baseUrl}/api/meetings/${meeting_id}/transcript/batch`, {
updates: updates updates: updates
}); });
@ -454,6 +515,27 @@ const MeetingDetails = ({ user }) => {
return items; return items;
}; };
const refreshMeetingSummary = async () => {
try {
const baseUrl = "";
const detailEndpoint = API_ENDPOINTS?.MEETINGS?.DETAIL?.(meeting_id) || `/api/meetings/${meeting_id}`;
// summaryloading
const response = await apiClient.get(`${baseUrl}${detailEndpoint}`);
// summary
setMeeting(prevMeeting => ({
...prevMeeting,
summary: response.data.summary
}));
console.log('会议摘要已更新无loading状态');
} catch (error) {
console.error('刷新会议摘要失败:', error);
}
};
// AI // AI
const generateSummary = async () => { const generateSummary = async () => {
if (summaryLoading) return; if (summaryLoading) return;
@ -461,7 +543,7 @@ const MeetingDetails = ({ user }) => {
setSummaryLoading(true); setSummaryLoading(true);
try { try {
const baseUrl = ""; const baseUrl = "";
const response = await axios.post(`${baseUrl}/api/meetings/${meeting_id}/generate-summary`, { const response = await apiClient.post(`${baseUrl}/api/meetings/${meeting_id}/generate-summary`, {
user_prompt: userPrompt user_prompt: userPrompt
}); });
@ -470,6 +552,9 @@ const MeetingDetails = ({ user }) => {
// //
await fetchSummaryHistory(); await fetchSummaryHistory();
//
await refreshMeetingSummary();
} catch (err) { } catch (err) {
console.error('Error generating summary:', err); console.error('Error generating summary:', err);
setError('生成AI总结失败请重试'); setError('生成AI总结失败请重试');
@ -481,7 +566,7 @@ const MeetingDetails = ({ user }) => {
const fetchSummaryHistory = async () => { const fetchSummaryHistory = async () => {
try { try {
const baseUrl = ""; const baseUrl = "";
const response = await axios.get(`${baseUrl}/api/meetings/${meeting_id}/summaries`); const response = await apiClient.get(`${baseUrl}/api/meetings/${meeting_id}/summaries`);
setSummaryHistory(response.data.summaries); setSummaryHistory(response.data.summaries);
} catch (err) { } catch (err) {
console.error('Error fetching summary history:', err); console.error('Error fetching summary history:', err);
@ -495,6 +580,14 @@ const MeetingDetails = ({ user }) => {
await fetchSummaryHistory(); await fetchSummaryHistory();
}; };
const closeSummaryModal = async () => {
setShowSummaryModal(false);
//
if (summaryResult) {
await refreshMeetingSummary();
}
};
const exportToPDF = async () => { const exportToPDF = async () => {
try { try {
// //
@ -900,13 +993,13 @@ const MeetingDetails = ({ user }) => {
<h3> <h3>
<MessageCircle size={20} /> <MessageCircle size={20} />
对话转录 对话转录
<button <span
className="auto-scroll-btn" className="sync-scroll-icon"
onClick={() => setAutoScrollEnabled(!autoScrollEnabled)} onClick={() => setAutoScrollEnabled(!autoScrollEnabled)}
title={autoScrollEnabled ? "关闭自动滚动" : "开启自动滚动"} title={autoScrollEnabled ? "关闭同步滚动" : "开启同步滚动"}
> >
{autoScrollEnabled ? <Lock size={16} /> : <Unlock size={16} />} {autoScrollEnabled ? <RefreshCw size={16} /> : <RefreshCwOff size={16} />}
</button> </span>
</h3> </h3>
<div className="transcript-controls"> <div className="transcript-controls">
{isCreator && ( {isCreator && (
@ -1127,13 +1220,13 @@ const MeetingDetails = ({ user }) => {
{/* AI Summary Modal */} {/* AI Summary Modal */}
{showSummaryModal && ( {showSummaryModal && (
<div className="summary-modal-overlay" onClick={() => setShowSummaryModal(false)}> <div className="summary-modal-overlay" onClick={closeSummaryModal}>
<div className="summary-modal" onClick={(e) => e.stopPropagation()}> <div className="summary-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header"> <div className="modal-header">
<h3><Brain size={20} /> AI会议总结</h3> <h3><Brain size={20} /> AI会议总结</h3>
<button <button
className="close-btn" className="close-btn"
onClick={() => setShowSummaryModal(false)} onClick={closeSummaryModal}
> >
<X size={20} /> <X size={20} />
</button> </button>

View File

@ -0,0 +1,41 @@
import axios from 'axios';
// 创建axios实例
const apiClient = axios.create();
// 请求拦截器 - 自动添加Authorization头
apiClient.interceptors.request.use(
(config) => {
const savedUser = localStorage.getItem('iMeetingUser');
if (savedUser) {
try {
const user = JSON.parse(savedUser);
if (user.token) {
config.headers.Authorization = `Bearer ${user.token}`;
}
} catch (error) {
console.error('Failed to parse user from localStorage:', error);
localStorage.removeItem('iMeetingUser');
}
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器 - 处理认证错误
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token过期或无效清除本地存储并跳转登录
localStorage.removeItem('iMeetingUser');
window.location.href = '/';
}
return Promise.reject(error);
}
);
export default apiClient;