Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
343828d018 | ||
|
df231c0a90 | ||
|
fb8a786f8c | ||
|
632cd3a0c8 | ||
|
d17ee3f4bd | ||
|
501fdaa4a1 |
@ -11,12 +11,16 @@ A simple, versatile language processing interface for websites.
|
||||
|
||||
## Usage
|
||||
After installing the extensions, selecting text and pressing `Ctrl+Space` will display the answer above the selected text in the configured opacity. To hide the answer, press `Ctrl+Space` again without selecting any text.
|
||||
|
||||
Additionally, pressing `Ctrl+Shift+Space` will write the answer into the nearest text field.
|
||||
|
||||
## Configuration
|
||||
The extension can be configured by clicking on the extension icon in the toolbar.
|
||||
|
||||
You will need to add your own OpenAI Endpoint (for example `https://api.elia.network`), and an API key.
|
||||
|
||||
There is an default URL and Key which is restricted to llama3.2-90b and 10 Requests per minute.
|
||||
|
||||
The opacity can be adjusted from 0 to 1, and is set to 0.1 per default. Adjust based on your screen's brightness and if you want to risk it being spotted.
|
||||
|
||||
## Installation
|
||||
|
@ -1,5 +1,9 @@
|
||||
browser.commands.onCommand.addListener((command) => {
|
||||
if (command === "print-selection" || command === "fill-input") {
|
||||
if (
|
||||
command === "print-selection" ||
|
||||
command === "fill-input" ||
|
||||
command === "toggle-stealth"
|
||||
) {
|
||||
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
|
||||
browser.tabs.sendMessage(tabs[0].id, { action: command });
|
||||
});
|
||||
|
BIN
src/build.xpi
Normal file
BIN
src/build.xpi
Normal file
Binary file not shown.
178
src/content.js
178
src/content.js
@ -1,20 +1,25 @@
|
||||
// Constants
|
||||
// contents
|
||||
const DEFAULT_SETTINGS = {
|
||||
apiUrl: "https://api.elia.network",
|
||||
apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA",
|
||||
textOpacity: 10,
|
||||
model: "llama3.2-90b",
|
||||
background: false,
|
||||
examModeStates: {},
|
||||
};
|
||||
|
||||
const MAX_CONTEXT_LENGTH = 6000;
|
||||
|
||||
// States (for later in the code)
|
||||
// States
|
||||
let overlay = null;
|
||||
let isVisible = false;
|
||||
let lastKnownSelection = null;
|
||||
let lastKnownRange = null;
|
||||
let scrollTimeout = null;
|
||||
let resizeRAF = null;
|
||||
let stealthMode = false;
|
||||
let stealthAnswer = null;
|
||||
let stealthFields = new Set();
|
||||
|
||||
// helper Functions
|
||||
const getPageContext = () => {
|
||||
@ -49,7 +54,7 @@ const queryLLM = async (text) => {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `You are tasked to solve an exam. Exam's page context: ${pageContext}\n\Answer this question of said exam in very short but accurate manner (Utilize context to figure out the exam's topic. ONLY REPLY WITH ANSWER TO THE QUESTION): ${text}`,
|
||||
content: `You are tasked to solve an exam. It is a website. Exam's page context: ${pageContext}\n\Answer this question of said exam in a very very short but accurate manner (Utilize context to figure out the exam's topic. ONLY REPLY WITH ANSWER TO THE QUESTION. If you do not know, reply with "."): ${text}`,
|
||||
},
|
||||
],
|
||||
temperature: 0.1,
|
||||
@ -66,6 +71,106 @@ const queryLLM = async (text) => {
|
||||
return data.choices[0].message.content;
|
||||
};
|
||||
|
||||
// Stealth Mode Functions
|
||||
const handleStealthTyping = (event) => {
|
||||
if (!stealthMode || !stealthAnswer || !stealthFields.has(event.target))
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
const input = event.target;
|
||||
const currentLength = input.value.length;
|
||||
|
||||
if (currentLength < stealthAnswer.length) {
|
||||
input.value = stealthAnswer.substring(0, currentLength + 1);
|
||||
} else {
|
||||
deactivateStealthMode();
|
||||
}
|
||||
};
|
||||
|
||||
const findQuestionTextAbove = (inputElement) => {
|
||||
const inputRect = inputElement.getBoundingClientRect();
|
||||
const spans = Array.from(
|
||||
document.querySelectorAll("span.text-format-content"),
|
||||
);
|
||||
|
||||
return (
|
||||
spans
|
||||
.filter((span) => {
|
||||
const spanRect = span.getBoundingClientRect();
|
||||
return (
|
||||
spanRect.bottom <= inputRect.top &&
|
||||
Math.abs(spanRect.left - inputRect.left) < 200
|
||||
); // Allow some horizontal tolerance
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aRect = a.getBoundingClientRect();
|
||||
const bRect = b.getBoundingClientRect();
|
||||
return bRect.bottom - aRect.bottom; // Get the closest span above
|
||||
})[0]
|
||||
?.textContent.trim() || null
|
||||
);
|
||||
};
|
||||
|
||||
const activateStealthMode = async () => {
|
||||
const activeElement = document.activeElement;
|
||||
const isInputField = activeElement.matches(
|
||||
'input[type="text"], input:not([type]), textarea',
|
||||
);
|
||||
|
||||
if (isInputField) {
|
||||
// Input field is focused, find question text above
|
||||
const questionText = findQuestionTextAbove(activeElement);
|
||||
if (questionText) {
|
||||
try {
|
||||
stealthAnswer = await queryLLM(questionText);
|
||||
stealthMode = true;
|
||||
stealthFields.add(activeElement);
|
||||
activeElement.addEventListener("keypress", handleStealthTyping);
|
||||
} catch (error) {
|
||||
console.error("Error activating stealth mode:", error);
|
||||
stealthMode = false;
|
||||
stealthAnswer = null;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Original selection-based functionality
|
||||
const selection = window.getSelection();
|
||||
const selectedText = selection.toString().trim();
|
||||
|
||||
if (!selectedText) return;
|
||||
|
||||
try {
|
||||
stealthAnswer = await queryLLM(selectedText);
|
||||
stealthMode = true;
|
||||
|
||||
const inputs = document.querySelectorAll(
|
||||
'input[type="text"], input:not([type]), textarea',
|
||||
);
|
||||
|
||||
inputs.forEach((input) => {
|
||||
stealthFields.add(input);
|
||||
input.addEventListener("keypress", handleStealthTyping);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error activating stealth mode:", error);
|
||||
stealthMode = false;
|
||||
stealthAnswer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const deactivateStealthMode = () => {
|
||||
stealthMode = false;
|
||||
stealthAnswer = null;
|
||||
|
||||
stealthFields.forEach((input) => {
|
||||
input.removeEventListener("keypress", handleStealthTyping);
|
||||
});
|
||||
|
||||
stealthFields.clear();
|
||||
};
|
||||
|
||||
// Overlay/input fields
|
||||
const createOverlay = (text) => {
|
||||
if (overlay) overlay.remove();
|
||||
@ -80,10 +185,19 @@ const createOverlay = (text) => {
|
||||
padding: 5px;
|
||||
z-index: 2147483647;
|
||||
color: rgba(0, 0, 0, ${settings.textOpacity / 100});
|
||||
font-size: 16px;
|
||||
font-size: 12px;
|
||||
max-width: 600px;
|
||||
white-space: pre-wrap;
|
||||
pointer-events: none;
|
||||
${
|
||||
settings.background
|
||||
? `
|
||||
background-color: rgba(255, 255, 255, ${settings.textOpacity / 100});
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
@ -103,29 +217,22 @@ const positionOverlay = () => {
|
||||
const range = selection.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
// Get scroll positions
|
||||
// this is deprecated but currently the only way thank you mr mozilla
|
||||
const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
||||
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
// Get viewport dimensions
|
||||
const viewportWidth =
|
||||
window.innerWidth || document.documentElement.clientWidth;
|
||||
const viewportHeight =
|
||||
window.innerHeight || document.documentElement.clientHeight;
|
||||
|
||||
// Calculate overlay dimensions
|
||||
const overlayWidth = overlay.offsetWidth;
|
||||
const overlayHeight = overlay.offsetHeight;
|
||||
|
||||
// Calculate positions
|
||||
let top = rect.top + scrollY;
|
||||
let left = rect.left + scrollX + rect.width / 2 - overlayWidth / 2;
|
||||
|
||||
// Position above selection by default
|
||||
top -= overlayHeight + 5;
|
||||
|
||||
// If overlay would go above viewport, position it below selection
|
||||
if (top - scrollY < 0) {
|
||||
top = rect.bottom + scrollY + 5;
|
||||
}
|
||||
@ -135,7 +242,6 @@ const positionOverlay = () => {
|
||||
Math.min(left, scrollX + viewportWidth - overlayWidth - 10),
|
||||
);
|
||||
|
||||
// apply
|
||||
overlay.style.top = `${top}px`;
|
||||
overlay.style.left = `${left}px`;
|
||||
} catch (error) {
|
||||
@ -166,18 +272,15 @@ const handleResize = () => {
|
||||
|
||||
window.addEventListener("resize", handleResize, { passive: true });
|
||||
|
||||
// Shitty fix for microsoft forms (they use containers)
|
||||
// Scroll handlers
|
||||
const attachScrollListeners = () => {
|
||||
// Listen to window scroll
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
|
||||
// Listen to scroll events on all scrollable elements
|
||||
document.addEventListener("scroll", handleScroll, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
});
|
||||
|
||||
// Find and attach listeners to all scrollable containers
|
||||
const scrollableElements = document.querySelectorAll(
|
||||
[
|
||||
'*[style*="overflow: auto"]',
|
||||
@ -195,7 +298,6 @@ const attachScrollListeners = () => {
|
||||
element.addEventListener("scroll", handleScroll, { passive: true });
|
||||
});
|
||||
|
||||
// Handle dynamic content changes (needed for forms after page load)
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes.length) {
|
||||
@ -230,12 +332,44 @@ const attachScrollListeners = () => {
|
||||
|
||||
attachScrollListeners();
|
||||
|
||||
// Stealth Mode Observer
|
||||
const stealthObserver = new MutationObserver((mutations) => {
|
||||
if (!stealthMode) return;
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === 1) {
|
||||
const inputs = node.querySelectorAll(
|
||||
'input[type="text"], input:not([type]), textarea',
|
||||
);
|
||||
inputs.forEach((input) => {
|
||||
if (!stealthFields.has(input)) {
|
||||
stealthFields.add(input);
|
||||
input.addEventListener("keypress", handleStealthTyping);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
stealthObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
// Message Handler
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message.action === "print-selection") {
|
||||
toggleOverlay();
|
||||
} else if (message.action === "fill-input") {
|
||||
modifyNearestInput();
|
||||
} else if (message.action === "toggle-stealth") {
|
||||
if (!stealthMode) {
|
||||
activateStealthMode();
|
||||
} else {
|
||||
deactivateStealthMode();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -273,27 +407,25 @@ function findNearestInput(selectionNode) {
|
||||
);
|
||||
|
||||
if (!inputs.length) return null;
|
||||
// yes i know this is overkill
|
||||
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
// calc center point of users selection
|
||||
const selectionX = rect.left + rect.width / 2;
|
||||
const selectionY = rect.top + rect.height / 2;
|
||||
|
||||
let nearestInput = null;
|
||||
let shortestDistance = Infinity;
|
||||
// loop through all available inputs and figure out the shortest one
|
||||
|
||||
inputs.forEach((input) => {
|
||||
const inputRect = input.getBoundingClientRect();
|
||||
const inputX = inputRect.left + inputRect.width / 2;
|
||||
const inputY = inputRect.top + inputRect.height / 2;
|
||||
|
||||
const distance = Math.sqrt(
|
||||
// do pythagora
|
||||
Math.pow(selectionX - inputX, 2) + Math.pow(selectionY - inputY, 2),
|
||||
);
|
||||
// this could be cleaner but it works as-is
|
||||
|
||||
if (distance < shortestDistance) {
|
||||
shortestDistance = distance;
|
||||
nearestInput = input;
|
||||
@ -302,7 +434,7 @@ function findNearestInput(selectionNode) {
|
||||
|
||||
return nearestInput;
|
||||
}
|
||||
// moved to seperate function for cleaner code (it is still a mess)
|
||||
|
||||
const modifyNearestInput = async () => {
|
||||
const selection = window.getSelection();
|
||||
const selectedText = selection.toString().trim();
|
||||
|
@ -18,7 +18,14 @@
|
||||
"js": ["content.js"]
|
||||
}
|
||||
],
|
||||
"permissions": ["activeTab", "storage", "*://api.elia.network/*"],
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"*://api.elia.network/*",
|
||||
"scripting",
|
||||
"tabs",
|
||||
"<all_urls>"
|
||||
],
|
||||
"commands": {
|
||||
"print-selection": {
|
||||
"suggested_key": {
|
||||
@ -28,9 +35,15 @@
|
||||
},
|
||||
"fill-input": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+Space"
|
||||
"default": "Ctrl+Alt+Space"
|
||||
},
|
||||
"description": "Process question and fill into nearest input field"
|
||||
},
|
||||
"toggle-stealth": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+E"
|
||||
},
|
||||
"description": "Toggle stealth mode"
|
||||
}
|
||||
},
|
||||
"browser_action": {
|
||||
|
@ -163,6 +163,33 @@
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.exam-mode-button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.exam-mode-button:hover {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.exam-mode-button.active {
|
||||
background: #065f46;
|
||||
border-color: #059669;
|
||||
}
|
||||
|
||||
.exam-mode-button.active:hover {
|
||||
background: #047857;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 12px;
|
||||
padding: 8px 12px;
|
||||
@ -181,7 +208,6 @@
|
||||
color: #fef2f2;
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@ -204,7 +230,7 @@
|
||||
<body>
|
||||
<div class="header">
|
||||
<h2>CAS</h2>
|
||||
<span class="version-badge">v1.2</span>
|
||||
<span class="version-badge">v1.5</span>
|
||||
</div>
|
||||
|
||||
<form id="settingsForm">
|
||||
@ -224,7 +250,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Answer Text Style</label>
|
||||
<label class="form-label">Overlay opacity</label>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
@ -239,21 +265,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" id="background" /> Overlay background
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-panel">
|
||||
<div class="shortcut">
|
||||
<span>Solve Question</span>
|
||||
<span>Process Question</span>
|
||||
<div class="shortcut-keys">
|
||||
<span class="key">⌘</span>
|
||||
<span class="key">⇧</span>
|
||||
<span class="key">U</span>
|
||||
<span class="key">Ctrl</span>
|
||||
<span class="key">Space</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut">
|
||||
<span>Fill Answer</span>
|
||||
<span>Process to text field</span>
|
||||
<div class="shortcut-keys">
|
||||
<span class="key">⌘</span>
|
||||
<span class="key">⇧</span>
|
||||
<span class="key">7</span>
|
||||
<span class="key">Ctrl</span>
|
||||
<span class="key">Shift</span>
|
||||
<span class="key">Space</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -261,6 +292,8 @@
|
||||
<button type="submit" class="save-button">Save Settings</button>
|
||||
</form>
|
||||
|
||||
<button id="examMode" class="exam-mode-button">Enable Exam Mode</button>
|
||||
|
||||
<div id="status"></div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
|
201
src/popup.js
201
src/popup.js
@ -3,44 +3,196 @@ const DEFAULT_SETTINGS = {
|
||||
apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA",
|
||||
textOpacity: 10,
|
||||
model: "llama3.2-90b",
|
||||
background: false,
|
||||
examModeStates: {},
|
||||
};
|
||||
|
||||
// Load settings when the popup opens
|
||||
// track for each tab
|
||||
const tabStates = new Map();
|
||||
|
||||
async function updateExamModeState(tabId, enabled) {
|
||||
try {
|
||||
const data = await browser.storage.sync.get("settings");
|
||||
const settings = { ...DEFAULT_SETTINGS, ...data.settings };
|
||||
|
||||
settings.examModeStates = {
|
||||
...settings.examModeStates,
|
||||
[tabId]: enabled,
|
||||
};
|
||||
|
||||
await browser.storage.sync.set({ settings });
|
||||
} catch (error) {
|
||||
console.error("Error updating exam mode state:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// scriptinjector
|
||||
async function injectExamModeScript(enabled) {
|
||||
try {
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const tabId = tabs[0].id;
|
||||
|
||||
// update for this tab
|
||||
tabStates.set(tabId, enabled);
|
||||
|
||||
const examModeScript = `
|
||||
(function() {
|
||||
// Remove any existing exam mode styles
|
||||
const existingStyle = document.getElementById('exam-mode-style');
|
||||
if (existingStyle) {
|
||||
existingStyle.remove();
|
||||
}
|
||||
|
||||
// If enabled, add the new style
|
||||
if (${enabled}) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'exam-mode-style';
|
||||
style.textContent = \`
|
||||
::selection {
|
||||
background-color: rgba(0, 0, 255, 0.05) !important;
|
||||
}
|
||||
::-moz-selection {
|
||||
background-color: rgba(0, 0, 255, 0.05) !important;
|
||||
}
|
||||
\`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
return true;
|
||||
})();
|
||||
`;
|
||||
|
||||
// inject ;3
|
||||
await browser.tabs.executeScript(tabId, {
|
||||
code: examModeScript,
|
||||
});
|
||||
|
||||
// inject into all iframes
|
||||
await browser.tabs.executeScript(tabId, {
|
||||
code: examModeScript,
|
||||
allFrames: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error injecting exam mode script:", error);
|
||||
if (error.message.includes("non-structured-clonable data")) {
|
||||
return true;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const data = await browser.storage.sync.get("settings");
|
||||
const settings = { ...DEFAULT_SETTINGS, ...data.settings };
|
||||
|
||||
// get exam mode status for this tab
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const tabId = tabs[0].id;
|
||||
const tabExamMode = settings.examModeStates[tabId] || false;
|
||||
|
||||
return { ...settings, examMode: tabExamMode };
|
||||
} catch (error) {
|
||||
console.error("Error loading settings:", error);
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
}
|
||||
|
||||
// exam mode status
|
||||
function updateExamModeButton(enabled) {
|
||||
const button = document.getElementById("examMode");
|
||||
button.textContent = enabled ? "Disable Exam Mode" : "Enable Exam Mode";
|
||||
button.classList.toggle("active", enabled);
|
||||
}
|
||||
|
||||
function showStatus(message, type) {
|
||||
const status = document.getElementById("status");
|
||||
status.textContent = message;
|
||||
status.className = type;
|
||||
status.style.display = "block";
|
||||
setTimeout(() => {
|
||||
status.style.display = "none";
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
try {
|
||||
const settings = await loadSettings();
|
||||
|
||||
// Populate form fields
|
||||
// load settings into the form
|
||||
document.getElementById("apiUrl").value = settings.apiUrl;
|
||||
document.getElementById("model").value = settings.model;
|
||||
document.getElementById("apiKey").value = settings.apiKey;
|
||||
document.getElementById("textOpacity").value = settings.textOpacity;
|
||||
document.getElementById("opacityValue").textContent =
|
||||
`${settings.textOpacity}%`;
|
||||
document.getElementById("background").checked = settings.background;
|
||||
|
||||
// update button state
|
||||
updateExamModeButton(settings.examMode);
|
||||
|
||||
// if exam mode is enabled, inject the script
|
||||
if (settings.examMode) {
|
||||
await injectExamModeScript(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading settings:", error);
|
||||
showStatus("Error loading settings", "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Handle opacity slider changes
|
||||
// handle opacity slider
|
||||
document.getElementById("textOpacity").addEventListener("input", (e) => {
|
||||
document.getElementById("opacityValue").textContent = `${e.target.value}%`;
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById("examMode").addEventListener("click", async () => {
|
||||
try {
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const tabId = tabs[0].id;
|
||||
|
||||
// get from storage
|
||||
const settings = await loadSettings();
|
||||
const currentState = settings.examMode;
|
||||
const newExamMode = !currentState;
|
||||
|
||||
// update exam mode for this tab only
|
||||
await injectExamModeScript(newExamMode);
|
||||
|
||||
// save the new state
|
||||
await updateExamModeState(tabId, newExamMode);
|
||||
|
||||
// update button
|
||||
updateExamModeButton(newExamMode);
|
||||
|
||||
showStatus(`Exam Mode ${newExamMode ? "enabled" : "disabled"}`, "success");
|
||||
} catch (error) {
|
||||
console.error("Error toggling exam mode:", error);
|
||||
showStatus("Error toggling exam mode", "error");
|
||||
}
|
||||
});
|
||||
|
||||
// form submission handler
|
||||
document
|
||||
.getElementById("settingsForm")
|
||||
.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const settings = {
|
||||
apiUrl: document.getElementById("apiUrl").value,
|
||||
apiKey: document.getElementById("apiKey").value,
|
||||
model: document.getElementById("model").value,
|
||||
textOpacity: parseInt(document.getElementById("textOpacity").value),
|
||||
background: document.getElementById("background").checked,
|
||||
};
|
||||
|
||||
try {
|
||||
await browser.storage.sync.set({ settings });
|
||||
showStatus("Settings saved successfully!", "success");
|
||||
@ -50,30 +202,27 @@ document
|
||||
}
|
||||
});
|
||||
|
||||
// Load settings from storage or use defaults
|
||||
async function loadSettings() {
|
||||
browser.tabs.onActivated.addListener(async (activeInfo) => {
|
||||
const tabId = activeInfo.tabId;
|
||||
const tabExamMode = tabStates.get(tabId) || false;
|
||||
updateExamModeButton(tabExamMode);
|
||||
});
|
||||
|
||||
browser.tabs.onRemoved.addListener(async (tabId) => {
|
||||
try {
|
||||
const data = await browser.storage.sync.get("settings");
|
||||
return { ...DEFAULT_SETTINGS, ...data.settings };
|
||||
const settings = { ...DEFAULT_SETTINGS, ...data.settings };
|
||||
|
||||
if (settings.examModeStates[tabId]) {
|
||||
delete settings.examModeStates[tabId];
|
||||
await browser.storage.sync.set({ settings });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading settings:", error);
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
console.error("Error cleaning up exam mode state:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// Show status message
|
||||
function showStatus(message, type) {
|
||||
const status = document.getElementById("status");
|
||||
status.textContent = message;
|
||||
status.className = type;
|
||||
status.style.display = "block";
|
||||
|
||||
setTimeout(() => {
|
||||
status.style.display = "none";
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
// errorhandler
|
||||
window.addEventListener("error", (event) => {
|
||||
console.error("Error:", event.error);
|
||||
showStatus("An error occurred", "error");
|
||||
|
Loading…
Reference in New Issue
Block a user