959 lines
32 KiB
JavaScript
959 lines
32 KiB
JavaScript
// 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 = `
|
|
<div class="transcription-header">
|
|
<span class="participant-name">Participant ${transcription.participant_id.substring(0, 8)}</span>
|
|
<span class="timestamp">${transcription.timestamp.toLocaleTimeString()}</span>
|
|
</div>
|
|
<div class="transcription-text">${transcription.text}</div>
|
|
`;
|
|
container.appendChild(entry);
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
|
|
// Chat functionality
|
|
function sendChatMessage() {
|
|
const input = document.getElementById('chatInput');
|
|
const content = input.value.trim();
|
|
|
|
if (!content) return;
|
|
|
|
const message = {
|
|
type: MessageType.CHAT_MESSAGE,
|
|
room_id: state.meetingId,
|
|
participant_id: 'current-user',
|
|
content: content,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
sendMessage(message);
|
|
|
|
// Add to local chat
|
|
addChatMessage({
|
|
...message,
|
|
is_self: true
|
|
});
|
|
|
|
input.value = '';
|
|
}
|
|
|
|
function handleChatMessage(message) {
|
|
addChatMessage({
|
|
...message,
|
|
is_self: false
|
|
});
|
|
|
|
// Update unread count if chat panel is hidden
|
|
const chatPanel = document.getElementById('chatPanel');
|
|
if (chatPanel.style.display === 'none') {
|
|
state.unreadCount++;
|
|
updateUnreadBadge();
|
|
}
|
|
}
|
|
|
|
function addChatMessage(message) {
|
|
state.chatMessages.push(message);
|
|
|
|
const container = document.getElementById('chatMessages');
|
|
const messageEl = document.createElement('div');
|
|
messageEl.className = `chat-message ${message.is_self ? 'self' : ''}`;
|
|
messageEl.innerHTML = `
|
|
<div class="message-header">
|
|
<span class="sender-name">${message.is_self ? 'You' : 'Participant'}</span>
|
|
<span class="message-time">${new Date(message.timestamp).toLocaleTimeString()}</span>
|
|
</div>
|
|
<div class="message-content">${message.content}</div>
|
|
`;
|
|
container.appendChild(messageEl);
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
|
|
// Bot integration
|
|
function sendBotCommand(command) {
|
|
const message = {
|
|
type: MessageType.BOT_REQUEST,
|
|
room_id: state.meetingId,
|
|
participant_id: 'current-user',
|
|
command: command,
|
|
parameters: {}
|
|
};
|
|
|
|
sendMessage(message);
|
|
|
|
// Show loading in bot responses
|
|
const responsesContainer = document.getElementById('botResponses');
|
|
const loadingEl = document.createElement('div');
|
|
loadingEl.className = 'bot-response loading';
|
|
loadingEl.innerHTML = '<span class="loading-dots">Processing...</span>';
|
|
responsesContainer.appendChild(loadingEl);
|
|
}
|
|
|
|
function handleBotMessage(message) {
|
|
const responsesContainer = document.getElementById('botResponses');
|
|
|
|
// Remove loading indicator
|
|
const loadingEl = responsesContainer.querySelector('.loading');
|
|
if (loadingEl) loadingEl.remove();
|
|
|
|
// Add bot response
|
|
const responseEl = document.createElement('div');
|
|
responseEl.className = 'bot-response';
|
|
responseEl.innerHTML = `
|
|
<div class="response-header">
|
|
<span class="bot-icon">🤖</span>
|
|
<span class="response-time">${new Date().toLocaleTimeString()}</span>
|
|
</div>
|
|
<div class="response-content">${marked.parse(message.content)}</div>
|
|
`;
|
|
responsesContainer.appendChild(responseEl);
|
|
responsesContainer.scrollTop = responsesContainer.scrollHeight;
|
|
}
|
|
|
|
function processBotCommand(text, participantId) {
|
|
// Process voice command with bot
|
|
sendMessage({
|
|
type: MessageType.BOT_REQUEST,
|
|
room_id: state.meetingId,
|
|
participant_id: participantId,
|
|
command: 'voice_command',
|
|
parameters: { text: text }
|
|
});
|
|
}
|
|
|
|
// Participant management
|
|
function handleParticipantConnected(participant) {
|
|
state.participants.set(participant.sid, participant);
|
|
updateParticipantsList();
|
|
updateParticipantCount();
|
|
|
|
showNotification(`${participant.identity} joined the meeting`);
|
|
}
|
|
|
|
function handleParticipantDisconnected(participant) {
|
|
state.participants.delete(participant.sid);
|
|
|
|
// Remove participant video
|
|
const videoContainer = document.getElementById(`video-${participant.sid}`);
|
|
if (videoContainer) videoContainer.remove();
|
|
|
|
updateParticipantsList();
|
|
updateParticipantCount();
|
|
|
|
showNotification(`${participant.identity} left the meeting`);
|
|
}
|
|
|
|
function handleParticipantUpdate(message) {
|
|
// Update participant status
|
|
updateParticipantsList();
|
|
}
|
|
|
|
function updateParticipantsList() {
|
|
const listContainer = document.getElementById('participantsList');
|
|
listContainer.innerHTML = '';
|
|
|
|
// Add self
|
|
const selfEl = createParticipantElement('You', 'current-user', true);
|
|
listContainer.appendChild(selfEl);
|
|
|
|
// Add other participants
|
|
state.participants.forEach((participant, sid) => {
|
|
const el = createParticipantElement(participant.identity, sid, false);
|
|
listContainer.appendChild(el);
|
|
});
|
|
}
|
|
|
|
function createParticipantElement(name, id, isSelf) {
|
|
const el = document.createElement('div');
|
|
el.className = 'participant-item';
|
|
el.innerHTML = `
|
|
<div class="participant-info">
|
|
<span class="participant-avatar">${name[0].toUpperCase()}</span>
|
|
<span class="participant-name">${name}${isSelf ? ' (You)' : ''}</span>
|
|
</div>
|
|
<div class="participant-controls">
|
|
<span class="indicator ${state.isMuted && isSelf ? 'muted' : ''}">🎤</span>
|
|
<span class="indicator ${state.isVideoOff && isSelf ? 'off' : ''}">📹</span>
|
|
</div>
|
|
`;
|
|
return el;
|
|
}
|
|
|
|
function updateParticipantCount() {
|
|
const count = state.participants.size + 1; // +1 for self
|
|
document.getElementById('participantCount').textContent = count;
|
|
}
|
|
|
|
// Track handling
|
|
function handleTrackSubscribed(track, publication, participant) {
|
|
if (track.kind === 'video') {
|
|
// Create video container for participant
|
|
const videoGrid = document.getElementById('videoGrid');
|
|
const container = document.createElement('div');
|
|
container.className = 'video-container';
|
|
container.id = `video-${participant.sid}`;
|
|
container.innerHTML = `
|
|
<video autoplay></video>
|
|
<div class="video-overlay">
|
|
<span class="participant-name">${participant.identity}</span>
|
|
<div class="video-indicators">
|
|
<span class="indicator mic-indicator">🎤</span>
|
|
<span class="indicator video-indicator">📹</span>
|
|
</div>
|
|
</div>
|
|
<div class="speaking-indicator hidden"></div>
|
|
`;
|
|
|
|
const video = container.querySelector('video');
|
|
track.attach(video);
|
|
|
|
videoGrid.appendChild(container);
|
|
}
|
|
}
|
|
|
|
function handleTrackUnsubscribed(track, publication, participant) {
|
|
track.detach();
|
|
}
|
|
|
|
function handleActiveSpeakersChanged(speakers) {
|
|
// Update speaking indicators
|
|
document.querySelectorAll('.speaking-indicator').forEach(el => {
|
|
el.classList.add('hidden');
|
|
});
|
|
|
|
speakers.forEach(participant => {
|
|
const container = document.getElementById(`video-${participant.sid}`);
|
|
if (container) {
|
|
container.querySelector('.speaking-indicator').classList.remove('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
// UI helpers
|
|
function togglePanel(panelName) {
|
|
const panels = {
|
|
participants: 'participantsPanel',
|
|
chat: 'chatPanel',
|
|
transcription: 'transcriptionPanel',
|
|
bot: 'botPanel'
|
|
};
|
|
|
|
const panelId = panels[panelName];
|
|
const panel = document.getElementById(panelId);
|
|
|
|
if (panel) {
|
|
const isVisible = panel.style.display !== 'none';
|
|
|
|
// Hide all panels
|
|
Object.values(panels).forEach(id => {
|
|
document.getElementById(id).style.display = 'none';
|
|
});
|
|
|
|
// Toggle selected panel
|
|
if (!isVisible) {
|
|
panel.style.display = 'block';
|
|
|
|
// Clear unread count for chat
|
|
if (panelName === 'chat') {
|
|
state.unreadCount = 0;
|
|
updateUnreadBadge();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateLocalIndicators() {
|
|
const micIndicator = document.getElementById('localMicIndicator');
|
|
const videoIndicator = document.getElementById('localVideoIndicator');
|
|
|
|
micIndicator.classList.toggle('muted', state.isMuted);
|
|
videoIndicator.classList.toggle('off', state.isVideoOff);
|
|
}
|
|
|
|
function updateUnreadBadge() {
|
|
const badge = document.getElementById('unreadCount');
|
|
badge.textContent = state.unreadCount;
|
|
badge.classList.toggle('hidden', state.unreadCount === 0);
|
|
}
|
|
|
|
function showNotification(message) {
|
|
// Simple notification - could be enhanced with toast notifications
|
|
console.log('Notification:', message);
|
|
}
|
|
|
|
// Modals
|
|
function showJoinModal() {
|
|
document.getElementById('joinModal').classList.remove('hidden');
|
|
setupPreview();
|
|
}
|
|
|
|
function showCreateModal() {
|
|
document.getElementById('createModal').classList.remove('hidden');
|
|
}
|
|
|
|
function showInviteModal() {
|
|
const meetingLink = `${window.location.origin}/meet?meeting=${state.meetingId}`;
|
|
document.getElementById('meetingLink').value = meetingLink;
|
|
document.getElementById('inviteModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeModal(modalId) {
|
|
document.getElementById(modalId).classList.add('hidden');
|
|
}
|
|
|
|
window.closeModal = closeModal;
|
|
|
|
async function setupPreview() {
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
video: true,
|
|
audio: true
|
|
});
|
|
|
|
const previewVideo = document.getElementById('previewVideo');
|
|
previewVideo.srcObject = stream;
|
|
|
|
// Stop tracks when modal closes
|
|
setTimeout(() => {
|
|
stream.getTracks().forEach(track => track.stop());
|
|
}, 30000);
|
|
} catch (error) {
|
|
console.error('Failed to setup preview:', error);
|
|
}
|
|
}
|
|
|
|
// Timer
|
|
function startTimer() {
|
|
setInterval(() => {
|
|
if (state.meetingStartTime) {
|
|
const duration = Date.now() - state.meetingStartTime;
|
|
const hours = Math.floor(duration / 3600000);
|
|
const minutes = Math.floor((duration % 3600000) / 60000);
|
|
const seconds = Math.floor((duration % 60000) / 1000);
|
|
|
|
const timerEl = document.getElementById('meetingTimer');
|
|
timerEl.textContent = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
// Invite functions
|
|
async function sendInvites() {
|
|
const emails = document.getElementById('inviteEmails').value
|
|
.split('\n')
|
|
.map(e => e.trim())
|
|
.filter(e => e);
|
|
|
|
if (emails.length === 0) {
|
|
alert('Please enter at least one email address');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/meet/invite', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
meeting_id: state.meetingId,
|
|
emails: emails
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
alert('Invitations sent successfully!');
|
|
closeModal('inviteModal');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to send invites:', error);
|
|
alert('Failed to send invitations. Please try again.');
|
|
}
|
|
}
|
|
|
|
window.copyMeetingLink = function() {
|
|
const linkInput = document.getElementById('meetingLink');
|
|
linkInput.select();
|
|
document.execCommand('copy');
|
|
alert('Meeting link copied to clipboard!');
|
|
};
|
|
|
|
window.shareVia = function(platform) {
|
|
const meetingLink = document.getElementById('meetingLink').value;
|
|
const message = `Join my meeting: ${meetingLink}`;
|
|
|
|
switch (platform) {
|
|
case 'whatsapp':
|
|
window.open(`https://wa.me/?text=${encodeURIComponent(message)}`);
|
|
break;
|
|
case 'teams':
|
|
// Teams integration would require proper API
|
|
alert('Teams integration coming soon!');
|
|
break;
|
|
case 'email':
|
|
window.location.href = `mailto:?subject=Meeting Invitation&body=${encodeURIComponent(message)}`;
|
|
break;
|
|
}
|
|
};
|
|
|
|
// Redirect handling for Teams/WhatsApp
|
|
function handleRedirect(platform, meetingId) {
|
|
document.getElementById('redirectHandler').classList.remove('hidden');
|
|
document.getElementById('callerPlatform').textContent = platform;
|
|
|
|
// Auto-accept after 3 seconds
|
|
setTimeout(() => {
|
|
acceptCall();
|
|
}, 3000);
|
|
}
|
|
|
|
window.acceptCall = async function() {
|
|
document.getElementById('redirectHandler').classList.add('hidden');
|
|
|
|
if (state.meetingId) {
|
|
// Already in a meeting, ask to switch
|
|
if (confirm('You are already in a meeting. Switch to the new call?')) {
|
|
await leaveMeeting();
|
|
await joinMeetingRoom(state.meetingId, 'Guest');
|
|
}
|
|
} else {
|
|
await joinMeetingRoom(state.meetingId || 'redirect-room', 'Guest');
|
|
}
|
|
};
|
|
|
|
window.rejectCall = function() {
|
|
document.getElementById('redirectHandler').classList.add('hidden');
|
|
window.location.href = '/chat';
|
|
};
|
|
|
|
// Transcript download
|
|
function downloadTranscript() {
|
|
const transcript = state.transcriptions
|
|
.map(t => `[${t.timestamp.toLocaleTimeString()}] ${t.participant_id}: ${t.text}`)
|
|
.join('\n');
|
|
|
|
const blob = new Blob([transcript], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `meeting-transcript-${state.meetingId}.txt`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function clearTranscript() {
|
|
if (confirm('Are you sure you want to clear the transcript?')) {
|
|
state.transcriptions = [];
|
|
document.getElementById('transcriptionContainer').innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// Status updates
|
|
function handleStatusUpdate(message) {
|
|
console.log('Meeting status update:', message.status);
|
|
|
|
if (message.status === 'ended') {
|
|
alert('The meeting has ended.');
|
|
window.location.href = '/chat';
|
|
}
|
|
}
|
|
|
|
// Initialize on load
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
// Public API
|
|
return {
|
|
joinMeeting: joinMeetingRoom,
|
|
leaveMeeting: leaveMeeting,
|
|
sendMessage: sendMessage,
|
|
toggleMicrophone: toggleMicrophone,
|
|
toggleVideo: toggleVideo,
|
|
toggleScreenShare: toggleScreenShare,
|
|
sendBotCommand: sendBotCommand
|
|
};
|
|
})();
|