// Meet Application - Video Conferencing with Bot Integration const meetApp = (function() { 'use strict'; // State management let state = { room: null, localTracks: [], participants: new Map(), isConnected: false, isMuted: false, isVideoOff: false, isScreenSharing: false, isRecording: false, isTranscribing: true, meetingId: null, meetingStartTime: null, ws: null, botEnabled: true, transcriptions: [], chatMessages: [], unreadCount: 0 }; // WebSocket message types const MessageType = { JOIN_MEETING: 'join_meeting', LEAVE_MEETING: 'leave_meeting', TRANSCRIPTION: 'transcription', CHAT_MESSAGE: 'chat_message', BOT_MESSAGE: 'bot_message', SCREEN_SHARE: 'screen_share', STATUS_UPDATE: 'status_update', PARTICIPANT_UPDATE: 'participant_update', RECORDING_CONTROL: 'recording_control', BOT_REQUEST: 'bot_request' }; // Initialize the application async function init() { console.log('Initializing meet application...'); // Setup event listeners setupEventListeners(); // Check for meeting ID in URL const urlParams = new URLSearchParams(window.location.search); const meetingIdFromUrl = urlParams.get('meeting'); const redirectFrom = urlParams.get('from'); if (redirectFrom) { handleRedirect(redirectFrom, meetingIdFromUrl); } else if (meetingIdFromUrl) { state.meetingId = meetingIdFromUrl; showJoinModal(); } else { showCreateModal(); } // Initialize WebSocket connection await connectWebSocket(); // Start timer update startTimer(); } // Setup event listeners function setupEventListeners() { // Control buttons document.getElementById('micBtn').addEventListener('click', toggleMicrophone); document.getElementById('videoBtn').addEventListener('click', toggleVideo); document.getElementById('screenShareBtn').addEventListener('click', toggleScreenShare); document.getElementById('leaveBtn').addEventListener('click', leaveMeeting); // Top controls document.getElementById('recordBtn').addEventListener('click', toggleRecording); document.getElementById('transcribeBtn').addEventListener('click', toggleTranscription); document.getElementById('participantsBtn').addEventListener('click', () => togglePanel('participants')); document.getElementById('chatBtn').addEventListener('click', () => togglePanel('chat')); document.getElementById('botBtn').addEventListener('click', () => togglePanel('bot')); // Modal buttons document.getElementById('joinMeetingBtn').addEventListener('click', joinMeeting); document.getElementById('createMeetingBtn').addEventListener('click', createMeeting); document.getElementById('sendInvitesBtn').addEventListener('click', sendInvites); // Chat document.getElementById('chatInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') sendChatMessage(); }); document.getElementById('sendChatBtn').addEventListener('click', sendChatMessage); // Bot commands document.querySelectorAll('.bot-cmd-btn').forEach(btn => { btn.addEventListener('click', (e) => { const command = e.currentTarget.dataset.command; sendBotCommand(command); }); }); // Transcription controls document.getElementById('downloadTranscriptBtn').addEventListener('click', downloadTranscript); document.getElementById('clearTranscriptBtn').addEventListener('click', clearTranscript); } // WebSocket connection async function connectWebSocket() { return new Promise((resolve, reject) => { const wsUrl = `ws://localhost:8080/ws/meet`; state.ws = new WebSocket(wsUrl); state.ws.onopen = () => { console.log('WebSocket connected'); resolve(); }; state.ws.onmessage = (event) => { handleWebSocketMessage(JSON.parse(event.data)); }; state.ws.onerror = (error) => { console.error('WebSocket error:', error); reject(error); }; state.ws.onclose = () => { console.log('WebSocket disconnected'); // Attempt reconnection setTimeout(connectWebSocket, 5000); }; }); } // Handle WebSocket messages function handleWebSocketMessage(message) { console.log('Received message:', message.type); switch (message.type) { case MessageType.TRANSCRIPTION: handleTranscription(message); break; case MessageType.CHAT_MESSAGE: handleChatMessage(message); break; case MessageType.BOT_MESSAGE: handleBotMessage(message); break; case MessageType.PARTICIPANT_UPDATE: handleParticipantUpdate(message); break; case MessageType.STATUS_UPDATE: handleStatusUpdate(message); break; default: console.log('Unknown message type:', message.type); } } // Send WebSocket message function sendMessage(message) { if (state.ws && state.ws.readyState === WebSocket.OPEN) { state.ws.send(JSON.stringify(message)); } } // Meeting controls async function createMeeting() { const name = document.getElementById('meetingName').value; const description = document.getElementById('meetingDescription').value; const settings = { enable_transcription: document.getElementById('enableTranscription').checked, enable_recording: document.getElementById('enableRecording').checked, enable_bot: document.getElementById('enableBot').checked, waiting_room: document.getElementById('enableWaitingRoom').checked }; try { const response = await fetch('/api/meet/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, description, settings }) }); const data = await response.json(); state.meetingId = data.id; closeModal('createModal'); await joinMeetingRoom(data.id, 'Host'); // Show invite modal setTimeout(() => showInviteModal(), 1000); } catch (error) { console.error('Failed to create meeting:', error); alert('Failed to create meeting. Please try again.'); } } async function joinMeeting() { const userName = document.getElementById('userName').value; const meetingCode = document.getElementById('meetingCode').value; if (!userName || !meetingCode) { alert('Please enter your name and meeting code'); return; } closeModal('joinModal'); await joinMeetingRoom(meetingCode, userName); } async function joinMeetingRoom(roomId, userName) { state.meetingId = roomId; state.meetingStartTime = Date.now(); // Update UI document.getElementById('meetingId').textContent = `Meeting ID: ${roomId}`; document.getElementById('meetingTitle').textContent = userName + "'s Meeting"; // Initialize WebRTC await initializeWebRTC(roomId, userName); // Send join message sendMessage({ type: MessageType.JOIN_MEETING, room_id: roomId, participant_name: userName }); state.isConnected = true; } async function leaveMeeting() { if (!confirm('Are you sure you want to leave the meeting?')) return; // Send leave message sendMessage({ type: MessageType.LEAVE_MEETING, room_id: state.meetingId, participant_id: 'current-user' }); // Clean up if (state.room) { state.room.disconnect(); } state.localTracks.forEach(track => track.stop()); state.localTracks = []; state.participants.clear(); state.isConnected = false; // Redirect window.location.href = '/chat'; } // WebRTC initialization async function initializeWebRTC(roomId, userName) { try { // For LiveKit integration if (window.LiveKitClient) { const room = new LiveKitClient.Room({ adaptiveStream: true, dynacast: true, videoCaptureDefaults: { resolution: LiveKitClient.VideoPresets.h720.resolution } }); // Get token from server const response = await fetch('/api/meet/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room_id: roomId, user_name: userName }) }); const { token } = await response.json(); // Connect to room await room.connect('ws://localhost:7880', token); state.room = room; // Setup event handlers room.on('participantConnected', handleParticipantConnected); room.on('participantDisconnected', handleParticipantDisconnected); room.on('trackSubscribed', handleTrackSubscribed); room.on('trackUnsubscribed', handleTrackUnsubscribed); room.on('activeSpeakersChanged', handleActiveSpeakersChanged); // Publish local tracks await publishLocalTracks(); } else { // Fallback to basic WebRTC await setupBasicWebRTC(roomId, userName); } } catch (error) { console.error('Failed to initialize WebRTC:', error); alert('Failed to connect to meeting. Please check your connection.'); } } async function setupBasicWebRTC(roomId, userName) { // Get user media const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); // Display local video const localVideo = document.getElementById('localVideo'); localVideo.srcObject = stream; state.localTracks = stream.getTracks(); } async function publishLocalTracks() { try { const tracks = await LiveKitClient.createLocalTracks({ audio: true, video: true }); for (const track of tracks) { await state.room.localParticipant.publishTrack(track); state.localTracks.push(track); if (track.kind === 'video') { const localVideo = document.getElementById('localVideo'); track.attach(localVideo); } } } catch (error) { console.error('Failed to publish tracks:', error); } } // Media controls function toggleMicrophone() { state.isMuted = !state.isMuted; state.localTracks.forEach(track => { if (track.kind === 'audio') { track.enabled = !state.isMuted; } }); const micBtn = document.getElementById('micBtn'); micBtn.classList.toggle('muted', state.isMuted); micBtn.querySelector('.icon').textContent = state.isMuted ? '🔇' : '🎤'; updateLocalIndicators(); } function toggleVideo() { state.isVideoOff = !state.isVideoOff; state.localTracks.forEach(track => { if (track.kind === 'video') { track.enabled = !state.isVideoOff; } }); const videoBtn = document.getElementById('videoBtn'); videoBtn.classList.toggle('off', state.isVideoOff); videoBtn.querySelector('.icon').textContent = state.isVideoOff ? '📷' : '📹'; updateLocalIndicators(); } async function toggleScreenShare() { if (!state.isScreenSharing) { try { const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }); if (state.room) { const screenTrack = stream.getVideoTracks()[0]; await state.room.localParticipant.publishTrack(screenTrack); screenTrack.onended = () => { stopScreenShare(); }; } state.isScreenSharing = true; document.getElementById('screenShareBtn').classList.add('active'); // Show screen share overlay const screenShareVideo = document.getElementById('screenShareVideo'); screenShareVideo.srcObject = stream; document.getElementById('screenShareOverlay').classList.remove('hidden'); // Send screen share status sendMessage({ type: MessageType.SCREEN_SHARE, room_id: state.meetingId, participant_id: 'current-user', is_sharing: true, share_type: 'screen' }); } catch (error) { console.error('Failed to share screen:', error); alert('Failed to share screen. Please try again.'); } } else { stopScreenShare(); } } function stopScreenShare() { state.isScreenSharing = false; document.getElementById('screenShareBtn').classList.remove('active'); document.getElementById('screenShareOverlay').classList.add('hidden'); // Send screen share status sendMessage({ type: MessageType.SCREEN_SHARE, room_id: state.meetingId, participant_id: 'current-user', is_sharing: false }); } // Recording and transcription function toggleRecording() { state.isRecording = !state.isRecording; const recordBtn = document.getElementById('recordBtn'); recordBtn.classList.toggle('recording', state.isRecording); sendMessage({ type: MessageType.RECORDING_CONTROL, room_id: state.meetingId, action: state.isRecording ? 'start' : 'stop', participant_id: 'current-user' }); if (state.isRecording) { showNotification('Recording started'); } else { showNotification('Recording stopped'); } } function toggleTranscription() { state.isTranscribing = !state.isTranscribing; const transcribeBtn = document.getElementById('transcribeBtn'); transcribeBtn.classList.toggle('active', state.isTranscribing); if (state.isTranscribing) { showNotification('Transcription enabled'); } else { showNotification('Transcription disabled'); } } function handleTranscription(message) { if (!state.isTranscribing) return; const transcription = { participant_id: message.participant_id, text: message.text, timestamp: new Date(message.timestamp), is_final: message.is_final }; if (message.is_final) { state.transcriptions.push(transcription); addTranscriptionToUI(transcription); // Check for bot wake words if (state.botEnabled && ( message.text.toLowerCase().includes('hey bot') || message.text.toLowerCase().includes('assistant') )) { processBotCommand(message.text, message.participant_id); } } } function addTranscriptionToUI(transcription) { const container = document.getElementById('transcriptionContainer'); const entry = document.createElement('div'); entry.className = 'transcription-entry'; entry.innerHTML = `