Skip to main content
Glama

Smart Code Search MCP Server

media-app.js27 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, '&quot;')})"> <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;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/stevenjjobson/scs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server