@ojas-sta/qalify-plus 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ class AnalyzerLayer {
2
+ constructor() {
3
+ this.stats = {
4
+ totalAttempts: 0,
5
+ domSuccesses: 0,
6
+ ocrAttempts: 0,
7
+ ocrSuccesses: 0,
8
+ aiResponses: 0
9
+ };
10
+ this.vulnerabilities = [];
11
+ }
12
+
13
+ evaluate(extractedData, extractionMethod, aiOutput, stressMode) {
14
+ this.stats.totalAttempts++;
15
+
16
+ let report = {
17
+ scrapableViaDOM: false,
18
+ ocrSucceeded: false,
19
+ aiSuccessfullyAnswered: false,
20
+ vulnerabilities: [],
21
+ defenses: []
22
+ };
23
+
24
+ if (extractionMethod === 'DOM' && extractedData) {
25
+ this.stats.domSuccesses++;
26
+ report.scrapableViaDOM = true;
27
+ report.vulnerabilities.push("Easily Scrapable via DOM: The question and options are presented in semantic HTML that bots can easily parse.");
28
+ report.defenses.push("DOM Obfuscation: Randomize class names, insert invisible decoy text, or split characters into separate span tags to confuse naive scrapers.");
29
+ }
30
+
31
+ if (extractionMethod === 'OCR') {
32
+ this.stats.ocrAttempts++;
33
+ if (extractedData && extractedData.question && extractedData.question.length > 5) {
34
+ this.stats.ocrSuccesses++;
35
+ report.ocrSucceeded = true;
36
+ report.vulnerabilities.push("OCR Susceptible: Text is rendered clearly with high contrast, allowing fallback OCR tools to easily extract the quiz contents.");
37
+ report.defenses.push("Canvas Rendering / Distortion: Render text to a canvas element with subtle background noise or distortions that are readable by humans but trip up OCR engines like Tesseract.");
38
+ } else {
39
+ report.vulnerabilities.push("Partial OCR defense detected (or OCR failed). Text was not easily readable.");
40
+ }
41
+ }
42
+
43
+ if (aiOutput && aiOutput.selectedOption) {
44
+ this.stats.aiResponses++;
45
+ report.aiSuccessfullyAnswered = true;
46
+ if (aiOutput.confidenceScore > 80) {
47
+ report.vulnerabilities.push("Easily Queryable: The question format is standard and can be directly piped into an LLM for an accurate answer.");
48
+ report.defenses.push("Contextual/Visual Questions: Use questions that require interpreting a complex image or diagram, making it harder for text-only LLMs to solve without multi-modal processing.");
49
+ }
50
+ }
51
+
52
+ // Timing patterns (simulated)
53
+ report.vulnerabilities.push("Predictable Timing: The bot completed extraction and AI reasoning in a very short, non-human timeframe.");
54
+ report.defenses.push("Behavioral Monitoring & Timing Checks: Monitor the time between page load and 'answer selection'. Bots often interact too quickly or with mathematically perfect intervals. Implement 'honeypot' delays.");
55
+
56
+ // If stress mode is active but the bot still succeeded, highlight that DOM randomization isn't enough
57
+ if (stressMode && report.ocrSucceeded) {
58
+ report.vulnerabilities.push("Weak Obfuscation: While the DOM was obfuscated (stress mode), the visual rendering was unaffected, allowing OCR to bypass the DOM defenses.");
59
+ }
60
+
61
+ this.vulnerabilities.push(report);
62
+ return report;
63
+ }
64
+
65
+ printSummary() {
66
+ console.log("\n=================================================");
67
+ console.log(" VULNERABILITY & DEFENSE RECOMMENDATION REPORT ");
68
+ console.log("=================================================");
69
+ console.log(`Total Quiz Attempts: ${this.stats.totalAttempts}`);
70
+ console.log(`Successful DOM Scrapes: ${this.stats.domSuccesses}`);
71
+ console.log(`OCR Fallback Attempts: ${this.stats.ocrAttempts}`);
72
+ console.log(`Successful OCR Extracts: ${this.stats.ocrSuccesses}`);
73
+ console.log(`Valid AI Responses: ${this.stats.aiResponses}`);
74
+
75
+ console.log("\n--- Common Vulnerabilities Detected ---");
76
+ const allVulns = new Set();
77
+ const allDefs = new Set();
78
+
79
+ this.vulnerabilities.forEach(v => {
80
+ v.vulnerabilities.forEach(vuln => allVulns.add(vuln));
81
+ v.defenses.forEach(def => allDefs.add(def));
82
+ });
83
+
84
+ allVulns.forEach(v => console.log(`[!] ${v}`));
85
+
86
+ console.log("\n--- Recommended Defenses ---");
87
+ allDefs.forEach(d => console.log(`[+] ${d}`));
88
+
89
+ console.log("=================================================\n");
90
+ }
91
+ }
92
+
93
+ module.exports = AnalyzerLayer;
package/src/browser.js ADDED
@@ -0,0 +1,229 @@
1
+ const { chromium } = require('playwright');
2
+
3
+ class BrowserLayer {
4
+ constructor() {
5
+ this.browser = null;
6
+ this.page = null;
7
+ }
8
+
9
+ async init() {
10
+ console.log("[Browser] Attempting to connect to existing Chrome (CDP on 9222)...");
11
+ try {
12
+ this.browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
13
+ console.log("[Browser] Connected to existing browser via Remote Debugging.");
14
+ const context = this.browser.contexts()[0];
15
+ this.page = await context.newPage();
16
+ } catch (error) {
17
+ console.log("[Browser] Could not connect to CDP. Launching new visual browser.");
18
+ this.browser = await chromium.launch({ headless: false });
19
+ this.page = await this.browser.newPage();
20
+ }
21
+
22
+ await this.page.addInitScript(() => {
23
+ document.addEventListener('DOMContentLoaded', () => {
24
+ const cursor = document.createElement('div');
25
+ cursor.id = 'ai-custom-cursor';
26
+ cursor.style.width = '20px';
27
+ cursor.style.height = '20px';
28
+ cursor.style.borderRadius = '50%';
29
+ cursor.style.backgroundColor = 'rgba(255, 0, 0, 0.7)';
30
+ cursor.style.border = '2px solid white';
31
+ cursor.style.position = 'absolute';
32
+ cursor.style.zIndex = '2147483647';
33
+ cursor.style.pointerEvents = 'none';
34
+ cursor.style.transition = 'top 0.05s, left 0.05s';
35
+ cursor.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
36
+ document.body.appendChild(cursor);
37
+
38
+ document.addEventListener('mousemove', (e) => {
39
+ cursor.style.left = (e.pageX - 10) + 'px';
40
+ cursor.style.top = (e.pageY - 10) + 'px';
41
+ });
42
+ });
43
+ });
44
+ }
45
+
46
+ async setupChatCallback(callback) {
47
+ // Expose a function that the browser UI can call
48
+ await this.page.exposeFunction('sendChatMessageToNode', async (msg) => {
49
+ return await callback(msg);
50
+ });
51
+ }
52
+
53
+ async navigateToQuiz(url, stressMode = false) {
54
+ const finalUrl = stressMode ? `${url}?stress=true` : url;
55
+ console.log(`[Browser] Navigating to: ${finalUrl}`);
56
+ await this.page.goto(finalUrl);
57
+ await this.page.waitForLoadState('networkidle');
58
+ }
59
+
60
+ async extractFromDOM() {
61
+ console.log("[Browser] Attempting DOM extraction...");
62
+ try {
63
+ const questionElement = await this.page.$('.question-text');
64
+ const optionsElements = await this.page.$$('.option label');
65
+
66
+ if (!questionElement || optionsElements.length === 0) {
67
+ return null;
68
+ }
69
+
70
+ const questionText = await questionElement.innerText();
71
+ const options = [];
72
+ for (const el of optionsElements) {
73
+ options.push(await el.innerText());
74
+ }
75
+
76
+ return {
77
+ question: questionText.trim(),
78
+ options: options.map(o => o.trim())
79
+ };
80
+ } catch (error) {
81
+ console.error("[Browser] DOM Extraction encountered error:", error.message);
82
+ return null;
83
+ }
84
+ }
85
+
86
+ async captureScreenshot(outputPath) {
87
+ await this.page.screenshot({ path: outputPath, fullPage: true });
88
+ }
89
+
90
+ async showAIReasoning(aiOutput) {
91
+ console.log("[Browser] Displaying AI Reasoning Overlay with Chatbot...");
92
+ await this.page.evaluate((aiData) => {
93
+ let overlay = document.getElementById('ai-reasoning-overlay');
94
+ if (overlay) {
95
+ overlay.remove(); // Remove old overlay if cycling
96
+ }
97
+
98
+ overlay = document.createElement('div');
99
+ overlay.id = 'ai-reasoning-overlay';
100
+ overlay.style.position = 'fixed';
101
+ overlay.style.bottom = '20px';
102
+ overlay.style.right = '20px';
103
+ overlay.style.width = '350px';
104
+ overlay.style.backgroundColor = '#1e1e1e';
105
+ overlay.style.color = '#f0f0f0';
106
+ overlay.style.padding = '20px';
107
+ overlay.style.borderRadius = '12px';
108
+ overlay.style.boxShadow = '0 10px 30px rgba(0,0,0,0.5)';
109
+ overlay.style.zIndex = '2147483646';
110
+ overlay.style.fontFamily = 'sans-serif';
111
+ overlay.style.border = '1px solid #444';
112
+
113
+ overlay.innerHTML = `
114
+ <h3 style="margin-top: 0; color: #4caf50;">AI Agent Analysis</h3>
115
+ <p><strong>Selected:</strong> Option ${aiData.selectedOption}</p>
116
+ <p><strong>Confidence:</strong> ${aiData.confidenceScore}%</p>
117
+ <p style="font-size: 0.9em; line-height: 1.4; color: #aaa; max-height:80px; overflow-y:auto;"><strong>Reasoning:</strong><br/> ${aiData.reasoning}</p>
118
+ <hr style="border-color: #444; margin: 15px 0;" />
119
+ <div id="ai-chat-history" style="height: 150px; overflow-y: auto; margin-bottom: 10px; font-size: 0.85em; color: #ccc;"></div>
120
+ <div style="display: flex;">
121
+ <input type="text" id="ai-chat-input" placeholder="Ask AI a follow-up..." style="flex-grow: 1; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #333; color: white;" />
122
+ <button id="ai-chat-send" style="margin-left: 5px; background: #4caf50; color: white; border: none; border-radius: 4px; padding: 8px 12px; cursor: pointer; font-weight:bold;">Send</button>
123
+ </div>
124
+ `;
125
+ document.body.appendChild(overlay);
126
+
127
+ // Wire up chat functionality
128
+ const sendBtn = document.getElementById('ai-chat-send');
129
+ const inputField = document.getElementById('ai-chat-input');
130
+ const history = document.getElementById('ai-chat-history');
131
+
132
+ const sendMessage = async () => {
133
+ const msg = inputField.value.trim();
134
+ if (!msg) return;
135
+
136
+ // Disable while waiting
137
+ inputField.disabled = true;
138
+ sendBtn.disabled = true;
139
+
140
+ history.innerHTML += `<div style="margin-bottom:8px;"><b>You:</b> ${msg}</div>`;
141
+ inputField.value = '';
142
+ history.scrollTop = history.scrollHeight;
143
+
144
+ if (window.sendChatMessageToNode) {
145
+ try {
146
+ const aiResponse = await window.sendChatMessageToNode(msg);
147
+ history.innerHTML += `<div style="margin-bottom:8px; color: #4caf50;"><b>AI:</b> ${aiResponse}</div>`;
148
+ } catch (e) {
149
+ history.innerHTML += `<div style="margin-bottom:8px; color: red;"><b>Error:</b> Failed to get response.</div>`;
150
+ }
151
+ }
152
+
153
+ history.scrollTop = history.scrollHeight;
154
+ inputField.disabled = false;
155
+ sendBtn.disabled = false;
156
+ inputField.focus();
157
+ };
158
+
159
+ sendBtn.addEventListener('click', sendMessage);
160
+ inputField.addEventListener('keypress', (e) => {
161
+ if (e.key === 'Enter') sendMessage();
162
+ });
163
+
164
+ }, aiOutput);
165
+ }
166
+
167
+ async clickOption(selectedLetter) {
168
+ console.log(`[Browser] Simulating click on option ${selectedLetter}`);
169
+
170
+ const targetElement = await this.page.evaluateHandle((letter) => {
171
+ const standardOptions = Array.from(document.querySelectorAll('.option'));
172
+ const target = standardOptions.find(opt => opt.getAttribute('data-val') === letter);
173
+ if (target) return target;
174
+ return document.getElementById(`q_${letter.toLowerCase()}`) || document.querySelector('body');
175
+ }, selectedLetter);
176
+
177
+ if (targetElement) {
178
+ const box = await targetElement.boundingBox();
179
+ if (box) {
180
+ const targetX = box.x + box.width / 2;
181
+ const targetY = box.y + box.height / 2;
182
+
183
+ await this.page.mouse.move(targetX, targetY, { steps: 25 });
184
+ await this.page.waitForTimeout(500);
185
+ await this.page.mouse.down();
186
+ await this.page.waitForTimeout(100);
187
+ await this.page.mouse.up();
188
+ }
189
+ }
190
+ }
191
+
192
+ async getCurrentContainerText() {
193
+ return await this.page.evaluate(() => {
194
+ const c = document.getElementById('q-container');
195
+ return c ? c.textContent.trim() : "";
196
+ });
197
+ }
198
+
199
+ async waitForQuestionChange(previousText) {
200
+ console.log("[Browser] Monitoring page for question changes...");
201
+ try {
202
+ await this.page.waitForFunction((prev) => {
203
+ const c = document.getElementById('q-container');
204
+ if (!c) return false;
205
+ const current = c.textContent.trim();
206
+ return current !== prev && current !== "";
207
+ }, previousText, { timeout: 0 }); // Wait indefinitely until the user clicks 'Next'
208
+
209
+ const newText = await this.getCurrentContainerText();
210
+ if (newText.includes("Quiz Completed")) {
211
+ console.log("[Browser] Quiz Completed state detected.");
212
+ return false; // stop loop
213
+ }
214
+ return true; // continue loop
215
+ } catch (error) {
216
+ console.error("Error waiting for change:", error);
217
+ return false;
218
+ }
219
+ }
220
+
221
+ async close() {
222
+ if (this.browser) {
223
+ if (this.page) await this.page.close();
224
+ await this.browser.close();
225
+ }
226
+ }
227
+ }
228
+
229
+ module.exports = BrowserLayer;
package/src/generic.js ADDED
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env node
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ const globalEnvPath = path.join(os.homedir(), '.ai-quiz-assistant.env');
7
+ require('dotenv').config();
8
+ require('dotenv').config({ path: globalEnvPath }); // Load global keys if they exist
9
+
10
+ const { chromium } = require('playwright');
11
+ const readline = require('readline');
12
+ const OCRLayer = require('./ocr');
13
+ const AILayer = require('./ai');
14
+ const { marked } = require('marked');
15
+
16
+ const rl = readline.createInterface({
17
+ input: process.stdin,
18
+ output: process.stdout
19
+ });
20
+
21
+ async function main() {
22
+ const url = process.argv[2];
23
+ if (!url) {
24
+ console.error("Usage: node src/generic.js <URL>");
25
+ process.exit(1);
26
+ }
27
+
28
+ // Check for API Keys
29
+ if (!process.env.GEMINI_API_KEY && !process.env.OPENROUTER_API_KEY) {
30
+ console.log("\n❌ No API keys found. Let's set them up globally!");
31
+ let geminiKey = await new Promise(resolve => rl.question("Enter your Gemini API Key (or press Enter to skip): ", resolve));
32
+ let openRouterKey = await new Promise(resolve => rl.question("Enter your OpenRouter API Key (or press Enter to skip): ", resolve));
33
+
34
+ geminiKey = geminiKey ? geminiKey.trim().replace(/^["']|["']$/g, '') : '';
35
+ openRouterKey = openRouterKey ? openRouterKey.trim().replace(/^["']|["']$/g, '') : '';
36
+
37
+ if (!geminiKey && !openRouterKey) {
38
+ console.error("You must provide at least one API key. Exiting.");
39
+ process.exit(1);
40
+ }
41
+
42
+ let envContent = '';
43
+ if (geminiKey) {
44
+ envContent += `GEMINI_API_KEY=${geminiKey}\n`;
45
+ process.env.GEMINI_API_KEY = geminiKey;
46
+ }
47
+ if (openRouterKey) {
48
+ envContent += `OPENROUTER_API_KEY=${openRouterKey}\n`;
49
+ process.env.OPENROUTER_API_KEY = openRouterKey;
50
+ }
51
+
52
+ fs.writeFileSync(globalEnvPath, envContent);
53
+ console.log(`✅ Saved keys permanently to ${globalEnvPath}\n`);
54
+ }
55
+
56
+ console.log(`[Qalify+] Launching browser to navigate to: ${url}`);
57
+ const browser = await chromium.launch({ headless: false });
58
+ const context = await browser.newContext({ viewport: null });
59
+ const page = await context.newPage();
60
+
61
+ const ocrLayer = new OCRLayer();
62
+ let currentAILayer = new AILayer();
63
+
64
+ // 1. Expose a function so the browser UI can trigger the Node.js OCR pipeline
65
+ await page.exposeFunction('triggerNodeOCR', async (modelName) => {
66
+ currentAILayer = new AILayer(modelName || 'gemini-2.5-flash');
67
+ console.log(`\n[Overlay] 'Analyze' button clicked! Model: ${currentAILayer.modelName}`);
68
+
69
+ // Hide the overlay so it isn't captured in the screenshot
70
+ await page.evaluate(() => {
71
+ const container = document.getElementById('ai-assistant-container');
72
+ if(container) container.style.display = 'none';
73
+ });
74
+
75
+ const screenshotPath = path.join(__dirname, '..', 'temp_screenshot.png');
76
+ await page.screenshot({ path: screenshotPath });
77
+
78
+ // Bring the overlay back immediately after screenshot
79
+ await page.evaluate(() => {
80
+ const container = document.getElementById('ai-assistant-container');
81
+ if(container) container.style.display = 'block';
82
+ });
83
+
84
+ const preprocessedPath = path.join(__dirname, '..', 'temp_preprocessed.png');
85
+ const finalImagePath = await ocrLayer.preprocessImage(screenshotPath, preprocessedPath);
86
+
87
+ const ocrResult = await ocrLayer.extractText(finalImagePath);
88
+ if (!ocrResult || !ocrResult.question) {
89
+ console.error("[Generic] OCR failed to extract meaningful text.");
90
+ return { error: "OCR failed to extract clear text." };
91
+ } else {
92
+ console.log(`\n--- Extracted OCR Text ---\n${ocrResult.question}`);
93
+ const answer = await currentAILayer.determineAnswer(ocrResult.question, ocrResult.options);
94
+ if (answer && answer.reasoning) {
95
+ answer.reasoning = marked.parse(answer.reasoning);
96
+ }
97
+ return answer || { error: "AI failed to generate an answer." };
98
+ }
99
+ });
100
+
101
+ // 1b. Expose a function for DOM text extraction
102
+ await page.exposeFunction('triggerNodeDOM', async (pageText, modelName) => {
103
+ currentAILayer = new AILayer(modelName || 'gemini-2.5-flash');
104
+ console.log(`\n[Overlay] 'Analyze (DOM)' button clicked! Model: ${currentAILayer.modelName}`);
105
+ if (!pageText || pageText.trim() === '') {
106
+ return { error: "No text found in DOM." };
107
+ }
108
+ console.log(`\n--- Extracted DOM Text ---\n${pageText.substring(0, 300)}...`);
109
+ // We pass the raw text as the question and an empty array for options.
110
+ const answer = await currentAILayer.determineAnswer(pageText.substring(0, 5000), []);
111
+ if (answer && answer.reasoning) {
112
+ answer.reasoning = marked.parse(answer.reasoning);
113
+ }
114
+ return answer || { error: "AI failed to generate an answer." };
115
+ });
116
+
117
+ // 1c. Expose a function for Chat interaction
118
+ await page.exposeFunction('triggerNodeChat', async (message) => {
119
+ const response = await currentAILayer.chat(message);
120
+ return marked.parse(response);
121
+ });
122
+
123
+ // 2. Inject the UI overlay every time the page finishes loading or navigating
124
+ page.on('domcontentloaded', async (currentPage) => {
125
+ try {
126
+ await currentPage.evaluate(() => {
127
+ if (document.getElementById('ai-reasoning-overlay')) return;
128
+
129
+ const container = document.createElement('div');
130
+ container.id = 'ai-assistant-container';
131
+ container.style.cssText = 'position: fixed; bottom: 20px; right: 20px; z-index: 2147483647; font-family: sans-serif;';
132
+
133
+ const restoreBtn = document.createElement('button');
134
+ restoreBtn.id = 'ai-restore-btn';
135
+ restoreBtn.style.cssText = 'display: none; width: 40px; height: 40px; background: #4caf50; color: white; border: none; cursor: pointer; border-radius: 50%; font-weight: bold; font-size: 18px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); padding: 0; text-align: center; line-height: 40px;';
136
+ restoreBtn.innerText = 'Q';
137
+
138
+ const overlay = document.createElement('div');
139
+ overlay.id = 'ai-reasoning-overlay';
140
+ overlay.style.cssText = `
141
+ width: 350px;
142
+ background-color: #1e1e1e; color: #f0f0f0; padding: 20px;
143
+ border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.8);
144
+ border: 1px solid #444; position: relative;
145
+ `;
146
+ overlay.innerHTML = `
147
+ <button id="ai-hide-btn" style="position: absolute; top: 10px; right: 10px; background: transparent; border: none; color: #aaa; cursor: pointer; font-size: 16px;">✖</button>
148
+ <h3 style="margin-top: 0; margin-bottom: 15px; color: #4caf50;">Qalify+ Assistant</h3>
149
+ <select id="ai-model-select" style="width: 100%; height: 36px; padding-left: 8px; margin-bottom: 8px; background: #2a2a2a; color: white; border: 1px solid #444; border-radius: 4px; font-size: 14px; box-sizing: border-box; cursor: pointer;">
150
+ <option value="gemini-2.5-flash">Gemini 2.5 Flash</option>
151
+ <option value="openrouter/owl-alpha">Owl Alpha (Academia)</option>
152
+ <option value="openai/gpt-oss-120b">GPT-OSS 120B (Reasoning)</option>
153
+ <option value="poolside/laguna-m.1:free">Laguna M.1 (Science/Code)</option>
154
+ <option value="qwen/qwen3-coder:free">Qwen3 480B (Math)</option>
155
+ </select>
156
+ <button id="ai-analyze-dom-btn" style="width: 100%; padding: 10px; background: #2196F3; color: white; border: none; cursor: pointer; border-radius: 4px; font-weight: bold; margin-bottom: 8px;">Analyze Screen (DOM)</button>
157
+ <button id="ai-analyze-ocr-btn" style="width: 100%; padding: 10px; background: #4caf50; color: white; border: none; cursor: pointer; border-radius: 4px; font-weight: bold;">Analyze Screen (OCR)</button>
158
+ <div id="ai-result" style="margin-top: 15px; font-size: 0.9em; display: none;"></div>
159
+ <div id="ai-chat-container" style="margin-top: 15px; display: none; border-top: 1px solid #444; padding-top: 10px;">
160
+ <div id="ai-chat-log" style="max-height: 150px; overflow-y: auto; margin-bottom: 10px; font-size: 0.85em; display: flex; flex-direction: column; gap: 8px;"></div>
161
+ <div style="display: flex; gap: 5px;">
162
+ <input type="text" id="ai-chat-input" placeholder="Ask a follow-up..." style="flex: 1; padding: 8px; border-radius: 4px; border: 1px solid #444; background: #2a2a2a; color: white;">
163
+ <button id="ai-chat-send" style="padding: 8px 12px; background: #4caf50; color: white; border: none; border-radius: 4px; cursor: pointer;">Send</button>
164
+ </div>
165
+ </div>
166
+ `;
167
+ container.appendChild(restoreBtn);
168
+ container.appendChild(overlay);
169
+ document.body.appendChild(container);
170
+
171
+ // Setup Hide/Show Logic
172
+ document.getElementById('ai-hide-btn').addEventListener('click', () => {
173
+ overlay.style.display = 'none';
174
+ restoreBtn.style.display = 'block';
175
+ });
176
+
177
+ restoreBtn.addEventListener('click', () => {
178
+ restoreBtn.style.display = 'none';
179
+ overlay.style.display = 'block';
180
+ });
181
+
182
+ // Setup DOM Button
183
+ document.getElementById('ai-analyze-dom-btn').addEventListener('click', async () => {
184
+ const resultDiv = document.getElementById('ai-result');
185
+ resultDiv.style.display = 'block';
186
+ resultDiv.innerHTML = "<em>Extracting DOM & Analyzing...</em>";
187
+ document.getElementById('ai-analyze-dom-btn').disabled = true;
188
+ document.getElementById('ai-analyze-ocr-btn').disabled = true;
189
+
190
+ try {
191
+ const container = document.getElementById('ai-assistant-container');
192
+ container.style.display = 'none';
193
+ const pageText = document.body.innerText;
194
+ container.style.display = 'block';
195
+
196
+ const selectedModel = document.getElementById('ai-model-select').value;
197
+ const answer = await window.triggerNodeDOM(pageText, selectedModel);
198
+ if (answer.error) {
199
+ resultDiv.innerHTML = `<div style="color: red;">${answer.error}</div>`;
200
+ } else {
201
+ resultDiv.innerHTML = `
202
+ <p><strong>Selected:</strong> Option ${answer.selectedOption}</p>
203
+ <p><strong>Confidence:</strong> ${answer.confidenceScore}%</p>
204
+ <p style="color: #aaa;"><strong>Reasoning:</strong><br/>${answer.reasoning}</p>
205
+ `;
206
+ }
207
+ } catch (e) {
208
+ resultDiv.innerHTML = `<div style="color: red;">Error: ${e.message}</div>`;
209
+ }
210
+ document.getElementById('ai-analyze-dom-btn').disabled = false;
211
+ document.getElementById('ai-analyze-ocr-btn').disabled = false;
212
+ if (!resultDiv.innerHTML.includes('Error')) {
213
+ document.getElementById('ai-chat-container').style.display = 'block';
214
+ }
215
+ });
216
+
217
+ // Setup OCR Button
218
+ document.getElementById('ai-analyze-ocr-btn').addEventListener('click', async () => {
219
+ const resultDiv = document.getElementById('ai-result');
220
+ resultDiv.style.display = 'block';
221
+ resultDiv.innerHTML = "<em>Taking screenshot & running OCR...</em>";
222
+ document.getElementById('ai-analyze-dom-btn').disabled = true;
223
+ document.getElementById('ai-analyze-ocr-btn').disabled = true;
224
+
225
+ try {
226
+ const selectedModel = document.getElementById('ai-model-select').value;
227
+ const answer = await window.triggerNodeOCR(selectedModel);
228
+ if (answer.error) {
229
+ resultDiv.innerHTML = `<div style="color: red;">${answer.error}</div>`;
230
+ } else {
231
+ resultDiv.innerHTML = `
232
+ <p><strong>Selected:</strong> Option ${answer.selectedOption}</p>
233
+ <p><strong>Confidence:</strong> ${answer.confidenceScore}%</p>
234
+ <p style="color: #aaa;"><strong>Reasoning:</strong><br/>${answer.reasoning}</p>
235
+ `;
236
+ }
237
+ } catch (e) {
238
+ resultDiv.innerHTML = `<div style="color: red;">Error: ${e.message}</div>`;
239
+ }
240
+ document.getElementById('ai-analyze-dom-btn').disabled = false;
241
+ document.getElementById('ai-analyze-ocr-btn').disabled = false;
242
+ });
243
+
244
+ // Setup Chat
245
+ const chatSendBtn = document.getElementById('ai-chat-send');
246
+ const chatInput = document.getElementById('ai-chat-input');
247
+ const chatLog = document.getElementById('ai-chat-log');
248
+
249
+ async function handleChat() {
250
+ const msg = chatInput.value.trim();
251
+ if (!msg) return;
252
+
253
+ chatLog.innerHTML += `<div style="align-self: flex-end; background: #2196F3; padding: 6px 10px; border-radius: 12px; max-width: 80%;">${msg}</div>`;
254
+ chatInput.value = '';
255
+ chatSendBtn.disabled = true;
256
+
257
+ const loadingId = 'loading-' + Date.now();
258
+ chatLog.innerHTML += `<div id="${loadingId}" style="align-self: flex-start; background: #444; padding: 6px 10px; border-radius: 12px; max-width: 80%;"><em>...</em></div>`;
259
+ chatLog.scrollTop = chatLog.scrollHeight;
260
+
261
+ try {
262
+ const response = await window.triggerNodeChat(msg);
263
+ document.getElementById(loadingId).innerHTML = response;
264
+ } catch (e) {
265
+ document.getElementById(loadingId).innerHTML = `<span style="color:red">Error: ${e.message}</span>`;
266
+ }
267
+ chatSendBtn.disabled = false;
268
+ chatLog.scrollTop = chatLog.scrollHeight;
269
+ }
270
+
271
+ chatSendBtn.addEventListener('click', handleChat);
272
+ chatInput.addEventListener('keypress', (e) => {
273
+ if (e.key === 'Enter') handleChat();
274
+ });
275
+ });
276
+ } catch (e) {
277
+ // Ignore frame injection errors during navigation
278
+ }
279
+ });
280
+
281
+ await page.goto(url);
282
+ console.log("\n==================================================================");
283
+ console.log("[Qalify+] Browser launched. The Qalify+ Overlay has been injected!");
284
+ console.log("==================================================================");
285
+ console.log("- Click 'Analyze Screen (OCR)' in the browser to run the pipeline.");
286
+ console.log("- Type messages here in the terminal to chat with the AI.");
287
+ console.log("- Type 'exit' to close the simulator.");
288
+ console.log("==================================================================\n");
289
+
290
+ rl.on('line', async (input) => {
291
+ const command = input.trim().toLowerCase();
292
+ if (command === 'exit' || command === 'quit') {
293
+ console.log("[Generic] Closing...");
294
+ await browser.close();
295
+ process.exit(0);
296
+ } else if (command !== '') {
297
+ console.log(`\\n[You]: ${input}`);
298
+ const response = await currentAILayer.chat(input);
299
+ console.log(`[AI]: ${response}\\n`);
300
+ }
301
+ });
302
+ }
303
+
304
+ main().catch(console.error);