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:41, 23 May 2026 by FamilyAdmin (talk | contribs) (Install AI edit summary gadget)
(diff) ← Older revision | Latest revision (diff) | Newer revision β†’ (diff)

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

}() );