uCertify Helper

Enjoy your study

// ==UserScript==
// @name         uCertify Helper
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Enjoy your study
// @match        *.ucertify.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let enableAutoCard = false;
    let enableMarkAsRead = false;

    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    function createSmallSpinner() {
        const spinner = document.createElement('div');
        spinner.className = 'quiz-helper-small-spinner';
        spinner.style.border = '4px solid #f3f3f3';
        spinner.style.borderTop = '4px solid #3498db';
        spinner.style.borderRadius = '50%';
        spinner.style.width = '24px';
        spinner.style.height = '24px';
        spinner.style.animation = 'spin 1s linear infinite';

        const style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
        document.getElementsByTagName('head')[0].appendChild(style);

        return spinner;
    }

    function showSmallSpinner() {
        let answerElement = document.querySelector('.quiz-helper-answer');
        if (!answerElement) {
            answerElement = document.createElement('div');
            answerElement.className = 'quiz-helper-answer';
            const questionElement = document.querySelector('.test-question ._title') || document.querySelector('.test-question [data-itemtype="question"]');
            if (questionElement) {
                questionElement.appendChild(answerElement);
            }
        }
        const spinner = createSmallSpinner();
        answerElement.innerHTML = '';
        answerElement.appendChild(spinner);
    }

    function hideSmallSpinner() {
        const spinner = document.querySelector('.quiz-helper-small-spinner');
        if (spinner) {
            spinner.remove();
        }
    }

    function enableAndClickButton() {
        const correctButton = document.getElementById('correct');
        if (correctButton && correctButton.disabled) {
            correctButton.disabled = false;
            correctButton.click();
        }
    }

    function startAutoCard() {
        const interval = setInterval(() => {
            if (enableAutoCard) {
                enableAndClickButton();
            }
        }, 500);
    }

    function clickMarkAsRead() {
        if (!enableMarkAsRead) return;

        const readingIndicators = document.querySelectorAll('.dropdown.reset_reading_time.pointer > div[data-bs-toggle="dropdown"]');
        readingIndicators.forEach(indicator => {
            if (!indicator.classList.contains('show')) {
                indicator.click();
            }
            const markAsReadLink = indicator.nextElementSibling?.querySelector('.dropdown-item.mark_read');
            if (markAsReadLink) {
                markAsReadLink.click();
            }
        });
    }

    function startMarkAsRead() {
        window.addEventListener('load', clickMarkAsRead);

        (function() {
            let originalXHR = window.XMLHttpRequest;
            window.XMLHttpRequest = function() {
                let xhr = new originalXHR();
                let originalSend = xhr.send;
                xhr.send = function() {
                    this.addEventListener('load', function() {
                        clickMarkAsRead();
                    });
                    originalSend.apply(xhr, arguments);
                };
                return xhr;
            };
        })();

        setInterval(clickMarkAsRead, 500);
    }

    function getQuizTitle() {
        const titleElement = document.querySelector('a.nav-link.text-body.text-truncate');
        return titleElement ? titleElement.innerText.trim() : 'Quiz';
    }

    function getQuestionAndOptions() {
        const questionElement = document.querySelector('.test-question ._title') || document.querySelector('.test-question [data-itemtype="question"]');
        const question = questionElement ? questionElement.innerText.trim() : '';
        console.log('Question:', question); // Debug output

        let options = [];
        const optionElementsLeft = document.querySelectorAll('.shuffleList1 .matchlist_list');
        const optionElementsRight = document.querySelectorAll('.shuffleList2 .matchlist_list');

        if (optionElementsLeft.length > 0 && optionElementsRight.length > 0) {
            options = {
                left: Array.from(optionElementsLeft).map(option => option.innerText.trim()),
                right: Array.from(optionElementsRight).map(option => option.innerText.trim())
            };
            console.log('Matching options:', options); // Debug output
        } else {
            const optionsElements = document.querySelectorAll('#item_answer .radio_label, #item_answer .chekcbox_label');
            options = Array.from(optionsElements).map(option => option.innerText.trim().replace(/^\w\./, '').trim());
            console.log('Multiple choice options:', options); // Debug output
        }

        return { question, options };
    }

    // Function to perform DuckDuckGo search using GM_xmlhttpRequest
    async function duckDuckGoSearch(query) {
        const url = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: function(response) {
                    if (response.status === 200) {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, 'text/html');

                        const results = Array.from(doc.querySelectorAll('.result')).slice(0, 5).map(result => ({
                            title: result.querySelector('.result__a')?.innerText,
                            snippet: result.querySelector('.result__snippet')?.innerText,
                            link: result.querySelector('.result__a')?.href
                        }));

                        resolve(results);
                    } else {
                        console.error('Error fetching search results:', response);
                        reject('Error fetching search results');
                    }
                },
                onerror: function(error) {
                    console.error('Network error:', error);
                    reject('Network error');
                }
            });
        });
    }

    // Function to get search suggestions from GPT
    async function getSearchSuggestions(title, question, options) {
        const apiUrl = GM_getValue('openai_api_url');
        const apiKey = GM_getValue('openai_api_key');
        const apimodel = GM_getValue('openai_api_model');

        let prompt;
        if (options.left && options.right) {
            prompt = `Quiz Title: ${title}\nQuestion: ${question}\nMatch the following terms to their definitions:\nTerms:\n${options.left.join('\n')}\nDefinitions:\n${options.right.join('\n')}\nPlease provide only the search keywords.`;
        } else if (options.length > 0) {
            prompt = `Quiz Title: ${title}\nQuestion: ${question}\nOptions:\n${options.map((opt, index) => String.fromCharCode(65 + index) + '. ' + opt).join('\n')}\nPlease provide only the search keywords.`;
        }

        console.log('Prompt for search suggestions:', prompt);

        try {
            const response = await fetch(apiUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${apiKey}`
                },
                body: JSON.stringify({
                    model: apimodel,
                    messages: [
                        {"role": "system", "content": "You are a helpful assistant."},
                        {"role": "user", "content": prompt}
                    ]
                })
            });
            const data = await response.json();
            console.log('Search suggestions API Response:', data);

            if (data.choices && data.choices.length > 0) {
                return data.choices[0].message.content.trim();
            } else {
                console.error('No search suggestions found in API response');
                return 'No search suggestions found';
            }
        } catch (error) {
            console.error('Error fetching search suggestions:', error);
            return 'Error fetching search suggestions';
        }
    }

    // Function to get answer from GPT with search results
    async function getChatGPTAnswerWithSearchResults(title, question, options, searchResults) {
        const apiUrl = GM_getValue('openai_api_url');
        const apiKey = GM_getValue('openai_api_key');
        const apimodel = GM_getValue('openai_api_model');

        let prompt;
        if (options.left && options.right) {
            prompt = `Please provide only the correct matches in the format "1-A\\n2-B\\n3-C". Do not include any additional text.\n\nQuiz Title: ${title}\nQuestion: ${question}\nMatch the following terms to their definitions:\nTerms:\n${options.left.join('\n')}\nDefinitions:\n${options.right.join('\n')}\nSearch Results:\n${searchResults.map(result => `${result.title}\n${result.snippet}`).join('\n\n')}`;
        } else if (options.length > 0) {
            prompt = `Please provide only the letter(s) of the correct answer(s) (e.g., A, B, C, or D) without any explanation.\n\nQuiz Title: ${title}\nQuestion: ${question}\nOptions:\n${options.map((opt, index) => String.fromCharCode(65 + index) + '. ' + opt).join('\n')}\nSearch Results:\n${searchResults.map(result => `${result.title}\n${result.snippet}`).join('\n\n')}`;
        }

        console.log('Prompt for final answer:', prompt);

        try {
            const response = await fetch(apiUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${apiKey}`
                },
                body: JSON.stringify({
                    model: apimodel,
                    messages: [
                        {"role": "system", "content": "You are a helpful assistant."},
                        {"role": "user", "content": prompt}
                    ],
                    max_tokens: 1000
                })
            });
            const data = await response.json();
            console.log('Final answer API Response:', data);

            if (data.choices && data.choices.length > 0) {
                return data.choices[0].message.content.trim();
            } else {
                console.error('No choices found in API response');
                return 'No answer found';
            }
        } catch (error) {
            console.error('Error fetching final answer:', error);
            return 'Error fetching final answer';
        }
    }

    function displayAnswer(answer) {
        const answerElement = document.querySelector('.quiz-helper-answer');

        if (!answerElement) {
            const newAnswerElement = document.createElement('div');
        }
        const newAnswerElement = document.createElement('div');
        newAnswerElement.className = 'quiz-helper-answer';
        newAnswerElement.innerHTML = answer;
        newAnswerElement.style.color = 'red';
        newAnswerElement.style.fontWeight = 'bold';

        const questionElement = document.querySelector('.test-question ._title') || document.querySelector('.test-question [data-itemtype="question"]');
        if (questionElement) {
            questionElement.appendChild(newAnswerElement);
        }

        hideSmallSpinner();
    }

    async function answerMain() {
        if (!document.querySelector('.quiz-helper-answer')) {

            const title = getQuizTitle();
            const { question, options } = getQuestionAndOptions();

            if (question && ((options.left && options.left.length > 0) || options.length > 0)) {
                showSmallSpinner();

                const searchSuggestions = await getSearchSuggestions(title, question, options);
                console.log('Search Suggestions:', searchSuggestions);

                const searchResults = await duckDuckGoSearch(searchSuggestions);
                console.log('Search Results:', searchResults);

                const answer = await getChatGPTAnswerWithSearchResults(title, question, options, searchResults);
                displayAnswer(answer);
            }

            hideSmallSpinner();
        } else {
            console.log('No question or options found');
        }
    }

    function createControlPanel() {
        const panel = document.createElement('div');
        panel.style.position = 'fixed';
        panel.style.top = '10px';
        panel.style.right = '10px';
        panel.style.width = '300px';
        panel.style.height = '300px';
        panel.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
        panel.style.color = 'white';
        panel.style.padding = '10px';
        panel.style.borderRadius = '5px';
        panel.style.zIndex = '10000';
        panel.style.fontSize = '14px';
        panel.style.display = 'none'; // Initially hidden

        const autoCardToggle = document.createElement('label');
        autoCardToggle.innerHTML = `Auto Card: <input type="checkbox" ${enableAutoCard ? 'checked' : ''}>`;
        autoCardToggle.style.display = 'block';
        autoCardToggle.style.marginBottom = '5px';
        autoCardToggle.querySelector('input').addEventListener('change', (e) => {
            enableAutoCard = e.target.checked;
        });

        const markAsReadToggle = document.createElement('label');
        markAsReadToggle.innerHTML = `Mark as Read: <input type="checkbox" ${enableMarkAsRead ? 'checked' : ''}>`;
        markAsReadToggle.style.display = 'block';
        markAsReadToggle.style.marginBottom = '5px';
        markAsReadToggle.querySelector('input').addEventListener('change', (e) => {
            enableMarkAsRead = e.target.checked;
            if (enableMarkAsRead) {
                startMarkAsRead();
            }
        });

        const apiURLInput = document.createElement('input');
        apiURLInput.type = 'text';
        apiURLInput.placeholder = 'OpenAI API URL';
        apiURLInput.style.width = '100%';
        apiURLInput.style.marginBottom = '5px';
        apiURLInput.value = GM_getValue('openai_api_url', 'https://api.openai.com/v1/chat/completions');

        const apiKeyInput = document.createElement('input');
        apiKeyInput.type = 'text';
        apiKeyInput.placeholder = 'OpenAI API Key';
        apiKeyInput.style.width = '100%';
        apiKeyInput.style.marginBottom = '5px';
        apiKeyInput.value = GM_getValue('openai_api_key', '');

        const apiModelSelect = document.createElement('select');
        apiModelSelect.style.width = '100%';
        apiModelSelect.style.marginBottom = '10px';
        const models = ['', 'gpt-4o', 'gpt-4o-mini', 'gpt-4o-all', 'gpt-4-turbo', 'claude-3-5-sonnet-latest', 'gemini-2.0-flash-thinking-exp', 'gemini-2.0-flash-exp', 'gemini-1.5-flash'];
        models.forEach(model => {
            const option = document.createElement('option');
            option.value = model;
            option.textContent = model || 'Select a model';
            if (model === GM_getValue('openai_api_model')) {
                option.selected = true;
            }
            apiModelSelect.appendChild(option);
        });

        const saveButton = document.createElement('button');
        saveButton.textContent = 'Save Settings';
        saveButton.style.marginTop = '10px';
        saveButton.style.backgroundColor = 'green';
        saveButton.style.color = 'white';
        saveButton.style.border = 'none';
        saveButton.style.padding = '5px 10px';
        saveButton.style.cursor = 'pointer';
        saveButton.addEventListener('click', () => {
            GM_setValue('openai_api_url', apiURLInput.value);
            GM_setValue('openai_api_key', apiKeyInput.value);
            GM_setValue('openai_api_model', apiModelSelect.value);
            alert('OpenAI settings saved!');
        });

        panel.appendChild(autoCardToggle);
        panel.appendChild(markAsReadToggle);
        panel.appendChild(apiURLInput);
        panel.appendChild(apiKeyInput);
        panel.appendChild(apiModelSelect);
        panel.appendChild(saveButton);

        document.body.appendChild(panel);

        const dragIcon = document.createElement('div');
        dragIcon.style.position = 'fixed';
        dragIcon.style.top = '10px';
        dragIcon.style.right = '10px';
        dragIcon.style.width = '40px';
        dragIcon.style.height = '40px';
        dragIcon.style.backgroundImage = 'url(https://rcdn.tonyha7.com/sliver__wolf_playing.jpg)';
        dragIcon.style.backgroundSize = 'cover';
        dragIcon.style.backgroundPosition = 'center';
        dragIcon.style.borderRadius = '50%';
        dragIcon.style.cursor = 'pointer';
        dragIcon.style.opacity = '0.5';
        dragIcon.style.zIndex = '10001';

        dragIcon.addEventListener('mouseover', () => {
            dragIcon.style.opacity = '1';
        });

        dragIcon.addEventListener('mouseout', () => {
            dragIcon.style.opacity = '0.5';
        });

        dragIcon.addEventListener('click', () => {
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
        });

        let isDragging = false;
        let offsetX, offsetY;

        dragIcon.addEventListener('mousedown', (e) => {
            isDragging = true;
            offsetX = e.offsetX;
            offsetY = e.offsetY;
        });

        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                const x = e.clientX - offsetX;
                const y = e.clientY - offsetY;
                dragIcon.style.top = `${y}px`;
                dragIcon.style.left = `${x}px`;
                panel.style.top = `${y}px`;
                panel.style.left = `${x + 50}px`;
            }
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
        });

        document.body.appendChild(dragIcon);
    }

    // Call the createSmallSpinner function once to initialize the spinner element
    createSmallSpinner();

    // Observe changes in the DOM and rerun main function with debounce
    const debouncedMain = debounce(answerMain, 500);
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.addedNodes.length || mutation.removedNodes.length) {
                console.log('DOM changed, running main function');
                debouncedMain();
            }
        });
    });

    // Start observing the entire document for changes
    observer.observe(document.body, { childList: true, subtree: true });

    // Init
    if (window.location.href.includes('get_flash_card')) {
        startAutoCard();
    }

    if (window.location.href.includes('func=ebook') && enableMarkAsRead) {
        startMarkAsRead();
    }

    if (window.location.href.includes('app')) {
        window.addEventListener('load', answerMain);
    }

    if (window.location.href.includes('ucertify.com')) {
        createControlPanel();
    }
})();