Compare commits

..

2 Commits
v1.5 ... 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
5 changed files with 154 additions and 25 deletions

View File

@ -8,8 +8,6 @@ A simple, versatile language processing interface for websites.
- Adjust opacity of responses - Adjust opacity of responses
- Hidden - Hidden
- Only visible to those who know where to look - Only visible to those who know where to look
- Exam mode
- Make text selection hard to see to avoid detection
## 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.

View File

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

Binary file not shown.

View File

@ -10,13 +10,16 @@ const DEFAULT_SETTINGS = {
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 = () => {
@ -51,7 +54,7 @@ const queryLLM = async (text) => {
messages: [ messages: [
{ {
role: "user", 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, temperature: 0.1,
@ -68,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();
@ -114,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;
} }
@ -146,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) {
@ -177,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"]',
@ -206,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) {
@ -241,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();
}
} }
}); });
@ -284,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;
@ -313,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();

View File

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