您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds an AI assistant sidepanel using OpenRouter to Instructure Canvas pages.
当前为
// ==UserScript== // @name Canvas AI Panel (OpenRouter) // @namespace http://tampermonkey.net/ // @version 1.7.0_OpenRouter_ModUI // @description Adds an AI assistant sidepanel using OpenRouter to Instructure Canvas pages. // @author Original by Riley Campbell, AI modifications, OpenRouter mod, UI Mod by patmarvs // @match *.instructure.com/* // @license https://opensource.org/license/bsd-3-clause/ // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // ==/UserScript== (async function() { 'use strict'; // Helper function to log messages for this script function scriptLog(message) { console.log(`[Canvas AI Sidepanel] ${message}`); } scriptLog('Script starting... (OpenRouter Edition, ModUI)'); if (window.location.href.includes("conversations")) { scriptLog('On a "conversations" page, exiting script as per original design.'); return; } // --- Storage Functions --- const getStoredValue = async (key, defaultValue) => { let value = await GM_getValue(key, defaultValue); if (value === 'true') return true; if (value === 'false') return false; return value === undefined ? defaultValue : value; }; const setStoredValue = async (key, value) => { await GM_setValue(key, value.toString()); }; // --- Initialize Core Variables --- let currentAIprompt = await getStoredValue('AIprompt', ''); if (currentAIprompt === null || currentAIprompt === undefined || typeof currentAIprompt !== 'string') { currentAIprompt = ''; } window.defaultMessage = { "role": "system", "content": currentAIprompt }; window.AImessages = (currentAIprompt && currentAIprompt.trim() !== "") ? [JSON.parse(JSON.stringify(window.defaultMessage))] : []; let chatVisible = await getStoredValue('chatVisible', false); // MODIFIED: Default to hidden if (typeof chatVisible !== 'boolean') { chatVisible = (chatVisible === 'false'); // Default to false if not a proper boolean string } window.hotkeySetting = await getStoredValue('chatToggleHotkey', 'Control+Shift+X'); // --- Core Functions --- window.renderMessages = function() { scriptLog('Rendering messages...'); let container = document.getElementById('ai-assistant-container'); if (!container) { scriptLog('Error: ai-assistant-container not found for rendering messages.'); return; } container.innerHTML = ''; const messagesToRender = window.AImessages.filter(message => { return !(message.role === "system" && (!message.content || message.content.trim() === "")); }); for (let i = 0; i < messagesToRender.length; i++) { let message = messagesToRender[i]; let originalIndex = window.AImessages.findIndex(m => m === message); const messageRow = document.createElement('table'); messageRow.style.width = '100%'; const roleText = message.role.charAt(0).toUpperCase() + message.role.slice(1); const escapedContent = message.content ? message.content.replace(/</g, "<").replace(/>/g, ">") : ""; let headerHTML = `<p class="ai-assistant-role">${roleText}: </p>`; if (message.role === "user" || message.role === "assistant" || (message.role === "system" && message.content && message.content.trim() !== "")) { if (originalIndex !== -1) { headerHTML += `<button class="ai-assistant-chat-edit-button" data-message-index="${originalIndex}">Edit</button>`; } } messageRow.innerHTML = ` <tr> <th style="text-align: left; vertical-align: top; width: auto;">${headerHTML}</th> <td data-message-id="${originalIndex}" style="white-space: pre-wrap; word-break: break-word;">${escapedContent.replace(/\n/g, '<br>')}</td> </tr> `; container.appendChild(messageRow); if (i < messagesToRender.length - 1) { container.appendChild(document.createElement('hr')); } } container.querySelectorAll('.ai-assistant-chat-edit-button').forEach(button => { button.addEventListener('click', function() { const messageIndex = parseInt(this.getAttribute('data-message-index')); if (isNaN(messageIndex) || messageIndex < 0 || messageIndex >= window.AImessages.length) { scriptLog("Error: Invalid message index for editing."); return; } const messageContentCell = container.querySelector(`td[data-message-id="${messageIndex}"]`); const currentText = window.AImessages[messageIndex].content; messageContentCell.innerHTML = ` <textarea class="ai-assistant-chat-edit-area">${currentText}</textarea> <button class="ai-assistant-chat-save-button" data-message-index="${messageIndex}">Save</button> <button class="ai-assistant-chat-cancel-button">Cancel</button>`; messageContentCell.querySelector('.ai-assistant-chat-save-button').addEventListener('click', async function() { const newText = messageContentCell.querySelector('.ai-assistant-chat-edit-area').value; window.AImessages[messageIndex].content = newText; if (window.AImessages[messageIndex].role === "system") { window.defaultMessage.content = newText; await setStoredValue('AIprompt', newText); document.getElementById('ai-assistant-systemPrompt').value = newText; } window.renderMessages(); }); messageContentCell.querySelector('.ai-assistant-chat-cancel-button').addEventListener('click', function() { window.renderMessages(); }); const textarea = messageContentCell.querySelector('.ai-assistant-chat-edit-area'); textarea.focus(); textarea.selectionStart = textarea.selectionEnd = textarea.value.length; }); }); if (container) { container.scrollTop = container.scrollHeight; } }; window.sendMessage = async function() { scriptLog('Attempting to send message to OpenRouter...'); const chatContainer = document.getElementById('ai-assistant-container'); const myTextArea = document.getElementById('ai-assistant-myTextArea'); const apiKey = await getStoredValue('openaiAPIKey', ''); // Stored under old name, but it's OpenRouter key const aiModel = await getStoredValue('AImodel', 'openai/gpt-3.5-turbo'); if (!apiKey) { alert('OpenRouter API Key is not set. Please set it in the Settings panel (field is labeled OpenAI API Key).'); scriptLog('API Key missing.'); return; } if (!myTextArea || !myTextArea.value.trim()) { scriptLog('Message input is empty.'); return; } const loadingGifId = 'ai-assistant-loading-gif'; const existingLoadingGif = document.getElementById(loadingGifId); if (chatContainer && !existingLoadingGif) { chatContainer.insertAdjacentHTML('beforeend', `<div id="${loadingGifId}" style="text-align:center;"><img style="display: inline-block; width: 25px; margin: 8px auto;" src="https://i.gifer.com/origin/34/34338d26023e5515f6cc8969aa027bca_w200.gif" alt="Loading..."></div>`); chatContainer.scrollTop = chatContainer.scrollHeight; } let userMessage = { "role": "user", "content": myTextArea.value }; window.AImessages.push(userMessage); const userMessageContentForRestore = myTextArea.value; myTextArea.value = ''; window.renderMessages(); let messagesForAPI = []; const memoryEnabled = await getStoredValue('AImemory', false); window.defaultMessage.content = document.getElementById('ai-assistant-systemPrompt').value; if (memoryEnabled) { messagesForAPI = JSON.parse(JSON.stringify(window.AImessages)); if (window.defaultMessage.content && window.defaultMessage.content.trim() !== "") { const systemMsgIndex = messagesForAPI.findIndex(m => m.role === 'system'); if (systemMsgIndex > -1) { messagesForAPI[systemMsgIndex].content = window.defaultMessage.content; } else { messagesForAPI.unshift(JSON.parse(JSON.stringify(window.defaultMessage))); } } else { messagesForAPI = messagesForAPI.filter(m => m.role !== 'system'); } } else { if (window.defaultMessage.content && window.defaultMessage.content.trim() !== "") { messagesForAPI.push(JSON.parse(JSON.stringify(window.defaultMessage))); } messagesForAPI.push(userMessage); } messagesForAPI = messagesForAPI.filter(m => m.content && m.content.trim() !== ""); if (messagesForAPI.length === 0) { scriptLog('No messages to send to API after filtering.'); document.getElementById(loadingGifId)?.remove(); window.AImessages.push({role: "assistant", content: "Internal error: No content to send."}); window.renderMessages(); return; } scriptLog(`Sending to OpenRouter API with model ${aiModel}. Memory: ${memoryEnabled}. Messages count: ${messagesForAPI.length}.`); GM_xmlhttpRequest({ method: "POST", url: 'https://openrouter.ai/api/v1/chat/completions', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}`, }, data: JSON.stringify({ "model": aiModel, "messages": messagesForAPI, "temperature": 1.0 }), onload: function(response) { document.getElementById(loadingGifId)?.remove(); try { if (response.status >= 200 && response.status < 300) { let result = JSON.parse(response.responseText); if (result.choices && result.choices.length > 0 && result.choices[0].message) { window.AImessages.push(result.choices[0].message); } else { if (result.error && result.error.message) { throw new Error(`API Error: ${result.error.message} (Type: ${result.error.type}, Code: ${result.error.code || 'N/A'})`); } throw new Error("Invalid response structure from API."); } } else { let errorInfo = `API Error ${response.status}: ${response.statusText}`; try { const errData = JSON.parse(response.responseText); if (errData.error && errData.error.message) { errorInfo += ` - ${errData.error.message}`; if(errData.error.type) errorInfo += ` (Type: ${errData.error.type})`; if(errData.error.code) errorInfo += ` (Code: ${errData.error.code})`; } } catch (e) { /* Stick with statusText */ } throw new Error(errorInfo); } } catch (e) { scriptLog(`Error processing response: ${e.message}`); const errorP = document.createElement('p'); errorP.style.color = 'red'; // Error color still red for visibility errorP.style.padding = '5px'; errorP.innerHTML = `<strong>Error:</strong> ${e.message.replace(/</g, "<").replace(/>/g, ">")} <button class="ai-assistant-retry-button">Retry</button>`; if(chatContainer) { chatContainer.appendChild(errorP); chatContainer.scrollTop = chatContainer.scrollHeight; } errorP.querySelector('.ai-assistant-retry-button')?.addEventListener('click', () => { myTextArea.value = userMessageContentForRestore; errorP.remove(); scriptLog("Retry button clicked."); myTextArea.focus(); }); } finally { if (! (document.querySelector('#ai-assistant-container p[style*="color: red;"]')) ) { window.renderMessages(); } if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight; } }, onerror: function(response) { document.getElementById(loadingGifId)?.remove(); const errorText = `Network Error: ${response.statusText || 'Could not connect'}.`; scriptLog(`Request Error: ${errorText}`); const errorP = document.createElement('p'); errorP.style.color = 'red'; errorP.style.padding = '5px'; errorP.innerHTML = `<strong>${errorText}</strong>`; if(chatContainer) { chatContainer.appendChild(errorP); chatContainer.scrollTop = chatContainer.scrollHeight; } } }); }; window.populateModels = async function() { scriptLog('Populating models from OpenRouter...'); const modelsSelect = document.getElementById('ai-assistant-models'); const apiKey = await getStoredValue('openaiAPIKey', ''); // Stored under old name if (!apiKey) { scriptLog('API Key missing for populating models.'); if (modelsSelect) modelsSelect.innerHTML = '<option value="">Set API Key (labeled OpenAI API Key) to load models</option>'; return; } if (!modelsSelect) { scriptLog('Models select element not found.'); return; } modelsSelect.innerHTML = '<option value="">Loading models from OpenRouter...</option>'; const commonModels = [ 'openai/gpt-4o', 'openai/gpt-4-turbo', 'anthropic/claude-3-opus', 'anthropic/claude-3-sonnet', 'anthropic/claude-3-haiku', 'google/gemini-pro-1.5', 'google/gemini-flash-1.5', 'mistralai/mistral-large', ]; GM_xmlhttpRequest({ method: "GET", url: 'https://openrouter.ai/api/v1/models', headers: { "Authorization": `Bearer ${apiKey}` }, onload: async function(response) { try { modelsSelect.innerHTML = ''; if (response.status >= 200 && response.status < 300) { const options = JSON.parse(response.responseText); let availableModels = []; if (options.data) { options.data.forEach(item => { availableModels.push(item.id); }); } availableModels.sort((a, b) => a.localeCompare(b)); let effectiveCommonModels = commonModels.filter(m => availableModels.includes(m)); let finalModelList = [...new Set([...effectiveCommonModels, ...availableModels])]; if (finalModelList.length === 0) { modelsSelect.innerHTML = '<option value="">No models found on OpenRouter.</option>'; } else { finalModelList.forEach(modelId => { let element = document.createElement("option"); element.value = modelId; element.innerHTML = modelId; modelsSelect.appendChild(element); }); const storedModel = await getStoredValue('AImodel', 'openai/gpt-3.5-turbo'); if (finalModelList.includes(storedModel)) { modelsSelect.value = storedModel; } else if (finalModelList.length > 0) { modelsSelect.value = finalModelList[0]; await setStoredValue('AImodel', finalModelList[0]); } } } else { let errorDetail = `Failed: ${response.status} ${response.statusText}`; try { const errData = JSON.parse(response.responseText); if (errData.error && errData.error.message) { errorDetail = `API Error: ${errData.error.message.substring(0,50)}...`; } } catch (e) { /* ignore */ } modelsSelect.innerHTML = `<option value="">${errorDetail}</option>`; scriptLog(`Failed to fetch models: ${response.status} ${response.responseText}`); } } catch (e) { scriptLog(`Error parsing models response: ${e.message}`); modelsSelect.innerHTML = '<option value="">Error parsing models data</option>'; } }, onerror: function() { scriptLog('Network error fetching models.'); if (modelsSelect) modelsSelect.innerHTML = '<option value="">Network error</option>'; } }); }; function setupUI() { scriptLog('Setting up UI...'); const accentGrey = '#6A737C'; // Main grey accent const accentGreyHover = '#525960'; // Darker grey for hover const lightGreyBg = '#f0f0f0'; // Light grey for some backgrounds const lighterGreyBg = '#f8f9fa'; // Even lighter for collapsible content const focusRingColor = 'rgba(106, 115, 124, .5)'; // Grey focus ring const css = ` #ai-assistant-box { position: fixed; bottom: 15px; right: 15px; width: 450px; height: 30vh; /* MODIFIED: Fixed height */ background-color: #fff; box-shadow: 0 5px 20px rgba(0,0,0,0.25); border-radius: 12px; display: flex; flex-direction: column; z-index: 10001; border: 1px solid #ccc; font-family: Arial, sans-serif; font-size: 14px; transform: translateY(0); opacity: 1; transition: transform 0.3s ease-out, opacity 0.3s ease-out; } #ai-assistant-box.hidden { transform: translateY(calc(100% + 30px)); opacity: 0; pointer-events: none; } .ai-assistant-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 15px; background: ${accentGrey}; /* MODIFIED */ color: white; border-radius: 11px 11px 0 0; cursor: default; } .ai-assistant-header-title { font-weight: bold; font-size: 1.1em; } /* Title: "AI Assistant" */ .ai-assistant-toggle-visibility-header { background: none; border: none; color: white; font-size: 1.5em; cursor: pointer; padding: 0 5px;} .ai-assistant-toggle-visibility-header:hover { opacity:0.8; } #ai-assistant-box .ai-assistant-main-content { padding:15px; display: flex; flex-direction: column; flex-grow:1; overflow:hidden; background: #fdfdfd; } #ai-assistant-container { flex-grow: 1; overflow-y: auto; padding: 10px; border-bottom: 1px solid #eee; margin-bottom: 10px; min-height: 100px; /* min-height can be less critical with fixed overall box */ background: #fff; border: 1px solid #e0e0e0; border-radius: 4px; scroll-behavior: smooth;} #ai-assistant-container table { width: 100%; margin-bottom: 8px; border-collapse: collapse; } #ai-assistant-container th { font-weight: bold; text-align: left; vertical-align:top; padding: 4px 8px 4px 2px; color: #333; } #ai-assistant-container td { padding: 4px 2px 4px 8px; color: #555; } #ai-assistant-container hr { border: 0; border-top: 1px solid #f0f0f0; margin: 8px 0; } #ai-assistant-myTextArea { display: block; width: calc(100% - 22px); min-height:60px; max-height: 150px; resize: vertical; margin: 0 auto 10px auto; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size:1em; } #ai-assistant-myTextArea:focus { border-color: ${accentGrey}; box-shadow: 0 0 0 0.2rem ${focusRingColor}; } /* MODIFIED */ #ai-assistant-box .ai-assistant-button { background-color: ${accentGrey}; /* MODIFIED */ color: white; border: none; padding: 8px 15px; margin: 0 5px 5px 0; border-radius: 5px; cursor: pointer; text-align: center; font-size:0.9em; transition: background-color 0.2s ease; } #ai-assistant-box .ai-assistant-button:hover { background-color: ${accentGreyHover}; } /* MODIFIED */ #ai-assistant-button-bar { display: flex; justify-content: space-between; gap: 10px; } #ai-assistant-sendButton { flex-grow:1; background-color: ${accentGrey}; } /* MODIFIED */ #ai-assistant-sendButton:hover { background-color: ${accentGreyHover}; } /* MODIFIED */ #ai-assistant-clearHistoryButton { background-color: ${accentGrey}; } /* MODIFIED specific clear button if needed, else inherits */ #ai-assistant-clearHistoryButton:hover { background-color: ${accentGreyHover}; } /* MODIFIED */ .ai-assistant-settings-toggle { display: block; font-weight: bold; font-size: 1em; text-align: center; padding: 10px; color: ${accentGreyHover}; /* MODIFIED */ background: ${lightGreyBg}; /* MODIFIED */ cursor: pointer; border-top: 1px solid #ddd; transition: background-color 0.2s ease-out; margin:0; border-radius: 0 0 11px 11px; } .ai-assistant-settings-toggle:hover { background-color: #e2e6ea; } /* MODIFIED */ .ai-assistant-collapsible-content { max-height: 0px; overflow-y: auto; transition: max-height .35s ease-in-out; background: ${lighterGreyBg}; /* MODIFIED */ border-top:1px solid #ddd;} .ai-assistant-collapsible-content .content-inner { padding: 15px; border-bottom-left-radius: 11px; border-bottom-right-radius: 11px; } #ai-assistant-settings-checkbox:checked ~ .ai-assistant-collapsible-content { max-height: 450px; } #ai-assistant-settings-checkbox { display: none; } .ai-assistant-switch-container { display: flex; align-items: center; margin-bottom:12px; } .ai-assistant-switch { position: relative; display: inline-block; width: 44px; height: 24px; margin-right: 10px; } .ai-assistant-switch input { opacity: 0; width: 0; height: 0; } .ai-assistant-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; } .ai-assistant-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .ai-assistant-slider { background-color: ${accentGrey}; } /* MODIFIED */ input:checked + .ai-assistant-slider:before { transform: translateX(20px); } #ai-assistant-systemPrompt { display: block; width: calc(100% - 22px); min-height: 70px; max-height: 150px; resize: vertical; margin: 5px auto 10px auto; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size:0.9em; } #ai-assistant-apiKey { display: inline-block; width: calc(60% - 10px); margin-right:5px; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size:0.9em; box-sizing: border-box; } #ai-assistant-setAPIKeyButton { display: inline-block; width: calc(40% - 10px); padding: 8px 0px; box-sizing: border-box;} .ai-assistant-chat-edit-button, .ai-assistant-chat-save-button, .ai-assistant-chat-cancel-button { background-color: ${accentGrey}; /* MODIFIED */ font-size: 0.8em; padding: 3px 6px; margin-left: 8px; border-radius:3px; color:white; border:none; cursor:pointer; } .ai-assistant-chat-edit-button:hover, .ai-assistant-chat-save-button:hover, .ai-assistant-chat-cancel-button:hover { background-color: ${accentGreyHover}; } /* MODIFIED */ .ai-assistant-chat-edit-area { display:block; width: 98%; min-height: 60px; max-height: 120px; resize: vertical; margin: 5px 0; font-size:1em; padding:5px; border-radius:4px; border:1px solid #ccc; } .ai-assistant-role { display: inline; margin-right: 5px; font-weight:bold;} #ai-assistant-models { margin-bottom:10px; display:block; width:100%; padding:8px; border-radius:4px; border:1px solid #ccc; font-size:0.9em; box-sizing: border-box;} .ai-assistant-label { display: block; margin-bottom: 5px; font-weight: bold; font-size:0.9em; } .ai-assistant-retry-button { background-color: ${accentGrey}; color: white; border: none; padding: 3px 8px; border-radius: 3px; cursor: pointer; font-size: 0.9em; margin-left: 10px;} .ai-assistant-retry-button:hover { background-color: ${accentGreyHover};} #ai-assistant-hotkey-input-container { margin-top: 10px; margin-bottom: 5px; } #ai-assistant-hotkey-input { width: calc(60% - 10px); margin-right:5px; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size:0.9em; box-sizing: border-box;} #ai-assistant-setHotkeyButton {display: inline-block; width: calc(40% - 10px); padding: 8px 0px; box-sizing: border-box;} #ai-assistant-toggle-button { position: fixed; bottom: 20px; right: 20px; z-index: 10000; background-color: ${accentGrey}; /* MODIFIED */ color: white; border:none; border-radius: 50%; width: 50px; height: 50px; font-size: 24px; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; transition: background-color 0.2s ease, transform 0.2s ease; } #ai-assistant-toggle-button:hover { background-color: ${accentGreyHover}; transform: scale(1.1); } /* MODIFIED */ #ai-assistant-toggle-button.hidden { display: none !important; } /* MODIFIED: Ensure button also hides */ `; GM_addStyle(css); const chatToggleBtn = document.createElement('button'); chatToggleBtn.id = 'ai-assistant-toggle-button'; chatToggleBtn.innerHTML = '💬'; chatToggleBtn.title = `Toggle AI Assistant (Hotkey: ${window.hotkeySetting})`; if (!chatVisible) { // MODIFIED: Initially hide toggle button if chat is not visible chatToggleBtn.classList.add('hidden'); } document.body.appendChild(chatToggleBtn); const elem = document.createElement('div'); elem.id = 'ai-assistant-box'; if (!chatVisible) { elem.classList.add('hidden'); chatToggleBtn.innerHTML = '💬'; } else { elem.classList.remove('hidden'); chatToggleBtn.innerHTML = '✕'; } elem.innerHTML = ` <div class="ai-assistant-header"> <span class="ai-assistant-header-title">AI Assistant</span> <button class="ai-assistant-toggle-visibility-header" title="Hide AI Assistant (Hotkey: ${window.hotkeySetting})">✕</button> </div> <div class="ai-assistant-main-content"> <div id="ai-assistant-container"></div> <textarea id="ai-assistant-myTextArea" placeholder="Ask AI Assistant... (Shift+Enter for new line, Enter to send)"></textarea> <div id="ai-assistant-button-bar"> <button class="ai-assistant-button" id="ai-assistant-clearHistoryButton">Clear & Apply Prompt</button> <button class="ai-assistant-button" id="ai-assistant-sendButton">Send</button> </div> </div> <input id="ai-assistant-settings-checkbox" type="checkbox"> <label for="ai-assistant-settings-checkbox" class="ai-assistant-settings-toggle">Settings <span class="settings-arrow">▼</span></label> <div class="ai-assistant-collapsible-content"> <div class="content-inner"> <div class="ai-assistant-switch-container"> <label class="ai-assistant-switch"> <input id="ai-assistant-memory" type="checkbox"> <span class="ai-assistant-slider"></span> </label> <label for="ai-assistant-memory" style="font-size:0.9em;">Enable chat memory</label> </div> <label for="ai-assistant-models" class="ai-assistant-label">Model:</label> <select name="models" id="ai-assistant-models"></select> <label for="ai-assistant-systemPrompt" class="ai-assistant-label">System Prompt (Instructions for AI):</label> <textarea id="ai-assistant-systemPrompt" placeholder="e.g., Act as a helpful teaching assistant."></textarea> <label for="ai-assistant-apiKey" class="ai-assistant-label">OpenAI API Key:</label> <div> <input id="ai-assistant-apiKey" placeholder="sk-xxxxxxxxxx" type="password"> <button class="ai-assistant-button" id="ai-assistant-setAPIKeyButton">Set Key</button> </div> <div id="ai-assistant-hotkey-input-container"> <label for="ai-assistant-hotkey-input" class="ai-assistant-label">Toggle Hotkey (e.g. Control+Shift+X):</label> <div> <input id="ai-assistant-hotkey-input" type="text" placeholder="Example: Control+Shift+M"> <button class="ai-assistant-button" id="ai-assistant-setHotkeyButton">Set Hotkey</button> </div> </div> </div> </div> `; document.body.appendChild(elem); scriptLog('UI elements injected.'); const settingsCheckbox = document.getElementById('ai-assistant-settings-checkbox'); const settingsArrow = elem.querySelector('.settings-arrow'); settingsCheckbox.addEventListener('change', () => { if (settingsArrow) settingsArrow.innerHTML = settingsCheckbox.checked ? '▲' : '▼'; }); function toggleChatVisibility(show) { const chatBox = document.getElementById('ai-assistant-box'); const floatingToggleButton = document.getElementById('ai-assistant-toggle-button'); // const headerToggleButton = chatBox.querySelector('.ai-assistant-toggle-visibility-header'); // Already part of chatBox if (typeof show === 'boolean') { chatVisible = show; } else { chatVisible = !chatVisible; } setStoredValue('chatVisible', chatVisible); if (chatVisible) { chatBox.classList.remove('hidden'); if (floatingToggleButton) floatingToggleButton.classList.remove('hidden'); if (floatingToggleButton) floatingToggleButton.innerHTML = '✕'; // if (headerToggleButton) headerToggleButton.innerHTML = '✕'; // Not strictly needed as it's part of hidden box document.getElementById('ai-assistant-myTextArea')?.focus(); } else { chatBox.classList.add('hidden'); if (floatingToggleButton) floatingToggleButton.classList.remove('hidden'); // Keep toggle button visible even if chat is hidden by it if (floatingToggleButton) floatingToggleButton.innerHTML = '💬'; // if (headerToggleButton) headerToggleButton.innerHTML = '💬'; } } window.toggleChatVisibility = toggleChatVisibility; chatToggleBtn.addEventListener('click', () => toggleChatVisibility()); elem.querySelector('.ai-assistant-toggle-visibility-header').addEventListener('click', () => toggleChatVisibility(false)); (async () => { document.getElementById('ai-assistant-systemPrompt').value = await getStoredValue('AIprompt', ''); document.getElementById('ai-assistant-memory').checked = await getStoredValue('AImemory', false); const currentHotkeyDisplay = await getStoredValue('chatToggleHotkey', 'Control+Shift+X'); document.getElementById('ai-assistant-hotkey-input').value = currentHotkeyDisplay; const updateButtonTitles = (hotkey) => { const ftb = document.getElementById('ai-assistant-toggle-button'); if (ftb) { ftb.title = `Toggle AI Assistant (Hotkey: ${hotkey})`;} const headerToggle = elem.querySelector('.ai-assistant-toggle-visibility-header'); if (headerToggle) { headerToggle.title = `Hide AI Assistant (Hotkey: ${hotkey})`;} }; updateButtonTitles(currentHotkeyDisplay); await window.populateModels(); const storedModel = await getStoredValue('AImodel', 'openai/gpt-3.5-turbo'); const modelSelect = document.getElementById('ai-assistant-models'); if (modelSelect && modelSelect.options.length > 0) { if (Array.from(modelSelect.options).some(opt => opt.value === storedModel)) { modelSelect.value = storedModel; } else if (modelSelect.options[0] && modelSelect.options[0].value) { modelSelect.value = modelSelect.options[0].value; await setStoredValue('AImodel', modelSelect.options[0].value); } } document.getElementById('ai-assistant-myTextArea').addEventListener("keydown", async function(event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); await window.sendMessage(); } }); document.getElementById('ai-assistant-sendButton').addEventListener('click', window.sendMessage); document.getElementById('ai-assistant-clearHistoryButton').onclick = async () => { scriptLog('Clear history clicked.'); const newSystemPromptText = document.getElementById('ai-assistant-systemPrompt').value; window.defaultMessage.content = newSystemPromptText; await setStoredValue('AIprompt', newSystemPromptText); if (newSystemPromptText && newSystemPromptText.trim() !== "") { window.AImessages = [JSON.parse(JSON.stringify(window.defaultMessage))]; } else { window.AImessages = []; } window.renderMessages(); scriptLog('History cleared.'); }; document.getElementById('ai-assistant-setAPIKeyButton').onclick = async () => { const apiKeyInput = document.getElementById('ai-assistant-apiKey'); if (apiKeyInput && apiKeyInput.value.trim()) { await setStoredValue('openaiAPIKey', apiKeyInput.value.trim()); apiKeyInput.value = ''; scriptLog('API Key set (for OpenRouter).'); alert('API Key saved! Model list will refresh.'); await window.populateModels(); } else { alert('Please enter a valid API key.'); } }; document.getElementById('ai-assistant-setHotkeyButton').onclick = async () => { const hotkeyInput = document.getElementById('ai-assistant-hotkey-input'); const newHotkey = hotkeyInput.value.trim(); if (newHotkey) { if (newHotkey.split('+').length > 0) { await setStoredValue('chatToggleHotkey', newHotkey); window.hotkeySetting = newHotkey; updateButtonTitles(newHotkey); alert(`Hotkey set to: ${newHotkey}.`); scriptLog(`Hotkey updated: ${newHotkey}`); } else { alert('Invalid hotkey format.'); } } else { alert('Please enter a hotkey.'); } }; let settingsSaveTimeout; const scheduleSaveSettings = async (eventSourceId = null) => { clearTimeout(settingsSaveTimeout); settingsSaveTimeout = setTimeout(async () => { await setStoredValue('AImemory', document.getElementById('ai-assistant-memory').checked); const modelSel = document.getElementById('ai-assistant-models'); if (modelSel && modelSel.value) await setStoredValue('AImodel', modelSel.value); const systemPromptText = document.getElementById('ai-assistant-systemPrompt').value; if (eventSourceId === 'ai-assistant-systemPrompt' || eventSourceId === null) { await setStoredValue('AIprompt', systemPromptText); window.defaultMessage.content = systemPromptText; const systemMsgIndex = window.AImessages.findIndex(m => m.role === 'system'); if (systemPromptText && systemPromptText.trim() !== "") { if (systemMsgIndex > -1) window.AImessages[systemMsgIndex].content = systemPromptText; else window.AImessages.unshift(JSON.parse(JSON.stringify(window.defaultMessage))); } else { if (systemMsgIndex > -1) window.AImessages.splice(systemMsgIndex, 1); } if (eventSourceId === 'ai-assistant-systemPrompt') window.renderMessages(); } scriptLog('Settings auto-saved.'); }, 1000); }; document.getElementById('ai-assistant-memory').addEventListener('change', () => scheduleSaveSettings('ai-assistant-memory')); document.getElementById('ai-assistant-models').addEventListener('change', () => scheduleSaveSettings('ai-assistant-models')); document.getElementById('ai-assistant-systemPrompt').addEventListener('input', () => scheduleSaveSettings('ai-assistant-systemPrompt')); document.addEventListener('keydown', (e) => { if (!window.hotkeySetting || typeof window.hotkeySetting !== 'string') return; const keys = window.hotkeySetting.toUpperCase().split('+'); const mainKey = keys.pop(); let ctrl = keys.includes('CONTROL') || keys.includes('CTRL'); let shift = keys.includes('SHIFT'); let alt = keys.includes('ALT'); let meta = keys.includes('META') || keys.includes('COMMAND'); if ((ctrl === e.ctrlKey) && (shift === e.shiftKey) && (alt === e.altKey) && (meta === e.metaKey) && (e.key.toUpperCase() === mainKey)) { const activeEl = document.activeElement; if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable) && activeEl.id !== 'ai-assistant-myTextArea') { if (['ai-assistant-systemPrompt', 'ai-assistant-apiKey', 'ai-assistant-hotkey-input'].includes(activeEl.id)) return; } e.preventDefault(); e.stopPropagation(); toggleChatVisibility(); // This will now also handle showing the floating button if it was hidden scriptLog(`Hotkey "${window.hotkeySetting}" pressed.`); } }); // MODIFIED: Initial render logic based on chatVisible if (chatVisible) { // If stored state is visible (e.g. after first hotkey use and subsequent loads) window.renderMessages(); } // Otherwise, it remains hidden, and renderMessages will be called when toggleChatVisibility makes it visible. scriptLog('Initial render logic applied. Event listeners attached.'); })(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupUI); } else { setupUI(); } scriptLog('Script initialization phase complete. (OpenRouter Edition, ModUI)'); })();