User:He7d3r/Tools/DraftAndArticleQualityCore.js
Appearance
From Meta, a Wikimedia project coordination wiki
This is an archived version of this page, as edited by He7d3r (talk | contribs) at 18:15, 2 March 2024 (Pass li.attr('data-mw-revid') to formatScoreHeader (see User talk:EpochFail/ArticleQuality-system.js#URLs with "undefined")). It may differ significantly from the current version .
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
mw.loader.using(['mediawiki.api']).done( (function($,mw){ vardefaultOptions={ parameters:{ format:'json' }, ajax:{ timeout:30*1000,// 30 seconds dataType:'json', type:'GET' }, score:{ batchSize:50, maxWorkers:4 } }; /** * Constructor to create a pool of workers to request a set of model-scores * from the ORES api. This object will allow scoring jobs to be * transparently started within a worker pool. The worker pool will then * manage all requests in order to comply with the options provided * (e.g. batchSize and maxWorkers). * * var ores = require('ext.ores.api'); * aqPool = ores.Pool([ "articlequality" ], {batchSize: 50, maxWorkers: 3}) * * aqPool.score(214412) * .done(function(scoreDoc){...}) * aqPool.score(214413) * .done(function(scoreDoc){...}) * * @constructor * @param {Object} [oresApi] An OresApi object to use for querying * @param {Array} [models] A list of models to query for in each request * @param {Object} [options] A set of options for querying. See defaultOptions.score above */ varOresScoreBatcherPool=function(oresApi,models,options){ this.oresApi=oresApi; this.models=models; this.batchSize=options.batchSize||defaultOptions.score.batchSize; this.maxWorkers=options.maxWorkers||defaultOptions.score.maxWorkers; this.liveWorkers=0; this.taskQueue=[]; this.tasksQueued=$.Deferred() .progress(this.ensureWorkers.bind(this)); }; OresScoreBatcherPool.prototype={ /** * Add a score job to the queue and return a promise for the result. * * @param {number} [revId] A revision to score with the given model */ score:function(revId){ varresultDfd=$.Deferred(), task={revId:revId,resultDfd:resultDfd}; this.taskQueue.push(task); this.tasksQueued.notify(); returnresultDfd.promise(); }, /** * Process a batch and recurse until the taskQueue is empty */ processTaskBatches:function(){ // Get a batch to process batch=this.taskQueue.splice(0,this.batchSize); // If there's stuff to process, start a new score batch processing job and // recurse when it finishes. if(batch.length){ this.scoreBatch(batch) .fail(function(){ for(vari=0;i<batch.length;i++){ batch[i].resultDfd.error.apply(null,arguments); } }) .always(this.processTaskBatches.bind(this)); }else{ // shut down worker and decrement the worker count this.liveWorkers-=1; console.debug("Shutting down worker"); } }, /** * Generate a set of scores for a batch. Results will be sent to specific * deferred result objects. */ scoreBatch:function(batch){ varbatchDfd=$.Deferred(), revIds=[]; for(vari=0;i<batch.length;i++){ revIds.push(batch[i].revId); } this.oresApi.get({revids:revIds,models:this.models}) .done(function(responseDoc){ for(vari=0;i<batch.length;i++){ varscoreDoc=responseDoc[this.oresApi.options.dbname].scores[batch[i].revId]; batch[i].resultDfd.resolve(scoreDoc); } }.bind(this)) .fail(function(){ for(vari=0;i<batch.length;i++){ batch[i].resultDfd.error.apply(null,arguments); } }) .always(function(){batchDfd.resolve()}); returnbatchDfd.promise(); }, /** * Ensure that workers are running. This method is used when a task is * added to the queue to make sure that the workers are started/restarted * if necessary. Note that a 50ms delay is implemented for starting a * worker process to ensure that tasks are given enough time to enqueue * before batch processing starts. */ ensureWorkers:function(){ while(this.liveWorkers<this.maxWorkers){ console.debug("Starting up worker"); setTimeout( function(){this.processTaskBatches()}.bind(this), 50); this.liveWorkers+=1; } } }; /** * Constructor to create an object to interact with the API of an ORES server. * OresApi objects represent the API of a particular ORES server. * * var ores = require('ext.ores.api'); * ores.get( { * revids: [1234, 1235], * models: [ 'damaging', 'articlequality' ] // same effect as 'damaging|articlequality' * } ).done( function ( data ) { * console.log( data ); * } ); * * @constructor * @param {Object} [options] See #defaultOptions documentation above. */ varOresApi=function(options){ options.parameters=$.extend({},defaultOptions.parameters,options.parameters); options.ajax=$.extend({},defaultOptions.ajax,options.ajax); options.score=$.extend({},defaultOptions.score,options.score); if(options.ajax.url){ options.ajax.url=String(options.ajax.url); }else{ options.ajax.url=options.host+'/v3/scores/'+options.dbname; } this.options=options; this.requests=[]; }; OresApi.prototype={ /** * Abort all unfinished requests issued by this Api object. * * @method */ abort:function(){ this.requests.forEach(function(request){ if(request){ request.abort(); } }); }, /** * Massage parameters from the nice format we accept into a format suitable for the API. * * @private * @param {Object} parameters (modified in-place) */ preprocessParameters:function(parameters){ varkey; for(keyinparameters){ if(Array.isArray(parameters[key])){ parameters[key]=parameters[key].join('|'); }elseif(parameters[key]===false||parameters[key]===undefined){ // Boolean values are only false when not given at all deleteparameters[key]; } } }, /** * Perform an API call. * * @param {Object} parameters * @return {jQuery.Promise} Done: API response data and the jqXHR object. * Fail: Error code */ get:function(parameters){ varrequestIndex, api=this, apiDeferred=$.Deferred(), xhr, ajaxOptions; parameters=$.extend({},this.options.parameters,parameters); ajaxOptions=$.extend({},this.options.ajax); this.preprocessParameters(parameters); ajaxOptions.data=$.param(parameters); xhr=$.ajax(ajaxOptions) .done(function(result,textStatus,jqXHR){ varcode; if(result.error){ code=result.error.code===undefined?'unknown':result.error.code; apiDeferred.reject(code,result,jqXHR); }else{ apiDeferred.resolve(result,jqXHR); } }); requestIndex=this.requests.length; this.requests.push(xhr); xhr.always(function(){ api.requests[requestIndex]=null; }); returnapiDeferred.promise({abort:xhr.abort}).fail(function(code,details){ if(!(code==='http'&&details&&details.textStatus==='abort')){ mw.log('OresApi error: ',code,details); } }); }, /** * Create a new OresScoreBatcherPool using this api session. * * @param {Array} [models] A list of models to query for in each request * @param {Object} [options] A set of options for querying. See defaultOptions.score above */ pool:function(models,options){ returnnewOresScoreBatcherPool(this,models,options); } }; varArticleQuality=function(options){ this.weights=options.weights; this.names=options.names; this.assessment_system=options.assessment_system; this.modelName=options.modelName||"articlequality"; this.weightedSumPlaceholder=options.weightedSumPlaceholder; this.mwApi=newmw.Api(); this.oresHost=options.ores_host; this.oresDbname=options.dbname; this.oresApi=newOresApi({host:options.ores_host,dbname:options.dbname}); this.aqPool=this.oresApi.pool(this.modelName,{batchSize:10}); }; ArticleQuality.prototype={ computeWeightedSum:function(score){ varclsProba=score.probability; varweightedSum=0; for(varclsinclsProba){ if(clsProba.hasOwnProperty(cls)){ varproba=clsProba[cls]; weightedSum+=proba*this.weights[cls]; } } returnweightedSum; }, computeWeightedProportion:function(score){ varweightedSum=this.computeWeightedSum(score); returnweightedSum/Math.max.apply(null,Object.values(this.weights)); }, extractPrediction:function(score){ returnscore.prediction; }, parseText:function(text){ vardfd=jQuery.Deferred(); this.mwApi.get({action:"parse",text:text,contentmodel:"wikitext",formatversion:2,prop:"text",disablelimitreport:true}) .done(function(data){dfd.resolve($(data.parse.text).find('p').html())}) .fail(function(error){dfd.reject(error)}); returndfd.promise(); }, getCurrentRevId:function(title){ vardfd=jQuery.Deferred(); this.mwApi.get({action:'query',prop:'revisions',titles:title,rvprop:'ids',formatversion:2}) .done(function(data){ if(data.query.pages[0].missing){ dfd.reject('Missing page: '+data.query.pages[0].title); }else{ dfd.resolve(data.query.pages[0].revisions[0].revid); } }) .fail(function(error){dfd.reject(error)}); returndfd.promise(); }, oresScore:function(revId){ vardfd=$.Deferred(); this.aqPool.score(revId) .done(function(scoreDoc){dfd.resolve(scoreDoc[this.modelName].score,revId)}.bind(this)) .fail(function(){dfd.error.apply(null,arguments)}); returndfd.promise(); }, getAndRenderScoreHeader:function(){ varrevId=mw.config.get('wgRevisionId'); this.oresScore(revId) .done(this.renderScoreHeader.bind(this)) .fail(function(error){console.error(error)}); }, renderScoreHeader:function(score,revId){ varrawText=this.formatScoreHeader(score,revId); varv2022=document.body.classList.contains('skin-vector-2022'); varqualityBlock=$('<div>').addClass("article_quality"); if(v2022){ qualityBlock.addClass('mw-indicator'); $('.mw-indicators').prepend(qualityBlock); }else{ $('#bodyContent').prepend(qualityBlock); } this.parseText(rawText) .done(function(html){qualityBlock.html(html)}) .fail(function(error){console.error(error)}); }, formatScoreHeader:function(score,revId){ varprediction=this.extractPrediction(score); if(!this.weightedSumPlaceholder){ varweightedSum=this.computeWeightedSum(score); varroundedWeightedSum=Math.round(weightedSum*100)/100; varlocalizedWeightedSum=roundedWeightedSum.toLocaleString( mw.config.get("wgULSAcceptLanguageList",window.navigator.languages||[])[0]); }else{ varlocalizedWeightedSum=this.weightedSumPlaceholder; } varanchoredWeightedSum='['+this.oresHost+"/v3/scores/"+ this.oresDbname+"/"+revId+"/articlequality "+ localizedWeightedSum+"]" returnthis.assessment_system+": "+ this.names[prediction]+" ("+ anchoredWeightedSum+")"; }, renderScoreLink:function(score,span){ varprediction=this.extractPrediction(score); this.parseText(this.names[prediction]) .done(function(html){span.prepend(html)}) .fail(function(error){console.error(error)}); }, getAndRenderScoreLink:function(revId,span){ this.oresScore(revId) .done(function(score){this.renderScoreLink(score,span)}.bind(this)) .fail(function(error){console.error(error)}); }, addScoresToArticleLinks:function(){ $("span.ores-wp10-prediction, span.ores-quality-prediction").each(function(i,element){ varspan=$(element); varanchor=span.find('a'); varpageTitle=anchor.attr('title'); this.getCurrentRevId(pageTitle) .done(function(revId){this.getAndRenderScoreLink(revId,span)}.bind(this)) .fail(function(error){console.error(error)}); }.bind(this)); }, renderHistoryScore:function(li,score){ varlevel=Math.round(this.computeWeightedSum(score)); varweightedProportion=this.computeWeightedProportion(score); varqualityPredictionNode=$("<div>").addClass("qualityprediction") .addClass("level_"+level) .append($("<div>").addClass("bar").css("width",Math.round(weightedProportion*100)+"%").append(" ")) .attr('title',this.formatScoreHeader(score,li.attr('data-mw-revid'))); li.prepend(qualityPredictionNode); }, getAndRenderHistoryScore:function(li){ varrevId=li.attr('data-mw-revid'); this.oresScore(revId) .done(function(score){this.renderHistoryScore(li,score)}.bind(this)) .fail(function(error){console.error(error)}); }, getAndRenderHistoryScores:function(){ varrevisionNodes=$('#pagehistory li'); revisionNodes.each(function(i,element){this.getAndRenderHistoryScore($(element))}.bind(this)); } } varDraftQuality=function(options){ // this.weights = options.weights; this.names=options.names; this.assessment_system=options.assessment_system; this.modelName=options.modelName||"draftquality"; this.probaPlaceholder=options.probaPlaceholder // this.mwApi = new mw.Api(); this.oresHost=options.ores_host this.oresDbname=options.dbname this.oresApi=newOresApi({host:options.ores_host,dbname:options.dbname}); this.dqPool=this.oresApi.pool(this.modelName,{batchSize:10}); }; DraftQuality.prototype={ // computeWeightedSum: function(score){ // var clsProba = score.probability; // var weightedSum = 0; // for (var cls in clsProba) { // if (clsProba.hasOwnProperty(cls)) { // var proba = clsProba[cls]; // weightedSum += proba * this.weights[cls]; // } // } // return weightedSum; // }, // computeWeightedProportion: function(score){ // var weightedSum = this.computeWeightedSum(score); // return weightedSum / Math.max.apply(null, Object.values(this.weights)); // }, extractPrediction:function(score){ returnscore.prediction; }, extractProbability:function(score){ returnscore.probability[score.prediction]; }, // parseText: function(text){ // var dfd = jQuery.Deferred(); // this.mwApi.get({action: "parse", text: text, contentmodel: "wikitext", formatversion: 2, prop: "text", disablelimitreport: true}) // .done(function(data){dfd.resolve($(data.parse.text).find('p').html())}) // .fail(function(error){dfd.reject(error)}); // return dfd.promise(); // }, // getCurrentRevId: function(title){ // var dfd = jQuery.Deferred(); // this.mwApi.get({action: 'query', prop: 'revisions', titles: title, rvprop: 'ids', formatversion: 2}) // .done(function(data){dfd.resolve(data.query.pages[0].revisions[0].revid)}) // .fail(function(error){dfd.reject(error)}); // return dfd.promise(); // }, oresScore:function(revId){ vardfd=$.Deferred(); this.dqPool.score(revId) .done(function(scoreDoc){dfd.resolve(scoreDoc[this.modelName].score,revId)}.bind(this)) .fail(function(){dfd.error.apply(null,arguments)}); returndfd.promise(); }, // getAndRenderScoreHeader: function(){ // var revId = mw.config.get('wgRevisionId'); // this.oresScore(revId) // .done(this.renderScoreHeader.bind(this)) // .fail(function(error){console.error(error)}); // }, renderScoreHeader:function(score,revId){ varrawText=this.formatScoreHeader(score,revId); varv2022=document.body.classList.contains('skin-vector-2022'); varqualityBlock=$('<div>').addClass("draft_quality"); if(v2022){ qualityBlock.addClass('mw-indicator'); $('.mw-indicators').prepend(qualityBlock); }else{ $('#bodyContent').prepend(qualityBlock); } this.parseText(rawText) .done(function(html){qualityBlock.html(html)}) .fail(function(error){console.error(error)}); }, formatScoreHeader:function(score,revId){ varprediction=this.extractPrediction(score); if(!this.probaPlaceholder){ varproba=this.extractProbability(score); varroundedProba=Math.round(proba*100)/100; varlocalizedProba=roundedProba.toLocaleString( mw.config.get("wgULSAcceptLanguageList",window.navigator.languages||[])[0]); }else{ varlocalizedProba=this.probaPlaceholder; } varanchoredProba='['+this.oresHost+"/v3/scores/"+ this.oresDbname+"/"+revId+"/draftquality "+ localizedProba+"]" returnthis.assessment_system+": "+ this.names[prediction]+" ("+ anchoredProba+")"; }, // renderScoreLink: function(score, span){ // var prediction = this.extractPrediction(score); // this.parseText(this.names[prediction]) // .done(function(html){span.prepend(html)}) // .fail(function(error){console.error(error)}); // }, // getAndRenderScoreLink: function(revId, span){ // this.oresScore(revId) // .done(function(score){this.renderScoreLink(score, span)}.bind(this)) // .fail(function(error){console.error(error)}); // }, // addScoresToArticleLinks: function(){ // $("span.ores-wp10-prediction, span.ores-quality-prediction").each(function(i, element){ // var span = $(element); // var anchor = span.find('a'); // var pageTitle = anchor.attr('title'); // this.getCurrentRevId(pageTitle) // .done(function(revId){this.getAndRenderScoreLink(revId, span)}.bind(this)) // .fail(function(error){console.error(error)}); // }.bind(this)); // }, renderNewPageScore:function(li,score){ // var level = Math.round(this.computeWeightedSum(score)); // var weightedProportion = this.computeWeightedProportion(score); // var qualityPredictionNode = $("<div>").addClass("qualityprediction") // .addClass("level_" + level) // .append($("<div>").addClass("bar").css("width", Math.round(weightedProportion*100) + "%").append(" ")) // .attr('title', this.formatScoreHeader(score)); // li.prepend(qualityPredictionNode); li.prepend( $("<div>").addClass("draftprediction") .addClass("class_"+score.prediction) .attr('title',this.formatScoreHeader(score)) ); }, getAndRenderNewPageScore:function(li){ varrevId=li.attr('data-mw-revid'); this.oresScore(revId) .done(function(score){this.renderNewPageScore(li,score)}.bind(this)) .fail(function(error){console.error(error)}); }, getAndRenderNewPageScores:function(){ varpageNodes=$('#mw-content-text li[data-mw-revid]'); pageNodes.each(function(i,element){this.getAndRenderNewPageScore($(element))}.bind(this)); } } window.DraftQuality=DraftQuality; window.ArticleQuality=ArticleQuality; })(jQuery,mediaWiki) );