950 lines
24 KiB
HTML
950 lines
24 KiB
HTML
|
|
{% 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 %}
|