AtutorSolver

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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