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 { 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 Dashboard from './pages/Dashboard';
import MeetingDetails from './pages/MeetingDetails';
@ -33,9 +35,18 @@ function App() {
localStorage.setItem('iMeetingUser', JSON.stringify(userData));
};
const handleLogout = () => {
setUser(null);
localStorage.removeItem('iMeetingUser');
const handleLogout = async () => {
try {
// 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) {

View File

@ -3,7 +3,11 @@ const API_CONFIG = {
BASE_URL: "",
ENDPOINTS: {
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: {
LIST: '/api/users',

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
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 { buildApiUrl, API_ENDPOINTS } from '../config/api';
import DateTimePicker from '../components/DateTimePicker';
@ -26,7 +26,7 @@ const CreateMeeting = ({ user }) => {
const fetchUsers = async () => {
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));
} catch (err) {
console.error('Error fetching users:', err);
@ -100,7 +100,7 @@ const CreateMeeting = ({ user }) => {
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;
// Upload audio file if provided
@ -109,7 +109,7 @@ const CreateMeeting = ({ user }) => {
formDataUpload.append('audio_file', audioFile);
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: {
'Content-Type': 'multipart/form-data',
},

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from '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 { buildApiUrl, API_ENDPOINTS } from '../config/api';
import MeetingTimeline from '../components/MeetingTimeline';
@ -21,11 +21,11 @@ const Dashboard = ({ user, onLogout }) => {
setLoading(true);
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);
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);
setMeetings(meetingsResponse.data);
@ -39,9 +39,9 @@ const Dashboard = ({ user, onLogout }) => {
const handleDeleteMeeting = async (meetingId) => {
try {
await axios.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId)));
await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId)));
// 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);
} catch (err) {
console.error('Error deleting meeting:', err);

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
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 MDEditor, * as commands from '@uiw/react-md-editor';
import '@uiw/react-md-editor/markdown-editor.css';
@ -42,7 +42,7 @@ const EditMeeting = ({ user }) => {
const fetchMeetingData = async () => {
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;
// Check if current user is the creator
@ -68,7 +68,7 @@ const EditMeeting = ({ user }) => {
const fetchUsers = async () => {
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));
} catch (err) {
console.error('Error fetching users:', err);
@ -141,7 +141,7 @@ const EditMeeting = ({ user }) => {
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}`);
} catch (err) {
setError(err.response?.data?.detail || '更新会议失败,请重试');
@ -165,7 +165,7 @@ const EditMeeting = ({ user }) => {
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 apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formDataUpload, {
headers: {
'Content-Type': 'multipart/form-data',
},
@ -209,7 +209,7 @@ const EditMeeting = ({ user }) => {
const formData = new FormData();
formData.append('image_file', file);
const response = await axios.post(
const response = await apiClient.post(
buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_IMAGE(meeting_id)),
formData,
{

View File

@ -1,6 +1,6 @@
import React, { useState } from '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 './HomePage.css';
@ -16,7 +16,7 @@ const HomePage = ({ onLogin }) => {
setLoginError('');
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);
setShowLoginModal(false);
} catch (error) {

View File

@ -738,6 +738,22 @@
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 {
display: flex;
align-items: center;

View File

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