Canvas ChatGPT BETTER (OpenRouter)

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

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.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

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 ChatGPT BETTER (OpenRouter)
// @namespace    http://tampermonkey.net/
// @version      1.8.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';

    function scriptLog(message) {
        console.log(`[Canvas AI Sidepanel] ${message}`);
    }

    scriptLog('Script starting... (OCR.space Edition, ModUI v1.9.1)');

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

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

    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);
    if (typeof chatVisible !== 'boolean') { chatVisible = (chatVisible === 'false'); }
    window.hotkeySetting = await getStoredValue('chatToggleHotkey', 'Control+Shift+X');

    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); // Still needed for data-message-id if any other feature uses it

            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;") : "";

            // MODIFIED: Removed Edit button generation
            let headerHTML = `<p class="ai-assistant-role">${roleText}: </p>`;

            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'));
            }
        }
        // MODIFIED: Removed event listener setup for edit buttons
        if (container) {
            container.scrollTop = container.scrollHeight;
        }
     };

    window.sendMessage = async function() { /* ... (unchanged: OpenRouter chat logic) ... */
        scriptLog('Attempting to send message to OpenRouter...');
        const chatContainer = document.getElementById('ai-assistant-container'); // Keep for error display
        const myTextArea = document.getElementById('ai-assistant-myTextArea');
        const apiKey = await getStoredValue('openaiAPIKey', '');
        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 might be null if UI not fully ready
            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; // Save for potential retry
        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';
                    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>`;
                    const currentChatContainer = document.getElementById('ai-assistant-container');
                    if(currentChatContainer) {
                        currentChatContainer.appendChild(errorP);
                        currentChatContainer.scrollTop = currentChatContainer.scrollHeight;
                    }
                    errorP.querySelector('.ai-assistant-retry-button')?.addEventListener('click', () => {
                        const currentMyTextArea = document.getElementById('ai-assistant-myTextArea');
                        currentMyTextArea.value = userMessageContentForRestore;
                        errorP.remove();
                        scriptLog("Retry button clicked.");
                        currentMyTextArea.focus();
                    });
                } finally {
                    if (! (document.querySelector('#ai-assistant-container p[style*="color: red;"]')) ) {
                        window.renderMessages();
                    }
                     const currentChatContainer = document.getElementById('ai-assistant-container');
                     if (currentChatContainer) currentChatContainer.scrollTop = currentChatContainer.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>`;
                const currentChatContainer = document.getElementById('ai-assistant-container');
                if(currentChatContainer) {
                     currentChatContainer.appendChild(errorP);
                     currentChatContainer.scrollTop = currentChatContainer.scrollHeight;
                }
            }
        });
    };
    window.populateModels = async function() { /* ... (unchanged: OpenRouter model population) ... */
        scriptLog('Populating models from OpenRouter...');
        const modelsSelect = document.getElementById('ai-assistant-models');
        const apiKey = await getStoredValue('openaiAPIKey', '');

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

    async function handleOCRSnip() { /* ... (unchanged: OCR.space logic) ... */
        scriptLog("OCR Snip initiated (OCR.space).");
        const myTextArea = document.getElementById('ai-assistant-myTextArea');
        if (!myTextArea) return;

        const ocrApiKey = await getStoredValue('ocrSpaceApiKey', '');
        if (!ocrApiKey) {
            showTemporaryNotification("OCR.space API Key not set in Settings.", "error", 5000);
            alert("Please set your OCR.space API Key in the AI Sidepanel settings to use the OCR feature. A free key 'helloworld' can be used for limited testing.");
            return;
        }

        let originalText = myTextArea.value;
        myTextArea.value = "Preparing OCR... Select screen area to capture...";
        myTextArea.disabled = true;
        showTemporaryNotification("Select screen area for OCR...", "info", 5000);

        try {
            const stream = await navigator.mediaDevices.getDisplayMedia({ video: { mediaSource: "screen", cursor: "always" }, audio: false });

            const videoEl = document.createElement('video');
            videoEl.style.display = 'none';
            document.body.appendChild(videoEl);

            await new Promise((resolvePlay, rejectPlay) => {
                videoEl.onloadedmetadata = () => {
                    videoEl.play().then(resolvePlay).catch(err => {
                        scriptLog("Video play error: " + err);
                        rejectPlay(new Error("Could not play screen capture stream."));
                    });
                };
                videoEl.srcObject = stream;
            });

            if (!videoEl.videoWidth || !videoEl.videoHeight) {
                await new Promise(r => setTimeout(r, 200));
                 if (!videoEl.videoWidth || !videoEl.videoHeight) {
                    stream.getTracks().forEach(track => track.stop());
                    videoEl.remove();
                    throw new Error("Could not get screen capture dimensions after delay.");
                }
            }

            const canvas = document.createElement('canvas');
            canvas.width = videoEl.videoWidth;
            canvas.height = videoEl.videoHeight;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height);

            stream.getTracks().forEach(track => track.stop());
            videoEl.remove();

            const base64Image = canvas.toDataURL('image/png');
            myTextArea.value = "Processing OCR with OCR.space...";
            showTemporaryNotification("Sending image to OCR.space...", "info", 10000);


            GM_xmlhttpRequest({
                method: "POST",
                url: "https://api.ocr.space/parse/image",
                headers: { "Content-Type": "application/x-www-form-urlencoded" },
                data: `apikey=${encodeURIComponent(ocrApiKey)}&base64Image=${encodeURIComponent(base64Image)}&language=eng&isOverlayRequired=false&detectOrientation=true`,
                onload: function(response) {
                    myTextArea.disabled = false;
                    myTextArea.focus();
                    try {
                        const result = JSON.parse(response.responseText);
                        if (result.IsErroredOnProcessing) {
                            scriptLog(`OCR.space Error: ${result.ErrorMessage.join(", ")}`);
                            showTemporaryNotification(`OCR.space Error: ${result.ErrorMessage[0]}`, "error", 7000);
                            myTextArea.value = originalText;
                        } else if (result.ParsedResults && result.ParsedResults.length > 0) {
                            const parsedText = result.ParsedResults[0].ParsedText.trim();
                            if (parsedText) {
                                myTextArea.value = originalText + (originalText && parsedText ? "\n" : "") + parsedText;
                                showTemporaryNotification("OCR complete! Text added.", "success");
                            } else {
                                showTemporaryNotification("OCR complete, but no text found.", "info");
                                myTextArea.value = originalText;
                            }
                        } else {
                            showTemporaryNotification("OCR completed, but no results returned.", "info");
                            myTextArea.value = originalText;
                        }
                    } catch (e) {
                        scriptLog("Error parsing OCR.space response: " + e);
                        showTemporaryNotification("Failed to parse OCR response.", "error");
                        myTextArea.value = originalText;
                    }
                },
                onerror: function(responseDetails) {
                    scriptLog("OCR.space request failed: " + responseDetails.statusText);
                    myTextArea.disabled = false;
                    myTextArea.focus();
                    myTextArea.value = originalText;
                    showTemporaryNotification("OCR request failed. Check network/console.", "error");
                }
            });

        } catch (err) {
            scriptLog(`OCR Snip Error: ${err.message || err}`);
            myTextArea.disabled = false;
            myTextArea.focus();
            myTextArea.value = originalText;
            if (err.name === "NotAllowedError" || (err.message && err.message.toLowerCase().includes("permission denied"))) {
                showTemporaryNotification("Screen capture permission denied.", "error");
            } else {
                showTemporaryNotification(`OCR setup failed: ${err.message.substring(0, 50)}...`, "error", 5000);
            }
        }
    }
    function showTemporaryNotification(message, type = "info", duration = 3000) { /* ... (unchanged) ... */
        const notificationArea = document.getElementById('ai-assistant-notification-area');
        if (!notificationArea) {
            scriptLog("Notification area not found, logging: " + message);
            return;
        }
        const existingSameType = notificationArea.querySelector(`.ai-assistant-notification.${type}`);
        if(existingSameType && existingSameType.textContent.startsWith(message.substring(0,10))) {
            existingSameType.remove();
        }

        const notification = document.createElement('div');
        notification.className = `ai-assistant-notification ${type}`;
        notification.textContent = message;
        notificationArea.appendChild(notification);

        setTimeout(() => {
            notification.remove();
        }, duration);
    }

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

        const accentGrey = '#6A737C';
        const accentGreyHover = '#525960';
        const lightGreyBg = '#f0f0f0';
        const lighterGreyBg = '#f8f9fa';
        const focusRingColor = 'rgba(106, 115, 124, .5)';

        const css = `
            #ai-assistant-box { position: fixed; bottom: 15px; right: 15px; width: 450px; height: 30vh; background-color: #fff; box-shadow: 0 5px 20px rgba(0,0,0,0.25); border-radius: 16px; 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; overflow: hidden; }
            #ai-assistant-box.hidden { transform: translateY(calc(100% + 30px)); opacity: 0; pointer-events: none; }
            #ai-assistant-notification-area { position: absolute; top: 0; left: 0; right: 0; z-index: 10002; padding-top: 10px; pointer-events: none; }
            .ai-assistant-notification { padding: 8px 15px; margin: 0 15px 5px 15px; border-radius: 4px; color: white; text-align: center; font-size: 0.9em; box-shadow: 0 2px 4px rgba(0,0,0,0.1); opacity: 0.95; pointer-events: auto; }
            .ai-assistant-notification.success { background-color: #28a745; }
            .ai-assistant-notification.error { background-color: #dc3545; }
            .ai-assistant-notification.info { background-color: ${accentGrey}; }
            #ai-assistant-box .ai-assistant-main-content { padding: 10px 15px 15px 15px; display: flex; flex-direction: column; flex-grow:1; overflow:hidden; background: #fdfdfd; position:relative; }
            #ai-assistant-container { flex-grow: 1; overflow-y: auto; padding: 10px; border-bottom: 1px solid #eee; margin-bottom: 10px; min-height: 50px; 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:40px; max-height: 100px; 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}; }
            #ai-assistant-box .ai-assistant-button { background-color: ${accentGrey}; color: white; border: none; padding: 8px 10px; margin-bottom: 8px; /* Added margin for buttons in settings */ border-radius: 5px; cursor: pointer; text-align: center; font-size:0.9em; /* Restored font size */ transition: background-color 0.2s ease; display: block; width: 100%; box-sizing: border-box; }
            #ai-assistant-box .ai-assistant-button:hover { background-color: ${accentGreyHover}; }
            /* #ai-assistant-button-bar REMOVED */
            .ai-assistant-settings-toggle { display: block; font-weight: bold; font-size: 1em; text-align: center; padding: 10px; color: ${accentGreyHover}; background: ${lightGreyBg}; cursor: pointer; border-top: 1px solid #ddd; transition: background-color 0.2s ease-out; margin:0; border-radius: 0 0 15px 15px; }
            .ai-assistant-settings-toggle:hover { background-color: #e2e6ea; }
            .ai-assistant-collapsible-content { max-height: 0px; overflow-y: auto; transition: max-height .35s ease-in-out; background: ${lighterGreyBg}; border-top:1px solid #ddd;}
            .ai-assistant-collapsible-content .content-inner { padding: 15px; border-bottom-left-radius: 15px; border-bottom-right-radius: 15px; }
            #ai-assistant-settings-checkbox:checked ~ .ai-assistant-collapsible-content { max-height: 500px; /* Increased max-height for settings */ }
            #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}; }
            input:checked + .ai-assistant-slider:before { transform: translateX(20px); }
            #ai-assistant-systemPrompt { display: block; width: calc(100% - 22px); min-height: 50px; max-height: 100px; resize: vertical; margin: 5px auto 10px auto; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size:0.9em; }
            #ai-assistant-apiKey, #ai-assistant-ocrApiKey { 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, #ai-assistant-setOcrApiKeyButton { display: inline-block; width: calc(40% - 10px); padding: 8px 0px; box-sizing: border-box;}
            /* Removed Edit Button CSS */
            .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}; 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); }
            #ai-assistant-toggle-button.hidden { display: none !important; }
            .api-key-group { margin-bottom: 10px; }
            .settings-action-buttons-group { margin-top: 15px; border-top: 1px solid #ddd; padding-top: 15px; } /* Group for action buttons in settings */
        `;
        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) {
            chatToggleBtn.classList.add('hidden');
        }
        document.body.appendChild(chatToggleBtn);

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

        // MODIFIED: HTML structure for main content and settings
        elem.innerHTML = `
            <div class="ai-assistant-main-content">
                <div id="ai-assistant-notification-area"></div>
                <div id="ai-assistant-container"></div>
                <textarea id="ai-assistant-myTextArea" placeholder="Ask AI Assistant..."></textarea>
                </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">Chat Model (OpenRouter):</label>
                    <select name="models" id="ai-assistant-models"></select>
                    <div class="api-key-group">
                        <label for="ai-assistant-apiKey" class="ai-assistant-label">OpenRouter API Key:</label>
                        <div>
                            <input id="ai-assistant-apiKey" placeholder="OpenRouter Key (sk-or-...)" type="password">
                            <button class="ai-assistant-button" id="ai-assistant-setAPIKeyButton">Set Chat Key</button>
                        </div>
                    </div>
                    <div class="api-key-group">
                        <label for="ai-assistant-ocrApiKey" class="ai-assistant-label">OCR.space API Key:</label>
                        <div>
                            <input id="ai-assistant-ocrApiKey" placeholder="OCR.space Key" type="password">
                            <button class="ai-assistant-button" id="ai-assistant-setOcrApiKeyButton">Set OCR Key</button>
                        </div>
                    </div>
                    <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>
                    <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 class="settings-action-buttons-group"> <button class="ai-assistant-button" id="ai-assistant-ocrButton" title="Capture screen area for OCR">OCR Screen</button>
                        <button class="ai-assistant-button" id="ai-assistant-clearHistoryButton">Clear Chat & Apply Prompt</button>
                    </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) { /* ... (unchanged) ... */
            const chatBox = document.getElementById('ai-assistant-box');
            const floatingToggleButton = document.getElementById('ai-assistant-toggle-button');

            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;';
                document.getElementById('ai-assistant-myTextArea')?.focus();
            } else {
                chatBox.classList.add('hidden');
                if (floatingToggleButton ) {
                     floatingToggleButton.innerHTML = '&#128172;';
                }
            }
        }
        window.toggleChatVisibility = toggleChatVisibility;

        chatToggleBtn.addEventListener('click', () => toggleChatVisibility());
        // Removed listener for header 'X' button as header is gone

        (async () => { /* ... (Event listeners setup, send button listener removed) ... */
            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})`;}
            };
            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();
                }
            });
            // MODIFIED: Send button listener removed
            document.getElementById('ai-assistant-ocrButton').addEventListener('click', handleOCRSnip);
            document.getElementById('ai-assistant-clearHistoryButton').addEventListener('click', 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('OpenRouter API Key set.');
                    alert('OpenRouter API Key saved! Model list will refresh.');
                    await window.populateModels();
                } else {
                    alert('Please enter a valid OpenRouter API key.');
                }
            };
            document.getElementById('ai-assistant-setOcrApiKeyButton').onclick = async () => {
                const ocrApiKeyInput = document.getElementById('ai-assistant-ocrApiKey');
                if (ocrApiKeyInput && ocrApiKeyInput.value.trim()) {
                    await setStoredValue('ocrSpaceApiKey', ocrApiKeyInput.value.trim());
                    ocrApiKeyInput.value = '';
                    scriptLog('OCR.space API Key set.');
                    alert('OCR.space API Key saved!');
                } else {
                    alert('Please enter a valid OCR.space 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 (excluding API keys set by button).');
                }, 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) => { /* ... (unchanged) ... */
                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-ocrApiKey', 'ai-assistant-hotkey-input'].includes(activeEl.id)) return;
                    }
                    e.preventDefault();
                    e.stopPropagation();
                    toggleChatVisibility();
                    scriptLog(`Hotkey "${window.hotkeySetting}" pressed.`);
                }
            });

            if (chatVisible) {
                 window.renderMessages();
            }
            scriptLog('Initial render logic applied. Event listeners attached.');
        })();
    }

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

    scriptLog('Script initialization phase complete.');

})();