Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Caption Judge β Human Label UI</title> | |
<style> | |
:root { | |
--bg: #f5f7fa; | |
--card: #ffffff; | |
--primary: #2563eb; | |
--danger: #dc2626; | |
--warn: #eab308; | |
--text: #111827; | |
--muted: #6b7280; | |
--model: #10b981; | |
--truth: #8b5cf6; | |
} | |
* { box-sizing: border-box; } | |
body { | |
margin: 0; | |
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, | |
Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; | |
background: var(--bg); | |
color: var(--text); | |
display: flex; | |
flex-direction: column; | |
min-height: 100vh; | |
} | |
header { | |
background: var(--card); | |
padding: 1rem 1.5rem; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
display: flex; | |
align-items: center; | |
gap: 1rem; | |
position: sticky; | |
top: 0; | |
z-index: 10; | |
flex-wrap: wrap; | |
} | |
header h1 { font-size: 1.25rem; margin: 0; } | |
main { | |
flex: 1; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
padding: 2rem 1rem 4rem; | |
width: 100%; | |
} | |
#card { | |
width: 100%; | |
max-width: 900px; | |
background: var(--card); | |
border-radius: 1rem; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); | |
padding: 2rem; | |
display: none; | |
} | |
.kv-pair { | |
margin-bottom: 1.25rem; | |
position: relative; | |
} | |
.kv-pair .label { | |
font-weight: 600; | |
display: flex; | |
align-items: center; | |
margin-bottom: 0.25rem; | |
} | |
.label-tag { | |
display: inline-block; | |
font-size: 0.75rem; | |
padding: 0.2rem 0.5rem; | |
border-radius: 0.25rem; | |
color: white; | |
margin-left: 0.5rem; | |
} | |
.model-tag { | |
background-color: var(--model); | |
} | |
.truth-tag { | |
background-color: var(--truth); | |
} | |
.kv-pair .value { | |
padding: 0.75rem 1rem; | |
background: #f9fafb; | |
border-radius: 0.375rem; | |
white-space: pre-wrap; | |
word-break: break-word; | |
} | |
.model-value { | |
border-left: 4px solid var(--model); | |
} | |
.truth-value { | |
border-left: 4px solid var(--truth); | |
} | |
.question-value { | |
border-left: 4px solid var(--primary); | |
} | |
.label-btn { | |
border: none; | |
padding: 0.6rem 1.5rem; | |
font-size: 1rem; | |
border-radius: 0.5rem; | |
cursor: pointer; | |
color: #fff; | |
margin-right: 0.75rem; | |
transition: transform 0.1s ease; | |
} | |
.label-btn:active { transform: scale(0.97); } | |
#trueBtn { background: var(--primary); } | |
#partialBtn { background: var(--warn); color: #000; } | |
#falseBtn { background: var(--danger); } | |
#progressWrap { | |
width: 100%; | |
max-width: 300px; | |
height: 8px; | |
background: #e5e7eb; | |
border-radius: 4px; | |
overflow: hidden; | |
} | |
#progressBar { | |
height: 100%; | |
width: 0%; | |
background: var(--primary); | |
transition: width 0.3s ease; | |
} | |
#controls { margin-top: 1.5rem; } | |
footer { | |
padding: 1rem; | |
text-align: center; | |
font-size: 0.85rem; | |
color: var(--muted); | |
} | |
#loader { display: none; } | |
#fileInput { | |
margin-right: 10px; | |
} | |
.judgement-question { | |
font-weight: 500; | |
margin-bottom: 1rem; | |
padding: 0.5rem; | |
background: #f0f4f8; | |
border-radius: 0.5rem; | |
text-align: center; | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<h1>Caption Judge β Human Label UI</h1> | |
<input type="file" id="fileInput" accept=".jsonl" /> | |
<div id="progressWrap"><div id="progressBar"></div></div> | |
<span id="counter" class="muted"></span> | |
</header> | |
<main> | |
<div id="loader">Loading datasetβ¦</div> | |
<div id="card"> | |
<div class="kv-pair"> | |
<span class="label">Question</span> | |
<div id="question" class="value question-value"></div> | |
</div> | |
<div class="kv-pair"> | |
<span class="label"> | |
Model Caption | |
<span class="label-tag model-tag">AI GENERATED</span> | |
</span> | |
<div id="generated" class="value model-value"></div> | |
</div> | |
<div class="kv-pair"> | |
<span class="label"> | |
Ground Truth | |
<span class="label-tag truth-tag">CORRECT ANSWER</span> | |
</span> | |
<div id="reference" class="value truth-value"></div> | |
</div> | |
<div class="judgement-question"> | |
Is the model caption accurate compared to the ground truth? | |
</div> | |
<div id="controls"> | |
<button id="trueBtn" class="label-btn" title="Y key">TRUE (Y)</button> | |
<button id="partialBtn" class="label-btn" title="P key">PARTIAL (P)</button> | |
<button id="falseBtn" class="label-btn" title="X key">FALSE (X)</button> | |
</div> | |
</div> | |
</main> | |
<footer> | |
Keyboard shortcuts: <strong>Y</strong> = True, <strong>P</strong> = Partial, <strong>X</strong> = False · | |
When finished a download dialog will appear with your labels. | |
</footer> | |
<script> | |
const fileInput = document.getElementById('fileInput'); | |
const loader = document.getElementById('loader'); | |
const card = document.getElementById('card'); | |
const questionEl = document.getElementById('question'); | |
const generatedEl = document.getElementById('generated'); | |
const referenceEl = document.getElementById('reference'); | |
const trueBtn = document.getElementById('trueBtn'); | |
const falseBtn = document.getElementById('falseBtn'); | |
const partialBtn = document.getElementById('partialBtn'); | |
const progressBar = document.getElementById('progressBar'); | |
const counter = document.getElementById('counter'); | |
let dataset = []; | |
let index = 0; | |
let labels = []; | |
let currentFileName = ''; | |
const updateProgress = () => { | |
counter.textContent = dataset.length ? `${index + 1}/${dataset.length}` : ''; | |
progressBar.style.width = dataset.length ? `${((index + 1) / dataset.length) * 100}%` : '0%'; | |
}; | |
const showEntry = () => { | |
if (index >= dataset.length) { | |
finish(); | |
return; | |
} | |
const entry = dataset[index]; | |
questionEl.textContent = entry.original_question || 'β'; | |
generatedEl.textContent = entry.generated_text || 'β'; | |
referenceEl.textContent = entry.ground_truth_text || 'β'; | |
updateProgress(); | |
}; | |
const label = (value) => { | |
const entry = dataset[index]; | |
labels.push({ id: entry.id, label: value }); | |
index += 1; | |
showEntry(); | |
}; | |
trueBtn.onclick = () => label('TRUE'); | |
falseBtn.onclick = () => label('FALSE'); | |
partialBtn.onclick = () => label('PARTIAL'); | |
document.addEventListener('keydown', (e) => { | |
if (!dataset.length) return; | |
if (['y', 'Y'].includes(e.key)) label('TRUE'); | |
if (['x', 'X'].includes(e.key)) label('FALSE'); | |
if (['p', 'P'].includes(e.key)) label('PARTIAL'); | |
}); | |
const finish = () => { | |
card.style.display = 'none'; | |
counter.textContent = 'β Done'; | |
progressBar.style.width = '100%'; | |
const blob = new Blob(labels.map(l => JSON.stringify(l) + '\n'), { type: 'text/plain' }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = 'labels_' + currentFileName.replace(/\.jsonl$/, '') + '.jsonl'; | |
a.click(); | |
URL.revokeObjectURL(url); | |
alert('All items labelled! A labels file has been downloaded.'); | |
}; | |
const parseJSONL = (text) => text.trim().split(/\n+/).map(line => JSON.parse(line)); | |
fileInput.addEventListener('change', async (event) => { | |
if (!event.target.files.length) return; | |
const file = event.target.files[0]; | |
if (!file.name.endsWith('.jsonl')) { | |
alert('Please select a JSONL file.'); | |
return; | |
} | |
currentFileName = file.name; | |
loader.style.display = 'block'; | |
card.style.display = 'none'; | |
try { | |
const text = await file.text(); | |
dataset = parseJSONL(text); | |
// randomize order for fairness | |
for (let i = dataset.length - 1; i > 0; i--) { | |
const j = Math.floor(Math.random() * (i + 1)); | |
[dataset[i], dataset[j]] = [dataset[j], dataset[i]]; | |
} | |
index = 0; | |
labels = []; | |
loader.style.display = 'none'; | |
card.style.display = 'block'; | |
showEntry(); | |
} catch (err) { | |
alert('Failed to load dataset: ' + err.message); | |
loader.style.display = 'none'; | |
} | |
}); | |
</script> | |
</body> | |
</html> |