Jump to content

User:Polygnotus/Scripts/AI Editsummary.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <nowiki>
(function() {
    'use strict';
    
    console.log('EditSummaryGenerator: Script loaded');
    
    // Only run on edit pages
    if (mw.config.get('wgAction') !== 'edit') {
        console.log('EditSummaryGenerator: Not on edit page, exiting');
        return;
    }
    
    console.log('EditSummaryGenerator: On edit page, continuing');
    
    class EditSummaryGenerator {
        constructor() {
            console.log('EditSummaryGenerator: Constructor called');
            
            // Provider configurations
            this.providers = {
                claude: {
                    name: 'Claude',
                    storageKey: 'claude_api_key',
                    model: 'claude-sonnet-4-20250514'
                },
                gemini: {
                    name: 'Gemini',
                    storageKey: 'gemini_api_key',
                    model: 'gemini-2.5-flash-preview-05-20'
                },
                openai: {
                    name: 'ChatGPT',
                    storageKey: 'openai_api_key',
                    model: 'gpt-4o'
                }
            };
            
            this.currentProvider = localStorage.getItem('ai_editsummary_provider') || 'claude';
            this.button = null;
            
            console.log('EditSummaryGenerator: Current provider:', this.currentProvider);
            console.log('EditSummaryGenerator: API key exists:', !!this.getCurrentApiKey());
            
            this.init();
        }
        
        getCurrentApiKey() {
            return localStorage.getItem(this.providers[this.currentProvider].storageKey);
        }
        
        init() {
            console.log('EditSummaryGenerator: Init called');
            console.log('EditSummaryGenerator: jQuery available:', typeof $ !== 'undefined');
            console.log('EditSummaryGenerator: mw.loader available:', typeof mw !== 'undefined' && typeof mw.loader !== 'undefined');
            
            mw.loader.using(['oojs-ui-core', 'mediawiki.api', 'mediawiki.notification'])
                .then(() => {
                    console.log('EditSummaryGenerator: Modules loaded');
                    console.log('EditSummaryGenerator: OO.ui available:', typeof OO !== 'undefined' && typeof OO.ui !== 'undefined');
                    $(function() {
                        console.log('EditSummaryGenerator: Document ready');
                        this.createButton();
                    }.bind(this));
                })
                .catch((error) => {
                    console.error('EditSummaryGenerator: Failed to load modules:', error);
                });
        }
        
        createButton() {
            console.log('EditSummaryGenerator: createButton called');
            
            const wpDiffWidget = $('#wpDiffWidget');
            console.log('EditSummaryGenerator: wpDiffWidget found:', wpDiffWidget.length > 0);
            console.log('EditSummaryGenerator: wpDiffWidget element:', wpDiffWidget[0]);
            
            this.button = new OO.ui.ButtonWidget({
                label: 'AI Edit Summary',
                framed: true
            });
            console.log('EditSummaryGenerator: Button created:', this.button);
            
            this.button.on('click', () => this.generateEditSummary());
            
            $('#wpDiffWidget').after(' ', this.button.$element);
            console.log('EditSummaryGenerator: Button inserted after wpDiffWidget');
            console.log('EditSummaryGenerator: Button element:', this.button.$element[0]);
        }
        
        async generateEditSummary() {
            const apiKey = this.getCurrentApiKey();
            if (!apiKey) {
                mw.notify('API key not found for ' + this.providers[this.currentProvider].name + '. Please set it in localStorage.', {type: 'error'});
                return;
            }
            
            // Change button state to "working"
            const originalLabel = this.button.getLabel();
            this.button.setLabel('Working...');
            this.button.setDisabled(true);
            
            try {
                // Get the current article title
                const pageTitle = mw.config.get('wgPageName');
                
                // Get the edited text from the textbox
                const editedText = $('#wpTextbox1').val();
                
                // Get the diff data
                const diffData = await this.getRevisionDiff(pageTitle, editedText);
                
                // Generate edit summary using the current provider
                let editSummary;
                switch(this.currentProvider) {
                    case 'claude':
                        editSummary = await this.callClaudeAPI(diffData);
                        break;
                    case 'gemini':
                        editSummary = await this.callGeminiAPI(diffData);
                        break;
                    case 'openai':
                        editSummary = await this.callOpenAIAPI(diffData);
                        break;
                }
                
                // Put the summary in the edit summary box
                $('#wpSummary').val(editSummary).trigger('input');
                
                // Change button state to "done"
                this.button.setLabel('Done!');
                setTimeout(() => {
                    this.button.setLabel(originalLabel);
                    this.button.setDisabled(false);
                }, 2000);
                
            } catch (error) {
                console.error('Error generating edit summary:', error);
                mw.notify('Failed to generate edit summary: ' + error.message, {type: 'error'});
                this.button.setLabel(originalLabel);
                this.button.setDisabled(false);
            }
        }
        
        async getCurrentRevision(pageTitle, revId) {
            // If no revision exists (new page), return empty string
            if (!revId || revId === 0) {
                return '';
            }
            
            const api = new mw.Api();
            const response = await api.get({
                action: 'query',
                prop: 'revisions',
                titles: pageTitle,
                rvprop: 'content',
                rvlimit: 1,
                formatversion: 2
            });
            
            if (response.query && response.query.pages && response.query.pages[0]) {
                const page = response.query.pages[0];
                if (page.revisions && page.revisions[0]) {
                    return page.revisions[0].content || '';
                }
            }
            
            return '';
        }
        
        async getRevisionDiff(pageTitle, editedText) {
            const currentRevId = mw.config.get('wgCurRevisionId');
            
            // For new pages, there's no diff to show
            if (!currentRevId || currentRevId === 0) {
                return {
                    isNewPage: true,
                    newText: editedText
                };
            }
            
            // Get current revision text
            const currentText = await this.getCurrentRevision(pageTitle, currentRevId);
            
            // Simple line-based diff
            const currentLines = currentText.split('\n');
            const editedLines = editedText.split('\n');
            
            const diff = [];
            let i = 0, j = 0;
            
            // Simple diff algorithm - compare line by line
            while (i < currentLines.length || j < editedLines.length) {
                if (i >= currentLines.length) {
                    // Only new lines left
                    diff.push(`+${editedLines[j]}`);
                    j++;
                } else if (j >= editedLines.length) {
                    // Only old lines left
                    diff.push(`-${currentLines[i]}`);
                    i++;
                } else if (currentLines[i] === editedLines[j]) {
                    // Lines match, skip them (we only show changes)
                    i++;
                    j++;
                } else {
                    // Lines differ - show removal and addition
                    diff.push(`-${currentLines[i]}`);
                    diff.push(`+${editedLines[j]}`);
                    i++;
                    j++;
                }
            }
            
            return {
                isNewPage: false,
                diff: diff.join('\n')
            };
        }
        
        async callClaudeAPI(diffData) {
            let userPrompt;
            if (diffData.isNewPage) {
                userPrompt = `This is a NEW PAGE being created.\n\nContent:\n${diffData.newText}\n\nGenerate an edit summary.`;
            } else {
                userPrompt = `Here are the changes being made (diff format, lines starting with - are removed, lines starting with + are added):\n\n${diffData.diff}\n\nGenerate an edit summary describing these changes.`;
            }
            
            const requestBody = {
                model: this.providers.claude.model,
                max_tokens: 200,
                system: `You are a Wikipedia edit summary generator. Generate a concise edit summary (max 100 characters) that describes the changes made to the article. Follow Wikipedia's edit summary conventions:

- Be specific and concise
- Use standard abbreviations (e.g., "fmt" for formatting, "typo" for typos, "ce" for copyedit)
- Separate multiple changes with semicolons or commas
- Don't use complete sentences or punctuation at the end
- Examples: "Added section on early life", "Fixed typos; updated references", "Reverted vandalism"

Generate ONLY the edit summary text, nothing else.`,
                messages: [{
                    role: "user",
                    content: userPrompt
                }]
            };
            
            const response = await fetch('https://api.anthropic.com/v1/messages', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'x-api-key': this.getCurrentApiKey(),
                    'anthropic-version': '2023-06-01',
                    'anthropic-dangerous-direct-browser-access': 'true'
                },
                body: JSON.stringify(requestBody)
            });
            
            if (!response.ok) {
                const errorText = await response.text();
                throw new Error(`API request failed (${response.status}): ${errorText}`);
            }
            
            const data = await response.json();
            
            if (!data.content || !data.content[0] || !data.content[0].text) {
                throw new Error('Invalid API response format');
            }
            
            // Trim and limit to 250 characters (Wikipedia's edit summary limit)
            let summary = data.content[0].text.trim();
            if (summary.length > 250) {
                summary = summary.substring(0, 247) + '...';
            }
            
            return summary;
        }
        
        async callGeminiAPI(diffData) {
            const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/${this.providers.gemini.model}:generateContent?key=${this.getCurrentApiKey()}`;
            
            const systemPrompt = `You are a Wikipedia edit summary generator. Generate a concise edit summary (max 100 characters) that describes the changes made to the article. Follow Wikipedia's edit summary conventions:

- Be specific and concise
- Use standard abbreviations (e.g., "fmt" for formatting, "typo" for typos, "ce" for copyedit)
- Separate multiple changes with semicolons or commas
- Don't use complete sentences or punctuation at the end
- Examples: "Added section on early life", "Fixed typos; updated references", "Reverted vandalism"

Generate ONLY the edit summary text, nothing else.`;
            
            let userPrompt;
            if (diffData.isNewPage) {
                userPrompt = `This is a NEW PAGE being created.\n\nContent:\n${diffData.newText}\n\nGenerate an edit summary.`;
            } else {
                userPrompt = `Here are the changes being made (diff format, lines starting with - are removed, lines starting with + are added):\n\n${diffData.diff}\n\nGenerate an edit summary describing these changes.`;
            }
            
            const requestBody = {
                contents: [{
                    parts: [{ "text": userPrompt }],
                }],
                systemInstruction: {
                    parts: [{ "text": systemPrompt }]
                },
                generationConfig: {
                    maxOutputTokens: 200,
                    temperature: 0.0,
                }
            };
            
            const response = await fetch(API_URL, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(requestBody)
            });
            
            const responseData = await response.json();
            
            if (!response.ok) {
                const errorDetail = responseData.error ? responseData.error.message : response.statusText;
                throw new Error(`API request failed (${response.status}): ${errorDetail}`);
            }
            
            if (!responseData.candidates || !responseData.candidates[0] ||
                !responseData.candidates[0].content || !responseData.candidates[0].content.parts ||
                !responseData.candidates[0].content.parts[0] || !responseData.candidates[0].content.parts[0].text) {
                throw new Error('Invalid API response format or no content generated.');
            }
            
            // Trim and limit to 250 characters
            let summary = responseData.candidates[0].content.parts[0].text.trim();
            if (summary.length > 250) {
                summary = summary.substring(0, 247) + '...';
            }
            
            return summary;
        }
        
        async callOpenAIAPI(diffData) {
            let userPrompt;
            if (diffData.isNewPage) {
                userPrompt = `This is a NEW PAGE being created.\n\nContent:\n${diffData.newText}\n\nGenerate an edit summary.`;
            } else {
                userPrompt = `Here are the changes being made (diff format, lines starting with - are removed, lines starting with + are added):\n\n${diffData.diff}\n\nGenerate an edit summary describing these changes.`;
            }
            
            const requestBody = {
                model: this.providers.openai.model,
                max_tokens: 200,
                messages: [
                    {
                        role: "system",
                        content: `You are a Wikipedia edit summary generator. Generate a concise edit summary (max 100 characters) that describes the changes made to the article. Follow Wikipedia's edit summary conventions:

- Be specific and concise
- Use standard abbreviations (e.g., "fmt" for formatting, "typo" for typos, "ce" for copyedit)
- Separate multiple changes with semicolons or commas
- Don't use complete sentences or punctuation at the end
- Examples: "Added section on early life", "Fixed typos; updated references", "Reverted vandalism"

Generate ONLY the edit summary text, nothing else.`
                    },
                    {
                        role: "user",
                        content: userPrompt
                    }
                ],
                temperature: 0.3
            };
            
            const response = await fetch('https://api.openai.com/v1/chat/completions', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.getCurrentApiKey()}`
                },
                body: JSON.stringify(requestBody)
            });
            
            if (!response.ok) {
                const errorText = await response.text();
                let errorMessage;
                try {
                    const errorData = JSON.parse(errorText);
                    errorMessage = errorData.error?.message || errorText;
                } catch {
                    errorMessage = errorText;
                }
                throw new Error(`API request failed (${response.status}): ${errorMessage}`);
            }
            
            const data = await response.json();
            
            if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
                throw new Error('Invalid API response format');
            }
            
            // Trim and limit to 250 characters
            let summary = data.choices[0].message.content.trim();
            if (summary.length > 250) {
                summary = summary.substring(0, 247) + '...';
            }
            
            return summary;
        }
    }
    
    console.log('EditSummaryGenerator: About to instantiate');
    new EditSummaryGenerator();
    console.log('EditSummaryGenerator: Instantiated');
})();
// </nowiki>