MediaWiki:Common.js: Difference between revisions
OCR review UI: groups, progress, multipage, identity guard |
Multi-person OCR review UI for team rosters |
||
| Line 1,347: | Line 1,347: | ||
{ id: 'enlistment', label: 'Enlistment / induction' }, | { id: 'enlistment', label: 'Enlistment / induction' }, | ||
{ id: 'ritchie_cert', label: 'Camp Ritchie / MITC certificate' }, | { id: 'ritchie_cert', label: 'Camp Ritchie / MITC certificate' }, | ||
{ id: 'team_roster', label: 'IPW / team roster (special orders)' }, | |||
{ id: 'award', label: 'Award / citation' }, | { id: 'award', label: 'Award / citation' }, | ||
{ id: 'newspaper', label: 'Newspaper / clipping' }, | { id: 'newspaper', label: 'Newspaper / clipping' }, | ||
| Line 1,352: | Line 1,353: | ||
]; | ]; | ||
function renderFactRow(fact, job) { | function renderFactRow(fact, job, opts) { | ||
opts = opts || {}; | |||
var $row = $('<div>', { | var $row = $('<div>', { | ||
'class': 'ocr-fact-row' + (fact.already_on_page ? ' ocr-fact-already' : ''), | 'class': 'ocr-fact-row' + (fact.already_on_page ? ' ocr-fact-already' : ''), | ||
'data-id': fact.id | 'data-id': fact.id, | ||
'data-person': fact.person_key || '' | |||
}); | }); | ||
var $check = $('<input>', { type: 'checkbox', checked: !!fact.selected }); | var $check = $('<input>', { type: 'checkbox', checked: !!fact.selected }); | ||
| Line 1,363: | Line 1,366: | ||
type: 'text', | type: 'text', | ||
value: fact.target_title || job.bound_title || '', | value: fact.target_title || job.bound_title || '', | ||
placeholder: 'Soldier page title' | placeholder: 'Soldier page title', | ||
'class': 'ocr-fact-target' | |||
}); | }); | ||
if (job.bound_title && !isInbox) { | if (job.bound_title && !isInbox) { | ||
$tgt.prop('readonly', true); | $tgt.prop('readonly', true); | ||
} | |||
if (opts.hideTarget) { | |||
$tgt.hide(); | |||
} | } | ||
var metaBits = [(fact.kind || ''), 'conf ' + (fact.confidence || 0) + '%']; | var metaBits = [(fact.kind || ''), 'conf ' + (fact.confidence || 0) + '%']; | ||
| Line 1,374: | Line 1,381: | ||
var $meta = $('<span>', { 'class': 'ocr-fact-meta' }).text(metaBits.join(' · ')); | var $meta = $('<span>', { 'class': 'ocr-fact-meta' }).text(metaBits.join(' · ')); | ||
$row.append($check, $label, $val, $tgt, $meta); | $row.append($check, $label, $val, $tgt, $meta); | ||
if (fact.match_candidates && fact.match_candidates.length && !fact.target_title) { | |||
var $pick = $('<select>', { 'class': 'ocr-match-pick' }); | |||
$pick.append($('<option>', { value: '', text: 'Pick match…' })); | |||
fact.match_candidates.forEach(function (t) { | |||
$pick.append($('<option>', { value: t, text: t })); | |||
}); | |||
$pick.on('change', function () { | |||
var v = $pick.val(); | |||
if (v) { | |||
$tgt.val(v); | |||
/* Propagate title to all rows for this person */ | |||
var pk = fact.person_key; | |||
if (pk) { | |||
$listRef.find('.ocr-fact-row[data-person="' + pk.replace(/"/g, '') + '"] .ocr-fact-target').val(v); | |||
} | |||
} | |||
}); | |||
$row.append($pick); | |||
} | |||
return $row; | return $row; | ||
} | |||
var $listRef = null; | |||
function renderPersonGroups($list, proposed, job) { | |||
var order = []; | |||
var byPerson = {}; | |||
proposed.forEach(function (fact) { | |||
var key = fact.person_key || '_ungrouped'; | |||
if (!byPerson[key]) { | |||
byPerson[key] = []; | |||
order.push(key); | |||
} | |||
byPerson[key].push(fact); | |||
}); | |||
order.forEach(function (key) { | |||
var items = byPerson[key]; | |||
var first = items[0] || {}; | |||
var $g = $('<div>', { 'class': 'ocr-person-group' + (first.target_title ? ' ocr-person-matched' : ' ocr-person-unresolved') }); | |||
var head = first.person_label || first.target_title || 'Unresolved person'; | |||
var $h = $('<h4>').text(head); | |||
if (first.target_title) { | |||
$h.append($('<span>', { 'class': 'ocr-fact-meta' }).text(' → ' + first.target_title)); | |||
} else { | |||
$h.append($('<span>', { 'class': 'ocr-fact-meta' }).text(' — needs soldier match')); | |||
} | |||
$g.append($h); | |||
var sharedTitle = first.target_title || ''; | |||
var $tgtShared = $('<input>', { | |||
type: 'text', | |||
'class': 'ocr-person-title', | |||
value: sharedTitle, | |||
placeholder: 'Soldier page title for this person' | |||
}); | |||
$g.append($('<label>').text('Wiki page ').append($tgtShared)); | |||
$tgtShared.on('input change', function () { | |||
var v = $.trim($tgtShared.val() || ''); | |||
$g.find('.ocr-fact-target').val(v); | |||
}); | |||
items.forEach(function (fact) { | |||
var $row = renderFactRow(fact, job, { hideTarget: true }); | |||
$row.find('.ocr-fact-target').val(sharedTitle || fact.target_title || ''); | |||
$g.append($row); | |||
}); | |||
$list.append($g); | |||
}); | |||
} | } | ||
| Line 1,391: | Line 1,463: | ||
(job.error ? (' · Error: ' + job.error) : '') | (job.error ? (' · Error: ' + job.error) : '') | ||
)); | )); | ||
if (job.multi_person && job.people_summary) { | |||
var ps = job.people_summary; | |||
$panel.append($('<p>', { 'class': 'ocr-people-summary' }).text( | |||
(ps.detected || 0) + ' people · ' + | |||
(ps.matched || 0) + ' matched · ' + | |||
(ps.unresolved || 0) + ' need review · ' + | |||
(ps.already_on_page || 0) + ' already have team' + | |||
(ps.teams && ps.teams.length ? (' · teams ' + ps.teams.join(', ')) : '') | |||
)); | |||
} | |||
if (job.identity_warning) { | if (job.identity_warning) { | ||
| Line 1,454: | Line 1,537: | ||
var proposed = job.proposed || []; | var proposed = job.proposed || []; | ||
var $list = $('<div>', { 'class': 'ocr-fact-groups' }); | var $list = $('<div>', { 'class': 'ocr-fact-groups' }); | ||
groups.forEach(function (g) { | $listRef = $list; | ||
if (job.multi_person) { | |||
$facts.append($('<p>').append($('<strong>').text('People on this document'))); | |||
var $toolbar = $('<div>', { 'class': 'ocr-multi-toolbar' }); | |||
var $selMatched = $('<button>', { type: 'button', text: 'Select matched team rows' }); | |||
var $deselUnres = $('<button>', { type: 'button', text: 'Deselect unresolved' }); | |||
$toolbar.append($selMatched, $deselUnres); | |||
$facts.append($toolbar); | |||
renderPersonGroups($list, proposed, job); | |||
$selMatched.on('click', function () { | |||
$list.find('.ocr-person-matched .ocr-fact-row').each(function () { | |||
var $row = $(this); | |||
var label = ($row.find('.ocr-fact-label').text() || '').toLowerCase(); | |||
if (label.indexOf('team') !== -1 || label.indexOf('ipw') !== -1) { | |||
$row.find('input[type="checkbox"]').prop('checked', !$row.hasClass('ocr-fact-already')); | |||
} | |||
}); | |||
}); | |||
$deselUnres.on('click', function () { | |||
$list.find('.ocr-person-unresolved input[type="checkbox"]').prop('checked', false); | |||
}); | |||
} else { | |||
var groups = [ | |||
{ id: 'infobox', title: 'Infobox fields' }, | |||
{ id: 'bio', title: 'Biography' }, | |||
{ id: 'notes', title: 'Notes (review only)' } | |||
]; | |||
groups.forEach(function (g) { | |||
var items = proposed.filter(function (f) { | |||
var grp = f.group || (f.kind === 'bio' ? 'bio' : (f.kind === 'field' ? 'infobox' : 'notes')); | |||
return grp === g.id; | |||
}); | |||
if (!items.length) { | |||
return; | |||
} | |||
var $g = $('<div>', { 'class': 'ocr-fact-group' }); | |||
$g.append($('<h4>').text(g.title + ' (' + items.length + ')')); | |||
items.forEach(function (fact) { | |||
$g.append(renderFactRow(fact, job)); | |||
}); | |||
$list.append($g); | |||
}); | |||
var shown = {}; | |||
$list.find('.ocr-fact-row').each(function () { | |||
shown[$(this).attr('data-id')] = true; | |||
}); | }); | ||
proposed.forEach(function (fact) { | |||
if (!shown[fact.id]) { | |||
$list.append(renderFactRow(fact, job)); | |||
} | |||
}); | }); | ||
$facts.append($('<p>').append($('<strong>').text('Proposed facts'))); | |||
} | |||
$facts.append($list); | |||
var $overwrite = $('<label>').append( | var $overwrite = $('<label>').append( | ||
| Line 1,492: | Line 1,599: | ||
); | ); | ||
var $gallery = $('<label>').append( | var $gallery = $('<label>').append( | ||
$('<input>', { type: 'checkbox', checked: job.add_to_gallery | $('<input>', { type: 'checkbox', checked: job.add_to_gallery === true }), | ||
' Add document to Gallery on Apply' | ' Add document to Gallery on Apply' + (job.multi_person ? ' (off by default for multi-person)' : '') | ||
); | ); | ||
var $forceId = $('<label>', { 'class': 'ocr-force-identity' }).append( | var $forceId = $('<label>', { 'class': 'ocr-force-identity' }).append( | ||
| Line 1,499: | Line 1,606: | ||
' I confirmed this document matches this soldier (override identity guard)' | ' I confirmed this document matches this soldier (override identity guard)' | ||
); | ); | ||
if (job.identity_ok) { | if (job.identity_ok || job.multi_person) { | ||
$forceId.hide(); | $forceId.hide(); | ||
} | } | ||
| Line 1,514: | Line 1,621: | ||
} | } | ||
var $discard = $('<button>', { type: 'button', text: 'Discard' }); | var $discard = $('<button>', { type: 'button', text: 'Discard' }); | ||
var $apply = $('<button>', { type: 'button', text: 'Apply selected' }); | var $apply = $('<button>', { type: 'button', text: job.multi_person ? 'Apply selected to all matched' : 'Apply selected' }); | ||
var $status = $('<p>', { 'class': 'ocr-review-status' }); | var $status = $('<p>', { 'class': 'ocr-review-status' }); | ||
if (!cfg.canApply) { | if (!cfg.canApply) { | ||
| Line 1,539: | Line 1,646: | ||
} | } | ||
base.selected = $row.find('input[type="checkbox"]').prop('checked'); | base.selected = $row.find('input[type="checkbox"]').prop('checked'); | ||
base.value = $.trim($row.find('input[type="text"]'). | base.value = $.trim($row.find('input[type="text"]').not('.ocr-fact-target').not('.ocr-person-title').first().val() || ''); | ||
base.target_title = $.trim($row.find('input[type="text"]').eq(1).val() || ''); | var $t = $row.find('.ocr-fact-target'); | ||
if (!$t.length || !$t.is(':visible')) { | |||
$t = $row.closest('.ocr-person-group').find('.ocr-person-title'); | |||
} | |||
if ($t.length) { | |||
base.target_title = $.trim($t.val() || ''); | |||
} else { | |||
base.target_title = $.trim($row.find('input[type="text"]').eq(1).val() || ''); | |||
} | |||
out.push(base); | out.push(base); | ||
}); | }); | ||
| Line 1,805: | Line 1,920: | ||
$up.append($('<h3>').text('Upload a document')); | $up.append($('<h3>').text('Upload a document')); | ||
$up.append($('<p>').text( | $up.append($('<p>').text( | ||
'For one soldier or | 'For one soldier or a multi-person document (e.g. IPW team special orders). OCR groups people, matches by ASN when possible, and can Apply team membership to every matched profile in one pass.' | ||
)); | )); | ||
var $file2 = $('<input>', { type: 'file', accept: '.jpg,.jpeg,.png,.gif,.webp,.tif,.tiff,.pdf' }); | var $file2 = $('<input>', { type: 'file', accept: '.jpg,.jpeg,.png,.gif,.webp,.tif,.tiff,.pdf' }); | ||