misra-annotation-tool / index.html
metamyth's picture
add strict autosave on page state every character entry or change in expert_comments - so that I dont loose any comments - Follow Up Deployment
a0f762a verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code Annotation Tool</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
<style>
.code-block {
font-family: 'Courier New', monospace;
white-space: pre;
overflow-x: auto;
padding: 1rem;
border-radius: 0.375rem;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
color: #212529;
tab-size: 4;
}
.hljs {
background: transparent !important;
}
.file-input-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
}
.file-input-wrapper input[type=file] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
}
.sticky-header {
position: sticky;
top: 0;
background-color: white;
z-index: 10;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.virtual-scroll {
height: calc(100vh - 200px);
overflow-y: auto;
}
.code-block {
font-size: 0.95rem;
line-height: 1.6;
min-height: 100px;
}
.code-block .error-marker {
color: #d73a49;
font-weight: bold;
margin-bottom: 0.5rem;
display: block;
}
.virtual-scroll {
height: calc(100vh - 150px);
}
</style>
</head>
<body class="bg-gray-50">
<div class="container mx-auto px-0">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-800 mb-2">Code Annotation Tool</h1>
<p class="text-gray-600">Compare and annotate C code from CSV files</p>
</div>
<!-- Upload Section -->
<div id="upload-section" class="bg-white rounded-lg shadow-md p-6 mb-8 mx-4">
<h2 class="text-xl font-semibold mb-4">1. Upload CSV File</h2>
<div id="drop-zone" class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center mb-4 transition-colors hover:border-blue-500 hover:bg-blue-50">
<div class="flex flex-col items-center justify-center">
<i data-feather="upload" class="w-12 h-12 text-gray-400 mb-3"></i>
<p class="text-gray-600 mb-2">Drag & drop your CSV file here</p>
<p class="text-gray-400 text-sm mb-4">or</p>
<div class="file-input-wrapper">
<label class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200 flex items-center cursor-pointer">
<i data-feather="upload" class="mr-2"></i>
Choose CSV File
<input id="csv-file" type="file" accept=".csv" class="hidden">
</label>
</div>
</div>
</div>
<span id="file-name" class="ml-4 text-gray-600">No file selected</span>
<div class="mt-4 text-sm text-gray-500">
<p>Note: Large files (>100MB) are supported. Processing may take a moment.</p>
</div>
</div>
<!-- Column Selection (Hidden Initially) -->
<div id="column-selection" class="bg-white rounded-lg shadow-md p-6 mb-8 mx-4 hidden">
<h2 class="text-xl font-semibold mb-4">2. Select Columns to Compare</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Left Column</label>
<select id="left-column" class="w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Select a column</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Right Column</label>
<select id="right-column" class="w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Select a column</option>
</select>
</div>
</div>
<button id="load-data-btn" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200 flex items-center">
<i data-feather="play" class="mr-2"></i>
Load Data
</button>
</div>
<!-- Comparison Viewer (Hidden Initially) -->
<div id="comparison-viewer" class="hidden w-full">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold">Code Comparison</h2>
<div class="flex items-center">
<div class="relative">
<input type="text" id="search-input" placeholder="Search..." class="border border-gray-300 rounded-md py-2 px-3 pl-10 focus:outline-none focus:ring-2 focus:ring-blue-500">
<i data-feather="search" class="absolute left-3 top-2.5 text-gray-400"></i>
</div>
<button id="export-btn" class="ml-4 bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200 flex items-center">
<i data-feather="download" class="mr-2"></i>
Export Annotations
</button>
</div>
</div>
<div class="bg-white shadow-md overflow-hidden w-full">
<div class="sticky-header grid grid-cols-12 bg-gray-100 border-b border-gray-200 p-4 font-medium text-gray-700">
<div class="col-span-1">File Name</div>
<div class="col-span-5">Left Code</div>
<div class="col-span-4">Right Code</div>
<div class="col-span-2">Expert Comments</div>
</div>
<div id="comparison-content" class="virtual-scroll">
<!-- Content will be loaded here dynamically -->
</div>
</div>
</div>
<!-- Loading Indicator -->
<div id="loading-indicator" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-lg p-6 max-w-md w-full text-center">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mx-auto mb-4"></div>
<h3 class="text-lg font-medium text-gray-900">Processing File</h3>
<p class="mt-2 text-gray-600" id="loading-text">Reading CSV data...</p>
<div class="w-full bg-gray-200 rounded-full h-2.5 mt-4">
<div id="progress-bar" class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
feather.replace();
AOS.init();
const fileInput = document.getElementById('csv-file');
const fileNameDisplay = document.getElementById('file-name');
const columnSelection = document.getElementById('column-selection');
// Add click handler for file input label
document.querySelector('.file-input-wrapper label').addEventListener('click', function(e) {
if (e.target.tagName !== 'INPUT') {
fileInput.click();
}
});
// Drag and drop handlers
const dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-blue-500', 'bg-blue-50');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-blue-500', 'bg-blue-50');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('border-blue-500', 'bg-blue-50');
if (e.dataTransfer.files.length) {
const file = e.dataTransfer.files[0];
if (file && (file.name.endsWith('.csv') || file.type === 'text/csv')) {
handleFile(file);
} else {
alert('Please upload a valid CSV file');
}
}
});
// File upload handler
fileInput.addEventListener('change', function(e) {
if (e.target.files.length) {
const file = e.target.files[0];
if (file && (file.name.endsWith('.csv') || file.type === 'text/csv')) {
handleFile(file);
} else {
alert('Please upload a valid CSV file');
}
}
});
function handleFile(file) {
// Update the file input
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
fileInput.files = dataTransfer.files;
// Update UI
fileNameDisplay.textContent = file.name;
showLoading('Reading CSV file...');
// Process file with slight delay to ensure UI updates
setTimeout(() => {
processFile(file);
}, 100);
}
const leftColumnSelect = document.getElementById('left-column');
const rightColumnSelect = document.getElementById('right-column');
const loadDataBtn = document.getElementById('load-data-btn');
const comparisonViewer = document.getElementById('comparison-viewer');
const comparisonContent = document.getElementById('comparison-content');
const loadingIndicator = document.getElementById('loading-indicator');
const loadingText = document.getElementById('loading-text');
const progressBar = document.getElementById('progress-bar');
const searchInput = document.getElementById('search-input');
const exportBtn = document.getElementById('export-btn');
let csvData = [];
let headers = [];
let leftColumn = '';
let rightColumn = '';
let filteredData = [];
let chunkSize = 1000; // Number of rows to process at a time
let currentChunk = 0;
let totalChunks = 0;
// Handle file processing
function processFile(file) {
if (!file) {
alert('No file selected');
hideLoading();
return;
}
try {
// Use PapaParse to read the file
Papa.parse(file, {
worker: true,
header: true,
preview: 10,
skipEmptyLines: true,
step: function(results) {
// Just get headers from first row
if (!headers.length && results.meta.fields) {
headers = results.meta.fields;
populateColumnSelects();
hideLoading();
columnSelection.classList.remove('hidden');
return false; // Stop parsing after getting headers
}
},
error: function(error) {
console.error('Error reading CSV:', error);
hideLoading();
alert('Error reading CSV file: ' + error.message);
},
complete: function() {
hideLoading();
}
});
} catch (error) {
console.error('File processing error:', error);
hideLoading();
alert('Error processing file: ' + error.message);
}
}
// Populate column dropdowns
function populateColumnSelects() {
leftColumnSelect.innerHTML = '<option value="">Select a column</option>';
rightColumnSelect.innerHTML = '<option value="">Select a column</option>';
headers.forEach(header => {
const option1 = document.createElement('option');
option1.value = header;
option1.textContent = header;
leftColumnSelect.appendChild(option1);
const option2 = document.createElement('option');
option2.value = header;
option2.textContent = header;
rightColumnSelect.appendChild(option2);
});
}
// Load data button handler
loadDataBtn.addEventListener('click', function() {
leftColumn = leftColumnSelect.value;
rightColumn = rightColumnSelect.value;
if (!leftColumn || !rightColumn) {
alert('Please select both columns to compare');
return;
}
if (!fileInput.files || !fileInput.files[0]) {
alert('No file selected');
return;
}
showLoading('Loading CSV data...');
csvData = [];
filteredData = [];
currentChunk = 0;
// Read the entire file at once (PapaParse handles large files well)
const file = fileInput.files[0];
Papa.parse(file, {
worker: true,
header: true,
skipEmptyLines: true,
step: function(results) {
csvData.push(results.data);
progressBar.style.width = `${(results.meta.cursor / file.size) * 100}%`;
loadingText.textContent = `Processing file (${Math.round(results.meta.cursor / 1024)}KB)`;
},
complete: function() {
filteredData = [...csvData];
renderComparisonTable();
hideLoading();
columnSelection.classList.add('hidden');
comparisonViewer.classList.remove('hidden');
},
error: function(error) {
console.error('Error parsing file:', error);
hideLoading();
alert('Error processing file: ' + error.message);
}
});
});
// Render the comparison table with virtual scrolling
function renderComparisonTable() {
comparisonContent.innerHTML = '';
// Only render a subset of rows for performance
const rowsToRender = Math.min(100, filteredData.length);
for (let i = 0; i < rowsToRender; i++) {
const row = filteredData[i];
const rowElement = document.createElement('div');
rowElement.className = 'grid grid-cols-12 border-b border-gray-200 hover:bg-gray-50';
rowElement.innerHTML = `
<div class="col-span-1 p-4 border-r border-gray-200 overflow-auto">
<div class="text-sm font-medium text-gray-700">${row.file || row.file_name || ''}</div>
</div>
<div class="col-span-5 p-4 border-r border-gray-200 overflow-auto">
<div class="code-block">${formatCode(row[leftColumn], leftColumn, row)}</div>
</div>
<div class="col-span-4 p-4 border-r border-gray-200 overflow-auto">
<div class="code-block">${formatCode(row[rightColumn], rightColumn, row)}</div>
</div>
<div class="col-span-2 p-4">
<textarea class="w-full h-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter comments..."
data-row-index="${i}"
oninput="saveComment(this)">${row.expert_comments || ''}</textarea>
</div>
`;
comparisonContent.appendChild(rowElement);
}
// Highlight the code blocks
document.querySelectorAll('.code-block').forEach(block => {
hljs.highlightElement(block);
});
// Set up scroll event for virtual scrolling
comparisonContent.addEventListener('scroll', handleScroll);
}
// Format code with proper indentation and add special markers for specific columns
function formatCode(code, columnName, row) {
if (!code) return '';
// Replace tabs with spaces for consistent display
let formattedCode = code.replace(/\t/g, ' ');
// Add filename at the top if available
const filename = row.file || row.file_name;
if (filename) {
formattedCode = `// File: ${filename}\n\n${formattedCode}`;
}
// Add special markers for specific columns
if (columnName === 'filtered_source_code' && row.errors_before) {
formattedCode = `######\n${row.errors_before}\n######\n\n${formattedCode}`;
} else if (columnName === 'merged_code' && row.final_errors_after_pclint_corrected_code) {
formattedCode = `######\n${row.final_errors_after_pclint_corrected_code}\n######\n\n${formattedCode}`;
}
return formattedCode;
}
// Auto-save comments before page unload
window.addEventListener('beforeunload', function(e) {
// Save all comments before leaving
const textareas = comparisonContent.querySelectorAll('textarea');
textareas.forEach((textarea, index) => {
if (index < filteredData.length) {
filteredData[index].expert_comments = textarea.value;
const rowId = filteredData[index].id || filteredData[index].file;
if (rowId && csvData.length > 0) {
const mainRowIndex = csvData.findIndex(row => (row.id === rowId) || (row.file === rowId));
if (mainRowIndex !== -1) {
csvData[mainRowIndex].expert_comments = textarea.value;
}
}
}
});
});
// Handle scroll for virtual scrolling
function handleScroll() {
// Implement virtual scrolling here if needed
// For now, we're just rendering the first 100 rows
}
// Search functionality
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
if (searchTerm.length < 2) {
filteredData = [...csvData];
} else {
filteredData = csvData.filter(row => {
return (row[leftColumn] && row[leftColumn].toLowerCase().includes(searchTerm)) ||
(row[rightColumn] && row[rightColumn].toLowerCase().includes(searchTerm)) ||
(row.expert_comments && row.expert_comments.toLowerCase().includes(searchTerm));
});
}
renderComparisonTable();
});
// Save comment as user types
function saveComment(textarea) {
const rowIndex = parseInt(textarea.dataset.rowIndex);
if (rowIndex >= 0 && rowIndex < filteredData.length) {
filteredData[rowIndex].expert_comments = textarea.value;
// Also update the main data array if we can find the corresponding row
const rowId = filteredData[rowIndex].id || filteredData[rowIndex].file; // Use a unique identifier if available
if (rowId && csvData.length > 0) {
const mainRowIndex = csvData.findIndex(row => (row.id === rowId) || (row.file === rowId));
if (mainRowIndex !== -1) {
csvData[mainRowIndex].expert_comments = textarea.value;
}
}
}
}
// Export annotations
exportBtn.addEventListener('click', function() {
showLoading('Preparing export...');
// Collect all comments
const textareas = comparisonContent.querySelectorAll('textarea');
textareas.forEach((textarea, index) => {
if (index < csvData.length) {
csvData[index].expert_comments = textarea.value;
}
});
// Convert to CSV
const csv = Papa.unparse(csvData);
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
// Create download link
const a = document.createElement('a');
a.href = url;
a.download = 'annotated_code_comparison.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
hideLoading();
});
// Show loading indicator
function showLoading(text) {
loadingText.textContent = text;
loadingIndicator.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
// Hide loading indicator
function hideLoading() {
loadingIndicator.classList.add('hidden');
document.body.style.overflow = '';
}
});
</script>
</body>
</html>