您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Ctrl+Shift+A voor antwoord. Ctrl+Shift+G als de docent langskomt.
// ==UserScript== // @name Noordhoff AI // @version 1.0 // @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 // @license GPL v3.0+ // @namespace https://gf.qytechs.cn/users/1511158 // ==/UserScript== (function() { 'use strict'; const OPENROUTER_API_KEY_GM_ID = 'openrouter_api_key'; const CONCISENESS_LEVEL_GM_ID = 'ai_conciseness_level'; const concisenessLevels = { '-2': 'extremely concise. Use only essential keywords or numbers.', '-1': 'very concise. Be as brief as possible.', '0': 'default. Provide a clear and standard-length answer.', '1': 'a bit more detailed. Explain the steps briefly.', '2': 'very detailed. Provide a full explanation as if teaching a student.' }; let currentConciseness = '0'; // Default // --- API Key Management --- async function getApiKey() { let apiKey = await GM_getValue(OPENROUTER_API_KEY_GM_ID); if (!apiKey || apiKey.trim() === "") { apiKey = window.prompt('Please enter your OpenRouter API key:'); if (apiKey && apiKey.trim() !== "") { await GM_setValue(OPENROUTER_API_KEY_GM_ID, apiKey); } else { return null; } } return apiKey; } // --- Main execution starts here --- (async function() { const apiKey = await getApiKey(); if (!apiKey) { alert('OpenRouter API key is not set. The hotkeys will not work.'); return; } currentConciseness = await GM_getValue(CONCISENESS_LEVEL_GM_ID, '0'); console.log(`AI Answer Assistant: Key is set. Initial conciseness: ${concisenessLevels[currentConciseness]}.`); console.log("Hotkeys: Ctrl+Shift+A (Show Answer), Ctrl+Shift+G (Hide All)."); 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(); } }); })(); // --- Helper to find the current question block --- 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; } // --- Main Hotkey Logic --- async function handleGenerateHotkey(apiKey, newConciseness = null) { if (newConciseness !== null) { currentConciseness = newConciseness; await GM_setValue(CONCISENESS_LEVEL_GM_ID, currentConciseness); console.log(`Conciseness updated to: ${concisenessLevels[currentConciseness]}`); } const questionBlock = findCurrentQuestionBlock(); if (!questionBlock) { alert('Could not find a focused or visible question. Please scroll to a question and try again.'); return; } displayAnswer(questionBlock, '🤖 Analyzing question...', true); try { const questionHeader = questionBlock.querySelector('h5'); const questionElement = questionBlock.querySelector('.pl-content-question'); if (!questionElement) throw new Error('Could not find question content element.'); const headerText = questionHeader ? `Sub-question: ${questionHeader.innerText}\n\n` : ''; const questionHtml = questionElement.innerHTML; const concisenessInstruction = concisenessLevels[currentConciseness]; // --- NEW: Image Processing Step --- const images = questionElement.querySelectorAll('img'); let imageDescriptions = ''; if (images.length > 0) { displayAnswer(questionBlock, `🤖 Found ${images.length} image(s), analyzing...`, true); const descriptionPromises = Array.from(images).map(img => describeImageWithAI(img.src, apiKey)); const descriptions = await Promise.all(descriptionPromises); imageDescriptions = "\n\n## Image Descriptions:\n" + descriptions.map((desc, i) => `[Image ${i+1}]: ${desc}`).join("\n"); displayAnswer(questionBlock, '🤖 Generating answer...', true); } // --- End of Image Processing --- const prompt = `You are an expert teaching assistant. A student has asked for help with the following question. Your response should be ${concisenessInstruction}. Analyze the question's HTML content and any provided image descriptions. Provide the answer. Do not include any extra conversational text like "Here is the answer:". Just provide the answer itself.\n\n${headerText}## Question HTML:\n\`\`\`html\n${questionHtml}\n\`\`\`${imageDescriptions}`; const response = await callOpenRouter(prompt, apiKey, 'google/gemini-flash-1.5'); const answer = response.choices[0].message.content.trim(); displayAnswer(questionBlock, answer, false, apiKey); } catch (error) { console.error('AI Assistant Error:', error); displayAnswer(questionBlock, `Error: ${error.message}`); } } // --- Universal API Call Function --- function callOpenRouter(prompt, apiKey, model, multimodalContent = null) { return new Promise((resolve, reject) => { const messages = multimodalContent ? [{ role: 'user', content: multimodalContent }] : [{ 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 { const errorBody = JSON.parse(response.responseText); const errorMessage = errorBody.error?.message || response.responseText; reject(new Error(`API Error (${response.status}): ${errorMessage}`)); } }, onerror: (response) => reject(new Error(`Network error: ${response.statusText || 'Failed to send request'}`)) }); }); } // --- NEW: Function to describe an image using a vision model --- async function describeImageWithAI(imageUrl, apiKey) { console.log("Requesting description for image:", imageUrl); const multimodalContent = [ { type: "text", text: "Describe this image concisely for a physics student. Focus on labels, values, and the relationships between components." }, { type: "image_url", image_url: { "url": imageUrl } } ]; try { const response = await callOpenRouter(null, apiKey, 'google/gemini-pro-vision', multimodalContent); return response.choices[0].message.content.trim(); } catch (error) { console.error(`Failed to describe image ${imageUrl}:`, error); return `[Error describing image: ${error.message}]`; // Return an error message to be included in the prompt } } // --- Function to display the answer and control buttons --- function displayAnswer(questionBlock, answerText, isThinking = false, apiKey = null) { const answerContainerId = 'ai-answer-display'; let answerContainer = questionBlock.querySelector(`#${answerContainerId}`); if (!answerContainer) { answerContainer = document.createElement('div'); answerContainer.id = answerContainerId; const mainContentArea = questionBlock.querySelector('.pl-main'); if (mainContentArea) mainContentArea.parentNode.insertBefore(answerContainer, mainContentArea); else questionBlock.appendChild(answerContainer); } const answerContent = `<div id="ai-answer-text" style="margin-bottom: 8px; white-space: pre-wrap;">${answerText.replace(/\n/g, '<br>')}</div>`; const controlButtonsHTML = ` <div id="ai-controls" style="display: flex; gap: 8px; margin-top: 8px;"> <button id="ai-less-concise" style="padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; background: #f0f0f0; cursor: pointer;">Less Concise</button> <button id="ai-more-concise" style="padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; background: #f0f0f0; cursor: pointer;">More Concise</button> </div>`; answerContainer.innerHTML = answerContent + (isThinking || !apiKey ? '' : controlButtonsHTML); 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' }); if (!isThinking && apiKey) { answerContainer.querySelector('#ai-more-concise').addEventListener('click', () => { const newLevel = Math.max(-2, parseInt(currentConciseness) - 1).toString(); handleGenerateHotkey(apiKey, newLevel); }); answerContainer.querySelector('#ai-less-concise').addEventListener('click', () => { const newLevel = Math.min(2, parseInt(currentConciseness) + 1).toString(); handleGenerateHotkey(apiKey, newLevel); }); } } // --- Function to hide all generated answers --- function hideAllAnswers() { console.log("--- AI Assistant: Hide hotkey pressed ---"); document.querySelectorAll('#ai-answer-display').forEach(display => display.remove()); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址