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)
*
* Automatically fills the edit summary field with a Claude-generated
* description the moment the save dialog opens. Works in 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
// ββ Visual Editor ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function setupVEObserver() {
// Check immediately in case save dialog already open
var $ta = $( '.ve-ui-mwSaveDialog-summary textarea' );
if ( $ta.length && !$ta.data( 'ai-done' ) ) {
$ta.data( 'ai-done', true );
autoFill( $ta, 've' );
addRedoLink( $ta, 've' );
}
var observer = new MutationObserver( function () {
var $t = $( '.ve-ui-mwSaveDialog-summary textarea' );
if ( $t.length && !$t.data( 'ai-done' ) ) {
$t.data( 'ai-done', true );
autoFill( $t, 've' );
addRedoLink( $t, 've' );
}
} );
observer.observe( document.body, { childList: true, subtree: true } );
}
// Timing fix: VE may already be active when gadget first loads
if ( window.ve && ve.init && ve.init.target ) {
setupVEObserver();
}
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' );
} );
} );
// ββ Auto-fill ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function autoFill( $field, editorType ) {
// Don't overwrite something the user already typed
if ( $field.val().trim() ) { return; }
var origPlaceholder = $field.attr( 'placeholder' ) || '';
$field.attr( 'placeholder', 'β¨ Writing AI summaryβ¦' );
buildPrompt( editorType )
.then( callClaude )
.then( function ( summary ) {
// Only fill if still empty
if ( !$field.val().trim() ) {
$field.val( summary ).trigger( 'input change' );
}
} )
.catch( function ( err ) {
mw.log.warn( 'AI summary error:', err );
} )
.always( function () {
$field.attr( 'placeholder', origPlaceholder );
} );
}
// Small "βΊ redo" link the user can click for a fresh suggestion
function addRedoLink( $field, editorType ) {
// Guard against duplicate links
if ( $field.next( '.ai-redo' ).length ) { return; }
var $redo = $( '<a>' )
.addClass( 'ai-redo' )
.attr( 'href', '#' )
.attr( 'title', 'Generate a new AI summary' )
.text( 'βΊ redo AI summary' )
.css( {
display: 'inline-block',
marginTop: '4px',
fontSize: '0.8em',
color: '#36c',
cursor: 'pointer'
} )
.on( 'click', function ( e ) {
e.preventDefault();
$field.val( '' );
autoFill( $field, editorType );
} );
$field.after( $redo );
}
// ββ Step 1: get editor content βββββββββββββββββββββββββββββββββββββββββ
function getEditorText( editorType ) {
if ( editorType === 'wikitext' ) {
return $( '#wpTextbox1' ).val() || '';
}
// Visual Editor: get plain text from document model
if ( window.ve && ve.init && ve.init.target ) {
var surface = ve.init.target.getSurface();
if ( surface ) {
try {
return surface.getModel().getDocument().data.getText( true );
} catch ( e ) {
mw.log.warn( 'VE getText failed:', e );
}
}
}
return '';
}
// ββ Step 2: build prompt using saved wikitext + current editor text ββββ
function buildPrompt( editorType ) {
var title = mw.config.get( 'wgPageName' ).replace( /_/g, ' ' );
var revId = mw.config.get( 'wgRevisionId' );
var newTxt = getEditorText( editorType );
// New page β no previous revision to compare
if ( !revId ) {
return $.Deferred().resolve(
'Family wiki. New page: "' + title + '".\n\n' +
'Content:\n' + newTxt.slice( 0, 1500 )
).promise();
}
// Existing page: fetch saved wikitext and compare with current editor text
return new mw.Api().get( {
action: 'query',
titles: mw.config.get( 'wgPageName' ),
prop: 'revisions',
rvprop: 'content',
rvlimit: 1,
format: 'json'
} ).then( function ( d ) {
var pages = d.query.pages;
var page = pages[ Object.keys( pages )[ 0 ] ];
var oldText = ( page.revisions && page.revisions[ 0 ][ '*' ] ) || '';
return 'Family wiki. Page: "' + title + '".\n\n' +
'SAVED WIKITEXT (before edit):\n' + oldText.slice( 0, 1400 ) + '\n\n' +
'CURRENT EDITOR TEXT (after edit):\n' + newTxt.slice( 0, 1400 );
} ).catch( function () {
return 'Family wiki. Page "' + title + '" edited.\n\nEditor text:\n' + newTxt.slice( 0, 1500 );
} );
}
// ββ Step 3: call Claude ββββββββββββββββββββββββββββββββββββββββββββββββ
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, no trailing punctuation.',
'Maximum 70 characters. Use active voice.',
'Examples: "Add birth place for John Gianutsos", "Fix spouse link", "Update death date to 1965"'
].join( ' ' ),
messages: [ { role: 'user', content: prompt } ]
} )
} ).then( function ( resp ) {
return resp.content[ 0 ].text
.trim()
.replace( /^["']|["']$/g, '' )
.slice( 0, 70 );
}, function ( xhr ) {
var msg = xhr.responseJSON &&
xhr.responseJSON.error &&
xhr.responseJSON.error.message;
return $.Deferred().reject( msg || xhr.statusText ).promise();
} );
}
}() );