botserver/templates/meet.html

950 lines
24 KiB
HTML
Raw Normal View History

2025-11-29 16:29:28 -03:00
{% extends "base.html" %}
{% block title %}Meet - BotServer{% endblock %}
{% block content %}
<div class="meet-container">
<!-- Meet Header -->
<div class="meet-header">
<h1>Video Meetings</h1>
<div class="meet-actions">
<button class="btn btn-primary"
hx-get="/api/meet/new"
hx-target="#modal-container"
hx-swap="innerHTML">
<span>🎥</span> New Meeting
</button>
<button class="btn btn-secondary"
hx-get="/api/meet/join</span>"
hx-target="#modal-container"
hx-swap="innerHTML">
<span>🔗</span> Join Meeting
</button>
</div>
</div>
<!-- Main Content -->
<div class="meet-content">
<!-- Left Panel - Meetings List -->
<div class="meet-sidebar">
<!-- Meeting Tabs -->
<div class="meet-tabs">
<button class="tab-btn active"
hx-get="/api/meet/upcoming"
hx-target="#meetings-list"
hx-swap="innerHTML">
Upcoming
</button>
<button class="tab-btn"
hx-get="/api/meet/past"
hx-target="#meetings-list"
hx-swap="innerHTML">
Past
</button>
<button class="tab-btn"
hx-get="/api/meet/recorded"
hx-target="#meetings-list"
hx-swap="innerHTML">
Recorded
</button>
</div>
<!-- Meetings List -->
<div class="meetings-list" id="meetings-list"
hx-get="/api/meet/upcoming"
hx-trigger="load, every 60s"
hx-swap="innerHTML">
<div class="loading">Loading meetings...</div>
</div>
</div>
<!-- Center - Meeting Area -->
<div class="meet-main" id="meet-main">
<!-- Pre-meeting Screen -->
<div class="pre-meeting" id="pre-meeting">
<div class="preview-container">
<div class="video-preview">
<video id="local-preview" autoplay muted></video>
<div class="preview-controls">
<button class="control-btn" id="toggle-camera" onclick="toggleCamera()">
<span>📹</span>
</button>
<button class="control-btn" id="toggle-mic" onclick="toggleMic()">
<span>🎤</span>
</button>
<button class="control-btn" onclick="testAudio()">
<span>🔊</span> Test Audio
</button>
</div>
</div>
<div class="meeting-info">
<h2>Ready to join?</h2>
<p>Check your audio and video before joining</p>
<div class="device-selectors">
<div class="device-selector">
<label>Camera</label>
<select id="camera-select" onchange="changeCamera()">
<option>Loading cameras...</option>
</select>
</div>
<div class="device-selector">
<label>Microphone</label>
<select id="mic-select" onchange="changeMic()">
<option>Loading microphones...</option>
</select>
</div>
<div class="device-selector">
<label>Speaker</label>
<select id="speaker-select" onchange="changeSpeaker()">
<option>Loading speakers...</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- In-Meeting Screen (hidden by default) -->
<div class="in-meeting" id="in-meeting" style="display: none;">
<!-- Video Grid -->
<div class="video-grid" id="video-grid">
<div class="video-container main-video">
<video id="main-video" autoplay></video>
<div class="participant-info">
<span class="participant-name">You</span>
<span class="participant-status">🎤</span>
</div>
</div>
</div>
<!-- Meeting Controls -->
<div class="meeting-controls">
<div class="controls-left">
<span class="meeting-timer" id="meeting-timer">00:00</span>
<span class="meeting-id" id="meeting-id"></span>
</div>
<div class="controls-center">
<button class="control-btn" onclick="toggleMicrophone()">
<span>🎤</span>
</button>
<button class="control-btn" onclick="toggleVideo</span>()">
<span>📹</span>
</button>
<button class="control-btn" onclick="toggleScreenShare()">
<span>🖥️</span>
</button>
<button class="control-btn" onclick="toggleRecording()">
<span>⏺️</span>
</button>
<button class="control-btn danger" onclick="leaveMeeting()">
<span>📞</span></span> Leave
</button>
</div>
<div class="controls-right">
<button class="control-btn" onclick="toggleParticipants()">
<span>👥</span>
<span class="participant-count">1</span>
</button>
<button class="control-btn" onclick="toggleChat()">
<span>💬</span>
<span class="chat-badge" style="display: none;">0</span>
</button>
<button class="control-btn" onclick="toggleTranscription()">
<span>📝</span>
</button>
<button class="control-btn" onclick="toggleSettings()">
<span>⚙️</span>
</button>
</div>
</div>
</div>
</div>
<!-- Right Panel - Chat/Participants (hidden by default) -->
<div class="meet-panel" id="meet-panel" style="display: none;">
<!-- Panel Tabs -->
<div class="panel-tabs">
<button class="panel-tab active" onclick="showPanelTab('participants')">
Participants
</button>
<button class="panel-tab" onclick="showPanelTab('chat')">
Chat
</button>
<button class="panel-tab" onclick="showPanelTab('transcription')">
Transcription
</button>
</div>
<!-- Participants Panel -->
<div class="panel-content" id="participants-panel">
<div class="participants-list" id="participants-list"
hx-get="/api/meet/participants"
hx-trigger="load, every 5s"
hx-swap="innerHTML">
<div class="participant-item">
<span class="participant-avatar">👤</span>
<span class="participant-name">{{ user_name }} (You)</span>
<span class="participant-controls">
<span>🎤</span>
<span>📹</span>
</span>
</div>
</div></span>
</div>
<!-- Chat Panel -->
<div class="panel-content" id="chat-panel" style="display: none;">
<div class="chat-messages" id="meet-chat-messages"
hx-get="/api/meet/chat/messages"
hx-trigger="load, sse:message"
hx-swap="innerHTML">
</div>
<form class="chat-input-form"
hx-post="/api/meet/chat/send"
hx-target="#meet-chat-messages"
hx-swap="beforeend">
<input type="text"
name="message"
placeholder="Type a message..."
autocomplete="off">
<button type="submit">Send</button>
</form>
</div>
<!-- Transcription Panel -->
<div class="panel-content" id="transcription-panel" style="display: none;">
<div class="transcription-content" id="transcription-content"
hx-get="/api/meet/transcription"
hx-trigger="load, sse:transcription"
hx-swap="innerHTML">
<p class="transcription-empty">Transcription will appear here when enabled</p>
</div>
<div class="transcription-actions">
<button class="btn btn-sm" onclick="downloadTranscript()">
Download Transcript
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal Container -->
<div id="modal-container"></div>
<!-- WebSocket Connection for Real-time -->
<div hx-ext="ws" ws-connect="/ws/meet" id="meet-ws"></div>
<style>
.meet-container {
height: calc(100vh - var(--header-height));
display: flex;
flex-direction: column;
}
.meet-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.meet-actions {
display: flex;
gap: 0.5rem;
}
.meet-content {
flex: 1;
display: flex;
overflow: hidden;
}
.meet-sidebar {
width: 320px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.meet-tabs {
display: flex;
padding: 1rem;
gap: 0.5rem;
border-bottom: 1px solid var(--border);
}
.tab-btn {
flex: 1;
padding: 0.5rem;
background: var(--background);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
}
.tab-btn.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.meetings-list {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.meeting-item {
background: var(--background);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.meeting-item:hover {
background: var(--hover);
transform: translateX(4px);
}
.meeting-title {
font-weight: 500;
margin-bottom: 0.5rem;
}
.meeting-time {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.meeting-participants {
font-size: 0.8125rem;
color: var(--text-secondary);
}
.meet-main {
flex: 1;
display: flex;
flex-direction: column;
background: var(--background);
}
.pre-meeting {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.preview-container {
display: flex;
gap: 2rem;
align-items: center;
}
.video-preview {
position: relative;
width: 480px;
height: 360px;
background: #000;
border-radius: 12px;
overflow: hidden;
}
.video-preview video {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-controls {
position: absolute;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 0.5rem;
}
.meeting-info {
max-width: 300px;
}
.meeting-info h2 {
margin-bottom: 0.5rem;
}
.meeting-info p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.device-selectors {
display: flex;
flex-direction: column;
gap: 1rem;
}
.device-selector {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.device-selector label {
font-size: 0.875rem;
color: var(--text-secondary);
}
.device-selector select {
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
font-size: 0.875rem;
}
.in-meeting {
flex: 1;
display: flex;
flex-direction: column;
}
.video-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1rem;
padding: 1rem;
background: #000;
}
.video-container {
position: relative;
background: #1a1a1a;
border-radius: 8px;
overflow: hidden;
aspect-ratio: 16/9;
}
.video-container video {
width: 100%;
height: 100%;
object-fit: cover;
}
.participant-info {
position: absolute;
bottom: 0.5rem;
left: 0.5rem;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.meeting-controls {
background: var(--surface);
border-top: 1px solid var(--border);
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.controls-left,
.controls-center,
.controls-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-btn {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--background);
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
font-size: 1.25rem;
}
.control-btn:hover {
background: var(--hover);
transform: scale(1.1);
}
.control-btn.active {
background: var(--primary);
color: white;
}
.control-btn.danger {
background: #ef4444;
color: white;
width: auto;
padding: 0 1rem;
border-radius: 24px;
}
.meeting-timer {
font-family: monospace;
font-size: 1rem;
color: var(--text-secondary);
}
.meeting-id {
font-size: 0.875rem;
color: var(--text-secondary);
}
.participant-count,
.chat-badge {
background: var(--primary);
color: white;
font-size: 0.75rem;
padding: 0.125rem 0.25rem;
border-radius: 10px;
margin-left: 0.25rem;
}
.meet-panel {
width: 360px;
background: var(--surface);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.panel-tabs {
display: flex;
border-bottom: 1px solid var(--border);
}
.panel-tab {
flex: 1;
padding: 1rem;
background: none;
border: none;
cursor: pointer;
font-size: 0.875rem;
color: var(--text-secondary);
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.panel-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.panel-content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.participants-list {
padding: 1rem;
}
.participant-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 0.5rem;
}
.participant-item:hover {
background: var(--hover);
}
.participant-avatar {
font-size: 1.5rem;
}
.participant-name {
flex: 1;
font-size: 0.875rem;
}
.participant-controls {
display: flex;
gap: 0.5rem;
}
.chat-messages {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
.chat-message {
margin-bottom: 1rem;
}
.chat-sender {
font-weight: 500;
font-size: 0.875rem;
margin-bottom: 0.25rem;
}
.chat-text {
font-size: 0.875rem;
color: var(--text-secondary);
}
.chat-time {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 0.25rem;
}
.chat-input-form {
display: flex;
padding: 1rem;
border-top: 1px solid var(--border);
}
.chat-input-form input {
flex: 1;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 6px 0 0 6px;
font-size: 0.875rem;
}
.chat-input-form button {
padding: 0.5rem 1rem;
background: var(--primary);
color: white;
border: none;
border-radius: 0 6px 6px 0;
cursor: pointer;
}
.transcription-content {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
.transcription-empty {
text-align: center;
color: var(--text-secondary);
padding: 2rem;
}
.transcription-actions {
padding: 1rem;
border-top: 1px solid var(--border);
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
color: var(--text-secondary);
}
/* Responsive */
@media (max-width: 1024px) {
.meet-sidebar {
width: 280px;
}
.meet-panel {
width: 320px;
}
}
@media (max-width: 768px) {
.meet-sidebar {
display: none;
}
.meet-panel {
position: fixed;
top: 0;
right: -100%;
bottom: 0;
width: 100%;
z-index: 1000;
transition: right 0.3s;
}
.meet-panel.active {
right: 0;
}
.preview-container {
flex-direction: column;
}
.video-preview {
width: 100%;
max-width: 480px;
}
}
</style>
<script>
let localStream = null;
let meetingTimer = null;
let meetingStartTime = null;
// Initialize local preview
async function initPreview() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
const video = document.getElementById('local-preview');
if (video) {
video.srcObject = localStream;
}
// Populate device selectors
await updateDeviceList();
} catch (err) {
console.error('Error accessing media devices:', err);
}
}
// Update device list
async function updateDeviceList() {
const devices = await navigator.mediaDevices.enumerateDevices();
const cameras = devices.filter(d => d.kind === 'videoinput');
const mics = devices.filter(d => d.kind === 'audioinput');
const speakers = devices.filter(d => d.kind === 'audiooutput');
updateSelect('camera-select', cameras);
updateSelect('mic-select', mics);
updateSelect('speaker-select', speakers);
}
function updateSelect(selectId, devices) {
const select = document.getElementById(selectId);
if (!select) return;
select.innerHTML = '';
devices.forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `Device ${device.deviceId.substr(0, 5)}`;
select.appendChild(option);
});
}
// Toggle functions
function toggleCamera() {
if (localStream) {
const videoTrack = localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
document.getElementById('toggle-camera').classList.toggle('active');
}
}
}
function toggleMic() {
if (localStream) {
const audioTrack = localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
document.getElementById('toggle-mic').classList.toggle('active');
}
}
}
function toggleMicrophone() {
toggleMic();
}
function toggleVideo() {
toggleCamera();
}
function toggleScreenShare() {
// Screen share implementation
console.log('Toggle screen share');
}
function toggleRecording() {
// Recording implementation
console.log('Toggle recording');
}
function toggleTranscription() {
// Transcription implementation
console.log('Toggle transcription');
}
function toggleParticipants() {
togglePanel('participants');
}
function toggleChat() {
togglePanel('chat');
}
function toggleSettings() {
// Settings implementation
console.log('Toggle settings');
}
function togglePanel(panelName) {
const panel = document.getElementById('meet-panel');
if (panel) {
if (panel.style.display === 'none') {
panel.style.display = 'flex';
showPanelTab(panelName);
} else {
panel.style.display = 'none';
}
}
}
function showPanelTab(tabName) {
// Hide all panels
document.querySelectorAll('.panel-content').forEach(p => {
p.style.display = 'none';
});
// Remove active class from all tabs
document.querySelectorAll('.panel-tab').forEach(t => {
t.classList.remove('active');
});
// Show selected panel
const panel = document.getElementById(`${tabName}-panel`);
if (panel) {
panel.style.display = 'flex';
}
// Set active tab
event.target.classList.add('active');
}
function joinMeeting(meetingId) {
// Hide pre-meeting screen
document.getElementById('pre-meeting').style.display = 'none';
// Show in-meeting screen
document.getElementById('in-meeting').style.display = 'flex';
// Start timer
startMeetingTimer();
// Set meeting ID
document.getElementById('meeting-id').textContent = `Meeting: ${meetingId}`;
}
function leaveMeeting() {
if (confirm('Are you sure you want to leave the meeting?')) {
// Stop all tracks
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
// Stop timer
if (meetingTimer) {
clearInterval(meetingTimer);
}
// Show pre-meeting screen
document.getElementById('in-meeting').style.display = 'none';
document.getElementById('pre-meeting').style.display = 'flex';
// Reinitialize preview
initPreview();
}
}
function startMeetingTimer() {
meetingStartTime = Date.now();
meetingTimer = setInterval(() => {
const elapsed = Math.floor((Date.now() - meetingStartTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
document.getElementById('meeting-timer').textContent =
`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}, 1000);
}
function testAudio() {
// Play test sound
const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSp9y+7hljcFHWzA7+OZURE');
audio.play();
}
function downloadTranscript() {
// Download transcript implementation
console.log('Download transcript');
}
// Initialize on load
document.addEventListener('DOMContentLoaded', function() {
initPreview();
});
// Handle WebSocket messages
document.addEventListener('htmx:wsMessage', function(event) {
const message = JSON.parse(event.detail.message);
switch(message.type) {
case 'participant_joined':
console.log('Participant joined:', message.participant);
break;
case 'participant_left':
console.log('Participant left:', message.participant);
break;
case 'chat_message':
// Update chat badge
const badge = document.querySelector('.chat-badge');
if (badge && document.getElementById('chat-panel').style.display === 'none') {
const count = parseInt(badge.textContent) + 1;
badge.textContent = count;
badge.style.display = count > 0 ? 'inline' : 'none';
}
break;
case 'transcription':
// Update transcription
if (document.getElementById('transcription-panel').style.display !== 'none') {
htmx.ajax('GET', '/api/meet/transcription', {
target: '#transcription-content',
swap: 'innerHTML'
});
}
break;
}
});
</script>
{% endblock %}