Canvas AI Panel (OpenRouter)

Adds an AI assistant sidepanel using OpenRouter to Instructure Canvas pages.

As of 14. 05. 2025. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Canvas AI Panel (OpenRouter)
// @namespace    http://tampermonkey.net/
// @version      1.7.1_OpenRouter_ModUI
// @description  Adds an AI assistant sidepanel using OpenRouter to Instructure Canvas pages.
// @author       Original by Riley Campbell, AI modifications, OpenRouter mod, UI Mod by patmarvs
// @match        *.instructure.com/*
// @license      https://opensource.org/license/bsd-3-clause/
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

(async function() {
    'use strict';

    // Helper function to log messages for this script
    function scriptLog(message) {
        console.log(`[Canvas AI Sidepanel] ${message}`);
    }

    scriptLog('Script starting... (OpenRouter Edition, ModUI)');

    if (window.location.href.includes("conversations")) {
        scriptLog('On a "conversations" page, exiting script as per original design.');
        return;
    }

    // --- Storage Functions ---
    const getStoredValue = async (key, defaultValue) => {
        let value = await GM_getValue(key, defaultValue);
        if (value === 'true') return true;
        if (value === 'false') return false;
        return value === undefined ? defaultValue : value;
    };

    const setStoredValue = async (key, value) => {
        await GM_setValue(key, value.toString());
    };

    // --- Initialize Core Variables ---
    let currentAIprompt = await getStoredValue('AIprompt', '');
    if (currentAIprompt === null || currentAIprompt === undefined || typeof currentAIprompt !== 'string') {
        currentAIprompt = '';
    }

    window.defaultMessage = {
        "role": "system",
        "content": currentAIprompt
    };
    window.AImessages = (currentAIprompt && currentAIprompt.trim() !== "") ? [JSON.parse(JSON.stringify(window.defaultMessage))] : [];

    let chatVisible = await getStoredValue('chatVisible', false); // MODIFIED: Default to hidden
    if (typeof chatVisible !== 'boolean') {
        chatVisible = (chatVisible === 'false'); // Default to false if not a proper boolean string
    }
    window.hotkeySetting = await getStoredValue('chatToggleHotkey', 'Control+Shift+X');


    // --- Core Functions ---

    window.renderMessages = function() {
        scriptLog('Rendering messages...');
        let container = document.getElementById('ai-assistant-container');
        if (!container) {
            scriptLog('Error: ai-assistant-container not found for rendering messages.');
            return;
        }
        container.innerHTML = '';

        const messagesToRender = window.AImessages.filter(message => {
            return !(message.role === "system" && (!message.content || message.content.trim() === ""));
        });

        for (let i = 0; i < messagesToRender.length; i++) {
            let message = messagesToRender[i];
            let originalIndex = window.AImessages.findIndex(m => m === message);

            const messageRow = document.createElement('table');
            messageRow.style.width = '100%';
            const roleText = message.role.charAt(0).toUpperCase() + message.role.slice(1);
            const escapedContent = message.content ? message.content.replace(/</g, "&lt;").replace(/>/g, "&gt;") : "";

            let headerHTML = `<p class="ai-assistant-role">${roleText}: </p>`;
            if (message.role === "user" || message.role === "assistant" || (message.role === "system" && message.content && message.content.trim() !== "")) {
                 if (originalIndex !== -1) {
                    headerHTML += `<button class="ai-assistant-chat-edit-button" data-message-index="${originalIndex}">Edit</button>`;
                }
            }

            messageRow.innerHTML = `
                <tr>
                    <th style="text-align: left; vertical-align: top; width: auto;">${headerHTML}</th>
                    <td data-message-id="${originalIndex}" style="white-space: pre-wrap; word-break: break-word;">${escapedContent.replace(/\n/g, '<br>')}</td>
                </tr>
            `;
            container.appendChild(messageRow);

            if (i < messagesToRender.length - 1) {
                 container.appendChild(document.createElement('hr'));
            }
        }

        container.querySelectorAll('.ai-assistant-chat-edit-button').forEach(button => {
            button.addEventListener('click', function() {
                const messageIndex = parseInt(this.getAttribute('data-message-index'));
                if (isNaN(messageIndex) || messageIndex < 0 || messageIndex >= window.AImessages.length) {
                    scriptLog("Error: Invalid message index for editing.");
                    return;
                }

                const messageContentCell = container.querySelector(`td[data-message-id="${messageIndex}"]`);
                const currentText = window.AImessages[messageIndex].content;

                messageContentCell.innerHTML = `
                    <textarea class="ai-assistant-chat-edit-area">${currentText}</textarea>
                    <button class="ai-assistant-chat-save-button" data-message-index="${messageIndex}">Save</button>
                    <button class="ai-assistant-chat-cancel-button">Cancel</button>`;

                messageContentCell.querySelector('.ai-assistant-chat-save-button').addEventListener('click', async function() {
                    const newText = messageContentCell.querySelector('.ai-assistant-chat-edit-area').value;
                    window.AImessages[messageIndex].content = newText;
                    if (window.AImessages[messageIndex].role === "system") {
                        window.defaultMessage.content = newText;
                        await setStoredValue('AIprompt', newText);
                        document.getElementById('ai-assistant-systemPrompt').value = newText;
                    }
                    window.renderMessages();
                });
                messageContentCell.querySelector('.ai-assistant-chat-cancel-button').addEventListener('click', function() {
                    window.renderMessages();
                });
                const textarea = messageContentCell.querySelector('.ai-assistant-chat-edit-area');
                textarea.focus();
                textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
            });
        });

        if (container) {
            container.scrollTop = container.scrollHeight;
        }
    };

    window.sendMessage = async function() {
        scriptLog('Attempting to send message to OpenRouter...');
        const chatContainer = document.getElementById('ai-assistant-container');
        const myTextArea = document.getElementById('ai-assistant-myTextArea');
        const apiKey = await getStoredValue('openaiAPIKey', ''); // Stored under old name, but it's OpenRouter key
        const aiModel = await getStoredValue('AImodel', 'openai/gpt-3.5-turbo');

        if (!apiKey) {
            alert('OpenRouter API Key is not set. Please set it in the Settings panel (field is labeled OpenAI API Key).');
            scriptLog('API Key missing.');
            return;
        }

        if (!myTextArea || !myTextArea.value.trim()) {
            scriptLog('Message input is empty.');
            return;
        }

        const loadingGifId = 'ai-assistant-loading-gif';
        const existingLoadingGif = document.getElementById(loadingGifId);
        if (chatContainer && !existingLoadingGif) {
            chatContainer.insertAdjacentHTML('beforeend', `<div id="${loadingGifId}" style="text-align:center;"><img style="display: inline-block; width: 25px; margin: 8px auto;" src="https://i.gifer.com/origin/34/34338d26023e5515f6cc8969aa027bca_w200.gif" alt="Loading..."></div>`);
            chatContainer.scrollTop = chatContainer.scrollHeight;
        }

        let userMessage = { "role": "user", "content": myTextArea.value };
        window.AImessages.push(userMessage);
        const userMessageContentForRestore = myTextArea.value;
        myTextArea.value = '';
        window.renderMessages();

        let messagesForAPI = [];
        const memoryEnabled = await getStoredValue('AImemory', false);
        window.defaultMessage.content = document.getElementById('ai-assistant-systemPrompt').value;

        if (memoryEnabled) {
            messagesForAPI = JSON.parse(JSON.stringify(window.AImessages));
            if (window.defaultMessage.content && window.defaultMessage.content.trim() !== "") {
                const systemMsgIndex = messagesForAPI.findIndex(m => m.role === 'system');
                if (systemMsgIndex > -1) {
                    messagesForAPI[systemMsgIndex].content = window.defaultMessage.content;
                } else {
                    messagesForAPI.unshift(JSON.parse(JSON.stringify(window.defaultMessage)));
                }
            } else {
                 messagesForAPI = messagesForAPI.filter(m => m.role !== 'system');
            }
        } else {
            if (window.defaultMessage.content && window.defaultMessage.content.trim() !== "") {
                messagesForAPI.push(JSON.parse(JSON.stringify(window.defaultMessage)));
            }
            messagesForAPI.push(userMessage);
        }

        messagesForAPI = messagesForAPI.filter(m => m.content && m.content.trim() !== "");

        if (messagesForAPI.length === 0) {
            scriptLog('No messages to send to API after filtering.');
            document.getElementById(loadingGifId)?.remove();
            window.AImessages.push({role: "assistant", content: "Internal error: No content to send."});
            window.renderMessages();
            return;
        }

        scriptLog(`Sending to OpenRouter API with model ${aiModel}. Memory: ${memoryEnabled}. Messages count: ${messagesForAPI.length}.`);

        GM_xmlhttpRequest({
            method: "POST",
            url: 'https://openrouter.ai/api/v1/chat/completions',
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${apiKey}`,
            },
            data: JSON.stringify({ "model": aiModel, "messages": messagesForAPI, "temperature": 1.0 }),
            onload: function(response) {
                document.getElementById(loadingGifId)?.remove();
                try {
                    if (response.status >= 200 && response.status < 300) {
                        let result = JSON.parse(response.responseText);
                        if (result.choices && result.choices.length > 0 && result.choices[0].message) {
                            window.AImessages.push(result.choices[0].message);
                        } else {
                            if (result.error && result.error.message) {
                                throw new Error(`API Error: ${result.error.message} (Type: ${result.error.type}, Code: ${result.error.code || 'N/A'})`);
                            }
                            throw new Error("Invalid response structure from API.");
                        }
                    } else {
                        let errorInfo = `API Error ${response.status}: ${response.statusText}`;
                        try {
                            const errData = JSON.parse(response.responseText);
                            if (errData.error && errData.error.message) {
                                errorInfo += ` - ${errData.error.message}`;
                                if(errData.error.type) errorInfo += ` (Type: ${errData.error.type})`;
                                if(errData.error.code) errorInfo += ` (Code: ${errData.error.code})`;
                            }
                        } catch (e) { /* Stick with statusText */ }
                        throw new Error(errorInfo);
                    }
                } catch (e) {
                    scriptLog(`Error processing response: ${e.message}`);
                    const errorP = document.createElement('p');
                    errorP.style.color = 'red'; // Error color still red for visibility
                    errorP.style.padding = '5px';
                    errorP.innerHTML = `<strong>Error:</strong> ${e.message.replace(/</g, "&lt;").replace(/>/g, "&gt;")} <button class="ai-assistant-retry-button">Retry</button>`;
                    if(chatContainer) {
                        chatContainer.appendChild(errorP);
                        chatContainer.scrollTop = chatContainer.scrollHeight;
                    }
                    errorP.querySelector('.ai-assistant-retry-button')?.addEventListener('click', () => {
                        myTextArea.value = userMessageContentForRestore;
                        errorP.remove();
                        scriptLog("Retry button clicked.");
                        myTextArea.focus();
                    });
                } finally {
                    if (! (document.querySelector('#ai-assistant-container p[style*="color: red;"]')) ) {
                        window.renderMessages();
                    }
                     if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
                }
            },
            onerror: function(response) {
                document.getElementById(loadingGifId)?.remove();
                const errorText = `Network Error: ${response.statusText || 'Could not connect'}.`;
                scriptLog(`Request Error: ${errorText}`);
                const errorP = document.createElement('p');
                errorP.style.color = 'red';
                errorP.style.padding = '5px';
                errorP.innerHTML = `<strong>${errorText}</strong>`;
                if(chatContainer) {
                     chatContainer.appendChild(errorP);
                     chatContainer.scrollTop = chatContainer.scrollHeight;
                }
            }
        });
    };

    window.populateModels = async function() {
        scriptLog('Populating models from OpenRouter...');
        const modelsSelect = document.getElementById('ai-assistant-models');
        const apiKey = await getStoredValue('openaiAPIKey', ''); // Stored under old name

        if (!apiKey) {
            scriptLog('API Key missing for populating models.');
            if (modelsSelect) modelsSelect.innerHTML = '<option value="">Set API Key (labeled OpenAI API Key) to load models</option>';
            return;
        }
        if (!modelsSelect) { scriptLog('Models select element not found.'); return; }

        modelsSelect.innerHTML = '<option value="">Loading models from OpenRouter...</option>';
        const commonModels = [
            'openai/gpt-4o', 'openai/gpt-4-turbo', 'anthropic/claude-3-opus', 'anthropic/claude-3-sonnet',
            'anthropic/claude-3-haiku', 'google/gemini-pro-1.5', 'google/gemini-flash-1.5', 'mistralai/mistral-large',
        ];

        GM_xmlhttpRequest({
            method: "GET",
            url: 'https://openrouter.ai/api/v1/models',
            headers: { "Authorization": `Bearer ${apiKey}` },
            onload: async function(response) {
                try {
                    modelsSelect.innerHTML = '';
                    if (response.status >= 200 && response.status < 300) {
                        const options = JSON.parse(response.responseText);
                        let availableModels = [];
                        if (options.data) {
                            options.data.forEach(item => { availableModels.push(item.id); });
                        }
                        availableModels.sort((a, b) => a.localeCompare(b));
                        let effectiveCommonModels = commonModels.filter(m => availableModels.includes(m));
                        let finalModelList = [...new Set([...effectiveCommonModels, ...availableModels])];

                        if (finalModelList.length === 0) {
                            modelsSelect.innerHTML = '<option value="">No models found on OpenRouter.</option>';
                        } else {
                            finalModelList.forEach(modelId => {
                                let element = document.createElement("option");
                                element.value = modelId;
                                element.innerHTML = modelId;
                                modelsSelect.appendChild(element);
                            });
                            const storedModel = await getStoredValue('AImodel', 'openai/gpt-3.5-turbo');
                            if (finalModelList.includes(storedModel)) {
                                modelsSelect.value = storedModel;
                            } else if (finalModelList.length > 0) {
                                modelsSelect.value = finalModelList[0];
                                await setStoredValue('AImodel', finalModelList[0]);
                            }
                        }
                    } else {
                        let errorDetail = `Failed: ${response.status} ${response.statusText}`;
                        try {
                            const errData = JSON.parse(response.responseText);
                            if (errData.error && errData.error.message) {
                                errorDetail = `API Error: ${errData.error.message.substring(0,50)}...`;
                            }
                        } catch (e) { /* ignore */ }
                        modelsSelect.innerHTML = `<option value="">${errorDetail}</option>`;
                        scriptLog(`Failed to fetch models: ${response.status} ${response.responseText}`);
                    }
                } catch (e) {
                    scriptLog(`Error parsing models response: ${e.message}`);
                    modelsSelect.innerHTML = '<option value="">Error parsing models data</option>';
                }
            },
            onerror: function() {
                scriptLog('Network error fetching models.');
                if (modelsSelect) modelsSelect.innerHTML = '<option value="">Network error</option>';
            }
        });
    };

    function setupUI() {
        scriptLog('Setting up UI...');

        const accentGrey = '#6A737C'; // Main grey accent
        const accentGreyHover = '#525960'; // Darker grey for hover
        const lightGreyBg = '#f0f0f0'; // Light grey for some backgrounds
        const lighterGreyBg = '#f8f9fa'; // Even lighter for collapsible content
        const focusRingColor = 'rgba(106, 115, 124, .5)'; // Grey focus ring


        const css = `
            #ai-assistant-box { position: fixed; bottom: 15px; right: 15px; width: 450px; height: 40vh; /* MODIFIED: Fixed height */ background-color: #fff; box-shadow: 0 5px 20px rgba(0,0,0,0.25); border-radius: 12px; display: flex; flex-direction: column; z-index: 10001; border: 1px solid #ccc; font-family: Arial, sans-serif; font-size: 14px; transform: translateY(0); opacity: 1; transition: transform 0.3s ease-out, opacity 0.3s ease-out; }
            #ai-assistant-box.hidden { transform: translateY(calc(100% + 30px)); opacity: 0; pointer-events: none; }
            .ai-assistant-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 15px; background: ${accentGrey}; /* MODIFIED */ color: white; border-radius: 11px 11px 0 0; cursor: default; }
            .ai-assistant-header-title { font-weight: bold; font-size: 1.1em; } /* Title: "AI Assistant" */
            .ai-assistant-toggle-visibility-header { background: none; border: none; color: white; font-size: 1.5em; cursor: pointer; padding: 0 5px;}
            .ai-assistant-toggle-visibility-header:hover { opacity:0.8; }
            #ai-assistant-box .ai-assistant-main-content { padding:15px; display: flex; flex-direction: column; flex-grow:1; overflow:hidden; background: #fdfdfd; }
            #ai-assistant-container { flex-grow: 1; overflow-y: auto; padding: 10px; border-bottom: 1px solid #eee; margin-bottom: 10px; min-height: 100px; /* min-height can be less critical with fixed overall box */ background: #fff; border: 1px solid #e0e0e0; border-radius: 4px; scroll-behavior: smooth;}
            #ai-assistant-container table { width: 100%; margin-bottom: 8px; border-collapse: collapse; }
            #ai-assistant-container th { font-weight: bold; text-align: left; vertical-align:top; padding: 4px 8px 4px 2px; color: #333; }
            #ai-assistant-container td { padding: 4px 2px 4px 8px; color: #555; }
            #ai-assistant-container hr { border: 0; border-top: 1px solid #f0f0f0; margin: 8px 0; }
            #ai-assistant-myTextArea { display: block; width: calc(100% - 22px); min-height:60px; max-height: 150px; resize: vertical; margin: 0 auto 10px auto; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size:1em; }
            #ai-assistant-myTextArea:focus { border-color: ${accentGrey}; box-shadow: 0 0 0 0.2rem ${focusRingColor}; } /* MODIFIED */
            #ai-assistant-box .ai-assistant-button { background-color: ${accentGrey}; /* MODIFIED */ color: white; border: none; padding: 8px 15px; margin: 0 5px 5px 0; border-radius: 5px; cursor: pointer; text-align: center; font-size:0.9em; transition: background-color 0.2s ease; }
            #ai-assistant-box .ai-assistant-button:hover { background-color: ${accentGreyHover}; } /* MODIFIED */
            #ai-assistant-button-bar { display: flex; justify-content: space-between; gap: 10px; }
            #ai-assistant-sendButton { flex-grow:1; background-color: ${accentGrey}; } /* MODIFIED */
            #ai-assistant-sendButton:hover { background-color: ${accentGreyHover}; } /* MODIFIED */
            #ai-assistant-clearHistoryButton { background-color: ${accentGrey}; } /* MODIFIED specific clear button if needed, else inherits */
            #ai-assistant-clearHistoryButton:hover { background-color: ${accentGreyHover}; } /* MODIFIED */

            .ai-assistant-settings-toggle { display: block; font-weight: bold; font-size: 1em; text-align: center; padding: 10px; color: ${accentGreyHover}; /* MODIFIED */ background: ${lightGreyBg}; /* MODIFIED */ cursor: pointer; border-top: 1px solid #ddd; transition: background-color 0.2s ease-out; margin:0; border-radius: 0 0 11px 11px; }
            .ai-assistant-settings-toggle:hover { background-color: #e2e6ea; } /* MODIFIED */
            .ai-assistant-collapsible-content { max-height: 0px; overflow-y: auto; transition: max-height .35s ease-in-out; background: ${lighterGreyBg}; /* MODIFIED */ border-top:1px solid #ddd;}
            .ai-assistant-collapsible-content .content-inner { padding: 15px; border-bottom-left-radius: 11px; border-bottom-right-radius: 11px; }
            #ai-assistant-settings-checkbox:checked ~ .ai-assistant-collapsible-content { max-height: 450px; }
            #ai-assistant-settings-checkbox { display: none; }
            .ai-assistant-switch-container { display: flex; align-items: center; margin-bottom:12px; }
            .ai-assistant-switch { position: relative; display: inline-block; width: 44px; height: 24px; margin-right: 10px; }
            .ai-assistant-switch input { opacity: 0; width: 0; height: 0; }
            .ai-assistant-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; }
            .ai-assistant-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
            input:checked + .ai-assistant-slider { background-color: ${accentGrey}; } /* MODIFIED */
            input:checked + .ai-assistant-slider:before { transform: translateX(20px); }
            #ai-assistant-systemPrompt { display: block; width: calc(100% - 22px); min-height: 70px; max-height: 150px; resize: vertical; margin: 5px auto 10px auto; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size:0.9em; }
            #ai-assistant-apiKey { display: inline-block; width: calc(60% - 10px); margin-right:5px; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size:0.9em; box-sizing: border-box; }
            #ai-assistant-setAPIKeyButton { display: inline-block; width: calc(40% - 10px); padding: 8px 0px; box-sizing: border-box;}
            .ai-assistant-chat-edit-button, .ai-assistant-chat-save-button, .ai-assistant-chat-cancel-button { background-color: ${accentGrey}; /* MODIFIED */ font-size: 0.8em; padding: 3px 6px; margin-left: 8px; border-radius:3px; color:white; border:none; cursor:pointer; }
            .ai-assistant-chat-edit-button:hover, .ai-assistant-chat-save-button:hover, .ai-assistant-chat-cancel-button:hover { background-color: ${accentGreyHover}; } /* MODIFIED */
            .ai-assistant-chat-edit-area { display:block; width: 98%; min-height: 60px; max-height: 120px; resize: vertical; margin: 5px 0; font-size:1em; padding:5px; border-radius:4px; border:1px solid #ccc; }
            .ai-assistant-role { display: inline; margin-right: 5px; font-weight:bold;}
            #ai-assistant-models { margin-bottom:10px; display:block; width:100%; padding:8px; border-radius:4px; border:1px solid #ccc; font-size:0.9em; box-sizing: border-box;}
            .ai-assistant-label { display: block; margin-bottom: 5px; font-weight: bold; font-size:0.9em; }
            .ai-assistant-retry-button { background-color: ${accentGrey}; color: white; border: none; padding: 3px 8px; border-radius: 3px; cursor: pointer; font-size: 0.9em; margin-left: 10px;}
            .ai-assistant-retry-button:hover { background-color: ${accentGreyHover};}

            #ai-assistant-hotkey-input-container { margin-top: 10px; margin-bottom: 5px; }
            #ai-assistant-hotkey-input { width: calc(60% - 10px); margin-right:5px; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size:0.9em; box-sizing: border-box;}
            #ai-assistant-setHotkeyButton {display: inline-block; width: calc(40% - 10px); padding: 8px 0px; box-sizing: border-box;}

            #ai-assistant-toggle-button { position: fixed; bottom: 20px; right: 20px; z-index: 10000; background-color: ${accentGrey}; /* MODIFIED */ color: white; border:none; border-radius: 50%; width: 50px; height: 50px; font-size: 24px; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; transition: background-color 0.2s ease, transform 0.2s ease; }
            #ai-assistant-toggle-button:hover { background-color: ${accentGreyHover}; transform: scale(1.1); } /* MODIFIED */
            #ai-assistant-toggle-button.hidden { display: none !important; } /* MODIFIED: Ensure button also hides */

        `;
        GM_addStyle(css);

        const chatToggleBtn = document.createElement('button');
        chatToggleBtn.id = 'ai-assistant-toggle-button';
        chatToggleBtn.innerHTML = '&#128172;';
        chatToggleBtn.title = `Toggle AI Assistant (Hotkey: ${window.hotkeySetting})`;
        if (!chatVisible) { // MODIFIED: Initially hide toggle button if chat is not visible
            chatToggleBtn.classList.add('hidden');
        }
        document.body.appendChild(chatToggleBtn);


        const elem = document.createElement('div');
        elem.id = 'ai-assistant-box';
        if (!chatVisible) {
            elem.classList.add('hidden');
            chatToggleBtn.innerHTML = '&#128172;';
        } else {
            elem.classList.remove('hidden');
            chatToggleBtn.innerHTML = '&#10005;';
        }

        elem.innerHTML = `
            <div class="ai-assistant-header">
                <span class="ai-assistant-header-title">AI Assistant</span> <button class="ai-assistant-toggle-visibility-header" title="Hide AI Assistant (Hotkey: ${window.hotkeySetting})">&#10005;</button>
            </div>
            <div class="ai-assistant-main-content">
                <div id="ai-assistant-container"></div>
                <textarea id="ai-assistant-myTextArea" placeholder="Ask AI Assistant... (Shift+Enter for new line, Enter to send)"></textarea>
                <div id="ai-assistant-button-bar">
                    <button class="ai-assistant-button" id="ai-assistant-clearHistoryButton">Clear & Apply Prompt</button>
                    <button class="ai-assistant-button" id="ai-assistant-sendButton">Send</button>
                </div>
            </div>
            <input id="ai-assistant-settings-checkbox" type="checkbox">
            <label for="ai-assistant-settings-checkbox" class="ai-assistant-settings-toggle">Settings <span class="settings-arrow">&#9660;</span></label>
            <div class="ai-assistant-collapsible-content">
                <div class="content-inner">
                    <div class="ai-assistant-switch-container">
                        <label class="ai-assistant-switch">
                            <input id="ai-assistant-memory" type="checkbox">
                            <span class="ai-assistant-slider"></span>
                        </label>
                        <label for="ai-assistant-memory" style="font-size:0.9em;">Enable chat memory</label>
                    </div>
                    <label for="ai-assistant-models" class="ai-assistant-label">Model:</label>
                    <select name="models" id="ai-assistant-models"></select>
                    <label for="ai-assistant-systemPrompt" class="ai-assistant-label">System Prompt (Instructions for AI):</label>
                    <textarea id="ai-assistant-systemPrompt" placeholder="e.g., Act as a helpful teaching assistant."></textarea>
                    <label for="ai-assistant-apiKey" class="ai-assistant-label">OpenAI API Key:</label> <div>
                        <input id="ai-assistant-apiKey" placeholder="sk-xxxxxxxxxx" type="password">
                        <button class="ai-assistant-button" id="ai-assistant-setAPIKeyButton">Set Key</button>
                    </div>
                    <div id="ai-assistant-hotkey-input-container">
                         <label for="ai-assistant-hotkey-input" class="ai-assistant-label">Toggle Hotkey (e.g. Control+Shift+X):</label>
                         <div>
                             <input id="ai-assistant-hotkey-input" type="text" placeholder="Example: Control+Shift+M">
                             <button class="ai-assistant-button" id="ai-assistant-setHotkeyButton">Set Hotkey</button>
                         </div>
                    </div>
                </div>
            </div>
        `;

        document.body.appendChild(elem);
        scriptLog('UI elements injected.');

        const settingsCheckbox = document.getElementById('ai-assistant-settings-checkbox');
        const settingsArrow = elem.querySelector('.settings-arrow');
        settingsCheckbox.addEventListener('change', () => {
            if (settingsArrow) settingsArrow.innerHTML = settingsCheckbox.checked ? '&#9650;' : '&#9660;';
        });

        function toggleChatVisibility(show) {
            const chatBox = document.getElementById('ai-assistant-box');
            const floatingToggleButton = document.getElementById('ai-assistant-toggle-button');
            // const headerToggleButton = chatBox.querySelector('.ai-assistant-toggle-visibility-header'); // Already part of chatBox

            if (typeof show === 'boolean') {
                chatVisible = show;
            } else {
                chatVisible = !chatVisible;
            }
            setStoredValue('chatVisible', chatVisible);

            if (chatVisible) {
                chatBox.classList.remove('hidden');
                if (floatingToggleButton) floatingToggleButton.classList.remove('hidden');
                if (floatingToggleButton) floatingToggleButton.innerHTML = '&#10005;';
                // if (headerToggleButton) headerToggleButton.innerHTML = '&#10005;'; // Not strictly needed as it's part of hidden box
                document.getElementById('ai-assistant-myTextArea')?.focus();
            } else {
                chatBox.classList.add('hidden');
                if (floatingToggleButton) floatingToggleButton.classList.remove('hidden'); // Keep toggle button visible even if chat is hidden by it
                if (floatingToggleButton) floatingToggleButton.innerHTML = '&#128172;';
                // if (headerToggleButton) headerToggleButton.innerHTML = '&#128172;';
            }
        }
        window.toggleChatVisibility = toggleChatVisibility;

        chatToggleBtn.addEventListener('click', () => toggleChatVisibility());
        elem.querySelector('.ai-assistant-toggle-visibility-header').addEventListener('click', () => toggleChatVisibility(false));


        (async () => {
            document.getElementById('ai-assistant-systemPrompt').value = await getStoredValue('AIprompt', '');
            document.getElementById('ai-assistant-memory').checked = await getStoredValue('AImemory', false);
            const currentHotkeyDisplay = await getStoredValue('chatToggleHotkey', 'Control+Shift+X');
            document.getElementById('ai-assistant-hotkey-input').value = currentHotkeyDisplay;

            const updateButtonTitles = (hotkey) => {
                const ftb = document.getElementById('ai-assistant-toggle-button');
                if (ftb) { ftb.title = `Toggle AI Assistant (Hotkey: ${hotkey})`;}
                const headerToggle = elem.querySelector('.ai-assistant-toggle-visibility-header');
                if (headerToggle) { headerToggle.title = `Hide AI Assistant (Hotkey: ${hotkey})`;}
            };
            updateButtonTitles(currentHotkeyDisplay);

            await window.populateModels();
            const storedModel = await getStoredValue('AImodel', 'openai/gpt-3.5-turbo');
            const modelSelect = document.getElementById('ai-assistant-models');
            if (modelSelect && modelSelect.options.length > 0) {
                if (Array.from(modelSelect.options).some(opt => opt.value === storedModel)) {
                    modelSelect.value = storedModel;
                } else if (modelSelect.options[0] && modelSelect.options[0].value) {
                    modelSelect.value = modelSelect.options[0].value;
                    await setStoredValue('AImodel', modelSelect.options[0].value);
                }
            }

            document.getElementById('ai-assistant-myTextArea').addEventListener("keydown", async function(event) {
                if (event.key === "Enter" && !event.shiftKey) {
                    event.preventDefault();
                    await window.sendMessage();
                }
            });
            document.getElementById('ai-assistant-sendButton').addEventListener('click', window.sendMessage);

            document.getElementById('ai-assistant-clearHistoryButton').onclick = async () => {
                scriptLog('Clear history clicked.');
                const newSystemPromptText = document.getElementById('ai-assistant-systemPrompt').value;
                window.defaultMessage.content = newSystemPromptText;
                await setStoredValue('AIprompt', newSystemPromptText);
                if (newSystemPromptText && newSystemPromptText.trim() !== "") {
                    window.AImessages = [JSON.parse(JSON.stringify(window.defaultMessage))];
                } else {
                    window.AImessages = [];
                }
                window.renderMessages();
                scriptLog('History cleared.');
            };

            document.getElementById('ai-assistant-setAPIKeyButton').onclick = async () => {
                const apiKeyInput = document.getElementById('ai-assistant-apiKey');
                if (apiKeyInput && apiKeyInput.value.trim()) {
                    await setStoredValue('openaiAPIKey', apiKeyInput.value.trim());
                    apiKeyInput.value = '';
                    scriptLog('API Key set (for OpenRouter).');
                    alert('API Key saved! Model list will refresh.');
                    await window.populateModels();
                } else {
                    alert('Please enter a valid API key.');
                }
            };

            document.getElementById('ai-assistant-setHotkeyButton').onclick = async () => {
                const hotkeyInput = document.getElementById('ai-assistant-hotkey-input');
                const newHotkey = hotkeyInput.value.trim();
                if (newHotkey) {
                    if (newHotkey.split('+').length > 0) {
                        await setStoredValue('chatToggleHotkey', newHotkey);
                        window.hotkeySetting = newHotkey;
                        updateButtonTitles(newHotkey);
                        alert(`Hotkey set to: ${newHotkey}.`);
                        scriptLog(`Hotkey updated: ${newHotkey}`);
                    } else {
                        alert('Invalid hotkey format.');
                    }
                } else {
                    alert('Please enter a hotkey.');
                }
            };

            let settingsSaveTimeout;
            const scheduleSaveSettings = async (eventSourceId = null) => {
                clearTimeout(settingsSaveTimeout);
                settingsSaveTimeout = setTimeout(async () => {
                    await setStoredValue('AImemory', document.getElementById('ai-assistant-memory').checked);
                    const modelSel = document.getElementById('ai-assistant-models');
                    if (modelSel && modelSel.value) await setStoredValue('AImodel', modelSel.value);

                    const systemPromptText = document.getElementById('ai-assistant-systemPrompt').value;
                    if (eventSourceId === 'ai-assistant-systemPrompt' || eventSourceId === null) {
                        await setStoredValue('AIprompt', systemPromptText);
                        window.defaultMessage.content = systemPromptText;
                        const systemMsgIndex = window.AImessages.findIndex(m => m.role === 'system');
                        if (systemPromptText && systemPromptText.trim() !== "") {
                            if (systemMsgIndex > -1) window.AImessages[systemMsgIndex].content = systemPromptText;
                            else window.AImessages.unshift(JSON.parse(JSON.stringify(window.defaultMessage)));
                        } else {
                            if (systemMsgIndex > -1) window.AImessages.splice(systemMsgIndex, 1);
                        }
                        if (eventSourceId === 'ai-assistant-systemPrompt') window.renderMessages();
                    }
                    scriptLog('Settings auto-saved.');
                }, 1000);
            };

            document.getElementById('ai-assistant-memory').addEventListener('change', () => scheduleSaveSettings('ai-assistant-memory'));
            document.getElementById('ai-assistant-models').addEventListener('change', () => scheduleSaveSettings('ai-assistant-models'));
            document.getElementById('ai-assistant-systemPrompt').addEventListener('input', () => scheduleSaveSettings('ai-assistant-systemPrompt'));

            document.addEventListener('keydown', (e) => {
                if (!window.hotkeySetting || typeof window.hotkeySetting !== 'string') return;
                const keys = window.hotkeySetting.toUpperCase().split('+');
                const mainKey = keys.pop();
                let ctrl = keys.includes('CONTROL') || keys.includes('CTRL');
                let shift = keys.includes('SHIFT');
                let alt = keys.includes('ALT');
                let meta = keys.includes('META') || keys.includes('COMMAND');

                if ((ctrl === e.ctrlKey) && (shift === e.shiftKey) && (alt === e.altKey) && (meta === e.metaKey) && (e.key.toUpperCase() === mainKey)) {
                    const activeEl = document.activeElement;
                    if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable) && activeEl.id !== 'ai-assistant-myTextArea') {
                        if (['ai-assistant-systemPrompt', 'ai-assistant-apiKey', 'ai-assistant-hotkey-input'].includes(activeEl.id)) return;
                    }
                    e.preventDefault();
                    e.stopPropagation();
                    toggleChatVisibility(); // This will now also handle showing the floating button if it was hidden
                    scriptLog(`Hotkey "${window.hotkeySetting}" pressed.`);
                }
            });

            // MODIFIED: Initial render logic based on chatVisible
            if (chatVisible) { // If stored state is visible (e.g. after first hotkey use and subsequent loads)
                 window.renderMessages();
            } // Otherwise, it remains hidden, and renderMessages will be called when toggleChatVisibility makes it visible.

            scriptLog('Initial render logic applied. Event listeners attached.');
        })();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', setupUI);
    } else {
        setupUI();
    }

    scriptLog('Script initialization phase complete. (OpenRouter Edition, ModUI)');

})();