Showing preview only (637K chars total). Download the full file or copy to clipboard to get everything.
Repository: Max-Eee/NeoPass
Branch: main
Commit: 079ea379f5db
Files: 25
Total size: 617.9 KB
Directory structure:
gitextract_mqhp625o/
├── README.md
├── contentScript.js
├── data/
│ ├── inject/
│ │ ├── anti-anti-debug.js
│ │ ├── chatbot.js
│ │ ├── content.js
│ │ ├── copyOverride.js
│ │ ├── customPaste.js
│ │ ├── exam.js
│ │ ├── isolated.js
│ │ ├── main.js
│ │ ├── mock_code/
│ │ │ ├── minifiedBackground.js
│ │ │ ├── minifiedContent-script.js
│ │ │ ├── mock_manifest.json
│ │ │ └── rules.json
│ │ ├── mock_code.js
│ │ ├── rightclickmenu.js
│ │ └── screenshare.js
│ └── nptel.json
├── devtools.js
├── manifest.json
├── metadata.json
├── nptel.txt
├── popup.html
├── popup.js
└── worker.js
================================================
FILE CONTENTS
================================================
================================================
FILE: 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!
- **`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>
================================================
FILE: 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
}
}
================================================
FILE: 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];
}
});
}
})()
================================================
FILE: 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">$&</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">$&</span>');
// Strings
code = code.replace(/'[^']*'/g, '<span class="string">$&</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">$&</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">$&</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('$') ||
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('$') || 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);
}
});
});
})();
================================================
FILE: 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() {
// 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.';
}
// Send data to background.js for querying
chrome.runtime.sendMessage({
action: 'extractData',
programmingLanguage: programmingLanguage,
question: questionText,
inputFormat: inputFormatText,
outputFormat: outputFormatText,
testCases: testCasesText,
isCoding: true
}, async (response) => {
if (response && response.success && response.response) {
try {
// Clean the response
let cleanedResponse = response.response.trim()
.replace(/^```[a-z]*\n/, '')
.replace(/\n```$/, '');
console.log('[AI Answer] Cleaned response length:', cleanedResponse.length);
// Copy to clipboard first
await navigator.clipboard.writeText(cleanedResponse);
console.log('[AI Answer] Code copied to clipboard');
// Dispatch custom event to page context (where ace is available)
// This will be handled by exam.js which runs in page context
window.dispatchEvent(new CustomEvent('NEOPASS_INSERT_CODE', {
detail: { code: cleanedResponse }
}));
console.log('[AI Answer] Dispatched code insertion event to page context');
} catch (error) {
console.error("Error processing AI 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();
}
});
// 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 butto
gitextract_mqhp625o/ ├── README.md ├── contentScript.js ├── data/ │ ├── inject/ │ │ ├── anti-anti-debug.js │ │ ├── chatbot.js │ │ ├── content.js │ │ ├── copyOverride.js │ │ ├── customPaste.js │ │ ├── exam.js │ │ ├── isolated.js │ │ ├── main.js │ │ ├── mock_code/ │ │ │ ├── minifiedBackground.js │ │ │ ├── minifiedContent-script.js │ │ │ ├── mock_manifest.json │ │ │ └── rules.json │ │ ├── mock_code.js │ │ ├── rightclickmenu.js │ │ └── screenshare.js │ └── nptel.json ├── devtools.js ├── manifest.json ├── metadata.json ├── nptel.txt ├── popup.html ├── popup.js └── worker.js
SYMBOL INDEX (116 symbols across 13 files)
FILE: contentScript.js
function replaceNeoBrowserButton (line 34) | function replaceNeoBrowserButton() {
function sendMessageToWebsite (line 157) | function sendMessageToWebsite(messageData) {
function removeInjectedElement (line 173) | function removeInjectedElement() {
FILE: data/inject/anti-anti-debug.js
function shouldLog (line 49) | function shouldLog(type) {
function wrapFn (line 164) | function wrapFn(newFn, old) {
FILE: data/inject/chatbot.js
function loadShowdown (line 20) | function loadShowdown() {
function loadPrism (line 35) | function loadPrism() {
function getShadowElement (line 431) | function getShadowElement(id) {
function getShadowRoot (line 437) | function getShadowRoot() {
function getChatButton (line 442) | function getChatButton() {
function detectPlatform (line 462) | function detectPlatform() {
function extractExamlyQuestion (line 476) | function extractExamlyQuestion() {
function extractHackerRankQuestion (line 540) | function extractHackerRankQuestion() {
function extractCurrentQuestion (line 695) | function extractCurrentQuestion() {
function formatQuestionForChat (line 707) | function formatQuestionForChat(questionData) {
function createChatOverlay (line 773) | function createChatOverlay() {
function addNotificationMessage (line 1928) | function addNotificationMessage(message) {
function createChatButton (line 1953) | function createChatButton() {
function detectLanguage (line 2127) | function detectLanguage(code) {
function renderChatContent (line 2282) | function renderChatContent(messageContainer, content) {
function addMessageToChat (line 2431) | function addMessageToChat(message, role) {
function clearErrorState (line 2477) | function clearErrorState() {
function createValidContext (line 2494) | function createValidContext(chatHistory) {
function addErrorMessageToChat (line 2530) | function addErrorMessageToChat(errorMessage, isRateLimitError = false) {
function addLoadingIndicator (line 2604) | function addLoadingIndicator() {
function toggleChatOverlay (line 2653) | function toggleChatOverlay() {
function clearChatHistoryAndUI (line 2694) | function clearChatHistoryAndUI(reason = 'manual') {
function blockClashingChatElements (line 2727) | function blockClashingChatElements() {
function init (line 2863) | async function init() {
FILE: data/inject/content.js
function enableTextSelectionGlobally (line 12) | function enableTextSelectionGlobally() {
function htmlToText (line 132) | function htmlToText(element) {
function extractQuestionCodeAndOptions (line 158) | function extractQuestionCodeAndOptions() {
function handleQuestionExtraction (line 188) | async function handleQuestionExtraction() {
function extractCodingQuestion (line 211) | function extractCodingQuestion() {
function solveIamneoExamly (line 348) | function solveIamneoExamly(){
function extractSnippets (line 379) | function extractSnippets() {
function extractHackerRankMCQ (line 715) | function extractHackerRankMCQ() {
function extractHackerRankCoding (line 862) | function extractHackerRankCoding() {
function normalizeCodeIndentation (line 937) | function normalizeCodeIndentation(code) {
function insertCodeIntoMonacoEditor (line 974) | async function insertCodeIntoMonacoEditor(text) {
function handleHackerRankMCQ (line 1101) | function handleHackerRankMCQ() {
FILE: data/inject/copyOverride.js
function customCopy (line 64) | async function customCopy(selectedText) {
function getSelectedText (line 107) | function getSelectedText() {
FILE: data/inject/customPaste.js
function performPasteByTyping (line 1) | async function performPasteByTyping() {
function performDragDropPaste (line 127) | async function performDragDropPaste() {
FILE: data/inject/exam.js
function checkForQuestionChange (line 20) | function checkForQuestionChange() {
function typeNextCharacter (line 50) | function typeNextCharacter() {
function getAnswerFromAI (line 134) | async function getAnswerFromAI() {
FILE: data/inject/main.js
method get (line 22) | get() {
method get (line 30) | get() {
method get (line 78) | get() {
method get (line 86) | get() {
method apply (line 96) | apply(target, self, args) {
method apply (line 141) | apply(target, self, args) {
method apply (line 157) | apply(target, self, args) {
FILE: data/inject/mock_code/minifiedBackground.js
function handleMessage (line 1) | async function handleMessage(e,t,n){if(!t.id&&!t.url)return n({status:"E...
FILE: data/inject/screenshare.js
function bypassRestrictions (line 43) | function bypassRestrictions() {
function spoofScreenRecording (line 105) | function spoofScreenRecording() {
function showPopup (line 121) | function showPopup(resolve, reject, constraints, originalGetDisplayMedia) {
FILE: devtools.js
function isExamPage (line 2) | function isExamPage() {
function injectAntiDebug (line 9) | function injectAntiDebug() {
FILE: popup.js
function autoSaveAPIConfig (line 34) | function autoSaveAPIConfig() {
function clearChatHistoryOnProviderChange (line 70) | function clearChatHistoryOnProviderChange() {
function updateShortcutsForPlatform (line 93) | function updateShortcutsForPlatform() {
function refreshAllTabs (line 154) | function refreshAllTabs() {
function showError (line 163) | function showError(message, duration = 5000) {
function showLoggedInState (line 172) | function showLoggedInState(username, isPro, accountData) {
function displayAccountInfo (line 210) | function displayAccountInfo(account) {
function fetchAccountInfo (line 225) | async function fetchAccountInfo() {
function showLoggedOutState (line 281) | function showLoggedOutState() {
function checkSessionExpiration (line 305) | function checkSessionExpiration() {
function logoutUser (line 319) | function logoutUser() {
function initializeOpacityLevel (line 500) | function initializeOpacityLevel() {
function capitalizeFirstLetter (line 510) | function capitalizeFirstLetter(string) {
function loadAPIConfiguration (line 533) | function loadAPIConfiguration() {
FILE: worker.js
function canMakeRequest (line 13) | function canMakeRequest() {
function blockRequests (line 17) | function blockRequests() {
function unblockRequests (line 32) | function unblockRequests() {
function handleMessage (line 80) | async function handleMessage(request, sender, sendResponse) {
function checkForUpdate (line 218) | async function checkForUpdate() {
function compareVersions (line 268) | function compareVersions(v1, v2) {
function showUpdateToast (line 281) | function showUpdateToast(tabId, message, latestVersion) {
function setupUpdateAlarm (line 557) | function setupUpdateAlarm() {
function isLoggedIn (line 665) | function isLoggedIn(callback) {
function showLoginPrompt (line 672) | function showLoginPrompt(tabId) {
function handleNPTEL (line 901) | function handleNPTEL(result, tabId) {
function getSelectedText (line 934) | function getSelectedText() {
function handleQueryResponse (line 947) | function handleQueryResponse(response, tabId, isMCQ = false) {
function handleQueryResponseForIamNeoExamly (line 989) | function handleQueryResponseForIamNeoExamly(response, tabId, isMCQ = fal...
function queryRequest (line 1039) | async function queryRequest(text, isMCQ = false, isMultipleChoice = fals...
function getCustomAPIConfig (line 1235) | async function getCustomAPIConfig() {
function queryCustomAPI (line 1256) | async function queryCustomAPI(text, isMCQ, isMultipleChoice, config) {
constant API_BASE_URL (line 1414) | const API_BASE_URL = 'https://api.neopass.tech';
function getTokens (line 1417) | async function getTokens() {
function makeAuthenticatedRequest (line 1424) | async function makeAuthenticatedRequest(url, method, token, body = null) {
function handleChatMessage (line 1597) | async function handleChatMessage(message, sender) {
function sendChatResponse (line 1757) | function sendChatResponse(tabId, content) {
function sendChatErrorResponse (line 1766) | function sendChatErrorResponse(tabId, content) {
function copyToClipboard (line 1786) | async function copyToClipboard(text, tabId) {
function copyToClipboard (line 1815) | function copyToClipboard(text) {
function checkStealthMode (line 1844) | async function checkStealthMode() {
function removeExistingToast (line 1866) | function removeExistingToast(tabId) {
function toggleToastOpacity (line 1898) | async function toggleToastOpacity() {
function getToastOpacity (line 1933) | async function getToastOpacity() {
function showOpacityLevelToast (line 1945) | function showOpacityLevelToast(tabId, message) {
function showToast (line 2104) | async function showToast(tabId, message, isError = false, detailedInfo =...
function showStealthToast (line 2318) | async function showStealthToast(tabId, message, stealthEnabled) {
constant SESSION_DURATION (line 2669) | const SESSION_DURATION = 12 * 60 * 60 * 1000;
function checkAndHandleSessionExpiration (line 2673) | async function checkAndHandleSessionExpiration() {
function findAnswer (line 2758) | function findAnswer(query) {
function levenshteinDistance (line 2784) | function levenshteinDistance(s1, s2) {
function normalizeText (line 2806) | function normalizeText(text) {
function loadNptelDataset (line 2816) | async function loadNptelDataset() {
function showMCQToast (line 2830) | async function showMCQToast(tabId, message, detailedInfo = '') {
function showNPTELToast (line 3074) | async function showNPTELToast(tabId, message, isError = false, detailedI...
function showSpinnerToast (line 3287) | async function showSpinnerToast(tabId, message = 'Processing your reques...
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (660K chars).
[
{
"path": "README.md",
"chars": 7911,
"preview": "<img width=\"1500\" height=\"500\" alt=\"NeoPass Banner\" src=\"https://github.com/user-attachments/assets/7369dd86-838d-4fdc-a"
},
{
"path": "contentScript.js",
"chars": 6303,
"preview": "// Check if the chrome object is available (for compatibility)\nif (typeof chrome === \"undefined\") {\n // Handle the case"
},
{
"path": "data/inject/anti-anti-debug.js",
"chars": 5302,
"preview": "!(() => {\n const Proxy = window.Proxy;\n const Object = window.Object;\n const Array = window.Array;\n /**\n "
},
{
"path": "data/inject/chatbot.js",
"chars": 143595,
"preview": "if (typeof chrome === \"undefined\") {}\n\nif (typeof window.isMac === 'undefined') {\n window.isMac = navigator.platform."
},
{
"path": "data/inject/content.js",
"chars": 59495,
"preview": "window.addEventListener('blur', function() {\n window.focus();\n});\n\n// Declare shared isMac variable (this will be the"
},
{
"path": "data/inject/copyOverride.js",
"chars": 7487,
"preview": "// Custom Ctrl+C override functionality - Prevents default copy on divs\n(function() {\n 'use strict';\n\n // Create a"
},
{
"path": "data/inject/customPaste.js",
"chars": 19315,
"preview": "async function performPasteByTyping() {\n console.log('[PasteByTyping] Function called');\n \n const activeElement"
},
{
"path": "data/inject/exam.js",
"chars": 14819,
"preview": "// Use shared isMac variable if it exists, otherwise declare it\nif (typeof window.isMac === 'undefined') {\n window.is"
},
{
"path": "data/inject/isolated.js",
"chars": 1293,
"preview": "(function() {\nvar port;\ntry {\n port = document.getElementById('lwys-ctv-port');\n port.remove();\n}\ncatch (e) {\n port ="
},
{
"path": "data/inject/main.js",
"chars": 4522,
"preview": "(function() {\n /* port is used to communicate between chrome and page scripts */\n var port;\n try {\n port = documen"
},
{
"path": "data/inject/mock_code/minifiedBackground.js",
"chars": 2073,
"preview": "let allowedIPs=[];const getIPs=async()=>{let e=chrome.runtime.getManifest();return allowedIPs=e.metadata.ip||[],e.metada"
},
{
"path": "data/inject/mock_code/minifiedContent-script.js",
"chars": 200,
"preview": "window.addEventListener(\"message\",function(e){e.source===window&&\"extension\"===e.data.target&&chrome.runtime.sendMessage"
},
{
"path": "data/inject/mock_code/mock_manifest.json",
"chars": 2123,
"preview": "{\n \"action\": {\n \"default_icon\": {\n \"16\": \"images/icon16.png\",\n \"48\": \"images/icon48.png\""
},
{
"path": "data/inject/mock_code/rules.json",
"chars": 19341,
"preview": "[\n {\n \"id\": 1,\n \"priority\": 1,\n \"action\": {\n \"type\": \"block\"\n },\n \"condition\": {\n \"urlFilter\":"
},
{
"path": "data/inject/mock_code.js",
"chars": 3260,
"preview": "(function() {\n // Check if we're on YouTube or a chrome:// page\n if (window.location.href.toLowerCase().includes('"
},
{
"path": "data/inject/rightclickmenu.js",
"chars": 225,
"preview": "(function() {\n\"use strict\";\n\nif (!window.__ENABLE_RIGHT_CLICK_SETUP) {\n window.document.addEventListener('contextmenu',"
},
{
"path": "data/inject/screenshare.js",
"chars": 9309,
"preview": "// Mac detection - only declare if not already declared\nlet isMac;\nif (typeof isMac === 'undefined') {\n isMac = navig"
},
{
"path": "data/nptel.json",
"chars": 107548,
"preview": "[\n {\n \"question\": \"\\\"Enquiry into plants\\\" is a book written by\",\n \"answer\": \"Theophrastus\"\n },\n "
},
{
"path": "devtools.js",
"chars": 1073,
"preview": "// Check if URL contains \"/courses\" or \"/test\"\nfunction isExamPage() {\n return window.location.href.includes('/mycour"
},
{
"path": "manifest.json",
"chars": 4096,
"preview": "{\n \"manifest_version\": 3,\n \"name\": \"NeoExamShield\",\n \"version\": \"1.5.1\",\n \"description\": \"To prevent malpractice, id"
},
{
"path": "metadata.json",
"chars": 95,
"preview": "{\n \"ip\": [\n \"34.171.215.232\",\n \"34.233.30.196\",\n \"35.212.92.221\"\n ]\n }\n "
},
{
"path": "nptel.txt",
"chars": 484,
"preview": "const dataset = [];\ndocument.querySelectorAll('.qt-mc-question').forEach(questionBlock => {\n const questionText = que"
},
{
"path": "popup.html",
"chars": 32568,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width"
},
{
"path": "popup.js",
"chars": 30331,
"preview": "document.addEventListener('DOMContentLoaded', function () {\n const isMac = navigator.platform.toUpperCase().indexOf('"
},
{
"path": "worker.js",
"chars": 150011,
"preview": "// Track shortcut execution state to prevent multiple requests when held down\nconst shortcutStates = {\n 'search': false"
}
]
About this extraction
This page contains the full source code of the Max-Eee/NeoPass GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (617.9 KB), approximately 130.7k tokens, and a symbol index with 116 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.