NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Homework AI Chat Helper Enhanced (v1.18) // @namespace http://tampermonkey.net/ // @version 1.18 // @license MIT // @author ChatGPT [AI Made This] // @description Chat helper with history saving, markdown formatting, export (JSON, Markdown, or plain text), auto-detect API, response speed indicator, voice input, resizable container, tap-to-copy messages (copies only the message text), and customization of message colors and name. // @match *://*.r22.core.learn.edgenuity.com/player/* // @match *://*.thelearningodyssey.com/DLO/Player.aspx#/* // @match *://*.thelearningodyssey.com/assess/RuntimeQuestion.aspx?TaskId=0&LAId=* // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_addStyle // @connect openrouter.ai // @connect generativelanguage.googleapis.com // @connect cdn.jsdelivr.net // ==/UserScript== (async function() { 'use strict'; // ======= CONFIG & LOCAL STORAGE KEYS ======== const STORAGE_KEYS = { openRouterKey: 'aiHelper_openRouter_apiKey', geminiKey: 'aiHelper_gemini_apiKey', apiChoice: 'aiHelper_apiChoice', // "openrouter" or "gemini" darkMode: 'aiHelper_darkMode', openRouterModel: 'aiHelper_openRouterModel', chatHistory: 'aiHelper_chatHistory', customName: 'aiHelper_customName', customUserColor: 'aiHelper_customUserColor', customAIColor: 'aiHelper_customAIColor' }; // Global customization variables (loaded later) let customName = "You"; let customUserColor = ""; let customAIColor = ""; // ======= UTILITY FUNCTIONS ======== function saveSetting(key, value) { localStorage.setItem(key, value); } function getSetting(key) { return localStorage.getItem(key); } // Chat history functions function saveChatHistory(history) { localStorage.setItem(STORAGE_KEYS.chatHistory, JSON.stringify(history)); } function loadChatHistory() { const history = localStorage.getItem(STORAGE_KEYS.chatHistory); return history ? JSON.parse(history) : []; } // Simple markdown formatting: **bold**, *italic*, and triple backtick code blocks. function formatResponse(text) { text = text.replace(/```([\s\S]*?)```/g, (match, p1) => `<pre><code>${p1.trim()}</code></pre>`); text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>'); return text; } // ======= API FUNCTIONS ======== function fetchOpenRouterModels(apiKey, callback) { if (!apiKey) { callback(null, 'No OpenRouter API key provided.'); return; } console.log('Fetching OpenRouter models...'); GM_xmlhttpRequest({ method: 'GET', url: 'https://openrouter.ai/api/v1/models', headers: { 'Authorization': `Bearer ${apiKey}` }, onload: function(response) { if (response.status === 200) { const data = JSON.parse(response.responseText); callback(data.data, null); } else { callback(null, `Error: ${response.status} - ${response.responseText}`); } }, onerror: function() { callback(null, 'Failed to connect to OpenRouter API.'); } }); } function fetchGeminiModels(apiKey, callback) { if (!apiKey) { callback(null, 'No Gemini API key provided.'); return; } console.log('Fetching Google Gemini models...'); GM_xmlhttpRequest({ method: 'GET', url: 'https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, headers: { 'Content-Type': 'application/json' }, onload: function(response) { if (response.status === 200) { const data = JSON.parse(response.responseText); const models = data.models.filter(model => model.name.startsWith('models/gemini-2.0-')) .map(model => ({ id: model.name.split('/').pop(), name: model.displayName || model.name.split('/').pop() })); callback(models, null); } else { console.error('Failed to fetch Gemini models:', response.status, response.responseText); callback(null, `Error: ${response.status} - ${response.responseText}`); } }, onerror: function() { callback(null, 'Failed to connect to Google Gemini API.'); } }); } function generateOpenRouterText(apiKey, model, messages, callback) { const payload = { model, messages }; console.log('Sending request to OpenRouter:', payload); const startTime = performance.now(); GM_xmlhttpRequest({ method: 'POST', url: 'https://openrouter.ai/api/v1/chat/completions', headers: { 'Content-Type': 'application/json', 'Authorization': apiKey ? `Bearer ${apiKey}` : '' }, data: JSON.stringify(payload), onload: function(response) { const duration = ((performance.now() - startTime) / 1000).toFixed(2); if (response.status === 200) { const data = JSON.parse(response.responseText); const generatedText = data.choices[0].message.content.trim(); callback(null, generatedText, duration); } else { callback(`Error: ${response.status} - ${response.responseText}`, null, duration); } }, onerror: function() { callback('Failed to connect to OpenRouter API.', null, null); } }); } function generateGeminiText(apiKey, model, messages, temperature, callback) { const payload = { contents: [{ parts: messages.map(msg => ({ text: msg.content })) }], generationConfig: { temperature: parseFloat(temperature) || 1.0, maxOutputTokens: 2048 } }; console.log('Sending request to Gemini:', payload); const startTime = performance.now(); GM_xmlhttpRequest({ method: 'POST', url: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(payload), onload: function(response) { const duration = ((performance.now() - startTime) / 1000).toFixed(2); if (response.status === 200) { const data = JSON.parse(response.responseText); const generatedText = data.candidates[0].content.parts[0].text.trim(); callback(null, generatedText, duration); } else { callback(`Error: ${response.status} - ${response.responseText}`, null, duration); } }, onerror: function() { callback('Failed to connect to Google Gemini API.', null, null); } }); } // ======= UI BUILDING & STYLING ======== GM_addStyle(` /* Resizable Container */ #aiHelperContainer { position: fixed; bottom: 10px; right: 10px; width: 360px; height: auto; max-height: 90vh; background: #fff; color: #000; border: 1px solid #ccc; z-index: 10000; font-family: Arial, sans-serif; box-shadow: 0 2px 8px rgba(0,0,0,0.3); border-radius: 5px; overflow: auto; resize: both; cursor: move; } #aiHelperContainer.dark { background: #2e2e2e !important; color: #fff !important; border: 1px solid #555 !important; } /* Header */ #aiHelperHeader { background: #007acc; color: #fff; padding: 8px; cursor: move; } #aiHelperHeader.dark { background: #005a9e; } /* Tabs */ #aiHelperTabs { display: flex; border-bottom: 1px solid #ccc; } #aiHelperTabs button { flex: 1; padding: 8px; border: none; background: #f0f0f0; cursor: pointer; color: #000; } #aiHelperTabs button.active { background: #ddd; } #aiHelperContainer.dark #aiHelperTabs button { background: #555; color: #fff; } #aiHelperContainer.dark #aiHelperTabs button.active { background: #666; } /* Content */ #aiHelperContent { padding: 8px; } #aiHelperContent textarea, #aiHelperContent input, #aiHelperContent select { width: 100%; margin-bottom: 8px; padding: 4px; box-sizing: border-box; color: #000; background: #fff; } #aiHelperContainer.dark #aiHelperContent textarea, #aiHelperContainer.dark #aiHelperContent input, #aiHelperContainer.dark #aiHelperContent select { color: #fff !important; background: #555 !important; } #aiHelperContent button { width: 100%; padding: 8px; background: #007acc; color: #fff; border: none; cursor: pointer; margin-bottom: 8px; } #aiHelperContent .hidden { display: none; } /* Chat log styling */ #aiChatLog { height: 200px; overflow-y: auto; border: 1px solid #ccc; margin-bottom: 8px; padding: 4px; background: #fafafa; color: #000; } #aiHelperContainer.dark #aiChatLog { background: #3a3a3a; border-color: #555; color: #fff; } .chatMessage { margin-bottom: 8px; padding: 6px; border-radius: 4px; cursor: pointer; position: relative; } .chatTimestamp { position: absolute; right: 6px; bottom: 4px; font-size: 0.7em; color: inherit; opacity: 0.7; } .chatUser { background: #e1f5fe; color: #000; } .chatAI { background: #f1f8e9; color: #000; } /* Dark mode chat colors */ #aiHelperContainer.dark .chatUser { background: #2e4a7d; color: #fff; } #aiHelperContainer.dark .chatAI { background: #3a9d3a; color: #fff; } /* Dark Mode Toggle Button */ #darkModeToggleBtn { width: 100%; padding: 8px; background: #007acc; color: #fff; border: none; cursor: pointer; margin-bottom: 8px; } #aiHelperContainer.dark #darkModeToggleBtn { background: #005a9e; } /* Clear History Button (in Settings only) */ #clearHistoryBtn { width: 100%; padding: 8px; background: #d9534f; color: #fff; border: none; cursor: pointer; margin-bottom: 8px; } /* Download Button */ #downloadBtn { width: 100%; padding: 8px; background: #5cb85c; color: #fff; border: none; cursor: pointer; margin-bottom: 8px; } `); // Create container and header const container = document.createElement('div'); container.id = 'aiHelperContainer'; document.body.appendChild(container); const header = document.createElement('div'); header.id = 'aiHelperHeader'; header.textContent = 'Homework AI Chat Helper'; container.appendChild(header); // Tabs (Chat, Settings, Customization) const tabs = document.createElement('div'); tabs.id = 'aiHelperTabs'; container.appendChild(tabs); const tabMain = document.createElement('button'); tabMain.textContent = 'Chat'; tabMain.className = 'active'; tabs.appendChild(tabMain); const tabSettings = document.createElement('button'); tabSettings.textContent = 'Settings'; tabs.appendChild(tabSettings); const tabCustomization = document.createElement('button'); tabCustomization.textContent = 'Customization'; tabs.appendChild(tabCustomization); // Content container const content = document.createElement('div'); content.id = 'aiHelperContent'; container.appendChild(content); // ======= MAIN TAB UI ======== const chatLog = document.createElement('div'); chatLog.id = 'aiChatLog'; content.appendChild(chatLog); // Voice Input Button (only on Chat tab) const voiceBtn = document.createElement('button'); voiceBtn.id = 'voiceBtn'; voiceBtn.textContent = '🎤 Speak Your Question'; content.appendChild(voiceBtn); const questionInput = document.createElement('textarea'); questionInput.rows = 3; questionInput.placeholder = 'Enter your homework question here...'; content.appendChild(questionInput); // Keyboard shortcut: Enter sends message (Shift+Enter for newline). questionInput.addEventListener('keydown', (e) => { if(e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); generateButton.click(); } }); // Remove image sending elements (not added) // Send button const generateButton = document.createElement('button'); generateButton.textContent = 'Send'; content.appendChild(generateButton); // Export Container: Only the Download button now. const exportContainer = document.createElement('div'); exportContainer.style.overflow = 'hidden'; content.appendChild(exportContainer); const downloadBtn = document.createElement('button'); downloadBtn.id = 'downloadBtn'; downloadBtn.textContent = 'To download chat'; exportContainer.appendChild(downloadBtn); // Loading Bar (hidden by default) const loadingBar = document.createElement('div'); loadingBar.id = 'loadingBar'; loadingBar.style.display = 'none'; content.appendChild(loadingBar); // ======= SETTINGS TAB UI ======== const settingsDiv = document.createElement('div'); settingsDiv.className = 'hidden'; content.appendChild(settingsDiv); const apiChoiceLabel = document.createElement('label'); apiChoiceLabel.textContent = 'Select API: '; settingsDiv.appendChild(apiChoiceLabel); // Only OpenRouter and Google Gemini options const selectAPI = document.createElement('select'); const optionOpenRouter = document.createElement('option'); optionOpenRouter.value = 'openrouter'; optionOpenRouter.textContent = 'OpenRouter'; selectAPI.appendChild(optionOpenRouter); const optionGemini = document.createElement('option'); optionGemini.value = 'gemini'; optionGemini.textContent = 'Google Gemini'; selectAPI.appendChild(optionGemini); settingsDiv.appendChild(selectAPI); selectAPI.addEventListener('change', () => { saveSetting(STORAGE_KEYS.apiChoice, selectAPI.value); if(selectAPI.value === 'openrouter') { openRouterKeyInput.style.display = 'block'; openRouterModelSelect.style.display = 'block'; geminiKeyInput.style.display = 'none'; const key = openRouterKeyInput.value.trim(); fetchOpenRouterModels(key, (models, err) => { if(err){ console.error(err); return; } openRouterModelSelect.innerHTML = ''; models.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.id; openRouterModelSelect.appendChild(option); }); }); } else if(selectAPI.value === 'gemini') { openRouterKeyInput.style.display = 'none'; openRouterModelSelect.style.display = 'none'; geminiKeyInput.style.display = 'block'; } }); // OpenRouter API key input const openRouterKeyInput = document.createElement('input'); openRouterKeyInput.type = 'text'; openRouterKeyInput.placeholder = 'Enter OpenRouter API Key'; settingsDiv.appendChild(openRouterKeyInput); openRouterKeyInput.addEventListener('keypress', (e) => { if(e.key === 'Enter') { const key = openRouterKeyInput.value.trim(); saveSetting(STORAGE_KEYS.openRouterKey, key); fetchOpenRouterModels(key, (models, err) => { if(err){ console.error(err); return; } openRouterModelSelect.innerHTML = ''; models.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.id; openRouterModelSelect.appendChild(option); }); const savedModel = getSetting(STORAGE_KEYS.openRouterModel); if(savedModel){ openRouterModelSelect.value = savedModel; } }); } }); // Dropdown for models (used for OpenRouter) const openRouterModelSelect = document.createElement('select'); settingsDiv.appendChild(openRouterModelSelect); openRouterModelSelect.addEventListener('change', () => { saveSetting(STORAGE_KEYS.openRouterModel, openRouterModelSelect.value); }); // Google Gemini API key input const geminiKeyInput = document.createElement('input'); geminiKeyInput.type = 'text'; geminiKeyInput.placeholder = 'Enter Google Gemini API Key'; settingsDiv.appendChild(geminiKeyInput); // Dark Mode Toggle Button const darkModeToggleBtn = document.createElement('button'); darkModeToggleBtn.id = 'darkModeToggleBtn'; darkModeToggleBtn.textContent = 'Dark Mode: Off'; settingsDiv.appendChild(darkModeToggleBtn); darkModeToggleBtn.addEventListener('click', () => { let isDark = getSetting(STORAGE_KEYS.darkMode) === 'true'; isDark = !isDark; saveSetting(STORAGE_KEYS.darkMode, isDark ? 'true' : 'false'); applyDarkMode(); }); // Clear History Button const clearHistoryBtn = document.createElement('button'); clearHistoryBtn.id = 'clearHistoryBtn'; clearHistoryBtn.textContent = 'Clear Chat History'; settingsDiv.appendChild(clearHistoryBtn); clearHistoryBtn.addEventListener('click', () => { if (confirm("Are you sure you want to clear the chat history? This cannot be undone.")) { localStorage.removeItem(STORAGE_KEYS.chatHistory); chatLog.innerHTML = ''; alert("Chat history deleted"); } }); // Save Settings Button const saveSettingsBtn = document.createElement('button'); saveSettingsBtn.textContent = 'Save Settings'; settingsDiv.appendChild(saveSettingsBtn); saveSettingsBtn.addEventListener('click', () => { saveSetting(STORAGE_KEYS.openRouterKey, openRouterKeyInput.value.trim()); saveSetting(STORAGE_KEYS.geminiKey, geminiKeyInput.value.trim()); saveSetting(STORAGE_KEYS.apiChoice, selectAPI.value); alert('Settings saved!'); applyDarkMode(); }); // ======= CUSTOMIZATION TAB UI ======== const customizationDiv = document.createElement('div'); customizationDiv.className = 'hidden'; content.appendChild(customizationDiv); const customNameLabel = document.createElement('label'); customNameLabel.textContent = 'Your Name: '; customizationDiv.appendChild(customNameLabel); const customNameInput = document.createElement('input'); customNameInput.type = 'text'; customNameInput.placeholder = 'Enter your name'; customizationDiv.appendChild(customNameInput); const userColorLabel = document.createElement('label'); userColorLabel.textContent = 'Your Message Color: '; customizationDiv.appendChild(userColorLabel); const customUserColorInput = document.createElement('input'); customUserColorInput.type = 'color'; customizationDiv.appendChild(customUserColorInput); const aiColorLabel = document.createElement('label'); aiColorLabel.textContent = 'AI Message Color: '; customizationDiv.appendChild(aiColorLabel); const customAIColorInput = document.createElement('input'); customAIColorInput.type = 'color'; customizationDiv.appendChild(customAIColorInput); const saveCustomizationBtn = document.createElement('button'); saveCustomizationBtn.textContent = 'Save Customization'; customizationDiv.appendChild(saveCustomizationBtn); saveCustomizationBtn.addEventListener('click', () => { saveSetting(STORAGE_KEYS.customName, customNameInput.value.trim()); saveSetting(STORAGE_KEYS.customUserColor, customUserColorInput.value); saveSetting(STORAGE_KEYS.customAIColor, customAIColorInput.value); loadCustomization(); alert('Customization saved!'); }); function loadCustomization() { customName = getSetting(STORAGE_KEYS.customName) || "You"; customUserColor = getSetting(STORAGE_KEYS.customUserColor) || ""; customAIColor = getSetting(STORAGE_KEYS.customAIColor) || ""; customNameInput.value = customName; if(customUserColor) { customUserColorInput.value = customUserColor; } if(customAIColor) { customAIColorInput.value = customAIColor; } } // ======= DRAGGABLE FUNCTIONALITY ======== let isDragging = false; header.addEventListener('mousedown', startDrag); header.addEventListener('dragstart', (e) => e.preventDefault()); function startDrag(e) { isDragging = true; e.preventDefault(); let startX = e.clientX; let startY = e.clientY; const rect = container.getBoundingClientRect(); let origX = rect.left; let origY = rect.top; function onMouseMove(e) { let newX = origX + (e.clientX - startX); let newY = origY + (e.clientY - startY); container.style.left = newX + 'px'; container.style.top = newY + 'px'; container.style.right = 'auto'; container.style.bottom = 'auto'; } function onMouseUp() { isDragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); } header.addEventListener('dblclick', () => { container.style.height = container.style.height === '30px' ? 'auto' : '30px'; content.style.display = content.style.display === 'none' ? 'block' : 'none'; tabs.style.display = tabs.style.display === 'none' ? 'flex' : 'none'; }); // ======= TAB SWITCHING ======== function showTab(tab) { // Hide all tab-specific elements. chatLog.style.display = 'none'; questionInput.style.display = 'none'; generateButton.style.display = 'none'; exportContainer.style.display = 'none'; voiceBtn.style.display = 'none'; settingsDiv.classList.add('hidden'); customizationDiv.classList.add('hidden'); // Show elements for the selected tab. if(tab === 'chat') { chatLog.style.display = 'block'; questionInput.style.display = 'block'; generateButton.style.display = 'block'; exportContainer.style.display = 'block'; voiceBtn.style.display = 'block'; } else if(tab === 'settings') { settingsDiv.classList.remove('hidden'); } else if(tab === 'customization') { customizationDiv.classList.remove('hidden'); } } tabMain.addEventListener('click', () => { tabMain.classList.add('active'); tabSettings.classList.remove('active'); tabCustomization.classList.remove('active'); showTab('chat'); }); tabSettings.addEventListener('click', () => { tabSettings.classList.add('active'); tabMain.classList.remove('active'); tabCustomization.classList.remove('active'); showTab('settings'); }); tabCustomization.addEventListener('click', () => { tabCustomization.classList.add('active'); tabMain.classList.remove('active'); tabSettings.classList.remove('active'); showTab('customization'); }); // ======= INITIALIZE SETTINGS, CUSTOMIZATION & LOAD CHAT HISTORY ======== function loadSettings() { const savedOpenRouterKey = getSetting(STORAGE_KEYS.openRouterKey) || ''; const savedGeminiKey = getSetting(STORAGE_KEYS.geminiKey) || ''; const savedApiChoice = getSetting(STORAGE_KEYS.apiChoice) || 'openrouter'; const savedDarkMode = getSetting(STORAGE_KEYS.darkMode) || 'false'; const savedModel = getSetting(STORAGE_KEYS.openRouterModel) || ''; openRouterKeyInput.value = savedOpenRouterKey; geminiKeyInput.value = savedGeminiKey; selectAPI.value = savedApiChoice; darkModeToggleBtn.textContent = savedDarkMode === 'true' ? 'Dark Mode: On' : 'Dark Mode: Off'; if(savedApiChoice === 'openrouter') { openRouterKeyInput.style.display = 'block'; openRouterModelSelect.style.display = 'block'; geminiKeyInput.style.display = 'none'; fetchOpenRouterModels(savedOpenRouterKey, (models, err) => { if(err){ console.error(err); return; } openRouterModelSelect.innerHTML = ''; models.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.id; openRouterModelSelect.appendChild(option); }); if(savedModel){ openRouterModelSelect.value = savedModel; } }); } else if(savedApiChoice === 'gemini') { openRouterKeyInput.style.display = 'none'; openRouterModelSelect.style.display = 'none'; geminiKeyInput.style.display = 'block'; } applyDarkMode(); loadCustomization(); // Load and display chat history const history = loadChatHistory(); chatLog.innerHTML = ''; history.forEach(msg => appendMessage(msg.role, msg.text, false)); } function applyDarkMode() { let isDark = getSetting(STORAGE_KEYS.darkMode) === 'true'; if (isDark) { container.classList.add('dark'); header.classList.add('dark'); darkModeToggleBtn.textContent = 'Dark Mode: On'; } else { container.classList.remove('dark'); header.classList.remove('dark'); darkModeToggleBtn.textContent = 'Dark Mode: Off'; } } loadSettings(); // ======= CHAT HISTORY FUNCTIONS, TIMESTAMPS & TAP-TO-COPY ======== function appendMessage(role, text, save = true) { const messageDiv = document.createElement('div'); messageDiv.className = 'chatMessage ' + ((role === 'user') ? 'chatUser' : 'chatAI'); // Save original text (without name or timestamp) for tap-to-copy. messageDiv.dataset.original = text; // Create a timestamp element. const timestamp = document.createElement('div'); timestamp.className = 'chatTimestamp'; timestamp.textContent = new Date().toLocaleTimeString(); // Set message content based on role and customization. if(role === 'user'){ messageDiv.innerHTML = `${customName}: ${text}`; if(customUserColor) messageDiv.style.backgroundColor = customUserColor; } else { messageDiv.innerHTML = 'AI: ' + formatResponse(text); if(customAIColor) messageDiv.style.backgroundColor = customAIColor; } messageDiv.appendChild(timestamp); // Tap-to-copy functionality: copy only the original message (without name/timestamp) messageDiv.addEventListener('click', () => { navigator.clipboard.writeText(messageDiv.dataset.original) .then(() => alert('Message copied to clipboard!')) .catch(err => alert('Failed to copy message: ' + err)); }); chatLog.appendChild(messageDiv); chatLog.scrollTop = chatLog.scrollHeight; if (save) { const history = loadChatHistory(); history.push({ role, text }); saveChatHistory(history); } } // ======= EXPORT FUNCTION (Download Chat) ======== downloadBtn.addEventListener('click', () => { const choice = prompt("How would you like your chat downloaded?\nEnter 'json', 'md', or 'text'"); if (!choice) return; const history = loadChatHistory(); let content = ''; if(choice.toLowerCase() === 'json'){ content = JSON.stringify(history, null, 2); downloadFile(content, 'chat_history.json', 'application/json'); } else if(choice.toLowerCase() === 'md'){ history.forEach(msg => { if(msg.role === 'user'){ content += `**${customName}:** ${msg.text}\n\n`; } else { content += `**AI:** ${msg.text}\n\n`; } }); downloadFile(content, 'chat_history.md', 'text/markdown'); } else if(choice.toLowerCase() === 'text'){ history.forEach(msg => { content += (msg.role === 'user' ? `${customName}: ` : 'AI: ') + msg.text + '\n\n'; }); downloadFile(content, 'chat_history.txt', 'text/plain'); } else { alert("Invalid choice. Please enter 'json', 'md', or 'text'."); } }); function downloadFile(content, fileName, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); } // ======= VOICE INPUT (Web Speech API) ======== if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const recognition = new SpeechRecognition(); recognition.lang = 'en-US'; recognition.continuous = false; recognition.interimResults = false; voiceBtn.addEventListener('click', () => { voiceBtn.textContent = 'Listening...'; recognition.start(); }); recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; questionInput.value = transcript; voiceBtn.textContent = '🎤 Speak Your Question'; }; recognition.onerror = (event) => { console.error('Voice recognition error', event.error); voiceBtn.textContent = '🎤 Speak Your Question'; }; } else { voiceBtn.style.display = 'none'; } // ======= GENERATE ANSWER LOGIC, AUTO-DETECT API, & RESPONSE SPEED ======== generateButton.addEventListener('click', () => { const question = questionInput.value.trim(); if (!question) { alert('Please enter a question.'); return; } questionInput.value = ''; const formattedQuestion = `Please answer this question. Give me an explanation and breakdown of this question If it's a normal question like "hi" or "how to make a pizza" Answer normal without Breaking it down. if it sounds like a Homework question you can break it down And make the answer be at the end of your message Also don't say I understand please just answer question: ${question}`; appendMessage('user', question); loadingBar.style.display = 'block'; const messages = [{ role: 'user', content: formattedQuestion }]; const apiChoice = selectAPI.value; function processResponse(err, answer, duration) { loadingBar.style.display = 'none'; if (err) { if(apiChoice === 'openrouter' && geminiKeyInput.value.trim()) { appendMessage('chatAI', 'OpenRouter failed; switching to Google Gemini due to availability.'); generateGeminiText(geminiKeyInput.value.trim(), 'gemini-2.0-pro', messages, 1.0, (err2, answer2, duration2) => { if (err2) { appendMessage('chatAI', err2 + ' (Response time: ' + duration2 + 's)'); } else { appendMessage('chatAI', answer2 + ' (Response in ' + duration2 + 's)'); } }); } else if(apiChoice === 'gemini' && openRouterKeyInput.value.trim()) { appendMessage('chatAI', 'Google Gemini failed; switching to OpenRouter due to availability.'); generateOpenRouterText(openRouterKeyInput.value.trim(), openRouterModelSelect.value || 'gpt-3.5-turbo', messages, (err2, answer2, duration2) => { if (err2) { appendMessage('chatAI', err2 + ' (Response time: ' + duration2 + 's)'); } else { appendMessage('chatAI', answer2 + ' (Response in ' + duration2 + 's)'); } }); } else { appendMessage('chatAI', err + (duration ? ' (Response time: ' + duration + 's)' : '')); } } else { appendMessage('chatAI', answer + ' (Response in ' + duration + 's)'); } } if(apiChoice === 'openrouter') { const apiKey = openRouterKeyInput.value.trim(); const model = openRouterModelSelect.value || 'gpt-3.5-turbo'; generateOpenRouterText(apiKey, model, messages, processResponse); } else if(apiChoice === 'gemini') { const apiKey = geminiKeyInput.value.trim(); const model = 'gemini-2.0-pro'; generateGeminiText(apiKey, model, messages, 1.0, processResponse); } }); GM_registerMenuCommand("Toggle Homework AI Chat Helper", () => { container.style.display = container.style.display === 'none' ? 'block' : 'none'; }); })();