Spaces:
Running
Running
| <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 ; | |
| } | |
| .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> | |