Educake ChatGPT Auto-Integration

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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