Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Pagsets Labelling</title> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background: #f5f5f5; | |
| } | |
| .title { | |
| font-size: 24px; | |
| font-weight: bold; | |
| margin-bottom: 20px; | |
| color: #1a1a1a; | |
| } | |
| .controls { | |
| margin-bottom: 20px; | |
| padding: 15px; | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .file-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .metadata-container { | |
| background: white; | |
| padding: 15px; | |
| margin: 20px 0; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .metadata-item { | |
| margin-bottom: 15px; | |
| padding-bottom: 15px; | |
| border-bottom: 1px solid #f0f0f0; | |
| } | |
| .metadata-item:last-child { | |
| border-bottom: none; | |
| margin-bottom: 0; | |
| padding-bottom: 0; | |
| } | |
| .metadata-key { | |
| font-weight: bold; | |
| margin-right: 8px; | |
| color: #2196F3; | |
| display: block; | |
| margin-bottom: 5px; | |
| font-size: 14px; | |
| text-transform: uppercase; | |
| } | |
| .metadata-value { | |
| word-break: break-word; | |
| display: block; | |
| padding: 5px; | |
| background: #f9f9f9; | |
| border-radius: 4px; | |
| } | |
| .metadata-list { | |
| margin: 5px 0 0 0; | |
| padding-left: 0; | |
| list-style-type: none; | |
| } | |
| .metadata-list li { | |
| margin-bottom: 6px; | |
| padding: 5px 10px; | |
| background: #f5f5f5; | |
| border-radius: 4px; | |
| border-left: 3px solid #2196F3; | |
| } | |
| .metadata-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| border-bottom: 1px solid #e0e0e0; | |
| padding-bottom: 10px; | |
| } | |
| .metadata-content { | |
| padding: 5px; | |
| } | |
| #contentContainer { | |
| padding-top: 10px; | |
| } | |
| .tab-list { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| overflow-x: auto; | |
| padding: 10px; | |
| background: white; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| border-bottom: 1px solid #e0e0e0; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .tab { | |
| padding: 8px 16px; | |
| border: none; | |
| background: #e0e0e0; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| white-space: nowrap; | |
| } | |
| .tab.active { | |
| background: #2196F3; | |
| color: white; | |
| } | |
| .card { | |
| background: white; | |
| padding: 20px; | |
| margin-bottom: 15px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .text-field { | |
| background: #f5f5f5; | |
| padding: 10px; | |
| margin: 5px 0; | |
| border-radius: 4px; | |
| } | |
| .label { | |
| font-weight: 600; | |
| margin-bottom: 5px; | |
| display: block; | |
| } | |
| .thumbs-down { | |
| padding: 8px; | |
| background: none; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| margin-right: 10px; | |
| } | |
| .thumbs-down.active { | |
| color: red; | |
| border-color: red; | |
| } | |
| .correction-input { | |
| width: 100%; | |
| padding: 8px; | |
| margin-top: 10px; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| box-sizing: border-box; | |
| } | |
| .button { | |
| padding: 8px 16px; | |
| background: #2196F3; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| margin-left: 10px; | |
| } | |
| .button:hover { | |
| background: #1976D2; | |
| } | |
| .reset-button { | |
| background: #dc3545; | |
| } | |
| .reset-button:hover { | |
| background: #c82333; | |
| } | |
| .correction-container { | |
| margin-top: 10px; | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| .highlight { | |
| background-color: #fff3cd; | |
| padding: 2px 4px; | |
| border-radius: 2px; | |
| } | |
| .differences { | |
| margin-top: 10px; | |
| padding: 10px; | |
| background: #e3f2fd; | |
| border-radius: 4px; | |
| font-size: 0.9em; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1 class="title">Pagsets Labelling</h1> | |
| <div class="controls"> | |
| <div class="file-controls"> | |
| <input type="file" id="fileInput" accept=".json"> | |
| </div> | |
| <div class="action-buttons"> | |
| <button id="downloadBtn" class="button">Download Updated JSON</button> | |
| <button id="resetBtn" class="button reset-button">Reset</button> | |
| </div> | |
| </div> | |
| <div id="metadataContainer" class="metadata-container"> | |
| <div class="metadata-header"> | |
| <h3 style="margin-top: 0; display: inline-block;">Metadata</h3> | |
| <button id="toggleMetadata" class="button" style="float: right; padding: 4px 8px; margin: 0;">Toggle</button> | |
| </div> | |
| <div id="metadataContent"></div> | |
| </div> | |
| <div id="contentWrapper"> | |
| <div id="tabContainer" class="tab-list"></div> | |
| <div id="contentContainer"></div> | |
| </div> | |
| <script> | |
| let data = {}; | |
| let activeTab = ''; | |
| let originalFileName = ''; | |
| // String difference visualization functions | |
| function markBlueprintTextDifferences( | |
| blueprintText, | |
| locationText, | |
| startMarker = '<up-markup>', | |
| endMarker = '</up-markup>' | |
| ) { | |
| const cleanBlueprintText = blueprintText | |
| .trim() | |
| .replace(/\s+/g, ' ') | |
| .replace('<up-markup>', '') | |
| .replace('</up-markup>', ''); | |
| const cleanLocationText = locationText | |
| .trim() | |
| .replace(/\s+/g, ' ') | |
| .replace('<up-markup>', '') | |
| .replace('</up-markup>', ''); | |
| if (cleanBlueprintText === cleanLocationText) { | |
| return cleanLocationText; | |
| } | |
| const blueprintWords = cleanBlueprintText.split(' '); | |
| const locationWords = cleanLocationText.split(' '); | |
| function findLongestCommonSubsequence(arr1, arr2) { | |
| const m = arr1.length; | |
| const n = arr2.length; | |
| const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); | |
| for (let i = 1; i <= m; i++) { | |
| for (let j = 1; j <= n; j++) { | |
| if (arr1[i - 1] === arr2[j - 1]) { | |
| dp[i][j] = dp[i - 1][j - 1] + 1; | |
| } else { | |
| dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); | |
| } | |
| } | |
| } | |
| const lcs = []; | |
| let i = m, j = n; | |
| while (i > 0 && j > 0) { | |
| if (arr1[i - 1] === arr2[j - 1]) { | |
| lcs.unshift(arr1[i - 1]); | |
| i--; | |
| j--; | |
| } else if (dp[i - 1][j] > dp[i][j - 1]) { | |
| i--; | |
| } else { | |
| j--; | |
| } | |
| } | |
| return lcs; | |
| } | |
| const commonWords = findLongestCommonSubsequence(blueprintWords, locationWords); | |
| if (commonWords.length === 0) { | |
| return `${startMarker}${locationText}${endMarker}`; | |
| } | |
| const result = []; | |
| let blueprintIndex = 0; | |
| let locationIndex = 0; | |
| let markerOpen = false; | |
| while (locationIndex < locationWords.length) { | |
| const currentWord = locationWords[locationIndex]; | |
| if (commonWords[0] === currentWord) { | |
| if (markerOpen) { | |
| result.push(endMarker); | |
| markerOpen = false; | |
| } | |
| result.push(result.length > 0 ? ' ' + currentWord : currentWord); | |
| blueprintIndex = blueprintWords.indexOf(currentWord) + 1; | |
| commonWords.shift(); | |
| locationIndex++; | |
| } else { | |
| if (!markerOpen) { | |
| result.push(result.length > 0 ? ' ' + startMarker + currentWord : startMarker + currentWord); | |
| markerOpen = true; | |
| } else { | |
| result.push(' ' + currentWord); | |
| } | |
| locationIndex++; | |
| } | |
| } | |
| if (markerOpen) { | |
| result.push(endMarker); | |
| } | |
| return result.join(''); | |
| } | |
| function escapeHtml(text) { | |
| return text | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| function renderMarkerText(text) { | |
| return text | |
| .replace(/<up-markup>/g, '<span class="highlight">') | |
| .replace(/<\/up-markup>/g, '</span>'); | |
| } | |
| document.getElementById('fileInput').addEventListener('change', handleFileUpload); | |
| document.getElementById('downloadBtn').addEventListener('click', handleDownload); | |
| document.getElementById('resetBtn').addEventListener('click', handleReset); | |
| function handleFileUpload(event) { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| originalFileName = file.name.replace('.json', ''); | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| data = JSON.parse(e.target.result); | |
| const variantIds = getVariantIds(); | |
| if (variantIds.length > 0) { | |
| activeTab = variantIds[0]; | |
| renderInterface(); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| } | |
| } | |
| function handleReset() { | |
| data = {}; | |
| activeTab = ''; | |
| originalFileName = ''; | |
| document.getElementById('fileInput').value = ''; | |
| // Clear UI elements properly | |
| const metadataContainer = document.getElementById('metadataContainer'); | |
| const metadataContent = document.getElementById('metadataContent'); | |
| const tabContainer = document.getElementById('tabContainer'); | |
| const contentContainer = document.getElementById('contentContainer'); | |
| if (metadataContainer) metadataContainer.style.display = 'none'; | |
| if (metadataContent) metadataContent.innerHTML = ''; | |
| if (tabContainer) tabContainer.innerHTML = ''; | |
| if (contentContainer) contentContainer.innerHTML = ''; | |
| } | |
| function getVariantIds() { | |
| return Object.keys(data).filter(key => | |
| !key.includes('_feedback') && | |
| !key.includes('_correct') && | |
| !key.includes('_new') && | |
| key !== 'node' && | |
| key !== 'textValue' && | |
| key !== 'variantNodeId' && | |
| key !== 'metadata' && | |
| key !== 'mapper' | |
| ); | |
| } | |
| function renderInterface() { | |
| renderMetadata(); | |
| renderTabs(); | |
| renderContent(); | |
| } | |
| function renderMetadata() { | |
| const metadataContainer = document.getElementById('metadataContainer'); | |
| const metadataContent = document.getElementById('metadataContent'); | |
| metadataContent.innerHTML = ''; | |
| if (!data.metadata) { | |
| metadataContainer.style.display = 'none'; | |
| return; | |
| } | |
| metadataContainer.style.display = 'block'; | |
| Object.entries(data.metadata).forEach(([key, value]) => { | |
| const item = document.createElement('div'); | |
| item.className = 'metadata-item'; | |
| const keySpan = document.createElement('span'); | |
| keySpan.className = 'metadata-key'; | |
| keySpan.textContent = key + ':'; | |
| item.appendChild(keySpan); | |
| // Handle different types of values | |
| if (Array.isArray(value)) { | |
| const valueSpan = document.createElement('span'); | |
| valueSpan.className = 'metadata-value'; | |
| item.appendChild(valueSpan); | |
| const list = document.createElement('ul'); | |
| list.className = 'metadata-list'; | |
| value.forEach(item => { | |
| const listItem = document.createElement('li'); | |
| listItem.textContent = item; | |
| list.appendChild(listItem); | |
| }); | |
| item.appendChild(list); | |
| } else if (typeof value === 'object' && value !== null) { | |
| const valueSpan = document.createElement('span'); | |
| valueSpan.className = 'metadata-value'; | |
| valueSpan.textContent = JSON.stringify(value, null, 2); | |
| item.appendChild(valueSpan); | |
| } else { | |
| const valueSpan = document.createElement('span'); | |
| valueSpan.className = 'metadata-value'; | |
| valueSpan.textContent = value; | |
| item.appendChild(valueSpan); | |
| } | |
| metadataContent.appendChild(item); | |
| }); | |
| // Initialize toggle functionality | |
| const toggleBtn = document.getElementById('toggleMetadata'); | |
| toggleBtn.textContent = 'Hide'; | |
| toggleBtn.onclick = toggleMetadataVisibility; | |
| } | |
| function toggleMetadataVisibility() { | |
| const content = document.getElementById('metadataContent'); | |
| const toggleBtn = document.getElementById('toggleMetadata'); | |
| if (content.style.display === 'none') { | |
| content.style.display = 'block'; | |
| toggleBtn.textContent = 'Hide'; | |
| } else { | |
| content.style.display = 'none'; | |
| toggleBtn.textContent = 'Show'; | |
| } | |
| } | |
| function renderTabs() { | |
| const tabContainer = document.getElementById('tabContainer'); | |
| tabContainer.innerHTML = ''; | |
| getVariantIds().forEach(variantId => { | |
| const tab = document.createElement('button'); | |
| tab.className = `tab ${activeTab === variantId ? 'active' : ''}`; | |
| // Use the mapper to get the variant name, fallback to ID if not found | |
| const variantName = data.mapper?.[variantId] || variantId; | |
| tab.textContent = variantName; | |
| tab.onclick = () => { | |
| activeTab = variantId; | |
| renderInterface(); | |
| }; | |
| tabContainer.appendChild(tab); | |
| }); | |
| } | |
| function renderContent() { | |
| const contentContainer = document.getElementById('contentContainer'); | |
| contentContainer.innerHTML = ''; | |
| if (!activeTab) return; | |
| Object.keys(data.textValue || {}).forEach(rowIndex => { | |
| const card = document.createElement('div'); | |
| card.className = 'card'; | |
| // Original text | |
| const originalText = document.createElement('div'); | |
| originalText.innerHTML = ` | |
| <span class="label">Original Text:</span> | |
| <div class="text-field">${data.textValue[rowIndex]}</div> | |
| `; | |
| // Variant text with differences | |
| const variantText = document.createElement('div'); | |
| const markedDifferences = markBlueprintTextDifferences( | |
| data.textValue[rowIndex], | |
| data[activeTab][rowIndex] | |
| ); | |
| const escapedDifferences = escapeHtml(markedDifferences); | |
| const renderedDifferences = renderMarkerText(escapedDifferences); | |
| variantText.innerHTML = ` | |
| <span class="label">Variant Text:</span> | |
| <div class="text-field">${renderedDifferences}</div> | |
| `; | |
| // New text with differences | |
| const newText = document.createElement('div'); | |
| const newTextContent = data[`${activeTab}_new`]?.[rowIndex]; | |
| if (newTextContent) { | |
| const newMarkedDifferences = markBlueprintTextDifferences( | |
| data.textValue[rowIndex], | |
| newTextContent | |
| ); | |
| const newEscapedDifferences = escapeHtml(newMarkedDifferences); | |
| const newRenderedDifferences = renderMarkerText(newEscapedDifferences); | |
| newText.innerHTML = ` | |
| <span class="label">New Text:</span> | |
| <div class="text-field">${newRenderedDifferences}</div> | |
| `; | |
| } | |
| // Feedback section | |
| const feedbackSection = document.createElement('div'); | |
| const thumbsDown = document.createElement('button'); | |
| thumbsDown.className = `thumbs-down ${data[`${activeTab}_feedback`]?.[rowIndex] === false ? 'active' : ''}`; | |
| thumbsDown.innerHTML = '👎'; | |
| thumbsDown.onclick = () => handleThumbsDown(rowIndex); | |
| // Correction input | |
| const correctionContainer = document.createElement('div'); | |
| correctionContainer.className = `correction-container ${data[`${activeTab}_feedback`]?.[rowIndex] === false ? '' : 'hidden'}`; | |
| correctionContainer.innerHTML = ` | |
| <span class="label">Correction:</span> | |
| <textarea | |
| class="correction-input" | |
| rows="3" | |
| style="resize: vertical;" | |
| placeholder="Enter correction here" | |
| >${data[`${activeTab}_correct`]?.[rowIndex] || ''}</textarea> | |
| `; | |
| // Add event listener to correction input | |
| const correctionInput = correctionContainer.querySelector('textarea'); | |
| correctionInput.onchange = (e) => handleCorrection(rowIndex, e.target.value); | |
| feedbackSection.appendChild(thumbsDown); | |
| feedbackSection.appendChild(correctionContainer); | |
| // Append all elements to card | |
| card.appendChild(originalText); | |
| card.appendChild(variantText); | |
| if (newText.innerHTML) { | |
| card.appendChild(newText); | |
| } | |
| card.appendChild(feedbackSection); | |
| contentContainer.appendChild(card); | |
| }); | |
| } | |
| function handleThumbsDown(rowIndex) { | |
| if (!data[`${activeTab}_feedback`]) { | |
| data[`${activeTab}_feedback`] = {}; | |
| } | |
| data[`${activeTab}_feedback`][rowIndex] = !data[`${activeTab}_feedback`][rowIndex]; | |
| if (!data[`${activeTab}_correct`]) { | |
| data[`${activeTab}_correct`] = {}; | |
| } | |
| if (!data[`${activeTab}_feedback`][rowIndex]) { | |
| data[`${activeTab}_correct`][rowIndex] = data[activeTab][rowIndex]; | |
| } | |
| renderContent(); | |
| } | |
| function handleCorrection(rowIndex, value) { | |
| if (!data[`${activeTab}_correct`]) { | |
| data[`${activeTab}_correct`] = {}; | |
| } | |
| data[`${activeTab}_correct`][rowIndex] = value; | |
| } | |
| function handleDownload() { | |
| if (!originalFileName) return; | |
| const jsonString = JSON.stringify(data, null, 2); | |
| const blob = new Blob([jsonString], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `${originalFileName}_labelled.json`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| </script> | |
| </body> | |
| </html> |