/** * Enhanced Upload Functionality with Device Labeling * * Features: * - Real-time .sub file parsing in browser * - Manual device identification for training data * - Device search and autocomplete * - Photo upload for unknown devices * - Batch labeling for multiple files */ // Import .sub parser // Assumes sub_parser.js is loaded first let selectedFiles = []; let parsedMetadata = new Map(); // filename -> SignalMetadata let manualLabels = new Map(); // filename -> {device_id, manufacturer, model, ...} // Device database cache (fetched from API) let deviceDatabase = []; // Initialize document.addEventListener('DOMContentLoaded', async () => { await loadDeviceDatabase(); setupEnhancedUpload(); }); /** * Load device database from API for autocomplete */ async function loadDeviceDatabase() { try { const response = await fetch('/api/v1/devices?limit=1000'); if (response.ok) { const data = await response.json(); deviceDatabase = data.devices || []; console.log(`Loaded ${deviceDatabase.length} devices for autocomplete`); } } catch (error) { console.warn('Failed to load device database:', error); } } /** * Setup enhanced upload functionality */ function setupEnhancedUpload() { const dropZone = document.getElementById('drop-zone'); const fileInput = document.getElementById('file-input'); // Drag & drop dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('drag-over'); }); dropZone.addEventListener('drop', async (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); const files = Array.from(e.dataTransfer.files).filter(f => f.name.endsWith('.sub')); await addFilesWithParsing(files); }); // File input fileInput.addEventListener('change', async (e) => { const files = Array.from(e.target.files); await addFilesWithParsing(files); }); } /** * Add files and parse them immediately */ async function addFilesWithParsing(files) { if (files.length === 0) { alert('Please select .sub files'); return; } // Show loading indicator showParsingIndicator(files.length); for (const file of files) { selectedFiles.push(file); // Parse .sub file in browser try { const content = await file.text(); const metadata = SubParser.parseSubFile(content); parsedMetadata.set(file.name, metadata); console.log(`Parsed ${file.name}:`, metadata); } catch (error) { console.error(`Failed to parse ${file.name}:`, error); } } hideParsingIndicator(); // Show file list with device labeling UI document.getElementById('file-list').style.display = 'block'; renderEnhancedFileList(); } /** * Render file list with device labeling options */ function renderEnhancedFileList() { const container = document.getElementById('files-container'); container.innerHTML = selectedFiles.map((file, index) => { const metadata = parsedMetadata.get(file.name); const label = manualLabels.get(file.name); return `
${file.name}
${formatFileSize(file.size)} ${metadata ? ` | ${(metadata.frequency / 1e6).toFixed(2)} MHz` : ''} ${metadata?.protocol ? ` | ${metadata.protocol}` : ''}
${metadata ? renderSignalDetails(metadata) : ''} ${renderDeviceLabelingUI(file.name, index, label)}
`; }).join(''); // Setup autocomplete for each file selectedFiles.forEach((file, index) => { setupDeviceAutocomplete(file.name, index); }); } /** * Render parsed signal details */ function renderSignalDetails(metadata) { return `
Frequency: ${(metadata.frequency / 1e6).toFixed(3)} MHz
Modulation: ${metadata.modulation || 'Unknown'}
${metadata.protocol ? `
Protocol: ${metadata.protocol}
` : ''} ${metadata.bit_length ? `
Bit Length: ${metadata.bit_length} bits
` : ''} ${metadata.raw_format === 'RAW' ? `
Format: RAW (${metadata.pulse_count || 0} pulses)
` : ''}
`; } /** * Render device labeling UI for a file */ function renderDeviceLabelingUI(filename, index, existingLabel) { return `

Device Identification (Optional - Helps Train AI)

Know what device this is? Help us improve identification accuracy!

Upload a photo of the physical device to help verification
`; } /** * Show/hide labeling sections based on selected option */ function showLabelingSection(index, type) { document.getElementById(`existing-device-${index}`).style.display = type === 'existing' ? 'block' : 'none'; document.getElementById(`new-device-${index}`).style.display = type === 'new' ? 'block' : 'none'; } /** * Setup device autocomplete for search input */ function setupDeviceAutocomplete(filename, index) { const searchInput = document.getElementById(`device-search-${index}`); if (!searchInput) return; const suggestionsDiv = document.getElementById(`device-suggestions-${index}`); searchInput.addEventListener('input', (e) => { const query = e.target.value.toLowerCase().trim(); if (query.length < 2) { suggestionsDiv.innerHTML = ''; return; } // Filter devices const matches = deviceDatabase.filter(device => { const searchStr = `${device.manufacturer} ${device.model} ${device.device_type}`.toLowerCase(); return searchStr.includes(query); }).slice(0, 10); // Top 10 results if (matches.length === 0) { suggestionsDiv.innerHTML = '
No devices found
'; return; } // Render suggestions suggestionsDiv.innerHTML = matches.map(device => `
${escapeHtml(device.manufacturer || 'Unknown')} ${escapeHtml(device.model || 'Unknown')}
${escapeHtml(device.device_type || '')}
`).join(''); }); // Hide suggestions when clicking outside document.addEventListener('click', (e) => { if (!searchInput.contains(e.target) && !suggestionsDiv.contains(e.target)) { suggestionsDiv.innerHTML = ''; } }); } /** * Select a device from autocomplete */ function selectDevice(index, deviceId, manufacturer, model) { const filename = selectedFiles[index].name; // Update hidden input document.getElementById(`selected-device-id-${index}`).value = deviceId; // Update search input document.getElementById(`device-search-${index}`).value = `${manufacturer} ${model}`; // Clear suggestions document.getElementById(`device-suggestions-${index}`).innerHTML = ''; // Store label manualLabels.set(filename, { type: 'existing', device_id: deviceId, manufacturer, model, search: `${manufacturer} ${model}` }); console.log(`Selected device for ${filename}:`, deviceId); } /** * Collect manual labels from all files before upload */ function collectManualLabels() { const labels = []; selectedFiles.forEach((file, index) => { const labelType = document.querySelector(`input[name="label-type-${index}"]:checked`)?.value; if (labelType === 'existing') { const deviceId = document.getElementById(`selected-device-id-${index}`).value; if (deviceId) { labels.push({ filename: file.name, device_id: parseInt(deviceId), source: 'manual_existing' }); } } else if (labelType === 'new') { const deviceType = document.getElementById(`device-type-${index}`).value; const manufacturer = document.getElementById(`manufacturer-${index}`).value.trim(); const model = document.getElementById(`model-${index}`).value.trim(); const notes = document.getElementById(`notes-${index}`).value.trim(); if (deviceType && manufacturer && model) { labels.push({ filename: file.name, device_type: deviceType, manufacturer: manufacturer, model: model, notes: notes, source: 'manual_new' }); } } // 'skip' type: no label added }); return labels; } /** * Enhanced upload function with manual labels */ async function uploadFilesEnhanced() { if (selectedFiles.length === 0) { alert('Please select files to upload'); return; } // Validate GPS const lat = parseFloat(document.getElementById('default-lat').value); const lon = parseFloat(document.getElementById('default-lon').value); if (isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) { alert('Please provide valid GPS coordinates'); return; } // Collect manual labels const manualLabels = collectManualLabels(); // Show progress document.getElementById('file-list').style.display = 'none'; document.getElementById('upload-progress').style.display = 'block'; try { // Build manifest const sessionUuid = document.getElementById('session-uuid').value || generateUUID(); const accuracy = parseFloat(document.getElementById('default-accuracy').value) || 5.0; const altitude = parseFloat(document.getElementById('default-altitude').value) || null; const manifest = { session_uuid: sessionUuid, captures: selectedFiles.map(file => ({ filename: file.name, latitude: lat, longitude: lon, accuracy: accuracy, altitude: altitude, timestamp: new Date().toISOString() })), manual_labels: manualLabels // Include manual device labels }; // Build FormData const formData = new FormData(); formData.append('manifest', JSON.stringify(manifest)); selectedFiles.forEach(file => { formData.append('files', file); }); // Upload photos (if any) for (let i = 0; i < selectedFiles.length; i++) { const photoInput = document.getElementById(`photo-${i}`); if (photoInput && photoInput.files.length > 0) { formData.append(`photo_${selectedFiles[i].name}`, photoInput.files[0]); } } // Upload updateProgress(0, 'Uploading files...'); const response = await fetch('/api/v1/captures/upload', { method: 'POST', body: formData }); updateProgress(100, 'Processing...'); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Upload failed'); } const result = await response.json(); // Show results with device identification showEnhancedUploadResults(result); } catch (error) { console.error('Upload error:', error); alert(`Upload failed: ${error.message}`); // Reset document.getElementById('upload-progress').style.display = 'none'; document.getElementById('file-list').style.display = 'block'; } } /** * Show upload results with device matches */ function showEnhancedUploadResults(result) { document.getElementById('upload-progress').style.display = 'none'; const resultsDiv = document.getElementById('upload-results'); const container = document.getElementById('results-container'); resultsDiv.style.display = 'block'; const successCount = result.successful?.length || 0; const errorCount = result.failed?.length || 0; let html = `

Upload Complete!

Successfully uploaded: ${successCount} files

${errorCount > 0 ? `

Failed: ${errorCount} files

` : ''}
`; if (result.successful && result.successful.length > 0) { html += '

Device Identification Results:

'; result.successful.forEach(item => { const bestMatch = item.matches?.[0]; // Highest confidence match html += `
${item.filename}
${(item.frequency / 1e6).toFixed(2)} MHz | ${item.protocol || 'RAW'}
${bestMatch ? `
${(bestMatch.confidence * 100).toFixed(0)}% confidence ${bestMatch.manufacturer || 'Unknown'} ${bestMatch.model || 'Unknown'} ${bestMatch.device_type || ''}
` : `
Unknown Device ${item.manual_label ? '✓ Manually labeled' : ''}
`}
`; }); } container.innerHTML = html; } /** * Get CSS class for confidence level */ function getConfidenceLevelClass(confidence) { if (confidence >= 0.85) return 'high'; if (confidence >= 0.65) return 'medium'; if (confidence >= 0.40) return 'low'; return 'very-low'; } /** * Remove file from enhanced list */ function removeFileEnhanced(index) { const filename = selectedFiles[index].name; selectedFiles.splice(index, 1); parsedMetadata.delete(filename); manualLabels.delete(filename); if (selectedFiles.length === 0) { document.getElementById('file-list').style.display = 'none'; } else { renderEnhancedFileList(); } } /** * Show parsing indicator */ function showParsingIndicator(fileCount) { const indicator = document.createElement('div'); indicator.id = 'parsing-indicator'; indicator.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 2rem; border-radius: 0.5rem; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); z-index: 10000; text-align: center; `; indicator.innerHTML = `

Parsing ${fileCount} file${fileCount > 1 ? 's' : ''}...

`; document.body.appendChild(indicator); } /** * Hide parsing indicator */ function hideParsingIndicator() { const indicator = document.getElementById('parsing-indicator'); if (indicator) indicator.remove(); } /** * Utility functions */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function updateProgress(percent, text) { document.getElementById('progress-fill').style.width = `${percent}%`; document.getElementById('progress-text').textContent = text; } function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } // Export for global access window.uploadFilesEnhanced = uploadFilesEnhanced; window.removeFileEnhanced = removeFileEnhanced; window.showLabelingSection = showLabelingSection; window.selectDevice = selectDevice;