Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
343828d018 | ||
|
df231c0a90 |
@ -8,8 +8,6 @@ A simple, versatile language processing interface for websites.
|
||||
- Adjust opacity of responses
|
||||
- Hidden
|
||||
- Only visible to those who know where to look
|
||||
- Exam mode
|
||||
- Make text selection hard to see to avoid detection
|
||||
|
||||
## 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.
|
||||
|
@ -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.
163
src/content.js
163
src/content.js
@ -10,13 +10,16 @@ const DEFAULT_SETTINGS = {
|
||||
|
||||
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 = () => {
|
||||
@ -51,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,
|
||||
@ -68,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();
|
||||
@ -114,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;
|
||||
}
|
||||
@ -146,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) {
|
||||
@ -177,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"]',
|
||||
@ -206,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) {
|
||||
@ -241,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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -284,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;
|
||||
@ -313,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();
|
||||
|
@ -35,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": {
|
||||
|
Loading…
Reference in New Issue
Block a user