media-app.js•27 kB
/**
* Media App Client JavaScript
* Handles UI interactions and WebSocket communication for media history
*/
// WebSocket connection
let ws = null;
let reconnectTimer = null;
let isRecording = false;
let currentView = 'gallery';
let currentViewMode = 'grid';
let mediaItems = [];
let sessions = [];
let analytics = {};
// Voice recognition
let recognition = null;
let isListening = false;
let voiceMode = 'push'; // push, continuous, wake
// Annotation tools
let annotationTool = 'pen';
let annotationCanvas = null;
let annotationContext = null;
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
initializeWebSocket();
initializeVoiceRecognition();
initializeUI();
loadMediaHistory();
});
// WebSocket initialization
function initializeWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.hostname}:3001`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected');
updateConnectionStatus('scsStatus', true);
// Request initial data
ws.send(JSON.stringify({
type: 'request_media_history',
filters: {}
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleWebSocketMessage(message);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
updateConnectionStatus('scsStatus', false);
};
ws.onclose = () => {
console.log('WebSocket disconnected');
updateConnectionStatus('scsStatus', false);
// Attempt reconnection
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
initializeWebSocket();
}, 3000);
};
}
// Handle WebSocket messages
function handleWebSocketMessage(message) {
switch (message.type) {
case 'status':
updateSystemStatus(message.data);
break;
case 'media_history':
updateMediaHistory(message.data);
break;
case 'new_media':
addNewMedia(message.data);
break;
case 'screenshot_saved':
showNotification('Screenshot saved', 'success');
refreshMediaGrid();
break;
case 'recording_started':
updateRecordingStatus(true);
break;
case 'recording_stopped':
updateRecordingStatus(false);
showNotification('Recording saved', 'success');
refreshMediaGrid();
break;
case 'browser_capture':
addBrowserCapture(message.data);
break;
case 'context_update':
updateEditorContext(message.data);
break;
case 'response':
handleVoiceResponse(message.data);
break;
case 'audio':
playAudioResponse(message.data);
break;
case 'error':
showNotification(message.error, 'error');
break;
}
}
// Voice recognition initialization
function initializeVoiceRecognition() {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
console.log('Speech recognition not supported');
updateConnectionStatus('voiceStatus', false);
return;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SpeechRecognition();
recognition.lang = 'en-US';
recognition.continuous = false;
recognition.interimResults = true;
recognition.maxAlternatives = 1;
recognition.onstart = () => {
isListening = true;
document.getElementById('micButton').classList.add('listening');
document.getElementById('transcript').textContent = 'Listening...';
updateConnectionStatus('voiceStatus', true);
};
recognition.onresult = (event) => {
const transcript = Array.from(event.results)
.map(result => result[0].transcript)
.join('');
document.getElementById('transcript').textContent = transcript;
if (event.results[0].isFinal) {
sendVoiceCommand(transcript);
}
};
recognition.onerror = (event) => {
console.error('Speech recognition error:', event.error);
stopListening();
if (event.error === 'no-speech') {
document.getElementById('transcript').textContent = 'No speech detected. Try again.';
} else {
document.getElementById('transcript').textContent = `Error: ${event.error}`;
}
};
recognition.onend = () => {
stopListening();
// Restart if in continuous mode
if (voiceMode === 'continuous' && isListening) {
setTimeout(() => startListening(), 100);
}
};
}
// UI initialization
function initializeUI() {
// Load saved preferences
loadPreferences();
// Initialize date filter to today
const today = new Date().toISOString().split('T')[0];
document.getElementById('filterDate').value = today;
// Initialize analytics chart
initializeAnalyticsChart();
// Check browser MCP availability
checkBrowserMCP();
// Initialize storage display
updateStorageStatus();
}
// Voice control functions
function toggleVoice() {
if (isListening) {
stopListening();
} else {
startListening();
}
}
function startListening() {
if (!recognition) return;
try {
recognition.start();
isListening = true;
} catch (error) {
console.error('Failed to start recognition:', error);
stopListening();
}
}
function stopListening() {
if (!recognition) return;
try {
recognition.stop();
isListening = false;
document.getElementById('micButton').classList.remove('listening');
updateConnectionStatus('voiceStatus', false);
} catch (error) {
console.error('Failed to stop recognition:', error);
}
}
function setMode(mode) {
voiceMode = mode;
// Update UI
document.querySelectorAll('.voice-controls .control-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Handle mode change
if (mode === 'continuous' && !isListening) {
startListening();
} else if (mode === 'push' && isListening) {
stopListening();
}
// Save preference
localStorage.setItem('voiceMode', mode);
}
function sendVoiceCommand(text) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
showNotification('Not connected to server', 'error');
return;
}
ws.send(JSON.stringify({
type: 'voice',
text: text,
context: {
view: currentView,
mediaCount: mediaItems.length
}
}));
}
function voiceCommand(command) {
sendVoiceCommand(command);
}
// Media actions
async function takeScreenshot() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
showNotification('Not connected to server', 'error');
return;
}
const btn = document.getElementById('screenshotBtn');
btn.disabled = true;
// Check if we should use browser MCP
const useBrowserCapture = await checkBrowserMCP();
ws.send(JSON.stringify({
type: 'screenshot',
method: useBrowserCapture ? 'browser' : 'desktop',
context: getCurrentContext()
}));
setTimeout(() => {
btn.disabled = false;
}, 2000);
}
function toggleRecording() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
showNotification('Not connected to server', 'error');
return;
}
if (isRecording) {
ws.send(JSON.stringify({
type: 'stop_recording'
}));
} else {
ws.send(JSON.stringify({
type: 'start_recording',
context: getCurrentContext()
}));
}
}
function updateRecordingStatus(recording) {
isRecording = recording;
const btn = document.getElementById('recordBtn');
if (recording) {
btn.classList.add('recording');
btn.innerHTML = '⏹️ Stop';
} else {
btn.classList.remove('recording');
btn.innerHTML = '🔴 Record';
}
}
async function exportMedia() {
const filters = getActiveFilters();
const response = await fetch('/api/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filters })
});
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `media-export-${Date.now()}.zip`;
a.click();
URL.revokeObjectURL(url);
} else {
showNotification('Export failed', 'error');
}
}
// View management
function switchTab(tab) {
currentView = tab;
// Update tab buttons
document.querySelectorAll('.media-tabs .tab-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Hide all views
document.querySelectorAll('.view-content').forEach(view => {
view.style.display = 'none';
});
// Show selected view
document.getElementById(`${tab}View`).style.display = 'block';
// Load view-specific data
switch (tab) {
case 'gallery':
refreshMediaGrid();
break;
case 'timeline':
loadTimeline();
break;
case 'sessions':
loadSessions();
break;
case 'analytics':
loadAnalytics();
break;
}
}
function setViewMode(mode) {
currentViewMode = mode;
// Update buttons
document.querySelectorAll('.view-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Update grid display
const grid = document.getElementById('mediaGrid');
if (mode === 'list') {
grid.style.gridTemplateColumns = '1fr';
} else {
grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(250px, 1fr))';
}
}
// Media grid management
function refreshMediaGrid() {
const filters = getActiveFilters();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'request_media_history',
filters: filters
}));
}
}
function updateMediaHistory(data) {
mediaItems = data.items || [];
sessions = data.sessions || [];
analytics = data.analytics || {};
renderMediaGrid();
updateAnalytics();
}
function renderMediaGrid() {
const grid = document.getElementById('mediaGrid');
grid.innerHTML = '';
if (mediaItems.length === 0) {
grid.innerHTML = '<div style="text-align: center; padding: 2rem; color: #6b7280;">No media items found</div>';
return;
}
mediaItems.forEach(item => {
const element = createMediaElement(item);
grid.appendChild(element);
});
}
function createMediaElement(item) {
const div = document.createElement('div');
div.className = 'media-item';
div.onclick = () => openMediaViewer(item);
const thumbnail = item.thumbnail || item.path;
const typeClass = item.type.toLowerCase();
div.innerHTML = `
<img src="${thumbnail}" alt="${item.title}" class="media-thumbnail"
onerror="this.src='/placeholder.png'">
<div class="media-info">
<div class="media-title">${item.title || 'Untitled'}</div>
<div class="media-meta">
<span class="media-type ${typeClass}">${item.type}</span>
<span>${formatDate(item.timestamp)}</span>
</div>
${item.annotations ? '<span>📝</span>' : ''}
</div>
`;
return div;
}
function addNewMedia(item) {
mediaItems.unshift(item);
// Update grid if in gallery view
if (currentView === 'gallery') {
const grid = document.getElementById('mediaGrid');
const element = createMediaElement(item);
grid.insertBefore(element, grid.firstChild);
}
// Update counts
updateAnalytics();
}
// Timeline view
function loadTimeline() {
const timeline = document.getElementById('mediaTimeline');
timeline.innerHTML = '<div class="timeline-line"></div>';
// Group items by time
const grouped = groupByTime(mediaItems);
Object.entries(grouped).forEach(([time, items]) => {
const group = createTimelineGroup(time, items);
timeline.appendChild(group);
});
}
function createTimelineGroup(time, items) {
const div = document.createElement('div');
div.className = 'timeline-item';
div.innerHTML = `
<div class="timeline-dot"></div>
<div class="timeline-content">
<div class="timeline-time">${time}</div>
${items.map(item => `
<div style="margin: 0.5rem 0; cursor: pointer;"
onclick="openMediaViewer(${JSON.stringify(item).replace(/"/g, '"')})">
<strong>${item.title || 'Untitled'}</strong>
<span class="media-type ${item.type.toLowerCase()}">${item.type}</span>
</div>
`).join('')}
</div>
`;
return div;
}
// Sessions view
function loadSessions() {
const container = document.getElementById('sessionsList');
container.innerHTML = '';
if (sessions.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 2rem; color: #6b7280;">No sessions found</div>';
return;
}
sessions.forEach(session => {
const element = createSessionElement(session);
container.appendChild(element);
});
}
function createSessionElement(session) {
const div = document.createElement('div');
div.className = 'session-group';
div.innerHTML = `
<div class="session-header" onclick="toggleSession('${session.id}')">
<div class="session-title">${session.name}</div>
<div class="session-stats">
<span>📸 ${session.screenshots}</span>
<span>🎬 ${session.recordings}</span>
<span>⏱️ ${session.duration}</span>
</div>
</div>
<div class="session-content" id="session-${session.id}" style="display: none;">
<!-- Session items will be loaded here -->
</div>
`;
return div;
}
function toggleSession(sessionId) {
const content = document.getElementById(`session-${sessionId}`);
content.style.display = content.style.display === 'none' ? 'block' : 'none';
if (content.style.display === 'block' && !content.innerHTML) {
loadSessionContent(sessionId);
}
}
// Analytics view
function loadAnalytics() {
updateAnalyticsStats();
updateAnalyticsChart();
}
function updateAnalytics() {
if (currentView === 'analytics') {
updateAnalyticsStats();
updateAnalyticsChart();
}
// Update header storage status
updateStorageStatus();
}
function updateAnalyticsStats() {
document.getElementById('totalMedia').textContent = mediaItems.length;
document.getElementById('todayCaptures').textContent = getTodayCount();
document.getElementById('storageUsed').textContent = formatStorage(analytics.storageUsed || 0);
document.getElementById('avgDuration').textContent = formatDuration(analytics.avgDuration || 0);
}
function initializeAnalyticsChart() {
const canvas = document.getElementById('analyticsChart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
// Simple chart drawing (would use Chart.js in production)
ctx.fillStyle = '#6366f1';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function updateAnalyticsChart() {
// Update chart with new data
initializeAnalyticsChart();
}
// Filters
function applyFilters() {
refreshMediaGrid();
}
function getActiveFilters() {
return {
type: document.getElementById('filterType').value,
project: document.getElementById('filterProject').value,
date: document.getElementById('filterDate').value,
search: document.getElementById('filterSearch').value
};
}
// Media viewer modal
function openMediaViewer(item) {
const modal = document.getElementById('mediaModal');
const content = document.getElementById('modalContent');
if (item.type === 'screenshot' || item.type === 'browser') {
content.innerHTML = `
<img src="${item.path}" style="max-width: 100%; height: auto;">
<div style="margin-top: 1rem;">
<h3>${item.title || 'Untitled'}</h3>
<p>${item.description || ''}</p>
<p style="color: #6b7280; font-size: 0.875rem;">
${formatDate(item.timestamp)} • ${item.type}
</p>
${item.annotations ? `<pre>${JSON.stringify(item.annotations, null, 2)}</pre>` : ''}
</div>
`;
} else if (item.type === 'recording') {
content.innerHTML = `
<video src="${item.path}" controls style="max-width: 100%; height: auto;"></video>
<div style="margin-top: 1rem;">
<h3>${item.title || 'Untitled'}</h3>
<p>${item.description || ''}</p>
<p style="color: #6b7280; font-size: 0.875rem;">
${formatDate(item.timestamp)} • Duration: ${formatDuration(item.duration)}
</p>
</div>
`;
}
modal.classList.add('active');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
// Annotation modal
function openAnnotationModal() {
const modal = document.getElementById('annotationModal');
modal.classList.add('active');
// Initialize canvas
initializeAnnotationCanvas();
}
function initializeAnnotationCanvas() {
const container = document.getElementById('annotationCanvas');
// Load last media item for annotation
if (mediaItems.length > 0) {
const lastItem = mediaItems[0];
container.innerHTML = `
<img src="${lastItem.path}" style="max-width: 100%; height: auto;" id="annotationImage">
<canvas id="annotationOverlay" style="position: absolute; top: 0; left: 0;"></canvas>
`;
// Setup canvas
const img = document.getElementById('annotationImage');
img.onload = () => {
const canvas = document.getElementById('annotationOverlay');
canvas.width = img.width;
canvas.height = img.height;
annotationCanvas = canvas;
annotationContext = canvas.getContext('2d');
};
}
}
function setAnnotationTool(tool) {
annotationTool = tool;
// Update UI
document.querySelectorAll('.annotation-tool').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
}
function clearAnnotations() {
if (annotationContext) {
annotationContext.clearRect(0, 0, annotationCanvas.width, annotationCanvas.height);
}
}
function saveAnnotations() {
if (!annotationCanvas) return;
// Get annotation data
const dataUrl = annotationCanvas.toDataURL();
// Send to server
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'save_annotations',
mediaId: mediaItems[0].id,
annotations: dataUrl
}));
}
closeModal('annotationModal');
showNotification('Annotations saved', 'success');
}
// Helper functions
function updateConnectionStatus(elementId, connected) {
const element = document.getElementById(elementId);
if (element) {
if (connected) {
element.classList.add('connected');
} else {
element.classList.remove('connected');
}
}
}
function updateSystemStatus(status) {
updateConnectionStatus('scsStatus', status.scsMcp);
updateConnectionStatus('browserStatus', status.browserMcp);
updateConnectionStatus('voiceStatus', status.voice);
}
function updateStorageStatus() {
const used = analytics.storageUsed || 0;
const total = 5 * 1024 * 1024 * 1024; // 5GB
const percentage = (used / total * 100).toFixed(1);
document.getElementById('storageStatus').textContent =
`${formatStorage(used)} / 5 GB (${percentage}%)`;
}
function updateEditorContext(context) {
document.getElementById('currentFile').textContent =
context.currentFile ? context.currentFile.split('/').pop() : 'Not connected';
document.getElementById('currentLine').textContent = context.currentLine || '-';
document.getElementById('currentSymbol').textContent = context.currentSymbol || '-';
}
function handleVoiceResponse(data) {
const { text, code } = data;
// Update transcript
document.getElementById('transcript').textContent = text;
// Show notification
showNotification(text, 'info');
// If code is returned, offer to view it
if (code) {
// Could open a modal or update a code panel
console.log('Code response:', code);
}
}
function playAudioResponse(audioData) {
const audio = new Audio('data:audio/mpeg;base64,' + audioData);
audio.play();
}
async function checkBrowserMCP() {
try {
const response = await fetch('/api/browser-status');
const data = await response.json();
updateConnectionStatus('browserStatus', data.connected);
return data.connected;
} catch (error) {
updateConnectionStatus('browserStatus', false);
return false;
}
}
function getCurrentContext() {
return {
view: currentView,
project: document.getElementById('filterProject').value,
timestamp: Date.now()
};
}
function groupByTime(items) {
const grouped = {};
items.forEach(item => {
const date = new Date(item.timestamp);
const time = date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
if (!grouped[time]) {
grouped[time] = [];
}
grouped[time].push(item);
});
return grouped;
}
function getTodayCount() {
const today = new Date().toDateString();
return mediaItems.filter(item =>
new Date(item.timestamp).toDateString() === today
).length;
}
function formatDate(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
}
function formatDuration(seconds) {
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
return `${Math.floor(seconds / 3600)}h`;
}
function formatStorage(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`;
return `${(bytes / 1073741824).toFixed(2)} GB`;
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
background: ${type === 'error' ? '#ef4444' : type === 'success' ? '#10b981' : '#6366f1'};
color: white;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10000;
animation: slideIn 0.3s ease;
`;
notification.textContent = message;
document.body.appendChild(notification);
// Remove after 3 seconds
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
function loadPreferences() {
// Load saved preferences from localStorage
const savedMode = localStorage.getItem('voiceMode');
if (savedMode) {
voiceMode = savedMode;
}
const savedView = localStorage.getItem('defaultView');
if (savedView) {
currentView = savedView;
}
}
function loadSessionContent(sessionId) {
// Load session-specific media items
const sessionItems = mediaItems.filter(item => item.sessionId === sessionId);
const content = document.getElementById(`session-${sessionId}`);
content.innerHTML = sessionItems.map(item => `
<div style="padding: 0.5rem; border-left: 2px solid #e5e7eb; margin-left: 1rem;">
<strong>${item.title || 'Untitled'}</strong>
<span class="media-type ${item.type.toLowerCase()}">${item.type}</span>
<span style="color: #6b7280; font-size: 0.875rem;">${formatDate(item.timestamp)}</span>
</div>
`).join('');
}
// Add CSS animations
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
// Export functions for use in HTML
window.toggleVoice = toggleVoice;
window.setMode = setMode;
window.voiceCommand = voiceCommand;
window.takeScreenshot = takeScreenshot;
window.toggleRecording = toggleRecording;
window.exportMedia = exportMedia;
window.switchTab = switchTab;
window.setViewMode = setViewMode;
window.applyFilters = applyFilters;
window.openMediaViewer = openMediaViewer;
window.closeModal = closeModal;
window.openAnnotationModal = openAnnotationModal;
window.setAnnotationTool = setAnnotationTool;
window.clearAnnotations = clearAnnotations;
window.saveAnnotations = saveAnnotations;
window.scrollToTop = scrollToTop;
window.toggleSession = toggleSession;