MediaWiki:Gadget-aiSummary.js
Appearance
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.
/**
* Gadget: AI Edit Summary
*
* Adds an "β¨ AI summary" button to the MediaWiki save dialog.
* When clicked it diffs the current edit against the saved revision,
* sends the diff to Claude, and fills the summary field.
*
* Works in both VisualEditor and the classic wikitext editor.
*/
( function () {
'use strict';
// ββ Configuration ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
var CLAUDE_API_KEY = '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'; // β paste sk-ant-β¦ key
var CLAUDE_MODEL = 'claude-haiku-4-5-20251001'; // fast & cheap
// ββ Bootstrap ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Classic wikitext editor: summary field is present on page load
$( function () {
var $field = $( '#wpSummary' );
if ( $field.length ) {
injectButton( $field, 'wikitext' );
}
} );
// Visual Editor: save dialog is injected into the DOM later
mw.hook( 've.activationComplete' ).add( function () {
var observer = new MutationObserver( function () {
var $ta = $( '.ve-ui-mwSaveDialog-summary textarea' );
if ( $ta.length && !$ta.data( 'ai-btn-attached' ) ) {
$ta.data( 'ai-btn-attached', true );
injectButton( $ta, 've' );
}
} );
observer.observe( document.body, { childList: true, subtree: true } );
} );
// ββ Button βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function injectButton( $field, editorType ) {
var $btn = $( '<button>' )
.attr( 'type', 'button' )
.html( '✨ AI summary' )
.css( {
display: 'inline-block',
marginTop: '5px',
padding: '3px 12px',
fontSize: '0.85em',
fontWeight: 'bold',
cursor: 'pointer',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: '#fff',
border: 'none',
borderRadius: '3px',
lineHeight: '1.6'
} )
.on( 'click', function () {
$btn.prop( 'disabled', true ).text( 'β³ Thinkingβ¦' );
generateSummary( editorType )
.then( function ( summary ) {
$field.val( summary ).trigger( 'input change' );
$btn.prop( 'disabled', false ).html( '✨ AI summary' );
} )
.catch( function ( err ) {
mw.notify( 'AI summary failed: ' + err,
{ type: 'error', autoHideSeconds: 8 } );
$btn.prop( 'disabled', false ).html( '✨ AI summary' );
} );
} );
$field.after( $btn );
}
// ββ Pipeline βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function generateSummary( editorType ) {
return getNewWikitext( editorType )
.then( buildPrompt )
.then( callClaude );
}
// ββ Step 1: get the wikitext being saved βββββββββββββββββββββββββββββββ
function getNewWikitext( editorType ) {
// Classic editor: read the textarea directly
if ( editorType === 'wikitext' ) {
return $.Deferred().resolve( $( '#wpTextbox1' ).val() ).promise();
}
// Visual Editor: serialize the in-memory document via the API
if ( !window.ve || !ve.init || !ve.init.target ) {
return $.Deferred().reject( 'VisualEditor not ready' ).promise();
}
var surface = ve.init.target.getSurface();
if ( !surface ) {
return $.Deferred().reject( 'VE surface not available' ).promise();
}
try {
var htmlDoc = ve.dm.converter.getDomFromModel(
surface.getModel().getDocument() );
var serialized = new XMLSerializer().serializeToString( htmlDoc );
return new mw.Api().post( {
action: 'visualeditor',
paction: 'serialize',
page: mw.config.get( 'wgPageName' ),
html: serialized,
format: 'json'
} ).then( function ( data ) {
return ( data.visualeditor && data.visualeditor.content ) || '';
} );
} catch ( e ) {
return $.Deferred().reject( 'Could not read VE document: ' + e ).promise();
}
}
// ββ Step 2: build a prompt from the diff ββββββββββββββββββββββββββββββ
function buildPrompt( newText ) {
var title = mw.config.get( 'wgPageName' ).replace( /_/g, ' ' );
var revId = mw.config.get( 'wgRevisionId' );
// New page β no previous revision to diff against
if ( !revId ) {
return $.Deferred().resolve(
'Family wiki. New page created: "' + title + '".\n\n' +
'Content (first 1200 chars):\n' + newText.slice( 0, 1200 )
).promise();
}
// Existing page β fetch a diff
return new mw.Api().get( {
action: 'compare',
fromrev: revId,
totext: newText,
topst: 1,
prop: 'diff',
format: 'json'
} ).then( function ( data ) {
var diffText = parseDiff( ( data.compare && data.compare.body ) || '' );
return 'Family wiki. Page: "' + title + '".\n\n' +
'Diff (lines prefixed + added / - removed):\n' + diffText;
} ).catch( function () {
// If diff fails, still give Claude something to work with
return 'Family wiki. Page "' + title + '" was edited.';
} );
}
/** Strip HTML from the diff table and return plain-text +/- lines. */
function parseDiff( html ) {
var $d = $( '<div>' ).html( html );
var lines = [];
$d.find( 'tr' ).each( function () {
var $tr = $( this );
var del = $tr.find( 'td.diff-deletedline' ).text().trim();
var add = $tr.find( 'td.diff-addedline' ).text().trim();
if ( del ) { lines.push( '- ' + del ); }
if ( add ) { lines.push( '+ ' + add ); }
} );
return lines.join( '\n' ).slice( 0, 2500 ) || '(no textual changes detected)';
}
// ββ Step 3: call Claude ββββββββββββββββββββββββββββββββββββββββββββββββ
function callClaude( prompt ) {
if ( !prompt.trim() ) {
return $.Deferred().reject( 'Nothing to summarise' ).promise();
}
return $.ajax( {
url: 'https://api.anthropic.com/v1/messages',
method: 'POST',
contentType: 'application/json',
headers: {
'x-api-key': CLAUDE_API_KEY,
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true'
},
data: JSON.stringify( {
model: CLAUDE_MODEL,
max_tokens: 100,
system: [
'You write concise MediaWiki edit summaries for a family history wiki.',
'Reply with ONLY the summary β no quotes, no explanation, no punctuation at the end.',
'Maximum 70 characters. Use active voice and plain English.',
'Good examples:',
' Add birth place for John Gianutsos',
' Fix spouse link on Nicole Morbillo page',
' Update death date to 1965',
' Add children section',
' Correct spelling of Evtichia'
].join( '\n' ),
messages: [ { role: 'user', content: prompt } ]
} )
} ).then( function ( resp ) {
return resp.content[ 0 ].text
.trim()
.replace( /^["']|["']$/g, '' )
.slice( 0, 70 );
}, function ( xhr ) {
var err = xhr.responseJSON &&
xhr.responseJSON.error &&
xhr.responseJSON.error.message;
return $.Deferred().reject( err || xhr.statusText ).promise();
} );
}
}() );