Educake ChatGPT Auto-Integration

Automatically sends Educake questions to ChatGPT API and displays responses inline (make sure to add in apikey)

// ==UserScript==
// @name         Educake ChatGPT Auto-Integration
// @version      2.1
// @description  Automatically sends Educake questions to ChatGPT API and displays responses inline (make sure to add in apikey)
// @author       frozled @ guns.lol/frozled
// @match        *://*.educake.co.uk/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      api.openai.com
// @license      MIT
// @namespace https://greasyfork.org/users/1390797
// ==/UserScript==

(function() {
    'use strict';

    // Configuration object
    const config = {
        apiKey: '', // Store your API key here or use GM_getValue to get it from storage
        model: 'gpt-3.5-turbo',
        apiEndpoint: 'https://api.openai.com/v1/chat/completions'
    };

    // Styles for the UI elements
    const styles = `
        .gpt-response {
            margin-top: 10px;
            padding: 10px;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
            background-color: #f8f9fa;
        }
        .gpt-loading {
            color: #666;
            font-style: italic;
        }
        .api-key-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 1000;
            width: 400px;
        }
        .modal-backdrop {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            z-index: 999;
        }
        .error-message {
            color: #dc3545;
            margin-top: 10px;
            padding: 10px;
            border: 1px solid #dc3545;
            border-radius: 4px;
            background-color: #fff;
        }
        .debug-info {
            margin-top: 5px;
            font-size: 12px;
            color: #666;
            font-family: monospace;
            white-space: pre-wrap;
        }
    `;

    // Add styles to document
    function addStyles() {
        const styleSheet = document.createElement('style');
        styleSheet.textContent = styles;
        document.head.appendChild(styleSheet);
    }

    // Create and show API key modal
    function showApiKeyModal() {
        const modalHtml = `
            <div class="modal-backdrop">
                <div class="api-key-modal">
                    <h3>Enter your OpenAI API Key</h3>
                    <p>You need to enter your OpenAI API key to use this feature. You can get one from <a href="https://platform.openai.com/api-keys" target="_blank">OpenAI's website</a>.</p>
                    <input type="password" id="api-key-input" placeholder="sk-..." style="width: 100%; margin: 10px 0; padding: 5px;">
                    <button id="save-api-key" style="padding: 5px 10px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;">Save Key</button>
                    <div id="api-key-error" style="color: red; margin-top: 10px;"></div>
                </div>
            </div>
        `;

        const modalContainer = document.createElement('div');
        modalContainer.innerHTML = modalHtml;
        document.body.appendChild(modalContainer);

        document.getElementById('save-api-key').addEventListener('click', () => {
            const apiKey = document.getElementById('api-key-input').value.trim();
            if (apiKey.startsWith('sk-') && apiKey.length > 20) {
                GM_setValue('openai_api_key', apiKey);
                config.apiKey = apiKey;
                modalContainer.remove();
            } else {
                document.getElementById('api-key-error').textContent = 'Please enter a valid OpenAI API key (should start with sk-)';
            }
        });
    }

    // Function to get response from ChatGPT
    async function getGPTResponse(question) {
        if (!config.apiKey) {
            config.apiKey = GM_getValue('openai_api_key', '');
            if (!config.apiKey) {
                showApiKeyModal();
                return null;
            }
        }

        return new Promise((resolve, reject) => {
            const requestData = {
                model: config.model,
                messages: [{
                    role: 'user',
                    content: question
                }],
                temperature: 0.7
            };

            GM_xmlhttpRequest({
                method: 'POST',
                url: config.apiEndpoint,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${config.apiKey}`
                },
                data: JSON.stringify(requestData),
                onload: function(response) {
                    try {
                        const responseData = JSON.parse(response.responseText);
                        console.log('API Response:', responseData); // Debug log

                        if (response.status === 200 && responseData.choices && responseData.choices[0]) {
                            resolve(responseData.choices[0].message.content);
                        } else {
                            const errorMessage = responseData.error ? responseData.error.message : 'Unknown error';
                            reject(new Error(`API Error (${response.status}): ${errorMessage}`));
                        }
                    } catch (error) {
                        console.error('Response parsing error:', error);
                        reject(new Error(`Failed to parse API response: ${error.message}`));
                    }
                },
                onerror: function(error) {
                    console.error('Request error:', error);
                    reject(new Error(`Network error: ${error.statusText || 'Failed to connect to API'}`));
                }
            });
        });
    }

    // Function to create and inject the GPT button
    function injectGPTButton() {
        if (!document.getElementById('gpt-button')) {
            const button = document.createElement('div');
            button.id = 'gpt-button';
            button.className = 'btn bg-green-80 bg-green-hover r-bg-light r-bg-light-hover r-text-dark ml-2 lh-close mb-2 mb-sm-0 align-self-start';
            button.textContent = 'Ask ChatGPT';

            button.addEventListener('click', async function() {
                const questionElements = document.querySelectorAll('.question-text');
                const question = Array.from(questionElements).map(el => el.textContent).join('\n');

                // Create response container
                let responseContainer = document.querySelector('.gpt-response');
                if (!responseContainer) {
                    responseContainer = document.createElement('div');
                    responseContainer.className = 'gpt-response';
                    button.parentElement.appendChild(responseContainer);
                }

                responseContainer.innerHTML = '<div class="gpt-loading">Getting response from ChatGPT...</div>';

                try {
                    const response = await getGPTResponse(question);
                    if (response) {
                        responseContainer.innerHTML = `
                            <strong>ChatGPT Response:</strong>
                            <div style="margin-top: 8px;">${response.replace(/\n/g, '<br>')}</div>
                        `;
                    }
                } catch (error) {
                    responseContainer.innerHTML = `
                        <div class="error-message">
                            Error: ${error.message}
                            <div class="debug-info">
                                Status: ${error.status || 'N/A'}
                                Time: ${new Date().toISOString()}
                            </div>
                        </div>
                    `;

                    // If the error is related to authentication, show the API key modal
                    if (error.message.includes('401') || error.message.includes('authentication')) {
                        config.apiKey = ''; // Clear the invalid API key
                        GM_setValue('openai_api_key', ''); // Clear stored key
                        showApiKeyModal();
                    }
                }
            });

            const existingDiv = document.querySelector('.column');
            existingDiv.insertBefore(button, existingDiv.lastElementChild);
        }
    }

    // Remove paste restrictions
    function removePasteRestrictions() {
        const elements = document.querySelectorAll('.answer-text');
        elements.forEach(element => {
            element.removeAttribute('onpaste');
        });
    }

    // Initialize
    function init() {
        addStyles();
        setInterval(injectGPTButton, 500);
        setInterval(removePasteRestrictions, 500);
    }

    // Start the script
    init();
})();