```
├── README.md (1700 tokens)
├── contentScript.js (1300 tokens)
├── data/
├── inject/
├── anti-anti-debug.js (1100 tokens)
├── chatbot.js (28.7k tokens)
├── content.js (12.1k tokens)
├── copyOverride.js (1500 tokens)
├── customPaste.js (3.9k tokens)
├── exam.js (1300 tokens)
├── isolated.js (300 tokens)
├── main.js (900 tokens)
├── mock_code.js (700 tokens)
├── mock_code/
├── minifiedBackground.js (400 tokens)
├── minifiedContent-script.js
├── mock_manifest.json (400 tokens)
├── rules.json (3.9k tokens)
├── rightclickmenu.js
├── screenshare-bridge.js (200 tokens)
├── screenshare.js (3.8k tokens)
├── lib/
├── showdown.min.js (omitted)
├── nptel.json (21.5k tokens)
├── devtools.js (200 tokens)
├── images/
├── NeoExamShieldBanner.png
├── icon128.png
├── icon16.png
├── icon256.png
├── icon48.png
├── manifest.json (900 tokens)
├── metadata.json
├── nptel.txt (100 tokens)
├── popup.html (6.5k tokens)
├── popup.js (6.1k tokens)
├── worker.js (30.7k tokens)
```
## /README.md
<img width="1500" height="500" alt="NeoPass Banner" src="https://github.com/user-attachments/assets/7369dd86-838d-4fdc-abdd-6b41a9b14aed" />
# <i>**`Free`** NeoPass Extension</i>
> **NeoPass Pro** - [Click here to see Pro features and benefits](https://neopass.tech/pro)
This chrome extension is for students taking tests on the **`Iamneo portal`**, **`HackerRank`**, **`Wildlife Ecology NPTEL`**, **`conservation-geography NPTEL`**, **`forest management NPTEL`** and `other exam portals in chrome browser` that restrict your abilities
### [**Make sure to visit our website for the best experience!**](https://freeneopass.vercel.app) 🌐
<samp>
> [!IMPORTANT]
> **Free Users**: No sign-up needed! Configure your own AI API key by clicking the extension icon and going to the **Settings** tab.
> Supported providers: OpenAI, Google Gemini, Anthropic Claude, and custom endpoints.
>
>
> **Want a hassle-free experience?** Upgrade to Pro by visiting **neopass.tech/pro** for AI managed by NeoPass (GPT-5.1), increased rate limits, and NeoBrowser with built in Exam Helper access!
> [!WARNING]
> **Educational Purposes Only**: This extension is intended for educational purposes. Please use it responsibly and ethically.
> We am not responsible for any actions taken, and we do not encourage or promote cheating in any way.
> Be cautious when using the extension to maintain academic integrity.
## ✨ Features
### Free Version (Bring Your Own API Key)
- **`NPTEL Integration`** : Solve NPTEL Wildlife ecology answers
- **`NeoExamShield Bypass`** : Break free from Examly's limitations. NeoPass mimics the NeoExamShield extension
- **`Chatbot With Stealth Mode`** : Leverage AI Chatbot to enhance your search capabilities
- **`AI Search Answers/Code`** : Perform AI-powered searches, helping you find answers without switching tabs
- **`Solve MCQ`** : Quickly Search MCQ Answers by simply selecting
- **`Tab Switching Bypass`** : Prevents unwanted tab switch restrictions
- **`Pasting When Restricted`** : Quickly paste answers with ease, reducing the time spent on manual entry
- **`Multiple AI Providers`** : Support for OpenAI, Google Gemini, Anthropic Claude, and custom endpoints
### Pro Version Features
- **`Everything in free`** : All free features are included
- **`Managed AI by NeoPass`** : Powered by GPT-5.1 - no API key needed!
- **`Screenshare Bypass`** : Bypass fullscreen screenshare requirements with three modes:
- **Share Tab/Window** — share only the current tab or window instead of the entire screen
- **Share Blank Screen** — share a black screen without capturing anything real
- **Share Frozen Screen** — capture a single frame of your screen and freeze it
- **`NeoBrowser Access`** : Exclusive access to the NeoBrowser with built in Exam Helper
- **`No Network Restrictions`** : Works even if AI providers are blocked on your network
- **`Increased Rate Limits`** : Higher usage limits for intensive exam sessions
- **`Priority Support`** : Get help when you need it most
- **`Hassle-Free Experience`** : No configuration needed, just login and go!
## ⬇️ Installation
1. [Download](https://github.com/Max-Eee/NeoPass/archive/refs/heads/main.zip) the extension.
2. Open Chrome and go to the Extensions page by typing `chrome://extensions/`.
3. Enable **Developer mode** in the top right corner.
4. Click on **Load unpacked** and select the folder where the extension is located.
5. Your NeoPass extension is now installed!
### Installation Guide Video
https://github.com/user-attachments/assets/89fb986c-2edb-4252-8232-dbd10beec0cf
## 💻 Usage
### For Free Users:
1. Click the NeoPass extension icon in your browser toolbar
2. Navigate to the **Settings** tab
3. Enter your AI API key (OpenAI, Google Gemini, Anthropic, or custom endpoint)
4. Select your AI provider from the dropdown menu
5. Click "Test Connection" to verify your setup
6. Start using all NeoPass features with your own API!
> [!NOTE]
> **Network Restrictions**: If your school/organization blocks AI service providers (OpenAI, Google, etc.), the extension will not work even with a valid API key. In this case, consider using a VPN or upgrade to Pro by visiting **neopass.tech/pro**.
### For Pro Users:
1. Visit [neopass.tech/pro](https://freeneopass.vercel.app/pro) to subscribe
2. Click the extension icon and go to the **Pro** tab
3. Login with your Pro credentials you have created from the webstie
4. Enjoy hassle-free AI-powered assistance with no configuration needed!
## ⌨️ Shortcuts
### Windows/Linux Users:
- <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Q</kbd> : Solve Iam Neo MCQs/Coding Questions with 100% ACCURACY
- <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>A</kbd> : Solve Iam Neo MCQs/Coding Questions with using AI [Backup]
- <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>T</kbd> : Autotypes Iam Neo Coding Question Solution letter by letter
- <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>H</kbd> : Solve HackerRank Questions [BETA]
> [!NOTE]
> The following shortcuts **require text to be selected** before activation:
> - <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>N</kbd> : Solve NPTEL MCQs from selected text
> - <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd> : Search answers and code from selected text
> - <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd> : Search MCQs from selected text
- <kbd>Ctrl</kbd> + <kbd>V</kbd> : Paste content when blocked
- <kbd>Alt</kbd> + <kbd>C</kbd> : Open/Close Chatbot
<details>
<summary><strong>Mac Users (Click to expand)</strong></summary>
- <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Q</kbd> : Solve Iam Neo MCQs/Coding Questions with 100% ACCURACY
- <kbd>Option</kbd> + <kbd>Shift</kbd> + <kbd>A</kbd> : Solve Iam Neo MCQs/Coding Questions with using AI [Backup]
- <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>T</kbd> : Autotypes Iam Neo Coding Question Solution letter by letter
- <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>H</kbd> : Solve HackerRank Questions [BETA]
> [!NOTE]
> The following shortcuts **require text to be selected** before activation:
> - <kbd>Option</kbd> + <kbd>Shift</kbd> + <kbd>N</kbd> : Solve NPTEL MCQs from selected text
> - <kbd>Option</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd> : Search answers and code from selected text
> - <kbd>Option</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd> : Search MCQs from selected text
- <kbd>Cmd</kbd> + <kbd>V</kbd> : Paste content when blocked
- <kbd>Option</kbd> + <kbd>C</kbd> : Open/Close Chatbot
</details>
## 🤝 Contribute or Add NPTEL Dataset
If you want to contribute to the NPTEL question database, follow these steps:
1. Fork this repository
2. Open your NPTEL assignment page in the browser
3. Open browser developer tools (F12 or right-click > Inspect)
4. Go to the Console tab
5. Copy and paste the script from `nptel.txt` in the repository
6. Run the script by pressing Enter
7. The script will extract all questions and correct answers from the page
8. Copy the output JSON data
9. Update the `data/nptel.json` file with the new questions and answers
10. Create a pull request to contribute your additions back to the main repository
This helps expand our database and improves the accuracy of the NPTEL question solving feature!
## 💬 Feedback
We'd love to hear your thoughts! If you encounter any issues or have suggestions for improvement, please reach out. Your feedback is invaluable! 💌
📧 **Contact us at:** [freeneopass@gmail.com](mailto:freeneopass@gmail.com?subject=Issue%20Title%3A%20%5BBrief%20description%20of%20your%20issue%5D&body=Hello%20NeoPass%20Support%20Team%2C%0A%0AIssue%20Description%3A%0A%5BPlease%20describe%20your%20issue%20in%20detail%5D%0A%0AWhen%20does%20this%20occur%3A%0A%5BSpecify%20when%20the%20issue%20happens%20-%20e.g.%2C%20during%20login%2C%20while%20using%20a%20specific%20feature%2C%20etc.%5D%0A%0ASteps%20to%20Reproduce%3A%0A1.%20%5BFirst%20step%5D%0A2.%20%5BSecond%20step%5D%0A3.%20%5BThird%20step%5D%0A%0AScreenshots%2FError%20Messages%20if%20possible%3A%0A%5BPlease%20attach%20any%20relevant%20screenshots%20or%20paste%20error%20messages%20here%5D%0A%0AAdditional%20Information%3A%0A%5BAny%20other%20relevant%20details%5D%0A%0AThank%20you!)
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
</samp>
## /contentScript.js
```js path="/contentScript.js"
// Check if the chrome object is available (for compatibility)
if (typeof chrome === "undefined") {
// Handle the case where chrome is not defined (like in Firefox)
}
// Always inject mock_code.js interceptor to handle extension detection (even when not logged in)
(function injectMockCode() {
const mockScript = document.createElement('script');
mockScript.src = chrome.runtime.getURL('data/inject/mock_code.js');
mockScript.onload = function () {
console.log('✅ Mock code interceptor loaded');
this.remove(); // Clean up after execution
};
mockScript.onerror = function() {
console.error('❌ Failed to load mock code interceptor');
};
// Inject as early as possible
(document.head || document.documentElement).prepend(mockScript);
})();
// Inject exam.js (no login required)
const script = document.createElement('script');
script.src = chrome.runtime.getURL('data/inject/exam.js');
(document.head || document.documentElement).appendChild(script);
// Login prompt and status sync removed - extension features now available to all users
// Function removed - login check no longer required for extension features
// Neo Browser Download Link - Updated
const neoBrowserDownloadLink = "https://freeneopass.vercel.app";
// Function to add our NeoPass button left of the existing Neo Browser button
function replaceNeoBrowserButton() {
const neoButton = document.querySelector('button#neobrowser');
if (neoButton && !neoButton.dataset.replaced) {
// Create custom styled button/link
const ourBtn = document.createElement('a');
ourBtn.innerHTML = `
<div class="container jcc btn-align">
<div class="t-whitespace-nowrap ng-star-inserted">
<span>Download NeoPass Launcher</span>
</div>
</div>
`;
ourBtn.href = neoBrowserDownloadLink;
ourBtn.target = "_blank";
ourBtn.className = neoButton.className;
ourBtn.id = "neopass-browser-btn";
ourBtn.tabIndex = 0;
// Apply gradient styling
ourBtn.style.cssText = `
position: relative !important;
display: inline-flex !important;
padding: 8px 16px !important;
font-size: 14px !important;
font-weight: 500 !important;
color: white !important;
background-color: black !important;
border-radius: 8px !important;
text-align: center !important;
text-decoration: none !important;
cursor: pointer !important;
z-index: 1 !important;
border: 2px solid transparent !important;
transition: all 0.3s ease !important;
`;
// Create gradient border effect
const beforeStyle = document.createElement('style');
beforeStyle.textContent = `
a#neopass-browser-btn {
position: relative !important;
background: linear-gradient(black, black) padding-box,
linear-gradient(45deg, #3b82f6, #8b5cf6, #ec4899) border-box !important;
border: 2px solid transparent !important;
}
a#neopass-browser-btn:hover {
transform: scale(1.05) !important;
box-shadow: 0 0 20px rgba(139, 92, 246, 0.6) !important;
}
`;
if (!document.querySelector('style[data-neobrowser-style]')) {
beforeStyle.setAttribute('data-neobrowser-style', 'true');
document.head.appendChild(beforeStyle);
}
// Insert our button to the left of the existing button
neoButton.parentNode.insertBefore(ourBtn, neoButton);
// Make the parent (app-button) a flex row so both buttons sit side by side
neoButton.parentNode.style.cssText += `
display: flex !important;
flex-direction: row !important;
align-items: center !important;
gap: 8px !important;
`;
neoButton.dataset.replaced = "true";
console.log('✅ NeoPass NeoBrowser button added left of existing Neo Browser button');
}
}
// Observer to detect Neo Browser button and add our button
const buttonObserver = new MutationObserver((mutations) => {
replaceNeoBrowserButton();
});
// Start observing for button changes
buttonObserver.observe(document.body, {
childList: true,
subtree: true
});
// Initial check for Neo Browser button (in case already loaded)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', replaceNeoBrowserButton);
} else {
replaceNeoBrowserButton();
}
// Listen for window messages
window.addEventListener("message", function(event) {
// Only process messages that:
// 1. Come from the same window
// 2. Are targeted for the extension
if (event.data.target === "extension") {
// Forward the message to the extension's background script
chrome.runtime.sendMessage(event.data.message, response => {
// Send the response back to the window
window.postMessage({
source: "extension",
response: response
}, "*");
});
}
});
window.addEventListener("message", function (event) {
if (event.source === window && event.data.target === "extension") {
browser.runtime.sendMessage(event.data.message, (response) => {
window.postMessage({ source: "extension", response: response }, "*");
});
}
});
// Listen for the 'beforeunload' event to remove any injected elements
window.addEventListener("beforeunload", removeInjectedElement);
// Function to send a message to the website
function sendMessageToWebsite(messageData) {
removeInjectedElement(); // Clean up any previous injected elements
// Create a new span element with a unique ID
const injectedElement = document.createElement("span");
injectedElement.id = "x-template-base-" + messageData.currentKey; // Set a unique ID based on currentKey
// Append the new element to the document body
document.body.appendChild(injectedElement);
console.log("message", messageData); // Log the message data
// Send the message to the website
window.postMessage(0, messageData.url); // 0 is the targetOrigin, meaning the same origin
}
// Function to remove injected elements from the DOM
function removeInjectedElement() {
const injectedElement = document.querySelector("[id^='x-template-base-']"); // Select elements with ID starting with "x-template-base-"
if (injectedElement) {
injectedElement.remove(); // Remove the element if it exists
}
}
```
## /data/inject/anti-anti-debug.js
```js path="/data/inject/anti-anti-debug.js"
!(() => {
const Proxy = window.Proxy;
const Object = window.Object;
const Array = window.Array;
/**
* Save original methods before we override them
*/
const Originals = {
createElement: document.createElement,
log: console.log,
table: console.table,
clear: console.clear,
functionConstructor: window.Function.prototype.constructor,
setInterval: window.setInterval,
createElement: document.createElement,
toString: Function.prototype.toString,
addEventListener: window.addEventListener
}
/**
* Cutoffs for logging. After cutoff is reached, will no longer log anti debug warnings.
*/
const cutoffs = {
table: {
amount: 5,
within: 5000
},
clear: {
amount: 5,
within: 5000
},
redactedLog: {
amount: 5,
within: 5000
},
debugger: {
amount: 10,
within: 10000
},
debuggerThrow: {
amount: 10,
within: 10000
}
}
/**
* Decides if anti debug warnings should be logged
*/
function shouldLog(type) {
return false;
}
window.console.log = wrapFn((...args) => {
// Keep track of redacted arguments
let redactedCount = 0;
// Filter arguments for detectors
const newArgs = args.map((a) => {
// Don't print functions.
if (typeof a === 'function') {
redactedCount++;
return "Redacted Function";
}
// Passthrough if primitive
if (typeof a !== 'object' || a === null) return a;
// For objects, scan properties
var props = Object.getOwnPropertyDescriptors(a)
for (var name in props) {
// Redact custom getters
if (props[name].get !== undefined) {
redactedCount++;
return "Redacted Getter";
}
// Also block toString overrides
if (name === 'toString') {
redactedCount++;
return "Redacted Str";
}
}
// Defeat Performance Detector
// https://github.com/theajack/disable-devtool/blob/master/src/detector/sub-detector/performance.ts
if (Array.isArray(a) && a.length === 50 && typeof a[0] === "object") {
redactedCount++;
return "Redacted LargeObjArray";
}
return a;
});
// If most arguments are redacted, its probably spam
if (redactedCount >= Math.max(args.length - 1, 1)) {
if (!shouldLog("redactedLog")) {
return;
}
}
}, Originals.log);
window.console.table = wrapFn((obj) => {
if (shouldLog("table")) {
}
}, Originals.table);
window.console.clear = wrapFn(() => {
if (shouldLog("table")) {
}
}, Originals.clear);
let debugCount = 0;
window.Function.prototype.constructor = wrapFn((...args) => {
const originalFn = Originals.functionConstructor.apply(this, args);
var fnContent = args[0];
if (fnContent) {
if (fnContent.includes('debugger')) { // An anti-debugger is attempting to stop debugging
if (shouldLog("debugger")) {
}
debugCount++;
if (debugCount > 100) {
if (shouldLog("debuggerThrow")) {
}
throw new Error("You bad!");
} else {
setTimeout(() => {
debugCount--;
}, 1);
}
const newArgs = args.slice(0);
newArgs[0] = args[0].replaceAll("debugger", ""); // remove debugger statements
return new Proxy(Originals.functionConstructor.apply(this, newArgs),{
get: function (target, prop) {
if (prop === "toString") {
return originalFn.toString;
}
return target[prop];
}
});
}
}
return originalFn;
}, Originals.functionConstructor);
document.createElement = wrapFn((el, o) => {
var string = el.toString();
var element = Originals.createElement.apply(document, [string, o]);
if (string.toLowerCase() === "iframe") {
element.addEventListener("load", () => {
try {
element.contentWindow.window.console = window.console;
} catch (e) {
}
});
}
return element;
}, Originals.createElement);
function wrapFn(newFn, old) {
return new Proxy(newFn, {
get: function (target, prop) {
const callMethods = ['apply', 'bind', 'call'];
if (callMethods.includes(prop)) {
return target[prop];
}
return old[prop];
}
});
}
})()
```
## /data/inject/chatbot.js
```js path="/data/inject/chatbot.js"
if (typeof chrome === "undefined") {}
if (typeof window.isMac === 'undefined') {
window.isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 ||
navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
}
(function() {
chrome.storage.local.get(['stealth'], function(result) {
if (window.chatOverlayInjected) {
console.log("Chat overlay script already injected.");
return;
}
window.chatOverlayInjected = true;
const isStealthModeEnabled = result.stealth === true;
console.log("Initial stealth mode state:", isStealthModeEnabled);
function loadShowdown() {
return new Promise((resolve, reject) => {
if (typeof showdown !== 'undefined') {
resolve();
return;
}
const script = document.createElement('script');
script.src = chrome.runtime.getURL('data/lib/showdown.min.js'); // Local path
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function loadPrism() {
return new Promise((resolve) => {
// Create a lightweight inline syntax highlighter to bypass CSP
window.SimplePrism = {
highlightElement: function(codeElement) {
const code = codeElement.textContent;
const language = codeElement.className.replace('language-', '');
// Use a simpler approach to avoid overlapping replacements
let highlightedCode = this.simpleHighlight(code, language);
codeElement.innerHTML = highlightedCode;
},
simpleHighlight: function(code, language) {
// Escape HTML first
let highlighted = code.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
// Apply basic highlighting based on language
if (language === 'python') {
highlighted = this.highlightPython(highlighted);
} else if (language === 'javascript' || language === 'js') {
highlighted = this.highlightJavaScript(highlighted);
} else if (language === 'java') {
highlighted = this.highlightJava(highlighted);
} else if (language === 'css') {
highlighted = this.highlightCSS(highlighted);
} else if (language === 'html') {
highlighted = this.highlightHTML(highlighted);
} else if (language === 'sql') {
highlighted = this.highlightSQL(highlighted);
} else if (language === 'json') {
highlighted = this.highlightJSON(highlighted);
} else {
// Default to javascript-like highlighting
highlighted = this.highlightJavaScript(highlighted);
}
return highlighted;
},
highlightPython: function(code) {
// Use a token-based approach to avoid overlapping
let tokens = [];
let currentIndex = 0;
// First, find all comments
let match;
const commentRegex = /#.*$/gm;
while ((match = commentRegex.exec(code)) !== null) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'comment',
content: match[0]
});
}
// Find strings (avoiding those inside comments)
const stringRegex = /(['"])((?:\\.|(?!\1)[^\\])*?)\1/g;
while ((match = stringRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'string',
content: match[0]
});
}
}
// Find keywords (avoiding those inside comments and strings)
const keywordRegex = /\b(def|class|if|elif|else|for|while|return|import|from|try|except|finally|with|as|and|or|not|in|is)\b/g;
while ((match = keywordRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'keyword',
content: match[0]
});
}
}
// Find booleans and None
const booleanRegex = /\b(True|False|None)\b/g;
while ((match = booleanRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'boolean',
content: match[0]
});
}
}
// Find numbers
const numberRegex = /\b\d+(\.\d+)?\b/g;
while ((match = numberRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'number',
content: match[0]
});
}
}
// Sort tokens by position
tokens.sort((a, b) => a.start - b.start);
// Build highlighted code
let result = '';
let lastIndex = 0;
tokens.forEach(token => {
// Add unhighlighted text before this token
result += code.slice(lastIndex, token.start);
// Add highlighted token
result += `<span class="${token.type}">${token.content}</span>`;
lastIndex = token.end;
});
// Add remaining text
result += code.slice(lastIndex);
return result;
},
buildHighlightedCode: function(code, tokens) {
// Sort tokens by their start position
tokens.sort((a, b) => a.start - b.start);
let result = '';
let lastIndex = 0;
for (let token of tokens) {
// Add text before this token
result += code.slice(lastIndex, token.start);
// Add the highlighted token
result += `<span class="${token.type}">${token.content}</span>`;
lastIndex = token.end;
}
// Add remaining text
result += code.slice(lastIndex);
return result;
},
isInsideToken: function(position, tokens) {
return tokens.some(token => position >= token.start && position < token.end);
},
highlightJavaScript: function(code) {
let tokens = [];
let match;
// Find comments first
const singleLineCommentRegex = /\/\/.*$/gm;
while ((match = singleLineCommentRegex.exec(code)) !== null) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'comment',
content: match[0]
});
}
const multiLineCommentRegex = /\/\*[\s\S]*?\*\//g;
while ((match = multiLineCommentRegex.exec(code)) !== null) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'comment',
content: match[0]
});
}
// Find strings
const stringRegex = /(['"`])((?:\\.|(?!\1)[^\\])*?)\1/g;
while ((match = stringRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'string',
content: match[0]
});
}
}
// Find keywords
const keywordRegex = /\b(function|const|let|var|if|else|for|while|return|import|export|class|extends|new|this|typeof|instanceof)\b/g;
while ((match = keywordRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'keyword',
content: match[0]
});
}
}
// Find booleans
const booleanRegex = /\b(true|false|null|undefined)\b/g;
while ((match = booleanRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'boolean',
content: match[0]
});
}
}
// Find numbers
const numberRegex = /\b\d+(\.\d+)?\b/g;
while ((match = numberRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'number',
content: match[0]
});
}
}
return this.buildHighlightedCode(code, tokens);
},
highlightJava: function(code) {
let tokens = [];
let match;
// Find comments first
const singleLineCommentRegex = /\/\/.*$/gm;
while ((match = singleLineCommentRegex.exec(code)) !== null) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'comment',
content: match[0]
});
}
const multiLineCommentRegex = /\/\*[\s\S]*?\*\//g;
while ((match = multiLineCommentRegex.exec(code)) !== null) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'comment',
content: match[0]
});
}
// Find strings
const stringRegex = /(['"])((?:\\.|(?!\1)[^\\])*?)\1/g;
while ((match = stringRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'string',
content: match[0]
});
}
}
// Find keywords
const keywordRegex = /\b(public|private|protected|static|final|class|interface|extends|implements|if|else|for|while|return|import|package|new|this)\b/g;
while ((match = keywordRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'keyword',
content: match[0]
});
}
}
// Find booleans
const booleanRegex = /\b(true|false|null)\b/g;
while ((match = booleanRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'boolean',
content: match[0]
});
}
}
// Find numbers
const numberRegex = /\b\d+(\.\d+)?[fFdDlL]?\b/g;
while ((match = numberRegex.exec(code)) !== null) {
if (!this.isInsideToken(match.index, tokens)) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
type: 'number',
content: match[0]
});
}
}
return this.buildHighlightedCode(code, tokens);
},
highlightCSS: function(code) {
// Comments first
code = code.replace(/\/\*[\s\S]*?\*\//g, '<span class="comment">{{contextString}}amp;</span>');
// Selectors
code = code.replace(/([.#][a-zA-Z][a-zA-Z0-9_-]*)/g, '<span class="selector">$1</span>');
// Properties
code = code.replace(/([a-zA-Z-]+)(\s*:)/g, '<span class="property">$1</span>$2');
// Values
code = code.replace(/(#[0-9a-fA-F]+)/g, '<span class="value">$1</span>');
return code;
},
highlightHTML: function(code) {
// Comments first
code = code.replace(/(<!--[\s\S]*?-->)/g, '<span class="comment">$1</span>');
// Tags
code = code.replace(/(<\/?[^>]+>)/g, '<span class="tag">$1</span>');
return code;
},
highlightSQL: function(code) {
// Comments first
code = code.replace(/--.*$/gm, '<span class="comment">{{contextString}}amp;</span>');
// Strings
code = code.replace(/'[^']*'/g, '<span class="string">{{contextString}}amp;</span>');
// Keywords
code = code.replace(/\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TABLE|INDEX|PRIMARY|KEY|FOREIGN|NOT|NULL|DEFAULT|AND|OR|ORDER|BY|GROUP|HAVING|LIMIT)\b/gi, '<span class="keyword">$1</span>');
// Numbers
code = code.replace(/\b\d+(\.\d+)?\b/g, '<span class="number">{{contextString}}amp;</span>');
return code;
},
highlightJSON: function(code) {
// Property keys first (before general strings)
code = code.replace(/"([^"]*)"(\s*:)/g, '<span class="property">"$1"</span>$2');
// Remaining strings
code = code.replace(/"([^"]*)"/g, '<span class="string">"$1"</span>');
// Booleans and null
code = code.replace(/\b(true|false|null)\b/g, '<span class="boolean">$1</span>');
// Numbers
code = code.replace(/\b\d+(\.\d+)?\b/g, '<span class="number">{{contextString}}amp;</span>');
return code;
}
};
// Add CSS for syntax highlighting with clean default theme
// Styles will be added to shadow DOM later, not to document.head
window._chatSyntaxHighlightCSS = `
.keyword { color: #0066CC; font-weight: bold; }
.string { color: #008000; }
.comment { color: #808080; font-style: italic; }
.number { color: #FF6600; }
.boolean { color: #0066CC; font-weight: bold; }
.property { color: #9932CC; }
.selector { color: #008000; font-weight: bold; }
.value { color: #FF6600; }
.tag { color: #0066CC; }
`;
resolve();
});
}
// Chat icon SVG data URL (matching Crisp style)
const CHAT_ICON_SVG_URL = 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2235%22%20height%3D%2230%22%20viewBox%3D%220%200%2035%2030%22%3E%3Cdefs%3E%3Cfilter%20id%3D%22c%22%20width%3D%22123.1%25%22%20height%3D%22127.9%25%22%20x%3D%22-11.5%25%22%3E%3CfeOffset%20dy%3D%221%22%20in%3D%22SourceAlpha%22%20result%3D%22shadowOffsetOuter1%22%2F%3E%3CfeGaussianBlur%20in%3D%22shadowOffsetOuter1%22%20result%3D%22shadowBlurOuter1%22%20stdDeviation%3D%221%22%2F%3E%3CfeColorMatrix%20in%3D%22shadowBlurOuter1%22%20values%3D%220%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200.07%200%22%2F%3E%3C%2Ffilter%3E%3Cfilter%20id%3D%22e%22%20width%3D%22129.7%25%22%20height%3D%22135.9%25%22%20x%3D%22-14.8%25%22%20y%3D%22-14%25%22%3E%3CfeMorphology%20in%3D%22SourceAlpha%22%20radius%3D%221%22%20result%3D%22shadowSpreadInner1%22%2F%3E%3CfeGaussianBlur%20in%3D%22shadowSpreadInner1%22%20result%3D%22shadowBlurInner1%22%20stdDeviation%3D%222%22%2F%3E%3CfeOffset%20in%3D%22shadowBlurInner1%22%20result%3D%22shadowOffsetInner1%22%2F%3E%3CfeComposite%20in%3D%22shadowOffsetInner1%22%20in2%3D%22SourceAlpha%22%20k2%3D%22-1%22%20k3%3D%221%22%20operator%3D%22arithmetic%22%20result%3D%22shadowInnerInner1%22%2F%3E%3CfeColorMatrix%20in%3D%22shadowInnerInner1%22%20values%3D%220%200%200%200%201%200%200%200%200%201%200%200%200%200%201%200%200%200%200.750191215%200%22%2F%3E%3C%2Ffilter%3E%3ClinearGradient%20id%3D%22d%22%20x1%3D%2246.514%25%22%20x2%3D%2256.692%25%22%20y1%3D%2215.835%25%22%20y2%3D%2275.847%25%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23fff%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23fff%22%20stop-opacity%3D%22.601%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20id%3D%22a%22%20d%3D%22m40.34%2016.878.005.052%201.327%2014.35a2%202%200%200%201-1.754%202.17l-7.814.934-3.293%205.326a1%201%200%200%201-1.574.165l-4.207-4.407-8.113.969a2%202%200%200%201-2.228-1.802l-1.328-14.35a2%202%200%200%201%201.755-2.17l25-2.986a2%202%200%200%201%202.223%201.749%22%2F%3E%3C%2Fdefs%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%20transform%3D%22translate%28-9%20-14%29%22%3E%3Cuse%20xlink%3Ahref%3D%22%23a%22%20fill%3D%22%23000%22%20filter%3D%22url%28%23c%29%22%2F%3E%3Cuse%20xlink%3Ahref%3D%22%23a%22%20fill%3D%22url%28%23d%29%22%2F%3E%3Cuse%20xlink%3Ahref%3D%22%23a%22%20fill%3D%22%23000%22%20filter%3D%22url%28%23e%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")';
// State variables
let isOverlayVisible = false;
let chatHistory = [];
let isDragging = false;
let isResizing = false;
let markdownConverter = null; // Will be initialized when showdown loads
let currentStreamingDiv = null; // Tracks the current assistant message being streamed
let chatAboutQuestionEnabled = false; // Toggle for chat about question feature
let extractedQuestion = null; // Store extracted question
// Helper function to access elements in shadow DOM
function getShadowElement(id) {
const shadowHost = document.getElementById('chat-overlay-shadow-host');
if (!shadowHost || !shadowHost.shadowRoot) return null;
return shadowHost.shadowRoot.getElementById(id);
}
function getShadowRoot() {
const shadowHost = document.getElementById('chat-overlay-shadow-host');
return shadowHost ? shadowHost.shadowRoot : null;
}
function getChatButton() {
const buttonShadowHost = document.getElementById('chat-button-shadow-host');
if (!buttonShadowHost || !buttonShadowHost.shadowRoot) return null;
return buttonShadowHost.shadowRoot.getElementById('chat-button');
}
// Drag and resize state
let dragOffsetX;
let dragOffsetY;
let initialWidth;
let initialHeight;
let resizeStartX;
let resizeStartY;
const fontLink = document.createElement('link');
fontLink.href = 'https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap';
fontLink.rel = 'stylesheet';
document.head.appendChild(fontLink);
// Question extraction functions
function detectPlatform() {
// Check for Examly/IamNeo
if (document.querySelector('div[aria-labelledby="question-data"]')) {
return 'examly';
}
// Check for HackerRank
if (document.querySelector('.QuestionDetails_container__AIu0X') ||
document.querySelector('.monaco-editor') ||
document.querySelector('.grouped-mcq__question')) {
return 'hackerrank';
}
return null;
}
function extractExamlyQuestion() {
const questionElement = document.querySelector('div[aria-labelledby="question-data"]');
if (!questionElement) return null;
const questionText = questionElement.innerText.trim();
// Check if it's a coding question
const codingQuestionElement = document.querySelector('div[aria-labelledby="input-format"]');
if (codingQuestionElement) {
// Coding question
const programmingLanguageElement = document.querySelector('span.inner-text');
const programmingLanguage = programmingLanguageElement ? programmingLanguageElement.innerText.trim() : 'Programming language not found.';
const inputFormatElement = document.querySelector('div[aria-labelledby="input-format"]');
const inputFormatText = inputFormatElement ? inputFormatElement.innerText.trim() : '';
const outputFormatElement = document.querySelector('div[aria-labelledby="output-format"]');
const outputFormatText = outputFormatElement ? outputFormatElement.innerText.trim() : '';
const sampleTestCaseElements = document.querySelectorAll('div[aria-labelledby="each-tc-card"]');
let testCasesText = '';
sampleTestCaseElements.forEach((testCase, index) => {
const inputElement = testCase.querySelector('div[aria-labelledby="each-tc-input-container"] pre');
const outputElement = testCase.querySelector('div[aria-labelledby="each-tc-output-container"] pre');
const inputText = inputElement ? inputElement.innerText.trim() : 'Input not found';
const outputText = outputElement ? outputElement.innerText.trim() : 'Output not found';
testCasesText += `Sample Test Case ${index + 1}:\nInput:\n${inputText}\nOutput:\n${outputText}\n\n`;
});
return {
type: 'coding',
language: programmingLanguage,
question: questionText,
inputFormat: inputFormatText,
outputFormat: outputFormatText,
testCases: testCasesText
};
} else {
// MCQ question
const codeLines = [];
const codeElements = document.querySelectorAll('.ace_layer.ace_text-layer .ace_line');
codeElements.forEach(line => {
codeLines.push(line.innerText.trim());
});
const codeText = codeLines.length > 0 ? codeLines.join('\n') : null;
const optionsElements = document.querySelectorAll('div[aria-labelledby="each-option"]');
const optionsText = [];
optionsElements.forEach((option, index) => {
optionsText.push(`Option ${index + 1}: ${option.innerText.trim()}`);
});
return {
type: 'mcq',
question: questionText,
code: codeText,
options: optionsText.join('\n')
};
}
}
function extractHackerRankQuestion() {
const getCleanText = el => el?.innerText?.trim() || "";
// Check if it's a coding question (has Monaco editor)
const monacoEditor = document.querySelector('.monaco-editor, .hr-monaco-editor');
if (monacoEditor) {
// Coding question
let language = "Unknown";
let title = "No Title Found";
let instruction = "No Instructions Found";
let details = "";
const newLanguageSelector = document.querySelector('.select-language .css-3d4y2u-singleValue, .select-language .css-x7738g');
if (newLanguageSelector) {
language = getCleanText(newLanguageSelector);
} else {
language = getCleanText(document.querySelector('.select-language .css-x7738g')) || "Unknown";
}
let container = document.querySelector('.QuestionDetails_container__AIu0X');
if (container) {
const titleElement = container.querySelector('.qaas-block-question-title, h2');
if (titleElement) {
const titleText = titleElement.textContent || titleElement.innerText;
title = titleText.replace(/Bookmark question \d+/g, '').trim();
}
const instructionElement = container.querySelector('.qaas-block-question-instruction, .RichTextPreview_richText__1vKu5');
if (instructionElement) {
instruction = getCleanText(instructionElement);
}
const detailsElements = container.querySelectorAll('details');
if (detailsElements.length > 0) {
details = Array.from(detailsElements).map(detail => {
const summary = getCleanText(detail.querySelector('summary'));
const content = getCleanText(detail.querySelector('.collapsable-details'));
return `\n${summary}\n${'-'.repeat(summary.length)}\n${content}`;
}).join('\n');
}
} else {
container = document.querySelector('#main-splitpane-left');
if (container) {
title = getCleanText(container.querySelector('.question-view__title')) || "No Title Found";
instruction = getCleanText(container.querySelector('.question-view__instruction')) || "No Instructions Found";
details = Array.from(container.querySelectorAll('details') || []).map(detail => {
const summary = getCleanText(detail.querySelector('summary'));
const content = getCleanText(detail.querySelector('.collapsable-details'));
return `\n${summary}\n${'-'.repeat(summary.length)}\n${content}`;
}).join('\n');
}
}
return {
type: 'coding',
language: language,
title: title,
instruction: instruction,
details: details
};
} else {
// MCQ question
const newLayoutQuestions = document.querySelectorAll('.QuestionDetails_container__AIu0X');
if (newLayoutQuestions.length > 0) {
// New layout
const container = newLayoutQuestions[0]; // Get first question
let title = '';
let instruction = '';
let options = [];
const titleElement = container.querySelector('.qaas-block-question-title, h2');
if (titleElement) {
const titleText = titleElement.textContent || titleElement.innerText;
title = titleText.replace(/Bookmark question \d+/g, '').trim();
}
const instructionElement = container.querySelector('.qaas-block-question-instruction, .RichTextPreview_richText__1vKu5');
if (instructionElement) {
instruction = getCleanText(instructionElement);
}
let optionsContainer = container.nextElementSibling;
let attempts = 0;
while (optionsContainer && attempts < 5) {
const hasOptions = optionsContainer.querySelector('[role="checkbox"], [role="radio"]');
if (hasOptions) break;
optionsContainer = optionsContainer.nextElementSibling;
attempts++;
}
if (optionsContainer) {
let optionElements = optionsContainer.querySelectorAll('[role="radio"]');
if (optionElements.length === 0) {
optionElements = optionsContainer.querySelectorAll('[role="checkbox"]');
}
optionElements.forEach((option, index) => {
const labelId = option.getAttribute('aria-labelledby');
const labelElement = labelId ? document.getElementById(labelId) :
option.closest('.Control_optionList__vIubt, li')?.querySelector('label');
if (labelElement) {
options.push(`Option ${index + 1}: ${labelElement.textContent.trim()}`);
}
});
}
return {
type: 'mcq',
title: title,
instruction: instruction,
options: options.join('\n')
};
} else {
// Old layout
const oldLayoutQuestion = document.querySelector('.grouped-mcq__question');
if (oldLayoutQuestion) {
let title = '';
let instruction = '';
let options = [];
const titleElement = oldLayoutQuestion.querySelector('.question-view__title');
if (titleElement) {
title = titleElement.textContent.trim();
}
const instructionElement = oldLayoutQuestion.querySelector('.question-view__instruction');
if (instructionElement) {
instruction = instructionElement.textContent.trim();
}
const optionElements = oldLayoutQuestion.querySelectorAll('.ui-radio');
optionElements.forEach((option, index) => {
const labelElement = option.querySelector('.label');
if (labelElement) {
options.push(`Option ${index + 1}: ${labelElement.textContent.trim()}`);
}
});
return {
type: 'mcq',
title: title,
instruction: instruction,
options: options.join('\n')
};
}
}
}
return null;
}
function extractCurrentQuestion() {
const platform = detectPlatform();
if (platform === 'examly') {
return extractExamlyQuestion();
} else if (platform === 'hackerrank') {
return extractHackerRankQuestion();
}
return null;
}
function formatQuestionForChat(questionData) {
if (!questionData) return null;
let formattedQuestion = '';
if (questionData.type === 'coding') {
if (questionData.language) {
// Examly or HackerRank coding
formattedQuestion += `[Coding Question - ${questionData.language}]\n\n`;
if (questionData.title) {
formattedQuestion += `Title: ${questionData.title}\n\n`;
}
if (questionData.question) {
formattedQuestion += `Question:\n${questionData.question}\n\n`;
}
if (questionData.instruction) {
formattedQuestion += `Instruction:\n${questionData.instruction}\n\n`;
}
if (questionData.inputFormat) {
formattedQuestion += `Input Format:\n${questionData.inputFormat}\n\n`;
}
if (questionData.outputFormat) {
formattedQuestion += `Output Format:\n${questionData.outputFormat}\n\n`;
}
if (questionData.testCases) {
formattedQuestion += `Test Cases:\n${questionData.testCases}\n\n`;
}
if (questionData.details) {
formattedQuestion += `Additional Details:${questionData.details}\n\n`;
}
}
} else if (questionData.type === 'mcq') {
formattedQuestion += `[MCQ Question]\n\n`;
if (questionData.title) {
formattedQuestion += `Title: ${questionData.title}\n\n`;
}
if (questionData.question) {
formattedQuestion += `Question:\n${questionData.question}\n\n`;
}
if (questionData.instruction) {
formattedQuestion += `${questionData.instruction}\n\n`;
}
if (questionData.code) {
formattedQuestion += `Code:\n${questionData.code}\n\n`;
}
if (questionData.options) {
formattedQuestion += `Options:\n${questionData.options}\n`;
}
}
return formattedQuestion.trim();
}
// Create the main chat overlay UI
function createChatOverlay() {
// Check if shadow host already exists
let shadowHost = document.getElementById("chat-overlay-shadow-host");
if (shadowHost) {
return shadowHost.shadowRoot.querySelector("#chat-overlay");
}
// Create shadow host element
shadowHost = document.createElement("div");
shadowHost.id = "chat-overlay-shadow-host";
shadowHost.style.cssText = `
position: fixed;
bottom: 0;
right: 0;
z-index: 2147483647;
pointer-events: none;
`;
// Attach shadow root
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
const overlay = document.createElement("div");
overlay.id = "chat-overlay";
overlay.style.cssText = `
display: ${isOverlayVisible ? "flex" : "none"};
position: fixed;
bottom: 20px;
right: 20px;
width: 380px;
height: 500px;
background-color: #fff;
border: none;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
z-index: 2147483647;
flex-direction: column;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
transition: opacity 0.3s ease;
pointer-events: auto;
`;
// Create header
const header = document.createElement("div");
header.style.cssText = `
padding: 16px 20px !important;
font-weight: 500 !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
background-color: #fff !important;
color: #333 !important;
cursor: move !important;
`;
header.innerHTML = `
<div style="display: flex !important; flex-direction: column !important; align-items: flex-start !important; gap: 2px !important;">
<span style="display: flex !important; align-items: center !important; gap: 8px !important; font-size: 18px !important; font-weight: 700 !important; color: rgb(60, 84, 114) !important; opacity: 0.85 !important;">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
Chat
</span>
<span style="font-size: 12px !important; font-weight: 500 !important; color: #777 !important; margin-left: 30px !important;">
${window.isMac ? 'Option+C' : 'Alt+C'} to toggle
</span>
</div>
<div style="display: flex !important; gap: 14px !important; align-items: center !important;">
<span id="clear-chat" style="cursor: pointer !important; font-size: 14px !important; font-weight: 600 !important; color: rgb(220, 53, 69) !important; padding: 4px 8px !important; transition: all 0.2s ease !important;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='1'">Clear</span>
<span id="close-chat" style="cursor: pointer !important; font-size: 22px !important; line-height: 1 !important; color: #888 !important; transition: color 0.2s ease !important; font-weight: 500 !important;" onmouseover="this.style.color='#333'" onmouseout="this.style.color='#888'">×</span>
</div>
`;
// Create opacity slider container (Stealth mode control)
const sliderContainer = document.createElement("div");
sliderContainer.style.cssText = `
width: 100%;
height: 2px;
background-color: rgba(60, 84, 114, 0.1);
position: relative;
z-index: 10;
display: flex;
align-items: center;
`;
const opacitySlider = document.createElement("input");
opacitySlider.type = "range";
opacitySlider.min = "15";
opacitySlider.max = "100";
opacitySlider.value = "100";
opacitySlider.id = "opacity-slider";
opacitySlider.title = "Adjust opacity / Enable Stealth Mode";
sliderContainer.appendChild(opacitySlider);
// Create messages container
const messagesContainer = document.createElement("div");
messagesContainer.id = "chat-messages";
messagesContainer.style.cssText = `
padding: 20px;
flex: 1;
overflow-y: auto;
background-color: #fafafa;
color: #333;
scroll-behavior: smooth;
white-space: pre-wrap;
display: flex;
flex-direction: column;
gap: 12px;
`;
// Create input area
const inputArea = document.createElement("div");
inputArea.style.cssText = `
padding: 12px 16px 16px 16px;
background-color: #fff;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 10;
`;
// Create button container (which now acts as the pill wrapper)
const buttonContainer = document.createElement("div");
buttonContainer.style.cssText = `
display: flex;
align-items: stretch; /* Stretch children to fill height */
background-color: #f4f6f8;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 24px;
padding: 0; /* Remove all padding from container */
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.02);
gap: 0;
overflow: hidden; /* Ensures inner elements don't break the pill curve */
min-height: 44px;
`;
// Hover effect for the pill container
buttonContainer.addEventListener('mouseenter', () => {
buttonContainer.style.border = '1px solid rgba(60, 84, 114, 0.3)';
buttonContainer.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.05)';
});
buttonContainer.addEventListener('mouseleave', () => {
buttonContainer.style.border = '1px solid rgba(0, 0, 0, 0.08)';
buttonContainer.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.02)';
});
// Create input field with plain text only
const inputField = document.createElement("div");
inputField.contentEditable = "plaintext-only"; // Force plain text only
inputField.placeholder = "Message...";
inputField.style.cssText = `
flex: 1;
padding: 12px 12px 12px 16px; /* Put padding on the input instead */
border: none;
outline: none;
background-color: transparent;
color: #222;
font-family: 'Poppins', sans-serif;
font-size: 14px;
line-height: 1.5;
font-weight: 400;
min-height: 45px; /* Minimum height for 1 line */
max-height: 66px; /* Max height for exactly 2 lines (14px font * 1.5 line height * 2 + 24px padding = 66px) */
overflow-y: auto;
overflow-x: hidden;
white-space: pre-wrap;
word-wrap: break-word; /* Ensure text breaks into new lines */
-webkit-user-modify: read-write-plaintext-only;
display: block; /* Removed flex to allow proper text wrapping */
`;
// Simple paste event to ensure consistency (optional fallback)
inputField.addEventListener('paste', async function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
try {
let clipText = '';
// First try native clipboard (prioritize external app copies)
try {
clipText = await navigator.clipboard.readText();
console.log('[ChatBot Paste] Using native clipboard, length:', clipText.length);
} catch (err) {
console.log('[ChatBot Paste] Native clipboard read failed:', err.message);
}
// If empty, fall back to neoPassClipboard
if (!clipText && window.neoPassClipboard) {
clipText = window.neoPassClipboard;
console.log('[ChatBot Paste] Using neoPassClipboard, length:', clipText.length);
}
// Also try clipboardData from the paste event
if (!clipText && e.clipboardData) {
clipText = e.clipboardData.getData('text/plain');
console.log('[ChatBot Paste] Using event clipboardData, length:', clipText.length);
}
if (clipText) {
console.log('[ChatBot Paste] Attempting to insert text...');
let inserted = false;
// Try method 1: Use selection API
try {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
// Ensure the range is within our input field
if (this.contains(range.commonAncestorContainer)) {
range.deleteContents();
const textNode = document.createTextNode(clipText);
range.insertNode(textNode);
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
inserted = true;
console.log('[ChatBot Paste] Inserted using selection API');
}
}
} catch (selErr) {
console.log('[ChatBot Paste] Selection API failed:', selErr.message);
}
// Fallback method 2: Direct textContent manipulation
if (!inserted) {
console.log('[ChatBot Paste] Using fallback: direct insertion');
const currentText = this.textContent || '';
this.textContent = currentText + clipText;
// Move cursor to end
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(this);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
inserted = true;
}
if (inserted) {
// Dispatch input event to trigger any listeners
this.dispatchEvent(new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: clipText
}));
console.log('[ChatBot Paste] Paste successful');
}
}
// Clean any potential HTML that might slip through
setTimeout(() => {
if (this.children.length > 0) {
const text = this.textContent || this.innerText;
this.textContent = text;
}
}, 10);
} catch (err) {
console.error('[ChatBot Paste] Error:', err);
// Fallback: let browser handle it
setTimeout(() => {
if (this.children.length > 0) {
const text = this.textContent || this.innerText;
this.textContent = text;
}
}, 10);
}
}, true); // Use capture phase to intercept before document-level handlers
// Add Ctrl+V / Cmd+V handler for paste
inputField.addEventListener('keydown', async function(e) {
const ctrlKey = e.ctrlKey || e.metaKey; // Support both Ctrl (Windows/Linux) and Cmd (macOS)
// Handle Enter key for sending messages
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendButton.click();
return;
}
// Handle Ctrl+V / Cmd+V for paste
if (ctrlKey && (e.key === 'V' || e.key === 'v')) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
try {
let clipText = '';
// First try native clipboard (prioritize external app copies)
try {
clipText = await navigator.clipboard.readText();
console.log('[ChatBot Ctrl+V] Using native clipboard, length:', clipText.length);
} catch (err) {
console.log('[ChatBot Ctrl+V] Native clipboard read failed:', err.message);
}
// If empty, fall back to neoPassClipboard
if (!clipText && window.neoPassClipboard) {
clipText = window.neoPassClipboard;
console.log('[ChatBot Ctrl+V] Using neoPassClipboard, length:', clipText.length);
}
if (clipText) {
console.log('[ChatBot Ctrl+V] Attempting to insert text...');
let inserted = false;
// Try method 1: Use selection API
try {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
// Ensure the range is within our input field
if (this.contains(range.commonAncestorContainer)) {
range.deleteContents();
const textNode = document.createTextNode(clipText);
range.insertNode(textNode);
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
inserted = true;
console.log('[ChatBot Ctrl+V] Inserted using selection API');
}
}
} catch (selErr) {
console.log('[ChatBot Ctrl+V] Selection API failed:', selErr.message);
}
// Fallback method 2: Direct textContent manipulation
if (!inserted) {
console.log('[ChatBot Ctrl+V] Using fallback: direct insertion');
const currentText = this.textContent || '';
this.textContent = currentText + clipText;
// Move cursor to end
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(this);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
inserted = true;
}
if (inserted) {
// Dispatch input event to trigger any listeners
this.dispatchEvent(new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: clipText
}));
console.log('[ChatBot Ctrl+V] Paste successful');
}
} else {
console.log('[ChatBot Ctrl+V] No clipboard content available');
}
} catch (err) {
console.error('[ChatBot Ctrl+V] Error:', err);
}
}
}, true); // Use capture phase to intercept before document-level handlers
// Create checkbox container for "Chat about question"
const checkboxContainer = document.createElement("div");
checkboxContainer.style.cssText = `
display: none;
align-items: center;
gap: 8px;
padding: 4px 0;
`;
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = "chat-about-question-checkbox";
checkbox.style.cssText = `
width: 16px;
height: 16px;
cursor: pointer;
accent-color: rgb(60, 84, 114);
`;
const checkboxLabel = document.createElement("label");
checkboxLabel.htmlFor = "chat-about-question-checkbox";
checkboxLabel.style.cssText = `
font-family: 'Poppins', sans-serif;
font-size: 13px;
color: #666;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 6px;
`;
const questionIcon = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
`;
checkboxLabel.innerHTML = questionIcon + '<span>Chat about question</span>';
checkboxContainer.appendChild(checkbox);
checkboxContainer.appendChild(checkboxLabel);
// Store last question hash to detect question changes
let lastQuestionHash = null;
// Function to generate a simple hash from question data
function getQuestionHash(questionData) {
if (!questionData) return null;
// Create a unique string from the question data
let hashString = '';
if (questionData.type) hashString += questionData.type;
if (questionData.question) hashString += questionData.question;
if (questionData.title) hashString += questionData.title;
if (questionData.instruction) hashString += questionData.instruction;
// Simple hash function
let hash = 0;
for (let i = 0; i < hashString.length; i++) {
const char = hashString.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash;
}
// Function to check and update checkbox visibility based on platform detection
function updateCheckboxVisibility() {
const platform = detectPlatform();
if (platform) {
// Valid platform detected, show the checkbox
checkboxContainer.style.display = 'flex';
// If checkbox is enabled, check if question has changed
if (chatAboutQuestionEnabled && checkbox.checked) {
const currentQuestionData = extractCurrentQuestion();
const currentQuestionHash = getQuestionHash(currentQuestionData);
// If question hash changed, re-extract the question
if (currentQuestionHash !== lastQuestionHash && lastQuestionHash !== null) {
if (currentQuestionData) {
extractedQuestion = formatQuestionForChat(currentQuestionData);
lastQuestionHash = currentQuestionHash;
console.log('Question changed and re-extracted for chat');
// Clear chat history when question changes
clearChatHistoryAndUI('question-switch');
// Show notification that question was updated and chat cleared
addNotificationMessage('Question updated - Chat cleared');
} else {
// Question no longer available
checkbox.checked = false;
chatAboutQuestionEnabled = false;
extractedQuestion = null;
lastQuestionHash = null;
checkboxLabel.style.color = '#666';
checkboxLabel.style.fontWeight = '400';
addNotificationMessage('Question no longer detected');
}
}
}
} else {
// No valid platform, hide the checkbox and reset state
checkboxContainer.style.display = 'none';
checkbox.checked = false;
chatAboutQuestionEnabled = false;
extractedQuestion = null;
lastQuestionHash = null;
checkboxLabel.style.color = '#666';
checkboxLabel.style.fontWeight = '400';
}
}
// Initial check when overlay is created
updateCheckboxVisibility();
// Re-check periodically in case user navigates to a different page
setInterval(updateCheckboxVisibility, 2000);
// Handle checkbox change
checkbox.addEventListener('change', function() {
chatAboutQuestionEnabled = this.checked;
if (chatAboutQuestionEnabled) {
// Extract question when enabled
const questionData = extractCurrentQuestion();
if (questionData) {
extractedQuestion = formatQuestionForChat(questionData);
lastQuestionHash = getQuestionHash(questionData);
console.log('Question extracted for chat:', extractedQuestion);
// Update label to show question is attached
checkboxLabel.style.color = 'rgb(60, 84, 114)';
checkboxLabel.style.fontWeight = '500';
} else {
// No question found, disable checkbox
this.checked = false;
chatAboutQuestionEnabled = false;
extractedQuestion = null;
lastQuestionHash = null;
// Show notification
addNotificationMessage('No question detected on this page');
}
} else {
// Reset styles when disabled
checkboxLabel.style.color = '#666';
checkboxLabel.style.fontWeight = '400';
extractedQuestion = null;
lastQuestionHash = null;
}
});
// Create send button
const sendButton = document.createElement("button");
sendButton.innerHTML = "Send";
sendButton.style.cssText = `
padding: 0 20px 0 16px; /* Wider padding for text */
margin: 0;
background-color: rgb(60, 84, 114);
color: #fff;
border: none;
border-radius: 0; /* Let the container's overflow:hidden handle the curve */
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Poppins', sans-serif;
font-weight: 500;
font-size: 14px;
letter-spacing: 0.3px;
transition: all 0.2s ease;
flex-shrink: 0;
height: auto; /* Stretch to fill parent height */
box-shadow: -1px 0 3px rgba(0, 0, 0, 0.05); /* Very subtle separation */
`;
// Create resize handle
const resizeHandle = document.createElement("div");
resizeHandle.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 12px;
height: 12px;
background-color: rgb(60, 84, 114);
cursor: nw-resize;
border-radius: 12px 0 12px 0;
opacity: 0.8;
`;
// Add custom scrollbar styles and Prism theme overrides
const scrollbarStyles = document.createElement("style");
scrollbarStyles.innerHTML = `
${window._chatSyntaxHighlightCSS || ''}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
#chat-overlay ::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px;
transition: background-color 0.2s ease;
}
#chat-overlay ::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.3);
}
#chat-overlay ::-webkit-scrollbar-track {
background-color: transparent;
}
#chat-overlay [contenteditable]:empty:before {
content: attr(placeholder);
color: rgba(0, 0, 0, 0.4);
font-weight: 300;
}
/* Prism theme customizations for chat overlay */
#chat-overlay pre[class*="language-"] {
background: #f8f9fa !important;
border: 1px solid #e1e4e8 !important;
border-radius: 6px !important;
margin: 15px 0 !important;
padding: 12px !important;
overflow-x: auto !important;
}
#chat-overlay code[class*="language-"] {
background: transparent !important;
font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', Menlo, monospace !important;
font-size: 13px !important;
line-height: 1.4 !important;
color: #24292e !important;
}
/* Token colors for better readability */
#chat-overlay .token.comment,
#chat-overlay .token.prolog,
#chat-overlay .token.doctype,
#chat-overlay .token.cdata {
color: #6a737d !important;
}
#chat-overlay .token.punctuation {
color: #24292e !important;
}
#chat-overlay .token.property,
#chat-overlay .token.tag,
#chat-overlay .token.boolean,
#chat-overlay .token.number,
#chat-overlay .token.constant,
#chat-overlay .token.symbol,
#chat-overlay .token.deleted {
color: #005cc5 !important;
}
#chat-overlay .token.selector,
#chat-overlay .token.attr-name,
#chat-overlay .token.string,
#chat-overlay .token.char,
#chat-overlay .token.builtin,
#chat-overlay .token.inserted {
color: #032f62 !important;
}
#chat-overlay .token.operator,
#chat-overlay .token.entity,
#chat-overlay .token.url,
#chat-overlay .language-css .token.string,
#chat-overlay .style .token.string {
color: #e36209 !important;
}
#chat-overlay .token.atrule,
#chat-overlay .token.attr-value,
#chat-overlay .token.keyword {
color: #d73a49 !important;
}
#chat-overlay .token.function,
#chat-overlay .token.class-name {
color: #6f42c1 !important;
}
#chat-overlay .token.regex,
#chat-overlay .token.important,
#chat-overlay .token.variable {
color: #e36209 !important;
}
/* Remove any pseudo-elements that might cause overlay effects */
#chat-overlay pre[class*="language-"]:before,
#chat-overlay pre[class*="language-"]:after,
#chat-overlay code[class*="language-"]:before,
#chat-overlay code[class*="language-"]:after {
display: none !important;
}
/* Ensure no box-shadow or other effects */
#chat-overlay pre[class*="language-"] {
box-shadow: none !important;
text-shadow: none !important;
}
#chat-overlay code[class*="language-"] {
box-shadow: none !important;
text-shadow: none !important;
}
`;
// Assemble the components
buttonContainer.appendChild(inputField);
buttonContainer.appendChild(sendButton);
inputArea.appendChild(checkboxContainer);
inputArea.appendChild(buttonContainer);
// Create comprehensive CSS reset and styles for shadow DOM
const shadowStyles = document.createElement('style');
shadowStyles.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap');
/* CSS Reset for Shadow DOM */
* {
box-sizing: border-box;
}
#opacity-slider {
-webkit-appearance: none;
width: 100%;
height: 2px;
background: transparent;
outline: none;
margin: 0;
padding: 0;
}
#opacity-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 8px;
border-radius: 4px;
background: rgb(60, 84, 114);
cursor: pointer;
transition: transform 0.2s, background 0.2s;
}
#opacity-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
background: rgb(80, 104, 134);
}
#opacity-slider::-moz-range-thumb {
width: 16px;
height: 8px;
border-radius: 4px;
background: rgb(60, 84, 114);
cursor: pointer;
border: none;
transition: transform 0.2s, background 0.2s;
}
#opacity-slider::-moz-range-thumb:hover {
transform: scale(1.2);
background: rgb(80, 104, 134);
}
/* Re-apply base styles needed */
div, span, p {
display: block;
margin: 0;
padding: 0;
}
button {
cursor: pointer;
border: none;
background: none;
color: inherit;
font-family: 'Poppins', sans-serif;
}
input[type="checkbox"] {
cursor: pointer;
width: 16px;
height: 16px;
}
label {
cursor: pointer;
font-family: 'Poppins', sans-serif;
}
pre {
display: block;
margin: 0;
padding: 0;
font-family: monospace;
white-space: pre-wrap;
}
code {
font-family: monospace;
}
strong, b {
font-weight: bold;
}
em, i {
font-style: italic;
}
a {
color: #0066cc;
text-decoration: underline;
cursor: pointer;
}
ul, ol {
display: block;
margin: 10px 0;
padding-left: 20px;
}
li {
display: list-item;
margin: 5px 0;
}
p {
margin: 10px 0;
line-height: 1.5;
}
h1, h2, h3, h4, h5, h6 {
font-weight: bold;
margin: 15px 0 10px 0;
line-height: 1.3;
}
h1 { font-size: 2em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.3em; }
h4 { font-size: 1.1em; }
h5 { font-size: 1em; }
h6 { font-size: 0.9em; }
${scrollbarStyles.innerHTML}
`;
// Assemble the components in shadow DOM
shadowRoot.appendChild(shadowStyles);
overlay.appendChild(header);
overlay.appendChild(sliderContainer);
overlay.appendChild(messagesContainer);
overlay.appendChild(inputArea);
overlay.appendChild(resizeHandle);
shadowRoot.appendChild(overlay);
document.body.appendChild(shadowHost);
// Store shadow root reference for later access
shadowHost._shadowRoot = shadowRoot;
// Add placeholder behavior after element is in DOM
inputField.addEventListener('focus', function() {
if (this.textContent.trim() === '') {
this.setAttribute('data-placeholder', 'Type a message...');
}
});
inputField.addEventListener('blur', function() {
if (this.textContent.trim() === '') {
this.removeAttribute('data-placeholder');
}
});
// Add hover effect to send button
sendButton.addEventListener('mouseenter', () => {
sendButton.style.transform = 'translateY(-1px)';
sendButton.style.boxShadow = '0 4px 8px rgba(60, 84, 114, 0.3)';
});
sendButton.addEventListener('mouseleave', () => {
sendButton.style.transform = 'translateY(0)';
sendButton.style.boxShadow = '0 2px 4px rgba(60, 84, 114, 0.2)';
});
// Add event listeners for dragging
header.addEventListener("mousedown", (e) => {
isDragging = true;
dragOffsetX = e.clientX - overlay.getBoundingClientRect().left;
dragOffsetY = e.clientY - overlay.getBoundingClientRect().top;
});
// Add event listeners for stealth-mode
// Get the initial state from storage
chrome.storage.local.get(['stealth', 'stealthOpacity'], function(result) {
// Initialize stealth mode based on storage
let stealthModeEnabled = result.stealth === true;
let currentOpacity = result.stealthOpacity || (stealthModeEnabled ? 15 : 100);
const slider = shadowRoot.querySelector("#opacity-slider");
if (slider) {
slider.value = stealthModeEnabled ? currentOpacity : 100;
if (stealthModeEnabled) {
overlay.style.opacity = currentOpacity / 100;
} else {
overlay.style.opacity = "1";
}
slider.addEventListener("input", (e) => {
const val = parseInt(e.target.value);
overlay.style.opacity = val / 100;
});
slider.addEventListener("change", (e) => {
const val = parseInt(e.target.value);
const isStealth = val < 100;
// Only send notification if stealth mode STATE changed
if (isStealth !== stealthModeEnabled) {
stealthModeEnabled = isStealth;
const chatButton = getChatButton();
if (chatButton) {
chatButton.style.opacity = isStealth ? "0" : "1";
}
if (isStealth) {
chrome.runtime.sendMessage({
action: 'showStealthToast',
message: `Hover over the area where the chat icon is located \nor press ${window.isMac ? 'Option+C' : 'Alt+C'} to access [Chatbot opacity reduced]`,
stealthEnabled: true
});
} else {
chrome.runtime.sendMessage({
action: 'showStealthToast',
message: 'Chat icon is now visible',
stealthEnabled: false
});
}
}
chrome.storage.local.set({
stealth: isStealth,
stealthOpacity: val
});
});
}
// Listen for storage changes to update stealth mode state across all tabs
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'local' && slider) {
if (changes.stealthOpacity) {
currentOpacity = changes.stealthOpacity.newValue;
if (stealthModeEnabled) {
slider.value = currentOpacity;
if (overlay) overlay.style.opacity = currentOpacity / 100;
}
}
if (changes.stealth) {
const newStealthMode = changes.stealth.newValue === true;
stealthModeEnabled = newStealthMode;
if (newStealthMode) {
slider.value = currentOpacity;
if (overlay) overlay.style.opacity = currentOpacity / 100;
} else {
slider.value = 100;
if (overlay) overlay.style.opacity = "1";
}
// Update chat button visibility
const chatButton = getChatButton();
if (chatButton) {
chatButton.style.opacity = newStealthMode ? "0" : "1";
chatButton.style.pointerEvents = "auto";
}
}
}
});
});
// Add event listeners for resizing
// Add minimum size constants at the top with the other state variables
const MIN_WIDTH = 250; // Minimum width in pixels
const MIN_HEIGHT = 200; // Minimum height in pixels
const MAX_WIDTH = window.innerWidth - 40; // Maximum width (leaving 20px padding on each side)
const MAX_HEIGHT = window.innerHeight - 40; // Maximum height (leaving 20px padding on each side)
// Replace the resize event listener section with this updated version
resizeHandle.addEventListener("mousedown", (e) => {
isResizing = true;
resizeStartX = e.clientX;
resizeStartY = e.clientY;
initialWidth = overlay.offsetWidth;
initialHeight = overlay.offsetHeight;
e.stopPropagation(); // Prevent dragging when resizing
});
resizeHandle.addEventListener("mouseenter", () => {
resizeHandle.style.opacity = "1";
});
resizeHandle.addEventListener("mouseleave", () => {
resizeHandle.style.opacity = "0.8";
});
// Update the mousemove event listener to include size constraints
// This should be outside the createChatOverlay function as it's document level
// Add window resize handler to keep overlay within bounds
window.addEventListener('resize', () => {
const overlay = getShadowElement('chat-overlay');
if (overlay) {
const rect = overlay.getBoundingClientRect();
// Update maximum constraints
const newMaxWidth = window.innerWidth - 40;
const newMaxHeight = window.innerHeight - 40;
// Adjust size if necessary
if (rect.width > newMaxWidth) {
overlay.style.width = newMaxWidth + 'px';
}
if (rect.height > newMaxHeight) {
overlay.style.height = newMaxHeight + 'px';
}
// Keep overlay within viewport
if (rect.right > window.innerWidth) {
overlay.style.left = (window.innerWidth - rect.width) + "px";
}
if (rect.bottom > window.innerHeight) {
overlay.style.top = (window.innerHeight - rect.height) + "px";
}
}
});
// Add button event listeners
const closeButton = header.querySelector("#close-chat");
if (closeButton) {
closeButton.addEventListener("click", () => {
isOverlayVisible = false;
overlay.style.display = "none";
});
}
const clearChatButton = header.querySelector("#clear-chat");
if (clearChatButton) {
clearChatButton.addEventListener("click", () => {
clearChatHistoryAndUI('manual');
});
}
// Handle message sending
sendButton.addEventListener("click", async () => {
const message = inputField.innerText.trim();
if (message) {
try {
// Clear any error state before sending new message
clearErrorState();
// Prepare the final message to send
let finalMessage = message;
// If "Chat about question" is enabled, prepend the question
if (chatAboutQuestionEnabled && extractedQuestion) {
finalMessage = `Context: Below is the question I'm working on:\n\n${extractedQuestion}\n\n---\n\nMy Question: ${message}`;
console.log('Sending message with question context');
}
chatHistory.push({
role: "user",
content: message
});
addMessageToChat(message, "user");
inputField.innerText = "";
// Add enhanced loading indicator
const loadingDiv = addLoadingIndicator();
messagesContainer.appendChild(loadingDiv);
// Send message and wait for response with timeout
const response = await new Promise((resolve, reject) => {
let timeoutId;
let resolved = false;
// Set up timeout (30 seconds)
timeoutId = setTimeout(() => {
if (!resolved) {
resolved = true;
reject(new Error('Request timed out. Please try again.'));
}
}, 30000);
// Listen for response
const messageListener = (message) => {
if (message.action === "updateChatHistory" && !resolved) {
resolved = true;
clearTimeout(timeoutId);
chrome.runtime.onMessage.removeListener(messageListener);
resolve(message);
}
};
chrome.runtime.onMessage.addListener(messageListener);
// Send the message (with question context if enabled)
// Create valid conversation context (filters errors and ensures proper role flow)
const validContext = createValidContext(chatHistory);
chrome.runtime.sendMessage({
action: "processChatMessage",
message: finalMessage, // Send the final message with or without question context
context: validContext
}).catch((error) => {
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
chrome.runtime.onMessage.removeListener(messageListener);
reject(error);
}
});
});
// Remove loading indicator
const loadingMessage = getShadowElement("loading-message");
if (loadingMessage) {
loadingMessage.remove();
}
// The response will be handled by the runtime message listener
// No need to add the message here as it will be added via "updateChatHistory"
}
catch (error) {
console.error("Error sending message:", error);
// Remove loading indicator if it exists
const loadingMessage = getShadowElement("loading-message");
if (loadingMessage) {
loadingMessage.remove();
}
// Handle different types of errors with appropriate messages
let errorMessage = "I encountered an error processing your message. Please try again.";
let isRateLimitError = false;
if (error.message) {
if (error.message.includes('timeout') || error.message.includes('timed out')) {
errorMessage = "The request timed out. The service might be experiencing high load. Please try again in a moment.";
} else if (error.message.includes('rate limit') || error.message.includes('Daily request limit')) {
errorMessage = "You've reached your daily chat limit. Please try again tomorrow.";
isRateLimitError = true;
} else if (error.message.includes('Network') || error.message.includes('connection')) {
errorMessage = "Unable to connect to the chat service. Please check your internet connection and try again.";
} else if (error.message.includes('login') || error.message.includes('authentication')) {
errorMessage = "Please log in to use the chat feature. Click the extension icon to log in.";
} else {
// Use the error message if it's user-friendly
errorMessage = error.message;
}
}
// Add error message to chat with special styling
addErrorMessageToChat(errorMessage, isRateLimitError);
}
}
});
return overlay;
}
// Add notification message function
function addNotificationMessage(message) {
const messagesContainer = getShadowElement("chat-messages");
if (!messagesContainer) return;
const messageDiv = document.createElement("div");
messageDiv.textContent = message;
messageDiv.style.cssText = `
margin: 12px auto;
padding: 6px 12px;
background-color: rgba(60, 84, 114, 0.08);
border-radius: 12px;
color: rgb(60, 84, 114);
font-size: 11px;
text-align: center;
font-family: 'Poppins', sans-serif;
font-weight: 500;
letter-spacing: 0.2px;
width: fit-content;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
`;
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Create the chat button
function createChatButton() {
// Check if shadow host for button already exists
let buttonShadowHost = document.getElementById("chat-button-shadow-host");
if (buttonShadowHost) {
return buttonShadowHost.shadowRoot.querySelector("#chat-button");
}
// Create shadow host element for button
buttonShadowHost = document.createElement("div");
buttonShadowHost.id = "chat-button-shadow-host";
buttonShadowHost.style.cssText = `
position: fixed;
bottom: 0;
right: 0;
z-index: 2147483647;
pointer-events: none;
`;
// Attach shadow root
const buttonShadowRoot = buttonShadowHost.attachShadow({ mode: 'open' });
// Create comprehensive CSS reset for button shadow DOM
const buttonStyles = document.createElement('style');
buttonStyles.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap');
/* CSS Reset for Button Shadow DOM */
* {
box-sizing: border-box;
}
button {
display: block;
cursor: pointer;
border: none;
padding: 0;
margin: 0;
background: none;
outline: none;
font-family: 'Poppins', sans-serif;
position: relative;
}
.chat-icon-span {
display: block;
position: absolute;
top: 14px;
right: 10px;
left: 9px;
bottom: 10px;
width: 35px;
height: 30px;
background-image: ${CHAT_ICON_SVG_URL};
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: contain;
pointer-events: none;
user-select: none;
z-index: 2;
}
`;
const button = document.createElement("button");
button.id = "chat-button";
button.style.cssText = `
display: block;
position: fixed;
bottom: 20px;
right: 20px;
width: 54px;
height: 54px;
background-color: rgb(60, 84, 114);
border: none;
border-radius: 100%;
color: #fff;
cursor: pointer;
z-index: 2147483647;
box-shadow: rgba(0, 0, 0, 0.05) 0px 4px 10px 0px;
transition: background-color 0.1s linear, outline 0.15s ease-in-out, transform 0.15s ease-in-out;
pointer-events: auto;
padding: 0;
margin: 0;
outline: solid 0px rgba(0, 0, 0, 0);
user-select: none;
`;
// Chat bubble icon as background-image on child span (matching Crisp style)
const iconSpan = document.createElement("span");
iconSpan.className = "chat-icon-span";
button.appendChild(iconSpan);
// Assemble button in shadow DOM
buttonShadowRoot.appendChild(buttonStyles);
buttonShadowRoot.appendChild(button);
document.body.appendChild(buttonShadowHost);
// Add hover effects for stealth mode
button.addEventListener('mouseenter', () => {
chrome.storage.local.get(['stealth'], function(result) {
const stealthModeEnabled = result.stealth === true;
if (stealthModeEnabled) {
button.style.opacity = "0.3"; // Show with reduced opacity on hover in stealth mode
}
});
});
button.addEventListener('mouseleave', () => {
chrome.storage.local.get(['stealth'], function(result) {
const stealthModeEnabled = result.stealth === true;
if (stealthModeEnabled) {
button.style.opacity = "0"; // Hide again when not hovering in stealth mode
}
});
});
let dragStartX, dragStartY, initialX, initialY;
let isDraggingButton = false;
let hasMoved = false;
// Handle button dragging with improved click detection
button.addEventListener("mousedown", (e) => {
isDraggingButton = true;
hasMoved = false;
dragStartX = e.clientX;
dragStartY = e.clientY;
initialX = button.getBoundingClientRect().left;
initialY = button.getBoundingClientRect().top;
});
document.addEventListener("mousemove", (e) => {
if (isDraggingButton) {
const deltaX = e.clientX - dragStartX;
const deltaY = e.clientY - dragStartY;
// Check if the button has moved more than 5 pixels in any direction
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
hasMoved = true;
}
const newX = initialX + deltaX;
const newY = initialY + deltaY;
// Keep button within viewport bounds
const maxX = window.innerWidth - button.offsetWidth;
const maxY = window.innerHeight - button.offsetHeight;
button.style.left = Math.min(Math.max(0, newX), maxX) + "px";
button.style.top = Math.min(Math.max(0, newY), maxY) + "px";
button.style.bottom = "auto";
button.style.right = "auto";
}
});
document.addEventListener("mouseup", () => {
if (isDraggingButton) {
isDraggingButton = false;
// Only trigger click if the button hasn't moved
if (!hasMoved) {
toggleChatOverlay();
}
}
});
// Remove double click handler and use single click with movement detection
button.addEventListener("click", (e) => {
// Click handling is now managed in the mouseup event
e.preventDefault();
});
return button;
}
// Helper function to detect programming language from code content
function detectLanguage(code) {
const codeText = code.toLowerCase().trim();
// TypeScript detection (check before JavaScript)
if (codeText.includes('interface ') || codeText.includes('type ') || codeText.includes(': string') ||
codeText.includes(': number') || codeText.includes(': boolean') || codeText.includes('export interface') ||
codeText.includes('import type') || codeText.includes('as const') || codeText.includes('enum ')) {
return 'typescript';
}
// JSX/TSX detection
if (codeText.includes('<') && codeText.includes('>') &&
(codeText.includes('return (') || codeText.includes('jsx') || codeText.includes('tsx') ||
codeText.includes('component') || codeText.includes('props'))) {
return codeText.includes(': ') ? 'tsx' : 'jsx';
}
// JavaScript detection
if (codeText.includes('function') || codeText.includes('const ') || codeText.includes('let ') ||
codeText.includes('var ') || codeText.includes('=>') || codeText.includes('console.log') ||
codeText.includes('document.') || codeText.includes('window.') || codeText.includes('require(') ||
codeText.includes('import ') || codeText.includes('export ')) {
return 'javascript';
}
// Python detection
if (codeText.includes('def ') || codeText.includes('import ') || codeText.includes('from ') ||
codeText.includes('print(') || codeText.includes('if __name__') || codeText.includes('self.') ||
codeText.includes('class ') || codeText.includes('elif ') || codeText.includes('range(') ||
codeText.includes('lambda ') || codeText.includes('yield ')) {
return 'python';
}
// Java detection
if (codeText.includes('public class') || codeText.includes('private ') || codeText.includes('public ') ||
codeText.includes('import java') || codeText.includes('system.out.println') || codeText.includes('string ') ||
codeText.includes('void main') || codeText.includes('extends ') || codeText.includes('implements ') ||
codeText.includes('@override') || codeText.includes('new ')) {
return 'java';
}
// C# detection
if (codeText.includes('using system') || codeText.includes('namespace ') || codeText.includes('public static void main') ||
codeText.includes('console.writeline') || codeText.includes('[attribute]') || codeText.includes('var ')) {
return 'csharp';
}
// C++ detection (check before C)
if (codeText.includes('std::') || codeText.includes('cout <<') || codeText.includes('cin >>') ||
codeText.includes('#include <iostream>') || codeText.includes('using namespace std') ||
codeText.includes('class ') || codeText.includes('template<')) {
return 'cpp';
}
// C detection
if (codeText.includes('#include') || codeText.includes('printf(') || codeText.includes('scanf(') ||
codeText.includes('int main') || codeText.includes('malloc(') || codeText.includes('free(') ||
codeText.includes('sizeof(')) {
return 'c';
}
// PHP detection
if (codeText.includes('<?php') || codeText.includes('echo ') || codeText.includes('{{contextString}}#39;) ||
codeText.includes('function ') || codeText.includes('class ') || codeText.includes('->')) {
return 'php';
}
// Ruby detection
if (codeText.includes('def ') || codeText.includes('end') || codeText.includes('puts ') ||
codeText.includes('require ') || codeText.includes('class ') || codeText.includes('@')) {
return 'ruby';
}
// Go detection
if (codeText.includes('package ') || codeText.includes('func ') || codeText.includes('import (') ||
codeText.includes('fmt.println') || codeText.includes('go ') || codeText.includes('defer ')) {
return 'go';
}
// Rust detection
if (codeText.includes('fn ') || codeText.includes('let mut') || codeText.includes('println!') ||
codeText.includes('use ') || codeText.includes('struct ') || codeText.includes('impl ')) {
return 'rust';
}
// Swift detection
if (codeText.includes('import swift') || codeText.includes('var ') || codeText.includes('let ') ||
codeText.includes('func ') || codeText.includes('class ') || codeText.includes('print(')) {
return 'swift';
}
// Kotlin detection
if (codeText.includes('fun ') || codeText.includes('val ') || codeText.includes('var ') ||
codeText.includes('class ') || codeText.includes('println(') || codeText.includes('import kotlin')) {
return 'kotlin';
}
// HTML detection
if (codeText.includes('<!doctype') || codeText.includes('<html') || codeText.includes('<head') ||
codeText.includes('<body') || codeText.includes('<div') || codeText.includes('<span') ||
codeText.includes('<script') || codeText.includes('<style')) {
return 'html';
}
// CSS/SCSS detection
if (codeText.includes('{') && codeText.includes('}') && (codeText.includes(':') && codeText.includes(';'))) {
if (codeText.includes('{{contextString}}#39;) || codeText.includes('@mixin') || codeText.includes('@include')) {
return 'scss';
}
return 'css';
}
// SQL detection
if (codeText.includes('select ') || codeText.includes('from ') || codeText.includes('where ') ||
codeText.includes('insert ') || codeText.includes('update ') || codeText.includes('delete ') ||
codeText.includes('create table') || codeText.includes('alter table') || codeText.includes('drop table')) {
return 'sql';
}
// JSON detection
if ((codeText.trim().startsWith('{') && codeText.trim().endsWith('}')) ||
(codeText.trim().startsWith('[') && codeText.trim().endsWith(']'))) {
try {
JSON.parse(code);
return 'json';
} catch (e) {
// Not valid JSON, continue with other detections
}
}
// YAML detection
if (codeText.includes('---') || (codeText.includes(':') && !codeText.includes(';') && !codeText.includes('{')) ||
codeText.includes('- ') || codeText.includes('version:') || codeText.includes('name:')) {
return 'yaml';
}
// XML detection
if (codeText.includes('<?xml') || codeText.includes('<') && codeText.includes('/>') ||
(codeText.includes('<') && codeText.includes('>') && !codeText.includes('function'))) {
return 'xml';
}
// Bash/Shell detection
if (codeText.includes('#!/bin/bash') || codeText.includes('#!/bin/sh') ||
codeText.includes('echo ') || codeText.includes('grep ') || codeText.includes('awk ') ||
codeText.includes('sed ') || codeText.includes('chmod ') || codeText.includes('sudo ') ||
codeText.includes('ls ') || codeText.includes('cd ') || codeText.includes('mkdir ')) {
return 'bash';
}
// Default fallback
return 'javascript';
}
// Render content (for initial or streaming updates)
function renderChatContent(messageContainer, content) {
try {
// Convert markdown to HTML using showdown library
if (typeof showdown !== 'undefined') {
// Initialize markdown converter if not already done
if (!markdownConverter) {
markdownConverter = new showdown.Converter();
}
const htmlContent = markdownConverter.makeHtml(content);
// Clear and set new content
messageContainer.innerHTML = "";
const contentContainer = document.createElement("div");
contentContainer.innerHTML = htmlContent;
// Style code blocks and add copy functionality
contentContainer.querySelectorAll("pre code").forEach(codeBlock => {
// Detect language from class name first (from markdown \`\`\`language)
let language = '';
const classNames = codeBlock.className.split(' ');
for (const className of classNames) {
if (className.startsWith('language-')) {
language = className.replace('language-', '');
break;
}
}
// If no language specified in markdown, use auto-detection
if (!language || language === '') {
language = detectLanguage(codeBlock.textContent);
}
// Set the language class for Prism (ensure it's set even if detected)
codeBlock.className = `language-${language}`;
// Apply SimplePrism highlighting if available
if (typeof SimplePrism !== 'undefined') {
try {
SimplePrism.highlightElement(codeBlock);
} catch (error) {
console.warn('Failed to highlight code block:', error);
// Continue without highlighting
}
}
// Style the parent <pre> element to ensure clean background
const preElement = codeBlock.parentNode;
if (preElement && preElement.tagName === 'PRE') {
preElement.style.cssText = `
background: #f8f9fa !important;
border: 1px solid #e1e4e8 !important;
border-radius: 6px !important;
margin: 15px 0 !important;
padding: 0 !important;
overflow: visible !important;
position: relative !important;
`;
}
// Style the code block (let Prism handle syntax colors)
codeBlock.style.cssText = `
background: transparent !important;
border: none !important;
border-radius: 0 !important;
padding: 12px !important;
display: block !important;
margin: 0 !important;
overflow-x: auto !important;
white-space: pre !important;
font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', Menlo, monospace !important;
font-size: 13px !important;
line-height: 1.4 !important;
`;
// Create a wrapper for the code block to handle hover events
const codeWrapper = document.createElement("div");
codeWrapper.style.cssText = `
position: relative;
background: transparent;
border: none;
margin: 0;
padding: 0;
`;
// Move the code block into the wrapper
codeBlock.parentNode.insertBefore(codeWrapper, codeBlock);
codeWrapper.appendChild(codeBlock);
// Create copy button with new styling
const copyButton = document.createElement("button");
copyButton.innerText = "Copy";
copyButton.style.cssText = `
position: absolute;
right: 8px;
top: 8px;
background-color: rgb(60, 84, 114);
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
padding: 6px 12px;
font-size: 12px;
font-family: 'Poppins', sans-serif;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 10;
`;
// Add hover effects
codeWrapper.addEventListener('mouseenter', () => {
copyButton.style.opacity = "1";
});
codeWrapper.addEventListener('mouseleave', () => {
copyButton.style.opacity = "0";
});
// Add copy functionality
copyButton.addEventListener("click", () => {
navigator.clipboard.writeText(codeBlock.innerText)
.then(() => {
copyButton.innerText = "Copied";
setTimeout(() => {
copyButton.innerText = "Copy";
}, 5000);
})
.catch(error => {
console.error("Failed to copy: ", error);
});
});
// Add the copy button to the wrapper
codeWrapper.appendChild(copyButton);
});
// Add the content to the message container
messageContainer.appendChild(contentContainer);
} else {
// Fallback for when showdown is not available
messageContainer.textContent = content;
}
} catch (error) {
console.error('Error rendering chat content:', error);
// Fallback to plain text
messageContainer.textContent = content;
}
}
// Add message to chat
function addMessageToChat(message, role) {
// Get the chat messages container
const chatMessagesContainer = getShadowElement("chat-messages");
if (!chatMessagesContainer) return;
// Create a new message container
const messageContainer = document.createElement("div");
messageContainer.style.cssText = `
margin-bottom: 12px;
padding: 12px 16px;
border-radius: 16px;
max-width: 85%;
width: fit-content;
word-wrap: break-word;
font-size: 14px;
line-height: 1.5;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
`;
// Style the message differently based on the role (user or assistant)
if (role === "user") {
messageContainer.style.backgroundColor = "rgb(60, 84, 114)"; // User messages use blue
messageContainer.style.color = "#ffffff";
messageContainer.style.alignSelf = "flex-end";
messageContainer.style.borderBottomRightRadius = "4px";
} else {
messageContainer.style.backgroundColor = "#ffffff"; // Assistant messages use white/subtle grey
messageContainer.style.color = "#333333";
messageContainer.style.alignSelf = "flex-start";
messageContainer.style.border = "1px solid #eaeaea";
messageContainer.style.borderBottomLeftRadius = "4px";
}
// Add the message to the chat
chatMessagesContainer.appendChild(messageContainer);
// Render initial content
renderChatContent(messageContainer, message);
// Scroll to bottom
chatMessagesContainer.scrollTop = chatMessagesContainer.scrollHeight;
return messageContainer;
}
// Function to clear error state and remove error messages from chat history
function clearErrorState() {
// Remove error messages from chat history (in case any slipped through)
chatHistory = chatHistory.filter(msg => msg.role !== "error");
// Optionally clear error messages from UI after successful response
// This helps provide a cleaner experience when the user resolves their issue
// We keep them for now to maintain transparency, but you could uncomment below to remove them:
/*
const chatMessagesContainer = document.getElementById("chat-messages");
if (chatMessagesContainer) {
const errorMessages = chatMessagesContainer.querySelectorAll('[style*="f8d7da"], [style*="fff3cd"]');
errorMessages.forEach(errorMsg => errorMsg.remove());
}
*/
}
// Function to create valid conversation context for the API
function createValidContext(chatHistory) {
// First filter out error messages
let filteredHistory = chatHistory.filter(msg => msg.role !== "error");
// Ensure valid conversation flow (alternating user/assistant roles)
let validContext = [];
let lastRole = null;
for (const message of filteredHistory) {
// Skip consecutive messages with the same role (except the first)
if (lastRole === message.role) {
// If we have consecutive user messages, skip the earlier one
// If we have consecutive assistant messages, skip the earlier one
if (validContext.length > 0) {
validContext.pop(); // Remove the previous message of the same role
}
}
validContext.push(message);
lastRole = message.role;
}
// Ensure the conversation doesn't end with an assistant message if we're about to add a user message
// The API expects user -> assistant -> user flow
if (validContext.length > 0 && validContext[validContext.length - 1].role === "assistant") {
// This is fine, we can add a user message next
} else if (validContext.length > 0 && validContext[validContext.length - 1].role === "user") {
// We have a trailing user message, which is fine since we're about to send another user message
// But we should remove the trailing user message to avoid consecutive user messages
validContext.pop();
}
return validContext;
}
// Add error message to chat with special styling
function addErrorMessageToChat(errorMessage, isRateLimitError = false) {
const chatMessagesContainer = getShadowElement("chat-messages");
if (!chatMessagesContainer) return;
// Create error message container
const errorContainer = document.createElement("div");
errorContainer.style.cssText = `
margin-bottom: 12px;
padding: 12px 16px;
border-radius: 8px;
max-width: 95%;
word-wrap: break-word;
background-color: ${isRateLimitError ? '#fff3cd' : '#f8d7da'};
border: 1px solid ${isRateLimitError ? '#ffeaa7' : '#f5c6cb'};
color: ${isRateLimitError ? '#856404' : '#721c24'};
align-self: flex-start;
font-family: 'Poppins', sans-serif;
position: relative;
`;
// Add error icon and message
const errorContent = document.createElement("div");
errorContent.style.cssText = `
display: flex;
align-items: flex-start;
gap: 10px;
`;
// Error icon
const errorIcon = document.createElement("div");
errorIcon.innerHTML = isRateLimitError ?
`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>` :
`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>`;
errorIcon.style.cssText = `
flex-shrink: 0;
margin-top: 2px;
opacity: 0.8;
`;
// Error text
const errorText = document.createElement("div");
errorText.style.cssText = `
flex-grow: 1;
font-size: 14px;
line-height: 1.4;
`;
errorText.textContent = errorMessage;
// Add retry suggestion for certain errors
if (!isRateLimitError && !errorMessage.includes('log in')) {
const retryText = document.createElement("div");
retryText.style.cssText = `
margin-top: 8px;
font-size: 12px;
opacity: 0.8;
font-style: italic;
`;
retryText.textContent = "You can try sending your message again.";
errorText.appendChild(retryText);
}
errorContent.appendChild(errorIcon);
errorContent.appendChild(errorText);
errorContainer.appendChild(errorContent);
// Note: Don't add error messages to chatHistory to prevent them from being sent as context
// This prevents error states from persisting across requests
// Add to chat and scroll
chatMessagesContainer.appendChild(errorContainer);
chatMessagesContainer.scrollTop = chatMessagesContainer.scrollHeight;
}
// Add this new loading indicator function
function addLoadingIndicator() {
const loadingDiv = document.createElement("div");
loadingDiv.id = "loading-message";
loadingDiv.style.cssText = `
margin-bottom: 16px;
padding: 14px 16px;
border-radius: 14px;
background-color: #fff;
align-self: flex-start;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
gap: 8px;
font-family: 'Poppins', sans-serif;
font-size: 14px;
color: rgba(0, 0, 0, 0.6);
`;
// Add typing animation dots
const dotsContainer = document.createElement("div");
dotsContainer.style.cssText = `
display: flex;
gap: 4px;
margin-left: 4px;
`;
for (let i = 0; i < 3; i++) {
const dot = document.createElement("div");
dot.style.cssText = `
width: 6px;
height: 6px;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 50%;
animation: typingAnimation 1.4s infinite;
animation-delay: ${i * 0.2}s;
`;
dotsContainer.appendChild(dot);
}
loadingDiv.textContent = "Thinking";
loadingDiv.appendChild(dotsContainer);
// No need to add keyframes - they're already in shadow DOM styles
return loadingDiv;
}
// Function to toggle chat overlay visibility
function toggleChatOverlay() {
isOverlayVisible = !isOverlayVisible;
const shadowHost = document.getElementById("chat-overlay-shadow-host");
let chatOverlay = shadowHost ? shadowHost.shadowRoot.querySelector("#chat-overlay") : null;
if (!chatOverlay) {
chatOverlay = createChatOverlay(); // Creates shadow host and returns overlay
}
if (chatOverlay) {
chatOverlay.style.display = isOverlayVisible ? "flex" : "none";
// Focus on input field when showing overlay
if (isOverlayVisible) {
setTimeout(() => {
const inputField = getShadowRoot()?.querySelector('[contenteditable]');
if (inputField) {
inputField.focus();
// Place cursor at the end of existing text
const range = document.createRange();
const sel = window.getSelection();
// If there's content, move cursor to the end
if (inputField.childNodes.length > 0) {
range.setStart(inputField.childNodes[inputField.childNodes.length - 1],
inputField.childNodes[inputField.childNodes.length - 1].length || 0);
} else {
range.setStart(inputField, 0);
}
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}, 100);
}
}
}
// Function to clear chat history and UI (reusable)
function clearChatHistoryAndUI(reason = 'manual') {
try {
const messagesContainer = getShadowElement("chat-messages");
if (messagesContainer) {
// Clear the chat history array
chatHistory = [];
// Clear the UI
messagesContainer.innerHTML = "";
// Clear any error state
clearErrorState();
// Send message to background script to reset context
chrome.runtime.sendMessage({
action: "resetContext"
});
// Add a notification message based on the reason
let notificationMessage = "Chat history cleared.";
if (reason === 'providerChange') {
notificationMessage = "Chat history cleared - switched to new AI provider.";
}
addNotificationMessage(notificationMessage);
console.log(`Chat history cleared (${reason})`);
}
} catch (error) {
console.error('Error clearing chat history:', error);
}
}
// Function to detect and block clashing chat elements
function blockClashingChatElements() {
// List of class patterns to block (updated class names)
const blockedClassPatterns = [
'cc-1m2mf', // Old class
'cc-1qbp0', // New duplicate chatbot icon
'cc-1o31k', // New duplicate chatbot icon child
'cc-otlyh', // New duplicate chatbot icon child
'cc-11f3x', // New duplicate chatbot icon child
'cc-1v4wj' // New duplicate chatbot icon child
];
// Function to hide elements matching any of the blocked patterns
function hideBlockedElements() {
blockedClassPatterns.forEach(className => {
// Match elements with the exact class or classes containing this pattern
const selector = `[class*="${className}"]`;
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
// Only hide if it's not part of our chat overlay
if (!element.closest('#chat-overlay')) {
element.style.display = 'none';
}
});
});
}
// Add observer to continuously check for and block the element
const observer = new MutationObserver((mutations) => {
hideBlockedElements();
});
// Start observing document body for changes
observer.observe(document.body, {
childList: true,
subtree: true
});
// Also try to block any existing elements immediately
hideBlockedElements();
// Add CSS to ensure elements with these classes are always hidden
const styleElement = document.createElement('style');
const cssRules = blockedClassPatterns.map(className => `
[class*="${className}"]:not(#chat-overlay):not(#chat-overlay *) {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
`).join('\n');
styleElement.textContent = cssRules;
document.head.appendChild(styleElement);
}
// Set up document-level event handlers
document.addEventListener("mousemove", (e) => {
const shadowHost = document.getElementById("chat-overlay-shadow-host");
if (!shadowHost) return;
const overlay = shadowHost.shadowRoot?.querySelector("#chat-overlay");
if (!overlay) return;
if (isDragging) {
const newLeft = e.clientX - dragOffsetX;
const newTop = e.clientY - dragOffsetY;
// Prevent dragging outside viewport
const maxX = window.innerWidth - overlay.offsetWidth;
const maxY = window.innerHeight - overlay.offsetHeight;
overlay.style.left = Math.min(Math.max(0, newLeft), maxX) + "px";
overlay.style.top = Math.min(Math.max(0, newTop), maxY) + "px";
overlay.style.bottom = "auto";
overlay.style.right = "auto";
}
if (isResizing) {
const resizeHandle = overlay.querySelector("div[style*='nw-resize']");
if (!resizeHandle) return;
const MIN_WIDTH = 250;
const MIN_HEIGHT = 200;
const MAX_WIDTH = window.innerWidth - 40;
const MAX_HEIGHT = window.innerHeight - 40;
const dx = resizeStartX - e.clientX;
const dy = resizeStartY - e.clientY;
const newWidth = Math.min(Math.max(MIN_WIDTH, initialWidth + dx), MAX_WIDTH);
const newHeight = Math.min(Math.max(MIN_HEIGHT, initialHeight + dy), MAX_HEIGHT);
const rect = overlay.getBoundingClientRect();
const newLeft = rect.right - newWidth;
const newTop = rect.bottom - newHeight;
// Ensure the overlay stays within viewport bounds
if (newLeft >= 0 && newTop >= 0) {
overlay.style.width = newWidth + "px";
overlay.style.height = newHeight + "px";
overlay.style.left = newLeft + "px";
overlay.style.top = newTop + "px";
}
}
});
// Handle mouse up for drag and resize
document.addEventListener("mouseup", () => {
isDragging = false;
isResizing = false;
});
// Add global keyboard event listeners
document.addEventListener("keydown", (e) => {
// Use Alt (Option) on all platforms including Mac
const modifierKey = e.altKey;
// Toggle chat with Alt/Option + C
// Use e.code to be layout-independent (Option modifies e.key on macOS)
if (modifierKey && e.code === "KeyC") {
e.preventDefault(); // Prevent default browser behavior
toggleChatOverlay();
}
// Close chat with Escape
if (e.key === "Escape" && isOverlayVisible) {
isOverlayVisible = false;
const overlay = getShadowElement("chat-overlay");
if (overlay) {
overlay.style.display = "none";
}
}
});
// Initialize everything
async function init() {
try {
// Try to load showdown and our inline prism highlighter
await Promise.all([loadShowdown(), loadPrism()]);
console.log("Showdown and SimplePrism libraries loaded successfully");
} catch (error) {
console.error('Failed to load libraries:', error);
// Continue even if libraries fail to load
}
// Block clashing chat elements
blockClashingChatElements();
// Create the chat button
const chatButton = createChatButton();
// Get current stealth mode state
chrome.storage.local.get(['stealth'], function(result) {
const stealthModeEnabled = result.stealth === true;
// Hide chat button if stealth mode is enabled
if (stealthModeEnabled && chatButton) {
chatButton.style.opacity = "0"; // Use opacity instead of display none
chatButton.style.pointerEvents = "auto"; // Keep pointer events active
}
// Create the chat overlay initially but keep it hidden
// This ensures Alt+C (Option+C on Mac) will work right from the start
try {
const overlay = createChatOverlay();
// Set overlay opacity based on stealth mode
if (stealthModeEnabled && overlay) {
overlay.style.opacity = "0.15";
}
} catch (error) {
console.error('Error creating chat overlay:', error);
}
});
}
// Start the initialization
init();
// Add global storage change listener for stealth mode updates across tabs
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'local') {
// Clear error state when database or authentication changes occur
if (changes.accessToken || changes.refreshToken) {
clearErrorState();
console.log("Auth state changed, cleared chat error state");
}
if (changes.stealth) {
const newStealthMode = changes.stealth.newValue === true;
// Update chat button visibility globally
const chatButton = getChatButton();
if (chatButton) {
chatButton.style.opacity = newStealthMode ? "0" : "1";
chatButton.style.pointerEvents = "auto"; // Keep pointer events active in both states
// Icon is set via backgroundImage on child span, no innerHTML reset needed
}
// Update overlay opacity if it exists
const overlay = document.getElementById("chat-overlay");
if (overlay) {
overlay.style.opacity = newStealthMode ? "0.15" : "1";
}
}
}
});
// Listen for messages from Chrome runtime
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "updateChatHistory") {
const {
role,
content,
isStreaming
} = message;
// First remove loading indicator if it exists
const loadingMessage = getShadowElement("loading-message");
if (loadingMessage) {
loadingMessage.remove();
}
// Handle error responses from the background script
if (role === "error" || content.includes("error") || content.includes("failed")) {
// Determine if this is a rate limit error
const isRateLimitError = content.includes("limit") || content.includes("exceeded") || content.includes("tomorrow");
addErrorMessageToChat(content, isRateLimitError);
} else if (role === "assistant") {
// Clear any existing error state on successful response
clearErrorState();
if (isStreaming) {
if (!currentStreamingDiv) {
// Create a new assistant message container for streaming
currentStreamingDiv = addMessageToChat("", "assistant");
}
// Update the content incrementally
renderChatContent(currentStreamingDiv, content);
// Scroll to bottom during streaming
const chatMessagesContainer = getShadowElement("chat-messages");
if (chatMessagesContainer) {
chatMessagesContainer.scrollTop = chatMessagesContainer.scrollHeight;
}
} else {
// Stream finished or single response
if (currentStreamingDiv) {
// Final update for existing stream
renderChatContent(currentStreamingDiv, content);
currentStreamingDiv = null;
} else {
// Non-streaming assistant response
addMessageToChat(content, "assistant");
}
// Add to local chat history for conversation context
chatHistory.push({
role: "assistant",
content: content
});
}
} else {
// Handle other roles (like 'user' echo from server, though usually local)
addMessageToChat(content, role);
}
}
// Handle clear chat history action
if (message.action === "clearChatHistory") {
const reason = message.reason || 'external';
clearChatHistoryAndUI(reason);
if (sendResponse) {
sendResponse({ success: true });
}
}
// Handle direct error messages from background script
if (message.action === "chatError") {
// Remove loading indicator if it exists
const loadingMessage = getShadowElement("loading-message");
if (loadingMessage) {
loadingMessage.remove();
}
const { error, errorType, detailedInfo } = message;
let errorMessage = error || "An error occurred processing your message.";
let isRateLimitError = false;
// Enhance error message based on type
if (errorType === 'rateLimit') {
isRateLimitError = true;
if (!errorMessage.includes("tomorrow") && !errorMessage.includes("wait")) {
errorMessage += " Please try again later.";
}
} else if (errorType === 'auth') {
errorMessage = "Please log in to use the chat feature. Click the extension icon to log in.";
} else if (errorType === 'network') {
errorMessage += " Please check your internet connection and try again.";
} else if (errorType === 'server') {
errorMessage += " The service is temporarily unavailable.";
}
addErrorMessageToChat(errorMessage, isRateLimitError);
}
});
});
})();
```
## /data/inject/content.js
```js path="/data/inject/content.js"
window.addEventListener('blur', function() {
window.focus();
});
// Declare shared isMac variable (this will be the first to run)
window.isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 ||
navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
// Automatically enable text selection on all websites
(function() {
// Function to enable text selection globally
function enableTextSelectionGlobally() {
// Remove CSS rules that disable text selection
const style = document.createElement('style');
style.id = 'force-text-selection-style';
style.innerHTML = `
* {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
-webkit-touch-callout: default !important;
}
/* Override common classes that disable text selection */
.no-select, .noselect, .unselectable,
.qaas-disable-text-selection,
.qaas-disable-text-selection *,
[data-disable-text-selection],
[data-disable-text-selection] *,
[unselectable="on"],
[onselectstart],
[ondragstart] {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
-webkit-touch-callout: default !important;
}
`;
// Only add if not already present
if (!document.getElementById('force-text-selection-style')) {
document.head.appendChild(style);
}
// Remove specific attributes and classes that disable text selection
const disabledElements = document.querySelectorAll(`
.no-select, .noselect, .unselectable,
.qaas-disable-text-selection,
[data-disable-text-selection],
[unselectable="on"],
[onselectstart],
[ondragstart]
`);
disabledElements.forEach(element => {
// Remove classes
element.classList.remove('no-select', 'noselect', 'unselectable', 'qaas-disable-text-selection');
// Remove attributes
element.removeAttribute('data-disable-text-selection');
element.removeAttribute('unselectable');
element.removeAttribute('onselectstart');
element.removeAttribute('ondragstart');
// Force styles
element.style.userSelect = 'text';
element.style.webkitUserSelect = 'text';
element.style.mozUserSelect = 'text';
element.style.msUserSelect = 'text';
element.style.webkitTouchCallout = 'default';
});
// Override common event handlers that prevent text selection
document.onselectstart = null;
document.ondragstart = null;
document.oncontextmenu = null;
// Remove event listeners that might interfere with text selection
const body = document.body;
if (body) {
body.onselectstart = null;
body.ondragstart = null;
}
}
// Apply immediately
enableTextSelectionGlobally();
// Apply when DOM is fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', enableTextSelectionGlobally);
}
// Re-apply when new content is added (for dynamic websites)
const observer = new MutationObserver(function(mutations) {
let shouldReapply = false;
mutations.forEach(function(mutation) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Check if any added nodes have text selection disabled
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
const hasDisabledSelection = node.matches && node.matches(`
.no-select, .noselect, .unselectable,
.qaas-disable-text-selection,
[data-disable-text-selection],
[unselectable="on"],
[onselectstart],
[ondragstart]
`);
if (hasDisabledSelection || node.querySelector) {
shouldReapply = true;
}
}
});
}
});
if (shouldReapply) {
enableTextSelectionGlobally();
}
});
// Start observing
observer.observe(document.body || document.documentElement, {
childList: true,
subtree: true
});
})();
// Function to convert HTML to readable text with proper formatting
function htmlToText(element) {
if (!element) return '';
// Clone the element to avoid modifying the original
const clone = element.cloneNode(true);
// Handle superscripts - convert <sup>text</sup> to ^text
clone.querySelectorAll('sup').forEach(sup => {
sup.textContent = '^' + sup.textContent;
});
// Handle subscripts - convert <sub>text</sub> to _text
clone.querySelectorAll('sub').forEach(sub => {
sub.textContent = '_' + sub.textContent;
});
// Handle line breaks
clone.querySelectorAll('br').forEach(br => {
br.replaceWith('\n');
});
// Get the text content
return clone.innerText.trim();
}
// Function to extract the question, code, and options
function extractQuestionCodeAndOptions() {
// Extracting the question text
const questionElement = document.querySelector('div[aria-labelledby="question-data"]');
const questionText = questionElement ? htmlToText(questionElement) : '';
// Extracting the code
const codeLines = [];
const codeElements = document.querySelectorAll('.ace_layer.ace_text-layer .ace_line');
codeElements.forEach(line => {
codeLines.push(line.innerText.trim());
});
const codeText = codeLines.length > 0 ? codeLines.join('\n') : null; // Set to null if no code is found
// Extracting options
const optionsElements = document.querySelectorAll('div[aria-labelledby="each-option"]'); // Update this selector as necessary
const optionsText = [];
optionsElements.forEach((option, index) => {
optionsText.push(`Option ${index + 1}: ${htmlToText(option)}`);
});
return {
question: questionText,
code: codeText, // This can be null if no code is present
options: optionsText.join('\n') // Join options with new line characters
};
}
// Async function to handle question, code, and options extraction
async function handleQuestionExtraction() {
const { question, code, options } = extractQuestionCodeAndOptions();
if (!question) {
return;
}
console.log('Question:', question);
console.log('Code:\n', code ? code : 'No code available');
console.log('Options:\n', options);
// Send the extracted data to background.js
// The clicking will be handled by the clickMCQOption message handler
chrome.runtime.sendMessage({
action: 'extractData',
question: question,
code: code,
options: options,
isMCQ: true
});
}
// Function to extract coding question details
function extractCodingQuestion(isTyped = false) {
// Extract programming language
const programmingLanguageElement = document.querySelector('span.inner-text');
const programmingLanguage = programmingLanguageElement ? programmingLanguageElement.innerText.trim() : 'Programming language not found.';
// Extract question components
const questionElement = document.querySelector('div[aria-labelledby="question-data"]');
const questionText = questionElement ? htmlToText(questionElement) : 'Question not found.';
const inputFormatElement = document.querySelector('div[aria-labelledby="input-format"]');
const inputFormatText = inputFormatElement ? htmlToText(inputFormatElement) : '';
const outputFormatElement = document.querySelector('div[aria-labelledby="output-format"]');
const outputFormatText = outputFormatElement ? htmlToText(outputFormatElement) : '';
// Extract sample test cases with robust fallback method
const testCases = [];
// Try Method 1: Find test case containers with aria-labelledby="each-tc-card"
let containers = document.querySelectorAll('div[aria-labelledby="each-tc-card"]');
if (containers.length > 0) {
console.log('[Test Cases] Method 1: Found', containers.length, 'test case containers');
containers.forEach((container) => {
const inputPre = container.querySelector('div[aria-labelledby="each-tc-input-container"] pre');
const outputPre = container.querySelector('div[aria-labelledby="each-tc-output-container"] pre');
if (inputPre && outputPre) {
testCases.push({
input: inputPre.textContent.trim(),
output: outputPre.textContent.trim()
});
}
});
}
// Try Method 2: Find by aria-labelledby="each-tc-container"
if (testCases.length === 0) {
console.log('[Test Cases] Method 1 failed. Trying Method 2...');
containers = document.querySelectorAll('[aria-labelledby="each-tc-container"]');
if (containers.length > 0) {
console.log('[Test Cases] Method 2: Found', containers.length, 'test case containers');
containers.forEach((container) => {
const inputPre = container.querySelector('[aria-labelledby="each-tc-input"]');
const outputPre = container.querySelector('[aria-labelledby="each-tc-output"]');
if (inputPre && outputPre) {
testCases.push({
input: inputPre.textContent.trim(),
output: outputPre.textContent.trim()
});
}
});
}
}
// Try Method 3: Find pre elements with Input/Output labels
if (testCases.length === 0) {
console.log('[Test Cases] Method 2 failed. Trying Method 3...');
const allPres = document.querySelectorAll('pre');
const inputs = [];
const outputs = [];
allPres.forEach(pre => {
const text = pre.textContent.trim();
const prevElement = pre.previousElementSibling;
if (prevElement) {
const labelText = prevElement.textContent.toLowerCase();
if (labelText.includes('input') && !labelText.includes('output')) {
inputs.push(text);
} else if (labelText.includes('output')) {
outputs.push(text);
}
}
});
console.log('[Test Cases] Method 3: Found', inputs.length, 'inputs and', outputs.length, 'outputs');
// Pair inputs and outputs
for (let i = 0; i < Math.min(inputs.length, outputs.length); i++) {
testCases.push({
input: inputs[i],
output: outputs[i]
});
}
}
let testCasesText = '';
if (testCases.length > 0) {
testCases.forEach((testCase, index) => {
testCasesText += `Sample Test Case ${index + 1}:\nInput:\n${testCase.input}\nOutput:\n${testCase.output}\n\n`;
});
console.log('[Test Cases] Successfully extracted', testCases.length, 'test cases');
} else {
console.warn('[Test Cases] All methods failed. No test cases extracted.');
testCasesText = 'No test cases found. Please check the page structure.';
}
// Extract whitelist keywords from instruction cards
let whitelistText = '';
const instructionCards = document.querySelectorAll('div[aria-labelledby="instruction-card"]');
instructionCards.forEach(card => {
const header = card.querySelector('[aria-labelledby="instruction-header"]');
if (header && header.textContent.trim().toLowerCase().includes('whitelist')) {
const sets = card.querySelectorAll('[aria-labelledby="list"]');
sets.forEach(set => {
const setHeader = set.querySelector('[aria-labelledby="set-header"]');
const values = set.querySelectorAll('[aria-labelledby="list-value-card"]');
const keywords = Array.from(values).map(v => v.textContent.trim()).filter(Boolean);
if (keywords.length > 0) {
const setName = setHeader ? setHeader.textContent.trim() : '';
whitelistText += (setName ? setName + ' ' : '') + keywords.join(', ') + '\n';
}
});
}
});
whitelistText = whitelistText.trim();
// Extract header and footer snippet code from readonly editors
let headerSnippet = '';
let footerSnippet = '';
const headerEditorEl = document.querySelector('[aria-labelledby="editor-question"][id*="ttHeaderEditor"]');
const footerEditorEl = document.querySelector('[aria-labelledby="editor-question"][id*="ttFooterEditor"]');
if (headerEditorEl) {
const headerLines = headerEditorEl.querySelectorAll('.ace_line');
headerSnippet = Array.from(headerLines).map(line => line.textContent).join('\n').trim();
}
if (footerEditorEl) {
const footerLines = footerEditorEl.querySelectorAll('.ace_line');
footerSnippet = Array.from(footerLines).map(line => line.textContent).join('\n').trim();
}
// Send data to background.js for querying
chrome.runtime.sendMessage({
action: 'extractData',
programmingLanguage: programmingLanguage,
question: questionText,
inputFormat: inputFormatText,
outputFormat: outputFormatText,
testCases: testCasesText,
headerSnippet: headerSnippet,
footerSnippet: footerSnippet,
whitelist: whitelistText,
isCoding: true,
isTyped: isTyped
}, (response) => {
// Injection is handled directly by worker.js via chrome.scripting.executeScript.
// This callback may receive null due to multiple onMessage listeners — that's expected.
if (response && response.error) {
console.error('[AI Answer] Error from background:', response.error);
}
});
}
function solveIamneoExamly(){
// Check if this is a coding question or MCQ
const codingQuestionElement = document.querySelector('div[aria-labelledby="input-format"]');
if (codingQuestionElement) {
extractCodingQuestion();
} else {
handleQuestionExtraction();
}
}
document.addEventListener('keydown', (event) => {
// Use Option (Alt) key on all platforms
const modifierKey = event.altKey;
if (modifierKey && event.shiftKey && event.code === 'KeyA') {
solveIamneoExamly();
}
});
// Alt+Shift+T (Ctrl+Shift+T on Mac): Typed code insertion — only handles initial AI fetch.
// Resume/stop/continue typing is handled by exam.js locally.
let _typedFetchQuestion = null; // track which question we already fetched for
document.addEventListener('keydown', (event) => {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const modifierKey = isMac ? event.ctrlKey : event.altKey;
if (modifierKey && event.shiftKey && event.code === 'KeyT') {
console.log('[Alt+Shift+T] Key detected in content.js');
// Only fetch if this is a coding question
const codingQuestionElement = document.querySelector('div[aria-labelledby="input-format"]');
console.log('[Alt+Shift+T] codingQuestionElement found:', !!codingQuestionElement);
if (!codingQuestionElement) return;
// Get current question number to avoid re-fetching
const qEl = document.querySelector('div[class*="t-bg-primary"]');
const qMatch = qEl && qEl.textContent.match(/Question No : (\d+)/);
const qNum = qMatch ? qMatch[1] : null;
console.log('[Alt+Shift+T] question number:', qNum, 'already fetched for:', _typedFetchQuestion);
if (qNum && _typedFetchQuestion === qNum) {
console.log('[Alt+Shift+T] Already fetched for this question, skipping');
return;
}
_typedFetchQuestion = qNum;
console.log('[Alt+Shift+T] Calling extractCodingQuestion(true)');
extractCodingQuestion(true); // isTyped = true
}
});
// Add event listener for Option+O to toggle toast opacity
document.addEventListener('keydown', (event) => {
// Use Option (Alt) key on all platforms
const modifierKey = event.altKey;
if (modifierKey && event.code === 'KeyO') {
chrome.runtime.sendMessage({
action: 'toggleToastOpacity'
});
}
});
// Function to extract code from snippets
function extractSnippets() {
const headerContainer = Array.from(document.querySelectorAll('div[aria-labelledby="tt-header"]'))
.find(container => container.innerText.includes('Header Snippet'));
const footerContainer = Array.from(document.querySelectorAll('div[aria-labelledby="footer"]'))
.find(container => container.innerText.includes('Footer Snippet'));
const extractCode = container => {
if (!container) return '';
const codeLines = container.querySelectorAll('.ace_line');
return Array.from(codeLines).map(line => line.textContent).join('\n');
};
const snippets = {
header: extractCode(headerContainer),
footer: extractCode(footerContainer)
};
// Send snippets directly to background.js
chrome.runtime.sendMessage({
action: 'processSnippets',
snippets: snippets
});
}
// Remove old listener and add new one
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'extractSnippets') {
extractSnippets();
}
if (message.action === 'solveIamneoExamly') {
solveIamneoExamly();
}
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "updateChatHistory") {
const { role, content } = message;
// Remove loading indicator if it exists
const loadingMessage = document.getElementById("loading-message");
if (loadingMessage) {
loadingMessage.remove();
}
// Add the actual message
chatHistory.push({
role: role,
content: content
});
addMessageToChat(content, role);
}
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'clickMCQOption') {
(async () => {
try {
// Check if this is HackerRank
if (request.isHackerRank) {
let clicked = false;
// Handle multiple choice questions (checkboxes) differently
if (request.isMultipleChoice) {
console.log('Multiple choice question detected, response:', request.response);
// Enhanced parsing for multiple options
// Look for patterns like: "1. text, 3. text" or "A. text, C. text" or "1, 3" or "A, C"
const optionNumbers = [];
// Pattern 1: "1. text, 3. text" or "A. text, C. text"
let matches = request.response.match(/([A-Z]|\d+)\.\s*[^,]+/gi);
if (matches) {
matches.forEach(match => {
const num = match.match(/^([A-Z]|\d+)\./);
if (num) {
let optionIndex;
if (isNaN(num[1])) {
// Convert A,B,C to 0,1,2
optionIndex = num[1].charCodeAt(0) - 'A'.charCodeAt(0);
} else {
// Convert 1,2,3 to 0,1,2
optionIndex = parseInt(num[1]) - 1;
}
if (optionIndex >= 0) {
optionNumbers.push(optionIndex);
}
}
});
}
// Pattern 2: Simple comma-separated numbers or letters: "1, 3, 5" or "A, C, E"
if (optionNumbers.length === 0) {
const simpleMatches = request.response.match(/(?:^|[,\s])([A-Z]|\d+)(?=[,\s]|$)/gi);
if (simpleMatches) {
simpleMatches.forEach(match => {
const cleaned = match.trim().replace(/^[,\s]+|[,\s]+$/g, '');
let optionIndex;
if (isNaN(cleaned)) {
// Convert A,B,C to 0,1,2
optionIndex = cleaned.charCodeAt(0) - 'A'.charCodeAt(0);
} else {
// Convert 1,2,3 to 0,1,2
optionIndex = parseInt(cleaned) - 1;
}
if (optionIndex >= 0) {
optionNumbers.push(optionIndex);
}
});
}
}
// Remove duplicates
const uniqueOptionNumbers = [...new Set(optionNumbers)];
console.log('Parsed multiple choice options:', uniqueOptionNumbers.map(n => n + 1));
// Click all the selected options for multiple choice
const checkboxes = document.querySelectorAll('[role="checkbox"]');
if (checkboxes.length > 0) {
console.log(`Found ${checkboxes.length} checkboxes, will click options:`, uniqueOptionNumbers.map(n => n + 1));
// Click options with delay to ensure UI state is properly updated
for (let i = 0; i < uniqueOptionNumbers.length; i++) {
const optionNumber = uniqueOptionNumbers[i];
if (optionNumber >= 0 && optionNumber < checkboxes.length) {
const checkbox = checkboxes[optionNumber];
// Wait a bit before checking and clicking each option
await new Promise(resolve => setTimeout(resolve, 300));
// Re-check the current state after delay
const isCurrentlyChecked = checkbox.getAttribute('aria-checked') === 'true' ||
checkbox.getAttribute('data-state') === 'checked' ||
checkbox.checked === true;
console.log(`Option ${optionNumber + 1} current state: ${isCurrentlyChecked ? 'checked' : 'unchecked'}`);
// Only click if not already checked
if (!isCurrentlyChecked) {
console.log(`Clicking checkbox option ${optionNumber + 1}...`);
// Try multiple click methods to ensure it works
checkbox.click();
// Alternative click method - dispatch events directly
checkbox.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
checkbox.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
checkbox.dispatchEvent(new MouseEvent('click', { bubbles: true }));
// Wait a bit more to let the UI update
await new Promise(resolve => setTimeout(resolve, 200));
// Verify the click worked
const newState = checkbox.getAttribute('aria-checked') === 'true' ||
checkbox.getAttribute('data-state') === 'checked' ||
checkbox.checked === true;
if (newState) {
console.log(`✅ HackerRank checkbox option ${optionNumber + 1} clicked successfully`);
clicked = true;
} else {
console.log(`⚠️ HackerRank checkbox option ${optionNumber + 1} click may have failed - retrying...`);
// Retry once more
checkbox.click();
await new Promise(resolve => setTimeout(resolve, 100));
const retryState = checkbox.getAttribute('aria-checked') === 'true' ||
checkbox.getAttribute('data-state') === 'checked' ||
checkbox.checked === true;
if (retryState) {
console.log(`✅ HackerRank checkbox option ${optionNumber + 1} clicked successfully on retry`);
clicked = true;
} else {
console.log(`❌ HackerRank checkbox option ${optionNumber + 1} failed to click`);
}
}
} else {
console.log(`✅ HackerRank checkbox option ${optionNumber + 1} already selected`);
clicked = true; // Still count as successful
}
}
}
// If no options were found, fall back to single option logic
if (uniqueOptionNumbers.length === 0) {
console.log('No multiple options found, falling back to single option logic');
const optionMatch = request.response.match(/(?:options?\s*)?([A-Z]|\d+)\.?/i);
if (optionMatch) {
let optionNumber;
if (isNaN(optionMatch[1])) {
optionNumber = optionMatch[1].charCodeAt(0) - 'A'.charCodeAt(0);
} else {
optionNumber = parseInt(optionMatch[1]) - 1;
}
if (optionNumber >= 0 && optionNumber < checkboxes.length) {
await new Promise(resolve => setTimeout(resolve, 200));
const checkbox = checkboxes[optionNumber];
const isCurrentlyChecked = checkbox.getAttribute('aria-checked') === 'true' ||
checkbox.getAttribute('data-state') === 'checked' ||
checkbox.checked === true;
if (!isCurrentlyChecked) {
checkbox.click();
console.log(`HackerRank single checkbox option ${optionNumber + 1} clicked as fallback`);
clicked = true;
} else {
console.log(`HackerRank single checkbox option ${optionNumber + 1} already selected`);
clicked = true;
}
}
}
}
}
} else {
// Single choice question - use enhanced logic
const optionMatch = request.response.match(/(?:options?\s*)?([A-Z]|\d+)\.?/i);
if (optionMatch) {
let optionNumber;
if (isNaN(optionMatch[1])) {
// Handle letter options (A, B, C, etc.)
optionNumber = optionMatch[1].toUpperCase().charCodeAt(0) - 'A'.charCodeAt(0);
} else {
// Handle number options (1, 2, 3, etc.)
optionNumber = parseInt(optionMatch[1]) - 1;
}
console.log(`Single choice detected, clicking option: ${optionNumber + 1}`);
// Add a small delay before clicking
await new Promise(resolve => setTimeout(resolve, 200));
// Try new layout first - check for radio buttons
const newLayoutRadios = document.querySelectorAll('[role="radio"]');
if (newLayoutRadios.length > optionNumber && optionNumber >= 0) {
const radio = newLayoutRadios[optionNumber];
// Check if already selected
const isCurrentlySelected = radio.getAttribute('aria-checked') === 'true' ||
radio.getAttribute('data-state') === 'checked' ||
radio.checked === true;
if (!isCurrentlySelected) {
radio.click();
console.log(`HackerRank new layout radio option ${optionNumber + 1} clicked successfully`);
clicked = true;
} else {
console.log(`HackerRank new layout radio option ${optionNumber + 1} already selected`);
clicked = true;
}
} else {
// Try checkboxes if no radio buttons found (fallback for single checkbox)
const newLayoutCheckboxes = document.querySelectorAll('[role="checkbox"]');
if (newLayoutCheckboxes.length > optionNumber && optionNumber >= 0) {
const checkbox = newLayoutCheckboxes[optionNumber];
const isCurrentlyChecked = checkbox.getAttribute('aria-checked') === 'true' ||
checkbox.getAttribute('data-state') === 'checked' ||
checkbox.checked === true;
if (!isCurrentlyChecked) {
checkbox.click();
console.log(`HackerRank new layout checkbox option ${optionNumber + 1} clicked successfully`);
clicked = true;
} else {
console.log(`HackerRank new layout checkbox option ${optionNumber + 1} already selected`);
clicked = true;
}
} else {
// Fallback to old layout (radio buttons)
const questionContainer = document.querySelector('.grouped-mcq__question');
if (questionContainer) {
const radios = questionContainer.querySelectorAll('input[type="radio"]');
if (radios.length > optionNumber && optionNumber >= 0) {
const radio = radios[optionNumber];
if (!radio.checked) {
radio.click();
console.log(`HackerRank old layout option ${optionNumber + 1} clicked successfully`);
clicked = true;
} else {
console.log(`HackerRank old layout option ${optionNumber + 1} already selected`);
clicked = true;
}
}
}
}
}
}
}
if (!clicked) {
chrome.runtime.sendMessage({
action: 'showMCQToast',
message: request.response,
});
}
} else {
// Original logic for other platforms (Examly)
const optionMatch = request.response.match(/(?:options?\s*)?(\d+)\.?/i);
if (optionMatch) {
const optionNumber = parseInt(optionMatch[1])-1;
// Use exact same selector as Alt+Shift+Q
const answerElement = document.querySelector(`#tt-option-${optionNumber} > label > span.checkmark1`);
if (answerElement) {
answerElement.dispatchEvent(new Event("click", { bubbles: true }));
console.log(`Option element ${optionNumber + 1} clicked successfully`);
} else {
chrome.runtime.sendMessage({
action: 'showMCQToast',
message: request.response,
});
}
} else {
chrome.runtime.sendMessage({
action: 'showMCQToast',
message: request.response,
});
}
}
} catch (error) {
chrome.runtime.sendMessage({
action: 'showMCQToast',
message: request.response,
});
}
})();
}
});
// Function to extract HackerRank MCQ data (updated for new layout)
function extractHackerRankMCQ() {
const questions = [];
// Try new layout first (2024+ layout)
const newLayoutQuestions = document.querySelectorAll('.QuestionDetails_container__AIu0X');
if (newLayoutQuestions.length > 0) {
// New layout processing
newLayoutQuestions.forEach((container, index) => {
const questionData = {
questionNumber: index + 1,
title: '',
instruction: '',
options: [],
selectedAnswer: null
};
// Extract question title from new layout
const titleElement = container.querySelector('.qaas-block-question-title, h2');
if (titleElement) {
// Remove bookmark icon and get clean title
const titleText = titleElement.textContent || titleElement.innerText;
questionData.title = titleText.replace(/Bookmark question \d+/g, '').trim();
}
// Extract question instruction/content from new layout
const instructionElement = container.querySelector('.qaas-block-question-instruction, .RichTextPreview_richText__1vKu5');
if (instructionElement) {
let instructionText = instructionElement.textContent || instructionElement.innerText;
instructionText = instructionText.replace(/\s+/g, ' ').trim();
questionData.instruction = instructionText;
}
// Look for options in multiple possible containers
let optionsContainer = container.nextElementSibling;
let attempts = 0;
while (optionsContainer && attempts < 5) {
// Check for both radio buttons and checkboxes
const hasOptions = optionsContainer.querySelector('[role="checkbox"], [role="radio"], .ui-radio');
if (hasOptions) {
break;
}
optionsContainer = optionsContainer.nextElementSibling;
attempts++;
}
// Also check for options within the same container or nearby
if (!optionsContainer || !optionsContainer.querySelector('[role="checkbox"], [role="radio"]')) {
optionsContainer = container.parentElement?.querySelector('.Control_container__F35yA') ||
document.querySelector('.Control_container__F35yA');
}
if (optionsContainer) {
// Try radio buttons first (new layout)
let optionElements = optionsContainer.querySelectorAll('[role="radio"]');
// If no radio buttons, try checkboxes
if (optionElements.length === 0) {
optionElements = optionsContainer.querySelectorAll('[role="checkbox"]');
}
optionElements.forEach((option, optionIndex) => {
const labelId = option.getAttribute('aria-labelledby');
const labelElement = labelId ? document.getElementById(labelId) :
option.closest('.Control_optionList__vIubt, li')?.querySelector('label');
if (labelElement) {
const optionText = labelElement.textContent.trim();
const isChecked = option.getAttribute('aria-checked') === 'true' ||
option.getAttribute('data-state') === 'checked';
questionData.options.push({
value: option.value || optionIndex.toString(),
text: optionText,
isSelected: isChecked
});
if (isChecked) {
questionData.selectedAnswer = option.value || optionIndex.toString();
}
}
});
}
// Only add question if it has options (to distinguish from coding questions)
if (questionData.options.length > 0) {
questions.push(questionData);
}
});
} else {
// Fallback to old layout
const oldLayoutQuestions = document.querySelectorAll('.grouped-mcq__question');
oldLayoutQuestions.forEach((container, index) => {
const questionData = {
questionNumber: index + 1,
title: '',
instruction: '',
options: [],
selectedAnswer: null
};
// Extract question title from old layout
const titleElement = container.querySelector('.question-view__title');
if (titleElement) {
questionData.title = titleElement.textContent.trim();
}
// Extract question instruction/content from old layout
const instructionElement = container.querySelector('.question-view__instruction');
if (instructionElement) {
let instructionText = instructionElement.textContent.trim();
instructionText = instructionText.replace(/\s+/g, ' ').trim();
questionData.instruction = instructionText;
}
// Extract options from old layout
const optionElements = container.querySelectorAll('.ui-radio');
optionElements.forEach((option, optionIndex) => {
const labelElement = option.querySelector('.label');
const inputElement = option.querySelector('input[type="radio"]');
if (labelElement && inputElement) {
const optionText = labelElement.textContent.trim();
const optionValue = inputElement.value;
const isChecked = inputElement.checked;
questionData.options.push({
value: optionValue,
text: optionText,
isSelected: isChecked
});
if (isChecked) {
questionData.selectedAnswer = optionValue;
}
}
});
questions.push(questionData);
});
}
return questions;
}
// Function to extract HackerRank coding question (updated for new layout)
function extractHackerRankCoding() {
const getCleanText = el => el?.innerText?.trim() || "";
// Try new layout first (2024+ layout)
let language = "Unknown";
let title = "No Title Found";
let instruction = "No Instructions Found";
let details = "";
let starterCode = "";
// Check for new layout language selector
const newLanguageSelector = document.querySelector('.select-language .css-3d4y2u-singleValue, .select-language .css-x7738g');
if (newLanguageSelector) {
language = getCleanText(newLanguageSelector);
} else {
// Fallback to old layout
language = getCleanText(document.querySelector('.select-language .css-x7738g')) || "Unknown";
}
// Try new layout question container
let container = document.querySelector('.QuestionDetails_container__AIu0X');
if (container) {
// New layout
const titleElement = container.querySelector('.qaas-block-question-title, h2');
if (titleElement) {
const titleText = titleElement.textContent || titleElement.innerText;
title = titleText.replace(/Bookmark question \d+/g, '').trim();
}
const instructionElement = container.querySelector('.qaas-block-question-instruction, .RichTextPreview_richText__1vKu5');
if (instructionElement) {
instruction = getCleanText(instructionElement);
}
// Look for details sections in new layout
const detailsElements = container.querySelectorAll('details');
if (detailsElements.length > 0) {
details = Array.from(detailsElements).map(detail => {
const summary = getCleanText(detail.querySelector('summary'));
const content = getCleanText(detail.querySelector('.collapsable-details'));
return `\n${summary}\n${'-'.repeat(summary.length)}\n${content}`;
}).join('\n');
}
} else {
// Fallback to old layout
container = document.querySelector('#main-splitpane-left');
if (container) {
title = getCleanText(container.querySelector('.question-view__title')) || "No Title Found";
instruction = getCleanText(container.querySelector('.question-view__instruction')) || "No Instructions Found";
details = Array.from(container.querySelectorAll('details') || []).map(detail => {
const summary = getCleanText(detail.querySelector('summary'));
const content = getCleanText(detail.querySelector('.collapsable-details'));
return `\n${summary}\n${'-'.repeat(summary.length)}\n${content}`;
}).join('\n');
}
}
// Get starter code from Monaco editor (works for both layouts)
const codeLines = Array.from(document.querySelectorAll('.view-lines .view-line')).map(line =>
line.innerText
).join('\n').trim();
starterCode = codeLines;
return {
language,
title,
instruction,
details,
starterCode: starterCode
};
}
// Function to normalize code indentation
function normalizeCodeIndentation(code) {
if (!code) return code;
const lines = code.split('\n');
// Remove empty lines at the beginning and end
while (lines.length > 0 && lines[0].trim() === '') {
lines.shift();
}
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
lines.pop();
}
if (lines.length === 0) return '';
// Find the minimum indentation (excluding empty lines)
let minIndent = Infinity;
for (const line of lines) {
if (line.trim() !== '') {
const indent = line.match(/^\s*/)[0].length;
minIndent = Math.min(minIndent, indent);
}
}
// Remove the common indentation from all lines
if (minIndent > 0 && minIndent !== Infinity) {
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim() !== '') {
lines[i] = lines[i].substring(minIndent);
}
}
}
return lines.join('\n');
}
// Function to insert code into Monaco editor with proper formatting
async function insertCodeIntoMonacoEditor(text) {
console.log('insertCodeIntoMonacoEditor called with text length:', text.length);
// Normalize the code indentation first
const normalizedText = normalizeCodeIndentation(text);
console.log('Text after normalization:', normalizedText);
// 1. Try to find Monaco editor instance through the global scope
if (typeof monaco !== 'undefined' && window.monaco) {
try {
const editor = window.monaco.editor.getEditors()[0];
if (editor) {
console.log('Found Monaco editor instance, setting value directly...');
editor.setValue(normalizedText);
editor.focus();
return true;
}
} catch (error) {
console.log('Monaco API method failed, trying alternative approaches...');
}
}
// 2. Try to access Monaco editor through DOM manipulation
const monacoEditor = document.querySelector('.monaco-editor');
console.log('Monaco editor DOM element found:', !!monacoEditor);
if (!monacoEditor) {
console.error("❌ Monaco editor not found.");
return false;
}
try {
// 3. Focus the editor properly
const editorTextArea = monacoEditor.querySelector('textarea.inputarea') ||
monacoEditor.querySelector('textarea') ||
monacoEditor.querySelector('.monaco-editor-background');
if (editorTextArea) {
console.log('Found Monaco textarea, focusing...');
editorTextArea.focus();
editorTextArea.click();
} else {
console.log('Monaco textarea not found, clicking editor container...');
monacoEditor.focus();
monacoEditor.click();
}
// 4. Wait a bit for focus to settle
await new Promise(resolve => setTimeout(resolve, 200));
// 5. Clear existing content using keyboard shortcuts
console.log('Clearing existing content...');
// Use Select All (Cmd+A on macOS, Ctrl+A elsewhere)
document.dispatchEvent(new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
ctrlKey: !window.isMac,
metaKey: window.isMac,
bubbles: true
}));
await new Promise(resolve => setTimeout(resolve, 100));
// Use Delete or Backspace to clear
document.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Delete',
code: 'Delete',
bubbles: true
}));
await new Promise(resolve => setTimeout(resolve, 100));
// 6. Copy normalized text to clipboard
await navigator.clipboard.writeText(normalizedText);
console.log('Text copied to clipboard');
// 7. Paste (Cmd+V on macOS, Ctrl+V elsewhere)
console.log('Pasting content...');
document.dispatchEvent(new KeyboardEvent('keydown', {
key: 'v',
code: 'KeyV',
ctrlKey: !window.isMac,
metaKey: window.isMac,
bubbles: true
}));
await new Promise(resolve => setTimeout(resolve, 300));
// 8. Try input event as fallback
if (editorTextArea) {
console.log('Trying input event fallback...');
// Set the value directly on the textarea
editorTextArea.value = normalizedText;
// Trigger input events
editorTextArea.dispatchEvent(new Event('input', { bubbles: true }));
editorTextArea.dispatchEvent(new Event('change', { bubbles: true }));
// Try to trigger Monaco's internal update
editorTextArea.dispatchEvent(new KeyboardEvent('keydown', {
key: 'End',
code: 'End',
bubbles: true
}));
}
console.log('✅ Successfully inserted code into Monaco editor');
return true;
} catch (error) {
console.error("❌ Error inserting code into Monaco editor:", error);
// Final fallback: copy to clipboard
try {
await navigator.clipboard.writeText(normalizedText);
console.log('Fallback: copied normalized text to clipboard');
} catch (clipboardError) {
console.error('Clipboard fallback also failed:', clipboardError);
}
return false;
}
}
// Function to handle HackerRank extraction (both MCQ and coding, updated for new layout)
function handleHackerRankMCQ() {
// Check if it's a coding question first (Monaco editor present)
const monacoEditor = document.querySelector('.monaco-editor, .hr-monaco-editor');
// Check for MCQ options specifically (more precise detection)
const hasRadioOptions = document.querySelector('[role="radio"], [role="radiogroup"]');
const hasCheckboxOptions = document.querySelector('[role="checkbox"]');
const hasOldMcqOptions = document.querySelector('.grouped-mcq__question .ui-radio');
const hasOptionsControl = document.querySelector('.Control_container__F35yA');
// More precise MCQ detection
const isMCQ = hasRadioOptions || hasCheckboxOptions || hasOldMcqOptions ||
(hasOptionsControl && !monacoEditor);
if (monacoEditor && !isMCQ) {
// This is definitely a coding question
const codingData = extractHackerRankCoding();
if (!codingData.instruction || codingData.instruction === "No Instructions Found") {
chrome.runtime.sendMessage({
action: 'showToast',
message: 'No HackerRank coding question found.',
isError: true
});
return;
}
// Format the question for AI
const questionText = `
Language: ${codingData.language}
Title: ${codingData.title}
Instructions:
${codingData.instruction}
${codingData.details}
Starter Code:
-------------
${codingData.starterCode}
`.trim();
console.log('HackerRank Coding Question:', questionText);
// Send the extracted data to background.js
chrome.runtime.sendMessage({
action: 'extractData',
programmingLanguage: codingData.language,
question: questionText,
inputFormat: codingData.details,
outputFormat: '',
testCases: '',
isHackerRank: true,
isCoding: true }, async (response) => {
console.log('HackerRank coding response received:', response);
if (response && response.success && response.response) {
try {
console.log('Raw AI response:', response.response);
// Clean the response more thoroughly
let cleanedResponse = response.response.trim();
console.log('Response after trim:', cleanedResponse);
// Remove code block delimiters if present (more comprehensive)
cleanedResponse = cleanedResponse
.replace(/^\`\`\`[a-zA-Z]*\s*\n/, '') // Remove opening \`\`\` with optional language
.replace(/\n\s*\`\`\`\s*$/, '') // Remove closing \`\`\` with optional whitespace
.replace(/^\`\`\`[a-zA-Z]*\s*/, '') // Remove opening \`\`\` without newline
.replace(/\s*\`\`\`\s*$/, ''); // Remove closing \`\`\` without newline
// Remove any leading/trailing whitespace after code block removal
cleanedResponse = cleanedResponse.trim();
console.log('Cleaned response (after removing code blocks):', cleanedResponse);
// Insert code into Monaco editor with proper formatting
console.log('Attempting to insert code into Monaco editor...');
const success = await insertCodeIntoMonacoEditor(cleanedResponse);
console.log('Monaco editor insertion result:', success);
if (!success) {
// If insertion fails, copy to clipboard as fallback
console.log('Monaco insertion failed, copying to clipboard as fallback');
await navigator.clipboard.writeText(cleanedResponse);
chrome.runtime.sendMessage({
action: 'showToast',
message: 'Copied to clipboard - paste manually',
isError: false
});
} else {
console.log('Successfully inserted code into Monaco editor');
chrome.runtime.sendMessage({
action: 'showToast',
message: 'Code inserted successfully',
isError: false
});
}
} catch (error) {
console.error("Error processing coding response:", error);
chrome.runtime.sendMessage({
action: 'showToast',
message: 'Error processing response',
isError: true
});
}
} else {
console.error('Invalid response received:', response);
}
});
} else if (isMCQ) {
// This is an MCQ question
const extractedData = extractHackerRankMCQ();
if (extractedData.length === 0) {
chrome.runtime.sendMessage({
action: 'showToast',
message: 'No HackerRank MCQ questions found.',
isError: true
});
return;
}
// Process the first question
const firstQuestion = extractedData[0];
if (!firstQuestion.instruction && !firstQuestion.title) {
chrome.runtime.sendMessage({
action: 'showToast',
message: 'No question text found.',
isError: true
});
return;
}
if (firstQuestion.options.length === 0) {
chrome.runtime.sendMessage({
action: 'showToast',
message: 'No options found for MCQ question.',
isError: true
});
return;
}
// Format the question and options for AI with explicit instructions
const questionText = firstQuestion.title ? `${firstQuestion.title}\n${firstQuestion.instruction}` : firstQuestion.instruction;
const optionsText = firstQuestion.options.map((option, index) =>
`Option ${index + 1}: ${option.text}`
).join('\n');
// Detect if this is a multiple choice question (checkboxes) or single choice (radio buttons)
const hasCheckboxes = document.querySelector('[role="checkbox"]');
const isMultipleChoice = hasCheckboxes && !document.querySelector('[role="radio"]');
// Add explicit instruction for multiple choice questions
let finalQuestionText = questionText;
if (isMultipleChoice) {
finalQuestionText = `[MULTIPLE CHOICE QUESTION - SELECT ALL CORRECT OPTIONS]\n\n${questionText}\n\nIMPORTANT: This question allows multiple correct answers. Please respond with ALL correct option numbers separated by commas (e.g., "Options 1, 3, 5" or "1, 3, 5").`;
} else {
finalQuestionText = `[SINGLE CHOICE QUESTION - SELECT ONE OPTION]\n\n${questionText}\n\nIMPORTANT: This question allows only ONE correct answer. Please respond with the single correct option number (e.g., "Option 2" or "2").`;
}
console.log('HackerRank MCQ Question:', finalQuestionText);
console.log('Options:\n', optionsText);
console.log('Question type:', isMultipleChoice ? 'Multiple Choice (checkboxes)' : 'Single Choice (radio buttons)');
// Send the extracted data to background.js
chrome.runtime.sendMessage({
action: 'extractData',
question: finalQuestionText, // Use the enhanced question text
code: null,
options: optionsText,
isHackerRank: true,
isMCQ: true,
isMultipleChoice: isMultipleChoice // Add flag for multiple choice questions
}, (response) => {
console.log("Response from background:", response);
});
} else {
chrome.runtime.sendMessage({
action: 'showToast',
message: 'No HackerRank question found on this page.',
isError: true
});
}
}
// Add event listener for Ctrl+Shift+H (Mac) or Alt+Shift+H (Windows) for HackerRank MCQ extraction
document.addEventListener('keydown', (event) => {
// Use Ctrl on Mac, Alt on Windows/other platforms
const modifierKey = window.isMac ? event.ctrlKey : event.altKey;
if (modifierKey && event.shiftKey && event.code === 'KeyH') {
handleHackerRankMCQ();
}
});
```
## /data/inject/copyOverride.js
```js path="/data/inject/copyOverride.js"
// Custom Ctrl+C override functionality - Prevents default copy on divs
(function() {
'use strict';
// Create an invisible textarea for our controlled copy operations
const invisibleTextarea = document.createElement('textarea');
invisibleTextarea.id = 'neopass-invisible-copy';
invisibleTextarea.style.position = 'fixed';
invisibleTextarea.style.opacity = '0';
invisibleTextarea.style.pointerEvents = 'none';
invisibleTextarea.style.left = '-9999px';
invisibleTextarea.style.top = '-9999px';
invisibleTextarea.style.width = '1px';
invisibleTextarea.style.height = '1px';
invisibleTextarea.style.border = 'none';
invisibleTextarea.style.outline = 'none';
invisibleTextarea.style.resize = 'none';
invisibleTextarea.style.overflow = 'hidden';
document.body.appendChild(invisibleTextarea);
// Store the last copied text in a global variable for paste operations
window.neoPassClipboard = '';
// Flag to track when we're performing a custom copy operation
let isCustomCopying = false;
// Override navigator.clipboard.writeText to use our custom copy AND store in clipboard
const originalWriteText = navigator.clipboard.writeText;
navigator.clipboard.writeText = async function(text) {
console.log('[CopyOverride] Intercepted clipboard writeText:', text.substring(0, 100));
window.neoPassClipboard = text; // Store for later paste
try {
// Try to use the original writeText first for compatibility
await originalWriteText.call(navigator.clipboard, text);
console.log('[CopyOverride] Successfully wrote to native clipboard');
} catch (err) {
console.log('[CopyOverride] Native clipboard write failed, using custom copy:', err);
await customCopy(text);
}
console.log('[CopyOverride] Stored in neoPassClipboard, length:', text.length);
return Promise.resolve();
};
// Override document.execCommand to use our custom copy method
const originalExecCommand = document.execCommand;
document.execCommand = function(command, showUI, value) {
if (command === 'copy') {
const activeElement = document.activeElement;
if (activeElement !== invisibleTextarea) {
console.log('Intercepted execCommand copy, using custom copy');
const text = activeElement.value || activeElement.textContent;
if (text) {
return customCopy(text);
}
return false;
}
}
return originalExecCommand.call(this, command, showUI, value);
};
// Function to perform custom copy operation
async function customCopy(selectedText) {
if (!selectedText) return false;
try {
// Set flag to prevent blocking our own copy
isCustomCopying = true;
// Store in our global clipboard variable
window.neoPassClipboard = selectedText;
// Try to write to native clipboard first
try {
await originalWriteText.call(navigator.clipboard, selectedText);
console.log('[CopyOverride] Wrote to native clipboard via writeText');
} catch (clipErr) {
console.log('[CopyOverride] writeText failed, using execCommand:', clipErr);
}
invisibleTextarea.value = selectedText;
invisibleTextarea.select();
invisibleTextarea.setSelectionRange(0, selectedText.length);
const success = originalExecCommand.call(document, 'copy');
console.log('Text copied using invisible textarea:', success, 'Stored in neoPassClipboard');
// Clear the textarea
invisibleTextarea.value = '';
invisibleTextarea.blur();
// Reset flag after a longer delay to allow all copy events to complete
setTimeout(() => {
isCustomCopying = false;
}, 300);
return success;
} catch (err) {
console.error('Copy using invisible textarea failed:', err);
isCustomCopying = false;
return false;
}
}
// Function to get selected text
function getSelectedText() {
const selection = window.getSelection();
return selection.toString().trim();
}
// Function removed - login check no longer required
// CRITICAL: Block ALL copy events at the earliest phase
document.addEventListener('copy', function(event) {
// Allow copy if we're currently performing a custom copy
if (isCustomCopying) {
// Silently allow during custom copy window
return;
}
// Only allow copy from our invisible textarea - block everything else
if (event.target !== invisibleTextarea && document.activeElement !== invisibleTextarea) {
event.preventDefault();
event.stopImmediatePropagation();
}
}, true); // Capture phase - runs before any other handlers
// Handle keyboard copy (Ctrl+C / Cmd+C)
document.addEventListener('keydown', async function(event) {
if ((event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey && event.key === 'c') {
const selectedText = getSelectedText();
if (selectedText) {
// Prevent default FIRST
event.preventDefault();
event.stopImmediatePropagation();
// Clear selection IMMEDIATELY to prevent spurious copy events
window.getSelection().removeAllRanges();
console.log('[CopyOverride] Ctrl+C detected, initiating custom copy');
try {
// Store in global clipboard
window.neoPassClipboard = selectedText;
// Perform custom copy with flag protection
const success = await customCopy(selectedText);
console.log('[CopyOverride] Custom copy executed:', {
success,
textLength: selectedText.length,
preview: selectedText.substring(0, 40) + (selectedText.length > 40 ? '...' : '')
});
} catch (error) {
console.error('[CopyOverride] Error in custom copy handler:', error);
isCustomCopying = false; // Reset flag on error
}
}
}
}, true); // Capture phase
// Handle context menu copy
document.addEventListener('contextmenu', function(event) {
const selectedText = getSelectedText();
if (selectedText) {
window.neoPassSelectedText = selectedText;
window.neoPassClipboard = selectedText; // Also store in main clipboard
}
}, true);
// Log clipboard status for debugging
window.getNeoPassClipboard = function() {
console.log('[CopyOverride] Current neoPassClipboard:', window.neoPassClipboard);
return window.neoPassClipboard;
};
console.log('Custom copy prevention initialized - default copy blocked on all elements');
})();
```
## /data/inject/exam.js
```js path="/data/inject/exam.js"
// Use shared isMac variable if it exists, otherwise declare it
if (typeof window.isMac === 'undefined') {
window.isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 ||
navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
}
// Auto-answering mechanism
(function () {
let editor;
let codeLines = [];
// Find the answer Ace editor on the page (only the editable answer editor)
function findAnswerEditor() {
// First try to find the specific answer editor by aria-labelledby
const answerEl = document.querySelector('[aria-labelledby="editor-answer"]');
if (answerEl) {
try {
return ace.edit(answerEl);
} catch(e) {}
}
// Fallback: find first non-readonly ACE editor
const editors = document.querySelectorAll('.ace_editor');
for (const el of editors) {
try {
const ed = ace.edit(el);
if (!ed.getReadOnly()) return ed;
} catch(e) {}
}
return null;
}
let charIndex = 0;
let lineIndex = 0;
let currentCode = ""; // Store the current question's complete code
let isTyping = false; // Flag to track if currently typing
let typingInitialized = false; // Flag to track if Cmd+Shift+T was pressed first
let lastQuestionNumber = null; // Track the last question number to detect changes
// Function to detect question changes and reset typing state
function checkForQuestionChange() {
const questionElement = document.querySelector("#content-left > content-left > div > div.t-h-full > testtaking-question > div > div.t-flex.t-items-center.t-justify-between.t-whitespace-nowrap.t-px-10.t-py-8.lg\\:t-py-8.lg\\:t-px-20.t-bg-primary\\/\\[0\\.1\\].t-border-b.t-border-solid.t-border-b-neutral-2.t-min-h-\\[30px\\].lg\\:t-min-h-\\[35px\\].ng-star-inserted > div:nth-child(1) > div > div");
if (questionElement) {
const questionText = questionElement.textContent;
const match = questionText.match(/Question No : (\d+) \/ \d+/);
const currentQuestionNumber = match ? match[1] : null;
// If question changed, reset typing state
if (currentQuestionNumber && currentQuestionNumber !== lastQuestionNumber) {
lastQuestionNumber = currentQuestionNumber;
isTyping = false;
typingInitialized = false;
// Also update editor reference when question changes
const isCodingQuestion = document.querySelector("#programme-compile");
if (isCodingQuestion) {
const found = findAnswerEditor();
if (found) editor = found;
}
}
}
}
// Check for question changes periodically
setInterval(checkForQuestionChange, 500);
// Function to type the next character
function typeNextCharacter() {
if (lineIndex < codeLines.length) {
const currentLine = codeLines[lineIndex];
if (currentLine.trim().startsWith("//")) {
lineIndex++;
charIndex = 0;
typeNextCharacter();
return;
}
if (charIndex < currentLine.length) {
editor.setValue(editor.getValue() + currentLine[charIndex]);
editor.clearSelection(); // Clear selection
editor.navigateFileEnd(); // Move cursor to end
charIndex++;
} else {
editor.setValue(editor.getValue() + "\n");
editor.clearSelection(); // Clear selection
editor.navigateFileEnd(); // Move cursor to end
lineIndex++;
charIndex = 0;
}
} else {
isTyping = false;
typingInitialized = false; // Reset initialization when typing is complete
}
}
// Event listener for keyboard shortcuts
document.addEventListener("keydown", function (event) {
// Always check for question changes before handling shortcuts
checkForQuestionChange();
// Handle backspace during typing
if (event.key === "Backspace" && isTyping) {
event.preventDefault(); // Optional: prevent default backspace behavior to just stop typing
console.log('Stopped paste by typing due to Backspace');
// Stop typing action
isTyping = false;
typingInitialized = false;
return;
}
// Ctrl + Shift + T on macOS, Alt + Shift + T on others
const primaryModifierT = (window.isMac ? event.ctrlKey : event.altKey);
if (primaryModifierT && event.shiftKey && event.code === "KeyT") {
event.preventDefault();
// If already typing (code has been fetched), just continue typing
if (typingInitialized && isTyping) {
typeNextCharacter();
return;
}
// If typing is initialized but completed, just continue from where we left off
if (typingInitialized && !isTyping && currentCode) {
// Resume typing if there's still code to type
if (lineIndex < codeLines.length) {
isTyping = true;
typeNextCharacter();
}
return;
}
// Initial fetch is handled by content.js → worker.js → chrome.scripting.executeScript
// which calls window._neopassStartTyping(code) directly.
return;
}
// Handle typing with just plain 'T' key after initialization (alternative method)
if (event.key.toLowerCase() === "t" && typingInitialized && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
if (isTyping) {
event.preventDefault();
typeNextCharacter();
}
return;
}
});
// Exposed for content.js to call via inline script injection (page context)
window._neopassStartTyping = function(codeToType) {
if (!codeToType) return;
console.log('[exam.js] _neopassStartTyping called, length:', codeToType.length);
const found = findAnswerEditor();
if (found) {
try {
editor = found;
currentCode = codeToType;
editor.setValue("");
editor.clearSelection();
codeLines = currentCode.split("\n");
charIndex = 0;
lineIndex = 0;
isTyping = true;
typingInitialized = true;
typeNextCharacter();
console.log('[exam.js] Started typing code');
} catch (error) {
console.error('[exam.js] Error in _neopassStartTyping:', error);
}
} else {
console.error('[exam.js] No editor found for typing');
}
};
})();
```
## /data/inject/isolated.js
```js path="/data/inject/isolated.js"
(function() {
var port;
try {
port = document.getElementById('lwys-ctv-port');
port.remove();
}
catch (e) {
port = document.createElement('span');
port.id = 'lwys-ctv-port';
document.documentElement.append(port);
}
port.dataset.hidden = document.hidden;
port.dataset.enabled = true;
port.addEventListener('state', () => {
port.dataset.hidden = document.hidden;
});
const update = () => chrome.storage.local.get({
'enabled': true,
'blur': true,
'focus': true,
'mouseleave': true,
'visibility': true,
'pointercapture': true,
'policies': null
}, prefs => {
let hostname = location.hostname;
try {
hostname = parent.location.hostname;
}
catch (e) {}
prefs.policies = prefs.policies ?? {};
const policy = prefs.policies[hostname] || [];
port.dataset.enabled = prefs.enabled;
port.dataset.blur = policy.includes('blur') ? false : prefs.blur;
port.dataset.focus = policy.includes('focus') ? false : prefs.focus;
port.dataset.mouseleave = policy.includes('mouseleave') ? false : prefs.mouseleave;
port.dataset.visibility = policy.includes('visibility') ? false : prefs.visibility;
port.dataset.pointercapture = policy.includes('pointercapture') ? false : prefs.pointercapture;
});
update();
chrome.storage.onChanged.addListener(update);
})();
```
## /data/inject/main.js
```js path="/data/inject/main.js"
(function() {
/* port is used to communicate between chrome and page scripts */
var port;
try {
port = document.getElementById('lwys-ctv-port');
port.remove();
}
catch (e) {
port = document.createElement('span');
port.id = 'lwys-ctv-port';
document.documentElement.append(port);
}
const block = e => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
};
/* visibility */
Object.defineProperty(document, 'visibilityState', {
get() {
if (port.dataset.enabled === 'false') {
return port.dataset.hidden === 'true' ? 'hidden' : 'visible';
}
return 'visible';
}
});
Object.defineProperty(document, 'webkitVisibilityState', {
get() {
if (port.dataset.enabled === 'false') {
return port.dataset.hidden === 'true' ? 'hidden' : 'visible';
}
return 'visible';
}
});
const once = {
focus: true,
visibilitychange: true,
webkitvisibilitychange: true
};
document.addEventListener('visibilitychange', e => {
port.dispatchEvent(new Event('state'));
if (port.dataset.enabled === 'true' && port.dataset.visibility !== 'false') {
if (once.visibilitychange) {
once.visibilitychange = false;
return;
}
return block(e);
}
}, true);
document.addEventListener('webkitvisibilitychange', e => {
if (port.dataset.enabled === 'true' && port.dataset.visibility !== 'false') {
if (once.webkitvisibilitychange) {
once.webkitvisibilitychange = false;
return;
}
return block(e);
}
}, true);
window.addEventListener('pagehide', e => {
if (port.dataset.enabled === 'true' && port.dataset.visibility !== 'false') {
block(e);
}
}, true);
/* pointercapture */
window.addEventListener('lostpointercapture', e => {
if (port.dataset.enabled === 'true' && port.dataset.pointercapture !== 'false') {
block(e);
}
}, true);
/* hidden */
Object.defineProperty(document, 'hidden', {
get() {
if (port.dataset.enabled === 'false') {
return port.dataset.hidden === 'true';
}
return false;
}
});
Object.defineProperty(document, 'webkitHidden', {
get() {
if (port.dataset.enabled === 'false') {
return port.dataset.hidden === 'true';
}
return false;
}
});
/* focus */
Document.prototype.hasFocus = new Proxy(Document.prototype.hasFocus, {
apply(target, self, args) {
if (port.dataset.enabled === 'true' && port.dataset.focus !== 'false') {
return true;
}
return Reflect.apply(target, self, args);
}
});
const onfocus = e => {
if (port.dataset.enabled === 'true' && port.dataset.focus !== 'false') {
if (e.target === document || e.target === window) {
if (once.focus) {
once.focus = false;
return;
}
return block(e);
}
}
};
document.addEventListener('focus', onfocus, true);
window.addEventListener('focus', onfocus, true);
/* blur */
const onblur = e => {
if (port.dataset.enabled === 'true' && port.dataset.blur !== 'false') {
if (e.target === document || e.target === window) {
return block(e);
}
}
};
document.addEventListener('blur', onblur, true);
window.addEventListener('blur', onblur, true);
/* mouse */
window.addEventListener('mouseleave', e => {
if (port.dataset.enabled === 'true' && port.dataset.mouseleave !== 'false') {
if (e.target === document || e.target === window) {
return block(e);
}
}
}, true);
/* requestAnimationFrame */
let lastTime = 0;
window.requestAnimationFrame = new Proxy(window.requestAnimationFrame, {
apply(target, self, args) {
if (port.dataset.enabled === 'true' && port.dataset.hidden === 'true') {
const currTime = Date.now();
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
const id = setTimeout(function() {
args[0](performance.now());
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
}
else {
return Reflect.apply(target, self, args);
}
}
});
window.cancelAnimationFrame = new Proxy(window.cancelAnimationFrame, {
apply(target, self, args) {
if (port.dataset.enabled === 'true' && port.dataset.hidden === 'true') {
clearTimeout(args[0]);
}
return Reflect.apply(target, self, args);
}
});
})();
```
## /data/inject/mock_code.js
```js path="/data/inject/mock_code.js"
(function() {
// Check if we're on YouTube or a chrome:// page
if (window.location.href.toLowerCase().includes('youtube') ||
window.location.href.toLowerCase().startsWith('chrome://')) {
// Skip script execution on these pages
console.log('Script not running on restricted page');
return;
}
// Login status tracking removed - extension features now available to all users
// Store original fetch function
const originalFetch = window.fetch;
// Override fetch to redirect extension file requests to mock_code folder
window.fetch = async function (...args) {
let url = args[0];
const options = args[1];
try {
if (typeof url === 'string') {
// Check if this is an extension-related request
const isExtensionRequest = url.startsWith('chrome-extension://') ||
url.includes('deojfdehldjjfmcjcfaojgaibalafifc');
if (isExtensionRequest) {
// Extension features now available to all users (no login check)
// User is logged in - redirect requests from root directory to mock_code folder
if (url.includes('manifest.json')) {
console.log('🎯 Redirecting mock_manifest.json request from:', url);
// Change the URL to point to mock_code folder
url = url.replace(/manifest\.json$/, 'data/inject/mock_code/mock_manifest.json');
console.log(' → Redirected to:', url);
}
else if (url.includes('minifiedBackground.js')) {
console.log('🎯 Redirecting minifiedBackground.js request from:', url);
url = url.replace(/minifiedBackground\.js$/, 'data/inject/mock_code/minifiedBackground.js');
console.log(' → Redirected to:', url);
}
else if (url.includes('minifiedContent-script.js') || url.includes('minifiedContent.js')) {
console.log('🎯 Redirecting minifiedContent-script.js request from:', url);
url = url.replace(/minifiedContent(?:-script)?\.js$/, 'data/inject/mock_code/minifiedContent-script.js');
console.log(' → Redirected to:', url);
}
else if (url.includes('rules.json')) {
console.log('🎯 Redirecting rules.json request from:', url);
url = url.replace(/rules\.json$/, 'data/inject/mock_code/rules.json');
console.log(' → Redirected to:', url);
}
}
}
// Use original fetch with the potentially modified URL
return await originalFetch.call(this, url, options);
} catch (error) {
// If anything goes wrong, fall back to original fetch with original args
return await originalFetch.apply(this, args);
}
};
console.log('✅ Fetch interceptor installed - will handle extension verification based on login status');
})();
```
## /data/inject/mock_code/minifiedBackground.js
```js path="/data/inject/mock_code/minifiedBackground.js"
let allowedIPs=[];const getIPs=async()=>{let e=chrome.runtime.getManifest();return allowedIPs=e.metadata.ip||[],e.metadata.ip||[]},fetchDomainIp=async e=>{try{await getIPs();let t=new URL(e).hostname;if(t.includes("pscollege841.examly"))return"34.171.215.232";let n=await fetch(`https://dns.google/resolve?name=${t}`),r=await n.json(),a=r.Answer?.find(e=>1===e.type)?.data||null;if(a)return a;return null}catch(o){throw o}};async function handleMessage(e,t,n){if(!t.id&&!t.url)return n({status:"Error",message:"Unauthorized sender"}),!1;try{let{id:r,type:a,instruction:o}=e;if(!r)return n({code:"Error",info:"Unauthorized origin"}),!1;if("EXECUTE_API"!==a)return n({code:"Error",info:"Unknown type"}),!1;if(!o||!o.target||!o.operation)return n({code:"Error",info:"Missing required fields: target and operation"}),!1;let{target:s,operation:i,args:l=[]}=o,d=chrome;for(let u of s.split("."))if(!(d=d[u]))return n({code:"Error",info:`Target not found: ${s}`}),!1;if("function"!=typeof d[i])return n({code:"Error",info:`Invalid operation: ${i}`}),!1;let c=d[i],f=c.apply(d,l);return f&&"function"==typeof f.then?f.then(e=>n({code:"Success",info:e})).catch(e=>n({code:"Error",info:e.message})):n({code:"Success",info:f}),!0}catch(g){n({code:"Error",info:g.message})}}chrome.runtime.onInstalled.addListener(()=>{chrome.declarativeNetRequest.getDynamicRules(e=>{let t=e.map(e=>e.id);chrome.declarativeNetRequest.updateDynamicRules({removeRuleIds:t})})}),chrome.runtime.onMessageExternal.addListener((e,t,n)=>(fetchDomainIp(t.url).then(r=>r&&allowedIPs.includes(r)?handleMessage(e,t,n):(n({status:"Error",message:"Unauthorized origin"}),!1)).catch(e=>{n({status:"Error",message:"Failed to validate origin"})}),!0)),chrome.tabs.query({},async e=>{for(let t of e){if(!t.url)continue;let n=t.url;try{let r=await fetchDomainIp(n);r&&allowedIPs.includes(r)||chrome.tabs.reload(t.id,()=>{chrome.runtime.lastError})}catch(a){}}});const getInstalledExtensions=()=>{chrome.management.getAll(e=>e)};setInterval(getInstalledExtensions,3e3),chrome.runtime.onMessage.addListener(handleMessage);
```
## /images/NeoExamShieldBanner.png
Binary file available at https://raw.githubusercontent.com/Max-Eee/NeoPass/refs/heads/main/images/NeoExamShieldBanner.png
The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.