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