Jump to content

MediaWiki:Gadget-ArticleReview.js: Difference between revisions

From MonoCloud Wiki
Add dismissible article review interface
 
Add local writing review with passage evidence and optional draft checks
 
Line 3: Line 3:


var rootId = 'shalombux-article-review';
var rootId = 'shalombux-article-review';
var editorId = 'mayapedia-writing-review-editor';
var guideUrl = 'https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing';
var viewCard, editorCard, activeTarget, sourceInput, activeDocument;
var editorGeneration = 0, requestSerial = 0;
var maxFindings = 80;


function element( name, className, text ) {
function element( name, className, text ) {
Line 34: Line 39:
}
}


function initialize() {
function paragraphLink( parent, label, address ) {
var data = window.shalombuxArticleReviewData;
var link = safeLink( label, address, true );
var query = new URLSearchParams( window.location.search );
if ( link ) {
var pageId = Number( mw.config.get( 'wgArticleId' ) );
parent.appendChild( link );
var viewedRevision = Number( mw.config.get( 'wgRevisionId' ) );
if ( document.getElementById( rootId ) || !data || !data.pages ||
mw.config.get( 'wgNamespaceNumber' ) !== 0 || pageId <= 0 || viewedRevision <= 0 ||
mw.config.get( 'wgAction' ) !== 'view' || query.has( 'diff' ) || query.has( 'veaction' ) ||
mw.config.get( 'wgDiffOldId' ) || mw.config.get( 'wgDiffNewId' ) ) {
return;
}
var review = data.pages[ String( pageId ) ];
if ( !review || !Number( review.reviewedRevision ) ) {
return;
}
 
var reviewedRevision = Number( review.reviewedRevision );
var currentRevision = Number( mw.config.get( 'wgCurRevisionId' ) ) || viewedRevision;
var storageKey = 'shalombux-article-review:' + String( data.version ) + ':' +
pageId + ':' + reviewedRevision;
var dismissed = false;
try {
dismissed = window.localStorage.getItem( storageKey ) === 'dismissed';
} catch ( error ) {
// The card remains usable when browser storage is unavailable.
}
}
}


var root = element( 'div', 'sb-article-review' );
function makeCard( id, label, storageKey, editor ) {
root.id = rootId;
var root = element( 'div', 'sb-article-review' + ( editor ? ' sb-article-review--editor' : '' ) );
var badge = element( 'button', 'sb-article-review__badge', 'Article review' );
root.id = id;
var badge = element( 'button', 'sb-article-review__badge', label );
badge.type = 'button';
badge.type = 'button';
badge.setAttribute( 'aria-controls', rootId + '-panel' );
badge.setAttribute( 'aria-controls', id + '-panel' );
badge.setAttribute( 'aria-expanded', 'false' );
badge.setAttribute( 'aria-expanded', 'false' );
badge.title = 'Show the article review';
badge.title = editor ? 'Check the current unsaved writing' : 'Show the article review';
 
var panel = element( 'section', 'sb-article-review__panel' );
var panel = element( 'section', 'sb-article-review__panel' );
panel.id = rootId + '-panel';
panel.id = id + '-panel';
panel.setAttribute( 'aria-labelledby', rootId + '-heading' );
panel.hidden = true;
panel.setAttribute( 'aria-labelledby', id + '-heading' );
var header = element( 'div', 'sb-article-review__header' );
var header = element( 'div', 'sb-article-review__header' );
var heading = element( 'h2', 'sb-article-review__heading', 'Article review' );
var heading = element( 'h2', 'sb-article-review__heading', editor ? 'Writing review' : 'Article review' );
heading.id = rootId + '-heading';
heading.id = id + '-heading';
var close = element( 'button', 'sb-article-review__close', '\u00d7' );
var close = element( 'button', 'sb-article-review__close', '\u00d7' );
close.type = 'button';
close.type = 'button';
close.setAttribute( 'aria-label', 'Close article review' );
close.setAttribute( 'aria-label', 'Close ' + heading.textContent.toLowerCase() );
close.title = 'Close article review (Escape)';
close.title = 'Close review (Escape)';
header.appendChild( heading );
header.appendChild( heading );
header.appendChild( close );
header.appendChild( close );
panel.appendChild( header );
panel.appendChild( header );
var body = element( 'div', 'sb-article-review__body' );
var body = element( 'div', 'sb-article-review__body' );
body.appendChild( element( 'p', 'sb-article-review__date', 'Reviewed ' + String( data.reviewDate ) ) );
panel.appendChild( body );
if ( viewedRevision !== reviewedRevision || currentRevision !== reviewedRevision ) {
root.appendChild( badge );
var stale = viewedRevision !== reviewedRevision ?
root.appendChild( panel );
'This review covers a different revision. The text you are viewing has not been rechecked; the findings below may no longer apply.' :
var dismissed = false, hoverBlocked = false, hoverAfter = 0, suppressFocus = false;
'You are viewing the reviewed revision. A newer revision exists and has not been rechecked.';
try {
body.appendChild( element( 'p', 'sb-article-review__stale', stale ) );
dismissed = storageKey && window.localStorage.getItem( storageKey ) === 'dismissed';
} catch ( error ) {
// The card also works when browser storage is unavailable.
}
function remember( value ) {
if ( !storageKey ) {
return;
}
try {
if ( value ) {
window.localStorage.setItem( storageKey, 'dismissed' );
} else {
window.localStorage.removeItem( storageKey );
}
} catch ( error ) {
// Dismissal still works until this page is closed.
}
}
function open( rememberOpen ) {
if ( root.hidden ) {
return;
}
panel.hidden = false;
badge.setAttribute( 'aria-expanded', 'true' );
if ( rememberOpen ) {
remember( false );
}
}
function closePanel( returnFocus ) {
panel.hidden = true;
badge.setAttribute( 'aria-expanded', 'false' );
hoverBlocked = true;
hoverAfter = Date.now() + 800;
remember( true );
if ( returnFocus ) {
suppressFocus = true;
badge.focus( { preventScroll: true } );
suppressFocus = false;
}
}
}
var revisionLink = safeLink( 'View reviewed revision ' + reviewedRevision,
close.addEventListener( 'click', function () { closePanel( true ); } );
mw.util.getUrl( review.title || mw.config.get( 'wgPageName' ), { oldid: reviewedRevision } ), false );
badge.addEventListener( 'click', function () { open( true ); } );
if ( revisionLink ) {
if ( !editor ) {
var revisionParagraph = element( 'p', 'sb-article-review__revision' );
badge.addEventListener( 'focus', function () {
revisionParagraph.appendChild( revisionLink );
if ( !suppressFocus ) {
body.appendChild( revisionParagraph );
open( false );
}
} );
badge.addEventListener( 'mouseenter', function () {
if ( !hoverBlocked && Date.now() >= hoverAfter && window.matchMedia( '(hover: hover)' ).matches ) {
open( false );
}
} );
}
}
root.addEventListener( 'mouseleave', function () { hoverBlocked = false; } );
document.addEventListener( 'keydown', function ( event ) {
if ( event.key === 'Escape' && !root.hidden && !panel.hidden ) {
var focusInside = root.contains( document.activeElement );
closePanel( focusInside );
if ( focusInside ) {
event.preventDefault();
}
}
} );
// The controls are outside article content and editor serialization.
document.body.appendChild( root );
return { root: root, body: body, badge: badge, panel: panel, open: open, dismissed: dismissed };
}


function renderManual( body, review, data, viewedRevision ) {
if ( !review || !Number( review.reviewedRevision ) ) {
return;
}
var revision = Number( review.reviewedRevision );
body.appendChild( element( 'h3', 'sb-article-review__section', 'Recorded article review' ) );
body.appendChild( element( 'p', 'sb-article-review__date', 'Reviewed ' + String( data.reviewDate ) + ' \u00b7 revision ' + revision ) );
if ( viewedRevision !== revision || Number( mw.config.get( 'wgCurRevisionId' ) ) > revision ) {
body.appendChild( element( 'p', 'sb-article-review__stale', viewedRevision !== revision ?
'These recorded findings cover another revision and may no longer apply. Writing checks below cover the revision identified in that section.' :
'A newer revision exists. These recorded findings cover the older revision shown here.' ) );
}
var revisionParagraph = element( 'p', 'sb-article-review__revision' );
paragraphLink( revisionParagraph, 'View reviewed revision ' + revision,
mw.util.getUrl( review.title || mw.config.get( 'wgPageName' ), { oldid: revision } ) );
body.appendChild( revisionParagraph );
var issues = Array.isArray( review.issues ) ? review.issues : [];
var issues = Array.isArray( review.issues ) ? review.issues : [];
if ( issues.length ) {
if ( issues.length ) {
var list = element( 'ul', 'sb-article-review__issues' );
var list = element( 'ul', 'sb-article-review__issues' );
issues.forEach( function ( issue ) {
issues.slice( 0, maxFindings ).forEach( function ( issue ) {
if ( !issue || typeof issue !== 'object' ) {
if ( !issue || typeof issue !== 'object' ) {
return;
return;
}
}
var kind = [ 'error', 'review', 'note' ].indexOf( issue.kind ) !== -1 ? issue.kind : 'note';
var kind = [ 'error', 'review', 'note' ].indexOf( issue.kind ) !== -1 ? issue.kind : 'note';
var labels = { error: 'Verified error', review: 'Review needed', note: 'Note' };
var item = element( 'li', 'sb-article-review__issue sb-article-review__issue--' + kind );
var item = element( 'li', 'sb-article-review__issue sb-article-review__issue--' + kind );
item.appendChild( element( 'span', 'sb-article-review__kind', labels[ kind ] ) );
item.appendChild( element( 'span', 'sb-article-review__kind', { error: 'Verified error', review: 'Review needed', note: 'Note' }[ kind ] ) );
item.appendChild( element( 'strong', 'sb-article-review__issue-title', issue.title || 'Review note' ) );
item.appendChild( element( 'strong', 'sb-article-review__issue-title', issue.title || 'Review note' ) );
item.appendChild( element( 'p', 'sb-article-review__detail', issue.detail || '' ) );
item.appendChild( element( 'p', 'sb-article-review__detail', issue.detail || '' ) );
Line 115: Line 174:
var links = element( 'div', 'sb-article-review__links' );
var links = element( 'div', 'sb-article-review__links' );
issue.links.forEach( function ( source ) {
issue.links.forEach( function ( source ) {
if ( !source || typeof source.url !== 'string' ) {
if ( source && typeof source.url === 'string' ) {
return;
paragraphLink( links, source.label || 'Source', source.url );
}
var link = safeLink( source.label || 'Source', source.url, true );
if ( link ) {
links.appendChild( link );
}
}
} );
} );
if ( links.childNodes.length ) {
item.appendChild( links );
item.appendChild( links );
}
}
}
list.appendChild( item );
list.appendChild( item );
Line 131: Line 184:
body.appendChild( list );
body.appendChild( list );
} else {
} else {
body.appendChild( element( 'p', 'sb-article-review__no-issues',
body.appendChild( element( 'p', '', 'No article-specific issue was recorded in this limited review.' ) );
'No article-specific issue recorded in this limited review.' ) );
}
var scope = element( 'details', 'sb-article-review__scope' );
scope.appendChild( element( 'summary', '', 'Scope of the recorded review' ) );
scope.appendChild( element( 'p', 'sb-article-review__coverage', 'The following notes apply only to reviewed revision ' + revision + '.' ) );
if ( review.unicodeNote ) {
scope.appendChild( element( 'p', 'sb-article-review__unicode', review.unicodeNote ) );
}
scope.appendChild( element( 'p', 'sb-article-review__coverage', review.sourceReview === 'spot-checked' ?
'Selected citations were spot-checked; other sources remain unverified.' : 'Citation destinations were not checked for this article.' ) );
scope.appendChild( element( 'p', 'sb-article-review__coverage', 'Policy links provide review guidance; this was not a complete policy assessment.' ) );
body.appendChild( scope );
}
 
function sourceContents() {
if ( !sourceInput ) {
return null;
}
var input = window.jQuery( sourceInput );
return typeof input.textSelection === 'function' ? input.textSelection( 'getContents' ) : sourceInput.value;
}
 
function selectFinding( finding, checkedText ) {
if ( activeTarget || !sourceInput || sourceContents() !== checkedText ) {
return;
}
var encoded = new TextEncoder().encode( checkedText );
var startByte = Math.max( 0, Math.min( encoded.length, Number( finding.offset ) || 0 ) );
var endByte = Math.max( startByte, Math.min( encoded.length, startByte + ( Number( finding.length ) || 0 ) ) );
var decoder = new TextDecoder();
var start = decoder.decode( encoded.slice( 0, startByte ) ).length;
var end = decoder.decode( encoded.slice( 0, endByte ) ).length;
var input = window.jQuery( sourceInput );
if ( typeof input.textSelection === 'function' ) {
input.textSelection( 'setSelection', { start: start, end: end } );
input.textSelection( 'scrollToCaretPosition' );
} else {
sourceInput.setSelectionRange( start, end );
}
}
sourceInput.focus();
}


body.appendChild( element( 'p', 'sb-article-review__ai',
function renderWriting( body, result, options ) {
'AI indicators: none found in reviewed text/links; authorship unverified.' ) );
var findings = Array.isArray( result.findings ) ? result.findings : [];
body.appendChild( element( 'h3', 'sb-article-review__section', 'Writing checks' ) );
body.appendChild( element( 'p', 'sb-article-review__date', options.editor ?
'Checked an unsaved snapshot. Nothing has been saved or changed.' :
'Checked revision ' + Number( options.revision ) ) );
if ( options.editor ) {
var stale = element( 'p', 'sb-article-review__stale sb-writing-review__stale',
'The draft changed after this check. Check writing again to review the current text.' );
stale.hidden = !options.stale;
body.appendChild( stale );
}
body.appendChild( element( 'p', 'sb-article-review__limits',
'These are prompts for human review, not proof of AI authorship or policy violations. Saving is never blocked.' ) );
if ( !findings.length ) {
body.appendChild( element( 'p', 'sb-writing-review__empty', result.coverage && result.coverage.status === 'scanned' ?
'No matching signs were found by these checks. This does not establish who wrote the text or whether it is accurate.' :
'No findings are available from this check. See the coverage notes below.' ) );
} else {
var list = element( 'ul', 'sb-article-review__issues sb-writing-review__findings' );
findings.slice( 0, maxFindings ).forEach( function ( finding ) {
if ( !finding || typeof finding !== 'object' ) {
return;
}
var kind = finding.severity === 'review' ? 'review' : 'note';
var item = element( 'li', 'sb-article-review__issue sb-article-review__issue--' + kind );
item.appendChild( element( 'span', 'sb-article-review__kind', kind === 'review' ? 'Review this passage' : 'Context check' ) );
item.appendChild( element( 'strong', 'sb-article-review__issue-title', finding.title || 'Writing sign' ) );
item.appendChild( element( 'p', 'sb-article-review__detail', finding.explanation || '' ) );
if ( typeof finding.evidence === 'string' && finding.evidence ) {
item.appendChild( element( 'blockquote', 'sb-writing-review__evidence', finding.evidence.slice( 0, 1200 ) ) );
}
var links = element( 'div', 'sb-article-review__links' );
if ( Number( finding.line ) > 0 ) {
if ( options.editor && sourceInput && !activeTarget ) {
var select = element( 'button', 'sb-writing-review__source', 'Select source line ' + Number( finding.line ) );
select.type = 'button';
select.disabled = !!options.stale;
select.addEventListener( 'click', function () { selectFinding( finding, options.checkedText ); } );
links.appendChild( select );
} else if ( options.editor ) {
links.appendChild( element( 'span', '', 'Draft source line ' + Number( finding.line ) ) );
} else {
paragraphLink( links, 'Source line ' + Number( finding.line ), mw.util.getUrl(
options.title || mw.config.get( 'wgPageName' ), { action: 'edit', oldid: Number( options.revision ) } ) );
}
}
paragraphLink( links, 'Why this is flagged', typeof finding.guide === 'string' ? finding.guide : guideUrl );
item.appendChild( links );
list.appendChild( item );
} );
body.appendChild( list );
}
if ( result.truncated || findings.length > maxFindings ) {
body.appendChild( element( 'p', 'sb-article-review__stale', 'This report is limited in size. Additional passages may need review.' ) );
}
var scope = element( 'details', 'sb-article-review__scope' );
var scope = element( 'details', 'sb-article-review__scope' );
scope.appendChild( element( 'summary', '', 'Review scope' ) );
scope.appendChild( element( 'summary', '', 'What the checks cover' ) );
scope.appendChild( element( 'p', 'sb-article-review__unicode', review.unicodeNote ||
if ( result.coverage && result.coverage.reason ) {
'Unicode scan: no flagged hidden characters found in the reviewed source.' ) );
scope.appendChild( element( 'p', '', result.coverage.reason ) );
scope.appendChild( element( 'p', 'sb-article-review__coverage',
}
review.sourceReview === 'spot-checked' ?
var limitations = result.coverage && result.coverage.limitations;
'Selected citations were spot-checked; other sources remain unverified.' :
if ( Array.isArray( limitations ) ) {
'Citation destinations were not checked for this article.' ) );
limitations.slice( 0, 12 ).forEach( function ( note ) {
scope.appendChild( element( 'p', 'sb-article-review__coverage',
if ( typeof note === 'string' ) {
'Wikipedia policy links are review guidance; no complete policy assessment.' ) );
scope.appendChild( element( 'p', '', note ) );
}
} );
} else if ( typeof limitations === 'string' ) {
scope.appendChild( element( 'p', '', limitations ) );
}
scope.appendChild( element( 'p', '', 'Checks use this wiki\u2019s text. They do not establish plagiarism, verify every source, or identify the writer.' ) );
paragraphLink( scope, 'Wikipedia: Signs of AI writing', guideUrl );
body.appendChild( scope );
body.appendChild( scope );
panel.appendChild( body );
}
root.appendChild( badge );
 
root.appendChild( panel );
function initializeView() {
var data = window.shalombuxArticleReviewData || {};
var scan = mw.config.get( 'wgMayapediaWritingReview' );
var pageId = Number( mw.config.get( 'wgArticleId' ) );
var revision = Number( mw.config.get( 'wgRevisionId' ) );
var query = new URLSearchParams( window.location.search );
if ( document.getElementById( rootId ) || pageId <= 0 || revision <= 0 ||
mw.config.get( 'wgAction' ) !== 'view' || query.has( 'diff' ) ||
mw.config.get( 'wgDiffOldId' ) || mw.config.get( 'wgDiffNewId' ) ) {
return;
}
var review = data.pages && data.pages[ String( pageId ) ];
// Never present another page/revision's automated findings as current.
if ( scan && ( Number( scan.revision ) !== revision || Number( scan.pageId ) !== pageId ) ) {
scan = null;
}
if ( !scan && !review ) {
return;
}
var count = scan && Array.isArray( scan.findings ) ? scan.findings.length : 0;
var manualCount = review && Array.isArray( review.issues ) ? review.issues.length : 0;
var storageKey = 'shalombux-article-review:writing:' + String( data.version || '' ) + ':' +
String( scan && scan.version || '' ) + ':' + pageId + ':' + revision + ':' + String( review && review.reviewedRevision || '' );
viewCard = makeCard( rootId, count + manualCount ? 'Article review (' + ( count + manualCount ) + ')' : 'Article review', storageKey, false );
viewCard.revision = revision;
viewCard.currentRevision = Number( mw.config.get( 'wgCurRevisionId' ) ) || revision;
renderManual( viewCard.body, review, data, revision );
if ( scan ) {
renderWriting( viewCard.body, scan, { revision: revision, title: scan.title } );
} else {
viewCard.body.appendChild( element( 'p', 'sb-article-review__coverage', 'Automated writing checks are unavailable for this view.' ) );
}
if ( ( count || manualCount ) && !viewCard.dismissed ) {
viewCard.open( false );
}
}


var isOpen = false;
function markDraftChanged() {
var hoverBlocked = false;
editorGeneration++;
var hoverMayOpenAfter = 0;
if ( editorCard ) {
var suppressFocus = false;
var stale = editorCard.body.querySelector( '.sb-writing-review__stale' );
var suspended = false;
if ( stale ) {
stale.hidden = false;
}
editorCard.body.querySelectorAll( '.sb-writing-review__source' ).forEach( function ( button ) { button.disabled = true; } );
}
}


function rememberDismissal( value ) {
function currentDraft() {
try {
if ( activeTarget ) {
if ( value ) {
var doc = activeTarget.getDocToSave();
window.localStorage.setItem( storageKey, 'dismissed' );
if ( typeof doc === 'string' ) {
} else {
return Promise.resolve( doc );
window.localStorage.removeItem( storageKey );
}
return Promise.resolve( activeTarget.serialize( doc ) ).then( function ( data ) {
if ( !data || typeof data.content !== 'string' ) {
throw new Error( 'serialization' );
}
}
} catch ( error ) {
return data.content;
// Dismissal still works for the current page without storage.
} );
}
}
}
var text = sourceContents();
return typeof text === 'string' ? Promise.resolve( text ) : Promise.reject( new Error( 'editor' ) );
}


function openPanel( remember ) {
function checkDraft() {
if ( suspended ) {
if ( !editorCard || editorCard.badge.disabled ) {
return;
}
var serial = ++requestSerial, generation = editorGeneration, checkedText;
editorCard.badge.disabled = true;
editorCard.badge.textContent = 'Checking writing\u2026';
editorCard.body.textContent = '';
editorCard.body.appendChild( element( 'p', 'sb-writing-review__status', 'Checking the current unsaved text on Mayapedia\u2026' ) );
editorCard.body.setAttribute( 'aria-busy', 'true' );
editorCard.open( false );
currentDraft().then( function ( text ) {
checkedText = text;
return new mw.Api().postWithToken( 'csrf', { action: 'writingreview', text: text, format: 'json', formatversion: 2 } );
} ).then( function ( response ) {
if ( serial !== requestSerial || !editorCard ) {
return;
return;
}
}
isOpen = true;
if ( !response || !response.writingreview ) {
panel.hidden = false;
throw new Error( 'response' );
badge.setAttribute( 'aria-expanded', 'true' );
}
if ( remember ) {
editorCard.body.textContent = '';
rememberDismissal( false );
renderWriting( editorCard.body, response.writingreview, { editor: true, checkedText: checkedText,
stale: generation !== editorGeneration || ( !activeTarget && sourceContents() !== checkedText ) } );
} ).catch( function () {
if ( serial === requestSerial && editorCard ) {
editorCard.body.textContent = '';
editorCard.body.appendChild( element( 'p', 'sb-article-review__stale',
'The writing check could not finish. Your draft is unchanged and you can still save. Try Check writing again.' ) );
}
} ).then( function () {
if ( serial === requestSerial && editorCard ) {
editorCard.badge.disabled = false;
editorCard.badge.textContent = 'Check writing';
editorCard.body.removeAttribute( 'aria-busy' );
}
}
} );
}
function ensureEditor() {
if ( !editorCard ) {
editorCard = makeCard( editorId, 'Check writing', null, true );
editorCard.badge.addEventListener( 'click', checkDraft );
}
}
editorCard.root.hidden = false;
}


function closePanel( returnFocus ) {
function activateVisualEditor() {
isOpen = false;
var target = window.ve && ve.init && ve.init.target;
panel.hidden = true;
if ( !target || typeof target.getSurface !== 'function' || typeof target.getDocToSave !== 'function' || typeof target.serialize !== 'function' ) {
badge.setAttribute( 'aria-expanded', 'false' );
return;
hoverBlocked = true;
}
hoverMayOpenAfter = Date.now() + 800;
var surface = target.getSurface();
rememberDismissal( true );
if ( !surface || !surface.getModel || !surface.getModel().getDocument ) {
if ( returnFocus ) {
return;
suppressFocus = true;
}
badge.focus( { preventScroll: true } );
var doc = surface.getModel().getDocument();
suppressFocus = false;
activeTarget = target;
if ( activeDocument !== doc ) {
if ( activeDocument && typeof activeDocument.off === 'function' ) {
activeDocument.off( 'transact', markDraftChanged );
}
}
activeDocument = doc;
activeDocument.on( 'transact', markDraftChanged );
markDraftChanged();
}
}
ensureEditor();
if ( viewCard ) {
viewCard.root.hidden = true;
}
}


close.addEventListener( 'click', function () {
function initialize() {
closePanel( true );
if ( [ 0, 118 ].indexOf( Number( mw.config.get( 'wgNamespaceNumber' ) ) ) === -1 ) {
return;
}
initializeView();
sourceInput = document.getElementById( 'wpTextbox1' );
if ( sourceInput && !sourceInput.readOnly && !sourceInput.disabled ) {
ensureEditor();
sourceInput.addEventListener( 'input', markDraftChanged );
window.jQuery( sourceInput ).on( 'change', markDraftChanged );
}
mw.hook( 'ext.CodeMirror.input' ).add( markDraftChanged );
mw.hook( 've.activationStart' ).add( function () {
if ( viewCard ) {
viewCard.root.hidden = true;
}
if ( editorCard ) {
editorCard.root.hidden = true;
}
} );
} );
badge.addEventListener( 'click', function () {
mw.hook( 've.activationComplete' ).add( activateVisualEditor );
openPanel( true );
mw.hook( 've.deactivationComplete' ).add( function () {
} );
activeTarget = null;
badge.addEventListener( 'focus', function () {
if ( activeDocument && typeof activeDocument.off === 'function' ) {
if ( !suppressFocus ) {
activeDocument.off( 'transact', markDraftChanged );
openPanel( false );
}
}
} );
activeDocument = null;
badge.addEventListener( 'mouseenter', function () {
requestSerial++;
if ( !hoverBlocked && Date.now() >= hoverMayOpenAfter &&
if ( editorCard ) {
window.matchMedia( '(hover: hover)' ).matches ) {
editorCard.root.hidden = true;
openPanel( false );
editorCard.badge.disabled = false;
editorCard.badge.textContent = 'Check writing';
}
}
} );
if ( viewCard ) {
root.addEventListener( 'mouseleave', function () {
var revision = Number( mw.config.get( 'wgRevisionId' ) );
hoverBlocked = false;
var currentRevision = Number( mw.config.get( 'wgCurRevisionId' ) ) || revision;
} );
if ( revision !== viewCard.revision || currentRevision !== viewCard.currentRevision ) {
document.addEventListener( 'keydown', function ( event ) {
viewCard.body.textContent = '';
if ( event.key === 'Escape' && isOpen && !suspended ) {
viewCard.body.appendChild( element( 'p', 'sb-article-review__stale',
var focusInside = root.contains( document.activeElement );
'The saved page changed in the editor. Reload this page to check the new saved revision.' ) );
closePanel( focusInside );
paragraphLink( viewCard.body, 'Refresh saved-page checks', mw.util.getUrl( mw.config.get( 'wgPageName' ) ) );
if ( focusInside ) {
viewCard.badge.textContent = 'Refresh article review';
event.preventDefault();
}
}
viewCard.root.hidden = false;
}
}
} );
} );
 
if ( document.documentElement.classList.contains( 've-activated' ) || document.body.classList.contains( 've-activated' ) ) {
function updateEditorVisibility() {
activateVisualEditor();
var editorClasses = [ 've-active', 've-activated', 've-activating', 've-deactivating' ];
suspended = editorClasses.some( function ( name ) {
return document.documentElement.classList.contains( name ) || document.body.classList.contains( name );
} );
root.hidden = suspended;
}
mw.hook( 've.activationStart' ).add( function () {
suspended = true;
root.hidden = true;
} );
mw.hook( 've.deactivationComplete' ).add( updateEditorVisibility );
if ( window.MutationObserver ) {
var observer = new MutationObserver( updateEditorVisibility );
observer.observe( document.documentElement, { attributes: true, attributeFilter: [ 'class' ] } );
observer.observe( document.body, { attributes: true, attributeFilter: [ 'class' ] } );
}
 
// Keep review UI outside article content and VisualEditor serialization.
document.body.appendChild( root );
panel.hidden = dismissed;
if ( !dismissed ) {
openPanel( false );
}
}
updateEditorVisibility();
}
}


mw.loader.using( 'mediawiki.util' ).then( function () {
mw.loader.using( [ 'mediawiki.util', 'mediawiki.api', 'jquery.textSelection' ] ).then( function () {
if ( document.readyState === 'loading' ) {
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', initialize, { once: true } );
document.addEventListener( 'DOMContentLoaded', initialize, { once: true } );

Latest revision as of 03:37, 3 November 2026

( function () {
	'use strict';

	var rootId = 'shalombux-article-review';
	var editorId = 'mayapedia-writing-review-editor';
	var guideUrl = 'https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing';
	var viewCard, editorCard, activeTarget, sourceInput, activeDocument;
	var editorGeneration = 0, requestSerial = 0;
	var maxFindings = 80;

	function element( name, className, text ) {
		var node = document.createElement( name );
		if ( className ) {
			node.className = className;
		}
		if ( text !== undefined ) {
			node.textContent = String( text );
		}
		return node;
	}

	function safeLink( label, address, newWindow ) {
		var url;
		try {
			url = new URL( address, window.location.href );
		} catch ( error ) {
			return null;
		}
		if ( url.protocol !== 'https:' && url.protocol !== 'http:' ) {
			return null;
		}
		var link = element( 'a', '', label );
		link.href = url.href;
		if ( newWindow ) {
			link.target = '_blank';
			link.rel = 'noopener noreferrer';
		}
		return link;
	}

	function paragraphLink( parent, label, address ) {
		var link = safeLink( label, address, true );
		if ( link ) {
			parent.appendChild( link );
		}
	}

	function makeCard( id, label, storageKey, editor ) {
		var root = element( 'div', 'sb-article-review' + ( editor ? ' sb-article-review--editor' : '' ) );
		root.id = id;
		var badge = element( 'button', 'sb-article-review__badge', label );
		badge.type = 'button';
		badge.setAttribute( 'aria-controls', id + '-panel' );
		badge.setAttribute( 'aria-expanded', 'false' );
		badge.title = editor ? 'Check the current unsaved writing' : 'Show the article review';
		var panel = element( 'section', 'sb-article-review__panel' );
		panel.id = id + '-panel';
		panel.hidden = true;
		panel.setAttribute( 'aria-labelledby', id + '-heading' );
		var header = element( 'div', 'sb-article-review__header' );
		var heading = element( 'h2', 'sb-article-review__heading', editor ? 'Writing review' : 'Article review' );
		heading.id = id + '-heading';
		var close = element( 'button', 'sb-article-review__close', '\u00d7' );
		close.type = 'button';
		close.setAttribute( 'aria-label', 'Close ' + heading.textContent.toLowerCase() );
		close.title = 'Close review (Escape)';
		header.appendChild( heading );
		header.appendChild( close );
		panel.appendChild( header );
		var body = element( 'div', 'sb-article-review__body' );
		panel.appendChild( body );
		root.appendChild( badge );
		root.appendChild( panel );
		var dismissed = false, hoverBlocked = false, hoverAfter = 0, suppressFocus = false;
		try {
			dismissed = storageKey && window.localStorage.getItem( storageKey ) === 'dismissed';
		} catch ( error ) {
			// The card also works when browser storage is unavailable.
		}
		function remember( value ) {
			if ( !storageKey ) {
				return;
			}
			try {
				if ( value ) {
					window.localStorage.setItem( storageKey, 'dismissed' );
				} else {
					window.localStorage.removeItem( storageKey );
				}
			} catch ( error ) {
				// Dismissal still works until this page is closed.
			}
		}
		function open( rememberOpen ) {
			if ( root.hidden ) {
				return;
			}
			panel.hidden = false;
			badge.setAttribute( 'aria-expanded', 'true' );
			if ( rememberOpen ) {
				remember( false );
			}
		}
		function closePanel( returnFocus ) {
			panel.hidden = true;
			badge.setAttribute( 'aria-expanded', 'false' );
			hoverBlocked = true;
			hoverAfter = Date.now() + 800;
			remember( true );
			if ( returnFocus ) {
				suppressFocus = true;
				badge.focus( { preventScroll: true } );
				suppressFocus = false;
			}
		}
		close.addEventListener( 'click', function () { closePanel( true ); } );
		badge.addEventListener( 'click', function () { open( true ); } );
		if ( !editor ) {
			badge.addEventListener( 'focus', function () {
				if ( !suppressFocus ) {
					open( false );
				}
			} );
			badge.addEventListener( 'mouseenter', function () {
				if ( !hoverBlocked && Date.now() >= hoverAfter && window.matchMedia( '(hover: hover)' ).matches ) {
					open( false );
				}
			} );
		}
		root.addEventListener( 'mouseleave', function () { hoverBlocked = false; } );
		document.addEventListener( 'keydown', function ( event ) {
			if ( event.key === 'Escape' && !root.hidden && !panel.hidden ) {
				var focusInside = root.contains( document.activeElement );
				closePanel( focusInside );
				if ( focusInside ) {
					event.preventDefault();
				}
			}
		} );
		// The controls are outside article content and editor serialization.
		document.body.appendChild( root );
		return { root: root, body: body, badge: badge, panel: panel, open: open, dismissed: dismissed };
	}

	function renderManual( body, review, data, viewedRevision ) {
		if ( !review || !Number( review.reviewedRevision ) ) {
			return;
		}
		var revision = Number( review.reviewedRevision );
		body.appendChild( element( 'h3', 'sb-article-review__section', 'Recorded article review' ) );
		body.appendChild( element( 'p', 'sb-article-review__date', 'Reviewed ' + String( data.reviewDate ) + ' \u00b7 revision ' + revision ) );
		if ( viewedRevision !== revision || Number( mw.config.get( 'wgCurRevisionId' ) ) > revision ) {
			body.appendChild( element( 'p', 'sb-article-review__stale', viewedRevision !== revision ?
				'These recorded findings cover another revision and may no longer apply. Writing checks below cover the revision identified in that section.' :
				'A newer revision exists. These recorded findings cover the older revision shown here.' ) );
		}
		var revisionParagraph = element( 'p', 'sb-article-review__revision' );
		paragraphLink( revisionParagraph, 'View reviewed revision ' + revision,
			mw.util.getUrl( review.title || mw.config.get( 'wgPageName' ), { oldid: revision } ) );
		body.appendChild( revisionParagraph );
		var issues = Array.isArray( review.issues ) ? review.issues : [];
		if ( issues.length ) {
			var list = element( 'ul', 'sb-article-review__issues' );
			issues.slice( 0, maxFindings ).forEach( function ( issue ) {
				if ( !issue || typeof issue !== 'object' ) {
					return;
				}
				var kind = [ 'error', 'review', 'note' ].indexOf( issue.kind ) !== -1 ? issue.kind : 'note';
				var item = element( 'li', 'sb-article-review__issue sb-article-review__issue--' + kind );
				item.appendChild( element( 'span', 'sb-article-review__kind', { error: 'Verified error', review: 'Review needed', note: 'Note' }[ kind ] ) );
				item.appendChild( element( 'strong', 'sb-article-review__issue-title', issue.title || 'Review note' ) );
				item.appendChild( element( 'p', 'sb-article-review__detail', issue.detail || '' ) );
				if ( Array.isArray( issue.links ) ) {
					var links = element( 'div', 'sb-article-review__links' );
					issue.links.forEach( function ( source ) {
						if ( source && typeof source.url === 'string' ) {
							paragraphLink( links, source.label || 'Source', source.url );
						}
					} );
					item.appendChild( links );
				}
				list.appendChild( item );
			} );
			body.appendChild( list );
		} else {
			body.appendChild( element( 'p', '', 'No article-specific issue was recorded in this limited review.' ) );
		}
		var scope = element( 'details', 'sb-article-review__scope' );
		scope.appendChild( element( 'summary', '', 'Scope of the recorded review' ) );
		scope.appendChild( element( 'p', 'sb-article-review__coverage', 'The following notes apply only to reviewed revision ' + revision + '.' ) );
		if ( review.unicodeNote ) {
			scope.appendChild( element( 'p', 'sb-article-review__unicode', review.unicodeNote ) );
		}
		scope.appendChild( element( 'p', 'sb-article-review__coverage', review.sourceReview === 'spot-checked' ?
			'Selected citations were spot-checked; other sources remain unverified.' : 'Citation destinations were not checked for this article.' ) );
		scope.appendChild( element( 'p', 'sb-article-review__coverage', 'Policy links provide review guidance; this was not a complete policy assessment.' ) );
		body.appendChild( scope );
	}

	function sourceContents() {
		if ( !sourceInput ) {
			return null;
		}
		var input = window.jQuery( sourceInput );
		return typeof input.textSelection === 'function' ? input.textSelection( 'getContents' ) : sourceInput.value;
	}

	function selectFinding( finding, checkedText ) {
		if ( activeTarget || !sourceInput || sourceContents() !== checkedText ) {
			return;
		}
		var encoded = new TextEncoder().encode( checkedText );
		var startByte = Math.max( 0, Math.min( encoded.length, Number( finding.offset ) || 0 ) );
		var endByte = Math.max( startByte, Math.min( encoded.length, startByte + ( Number( finding.length ) || 0 ) ) );
		var decoder = new TextDecoder();
		var start = decoder.decode( encoded.slice( 0, startByte ) ).length;
		var end = decoder.decode( encoded.slice( 0, endByte ) ).length;
		var input = window.jQuery( sourceInput );
		if ( typeof input.textSelection === 'function' ) {
			input.textSelection( 'setSelection', { start: start, end: end } );
			input.textSelection( 'scrollToCaretPosition' );
		} else {
			sourceInput.setSelectionRange( start, end );
		}
		sourceInput.focus();
	}

	function renderWriting( body, result, options ) {
		var findings = Array.isArray( result.findings ) ? result.findings : [];
		body.appendChild( element( 'h3', 'sb-article-review__section', 'Writing checks' ) );
		body.appendChild( element( 'p', 'sb-article-review__date', options.editor ?
			'Checked an unsaved snapshot. Nothing has been saved or changed.' :
			'Checked revision ' + Number( options.revision ) ) );
		if ( options.editor ) {
			var stale = element( 'p', 'sb-article-review__stale sb-writing-review__stale',
				'The draft changed after this check. Check writing again to review the current text.' );
			stale.hidden = !options.stale;
			body.appendChild( stale );
		}
		body.appendChild( element( 'p', 'sb-article-review__limits',
			'These are prompts for human review, not proof of AI authorship or policy violations. Saving is never blocked.' ) );
		if ( !findings.length ) {
			body.appendChild( element( 'p', 'sb-writing-review__empty', result.coverage && result.coverage.status === 'scanned' ?
				'No matching signs were found by these checks. This does not establish who wrote the text or whether it is accurate.' :
				'No findings are available from this check. See the coverage notes below.' ) );
		} else {
			var list = element( 'ul', 'sb-article-review__issues sb-writing-review__findings' );
			findings.slice( 0, maxFindings ).forEach( function ( finding ) {
				if ( !finding || typeof finding !== 'object' ) {
					return;
				}
				var kind = finding.severity === 'review' ? 'review' : 'note';
				var item = element( 'li', 'sb-article-review__issue sb-article-review__issue--' + kind );
				item.appendChild( element( 'span', 'sb-article-review__kind', kind === 'review' ? 'Review this passage' : 'Context check' ) );
				item.appendChild( element( 'strong', 'sb-article-review__issue-title', finding.title || 'Writing sign' ) );
				item.appendChild( element( 'p', 'sb-article-review__detail', finding.explanation || '' ) );
				if ( typeof finding.evidence === 'string' && finding.evidence ) {
					item.appendChild( element( 'blockquote', 'sb-writing-review__evidence', finding.evidence.slice( 0, 1200 ) ) );
				}
				var links = element( 'div', 'sb-article-review__links' );
				if ( Number( finding.line ) > 0 ) {
					if ( options.editor && sourceInput && !activeTarget ) {
						var select = element( 'button', 'sb-writing-review__source', 'Select source line ' + Number( finding.line ) );
						select.type = 'button';
						select.disabled = !!options.stale;
						select.addEventListener( 'click', function () { selectFinding( finding, options.checkedText ); } );
						links.appendChild( select );
					} else if ( options.editor ) {
						links.appendChild( element( 'span', '', 'Draft source line ' + Number( finding.line ) ) );
					} else {
						paragraphLink( links, 'Source line ' + Number( finding.line ), mw.util.getUrl(
							options.title || mw.config.get( 'wgPageName' ), { action: 'edit', oldid: Number( options.revision ) } ) );
					}
				}
				paragraphLink( links, 'Why this is flagged', typeof finding.guide === 'string' ? finding.guide : guideUrl );
				item.appendChild( links );
				list.appendChild( item );
			} );
			body.appendChild( list );
		}
		if ( result.truncated || findings.length > maxFindings ) {
			body.appendChild( element( 'p', 'sb-article-review__stale', 'This report is limited in size. Additional passages may need review.' ) );
		}
		var scope = element( 'details', 'sb-article-review__scope' );
		scope.appendChild( element( 'summary', '', 'What the checks cover' ) );
		if ( result.coverage && result.coverage.reason ) {
			scope.appendChild( element( 'p', '', result.coverage.reason ) );
		}
		var limitations = result.coverage && result.coverage.limitations;
		if ( Array.isArray( limitations ) ) {
			limitations.slice( 0, 12 ).forEach( function ( note ) {
				if ( typeof note === 'string' ) {
					scope.appendChild( element( 'p', '', note ) );
				}
			} );
		} else if ( typeof limitations === 'string' ) {
			scope.appendChild( element( 'p', '', limitations ) );
		}
		scope.appendChild( element( 'p', '', 'Checks use this wiki\u2019s text. They do not establish plagiarism, verify every source, or identify the writer.' ) );
		paragraphLink( scope, 'Wikipedia: Signs of AI writing', guideUrl );
		body.appendChild( scope );
	}

	function initializeView() {
		var data = window.shalombuxArticleReviewData || {};
		var scan = mw.config.get( 'wgMayapediaWritingReview' );
		var pageId = Number( mw.config.get( 'wgArticleId' ) );
		var revision = Number( mw.config.get( 'wgRevisionId' ) );
		var query = new URLSearchParams( window.location.search );
		if ( document.getElementById( rootId ) || pageId <= 0 || revision <= 0 ||
			mw.config.get( 'wgAction' ) !== 'view' || query.has( 'diff' ) ||
			mw.config.get( 'wgDiffOldId' ) || mw.config.get( 'wgDiffNewId' ) ) {
			return;
		}
		var review = data.pages && data.pages[ String( pageId ) ];
		// Never present another page/revision's automated findings as current.
		if ( scan && ( Number( scan.revision ) !== revision || Number( scan.pageId ) !== pageId ) ) {
			scan = null;
		}
		if ( !scan && !review ) {
			return;
		}
		var count = scan && Array.isArray( scan.findings ) ? scan.findings.length : 0;
		var manualCount = review && Array.isArray( review.issues ) ? review.issues.length : 0;
		var storageKey = 'shalombux-article-review:writing:' + String( data.version || '' ) + ':' +
			String( scan && scan.version || '' ) + ':' + pageId + ':' + revision + ':' + String( review && review.reviewedRevision || '' );
		viewCard = makeCard( rootId, count + manualCount ? 'Article review (' + ( count + manualCount ) + ')' : 'Article review', storageKey, false );
		viewCard.revision = revision;
		viewCard.currentRevision = Number( mw.config.get( 'wgCurRevisionId' ) ) || revision;
		renderManual( viewCard.body, review, data, revision );
		if ( scan ) {
			renderWriting( viewCard.body, scan, { revision: revision, title: scan.title } );
		} else {
			viewCard.body.appendChild( element( 'p', 'sb-article-review__coverage', 'Automated writing checks are unavailable for this view.' ) );
		}
		if ( ( count || manualCount ) && !viewCard.dismissed ) {
			viewCard.open( false );
		}
	}

	function markDraftChanged() {
		editorGeneration++;
		if ( editorCard ) {
			var stale = editorCard.body.querySelector( '.sb-writing-review__stale' );
			if ( stale ) {
				stale.hidden = false;
			}
			editorCard.body.querySelectorAll( '.sb-writing-review__source' ).forEach( function ( button ) { button.disabled = true; } );
		}
	}

	function currentDraft() {
		if ( activeTarget ) {
			var doc = activeTarget.getDocToSave();
			if ( typeof doc === 'string' ) {
				return Promise.resolve( doc );
			}
			return Promise.resolve( activeTarget.serialize( doc ) ).then( function ( data ) {
				if ( !data || typeof data.content !== 'string' ) {
					throw new Error( 'serialization' );
				}
				return data.content;
			} );
		}
		var text = sourceContents();
		return typeof text === 'string' ? Promise.resolve( text ) : Promise.reject( new Error( 'editor' ) );
	}

	function checkDraft() {
		if ( !editorCard || editorCard.badge.disabled ) {
			return;
		}
		var serial = ++requestSerial, generation = editorGeneration, checkedText;
		editorCard.badge.disabled = true;
		editorCard.badge.textContent = 'Checking writing\u2026';
		editorCard.body.textContent = '';
		editorCard.body.appendChild( element( 'p', 'sb-writing-review__status', 'Checking the current unsaved text on Mayapedia\u2026' ) );
		editorCard.body.setAttribute( 'aria-busy', 'true' );
		editorCard.open( false );
		currentDraft().then( function ( text ) {
			checkedText = text;
			return new mw.Api().postWithToken( 'csrf', { action: 'writingreview', text: text, format: 'json', formatversion: 2 } );
		} ).then( function ( response ) {
			if ( serial !== requestSerial || !editorCard ) {
				return;
			}
			if ( !response || !response.writingreview ) {
				throw new Error( 'response' );
			}
			editorCard.body.textContent = '';
			renderWriting( editorCard.body, response.writingreview, { editor: true, checkedText: checkedText,
				stale: generation !== editorGeneration || ( !activeTarget && sourceContents() !== checkedText ) } );
		} ).catch( function () {
			if ( serial === requestSerial && editorCard ) {
				editorCard.body.textContent = '';
				editorCard.body.appendChild( element( 'p', 'sb-article-review__stale',
					'The writing check could not finish. Your draft is unchanged and you can still save. Try Check writing again.' ) );
			}
		} ).then( function () {
			if ( serial === requestSerial && editorCard ) {
				editorCard.badge.disabled = false;
				editorCard.badge.textContent = 'Check writing';
				editorCard.body.removeAttribute( 'aria-busy' );
			}
		} );
	}

	function ensureEditor() {
		if ( !editorCard ) {
			editorCard = makeCard( editorId, 'Check writing', null, true );
			editorCard.badge.addEventListener( 'click', checkDraft );
		}
		editorCard.root.hidden = false;
	}

	function activateVisualEditor() {
		var target = window.ve && ve.init && ve.init.target;
		if ( !target || typeof target.getSurface !== 'function' || typeof target.getDocToSave !== 'function' || typeof target.serialize !== 'function' ) {
			return;
		}
		var surface = target.getSurface();
		if ( !surface || !surface.getModel || !surface.getModel().getDocument ) {
			return;
		}
		var doc = surface.getModel().getDocument();
		activeTarget = target;
		if ( activeDocument !== doc ) {
			if ( activeDocument && typeof activeDocument.off === 'function' ) {
				activeDocument.off( 'transact', markDraftChanged );
			}
			activeDocument = doc;
			activeDocument.on( 'transact', markDraftChanged );
			markDraftChanged();
		}
		ensureEditor();
		if ( viewCard ) {
			viewCard.root.hidden = true;
		}
	}

	function initialize() {
		if ( [ 0, 118 ].indexOf( Number( mw.config.get( 'wgNamespaceNumber' ) ) ) === -1 ) {
			return;
		}
		initializeView();
		sourceInput = document.getElementById( 'wpTextbox1' );
		if ( sourceInput && !sourceInput.readOnly && !sourceInput.disabled ) {
			ensureEditor();
			sourceInput.addEventListener( 'input', markDraftChanged );
			window.jQuery( sourceInput ).on( 'change', markDraftChanged );
		}
		mw.hook( 'ext.CodeMirror.input' ).add( markDraftChanged );
		mw.hook( 've.activationStart' ).add( function () {
			if ( viewCard ) {
				viewCard.root.hidden = true;
			}
			if ( editorCard ) {
				editorCard.root.hidden = true;
			}
		} );
		mw.hook( 've.activationComplete' ).add( activateVisualEditor );
		mw.hook( 've.deactivationComplete' ).add( function () {
			activeTarget = null;
			if ( activeDocument && typeof activeDocument.off === 'function' ) {
				activeDocument.off( 'transact', markDraftChanged );
			}
			activeDocument = null;
			requestSerial++;
			if ( editorCard ) {
				editorCard.root.hidden = true;
				editorCard.badge.disabled = false;
				editorCard.badge.textContent = 'Check writing';
			}
			if ( viewCard ) {
				var revision = Number( mw.config.get( 'wgRevisionId' ) );
				var currentRevision = Number( mw.config.get( 'wgCurRevisionId' ) ) || revision;
				if ( revision !== viewCard.revision || currentRevision !== viewCard.currentRevision ) {
					viewCard.body.textContent = '';
					viewCard.body.appendChild( element( 'p', 'sb-article-review__stale',
						'The saved page changed in the editor. Reload this page to check the new saved revision.' ) );
					paragraphLink( viewCard.body, 'Refresh saved-page checks', mw.util.getUrl( mw.config.get( 'wgPageName' ) ) );
					viewCard.badge.textContent = 'Refresh article review';
				}
				viewCard.root.hidden = false;
			}
		} );
		if ( document.documentElement.classList.contains( 've-activated' ) || document.body.classList.contains( 've-activated' ) ) {
			activateVisualEditor();
		}
	}

	mw.loader.using( [ 'mediawiki.util', 'mediawiki.api', 'jquery.textSelection' ] ).then( function () {
		if ( document.readyState === 'loading' ) {
			document.addEventListener( 'DOMContentLoaded', initialize, { once: true } );
		} else {
			initialize();
		}
	} );
}() );