AtutorSolver

Helps solve tests using AI. Supports Google Gemini and local Ollama, bypasses copy-paste restrictions, batch solving with strict JSON key enforcement.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Advertisement:

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

Advertisement:

// ==UserScript==
// @name         AtutorSolver
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  Helps solve tests using AI. Supports Google Gemini and local Ollama, bypasses copy-paste restrictions, batch solving with strict JSON key enforcement.
// @author       Propsi4
// @match        https://dl.tntu.edu.ua/mods/_standard/tests/take_test*.php?*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=dl.tntu.edu.ua
// @license      MIT
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @require      https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @connect      generativelanguage.googleapis.com
// @connect      localhost
// @connect      127.0.0.1
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // 1. CONFIGURATION & STATE
    // ==========================================
    let PROVIDER = GM_getValue('AI_PROVIDER', 'gemini'); 
    let API_KEY = GM_getValue('GEMINI_API_KEY', '');
    let OLLAMA_URL = GM_getValue('OLLAMA_URL', 'http://localhost:11434');
    
    let SELECTED_GEMINI_MODEL = GM_getValue('GEMINI_MODEL', '');
    let SELECTED_OLLAMA_MODEL = GM_getValue('OLLAMA_MODEL', '');
    let isHidden = false;

    GM_registerMenuCommand('Set Gemini API Key', () => {
        const key = prompt('Enter your Gemini API Key:', API_KEY);
        if (key !== null) {
            GM_setValue('GEMINI_API_KEY', key.trim());
            API_KEY = key.trim();
            alert('API Key saved!');
            if (PROVIDER === 'gemini') loadUIModels();
        }
    });

    GM_registerMenuCommand('Set Ollama URL', () => {
        const url = prompt('Enter Ollama Base URL:', OLLAMA_URL);
        if (url !== null) {
            GM_setValue('OLLAMA_URL', url.trim());
            OLLAMA_URL = url.trim();
            alert('Ollama URL saved!');
            if (PROVIDER === 'ollama') loadUIModels();
        }
    });

    // ==========================================
    // 2. UNBLOCK COPY & PASTE
    // ==========================================
    function unblockCopyPaste() {
        const style = document.createElement('style');
        style.innerHTML = `
            * {
                -webkit-user-select: text !important;
                -moz-user-select: text !important;
                -ms-user-select: text !important;
                user-select: text !important;
            }
        `;
        document.head.appendChild(style);

        const removeInlineHandlers = () => {
            if (!document.body) return;
            const attributes = ['oncopy', 'onpaste', 'oncut', 'onselectstart', 'oncontextmenu', 'ondragstart'];
            attributes.forEach(attr => document.body.removeAttribute(attr));
        };
        
        const eventsToUnblock = ['copy', 'cut', 'paste', 'contextmenu', 'selectstart', 'dragstart'];
        eventsToUnblock.forEach(eventType => {
            document.addEventListener(eventType, function(e) { e.stopPropagation(); }, true); 
        });

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

    // ==========================================
    // 3. DOM PARSING
    // ==========================================
    function getQuestions() {
        const rows = Array.from(document.querySelectorAll('.row'));
        const questions = [];

        rows.forEach((row, index) => {
            const p = row.querySelector('p');
            const ul = row.querySelector('ul.multichoice-question, ul.multianswer-question');
            
            if (!p || !ul) return;

            const isMultiChoice = ul.classList.contains('multichoice-question');
            const type = isMultiChoice ? 'single' : 'multiple';
            
            const listItems = Array.from(ul.children);
            if (isMultiChoice) listItems.pop(); 

            const answers = listItems.map(li => {
                const input = li.querySelector('input');
                const text = li.innerText.replace(input?.value || '', '').trim(); 
                return { input, text };
            }).filter(a => a.input);

            questions.push({
                id: index,
                container: row,
                ulElement: ul, 
                questionText: p.innerText.trim(),
                hasImages: row.querySelector('img') !== null,
                type,
                answers
            });
        });

        return questions;
    }

    // ==========================================
    // 4. API ABSTRACTION LAYER
    // ==========================================
    async function fetchModels() {
        if (PROVIDER === 'gemini') {
            if (!API_KEY) return [];
            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: `https://generativelanguage.googleapis.com/v1beta/models?key=${API_KEY}`,
                    onload: (res) => {
                        if (res.status === 200) {
                            try {
                                const data = JSON.parse(res.responseText);
                                const valid = data.models.filter(m => m.supportedGenerationMethods?.includes("generateContent"));
                                resolve(valid.map(m => ({ id: m.name, name: m.displayName || m.name.replace('models/', '') })));
                            } catch (e) { resolve([]); }
                        } else { resolve([]); }
                    },
                    onerror: () => resolve([])
                });
            });
        } else {
            if (!OLLAMA_URL) return [];
            const endpoint = `${OLLAMA_URL.replace(/\/$/, '')}/api/tags`;
            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: endpoint,
                    onload: (res) => {
                        if (res.status === 200) {
                            try {
                                const data = JSON.parse(res.responseText);
                                resolve((data.models || []).map(m => ({ id: m.name, name: m.name })));
                            } catch (e) { resolve([]); }
                        } else { resolve([]); }
                    },
                    onerror: () => resolve([])
                });
            });
        }
    }

    function extractJSON(text, isBatch) {
        if (isBatch) {
            const match = text.match(/\{[\s\S]*\}/);
            if (match) return JSON.parse(match[0]);
            throw new Error("Failed to parse batch JSON");
        } else {
            const match = text.match(/\[(.*?)\]/); 
            if (match) return JSON.parse(`[${match[1]}]`);
            throw new Error("Failed to parse array");
        }
    }

    async function executeRequest(url, payload, onWaitUpdate, isBatch, parserCallback) {
        const makeReq = () => new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: url,
                headers: { "Content-Type": "application/json" },
                data: JSON.stringify(payload),
                onload: (res) => {
                    if (res.status === 429) return reject({ status: 429, message: "Rate limit" });
                    if (res.status !== 200) return reject({ status: res.status, message: res.responseText });
                    
                    try {
                        const rawText = parserCallback(JSON.parse(res.responseText));
                        resolve(extractJSON(rawText, isBatch));
                    } catch (e) {
                        reject({ status: 500, message: "Parse error" });
                    }
                },
                onerror: () => reject({ status: 500, message: "Network Error" })
            });
        });

        let retries = 4;
        let waitSeconds = 15;
        while (retries > 0) {
            try { return await makeReq(); } 
            catch (err) {
                if (err.status === 429 && retries > 1) {
                    retries--;
                    for (let i = waitSeconds; i > 0; i--) {
                        if (onWaitUpdate) onWaitUpdate(i);
                        await new Promise(r => setTimeout(r, 1000));
                    }
                    waitSeconds = Math.min(waitSeconds * 2, 60);
                    if (onWaitUpdate) onWaitUpdate(0); 
                } else {
                    throw new Error(err.message || "Unknown API Error");
                }
            }
        }
    }

    // ==========================================
    // 5. SOLVER ROUTINES
    // ==========================================
    async function askAI(questionData, captureBase64 = null, onWaitUpdate = null) {
        const promptText = `You are a test-solving assistant.\nType: ${questionData.type === 'single' ? 'Select EXACTLY ONE' : 'Select ALL correct options'}\nQuestion: ${questionData.questionText || "[See visual content]"}\nOptions:\n${questionData.answers.map((a, i) => `[${i}] ${a.text || '[Visual Option]'}`).join('\n')}\nCRITICAL INSTRUCTION: Return ONLY a raw JSON array containing the integer indices of the correct options. No markdown formatting. Example: [0] or [1, 3]`;

        if (PROVIDER === 'gemini') {
            if (!API_KEY || !SELECTED_GEMINI_MODEL) throw new Error('API Key or Model not set.');
            const parts = [{ text: promptText }];
            if (captureBase64) parts.push({ inlineData: { mimeType: "image/png", data: captureBase64.split(',')[1] } });
            
            const url = `https://generativelanguage.googleapis.com/v1beta/${SELECTED_GEMINI_MODEL}:generateContent?key=${API_KEY}`;
            const payload = { contents: [{ parts }], generationConfig: { temperature: 0.1 } };
            return executeRequest(url, payload, onWaitUpdate, false, (data) => data.candidates[0].content.parts[0].text);
            
        } else {
            if (!OLLAMA_URL || !SELECTED_OLLAMA_MODEL) throw new Error('Ollama URL or Model not set.');
            const url = `${OLLAMA_URL.replace(/\/$/, '')}/api/generate`;
            const payload = { model: SELECTED_OLLAMA_MODEL, prompt: promptText, stream: false, options: { temperature: 0.1 } };
            if (captureBase64) payload.images = [captureBase64.split(',')[1]];
            
            return executeRequest(url, payload, onWaitUpdate, false, (data) => data.response);
        }
    }

    async function askAIBatch(questions, onWaitUpdate = null) {
        // Build a strict expected keys list so the AI doesn't skip the last one
        const expectedKeys = questions.map(q => `"${q.id}"`).join(', ');
        
        let promptText = `You are a test-solving expert. I will provide a batch of ${questions.length} questions.\nCRITICAL INSTRUCTION: Return ONLY a raw JSON object where keys are the specific Question IDs and values are arrays containing the integer indices of the correct options. Do not wrap in markdown.\nFormat: {"0": [1], "1": [0, 2]}\nYou MUST include exactly these keys in your JSON response: ${expectedKeys}\n\nQUESTIONS:\n`;
        
        const imagesBase64 = [];
        const parts = [];

        for (const q of questions) {
            promptText += `\n---\nID: ${q.id}\nType: ${q.type === 'single' ? 'Select EXACTLY ONE' : 'Select ALL correct'}\nQuestion: ${q.questionText || "[See visual content]"}\nOptions:\n${q.answers.map((a, i) => `[${i}] ${a.text || '[Visual Option]'}`).join('\n')}\n`;
            
            if (q.questionText.length < 10 || q.hasImages) {
                const btn = q.container.querySelector('.ai-solve-btn');
                if (btn) btn.style.display = 'none';
                const canvas = await html2canvas(q.container);
                if (btn) btn.style.display = 'block';
                
                const b64 = canvas.toDataURL('image/png').split(',')[1];
                imagesBase64.push(b64);
                
                parts.push({ text: `Image for Question ID ${q.id}:` });
                parts.push({ inlineData: { mimeType: "image/png", data: b64 } });
                
                if (PROVIDER === 'ollama') promptText += `[Image Attached for ID ${q.id}]\n`;
            }
        }

        // Cap the prompt with a firm ending to force generation
        promptText += `\n--- END OF QUESTIONS ---\nRemember, output ONLY the JSON object containing ALL ${questions.length} keys (${expectedKeys}).`;

        parts.unshift({ text: promptText });

        if (PROVIDER === 'gemini') {
            if (!API_KEY || !SELECTED_GEMINI_MODEL) throw new Error('API Key or Model not set.');
            const url = `https://generativelanguage.googleapis.com/v1beta/${SELECTED_GEMINI_MODEL}:generateContent?key=${API_KEY}`;
            const payload = { contents: [{ parts }], generationConfig: { temperature: 0.1 } };
            return executeRequest(url, payload, onWaitUpdate, true, (data) => data.candidates[0].content.parts[0].text);
            
        } else {
            if (!OLLAMA_URL || !SELECTED_OLLAMA_MODEL) throw new Error('Ollama URL or Model not set.');
            const url = `${OLLAMA_URL.replace(/\/$/, '')}/api/generate`;
            const payload = { model: SELECTED_OLLAMA_MODEL, prompt: promptText, stream: false, options: { temperature: 0.1 } };
            if (imagesBase64.length > 0) payload.images = imagesBase64; 
            
            return executeRequest(url, payload, onWaitUpdate, true, (data) => data.response);
        }
    }

    // ==========================================
    // 6. UI ACTIONS
    // ==========================================
    function applyAnswersToQuestion(q, indices, btn) {
        q.answers.forEach(a => {
            a.input.checked = false;
            a.input.parentElement.style.backgroundColor = '';
        });
        indices.forEach(idx => {
            if (q.answers[idx]) {
                q.answers[idx].input.click(); 
                q.answers[idx].input.parentElement.style.backgroundColor = 'rgba(76, 175, 80, 0.2)';
            }
        });
        if (btn) {
            btn.innerText = "Solved ✓";
            btn.style.backgroundColor = "#4caf50";
            btn.dataset.running = "false";
        }
    }

    async function solveQuestion(q) {
        const btn = q.container.querySelector('.ai-solve-btn');
        if (btn.dataset.running === "true") return;
        
        btn.dataset.running = "true";
        btn.innerText = "Thinking...";
        btn.style.backgroundColor = "#ffc107";

        try {
            let screenshot = null;
            if (q.questionText.length < 10 || q.hasImages) {
                btn.style.display = 'none';
                const canvas = await html2canvas(q.container);
                btn.style.display = 'block';
                screenshot = canvas.toDataURL('image/png');
            }

            const correctIndices = await askAI(q, screenshot, (timeLeft) => {
                if (timeLeft > 0) {
                    btn.innerText = `Rate Limit! ${timeLeft}s...`;
                    btn.style.backgroundColor = "#ff9800";
                } else {
                    btn.innerText = "Thinking (Retry)...";
                    btn.style.backgroundColor = "#ffc107";
                }
            });
            
            applyAnswersToQuestion(q, correctIndices, btn);
        } catch (error) {
            btn.innerText = "Error (See Console)";
            btn.style.backgroundColor = "#f44336";
            btn.dataset.running = "false";
            console.error(error);
        }
    }

    async function batchSolveAll() {
        const questions = getQuestions();
        if (questions.length === 0) return;

        const btns = [document.getElementById('ai-solve-all-btn'), document.getElementById('ai-batch-solve-btn')];
        const progressContainer = document.getElementById('ai-progress-container');
        const progressFill = document.getElementById('ai-progress-fill');
        const progressText = document.getElementById('ai-progress-text');

        btns.forEach(b => b && (b.disabled = true));
        if (progressContainer) progressContainer.style.display = 'block';
        if (progressText) {
            progressText.style.display = 'block';
            progressText.innerText = `Batch Sending ${questions.length} questions...`;
        }
        if (progressFill) progressFill.style.width = '50%';

        questions.forEach(q => {
            const btn = q.container.querySelector('.ai-solve-btn');
            btn.dataset.running = "true";
            btn.innerText = "Batch Processing...";
            btn.style.backgroundColor = "#2196f3";
        });

        try {
            const batchResults = await askAIBatch(questions, (timeLeft) => {
                if (progressText) {
                    progressText.innerText = timeLeft > 0 ? `Rate Limit! Retrying in ${timeLeft}s...` : "Retrying Batch...";
                    progressFill.style.backgroundColor = timeLeft > 0 ? "#ff9800" : "#2196f3";
                }
            });

            let solvedCount = 0;
            questions.forEach(q => {
                const btn = q.container.querySelector('.ai-solve-btn');
                const indices = batchResults[q.id.toString()];
                if (indices && Array.isArray(indices)) {
                    applyAnswersToQuestion(q, indices, btn);
                    solvedCount++;
                } else {
                    btn.innerText = "Missed in Batch";
                    btn.style.backgroundColor = "#f44336";
                    btn.dataset.running = "false";
                }
            });

            if (progressFill) {
                progressFill.style.width = '100%';
                progressFill.style.backgroundColor = "#4caf50";
            }
            if (progressText) progressText.innerText = `Batch Complete! (${solvedCount}/${questions.length})`;

        } catch (error) {
            console.error(error);
            if (progressText) progressText.innerText = "Batch Error. Try sequential mode.";
            if (progressFill) progressFill.style.backgroundColor = "#f44336";
            questions.forEach(q => {
                const btn = q.container.querySelector('.ai-solve-btn');
                btn.innerText = "Batch Failed";
                btn.style.backgroundColor = "#f44336";
                btn.dataset.running = "false";
            });
        } finally {
            btns.forEach(b => b && (b.disabled = false));
        }
    }

    async function solveAll() {
        const questions = getQuestions();
        const total = questions.length;
        if (total === 0) return;

        const btns = [document.getElementById('ai-solve-all-btn'), document.getElementById('ai-batch-solve-btn')];
        const progressContainer = document.getElementById('ai-progress-container');
        const progressFill = document.getElementById('ai-progress-fill');
        const progressText = document.getElementById('ai-progress-text');

        btns.forEach(b => b && (b.disabled = true));
        if (progressContainer) progressContainer.style.display = 'block';
        if (progressText) {
            progressText.style.display = 'block';
            progressText.innerText = `0 / ${total} Solved`;
        }
        if (progressFill) {
            progressFill.style.width = '0%';
            progressFill.style.backgroundColor = "#4caf50";
        }

        for (let i = 0; i < total; i++) {
            await solveQuestion(questions[i]);
            const current = i + 1;
            if (progressFill) progressFill.style.width = `${(current / total) * 100}%`;
            if (progressText) progressText.innerText = `${current} / ${total} Solved`;
            if (current < total) await new Promise(r => setTimeout(r, 100));
        }

        if (progressText) progressText.innerText = "Sequential Solving Done! 🎉";
        btns.forEach(b => b && (b.disabled = false));
    }

    // ==========================================
    // 7. UI INJECTION & STATE MANGEMENT
    // ==========================================
    async function loadUIModels() {
        const select = document.getElementById('ai-model-select');
        if (!select) return;

        select.innerHTML = '<option value="">Loading models...</option>';
        const models = await fetchModels();

        if (models.length === 0) {
            select.innerHTML = PROVIDER === 'gemini' ? '<option value="">Invalid API Key</option>' : '<option value="">Ollama Offline or No Models</option>';
            return;
        }

        select.innerHTML = '';
        models.forEach(m => {
            const opt = document.createElement('option');
            opt.value = m.id;
            opt.innerText = m.name;
            const saved = PROVIDER === 'gemini' ? SELECTED_GEMINI_MODEL : SELECTED_OLLAMA_MODEL;
            if (m.id === saved) opt.selected = true;
            select.appendChild(opt);
        });

        const currentSaved = PROVIDER === 'gemini' ? SELECTED_GEMINI_MODEL : SELECTED_OLLAMA_MODEL;
        if (!currentSaved && models.length > 0) {
            if (PROVIDER === 'gemini') {
                SELECTED_GEMINI_MODEL = models[0].id;
                GM_setValue('GEMINI_MODEL', SELECTED_GEMINI_MODEL);
            } else {
                SELECTED_OLLAMA_MODEL = models[0].id;
                GM_setValue('OLLAMA_MODEL', SELECTED_OLLAMA_MODEL);
            }
        }
    }

    async function injectUI() {
        const questions = getQuestions();
        questions.forEach(q => {
            if (q.container.querySelector('.ai-solve-btn')) return;

            const btn = document.createElement('button');
            btn.className = 'ai-solve-btn atutor-ai-element';
            btn.innerText = '✨ Solve with AI';
            btn.dataset.running = "false";
            btn.style.cssText = 'margin: 10px 0; padding: 5px 10px; cursor: pointer; border: none; border-radius: 4px; background-color: #2196f3; color: white; font-weight: bold; display: block;';
            btn.onclick = (e) => { e.preventDefault(); solveQuestion(q); };
            
            if (q.ulElement && q.ulElement.parentNode) q.ulElement.parentNode.insertBefore(btn, q.ulElement);
            else q.container.appendChild(btn); 
        });

        if (!document.getElementById('ai-master-panel')) {
            const panel = document.createElement('div');
            panel.id = 'ai-master-panel';
            panel.className = 'atutor-ai-element';
            panel.style.cssText = 'position: fixed; bottom: 20px; right: 20px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); z-index: 9999; border: 1px solid #ddd; width: 220px;';
            
            const title = document.createElement('div');
            title.innerText = '🤖 AI Assistant';
            title.style.cssText = 'font-weight: bold; margin-bottom: 10px; font-family: sans-serif; text-align: center;';
            
            const providerSelect = document.createElement('select');
            providerSelect.style.cssText = 'width: 100%; margin-bottom: 5px; padding: 5px; border-radius: 4px; border: 1px solid #ccc; font-weight: bold;';
            providerSelect.innerHTML = `<option value="gemini" ${PROVIDER === 'gemini' ? 'selected' : ''}>☁️ Google Gemini</option>
                                        <option value="ollama" ${PROVIDER === 'ollama' ? 'selected' : ''}>🖥️ Local Ollama</option>`;
            
            providerSelect.addEventListener('change', (e) => {
                PROVIDER = e.target.value;
                GM_setValue('AI_PROVIDER', PROVIDER);
                loadUIModels();
            });

            const modelSelect = document.createElement('select');
            modelSelect.id = 'ai-model-select';
            modelSelect.style.cssText = 'width: 100%; margin-bottom: 10px; padding: 5px; border-radius: 4px; border: 1px solid #ccc;';
            
            modelSelect.addEventListener('change', (e) => {
                if (PROVIDER === 'gemini') {
                    SELECTED_GEMINI_MODEL = e.target.value;
                    GM_setValue('GEMINI_MODEL', SELECTED_GEMINI_MODEL);
                } else {
                    SELECTED_OLLAMA_MODEL = e.target.value;
                    GM_setValue('OLLAMA_MODEL', SELECTED_OLLAMA_MODEL);
                }
            });

            const batchSolveBtn = document.createElement('button');
            batchSolveBtn.id = 'ai-batch-solve-btn';
            batchSolveBtn.innerText = '⚡ Batch Solve (Fast)';
            batchSolveBtn.style.cssText = 'padding: 8px 15px; background: #009688; color: white; border: none; border-radius: 4px; cursor: pointer; width: 100%; margin-bottom: 5px; font-weight: bold;';
            batchSolveBtn.onclick = batchSolveAll;

            const solveAllBtn = document.createElement('button');
            solveAllBtn.id = 'ai-solve-all-btn';
            solveAllBtn.innerText = 'Solve Sequential (Safe)';
            solveAllBtn.style.cssText = 'padding: 8px 15px; background: #9c27b0; color: white; border: none; border-radius: 4px; cursor: pointer; width: 100%;';
            solveAllBtn.onclick = solveAll;

            const progressContainer = document.createElement('div');
            progressContainer.id = 'ai-progress-container';
            progressContainer.style.cssText = 'width: 100%; height: 8px; background-color: #eee; border-radius: 4px; margin-top: 10px; display: none; overflow: hidden;';
            const progressFill = document.createElement('div');
            progressFill.id = 'ai-progress-fill';
            progressFill.style.cssText = 'width: 0%; height: 100%; background-color: #4caf50; transition: width 0.3s ease, background-color 0.3s;';
            progressContainer.appendChild(progressFill);

            const progressText = document.createElement('div');
            progressText.id = 'ai-progress-text';
            progressText.style.cssText = 'font-size: 12px; font-weight: bold; text-align: center; margin-top: 5px; color: #333; display: none;';

            const helpText = document.createElement('div');
            helpText.innerText = 'Mid-Click or Shift+Z to hide';
            helpText.style.cssText = 'font-size: 10px; color: #888; margin-top: 10px; text-align: center;';

            panel.append(title, providerSelect, modelSelect, batchSolveBtn, solveAllBtn, progressContainer, progressText, helpText);
            document.body.appendChild(panel);

            loadUIModels();
        }
    }

    function toggleStealth() {
        isHidden = !isHidden;
        document.querySelectorAll('.atutor-ai-element').forEach(el => el.style.display = isHidden ? 'none' : 'block');
    }

    document.addEventListener('keydown', (e) => { if (e.shiftKey && e.key.toLowerCase() === 'z') toggleStealth(); });
    document.addEventListener('mousedown', (e) => { if (e.button === 1) { e.preventDefault(); toggleStealth(); } });

    unblockCopyPaste();
    setTimeout(injectUI, 1000);
})();