import { useState, useEffect, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Button } from '../ui/button'; import { Badge } from '../ui/badge'; import { Textarea } from '../ui/textarea'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '../ui/alert-dialog'; import { Video, VideoOff, Mic, MicOff, PhoneOff, MessageSquare, ClipboardEdit, Save, Wifi, AlertTriangle, Clock, LogOut, } from 'lucide-react'; import { toast } from 'sonner'; import { useAuth } from '../../lib/authContext'; import { useWebRTC } from '../../hooks/useWebRTC'; import { useLanguage } from '../../lib/languageContext'; import { getApiBaseUrl } from '../../lib/api/config'; interface LiveSessionProps { sessionId?: string; onNavigate?: (page: string) => void; } export function LiveSession({ sessionId: propSessionId, onNavigate }: LiveSessionProps) { const { sessionId: paramSessionId } = useParams(); const navigate = useNavigate(); const sessionId = propSessionId || paramSessionId || 'unknown'; const { user } = useAuth(); const { language = 'ar' } = useLanguage?.() ?? {}; const [isVideoOn, setIsVideoOn] = useState(true); const [isAudioOn, setIsAudioOn] = useState(true); const [sessionTime, setSessionTime] = useState(0); const [newMessage, setNewMessage] = useState(''); const [showChat, setShowChat] = useState(false); const [showNotes, setShowNotes] = useState(false); const [sessionNotes, setSessionNotes] = useState(''); const [isSavingNotes, setIsSavingNotes] = useState(false); const [lastSavedNotes, setLastSavedNotes] = useState(''); const [sessionData, setSessionData] = useState(null); const [isEndingSession, setIsEndingSession] = useState(false); const [endSessionError, setEndSessionError] = useState(null); const [showEndConfirm, setShowEndConfirm] = useState(false); const joinRecordedRef = useRef(false); const localVideoRef = useRef(null); const remoteVideoRef = useRef(null); const { localStream, remoteStream, isConnected, isCameraAvailable, messages, sendChatMessage, toggleVideo, toggleAudio, connectionQuality } = useWebRTC( sessionId, user?.id || 'anonymous' ); // Fetch session data useEffect(() => { const fetchSession = async () => { if (sessionId === 'unknown' || !user) return; try { const token = localStorage.getItem('authToken'); const rolePath = user.role === 'sheikh' ? 'sheikh' : 'student'; const response = await fetch(`${getApiBaseUrl()}/api/${rolePath}/sessions/${sessionId}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const data = await response.json(); setSessionData(data); if (data.sessionNotes) setSessionNotes(data.sessionNotes); const status = data.sessionStatus || data.status; if (status === 'MISSED' || status === 'CANCELLED') { if (user.role === 'student') { // Send student to session report so they can leave a review navigate(`/session-report/${sessionId}?mode=review`); } else { navigate('/sheikh/sessions'); } return; } // If the session hasn't started yet AND we somehow landed here without // a join in progress, go back to waiting room. But don't redirect if // the join was just recorded (status may lag behind by one poll cycle). if ((status === 'SCHEDULED' || status === 'PENDING') && !joinRecordedRef.current) { navigate(`/waiting-room/${sessionId}`, { replace: true }); return; } if (status === 'COMPLETED' && user.role === 'student') { navigate(`/session-report/${sessionId}?mode=review`); return; } } } catch (error) { console.error('Failed to fetch session data:', error); } }; fetchSession(); const interval = setInterval(fetchSession, 5000); // Poll status every 5 seconds return () => clearInterval(interval); }, [sessionId, user, navigate, onNavigate, language]); // Record join for BOTH sheikh and student on mount — this sets joinedAt timestamps // and transitions status to ON_GOING once both have joined. useEffect(() => { if (sessionId === 'unknown' || !user || joinRecordedRef.current) return; joinRecordedRef.current = true; const recordJoin = async () => { try { const token = localStorage.getItem('authToken'); // Sheikh uses /api/sheikh/sessions/{id}/join // Student uses /api/student/sessions/{id}/join const rolePath = user.role === 'sheikh' ? 'sheikh' : 'student'; const response = await fetch(`${getApiBaseUrl()}/api/${rolePath}/sessions/${sessionId}/join`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { console.log(`[${user.role}] join recorded for session ${sessionId}`); } else { console.error('Failed to record join:', await response.text()); } } catch (error) { console.error('Network error recording join:', error); } }; recordJoin(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessionId, user?.role]); // Bind local stream to video element useEffect(() => { if (localVideoRef.current && localStream) { localVideoRef.current.srcObject = localStream; } }, [localStream]); // Bind remote stream to video element useEffect(() => { if (remoteVideoRef.current && remoteStream) { remoteVideoRef.current.srcObject = remoteStream; } }, [remoteStream]); // Session timer useEffect(() => { const timer = setInterval(() => setSessionTime(prev => prev + 1), 1000); return () => clearInterval(timer); }, []); // Auto-save notes every 60s (sheikh only) useEffect(() => { if (user?.role !== 'sheikh') return; const timer = setInterval(() => handleSaveNotes(true), 60000); return () => clearInterval(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessionNotes, lastSavedNotes, sessionId]); const formatTime = (seconds: number) => { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; }; const handleSendMessage = () => { if (newMessage.trim()) { sendChatMessage(newMessage); setNewMessage(''); } }; const handleSaveNotes = async (isAutoSave = false) => { if (user?.role !== 'sheikh' || sessionNotes === lastSavedNotes) return; if (!isAutoSave) setIsSavingNotes(true); try { const token = localStorage.getItem('authToken'); const response = await fetch(`${getApiBaseUrl()}/api/sheikh/sessions/${sessionId}/notes`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ notes: sessionNotes }) }); if (!response.ok) throw new Error('Failed to save notes'); setLastSavedNotes(sessionNotes); if (!isAutoSave) toast.success('Notes saved successfully!'); } catch (error) { console.error('Failed to save notes:', error); if (!isAutoSave) toast.error('Failed to save notes. Please try again.'); } finally { if (!isAutoSave) setIsSavingNotes(false); } }; // ── Sheikh: confirm dialog → POST /end → navigate to sessions list ────────── const handleSheikhEndSession = async () => { setIsEndingSession(true); setEndSessionError(null); try { const token = localStorage.getItem('authToken'); const response = await fetch(`${getApiBaseUrl()}/api/sheikh/sessions/${sessionId}/end`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ notes: sessionNotes }), }); if (!response.ok) { const msg = await response.text().catch(() => 'Failed to end session'); setEndSessionError(msg || 'Failed to end session. Please try again.'); return; } // Record sheikh leave timestamp (best-effort) await fetch(`${getApiBaseUrl()}/api/sheikh/sessions/${sessionId}/leave`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, }).catch(() => {}); if (onNavigate) onNavigate('my-sessions'); else navigate('/sheikh/sessions'); } catch (error) { console.error('Failed to end session:', error); setEndSessionError('Network error. Please try again.'); } finally { setIsEndingSession(false); setShowEndConfirm(false); } }; // ── Student: record leave → navigate to waiting screen (NOT review page) ─── const handleStudentLeave = async () => { try { const token = localStorage.getItem('authToken'); await fetch(`${getApiBaseUrl()}/api/student/sessions/${sessionId}/leave`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, }); } catch (error) { console.error('Failed to record leave (best-effort):', error); } if (onNavigate) onNavigate('session-waiting'); else navigate(`/session-waiting/${sessionId}`); }; const getConnectionStatusColor = () => { switch (connectionQuality) { case 'good': return 'text-emerald-500'; case 'fair': return 'text-yellow-500'; case 'poor': return 'text-red-500'; default: return 'text-gray-500'; } }; const getConnectionStatusLabel = () => { if (!isConnected) return 'Connecting to Server...'; switch (connectionQuality) { case 'good': return 'Excellent'; case 'fair': return 'Stable'; case 'poor': return 'Poor Connection'; default: return 'Waiting for Peer...'; } }; return (
{/* Camera/Mic Denied Warning */} {!isCameraAvailable && (
Camera and microphone access was denied. The other participant cannot see or hear you. Please allow access in your browser settings and refresh.
)} {/* Header */}

{sessionData?.sessionDescription || 'Ongoing Session'}

with {user?.role === 'sheikh' ? sessionData?.studentName : sessionData?.sheikhName}

{connectionQuality === 'poor' ? : } {getConnectionStatusLabel()}
{formatTime(sessionTime)}
LIVE
{/* Main Content */}
{/* Video Area */}
{/* Remote Video */}
{remoteStream ? (
{/* Local Video (PiP) */}
{/* Chat Sidebar */} {showChat && (

Chat

{messages.map((msg, index) => (

{msg.message}

{msg.sender} • {msg.time}

))}