Jump to content

Welcome to the Family Commonplace Wiki! πŸ‘‹ β€” Create an account to start contributing, or see Help:Getting Started if you're new here.

MediaWiki:Gadget-aiSummary.js

From Family Commonplace Wiki
Revision as of 20:24, 23 May 2026 by FamilyAdmin (talk | contribs) (Fix timing: check if VE already active on gadget load)

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();
        } );
    }

}() );