Educake ChatGPT Auto-Integration

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();