/**
* 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 `
${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!
`;
}
/**
* 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;