franco-upflowy's picture
bugfix
2dfe8fa verified
<!DOCTYPE html>
<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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function renderMarkerText(text) {
return text
.replace(/&lt;up-markup&gt;/g, '<span class="highlight">')
.replace(/&lt;\/up-markup&gt;/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>