Jump to content

User:WeWake/GeminiChat.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.
// Fork of User:Phlsph7/WikiChatbot

(function(){
    // Value for the advanced model checkbox
    if(localStorage.getItem('WikiChatbotAdvancedModel') === null){
        if(localStorage.getItem('WikiChatbotGeminiAPIKey') === null){
            localStorage.setItem('WikiChatbotAdvancedModel', 'true'); // Default to true for new users
        } else {
            localStorage.setItem('WikiChatbotAdvancedModel', 'false');
        }
    }

    // define values
    let tokenLimit;
    let model;
    function updateModelAndTokenLimit() {
    if(localStorage.getItem('WikiChatbotAdvancedModel') === 'true'){ // Ensure boolean comparison
        tokenLimit = 100000; // For Gemini 1.5 Pro
        model = 'gemini-2.5-flash-preview-05-20';
    }
    else {
        tokenLimit = 500000; // For Gemini 1.5 Flash (generous, actual model supports 1M)
        model = 'gemini-2.0-flash';
    }
}
    updateModelAndTokenLimit(); // Initial call

    const temperature = 0.5;
    // This charLimit is a script-side heuristic. Gemini's tokenization is complex.
    // Based on ~3-4 chars per token on average for English.
    const charLimit = function(){ return tokenLimit * 3; };
    const articleContextLimit = function(){ return charLimit() * 0.05;}; // For initial context snippet
    const historyLimit = function(){ return charLimit() * 0.15;}; // For chat history
    const selectionLimit = function(){ return charLimit() * 0.20;}; // For selected text
    const promptLimit = function(){ return charLimit() * 0.20;}; // For user's typed prompt
    const fullArticleCharSoftLimit = function() { return charLimit() * 0.80; }; // Soft limit for "Analyze Entire Article"

    const backgroundColor = '#def';
    const backgroundColorUser = '#ddd';
    const backgroundColorBot = '#dfd';
    const backgroundColorError = '#faa';
    let messages = getInitialMessages(); // Initialize messages with system prompt
    const bodyContent = document.getElementById('bodyContent');
    let controlContainer;
    let reRotateControl;
    let chatContainer;
    let chatLog;
    let chatSend;
    let displayWarningMessage = false; // For "Suggest expansion" warnings

    const namespaceNumber = mw.config.get('wgNamespaceNumber');
    const allowedNamespaces = [0, 2, 4, 12, 118]; // Main, User, Wikipedia, Help, Draft

    if (allowedNamespaces.indexOf(namespaceNumber) != -1) {
        // mw.loader.using is needed for mw.Api and mw.util
        $.when(mw.loader.using(['mediawiki.util', 'mediawiki.api']), $.ready).then(addPortletAndActivate);
    }

    function getInitialMessages(){
        return [
            {"role":"system_instruction_holder", "content": `You are WikiChatbot, an AI assistant expertly programmed to help Wikipedia editors improve articles. Your responses MUST strictly adhere to Wikipedia's core content policies: Neutral Point of View (NPOV), Verifiability (V), and No Original Research (NOR). All suggestions and generated content must align with the Wikipedia Manual of Style (MoS), emphasizing formal tone, encyclopedic language, clarity, accessibility, proper structure (including lead sections, headings), and appropriate use of wikilinks and templates. When suggesting content or edits, prioritize information attributable to reliable, published sources, and explicitly state when citations are needed or sources seem weak. Avoid plagiarism rigorously; ensure suggestions are original or correctly attributed if quoting. NEVER invent facts, sources, or engage in speculation. Your advice should be constructive, specific, and actionable. Use the following excerpt from the beginning of the current article as general context, unless a broader context (like the full article wikitext) is explicitly provided for a specific task.\n\nArticle Context Snippet: """${getContext()}"""`}
        ];
    }

    // Fetches raw wikitext of the current page using mw.Api().
    // mw.Api() is preferred over mw.hook for reliability in fetching source wikitext.
    // Some mw.hook events might provide rendered HTML or editor-specific content,
    // which is less suitable for source analysis.
    async function getArticleWikitext() {
        const pageName = mw.config.get('wgPageName');
        const api = new mw.Api();
        logBotMessage("Fetching full article wikitext...");
        disableButtons();
        try {
            const apiResult = await api.get({
                action: 'query',
                prop: 'revisions',
                titles: pageName,
                rvprop: 'content',
                rvslots: 'main', // Ensure content from the main slot is fetched
                formatversion: 2 // Modern format, easier to parse
            });
            enableButtons();
            if (apiResult.query && apiResult.query.pages && apiResult.query.pages[0].revisions && apiResult.query.pages[0].revisions[0] && apiResult.query.pages[0].revisions[0].slots.main) {
                const content = apiResult.query.pages[0].revisions[0].slots.main.content;
                logBotMessage(`Successfully fetched wikitext (${(content.length / 1024).toFixed(2)} KB).`);
                return content;
            } else {
                const errorMsg = 'Wikichatbot: Could not retrieve wikitext. The page might be empty, deleted, or an unexpected API response was received.';
                logErrorMessage(errorMsg);
                console.error(errorMsg, apiResult); // Log full API response for debugging
                return null;
            }
        } catch (error) {
            enableButtons();
            const errorMsg = `Wikichatbot: API error fetching wikitext. ${error.message || error.toString()}`;
            logErrorMessage(errorMsg);
            console.error(errorMsg, error);
            return null;
        }
    }


    function createControlUI(){
        controlContainer = document.createElement('div');
        if(localStorage.getItem('WikiChatbotActivated') === 'true') controlContainer.style.display = 'flex';
        else controlContainer.style.display = 'none';
        bodyContent.appendChild(controlContainer);
        Object.assign(controlContainer.style, {
            position: 'fixed', right: '10px', bottom: '10px', backgroundColor: backgroundColor,
            overflowY: 'auto', padding: '10px', borderRadius: '10px', whiteSpace: 'nowrap',
            alignItems: 'center', zIndex: '999', resize: 'vertical', maxHeight: '80%',
            transform: 'rotateZ(180deg)'
        });

        reRotateControl = document.createElement('div');
        controlContainer.appendChild(reRotateControl);
        Object.assign(reRotateControl.style, {
            width: '100%', height: '100%', overflowY: 'auto', transform: 'rotateZ(180deg)',
            display: 'flex', flexDirection: 'column'
        });

        addButtons();
        if(controlContainer.clientHeight > 400) controlContainer.style.height = controlContainer.clientHeight + 'px';

        function addButtons(){
            // Standard action buttons
            addControlButton('Copyedit Selected', 'Copyedit the selected text for MoS, NPOV, V.', getQueryFunction(selectionLimit(), function(){
                return `Thoroughly copyedit the following selected text. Ensure it strictly adheres to Wikipedia's Manual of Style (formal tone, clarity, grammar, punctuation, wikilinks), Neutral Point of View, and implies Verifiability (i.e., is stated as fact that can be sourced). Polish the language for an encyclopedic register.\n\nSelected text: """${getSelectedText()}"""`;
            }));
            addControlButton('Check Grammar/Style', 'Assess selected text for grammar, spelling, and encyclopedic style.', getQueryFunction(selectionLimit(), function(){
                return `Review the selected text for any problems with spelling, grammar, punctuation, and overall encyclopedic style. Identify awkward phrasing or deviations from formal tone required by Wikipedia's MoS.\n\nSelected text: """${getSelectedText()}"""`;
            }));
            addControlButton('Reformulate Selected', 'Rephrase selected text for clarity, neutrality, and conciseness per MoS.', getQueryFunction(selectionLimit(), function(){
                return `Reformulate the selected text to significantly improve its clarity, neutrality (NPOV), and conciseness. Ensure the reformulation aligns with Wikipedia's Manual of Style for encyclopedic content.\n\nSelected text: """${getSelectedText()}"""`;
            }));
            addControlButton('Simplify Selected', 'Make complex language in selected text more accessible.', getQueryFunction(selectionLimit(), function(){
                return `Simplify any complex language or jargon in the selected text to make it more accessible to a general reader, while maintaining accuracy, a formal tone, and adherence to Wikipedia's MoS.\n\nSelected text: """${getSelectedText()}"""`;
            }));
             addControlButton('Suggest Expansion Ideas', 'Provide sourced ideas for expanding selected text, following NPOV, V, NOR.', getQueryFunction(selectionLimit(), function(){
                displayWarningMessage = true; // Remind user to verify AI suggestions
                return `Based on the selected text, suggest specific, verifiable ideas for expansion that align with Wikipedia's NPOV, Verifiability, and No Original Research policies. For each suggestion, indicate what kind of reliable sources would be necessary to support the expansion.\n\nSelected text: """${getSelectedText()}"""`;
            }));
            addControlButton('Suggest Relevant Wikilinks', 'Identify terms in selected text for appropriate wikilinking per MoS:LINKS.', getQueryFunction(selectionLimit(), function(){
                return `Review the selected text and identify all terms that should be wikilinked to other Wikipedia articles. Follow the guidelines in MoS:LINKS (e.g., link on first appropriate occurrence, avoid overlinking, ensure relevance, do not link common words).\n\nSelected text: """${getSelectedText()}"""`;
            }));

            addControlLine();

            // Article-level action buttons
            addControlButton('Suggest Article Outline', 'Generate a policy-compliant article outline for the current topic.', async function(){
                let userMessageText = `Generate a comprehensive and well-structured article outline for a Wikipedia article on the topic "${getTitle()}". The outline must adhere to Wikipedia's Manual of Style for article structure (including a compelling lead section, logical main sections and sub-sections, and standard appendix sections like 'See also', 'References', 'External links'). Each proposed section should clearly promote Neutral Point of View, Verifiability, and cover the topic thoroughly without introducing Original Research.`;
                let customChatHistory = [createUserMessage(userMessageText)];
                logUserMessage(userMessageText);
                await getResponse(customChatHistory, true); // true for isCustomQuery
            });

            addControlButton('Analyze Entire Article (Wikitext)', 'Fetch & analyze full article wikitext for policy/MoS. (Advanced model recommended)', async function(){
                if (!localStorage.getItem('WikiChatbotGeminiAPIKey')) {
                    logErrorMessage('Gemini API Key is not set. Please set it using the button below before using this feature.');
                    return;
                }
                const articleWikitext = await getArticleWikitext();
                if (!articleWikitext) {
                    logErrorMessage('Failed to fetch article wikitext. Cannot proceed with analysis.');
                    return;
                }

                const currentFullArticleLimit = fullArticleCharSoftLimit();
                if (articleWikitext.length > currentFullArticleLimit) {
                    const proceed = confirm(`The article wikitext is very long (${(articleWikitext.length / 1024).toFixed(2)} KB, approx. ${articleWikitext.length} chars). This is ${((articleWikitext.length / currentFullArticleLimit)*100).toFixed(0)}% of the script's current soft character limit for this action (${currentFullArticleLimit} chars). While Gemini 1.5 models have large context windows, sending extremely large inputs may be slow, costly, or hit API request size limits. Do you want to proceed?`);
                    if (!proceed) {
                        logBotMessage('Full article analysis cancelled by user due to length.');
                        return;
                    }
                }

                let userMessageText = `The following is the full **wikitext** of the Wikipedia article titled "${getTitle()}". Please act as an experienced Wikipedia editor conducting a peer review. Provide a comprehensive and actionable analysis focusing on adherence to Wikipedia's core content policies (Neutral Point of View, Verifiability, No Original Research) and the Manual of Style (formal tone, encyclopedic language, clarity, structure, sourcing, wikilinks, templates, categories, accessibility). Identify specific areas for improvement directly within the provided wikitext. Suggest ways to enhance neutrality, strengthen sourcing (mentioning where citations are weak or missing), improve clarity and flow, and resolve any MoS violations. Pay attention to wikitext markup (e.g., correct template usage, list formatting, table structure, magic words) and how it could be improved. Provide actionable feedback with specific examples from the wikitext where possible. Prioritize major policy or MoS issues first, then address more minor stylistic points. Frame your feedback constructively.

Full Wikitext:
"""${articleWikitext}"""`;
                let customChatHistory = [createUserMessage(userMessageText)];
                // Log a shorter message to the user interface
                logUserMessage(`Requesting full analysis of article wikitext for "${getTitle()}" (length: ${articleWikitext.length} chars). This may take some time...`);
                await getResponse(customChatHistory, true); // true for isCustomQuery
            });

            addControlLine();
            addModelAndAPIKeySettings(); // Settings for model choice and API key
        }

        function addControlButton(heading, tooltip, clickFunction){
            let button = document.createElement('button');
            reRotateControl.appendChild(button);
            Object.assign(button.style, {
                width: '100%', marginTop: '5px', marginBottom: '5px', borderRadius: '5px',
                border: '1px solid black', textAlign: 'left', padding: '4px 8px', fontSize: '0.9em'
            });
            button.innerHTML = heading;
            button.title = tooltip;
            button.onclick = clickFunction;
        }

        function addControlLine(){
            const borderLine = document.createElement('div');
            reRotateControl.appendChild(borderLine);
            Object.assign(borderLine.style, {
                width: '100%', marginTop: '5px', marginBottom: '5px', borderBottom: '1px solid grey'
            });
        }

        function getQueryFunction(currentSelectionLimit, promptFunction){
            return async function(){
                let selectedText = getSelectedText();
                // Some functions might not require selected text, e.g., if they operate on page title.
                // This check needs to be specific to the function if `getSelectedText()` is truly optional.
                // For now, assuming most `getQueryFunction` calls expect selected text.
                if(selectedText.length < 1){
                    logErrorMessage("No text was selected. Please use the mouse to select text from the article for this action.");
                    return;
                }
                if(selectedText.length > currentSelectionLimit){
                    logErrorMessage(`The selected text was too long: ${selectedText.length} characters were selected, but the limit for this action is ${currentSelectionLimit} characters.`);
                    return;
                }
                const promptText = promptFunction(); // This function generates the actual prompt text
                clearHistory(messages); // Clears previous user/bot messages, keeps system prompt
                messages.push(createUserMessage(promptText));
                logUserMessage(promptText);
                await getResponse(messages);
            };
        }

        function addModelAndAPIKeySettings() {
            const modelDiv = document.createElement('div');
            reRotateControl.appendChild(modelDiv);
            modelDiv.style.width = '100%';

            const checkbox = document.createElement('input');
            modelDiv.appendChild(checkbox);
            Object.assign(checkbox, { type: 'checkbox', id: 'advancedModelCheckbox', style: 'margin: 5px;' });
            checkbox.checked = localStorage.getItem('WikiChatbotAdvancedModel') === 'true';

            const label = document.createElement('label');
            modelDiv.appendChild(label);
            label.htmlFor = 'advancedModelCheckbox';
            label.style.fontSize = 'small';

            function updateLabelAndTitle() {
                updateModelAndTokenLimit(); // Ensure model and tokenLimit are current
                const currentModelName = model.includes('pro') ? 'Gemini 1.5 Pro (Advanced)' : 'Gemini 1.5 Flash (Standard)';
                label.innerHTML = ` Use ${currentModelName}`;
                label.title = `Currently using: ${model}. Toggling switches model. 'Pro' is more capable for complex tasks/long texts but may be slower/costlier. 'Flash' is faster for general tasks. Both have large context windows (up to 1M tokens). Script's character limit for forming prompts is ~${charLimit().toLocaleString()} chars.`;
            }
            updateLabelAndTitle(); // Initial setup

            checkbox.onchange = function(){
                localStorage.setItem('WikiChatbotAdvancedModel', checkbox.checked.toString());
                updateLabelAndTitle(); // Update label, model, and limits
                messages = getInitialMessages(); // Regenerate initial messages with new context limit
                logBotMessage(`Model changed to ${model}.`);
            };

            addControlLine();

            addControlButton('Set Gemini API key', 'Enter your Google AI Gemini API key. Stored locally.', function(){
                let currentAPIKey = localStorage.getItem('WikiChatbotGeminiAPIKey') || '';
                let input = prompt('Please enter your Gemini API key. It will be saved locally in your browser and used only for your queries to Google AI. To delete, leave empty and press OK.', currentAPIKey);
                if(input !== null){ // User did not press Cancel
                    localStorage.setItem('WikiChatbotGeminiAPIKey', input.trim());
                    logBotMessage(input.trim() ? 'Gemini API Key saved.' : 'Gemini API Key cleared.');
                }
            });
        }
    }

    function createChatUI(){
        chatContainer = document.createElement('div');
        if(localStorage.getItem('WikiChatbotActivated') === 'true') chatContainer.style.display = '';
        else chatContainer.style.display = 'none';
        bodyContent.appendChild(chatContainer);
        Object.assign(chatContainer.style, {
            position: 'fixed', bottom: '10px', left: '10px', width: '50%', height: '40%',
            backgroundColor: backgroundColor, resize: 'both', overflow: 'hidden', // overflow hidden on container
            transform: 'rotateX(180deg)', padding: '5px', borderRadius: '10px', zIndex: '999',
            display: 'flex', flexDirection: 'column' // For chatlog and input box
        });

        const reRotateChat = document.createElement('div');
        chatContainer.appendChild(reRotateChat);
        Object.assign(reRotateChat.style, {
            width: '100%', height: '100%', overflow: 'hidden', // overflow hidden
            transform: 'rotateX(180deg)',
            display: 'flex', flexDirection: 'column'
        });

        chatLog = document.createElement('div');
        reRotateChat.appendChild(chatLog);
        Object.assign(chatLog.style, { width: '100%', overflowY: 'auto', flex: 1, marginBottom: '5px' }); // scroll on chatLog

        const chatResponseArea = document.createElement('div'); // Renamed from chatResponse
        reRotateChat.appendChild(chatResponseArea);
        Object.assign(chatResponseArea.style, { width: '100%', height: 'auto', minHeight:'45px', display: 'flex', marginTop: '5px' });

        const chatTextarea = document.createElement('textarea');
        chatResponseArea.appendChild(chatTextarea);
        Object.assign(chatTextarea.style, {
            flexGrow: '1', backgroundColor: backgroundColorUser, resize: 'none',
            marginRight: '10px', borderRadius: '5px', padding: '5px', border: '1px solid #ccc',
            fontFamily: 'sans-serif', fontSize: '0.9em'
        });
        chatTextarea.placeholder = 'Enter questions/commands... (selected text context added automatically)';
        chatTextarea.title = 'If text was selected from the article, you can refer to it as "the selected text" in your prompt. It will be appended.';
        chatTextarea.onkeydown = function(event){
            if (event.key === 'Enter' && !event.shiftKey){ event.preventDefault(); chatSend.click(); }
        };

        let storedSelection = ''; // To store text selected on the page
        // Capture selection when textarea is focused, as page selection might be lost
        chatTextarea.addEventListener('focus', () => {
            const currentSelection = getSelectedText();
            if (currentSelection) {
                storedSelection = currentSelection;
                 // Optionally, notify user: chatTextarea.placeholder = `Selected: "${currentSelection.substring(0,30)}..." Enter prompt:`;
            }
        });
         chatTextarea.addEventListener('blur', () => {
             // Reset placeholder if needed, or clear storedSelection if not used.
             // For now, selection persists until next focus or send.
         });


        chatSend = document.createElement('button');
        chatResponseArea.appendChild(chatSend);
        chatSend.innerHTML = 'Send';
        Object.assign(chatSend.style, { height: '100%', padding: '0 15px', borderRadius: '5px', border: '1px solid black', alignSelf: 'stretch' });
        chatSend.title = 'Send your command/question to WikiChatbot';

        chatSend.onclick = async function(){
            let promptText = chatTextarea.value.trim();

            // Use selection captured at focus, or try to get current page selection if textarea was not focused prior
            let textToAppend = storedSelection || getSelectedText();
            storedSelection = ''; // Clear after retrieval for this send action

            if (!promptText && !textToAppend) { // Allow sending just selected text with a default action or empty prompt
                logErrorMessage("Please enter a prompt, or select text from the article to work on.");
                return;
            }

            const currentPromptLimit = promptLimit();
            if(promptText.length > currentPromptLimit){
                logErrorMessage(`Your typed prompt is too long: ${promptText.length} characters (limit for typed prompt is ${currentPromptLimit}).`);
                return;
            }

            const currentSelectionLimit = selectionLimit();
            if(textToAppend.length > currentSelectionLimit){
                logErrorMessage(`The selected text from the article is too long: ${textToAppend.length} characters (limit for selected text is ${currentSelectionLimit}).`);
                return; // Do not proceed if selected text is too long
            }

            chatTextarea.value = ''; // Clear textarea after processing
            chatTextarea.placeholder = 'Enter questions/commands...'; // Reset placeholder

            if(textToAppend.length > 0){
                // Append selected text clearly demarcated
                promptText += `\n\n--- Selected Text from Article for Context ---\n"""${textToAppend}"""\n--- End of Selected Text ---`;
            }

            imposeHistoryLimit(messages);
            messages.push(createUserMessage(promptText));
            logUserMessage(promptText); // Log the full prompt including appended text
            await getResponse(messages);
        };
    }

    async function getResponse(currentMessages, isCustomQuery = false) {
        disableButtons();
        const apiKey = localStorage.getItem('WikiChatbotGeminiAPIKey');
        if (!apiKey) {
            logErrorMessage('Gemini API Key is not set. Please use the "Set Gemini API key" button in the controls.');
            enableButtons();
            return null;
        }

        const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
        let systemInstructionContent = "";
        const geminiContents = [];

        // Prepare messages for Gemini API format
        currentMessages.forEach(msg => {
            if (msg.role === "system_instruction_holder") systemInstructionContent = msg.content;
            else if (msg.role === "user") geminiContents.push({ role: "user", parts: [{ text: msg.content }] });
            else if (msg.role === "model" || msg.role === "assistant") geminiContents.push({ role: "model", parts: [{ text: msg.content }] });
        });

        // Ensure conversation starts with user if only system prompt exists (should not happen with current logic)
        // or if last message was model (valid for Gemini)

        const body = { contents: geminiContents, generationConfig: { temperature: temperature } };
        if (systemInstructionContent) body.systemInstruction = { parts: [{ text: systemInstructionContent }] };
        
        let responseTextContent = null;

        try {
            const response = await fetch(url, {
                method: "POST", body: JSON.stringify(body), headers: { "Content-Type": "application/json" }
            });

            if (response.ok) {
                const json = await response.json();
                // Check for content and parts, and also for blocked responses
                if (json.candidates && json.candidates[0] && json.candidates[0].content && json.candidates[0].content.parts && json.candidates[0].content.parts[0] && json.candidates[0].finishReason !== "SAFETY") {
                    responseTextContent = json.candidates[0].content.parts[0].text;
                    if (!isCustomQuery) messages.push({ role: "model", content: responseTextContent });
                    
                    let logText = responseTextContent;
                    if (displayWarningMessage) {
                        logText = `(Please critically review the following AI-generated suggestions and verify all information with reliable sources before applying any changes to Wikipedia articles. AI can make mistakes or misinterpret context.)\n\n${responseTextContent}`;
                        displayWarningMessage = false; // Reset after showing
                    }
                    logBotMessage(logText);

                } else {
                    let errorReason = "Received an empty or malformed response from the API.";
                    if (json.candidates && json.candidates[0] && json.candidates[0].finishReason) {
                         errorReason = `Content generation stopped. Reason: ${json.candidates[0].finishReason}.`;
                         if (json.candidates[0].safetyRatings) {
                            errorReason += ` Safety Ratings: ${JSON.stringify(json.candidates[0].safetyRatings)}`;
                         }
                    } else if (json.promptFeedback && json.promptFeedback.blockReason) {
                         errorReason = `Prompt blocked. Reason: ${json.promptFeedback.blockReason}.`;
                         if (json.promptFeedback.safetyRatings) {
                            errorReason += ` Safety Ratings: ${JSON.stringify(json.promptFeedback.safetyRatings)}`;
                         }
                    }
                    logErrorMessage(errorReason);
                    if (!isCustomQuery && geminiContents.length > 0) messages.pop(); // Remove last user message on failure
                }
            } else { // HTTP error (not 2xx)
                const errorData = await response.json().catch(() => ({ error: { message: `HTTP error ${response.status} and failed to parse error body.` } }));
                logErrorMessage(composeGeminiErrorMessage(response.status, errorData.error ? errorData.error.message : `HTTP ${response.status}`));
                if (!isCustomQuery && geminiContents.length > 0) messages.pop(); // Remove last user message
            }
        } catch (error) { // Network error or other fetch-related issues
            logErrorMessage(`Network error or script error during fetch: ${error.message || error.toString()}`);
            console.error("getResponse error:", error);
            if (!isCustomQuery && geminiContents.length > 0) messages.pop(); // Remove last user message
        } finally {
            enableButtons();
        }
        return responseTextContent; // Return text for custom queries if needed
    }

    function composeGeminiErrorMessage(errorCode, additionalMessage){
        let specificAdvice = '';
        if (errorCode === 400) specificAdvice = 'Malformed request. Check console for details. Content might be too large or format incorrect.';
        else if (errorCode === 401 || errorCode === 403) specificAdvice = 'Authentication error. API key might be invalid, not enabled, or billing issue.';
        else if (errorCode === 429) specificAdvice = 'Rate limit exceeded or quota exhausted. Please wait or check your Google AI console.';
        else if (errorCode >= 500) specificAdvice = 'Google AI server error. Please try again later.';
        return `Gemini API Error Code: ${errorCode}. Message: ${additionalMessage}. ${specificAdvice}`;
    }


    function disableButtons(){
        if (chatSend) chatSend.disabled = true;
        if (reRotateControl) Array.from(reRotateControl.getElementsByTagName('button')).forEach(b => b.disabled = true);
    }

    function enableButtons(){
        if (chatSend) chatSend.disabled = false;
        if (reRotateControl) Array.from(reRotateControl.getElementsByTagName('button')).forEach(b => b.disabled = false);
    }

    function articleToObject(){
        const articleObject = {sectionTitles: ['Lead'], sectionContents: ['']};
        const contentTextElement = document.getElementById('mw-content-text');
        if (!contentTextElement || !contentTextElement.children[0]) {
            // console.warn("Wikichatbot: Could not find mw-content-text or its first child for context snippet.");
            return articleObject;
        }
        // Clone only necessary part for context, not entire article DOM for this function
        const articleContentClone = contentTextElement.children[0].cloneNode(true);
        const allowedElements = 'p, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dd, dt'.split(', '); // Include list items for context
        
        // Basic cleanup for context snippet - remove obviously non-prose elements
        articleContentClone.querySelectorAll('table, .infobox, .thumb, .gallery, .navbox, .metadata, .catlinks, #toc, .mw-editsection, .reference, .noprint').forEach(el => el.remove());

        let childNodes = Array.from(articleContentClone.childNodes).filter(node => node.nodeType === 1 || (node.nodeType === 3 && node.textContent.trim() !== '')); // Element nodes or non-empty text nodes

        let index = 0;
        for(const child of childNodes){
            if(child.nodeType === 1 && child.tagName.match(/^H[1-6]$/i) && child.classList.contains('mw-heading')){ // mw-heading check for WP headings
                 let titleText = (child.querySelector('.mw-headline') || child).innerText.replace(/\[edit\]$/, '').trim();
                 if (index > 0 || articleObject.sectionContents[0].length > 50) { // Start new section if not first or lead has some content
                    articleObject.sectionTitles.push(titleText);
                    index++; articleObject.sectionContents[index] = '';
                 } else if (index === 0 && !articleObject.sectionTitles[0].includes(titleText)) { // If still in lead, append to title if distinct
                     articleObject.sectionTitles[0] += ` / ${titleText}`;
                 }
            } else {
                if (articleObject.sectionContents[index] === undefined) articleObject.sectionContents[index] = '';
                articleObject.sectionContents[index] += (child.innerText || child.textContent) + '\n\n';
            }
        }
        return articleObject;
    }

    function getContext(){ // For initial system prompt context snippet
        const articleObject = articleToObject();
        let context = (articleObject.sectionTitles[0] === 'Lead' ? '' : articleObject.sectionTitles[0] + "\n\n") + (articleObject.sectionContents[0] || "");
        // Try to get a bit more if lead is very short
        if(context.length < 1000 && articleObject.sectionContents.length > 1 && articleObject.sectionTitles.length > 1){
            context += '\n\n' + (articleObject.sectionTitles[1] || "") + '\n\n' + (articleObject.sectionContents[1] || "");
        }
        return context.substring(0, articleContextLimit()); // Use defined limit
    }

    function getSelectedText(){
        // Hiding refs during selection copy can prevent copying "[1][2]" etc.
        hideRefs();
        let selectedText = window.getSelection().toString().trim();
        showRefs();
        return selectedText;
    }

    function hideRefs(){ document.body.querySelectorAll('.reference, .mw-ref').forEach(r => r.style.display = 'none'); }
    function showRefs(){ document.body.querySelectorAll('.reference, .mw-ref').forEach(r => r.style.display = ''); }

    function createUserMessage(promptText){ return {"role":"user","content": promptText}; }

    function imposeHistoryLimit(msgs){ // msgs is the main 'messages' array
        const systemMsg = msgs[0]; // Preserve system message
        let chatHistory = msgs.slice(1); // User/model messages
        let currentLength = getMessagesLength(chatHistory); // Length of user/model messages only
        const limit = historyLimit();

        while(currentLength > limit && chatHistory.length > 1){ // Keep at least one exchange if possible
            // Remove the oldest user/model message pair (or single oldest if unpaired)
            let removed = chatHistory.shift();
            currentLength -= (removed.content ? removed.content.length : 0);
            if (chatHistory.length > 0 && chatHistory[0].role !== (removed.role === 'user' ? 'model' : 'user')) {
                // if roles are not alternating, remove next one too to keep pairs, this is simplistic
                let removed2 = chatHistory.shift();
                if (removed2) currentLength -= (removed2.content ? removed2.content.length : 0);
            }
        }
        messages.length = 0; // Clear original global messages array
        messages.push(systemMsg, ...chatHistory); // Reconstruct with preserved system message and trimmed history
    }

    function clearHistory(msgsToClear){ // msgsToClear is the main 'messages' array
        // Keep the first message (system prompt with context)
        while(msgsToClear.length > 1){
            msgsToClear.pop();
        }
    }

    function getMessagesLength(arrayOfMessages){
        return arrayOfMessages.reduce((total, msg) => total + (msg.content ? msg.content.length : 0), 0);
    }

    function logBotMessage(text){ logMessage(`<strong>Bot:</strong> ${text}`, backgroundColorBot, '0.1em', '1em'); }
    function logUserMessage(text){ logMessage(`<strong>User:</strong> ${text}`, backgroundColorUser, '1em', '0.1em'); }
    function logErrorMessage(text){ logMessage(`<strong>Error:</strong> ${text}`, backgroundColorError, '0.1em', '0.1em'); }

    function logMessage(htmlContent, msgBgColor, marginLeft, marginRight){
        let pre = document.createElement('pre');
        pre.innerHTML = htmlContent; // Use innerHTML to render bold tags and links
        Object.assign(pre.style, {
            backgroundColor: msgBgColor, margin: '0.2em', padding: '0.5em',
            marginRight: marginRight, marginLeft: marginLeft, borderRadius: '5px',
            fontFamily: 'sans-serif', whiteSpace: 'pre-wrap', wordBreak: 'break-word',
            fontSize: '0.9em', lineHeight: '1.4'
        });
        chatLog.appendChild(pre);
        chatLog.scrollTop = chatLog.scrollHeight; // Auto-scroll to bottom
    }

    function getTitle(){
        let firstHeadingElement = document.getElementById('firstHeading');
        let pageTitle = mw.config.get('wgTitle'); // More reliable for actual title
        let displayTitle = (pageTitle ? pageTitle : firstHeadingElement.innerText) || "Current Page";

        // Clean up common prefixes from display title if present
        if(displayTitle.substring(0, 8).toLowerCase() === 'editing ') displayTitle = displayTitle.substring(8);
        if(displayTitle.substring(0, 6).toLowerCase() === 'draft:') displayTitle = displayTitle.substring(6);

        displayTitle = displayTitle.split("/").reverse()[0];
        
        // Use wgPageName for contexts where canonical name is better (like API calls)
        // For display in prompts, wgTitle or cleaned firstHeading is usually fine.
        return displayTitle.trim();
    }

    function addPortletAndActivate(){
        // Use unique IDs for portlet links
        const activateLinkId = 'ptb-wikichatbot-activate-link';
        const deactivateLinkId = 'ptb-wikichatbot-deactivate-link';

        const activatePortlet = mw.util.addPortletLink('p-tb', '#', 'Activate WikiChatbot', activateLinkId, 'Activate the Wikichatbot editing assistant');
        if(activatePortlet) activatePortlet.onclick = e => { e.preventDefault(); activate(); };

        const deactivatePortlet = mw.util.addPortletLink('p-tb', '#', 'Deactivate WikiChatbot', deactivateLinkId, 'Deactivate the Wikichatbot editing assistant');
        if(deactivatePortlet) deactivatePortlet.onclick = e => { e.preventDefault(); deactivate(); };

        // Create UIs once and then show/hide, prevents re-creating DOM elements repeatedly
        createControlUI();
        createChatUI();

        if(localStorage.getItem('WikiChatbotActivated') === 'true') activate();
        else deactivate(); // Ensures UI is hidden if not active

        function activate(){
            localStorage.setItem('WikiChatbotActivated', 'true');
            if (activatePortlet) $(activatePortlet).hide();
            if (deactivatePortlet) $(deactivatePortlet).show();

            if(controlContainer) controlContainer.style.display = 'flex';
            if(chatContainer) chatContainer.style.display = 'flex'; // Changed from '' to 'flex'

            if (chatLog && chatLog.children.length === 0) { // Only log welcome if chat is empty
                 logBotMessage('Wikichatbot activated. Select text on the page and use controls on the right, or type your questions/commands below. Remember to critically review all AI suggestions and verify information against <a href="https://en.wikipedia.org/wiki/Wikipedia:Core_content_policies" target="_blank">Wikipedia\'s policies</a> and reliable sources before making edits. See <a href="https://en.wikipedia.org/wiki/Wikipedia:Large_language_models" target="_blank">WP:LLM</a> guidelines.');
            }
        }
        function deactivate(){
            localStorage.setItem('WikiChatbotActivated', 'false');
            if (deactivatePortlet) $(deactivatePortlet).hide();
            if (activatePortlet) $(activatePortlet).show();
            if(controlContainer) controlContainer.style.display = 'none';
            if(chatContainer) chatContainer.style.display = 'none';
        }
    }
})();