data-use-annotation / app /components /AnnotationPanel.js
rafmacalaba's picture
feat: per-annotator validation support for multi-user overlap
c9986d8
"use client";
import { useState } from 'react';
const TAG_STYLES = {
named: { color: '#10b981', bg: '#10b98120', label: 'Named' },
descriptive: { color: '#f59e0b', bg: '#f59e0b20', label: 'Descriptive' },
vague: { color: '#a78bfa', bg: '#a78bfa20', label: 'Vague' },
'non-dataset': { color: '#64748b', bg: '#64748b20', label: 'Non-Dataset' },
};
const TAG_OPTIONS = ['named', 'descriptive', 'vague', 'non-dataset'];
export default function AnnotationPanel({
isOpen,
onClose,
datasets, // ALL datasets on current page (model + human)
annotatorName, // current user's name
onValidate, // (datasetIdx, updates) => void
onDelete,
}) {
const [validatingIdx, setValidatingIdx] = useState(null);
const [validationNotes, setValidationNotes] = useState('');
const [editingTagIdx, setEditingTagIdx] = useState(null);
const [editTag, setEditTag] = useState('');
const [confirmDelete, setConfirmDelete] = useState(null);
const startValidation = (idx, prefillNotes = '') => {
setValidatingIdx(idx);
setValidationNotes(prefillNotes);
};
const submitValidation = (ds, idx, verdict) => {
onValidate(ds._rawIndex ?? idx, {
human_validated: true,
human_verdict: verdict,
human_notes: validationNotes.trim() || null,
annotator: annotatorName || 'user',
validated_at: new Date().toISOString(),
});
setValidatingIdx(null);
setValidationNotes('');
};
const startEditTag = (idx, currentTag) => {
setEditingTagIdx(idx);
setEditTag(currentTag);
};
const saveEditTag = (ds, idx) => {
onValidate(ds._rawIndex ?? idx, { dataset_tag: editTag });
setEditingTagIdx(null);
setEditTag('');
};
const handleDelete = (ds, idx) => {
if (confirmDelete === idx) {
onDelete(ds, idx);
setConfirmDelete(null);
} else {
setConfirmDelete(idx);
setTimeout(() => setConfirmDelete(prev => prev === idx ? null : prev), 3000);
}
};
return (
<>
{isOpen && <div className="panel-backdrop" onClick={onClose} />}
<div className={`annotation-panel ${isOpen ? 'open' : ''}`}>
<div className="panel-header">
<h3>Data Mentions</h3>
<span className="panel-count">{datasets.length}</span>
<button className="panel-close" onClick={onClose}>&times;</button>
</div>
<div className="panel-body">
{datasets.length === 0 ? (
<div className="panel-empty">
<p>No datasets detected on this page.</p>
</div>
) : (
datasets.map((ds, i) => {
const text = ds.dataset_name?.text || '';
const tag = ds.dataset_tag || 'named';
const style = TAG_STYLES[tag] || TAG_STYLES.named;
const isHuman = !!ds.annotator;
// Per-annotator validation: look up current user's entry
const myValidation = (ds.validations || []).find(v => v.annotator === annotatorName);
const isValidated = myValidation?.human_validated === true;
const humanVerdict = myValidation?.human_verdict;
const humanNotes = myValidation?.human_notes;
const judgeVerdict = ds.dataset_name?.judge_verdict;
const judgeTag = ds.dataset_name?.judge_tag;
const isValidating = validatingIdx === i;
const isEditingTag = editingTagIdx === i;
return (
<div
key={`${text}-${ds.dataset_name?.start}-${i}`}
className={`panel-annotation-card ${isValidated ? (humanVerdict ? 'validated-correct' : 'validated-wrong') : ''}`}
>
{/* Top row: tag + source */}
<div className="panel-card-top">
{isEditingTag ? (
<div className="inline-edit">
<select
className="form-select-small"
value={editTag}
onChange={(e) => setEditTag(e.target.value)}
>
{TAG_OPTIONS.map(t => (
<option key={t} value={t}>
{TAG_STYLES[t]?.label || t}
</option>
))}
</select>
<button className="btn-panel save" onClick={() => saveEditTag(ds, i)}>βœ“</button>
<button className="btn-panel" onClick={() => setEditingTagIdx(null)}>βœ•</button>
</div>
) : (
<span
className="annotation-tag-badge clickable"
style={{ color: style.color, backgroundColor: style.bg }}
onClick={() => startEditTag(i, tag)}
title="Click to change tag"
>
{style.label}
</span>
)}
<span className="panel-card-source">
{isHuman ? `πŸ‘€ ${ds.annotator}` : 'πŸ€– model'}
</span>
</div>
{/* Dataset text */}
<p className="panel-card-text">"{text}"</p>
{/* Judge info (for model extractions) */}
{judgeTag && (
<div className="panel-card-judge">
<span className={`judge-verdict ${judgeVerdict ? 'correct' : 'wrong'}`}>
Judge: {judgeVerdict ? 'βœ“' : 'βœ•'}
</span>
<span className="judge-tag">{judgeTag}</span>
</div>
)}
{/* Position info */}
{ds.dataset_name?.start != null && (
<span className="panel-card-position">
chars {ds.dataset_name.start}–{ds.dataset_name.end}
</span>
)}
{/* Existing validation status (your own) */}
{isValidated && (
<div className={`validation-status ${humanVerdict ? 'correct' : 'wrong'}`}>
{humanVerdict ? 'βœ… Validated correct' : '❌ Marked incorrect'}
<span className="validation-by"> by you</span>
{humanNotes && (
<p className="validation-notes">Note: {humanNotes}</p>
)}
</div>
)}
{/* Validation UI */}
{isValidating ? (
<div className="validation-form">
<textarea
className="validation-notes-input"
placeholder="Optional notes..."
value={validationNotes}
onChange={(e) => setValidationNotes(e.target.value)}
rows={2}
/>
<div className="validation-buttons">
<button
className="btn-panel correct"
onClick={() => submitValidation(ds, i, true)}
>
βœ… Correct
</button>
<button
className="btn-panel wrong"
onClick={() => submitValidation(ds, i, false)}
>
❌ Wrong
</button>
<button
className="btn-panel"
onClick={() => setValidatingIdx(null)}
>
Cancel
</button>
</div>
</div>
) : (
<div className="panel-card-actions">
<button
className="btn-panel validate"
onClick={() => startValidation(i, humanNotes || '')}
>
{isValidated ? 'πŸ”„ Re-validate' : '🏷️ Validate'}
</button>
{isHuman && (
<button
className={`btn-panel delete ${confirmDelete === i ? 'confirming' : ''}`}
onClick={() => handleDelete(ds, i)}
>
{confirmDelete === i ? '⚠ Confirm?' : 'πŸ—‘ Delete'}
</button>
)}
</div>
)}
</div>
);
})
)}
</div>
</div>
</>
);
}