Noordhoff AI

Ctrl+Shift+A voor antwoord. Ctrl+Shift+G als de docent langskomt.

// ==UserScript==
// @name         Noordhoff AI
// @version      1.8
// @description  Ctrl+Shift+A voor antwoord. Ctrl+Shift+G als de docent langskomt.
// @author       incomplete_tree
// @match        https://*.noordhoff.nl/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @namespace https://greasyfork.org/users/1511158
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuratie ---
    const OPENROUTER_API_KEY_GM_ID = 'openrouter_api_key';
    const CONCISENESS_LEVEL_GM_ID = 'ai_conciseness_level';
    const DEBUG_MODE = true; // Zet op true voor gedetailleerde logs in de console

    const concisenessLevels = {
        '-2': 'extreem beknopt. Geef alleen het eindantwoord.',
        '-1': 'zeer beknopt. Geef de formule en het eindantwoord.',
        '0': 'standaard. Geef de formule, de ingevulde formule en het eindantwoord.',
        '1': 'iets gedetailleerder. Leg de stappen kort uit in de berekening.',
        '2': 'zeer gedetailleerd. Geef een volledige uitleg met de berekening, alsof je het een leerling leert.'
    };
    let currentConciseness = '0'; // Standaard

    function log(...args) {
        if (DEBUG_MODE) {
            console.log('[Noordhoff AI]', ...args);
        }
    }

    // --- API-sleutel Beheer (onveranderd) ---
    async function getApiKey() {
        let apiKey = await GM_getValue(OPENROUTER_API_KEY_GM_ID);
        if (!apiKey || apiKey.trim() === "") {
            apiKey = window.prompt('Voer alstublieft uw OpenRouter API-sleutel in:');
            if (apiKey && apiKey.trim() !== "") {
                await GM_setValue(OPENROUTER_API_KEY_GM_ID, apiKey);
            } else {
                return null;
            }
        }
        return apiKey;
    }

    // --- Hoofdscript start hier (onveranderd) ---
    (async function() {
        const apiKey = await getApiKey();
        if (!apiKey) {
            alert('OpenRouter API-sleutel is niet ingesteld. De sneltoetsen werken niet.');
            return;
        }
        currentConciseness = await GM_getValue(CONCISENESS_LEVEL_GM_ID, '0');
        log(`Script geladen. Sleutel is ingesteld. Initiële beknoptheid: ${concisenessLevels[currentConciseness]}.`);
        console.log("Sneltoetsen: Ctrl+Shift+A (Toon Antwoord), Ctrl+Shift+G (Verberg Alles).");
        document.addEventListener('keydown', (event) => {
            if (event.ctrlKey && event.shiftKey && (event.key === 'A' || event.key === 'a')) {
                event.preventDefault();
                handleGenerateHotkey(apiKey);
            }
            if (event.ctrlKey && event.shiftKey && (event.key === 'G' || event.key === 'g')) {
                event.preventDefault();
                hideAllAnswers();
            }
        });
    })();

    // --- Hulpfunctie om het huidige vraagblok te vinden (onveranderd) ---
    function findCurrentQuestionBlock() {
        const focusedElement = document.activeElement;
        if (focusedElement && focusedElement !== document.body) {
            const block = focusedElement.closest('.pl-particle');
            if (block) return block;
        }
        for (const block of document.querySelectorAll('.pl-particle')) {
            const rect = block.getBoundingClientRect();
            if (rect.top >= 0 && rect.top <= window.innerHeight * 0.8) return block;
        }
        return null;
    }

    // --- HOOFDLOGICA: HERSCHREVEN OM FLEXIBELER TE ZIJN ---
    async function handleGenerateHotkey(apiKey, newConciseness = null) {
        if (newConciseness !== null) {
            currentConciseness = newConciseness;
            await GM_setValue(CONCISENESS_LEVEL_GM_ID, currentConciseness);
        }
        const questionBlock = findCurrentQuestionBlock();
        if (!questionBlock) {
            alert('Kon geen vraag vinden. Klik in een vraag en probeer het opnieuw.');
            return;
        }
        displayAnswer(questionBlock, apiKey, '🤖 Vraag analyseren...', true);
        try {
            const questionContentContainer = questionBlock.querySelector('.pl-open-question');
            if (!questionContentContainer) throw new Error('Kon de hoofdcontainer van de vraag (.pl-open-question) niet vinden.');

            const questionHtml = questionContentContainer.innerHTML;
            const concisenessInstruction = concisenessLevels[currentConciseness];
            let currentAnswerSection = '';

            // Lees huidig antwoord uit het flexibel gevonden invoerveld
            const answerInput = findAnswerInput(questionBlock);
            if (answerInput && answerInput.innerText.trim() !== "") {
                 currentAnswerSection = `\n\n## Huidig Antwoord van de Leerling (ter context):\n${answerInput.innerText.trim()}`;
            }

            const prompt = `Je bent een deskundige Nederlandse onderwijsassistent. Analyseer de HTML van het vraagblok. Je antwoord moet ${concisenessInstruction} en in het Nederlands zijn. Geef ALLEEN de stapsgewijze berekening, zonder extra zinnen. Het antwoord moet direct in een invulveld geplakt kunnen worden.\n\n## Vraagblok HTML:\n\`\`\`html\n${questionHtml}\n\`\`\`${currentAnswerSection}`;
            const response = await callOpenRouter(prompt, apiKey, 'google/gemini-flash-1.5');
            const fullAnswer = response.choices[0].message.content.trim();
            log("Antwoord ontvangen van AI:", fullAnswer);

            displayAnswer(questionBlock, apiKey, fullAnswer, false);
            autoFillAnswer(questionBlock, fullAnswer);
        } catch (error) {
            console.error('AI Assistent Fout:', error);
            displayAnswer(questionBlock, apiKey, `Fout: ${error.message}`);
        }
    }

    // **NIEUWE, SLIMME FUNCTIE OM HET ANTWOORDVELD TE VINDEN**
    function findAnswerInput(questionBlock) {
        if (!questionBlock) return null;

        // Zoek eerst de algemene container voor het antwoord.
        const mainAnswerArea = questionBlock.querySelector('.pl-main');
        if (!mainAnswerArea) {
            log('Kon de .pl-main antwoordcontainer niet vinden.');
            return null;
        }

        // Zoek nu binnen die container naar het specifieke invoerveld.
        // Hoogste prioriteit voor .ql-editor, want die hebben we bevestigd.
        let inputElement = mainAnswerArea.querySelector('.ql-editor');
        if (inputElement) {
            log('Specifiek ".ql-editor" invoerveld gevonden!');
            return inputElement;
        }

        // Fallback voor andere soorten vragen: zoek naar een akit-matharea en kijk daarin.
        const mathArea = mainAnswerArea.querySelector('akit-matharea');
        if (mathArea && mathArea.shadowRoot) {
            log('akit-matharea gevonden, ik kijk in de Shadow DOM.');
            inputElement = mathArea.shadowRoot.querySelector('.ql-editor');
            if (inputElement) {
                log('".ql-editor" gevonden binnen de Shadow DOM van akit-matharea!');
                return inputElement;
            }
        }

        // Allerlaatste fallback: zoek naar *iets* bewerkbaars.
        inputElement = mainAnswerArea.querySelector('textarea, [contenteditable="true"]');
        if (inputElement) {
            log('Algemeen bewerkbaar element gevonden als fallback.');
            return inputElement;
        }

        log('Kon geen enkel bekend invoerveld vinden in .pl-main.');
        return null;
    }


    // **AUTOFILL GEBRUIKT NU DE NIEUWE VIND-FUNCTIE**
    function autoFillAnswer(questionBlock, answerText) {
        log(`Start automatisch invullen...`);

        const targetInput = findAnswerInput(questionBlock);

        if (!targetInput) {
            log('FATALE FOUT: Kon geen invoerveld vinden om het antwoord in te vullen.');
            return;
        }

        log('DOELWIT GEVONDEN! Het daadwerkelijke invoerveld is:', targetInput);

        try {
            // Maak de inhoud voor de editor, inclusief <p> tags zoals de Quill editor het verwacht.
            const formattedAnswer = `<p>${answerText.replace(/\n/g, '</p><p>')}</p>`;

            targetInput.focus();
            targetInput.innerHTML = formattedAnswer;
            targetInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
            targetInput.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
            targetInput.blur();
            log('Automatisch invullen succesvol voltooid.');

        } catch (e) {
            log('Er is een fout opgetreden tijdens het uitvoeren van de invul-acties:', e);
        }
    }


    // --- Universele API-aanroep Functie (onveranderd) ---
    function callOpenRouter(prompt, apiKey, model) {
        return new Promise((resolve, reject) => {
            const messages = [{ role: 'user', content: prompt }];
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://openrouter.ai/api/v1/chat/completions',
                headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
                data: JSON.stringify({ model, messages }),
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) resolve(JSON.parse(response.responseText));
                    else reject(new Error(`API Fout (${response.status})`));
                },
                onerror: (response) => reject(new Error(`Netwerkfout`))
            });
        });
    }

    // --- Functie om het antwoord en de knoppen te tonen (onveranderd) ---
    function displayAnswer(questionBlock, apiKey, answerText, isThinking = false) {
        const containerId = 'ai-answer-display';
        let answerContainer = questionBlock.querySelector(`#${containerId}`);
        if (!answerContainer) {
            answerContainer = document.createElement('div');
            answerContainer.id = containerId;
            const answerTextElement = document.createElement('div');
            answerTextElement.style.marginBottom = '8px';
            answerTextElement.style.whiteSpace = 'pre-wrap';
            const controlsContainer = document.createElement('div');
            controlsContainer.style.display = 'flex';
            controlsContainer.style.gap = '8px';
            controlsContainer.style.marginTop = '8px';
            const lessConciseButton = document.createElement('button');
            lessConciseButton.innerText = 'Minder Beknopt';
            const moreConciseButton = document.createElement('button');
            moreConciseButton.innerText = 'Beknopter';
            [lessConciseButton, moreConciseButton].forEach(button => Object.assign(button.style, { padding: '4px 8px', border: '1px solid #ccc', borderRadius: '4px', background: '#f0f0f0', cursor: 'pointer' }));
            lessConciseButton.addEventListener('click', () => handleGenerateHotkey(apiKey, Math.min(2, parseInt(currentConciseness) + 1).toString()));
            moreConciseButton.addEventListener('click', () => handleGenerateHotkey(apiKey, Math.max(-2, parseInt(currentConciseness) - 1).toString()));
            controlsContainer.appendChild(moreConciseButton);
            controlsContainer.appendChild(lessConciseButton);
            answerContainer.appendChild(answerTextElement);
            answerContainer.appendChild(controlsContainer);
            const mainContentArea = questionBlock.querySelector('.pl-main');
            if (mainContentArea) mainContentArea.parentNode.insertBefore(answerContainer, mainContentArea);
            else questionBlock.appendChild(answerContainer);
        }
        answerContainer.querySelector('div').innerHTML = answerText.replace(/\n/g, '<br>');
        Object.assign(answerContainer.style, { margin: '10px 0', padding: '12px', border: '1px solid', borderRadius: '5px', fontFamily: 'sans-serif', fontSize: '16px', lineHeight: '1.5', borderColor: isThinking ? '#ffc107' : '#007bff', backgroundColor: isThinking ? '#fff3cd' : '#e7f3ff', color: isThinking ? '#856404' : '#004085' });
        const controls = answerContainer.querySelector('div:last-child');
        if (controls) controls.style.display = isThinking ? 'none' : 'flex';
    }

    // --- Functie om alle gegenereerde antwoorden te verbergen (onveranderd) ---
    function hideAllAnswers() {
        log("--- Verberg-sneltoets ingedrukt ---");
        document.querySelectorAll('#ai-answer-display').forEach(display => display.remove());
    }
})();