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 (auto-fill)
* Fills the summary field automatically when the save dialog opens.
*/
( function () {
'use strict';
var CLAUDE_API_KEY = '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';
var CLAUDE_MODEL = 'claude-haiku-4-5-20251001';
function setupVEObserver() {
// Immediately check if save dialog is already open
var $ta = $( '.ve-ui-mwSaveDialog-summary textarea' );
if ( $ta.length && !$ta.data( 'ai-triggered' ) ) {
$ta.data( 'ai-triggered', true );
autoFill( $ta, 've' );
addRedoLink( $ta, 've' );
}
// Watch for it to open in the future
var observer = new MutationObserver( function () {
var $ta2 = $( '.ve-ui-mwSaveDialog-summary textarea' );
if ( $ta2.length && !$ta2.data( 'ai-triggered' ) ) {
$ta2.data( 'ai-triggered', true );
autoFill( $ta2, 've' );
addRedoLink( $ta2, 've' );
}
} );
observer.observe( document.body, { childList: true, subtree: true } );
}
// If VE is already active, set up immediately
if ( window.ve && ve.init && ve.init.target ) {
setupVEObserver();
}
// Also hook for future VE activations
mw.hook( 've.activationComplete' ).add( setupVEObserver );
// Classic wikitext editor
$( function () {
var $field = $( '#wpSummary' );
if ( !$field.length ) { return; }
addRedoLink( $field, 'wikitext' );
$field.one( 'focus', function () { autoFill( $field, 'wikitext' ); } );
} );
function autoFill( $field, editorType ) {
if ( $field.val().trim() ) { return; }
var orig = $field.attr( 'placeholder' ) || '';
$field.attr( 'placeholder', '\u2728 Writing AI summary\u2026' );
generateSummary( editorType )
.then( function ( s ) {
if ( !$field.val().trim() ) { $field.val( s ).trigger( 'input change' ); }
} )
.catch( function ( e ) { mw.log.warn( 'AI summary:', e ); } )
.always( function () { $field.attr( 'placeholder', orig ); } );
}
function addRedoLink( $field, editorType ) {
$( '<a>' ).attr( 'href', '#' ).text( '\u21ba redo AI summary' )
.css( { display: 'inline-block', marginTop: '4px', fontSize: '0.8em', color: '#36c' } )
.on( 'click', function ( e ) {
e.preventDefault(); $field.val( '' ); autoFill( $field, editorType );
} ).insertAfter( $field );
}
function generateSummary( editorType ) {
return getNewWikitext( editorType ).then( buildPrompt ).then( callClaude );
}
function getNewWikitext( editorType ) {
if ( editorType === 'wikitext' ) {
return $.Deferred().resolve( $( '#wpTextbox1' ).val() ).promise();
}
if ( !window.ve || !ve.init || !ve.init.target ) {
return $.Deferred().reject( 'VE not ready' ).promise();
}
var surface = ve.init.target.getSurface();
if ( !surface ) { return $.Deferred().reject( 'no surface' ).promise(); }
try {
var html = new XMLSerializer().serializeToString(
ve.dm.converter.getDomFromModel( surface.getModel().getDocument() ) );
return new mw.Api().post( {
action: 'visualeditor', paction: 'serialize',
page: mw.config.get( 'wgPageName' ), html: html, format: 'json'
} ).then( function ( d ) { return ( d.visualeditor && d.visualeditor.content ) || ''; } );
} catch ( e ) { return $.Deferred().reject( String( e ) ).promise(); }
}
function buildPrompt( newText ) {
var title = mw.config.get( 'wgPageName' ).replace( /_/g, ' ' );
var revId = mw.config.get( 'wgRevisionId' );
if ( !revId ) {
return $.Deferred().resolve( 'Family wiki. New page: "' + title + '".\n\n' + newText.slice( 0, 1200 ) ).promise();
}
return new mw.Api().get( {
action: 'compare', fromrev: revId, totext: newText, topst: 1, prop: 'diff', format: 'json'
} ).then( function ( d ) {
var diff = parseDiff( ( d.compare && d.compare.body ) || '' );
return 'Family wiki. Page: "' + title + '".\n\nDiff:\n' + diff;
} ).catch( function () { return 'Family wiki. Page "' + title + '" was edited.'; } );
}
function parseDiff( html ) {
var $d = $( '<div>' ).html( html ), lines = [];
$d.find( 'tr' ).each( function () {
var del = $( this ).find( 'td.diff-deletedline' ).text().trim();
var add = $( this ).find( 'td.diff-addedline' ).text().trim();
if ( del ) { lines.push( '- ' + del ); }
if ( add ) { lines.push( '+ ' + add ); }
} );
return lines.join( '\n' ).slice( 0, 2500 ) || '(no changes)';
}
function callClaude( prompt ) {
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. Max 70 chars. Active voice. Examples: "Add birth place for John Gianutsos", "Fix spouse link", "Update death date".',
messages: [ { role: 'user', content: prompt } ]
} )
} ).then( function ( r ) {
return r.content[ 0 ].text.trim().replace( /^["']|["']$/g, '' ).slice( 0, 70 );
}, function ( xhr ) {
var m = xhr.responseJSON && xhr.responseJSON.error && xhr.responseJSON.error.message;
return $.Deferred().reject( m || xhr.statusText ).promise();
} );
}
}() );