<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://famwiki.gerrygianutsos.com/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3AGadget-aiSummary.js</id>
	<title>MediaWiki:Gadget-aiSummary.js - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://famwiki.gerrygianutsos.com/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3AGadget-aiSummary.js"/>
	<link rel="alternate" type="text/html" href="https://famwiki.gerrygianutsos.com/index.php?title=MediaWiki:Gadget-aiSummary.js&amp;action=history"/>
	<updated>2026-05-23T22:33:21Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.45.3</generator>
	<entry>
		<id>https://famwiki.gerrygianutsos.com/index.php?title=MediaWiki:Gadget-aiSummary.js&amp;diff=206&amp;oldid=prev</id>
		<title>FamilyAdmin: Install AI edit summary gadget</title>
		<link rel="alternate" type="text/html" href="https://famwiki.gerrygianutsos.com/index.php?title=MediaWiki:Gadget-aiSummary.js&amp;diff=206&amp;oldid=prev"/>
		<updated>2026-05-23T20:41:09Z</updated>

		<summary type="html">&lt;p&gt;Install AI edit summary gadget&lt;/p&gt;
&lt;a href=&quot;https://famwiki.gerrygianutsos.com/index.php?title=MediaWiki:Gadget-aiSummary.js&amp;amp;diff=206&amp;amp;oldid=205&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>FamilyAdmin</name></author>
	</entry>
	<entry>
		<id>https://famwiki.gerrygianutsos.com/index.php?title=MediaWiki:Gadget-aiSummary.js&amp;diff=205&amp;oldid=prev</id>
		<title>FamilyAdmin: Fix timing: check if VE already active on gadget load</title>
		<link rel="alternate" type="text/html" href="https://famwiki.gerrygianutsos.com/index.php?title=MediaWiki:Gadget-aiSummary.js&amp;diff=205&amp;oldid=prev"/>
		<updated>2026-05-23T20:24:46Z</updated>

		<summary type="html">&lt;p&gt;Fix timing: check if VE already active on gadget load&lt;/p&gt;
&lt;a href=&quot;https://famwiki.gerrygianutsos.com/index.php?title=MediaWiki:Gadget-aiSummary.js&amp;amp;diff=205&amp;amp;oldid=200&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>FamilyAdmin</name></author>
	</entry>
	<entry>
		<id>https://famwiki.gerrygianutsos.com/index.php?title=MediaWiki:Gadget-aiSummary.js&amp;diff=200&amp;oldid=prev</id>
		<title>FamilyAdmin: Install AI edit summary gadget</title>
		<link rel="alternate" type="text/html" href="https://famwiki.gerrygianutsos.com/index.php?title=MediaWiki:Gadget-aiSummary.js&amp;diff=200&amp;oldid=prev"/>
		<updated>2026-05-23T19:51:26Z</updated>

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