MediaWiki:Common.js: Difference between revisions
OCR multi-person: Select all matched + one match picker per person |
OCR review UX: person cards, apply preview, Advanced |
||
| Line 1,354: | Line 1,354: | ||
]; | ]; | ||
function | function changeLineForFact(fact) { | ||
var field = fact.field || ''; | |||
var | var val = (fact.value || '').trim(); | ||
var label = (fact.label || '').toLowerCase(); | |||
if (fact.already_on_page) { | |||
return (fact.label || 'Already on page') + ' — skipped'; | |||
} | } | ||
if (field === 'ipw_ge') { | |||
return 'Team: set IPW ' + val; | |||
} | |||
if (field === 'teams') { | |||
return 'Team: add ' + val + ' to teams'; | |||
} | |||
if (field === 'asn') { | |||
' | return 'ASN: ' + val; | ||
} | } | ||
if ( | if (field === 'rank_final') { | ||
return 'Rank: ' + val; | |||
} | |||
if (field === 'specialty') { | |||
return 'Role: ' + val; | |||
} | |||
if (field === 'class_number') { | |||
return 'Class: ' + val; | |||
} | } | ||
if ( | if (fact.kind === 'bio') { | ||
return 'Biography: ' + (val.length > 80 ? val.slice(0, 80) + '…' : val); | |||
} | } | ||
if (fact.kind === 'note') { | |||
return (fact.label || 'Note') + ': ' + (val.length > 90 ? val.slice(0, 90) + '…' : val); | |||
} | } | ||
if (label.indexOf('team') !== -1 || label.indexOf('ipw') !== -1) { | |||
return fact.label + (val ? ': ' + val : ''); | |||
} | } | ||
return | return (fact.label || field || fact.kind) + (val ? ': ' + val : ''); | ||
} | |||
function personDisplayName(fact) { | |||
var lab = fact.person_label || fact.target_title || 'Person'; | |||
return lab.split('·')[0].trim().split('(')[0].trim() || lab; | |||
} | } | ||
var | function personMetaBits(items) { | ||
var bits = []; | |||
items.forEach(function (f) { | |||
if (f.field === 'asn' && f.value && bits.indexOf('ASN ' + f.value) < 0) { | |||
bits.push('ASN ' + f.value); | |||
} | |||
if (f.field === 'rank_final' && f.value && bits.indexOf(f.value) < 0) { | |||
bits.push(f.value); | |||
} | |||
if (f.field === 'specialty' && f.value && bits.indexOf(f.value) < 0) { | |||
bits.push(f.value); | |||
} | |||
if ((f.field === 'ipw_ge' || f.field === 'teams') && f.value) { | |||
var t = f.value.indexOf('IPW') === 0 ? f.value : ('IPW ' + f.value); | |||
if (bits.indexOf(t) < 0) { | |||
bits.push(t); | |||
} | |||
} | |||
}); | |||
return bits; | |||
} | |||
function | function groupFactsByPerson(proposed) { | ||
var order = []; | var order = []; | ||
var byPerson = {}; | var byPerson = {}; | ||
proposed.forEach(function (fact) { | proposed.forEach(function (fact) { | ||
var key = fact.person_key || ' | var key = fact.person_key || (fact.target_title ? 'title:' + fact.target_title : '_single'); | ||
if (!byPerson[key]) { | if (!byPerson[key]) { | ||
byPerson[key] = []; | byPerson[key] = []; | ||
| Line 1,417: | Line 1,429: | ||
byPerson[key].push(fact); | byPerson[key].push(fact); | ||
}); | }); | ||
order.forEach(function (key) { | return { order: order, byPerson: byPerson }; | ||
var items = byPerson[key]; | } | ||
function isWritableFact(fact) { | |||
return fact.kind === 'field' || fact.kind === 'bio'; | |||
} | |||
function bindSoldierTypeahead($input, $suggest, onPick) { | |||
var timer = null; | |||
$input.on('input', function () { | |||
var q = $.trim($input.val() || ''); | |||
window.clearTimeout(timer); | |||
$suggest.empty().hide(); | |||
if (q.length < 2) { | |||
return; | |||
} | |||
timer = window.setTimeout(function () { | |||
api().get({ | |||
action: 'opensearch', | |||
search: q, | |||
namespace: 0, | |||
limit: 8, | |||
redirects: 'resolve' | |||
}).then(function (data) { | |||
var titles = (data && data[1]) || []; | |||
$suggest.empty(); | |||
if (!titles.length) { | |||
$suggest.hide(); | |||
return; | |||
} | |||
titles.forEach(function (t) { | |||
var $opt = $('<button>', { type: 'button', 'class': 'ocr-suggest-item', text: t }); | |||
$opt.on('click', function () { | |||
onPick(t); | |||
$suggest.empty().hide(); | |||
}); | |||
$suggest.append($opt); | |||
}); | |||
$suggest.show(); | |||
}); | |||
}, 220); | |||
}); | |||
} | |||
function renderChangeRow(fact, job, opts) { | |||
opts = opts || {}; | |||
var writable = isWritableFact(fact) && !fact.already_on_page; | |||
var $row = $('<div>', { | |||
'class': 'ocr-fact-row' + | |||
(fact.already_on_page ? ' ocr-fact-already' : '') + | |||
(writable ? ' ocr-fact-writable' : ' ocr-fact-note') + | |||
(fact.kind === 'note' ? ' ocr-fact-is-note' : ''), | |||
'data-id': fact.id, | |||
'data-person': fact.person_key || '', | |||
'data-field': fact.field || '', | |||
'data-kind': fact.kind || '' | |||
}); | |||
var $check = $('<input>', { | |||
type: 'checkbox', | |||
checked: !!fact.selected && writable, | |||
disabled: !writable | |||
}); | |||
var $line = $('<span>', { 'class': 'ocr-change-line' }).text(changeLineForFact(fact)); | |||
var $val = $('<input>', { | |||
type: 'text', | |||
'class': 'ocr-fact-value', | |||
value: fact.value || '' | |||
}); | |||
var $tgt = $('<input>', { | |||
type: 'hidden', | |||
'class': 'ocr-fact-target', | |||
value: fact.target_title || job.bound_title || '' | |||
}); | |||
var $details = $('<details>', { 'class': 'ocr-fact-details' }); | |||
$details.append($('<summary>').text('Edit')); | |||
$details.append($('<label>').text('Value ').append($val)); | |||
if (!opts.hideTargetVisible) { | |||
var $tgtVis = $('<input>', { | |||
type: 'text', | |||
'class': 'ocr-fact-target-vis', | |||
value: fact.target_title || job.bound_title || '', | |||
placeholder: 'Soldier page title' | |||
}); | |||
if (job.bound_title && !isInbox) { | |||
$tgtVis.prop('readonly', true); | |||
} | |||
$tgtVis.on('input change', function () { | |||
$tgt.val($.trim($tgtVis.val() || '')); | |||
}); | |||
$details.append($('<label>').text('Page ').append($tgtVis)); | |||
} | |||
$details.append($('<span>', { 'class': 'ocr-fact-meta' }).text( | |||
(fact.kind || '') + ' · conf ' + (fact.confidence || 0) + '%' | |||
)); | |||
if (writable || fact.already_on_page) { | |||
$row.append($check, $line, $tgt, $details); | |||
} else { | |||
$row.append($line, $tgt, $details); | |||
$check.prop('checked', false); | |||
} | |||
/* Keep a hidden checkbox state for notes (never apply) */ | |||
if (!writable) { | |||
$row.append($('<input>', { type: 'checkbox', checked: false, 'class': 'ocr-fact-check-hidden' }).hide()); | |||
} | |||
return $row; | |||
} | |||
var $listRef = null; | |||
function renderPersonCards($list, proposed, job, onChange) { | |||
var grouped = groupFactsByPerson(proposed); | |||
grouped.order.forEach(function (key) { | |||
var items = grouped.byPerson[key]; | |||
var first = items[0] || {}; | var first = items[0] || {}; | ||
var $ | var title = first.target_title || ''; | ||
var head = | var writable = items.filter(function (f) { | ||
var $ | return isWritableFact(f) && !f.already_on_page; | ||
}); | |||
var notes = items.filter(function (f) { | |||
return f.kind === 'note' || f.already_on_page; | |||
$ | }); | ||
var included = writable.some(function (f) { return f.selected; }); | |||
var $card = $('<div>', { | |||
'class': 'ocr-person-card' + (title ? ' ocr-person-matched' : ' ocr-person-unresolved'), | |||
'data-person': key | |||
}); | |||
var $head = $('<div>', { 'class': 'ocr-card-head' }); | |||
var $include = $('<input>', { | |||
type: 'checkbox', | |||
'class': 'ocr-include', | |||
checked: included && !!title | |||
}); | |||
$head.append($('<label>', { 'class': 'ocr-include-label' }).append($include, ' Include')); | |||
$head.append($('<div>', { 'class': 'ocr-card-name' }).text(personDisplayName(first))); | |||
var meta = personMetaBits(items); | |||
if (meta.length) { | |||
$head.append($('<div>', { 'class': 'ocr-card-meta' }).text(meta.join(' · '))); | |||
} | } | ||
$ | $card.append($head); | ||
var | |||
var $match = $('<div>', { 'class': 'ocr-card-match' }); | |||
var $tgtShared = $('<input>', { | var $tgtShared = $('<input>', { | ||
type: 'text', | type: 'text', | ||
'class': 'ocr-person-title', | 'class': 'ocr-person-title', | ||
value: | value: title, | ||
placeholder: ' | placeholder: 'Find soldier (name or ASN)…' | ||
}); | |||
var $suggest = $('<div>', { 'class': 'ocr-suggest' }).hide(); | |||
var $matchStatus = $('<div>', { 'class': 'ocr-match-status' }); | |||
function setMatch(t) { | |||
title = t || ''; | |||
$tgtShared.val(title); | |||
$card.find('.ocr-fact-target').val(title); | |||
if (title) { | |||
$card.removeClass('ocr-person-unresolved').addClass('ocr-person-matched'); | |||
$matchStatus.empty().append( | |||
$('<span>').text('Matched: '), | |||
$('<a>', { href: mw.util.getUrl(title), text: title, target: '_blank' }) | |||
); | |||
} else { | |||
$card.removeClass('ocr-person-matched').addClass('ocr-person-unresolved'); | |||
$matchStatus.text('Needs a soldier match before Apply'); | |||
$include.prop('checked', false); | |||
} | |||
if (onChange) { | |||
onChange(); | |||
} | |||
} | |||
if (title) { | |||
setMatch(title); | |||
} else { | |||
$matchStatus.text('Needs a soldier match before Apply'); | |||
} | |||
$match.append($matchStatus); | |||
$match.append($('<label>').text('Wiki page ').append($tgtShared)); | |||
$match.append($suggest); | |||
bindSoldierTypeahead($tgtShared, $suggest, setMatch); | |||
$tgtShared.on('change', function () { | |||
setMatch($.trim($tgtShared.val() || '')); | |||
}); | }); | ||
var candidates = first.match_candidates || []; | var candidates = first.match_candidates || []; | ||
items.forEach(function (f) { | items.forEach(function (f) { | ||
| Line 1,444: | Line 1,615: | ||
} | } | ||
}); | }); | ||
if (candidates && candidates.length | if (candidates && candidates.length) { | ||
var $pick = $('<select>', { 'class': 'ocr-match-pick' }); | var $pick = $('<select>', { 'class': 'ocr-match-pick' }); | ||
$pick.append($('<option>', { value: '', text: ' | $pick.append($('<option>', { value: '', text: 'Suggested matches…' })); | ||
candidates.forEach(function (t) { | candidates.forEach(function (t) { | ||
$pick.append($('<option>', { value: t, text: t })); | $pick.append($('<option>', { value: t, text: t, selected: t === title })); | ||
}); | }); | ||
$pick.on('change', function () { | $pick.on('change', function () { | ||
if ($pick.val()) { | |||
setMatch($pick.val()); | |||
$ | |||
} | } | ||
}); | }); | ||
$ | $match.append($pick); | ||
} | |||
$card.append($match); | |||
var $changes = $('<div>', { 'class': 'ocr-card-changes' }); | |||
writable.forEach(function (fact) { | |||
var $row = renderChangeRow(fact, job, { hideTargetVisible: true }); | |||
$row.find('.ocr-fact-target').val(title); | |||
$changes.append($row); | |||
}); | |||
if (!writable.length && notes.some(function (n) { return n.already_on_page; })) { | |||
notes.filter(function (n) { return n.already_on_page; }).forEach(function (fact) { | |||
$changes.append(renderChangeRow(fact, job, { hideTargetVisible: true })); | |||
}); | |||
} | |||
$card.append($changes); | |||
var detailNotes = notes.filter(function (n) { return n.kind === 'note'; }); | |||
if (detailNotes.length) { | |||
var $more = $('<details>', { 'class': 'ocr-card-more' }); | |||
$more.append($('<summary>').text('Document details')); | |||
detailNotes.forEach(function (fact) { | |||
var $row = renderChangeRow(fact, job, { hideTargetVisible: true }); | |||
$row.find('.ocr-fact-target').val(title); | |||
$more.append($row); | |||
}); | |||
$card.append($more); | |||
} | } | ||
$ | |||
var | $include.on('change', function () { | ||
var on = $include.prop('checked'); | |||
if (on && !$.trim($tgtShared.val() || '')) { | |||
$include.prop('checked', false); | |||
$matchStatus.text('Match a soldier page first, then Include.'); | |||
return; | |||
} | |||
$card.find('.ocr-fact-writable input[type="checkbox"]').prop('checked', on); | |||
if (onChange) { | |||
onChange(); | |||
} | } | ||
}); | }); | ||
$card.on('change', 'input[type="checkbox"], input.ocr-fact-value', function () { | |||
if (onChange) { | |||
onChange(); | |||
} | |||
}); | }); | ||
$list.append($ | $list.append($card); | ||
}); | }); | ||
} | |||
function renderSingleChangeCard($list, proposed, job, onChange) { | |||
var $card = $('<div>', { 'class': 'ocr-person-card ocr-person-matched ocr-single-card' }); | |||
var title = job.bound_title || ''; | |||
$card.append($('<div>', { 'class': 'ocr-card-name' }).text(title || 'This soldier')); | |||
if (title) { | |||
$card.append($('<div>', { 'class': 'ocr-match-status' }).append( | |||
$('<a>', { href: mw.util.getUrl(title), text: title, target: '_blank' }) | |||
)); | |||
} | |||
var $tgtHidden = $('<input>', { type: 'hidden', 'class': 'ocr-person-title', value: title }); | |||
$card.append($tgtHidden); | |||
var primary = proposed.filter(function (f) { | |||
return isWritableFact(f) && !f.already_on_page; | |||
}); | |||
var skipped = proposed.filter(function (f) { return f.already_on_page; }); | |||
var notes = proposed.filter(function (f) { return f.kind === 'note'; }); | |||
var $changes = $('<div>', { 'class': 'ocr-card-changes' }); | |||
if (!primary.length) { | |||
$changes.append($('<p>', { 'class': 'ocr-fact-meta' }).text('No new fields to write (or all already on the page).')); | |||
} | |||
primary.forEach(function (fact) { | |||
var $row = renderChangeRow(fact, job, { hideTargetVisible: true }); | |||
$row.find('.ocr-fact-target').val(title); | |||
$changes.append($row); | |||
}); | |||
skipped.forEach(function (fact) { | |||
$changes.append(renderChangeRow(fact, job, { hideTargetVisible: true })); | |||
}); | |||
$card.append($changes); | |||
if (notes.length) { | |||
var $more = $('<details>', { 'class': 'ocr-card-more' }); | |||
$more.append($('<summary>').text('OCR notes (not written to the page)')); | |||
notes.forEach(function (fact) { | |||
var $row = renderChangeRow(fact, job, { hideTargetVisible: true }); | |||
$row.find('.ocr-fact-target').val(title); | |||
$more.append($row); | |||
}); | |||
$card.append($more); | |||
} | |||
$card.on('change', 'input', function () { | |||
if (onChange) { | |||
onChange(); | |||
} | |||
}); | |||
$list.append($card); | |||
} | |||
function renderApplySuccess($host, job) { | |||
$host.empty(); | |||
var $panel = $('<div>', { 'class': 'ocr-review-panel ocr-apply-success' }); | |||
$panel.append($('<p>').append($('<strong>').text('Applied'))); | |||
var log = job.apply_log || []; | |||
var titles = {}; | |||
log.forEach(function (line) { | |||
var m = /^([^:]+):\s*(.+)$/.exec(line); | |||
if (m && m[1].indexOf('team stub') < 0) { | |||
titles[m[1]] = titles[m[1]] || []; | |||
titles[m[1]].push(m[2]); | |||
} | |||
}); | |||
var keys = Object.keys(titles); | |||
if (keys.length) { | |||
var $ul = $('<ul>', { 'class': 'ocr-apply-links' }); | |||
keys.forEach(function (t) { | |||
var $li = $('<li>'); | |||
$li.append($('<a>', { href: mw.util.getUrl(t), text: t })); | |||
$li.append($('<span>', { 'class': 'ocr-fact-meta' }).text(' — ' + titles[t].join('; '))); | |||
$ul.append($li); | |||
}); | |||
$panel.append($ul); | |||
} else { | |||
$panel.append($('<p>').text(log.join(' · ') || 'Done.')); | |||
} | |||
if (isSoldier) { | |||
$panel.append($('<p>').append( | |||
$('<button>', { type: 'button', text: 'Reload page' }).on('click', function () { | |||
window.location.reload(); | |||
}), | |||
document.createTextNode(' to see updates on this profile.') | |||
)); | |||
} | |||
$host.append($panel); | |||
} | } | ||
function renderReview($host, job, cfg) { | function renderReview($host, job, cfg) { | ||
$host.empty(); | $host.empty(); | ||
var $panel = $('<div>', { 'class': 'ocr-review-panel' }); | var $panel = $('<div>', { 'class': 'ocr-review-panel ocr-review-v2' }); | ||
$panel.append($('<p>').append( | $panel.append($('<p>').append( | ||
$('<strong>').text(' | $('<strong>').text(job.multi_person ? 'Confirm people, then Apply' : 'Confirm changes, then Apply'), | ||
document.createTextNode(' — nothing is saved | document.createTextNode(' — nothing is saved until you click Apply.') | ||
)); | )); | ||
| Line 1,500: | Line 1,779: | ||
(ps.already_on_page || 0) + ' already have team' + | (ps.already_on_page || 0) + ' already have team' + | ||
(ps.teams && ps.teams.length ? (' · teams ' + ps.teams.join(', ')) : '') + | (ps.teams && ps.teams.length ? (' · teams ' + ps.teams.join(', ')) : '') + | ||
(ps.inferred_teams && ps.inferred_teams.length ? (' · inferred ' + ps.inferred_teams.join(', ')) : '') + | (ps.inferred_teams && ps.inferred_teams.length ? (' · inferred ' + ps.inferred_teams.join(', ')) : '') | ||
( | )); | ||
} else { | |||
$panel.append($('<p>', { 'class': 'ocr-fact-meta' }).text( | |||
(job.doc_type_label || 'Document') + | |||
' · OCR quality ' + (job.ocr_quality || 0) + '%' + | |||
(job.error ? (' · Error: ' + job.error) : '') | |||
)); | )); | ||
} | } | ||
| Line 1,515: | Line 1,799: | ||
var $layout = $('<div>', { 'class': 'ocr-review-layout' }); | var $layout = $('<div>', { 'class': 'ocr-review-layout' }); | ||
var $prev = $('<div>', { 'class': 'ocr-review-preview' }); | var $prev = $('<div>', { 'class': 'ocr-review-preview ocr-preview-sticky' }); | ||
var pages = job.pages || []; | var pages = job.pages || []; | ||
if (pages.length > 1) { | if (pages.length > 1) { | ||
$prev.append($('<p>').append($('<strong>').text('Pages | $prev.append($('<p>').append($('<strong>').text('Pages'))); | ||
var $pageList = $('<div>', { 'class': 'ocr-page-picker' }); | var $pageList = $('<div>', { 'class': 'ocr-page-picker' }); | ||
pages.forEach(function (pg) { | pages.forEach(function (pg) { | ||
| Line 1,527: | Line 1,811: | ||
$pg.append($('<img>', { src: imgUrl, alt: 'Page ' + (pg.index + 1) })); | $pg.append($('<img>', { src: imgUrl, alt: 'Page ' + (pg.index + 1) })); | ||
} | } | ||
$pg.append($cb, document.createTextNode(' Page ' + (pg.index + 1) | $pg.append($cb, document.createTextNode(' Page ' + (pg.index + 1))); | ||
$pageList.append($pg); | $pageList.append($pg); | ||
}); | }); | ||
| Line 1,541: | Line 1,825: | ||
var $facts = $('<div>', { 'class': 'ocr-review-facts' }); | var $facts = $('<div>', { 'class': 'ocr-review-facts' }); | ||
var proposed = job.proposed || []; | |||
var $list = $('<div>', { 'class': 'ocr-fact-groups ocr-card-list' }); | |||
$listRef = $list; | |||
var $previewBox = $('<div>', { 'class': 'ocr-apply-preview' }); | |||
$previewBox.append($('<strong>').text('What will change')); | |||
var $previewUl = $('<ul>', { 'class': 'ocr-apply-preview-list' }); | |||
$previewBox.append($previewUl); | |||
function refreshPreview() { | |||
var props = collectProposed(); | |||
var lines = []; | |||
props.forEach(function (p) { | |||
if (!p.selected || !isWritableFact(p) || p.already_on_page) { | |||
return; | |||
} | |||
if (!(p.target_title || '').trim()) { | |||
return; | |||
} | |||
var who = personDisplayName(p); | |||
lines.push(who + ' → ' + changeLineForFact(p)); | |||
}); | |||
$previewUl.empty(); | |||
if (!lines.length) { | |||
$previewUl.append($('<li>', { 'class': 'ocr-fact-meta' }).text('Nothing selected yet.')); | |||
} else { | |||
lines.forEach(function (ln) { | |||
$previewUl.append($('<li>').text(ln)); | |||
}); | |||
} | |||
} | |||
if (job.multi_person) { | |||
var $toolbar = $('<div>', { 'class': 'ocr-multi-toolbar' }); | |||
var $selMatched = $('<button>', { type: 'button', text: 'Include all matched' }); | |||
var $deselUnres = $('<button>', { type: 'button', text: 'Exclude unmatched' }); | |||
$toolbar.append($selMatched, $deselUnres); | |||
$facts.append($toolbar); | |||
renderPersonCards($list, proposed, job, refreshPreview); | |||
$selMatched.on('click', function () { | |||
$list.find('.ocr-person-matched').each(function () { | |||
var $card = $(this); | |||
$card.find('.ocr-include').prop('checked', true).trigger('change'); | |||
$card.find('.ocr-fact-writable input[type="checkbox"]').prop('checked', true); | |||
}); | |||
refreshPreview(); | |||
}); | |||
$deselUnres.on('click', function () { | |||
$list.find('.ocr-person-unresolved .ocr-include').prop('checked', false); | |||
$list.find('.ocr-person-unresolved input[type="checkbox"]').prop('checked', false); | |||
refreshPreview(); | |||
}); | |||
} else { | |||
renderSingleChangeCard($list, proposed, job, refreshPreview); | |||
} | |||
$facts.append($list); | |||
$facts.append($previewBox); | |||
/* Advanced: OCR text, doc type, gallery, overwrite, rebuild */ | |||
var $adv = $('<details>', { 'class': 'ocr-advanced' }); | |||
$adv.append($('<summary>').text('Advanced')); | |||
var $advBody = $('<div>', { 'class': 'ocr-advanced-body' }); | |||
var $docType = $('<label>', { 'class': 'ocr-doc-type' }).text('Document type '); | var $docType = $('<label>', { 'class': 'ocr-doc-type' }).text('Document type '); | ||
| Line 1,552: | Line 1,898: | ||
}); | }); | ||
$docType.append($docSelect); | $docType.append($docSelect); | ||
$ | $advBody.append($docType); | ||
$ | $advBody.append($('<label>').text('Gallery caption')); | ||
var $caption = $('<input>', { | var $caption = $('<input>', { | ||
type: 'text', | type: 'text', | ||
| Line 1,560: | Line 1,906: | ||
value: job.gallery_caption || '' | value: job.gallery_caption || '' | ||
}); | }); | ||
$ | $advBody.append($caption); | ||
$ | $advBody.append($('<label>').text('OCR text (editable)')); | ||
var $text = $('<textarea>').val(job.ocr_text || ''); | var $text = $('<textarea>').val(job.ocr_text || ''); | ||
$ | $advBody.append($text); | ||
var $overwrite = $('<label>').append( | var $overwrite = $('<label>').append( | ||
| Line 1,640: | Line 1,927: | ||
$forceId.hide(); | $forceId.hide(); | ||
} | } | ||
$ | $advBody.append($overwrite, $('<br>'), $gallery, $('<br>'), $forceId); | ||
var $save = $('<button>', { type: 'button', text: 'Save draft' }); | var $save = $('<button>', { type: 'button', text: 'Save draft' }); | ||
var $rebuild = $('<button>', { type: 'button', text: 'Rebuild from pages' }); | var $rebuild = $('<button>', { type: 'button', text: 'Rebuild from pages' }); | ||
| Line 1,651: | Line 1,934: | ||
$rebuild.hide(); | $rebuild.hide(); | ||
} | } | ||
$advBody.append($('<div>', { 'class': 'ocr-advanced-actions' }).append($save, $rebuild)); | |||
$adv.append($advBody); | |||
$facts.append($adv); | |||
$layout.append($prev, $facts); | |||
$panel.append($layout); | |||
var $actions = $('<div>', { 'class': 'ocr-review-actions' }); | |||
var $discard = $('<button>', { type: 'button', text: 'Discard' }); | var $discard = $('<button>', { type: 'button', text: 'Discard' }); | ||
var $apply = $('<button>', { type: 'button', text: job.multi_person ? 'Apply selected | var $apply = $('<button>', { | ||
type: 'button', | |||
'class': 'ocr-apply-btn', | |||
text: job.multi_person ? 'Apply selected' : 'Apply selected' | |||
}); | |||
var $status = $('<p>', { 'class': 'ocr-review-status' }); | var $status = $('<p>', { 'class': 'ocr-review-status' }); | ||
if (!cfg.canApply) { | if (!cfg.canApply) { | ||
| Line 1,658: | Line 1,953: | ||
$status.text('Apply requires the ocr-reviewer group (or admin). Your upload stays in the review queue.'); | $status.text('Apply requires the ocr-reviewer group (or admin). Your upload stays in the review queue.'); | ||
} | } | ||
$actions.append( | $actions.append($discard, $apply, $status); | ||
$panel.append($actions); | $panel.append($actions); | ||
$host.append($panel); | $host.append($panel); | ||
refreshPreview(); | |||
function collectProposed() { | function collectProposed() { | ||
| Line 1,676: | Line 1,972: | ||
return; | return; | ||
} | } | ||
var $cb = $row.find('input[type="checkbox"]').first(); | |||
base. | base.selected = $cb.length ? !!$cb.prop('checked') : false; | ||
if ($row.hasClass('ocr-fact-is-note') || $row.hasClass('ocr-fact-note') && !base.already_on_page) { | |||
base.selected = false; | |||
} | } | ||
base.value = $.trim($row.find('.ocr-fact-value').val() || base.value || ''); | |||
var $card = $row.closest('.ocr-person-card'); | |||
var $t = $card.find('.ocr-person-title'); | |||
if ($t.length) { | if ($t.length) { | ||
base.target_title = $.trim($t.val() || ''); | base.target_title = $.trim($t.val() || ''); | ||
} else { | } else { | ||
base.target_title = $.trim($row.find(' | base.target_title = $.trim($row.find('.ocr-fact-target').val() || job.bound_title || ''); | ||
} | } | ||
$row.find('.ocr-fact-target').val(base.target_title); | |||
out.push(base); | out.push(base); | ||
}); | }); | ||
| Line 1,723: | Line 2,022: | ||
job_id: job.id, | job_id: job.id, | ||
payload: JSON.stringify(payload()) | payload: JSON.stringify(payload()) | ||
}).then(function ( | }).then(function () { | ||
$status.text('Draft saved.'); | $status.text('Draft saved.'); | ||
}).catch(function (err) { | }).catch(function (err) { | ||
$status.text(String(err)); | $status.text(String(err)); | ||
| Line 1,742: | Line 2,037: | ||
payload: JSON.stringify(payload()) | payload: JSON.stringify(payload()) | ||
}).then(function (resp) { | }).then(function (resp) { | ||
renderReview($host, resp.job || resp, cfg); | |||
}).catch(function (err) { | }).catch(function (err) { | ||
$status.text(String(err && err.error && err.error.info || err)); | $status.text(String(err && err.error && err.error.info || err)); | ||
| Line 1,757: | Line 2,051: | ||
payload: JSON.stringify(payload()) | payload: JSON.stringify(payload()) | ||
}).then(function (resp) { | }).then(function (resp) { | ||
renderReview($host, resp.job || resp, cfg); | |||
}).catch(function (err) { | }).catch(function (err) { | ||
$status.text(String(err)); | $status.text(String(err)); | ||
| Line 1,775: | Line 2,068: | ||
$apply.on('click', function () { | $apply.on('click', function () { | ||
if (!job.identity_ok && !$forceId.find('input').prop('checked')) { | if (!job.identity_ok && !$forceId.find('input').prop('checked')) { | ||
$status.text('Confirm identity (checkbox) before Apply, or fix the wrong document.'); | $status.text('Confirm identity (Advanced → checkbox) before Apply, or fix the wrong document.'); | ||
$adv.prop('open', true); | |||
return; | |||
} | |||
var selected = collectProposed().filter(function (p) { | |||
return p.selected && isWritableFact(p) && (p.target_title || '').trim(); | |||
}); | |||
if (!selected.length) { | |||
$status.text('Select at least one change with a matched soldier.'); | |||
return; | return; | ||
} | } | ||
| Line 1,785: | Line 2,086: | ||
payload: JSON.stringify(payload()) | payload: JSON.stringify(payload()) | ||
}).then(function (resp) { | }).then(function (resp) { | ||
renderApplySuccess($host, resp.job || resp); | |||
}).catch(function (err) { | }).catch(function (err) { | ||
$status.text(String(err && err.error && err.error.info || err)); | $status.text(String(err && err.error && err.error.info || err)); | ||
| Line 1,988: | Line 2,283: | ||
jobs.forEach(function (job) { | jobs.forEach(function (job) { | ||
var $li = $('<li>'); | var $li = $('<li>'); | ||
var | var $row = $('<div>', { 'class': 'ocr-queue-item' }); | ||
(job.bound_title ? (' · ' + job. | var thumb = publicPreviewUrl(job); | ||
' · ' + (job.status || '?') + | if (thumb) { | ||
$row.append($('<img>', { | |||
'class': 'ocr-queue-thumb', | |||
src: thumb, | |||
alt: '' | |||
})); | |||
} | |||
var $meta = $('<div>', { 'class': 'ocr-queue-meta' }); | |||
var ps = job.people_summary || {}; | |||
var peopleBit = job.multi_person | |||
? ((ps.detected || 0) + ' people · ' + (ps.matched || 0) + ' matched · ' + (ps.unresolved || 0) + ' unmatched') | |||
: (job.bound_title ? job.bound_title : 'single document'); | |||
$meta.append($('<div>').append( | |||
$('<strong>').text(job.filename || job.id), | |||
document.createTextNode(' · ' + (job.doc_type_label || job.doc_type || 'document')) | |||
)); | |||
$meta.append($('<div>', { 'class': 'ocr-fact-meta' }).text( | |||
peopleBit + ' · ' + (job.status || '?') + | |||
(job.status === 'processing' ? (' ' + (job.progress || 0) + '%') : '') + | (job.status === 'processing' ? (' ' + (job.progress || 0) + '%') : '') + | ||
' · by ' + (job.uploader || '?') | |||
' · by ' + (job.uploader || '?'); | )); | ||
var $open = $('<button>', { type: 'button', text: 'Open' }); | var $open = $('<button>', { type: 'button', text: 'Open' }); | ||
$ | $meta.append($open); | ||
$row.append($meta); | |||
$li.append($row); | |||
$open.on('click', function () { | $open.on('click', function () { | ||
$host2.empty(); | $host2.empty(); | ||
| Line 2,005: | Line 2,319: | ||
}); | }); | ||
} | } | ||
window.scrollTo({ top: $host2.offset().top - 40, behavior: 'smooth' }); | |||
}); | }); | ||
$ul.append($li); | $ul.append($li); | ||