| "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, |
| onMentionClick, // (text) => void |
| }) { |
| 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}>×</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 clickable-mention" |
| onClick={(e) => { |
| e.stopPropagation(); |
| onMentionClick && onMentionClick(text); |
| }} |
| title="Click to locate in text & PDF" |
| > |
| "{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> |
| </> |
| ); |
| } |
|
|