Compare commits

..

3 Commits
v1.4 ... main

Author SHA1 Message Date
elijah
343828d018 Modifiy stealth mode 2024-11-01 03:21:01 +01:00
elijah
df231c0a90 Added accessibility features. 2024-10-31 19:56:45 +01:00
elijah
fb8a786f8c v1.5 2024-10-25 02:10:30 +02:00
6 changed files with 366 additions and 53 deletions

View File

@ -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

Binary file not shown.

View File

@ -1,21 +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 = () => {
@ -50,7 +54,7 @@ const queryLLM = async (text) => {
messages: [
{
role: "user",
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 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 "I do not know."): ${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,
@ -67,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();
@ -113,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;
}
@ -145,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) {
@ -176,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"]',
@ -205,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) {
@ -240,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();
}
}
});
@ -283,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;
@ -312,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();

View File

@ -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": {

View File

@ -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.4</span>
<span class="version-badge">v1.5</span>
</div>
<form id="settingsForm">
@ -266,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>

View File

@ -4,14 +4,128 @@ const DEFAULT_SETTINGS = {
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;
@ -19,23 +133,59 @@ document.addEventListener("DOMContentLoaded", async () => {
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,
@ -43,7 +193,6 @@ document
textOpacity: parseInt(document.getElementById("textOpacity").value),
background: document.getElementById("background").checked,
};
try {
await browser.storage.sync.set({ settings });
showStatus("Settings saved successfully!", "success");
@ -53,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");