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
|
## 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.
|
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.
|
Additionally, pressing `Ctrl+Shift+Space` will write the answer into the nearest text field.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
The extension can be configured by clicking on the extension icon in the toolbar.
|
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.
|
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.
|
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.
|
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
|
## Installation
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
browser.commands.onCommand.addListener((command) => {
|
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.query({ active: true, currentWindow: true }).then((tabs) => {
|
||||||
browser.tabs.sendMessage(tabs[0].id, { action: command });
|
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 = {
|
const DEFAULT_SETTINGS = {
|
||||||
apiUrl: "https://api.elia.network",
|
apiUrl: "https://api.elia.network",
|
||||||
apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA",
|
apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA",
|
||||||
textOpacity: 10,
|
textOpacity: 10,
|
||||||
model: "llama3.2-90b",
|
model: "llama3.2-90b",
|
||||||
|
background: false,
|
||||||
|
examModeStates: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_CONTEXT_LENGTH = 6000;
|
const MAX_CONTEXT_LENGTH = 6000;
|
||||||
|
|
||||||
// States (for later in the code)
|
// States
|
||||||
let overlay = null;
|
let overlay = null;
|
||||||
let isVisible = false;
|
let isVisible = false;
|
||||||
let lastKnownSelection = null;
|
let lastKnownSelection = null;
|
||||||
let lastKnownRange = null;
|
let lastKnownRange = null;
|
||||||
let scrollTimeout = null;
|
let scrollTimeout = null;
|
||||||
let resizeRAF = null;
|
let resizeRAF = null;
|
||||||
|
let stealthMode = false;
|
||||||
|
let stealthAnswer = null;
|
||||||
|
let stealthFields = new Set();
|
||||||
|
|
||||||
// helper Functions
|
// helper Functions
|
||||||
const getPageContext = () => {
|
const getPageContext = () => {
|
||||||
@ -49,7 +54,7 @@ const queryLLM = async (text) => {
|
|||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "user",
|
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,
|
temperature: 0.1,
|
||||||
@ -66,6 +71,106 @@ const queryLLM = async (text) => {
|
|||||||
return data.choices[0].message.content;
|
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
|
// Overlay/input fields
|
||||||
const createOverlay = (text) => {
|
const createOverlay = (text) => {
|
||||||
if (overlay) overlay.remove();
|
if (overlay) overlay.remove();
|
||||||
@ -80,10 +185,19 @@ const createOverlay = (text) => {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
color: rgba(0, 0, 0, ${settings.textOpacity / 100});
|
color: rgba(0, 0, 0, ${settings.textOpacity / 100});
|
||||||
font-size: 16px;
|
font-size: 12px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
pointer-events: none;
|
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);
|
document.body.appendChild(overlay);
|
||||||
@ -103,29 +217,22 @@ const positionOverlay = () => {
|
|||||||
const range = selection.getRangeAt(0);
|
const range = selection.getRangeAt(0);
|
||||||
const rect = range.getBoundingClientRect();
|
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 scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
||||||
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
|
||||||
// Get viewport dimensions
|
|
||||||
const viewportWidth =
|
const viewportWidth =
|
||||||
window.innerWidth || document.documentElement.clientWidth;
|
window.innerWidth || document.documentElement.clientWidth;
|
||||||
const viewportHeight =
|
const viewportHeight =
|
||||||
window.innerHeight || document.documentElement.clientHeight;
|
window.innerHeight || document.documentElement.clientHeight;
|
||||||
|
|
||||||
// Calculate overlay dimensions
|
|
||||||
const overlayWidth = overlay.offsetWidth;
|
const overlayWidth = overlay.offsetWidth;
|
||||||
const overlayHeight = overlay.offsetHeight;
|
const overlayHeight = overlay.offsetHeight;
|
||||||
|
|
||||||
// Calculate positions
|
|
||||||
let top = rect.top + scrollY;
|
let top = rect.top + scrollY;
|
||||||
let left = rect.left + scrollX + rect.width / 2 - overlayWidth / 2;
|
let left = rect.left + scrollX + rect.width / 2 - overlayWidth / 2;
|
||||||
|
|
||||||
// Position above selection by default
|
|
||||||
top -= overlayHeight + 5;
|
top -= overlayHeight + 5;
|
||||||
|
|
||||||
// If overlay would go above viewport, position it below selection
|
|
||||||
if (top - scrollY < 0) {
|
if (top - scrollY < 0) {
|
||||||
top = rect.bottom + scrollY + 5;
|
top = rect.bottom + scrollY + 5;
|
||||||
}
|
}
|
||||||
@ -135,7 +242,6 @@ const positionOverlay = () => {
|
|||||||
Math.min(left, scrollX + viewportWidth - overlayWidth - 10),
|
Math.min(left, scrollX + viewportWidth - overlayWidth - 10),
|
||||||
);
|
);
|
||||||
|
|
||||||
// apply
|
|
||||||
overlay.style.top = `${top}px`;
|
overlay.style.top = `${top}px`;
|
||||||
overlay.style.left = `${left}px`;
|
overlay.style.left = `${left}px`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -166,18 +272,15 @@ const handleResize = () => {
|
|||||||
|
|
||||||
window.addEventListener("resize", handleResize, { passive: true });
|
window.addEventListener("resize", handleResize, { passive: true });
|
||||||
|
|
||||||
// Shitty fix for microsoft forms (they use containers)
|
// Scroll handlers
|
||||||
const attachScrollListeners = () => {
|
const attachScrollListeners = () => {
|
||||||
// Listen to window scroll
|
|
||||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
|
|
||||||
// Listen to scroll events on all scrollable elements
|
|
||||||
document.addEventListener("scroll", handleScroll, {
|
document.addEventListener("scroll", handleScroll, {
|
||||||
capture: true,
|
capture: true,
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find and attach listeners to all scrollable containers
|
|
||||||
const scrollableElements = document.querySelectorAll(
|
const scrollableElements = document.querySelectorAll(
|
||||||
[
|
[
|
||||||
'*[style*="overflow: auto"]',
|
'*[style*="overflow: auto"]',
|
||||||
@ -195,7 +298,6 @@ const attachScrollListeners = () => {
|
|||||||
element.addEventListener("scroll", handleScroll, { passive: true });
|
element.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle dynamic content changes (needed for forms after page load)
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
if (mutation.addedNodes.length) {
|
if (mutation.addedNodes.length) {
|
||||||
@ -230,12 +332,44 @@ const attachScrollListeners = () => {
|
|||||||
|
|
||||||
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
|
// Message Handler
|
||||||
browser.runtime.onMessage.addListener((message) => {
|
browser.runtime.onMessage.addListener((message) => {
|
||||||
if (message.action === "print-selection") {
|
if (message.action === "print-selection") {
|
||||||
toggleOverlay();
|
toggleOverlay();
|
||||||
} else if (message.action === "fill-input") {
|
} else if (message.action === "fill-input") {
|
||||||
modifyNearestInput();
|
modifyNearestInput();
|
||||||
|
} else if (message.action === "toggle-stealth") {
|
||||||
|
if (!stealthMode) {
|
||||||
|
activateStealthMode();
|
||||||
|
} else {
|
||||||
|
deactivateStealthMode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -273,27 +407,25 @@ function findNearestInput(selectionNode) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!inputs.length) return null;
|
if (!inputs.length) return null;
|
||||||
// yes i know this is overkill
|
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
const range = selection.getRangeAt(0);
|
const range = selection.getRangeAt(0);
|
||||||
const rect = range.getBoundingClientRect();
|
const rect = range.getBoundingClientRect();
|
||||||
// calc center point of users selection
|
|
||||||
const selectionX = rect.left + rect.width / 2;
|
const selectionX = rect.left + rect.width / 2;
|
||||||
const selectionY = rect.top + rect.height / 2;
|
const selectionY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
let nearestInput = null;
|
let nearestInput = null;
|
||||||
let shortestDistance = Infinity;
|
let shortestDistance = Infinity;
|
||||||
// loop through all available inputs and figure out the shortest one
|
|
||||||
inputs.forEach((input) => {
|
inputs.forEach((input) => {
|
||||||
const inputRect = input.getBoundingClientRect();
|
const inputRect = input.getBoundingClientRect();
|
||||||
const inputX = inputRect.left + inputRect.width / 2;
|
const inputX = inputRect.left + inputRect.width / 2;
|
||||||
const inputY = inputRect.top + inputRect.height / 2;
|
const inputY = inputRect.top + inputRect.height / 2;
|
||||||
|
|
||||||
const distance = Math.sqrt(
|
const distance = Math.sqrt(
|
||||||
// do pythagora
|
|
||||||
Math.pow(selectionX - inputX, 2) + Math.pow(selectionY - inputY, 2),
|
Math.pow(selectionX - inputX, 2) + Math.pow(selectionY - inputY, 2),
|
||||||
);
|
);
|
||||||
// this could be cleaner but it works as-is
|
|
||||||
if (distance < shortestDistance) {
|
if (distance < shortestDistance) {
|
||||||
shortestDistance = distance;
|
shortestDistance = distance;
|
||||||
nearestInput = input;
|
nearestInput = input;
|
||||||
@ -302,7 +434,7 @@ function findNearestInput(selectionNode) {
|
|||||||
|
|
||||||
return nearestInput;
|
return nearestInput;
|
||||||
}
|
}
|
||||||
// moved to seperate function for cleaner code (it is still a mess)
|
|
||||||
const modifyNearestInput = async () => {
|
const modifyNearestInput = async () => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
const selectedText = selection.toString().trim();
|
const selectedText = selection.toString().trim();
|
||||||
|
@ -18,7 +18,14 @@
|
|||||||
"js": ["content.js"]
|
"js": ["content.js"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"permissions": ["activeTab", "storage", "*://api.elia.network/*"],
|
"permissions": [
|
||||||
|
"activeTab",
|
||||||
|
"storage",
|
||||||
|
"*://api.elia.network/*",
|
||||||
|
"scripting",
|
||||||
|
"tabs",
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
"commands": {
|
"commands": {
|
||||||
"print-selection": {
|
"print-selection": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
@ -28,9 +35,15 @@
|
|||||||
},
|
},
|
||||||
"fill-input": {
|
"fill-input": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
"default": "Ctrl+Shift+Space"
|
"default": "Ctrl+Alt+Space"
|
||||||
},
|
},
|
||||||
"description": "Process question and fill into nearest input field"
|
"description": "Process question and fill into nearest input field"
|
||||||
|
},
|
||||||
|
"toggle-stealth": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Ctrl+E"
|
||||||
|
},
|
||||||
|
"description": "Toggle stealth mode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
|
@ -163,6 +163,33 @@
|
|||||||
background: var(--primary-hover);
|
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 {
|
#status {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@ -181,7 +208,6 @@
|
|||||||
color: #fef2f2;
|
color: #fef2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar Styling */
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@ -204,7 +230,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2>CAS</h2>
|
<h2>CAS</h2>
|
||||||
<span class="version-badge">v1.2</span>
|
<span class="version-badge">v1.5</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="settingsForm">
|
<form id="settingsForm">
|
||||||
@ -224,7 +250,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Answer Text Style</label>
|
<label class="form-label">Overlay opacity</label>
|
||||||
<div class="slider-container">
|
<div class="slider-container">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@ -239,21 +265,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="shortcuts-panel">
|
||||||
<div class="shortcut">
|
<div class="shortcut">
|
||||||
<span>Solve Question</span>
|
<span>Process Question</span>
|
||||||
<div class="shortcut-keys">
|
<div class="shortcut-keys">
|
||||||
<span class="key">⌘</span>
|
<span class="key">Ctrl</span>
|
||||||
<span class="key">⇧</span>
|
<span class="key">Space</span>
|
||||||
<span class="key">U</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="shortcut">
|
<div class="shortcut">
|
||||||
<span>Fill Answer</span>
|
<span>Process to text field</span>
|
||||||
<div class="shortcut-keys">
|
<div class="shortcut-keys">
|
||||||
<span class="key">⌘</span>
|
<span class="key">Ctrl</span>
|
||||||
<span class="key">⇧</span>
|
<span class="key">Shift</span>
|
||||||
<span class="key">7</span>
|
<span class="key">Space</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -261,6 +292,8 @@
|
|||||||
<button type="submit" class="save-button">Save Settings</button>
|
<button type="submit" class="save-button">Save Settings</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<button id="examMode" class="exam-mode-button">Enable Exam Mode</button>
|
||||||
|
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
|
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
|
203
src/popup.js
203
src/popup.js
@ -3,44 +3,196 @@ const DEFAULT_SETTINGS = {
|
|||||||
apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA",
|
apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA",
|
||||||
textOpacity: 10,
|
textOpacity: 10,
|
||||||
model: "llama3.2-90b",
|
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 () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
try {
|
try {
|
||||||
const settings = await loadSettings();
|
const settings = await loadSettings();
|
||||||
|
// load settings into the form
|
||||||
// Populate form fields
|
|
||||||
document.getElementById("apiUrl").value = settings.apiUrl;
|
document.getElementById("apiUrl").value = settings.apiUrl;
|
||||||
document.getElementById("model").value = settings.model;
|
document.getElementById("model").value = settings.model;
|
||||||
document.getElementById("apiKey").value = settings.apiKey;
|
document.getElementById("apiKey").value = settings.apiKey;
|
||||||
document.getElementById("textOpacity").value = settings.textOpacity;
|
document.getElementById("textOpacity").value = settings.textOpacity;
|
||||||
document.getElementById("opacityValue").textContent =
|
document.getElementById("opacityValue").textContent =
|
||||||
`${settings.textOpacity}%`;
|
`${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) {
|
} catch (error) {
|
||||||
console.error("Error loading settings:", error);
|
console.error("Error loading settings:", error);
|
||||||
showStatus("Error loading settings", "error");
|
showStatus("Error loading settings", "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle opacity slider changes
|
// handle opacity slider
|
||||||
document.getElementById("textOpacity").addEventListener("input", (e) => {
|
document.getElementById("textOpacity").addEventListener("input", (e) => {
|
||||||
document.getElementById("opacityValue").textContent = `${e.target.value}%`;
|
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
|
document
|
||||||
.getElementById("settingsForm")
|
.getElementById("settingsForm")
|
||||||
.addEventListener("submit", async (e) => {
|
.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
apiUrl: document.getElementById("apiUrl").value,
|
apiUrl: document.getElementById("apiUrl").value,
|
||||||
apiKey: document.getElementById("apiKey").value,
|
apiKey: document.getElementById("apiKey").value,
|
||||||
model: document.getElementById("model").value,
|
model: document.getElementById("model").value,
|
||||||
textOpacity: parseInt(document.getElementById("textOpacity").value),
|
textOpacity: parseInt(document.getElementById("textOpacity").value),
|
||||||
|
background: document.getElementById("background").checked,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await browser.storage.sync.set({ settings });
|
await browser.storage.sync.set({ settings });
|
||||||
showStatus("Settings saved successfully!", "success");
|
showStatus("Settings saved successfully!", "success");
|
||||||
@ -50,30 +202,27 @@ document
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load settings from storage or use defaults
|
browser.tabs.onActivated.addListener(async (activeInfo) => {
|
||||||
async function loadSettings() {
|
const tabId = activeInfo.tabId;
|
||||||
|
const tabExamMode = tabStates.get(tabId) || false;
|
||||||
|
updateExamModeButton(tabExamMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.tabs.onRemoved.addListener(async (tabId) => {
|
||||||
try {
|
try {
|
||||||
const data = await browser.storage.sync.get("settings");
|
const data = await browser.storage.sync.get("settings");
|
||||||
return { ...DEFAULT_SETTINGS, ...data.settings };
|
const settings = { ...DEFAULT_SETTINGS, ...data.settings };
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading settings:", error);
|
if (settings.examModeStates[tabId]) {
|
||||||
return DEFAULT_SETTINGS;
|
delete settings.examModeStates[tabId];
|
||||||
|
await browser.storage.sync.set({ settings });
|
||||||
}
|
}
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error("Error cleaning up exam mode state:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Show status message
|
// errorhandler
|
||||||
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
|
|
||||||
window.addEventListener("error", (event) => {
|
window.addEventListener("error", (event) => {
|
||||||
console.error("Error:", event.error);
|
console.error("Error:", event.error);
|
||||||
showStatus("An error occurred", "error");
|
showStatus("An error occurred", "error");
|
||||||
|
Loading…
Reference in New Issue
Block a user