@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,52 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ - run: npm ci
19
+
20
+ publish-npm:
21
+ needs: build
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ contents: read
25
+ packages: write
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: actions/setup-node@v4
29
+ with:
30
+ node-version: 20
31
+ registry-url: https://registry.npmjs.org/
32
+ - run: npm ci
33
+ - run: npm publish --access public
34
+ env:
35
+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
36
+
37
+ publish-gpr:
38
+ needs: build
39
+ runs-on: ubuntu-latest
40
+ permissions:
41
+ contents: read
42
+ packages: write
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ - uses: actions/setup-node@v4
46
+ with:
47
+ node-version: 20
48
+ registry-url: https://npm.pkg.github.com/
49
+ - run: npm ci
50
+ - run: npm publish --access public
51
+ env:
52
+ NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # Qalify+ & Vulnerability Simulator
2
+
3
+ An educational toolkit demonstrating how modern web quizzes can be analyzed and automated using AI (Gemini 2.5 Flash), DOM extraction, and OCR (Tesseract.js).
4
+
5
+ This project explores the security boundaries of online assessments by highlighting how easily DOM-based quizzes can be read, and how even obfuscated (Canvas/Image-based) questions can be bypassed using automated screenshots and Optical Character Recognition (OCR).
6
+
7
+ ---
8
+
9
+ ## 🚀 Features
10
+
11
+ * **Generic Playwright Overlay (`src/generic.js`):** Injects a sleek, interactive dark-mode UI overlay into **any** website you visit. It provides dual-mode extraction:
12
+ * **Analyze (DOM):** Instantly extracts the text from standard HTML web pages.
13
+ * **Analyze (OCR):** Takes a background screenshot, sharpens the image, and runs it through Tesseract.js to defeat anti-scraping canvas/image obfuscation.
14
+ * **Interactive AI Chatbot:** After extracting a question, the UI allows you to chat directly with the AI to ask follow-up questions or have it explain its reasoning.
15
+ * **Multi-Model Support:** Integrates with both Google Gemini and OpenRouter, allowing you to seamlessly swap between models optimized for math (Qwen), academia (Owl Alpha), or coding (Laguna).
16
+ * **Native Markdown Rendering:** Chat bubbles and reasoning outputs natively parse LLM Markdown into clean HTML (bolding, lists, code blocks).
17
+ * **Anti-Inception Capture:** The UI overlay automatically hides itself in a fraction of a millisecond during screen capture to prevent the AI from "reading" its own UI elements.
18
+ * **Chrome Extension Version:** A lightweight Manifest V3 Chrome Extension version of the DOM analyzer, ready for unlisted publishing.
19
+ * **Local Autonomous Loop (`src/main.js`):** An automated loop that navigates a local test quiz, continuously identifying questions, selecting the best AI-determined answer, and auto-clicking the "Next" button.
20
+
21
+ ---
22
+
23
+ ## 🔑 Getting Your API Keys (Required)
24
+
25
+ To power the AI analysis, you will need at least one API key. You can get both entirely for free!
26
+
27
+ ### 1. Google Gemini API Key (Recommended for General Use)
28
+ Gemini 2.5 Flash is incredibly fast, smart, and provides a very generous free tier.
29
+ * **Link:** [Google AI Studio API Keys](https://aistudio.google.com/app/apikey)
30
+ * **How to get it:**
31
+ 1. Click the link above and sign in with your Google Account.
32
+ 2. Click the blue **"Create API key"** button on the screen.
33
+ 3. Select a project (or create a new one) and generate the key.
34
+ 4. Copy your key (it usually starts with `AIza...`).
35
+
36
+ ### 2. OpenRouter API Key (Recommended for Specialized Models)
37
+ OpenRouter gives you access to hundreds of open-source models, including powerful free models specifically optimized for Math (Qwen) and Academia (Owl Alpha).
38
+ * **Link:** [OpenRouter API Keys](https://openrouter.ai/settings/keys)
39
+ * **How to get it:**
40
+ 1. Create a free account on OpenRouter.
41
+ 2. Navigate to your settings and click **"Create Key"**.
42
+ 3. Give the key a name (e.g., "Quiz Assistant").
43
+ 4. Copy the key (it starts with `sk-or-v1-...`). *Note: Make sure you copy it immediately, as it is only shown once!*
44
+
45
+ ---
46
+
47
+ ## 📦 Setup & Installation
48
+
49
+ You do not need to download the code to use the assistant! It has been published globally to the NPM registry.
50
+
51
+ 1. **Ensure Node.js is installed:** Make sure you have [Node.js](https://nodejs.org/) (v18+) installed on your machine.
52
+ 2. **Install Playwright Browsers (One-time setup):**
53
+ Because the assistant uses a hidden browser to read and analyze quizzes, you need to install the browser engine first:
54
+ ```bash
55
+ npx playwright install chromium
56
+ ```
57
+ 3. **Run the universal command:**
58
+ Open your terminal and type:
59
+ ```bash
60
+ npx @ojas-sta/qalify-plus@latest https://google.com
61
+ ```
62
+ 4. **Interactive Setup:** On your very first run, the tool will automatically pause and ask you to paste the API keys you generated above. It securely saves them so you never have to enter them again!
63
+
64
+ ---
65
+
66
+ ## 🕹️ Usage Guide
67
+
68
+ ### Mode 1: The Universal UI Overlay (Recommended)
69
+ This mode launches a custom browser, navigates to a URL you specify, and injects our custom Qalify+ overlay into the page.
70
+
71
+ ```bash
72
+ npx @ojas-sta/qalify-plus@latest "https://example-quiz-site.com/login"
73
+ ```
74
+ * **How to use:** Navigate the browser manually (log in, pass captchas, etc.). When a question is on screen, click either the **DOM** or **OCR** analyze buttons in the floating overlay.
75
+ * **Chat:** After an analysis, a chat window will slide down allowing you to interrogate the AI about its answer.
76
+
77
+ ### Mode 2: Chrome Extension (DOM-only)
78
+ A lightweight version you can install directly into your primary Chrome browser. Because of Chrome Web Store security limits on screenshots and WASM models, this version only uses DOM extraction (no OCR).
79
+
80
+ 1. Open Chrome and navigate to `chrome://extensions`.
81
+ 2. Turn on **Developer mode** (top right).
82
+ 3. Click **Load unpacked** (top left).
83
+ 4. Select the `chrome-extension` folder inside this project.
84
+ 5. Click the extension icon in your browser to open the Side Panel. Don't forget to paste your API Key into the settings!
85
+
86
+ ### Mode 3: The Autonomous Local Simulator
87
+ This runs an aggressive automated bot against a local test file (`test/quiz.html`). It auto-detects questions, selects radio buttons, and advances to the next page entirely on its own.
88
+
89
+ ```bash
90
+ node src/main.js
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 🏗️ Architecture & Modules
96
+
97
+ * **`src/ai.js`:** Wraps the Gemini 2.5 API. Uses strict JSON Schema to force the LLM to return `selectedOption`, `confidenceScore`, and `reasoning`. Also maintains chat history for follow-ups.
98
+ * **`src/ocr.js`:** The heavy lifting. Takes a raw Playwright screenshot, uses Jimp to increase contrast and greyscale the image, and feeds it into the Tesseract.js engine to extract raw text.
99
+ * **`src/analyzer.js`:** Orchestrates the flow between extracting text, querying the AI, and parsing the response.
100
+ * **`src/browser.js`:** The autonomous Playwright controller that hunts for specific DOM selectors like `.question-text` and `#next-btn`.
101
+
102
+ ---
103
+
104
+ ## ⚠️ Disclaimer
105
+
106
+ **Educational Purposes Only.** This toolkit was developed to demonstrate the vulnerabilities inherent in client-side web assessments and to explore the defensive boundaries of CAPTCHAs, Canvas obfuscation, and DOM scraping. Do not use these tools to violate the terms of service of third-party platforms or to bypass academic integrity policies.
@@ -0,0 +1,50 @@
1
+ # Chrome Web Store Metadata: Quiz Assistant
2
+
3
+ ## 1. Store Listing Copy
4
+
5
+ **Title:** Quiz Assistant (Max 45 chars)
6
+ **Short Description:** An interactive AI assistant that analyzes active web pages to identify and explain educational quiz questions. (Max 132 chars)
7
+
8
+ **Detailed Description:**
9
+ Quiz Assistant is an educational tool designed to help you understand complex questions and study material on the web. By opening the side panel, the assistant analyzes the text of the page you are currently viewing. If it identifies a quiz question or a learning scenario, it leverages advanced AI to provide a suggested answer along with a detailed, plain-English explanation of its reasoning.
10
+
11
+ Features:
12
+ - **One-Click Analysis:** Instantly reads the text on your current tab.
13
+ - **AI-Powered Reasoning:** Provides clear explanations for why a specific answer is correct, helping you learn rather than just guessing.
14
+ - **Interactive Chatbot:** Ask follow-up questions directly in the side panel to dive deeper into the topic.
15
+ - **Privacy First:** You provide your own API key which is stored securely in your browser's local storage. The extension only analyzes the page when you explicitly click the "Analyze" button.
16
+
17
+ *Note: You must provide your own Gemini API Key to use this extension.*
18
+
19
+ ## 2. Permissions Justification
20
+
21
+ | Permission | Justification |
22
+ | :--- | :--- |
23
+ | `activeTab` | Required to allow the extension to read the text of the currently active tab when the user clicks the "Analyze Active Page" button. |
24
+ | `scripting` | Required to execute a script that extracts the `document.body.innerText` from the active tab so the AI can analyze the content. |
25
+ | `sidePanel` | Required to host the extension's interactive chat and analysis UI in the browser's native side panel. |
26
+ | `storage` | Required to securely save the user's provided Gemini API key locally on their device so they do not have to re-enter it every time. |
27
+
28
+ ### Host Permissions
29
+ | Host | Justification |
30
+ | :--- | :--- |
31
+ | `<all_urls>` | Required because the user might need educational assistance or question analysis on any website or domain they are currently studying on. |
32
+
33
+ ## 3. Privacy Policy & Data Use
34
+
35
+ **Does this extension collect or use user data?**
36
+ Yes.
37
+
38
+ **Data Collection & Usage:**
39
+ 1. **API Keys:** The extension asks for your Gemini API key. This key is stored entirely locally on your device using `chrome.storage.local`. It is never transmitted to our servers or any third-party other than Google's official Gemini API endpoint for the purpose of fulfilling your chat requests.
40
+ 2. **Web Page Content:** When you explicitly click "Analyze Active Page", the extension reads the text of the web page you are currently viewing. This text is sent directly to the Google Gemini API to generate an analysis. We do not store, log, or track your browsing history or the content of the pages you analyze.
41
+
42
+ **Data Retention:**
43
+ We do not operate any backend servers. All data (API keys and chat history) is stored locally within your Chrome browser profile and is cleared when you uninstall the extension or clear your extension data.
44
+
45
+ ## 4. Pre-Publish Checklist
46
+ - [x] manifest.json is V3
47
+ - [x] All permissions justified
48
+ - [x] Privacy policy documented
49
+ - [ ] Create 1280x800 promotional image
50
+ - [ ] Create 128x128 icon file (currently omitted from manifest for development)
@@ -0,0 +1,3 @@
1
+ chrome.action.onClicked.addListener(async (tab) => {
2
+ await chrome.sidePanel.open({ windowId: tab.windowId });
3
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Quiz Assistant",
4
+ "version": "1.0.0",
5
+ "description": "An interactive AI assistant that analyzes quiz questions on the current page and provides reasoning.",
6
+ "permissions": [
7
+ "activeTab",
8
+ "scripting",
9
+ "sidePanel",
10
+ "storage"
11
+ ],
12
+ "host_permissions": [
13
+ "<all_urls>"
14
+ ],
15
+ "action": {
16
+ "default_title": "Open Quiz Assistant"
17
+ },
18
+ "background": {
19
+ "service_worker": "background.js"
20
+ }
21
+ }
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <style>
5
+ body { font-family: system-ui, sans-serif; background: #1e1e1e; color: #f0f0f0; padding: 16px; margin: 0; }
6
+ h2 { color: #4caf50; margin-top: 0; }
7
+ button { background: #4caf50; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; width: 100%; font-weight: bold; margin-bottom: 16px; }
8
+ button:hover { background: #45a049; }
9
+ input[type="password"], input[type="text"] { width: 100%; padding: 8px; margin-bottom: 16px; box-sizing: border-box; background: #333; color: white; border: 1px solid #555; border-radius: 4px; }
10
+ .card { background: #2a2a2a; border-radius: 8px; padding: 12px; margin-bottom: 16px; border: 1px solid #444; }
11
+ .hidden { display: none; }
12
+ #chat-history { height: 200px; overflow-y: auto; margin-bottom: 12px; font-size: 0.9em; border: 1px solid #444; padding: 8px; border-radius: 4px; background: #222; }
13
+ #chat-input-container { display: flex; }
14
+ #chat-input { flex-grow: 1; margin-bottom: 0; margin-right: 8px; }
15
+ #chat-send { width: auto; margin-bottom: 0; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <h2>Quiz Assistant</h2>
20
+
21
+ <div id="setup-section">
22
+ <p style="font-size: 0.9em; color: #ccc;">Please enter your Gemini API Key. It will be stored securely in your browser's local storage.</p>
23
+ <label for="api-key" style="display:block; margin-bottom: 5px;">Gemini API Key:</label>
24
+ <input type="password" id="api-key" placeholder="AIzaSy...">
25
+ <button id="save-key-btn">Save Key</button>
26
+ </div>
27
+
28
+ <div id="main-section" class="hidden">
29
+ <button id="analyze-btn">Analyze Active Page</button>
30
+
31
+ <div id="result-card" class="card hidden">
32
+ <p><strong>Suggested Option:</strong> <span id="res-option"></span></p>
33
+ <p><strong>Confidence:</strong> <span id="res-confidence"></span>%</p>
34
+ <p style="font-size: 0.9em; color: #aaa;"><strong>Reasoning:</strong><br/><span id="res-reasoning"></span></p>
35
+ </div>
36
+
37
+ <div class="card">
38
+ <h3 style="margin-top:0; font-size: 1em;">Chat</h3>
39
+ <div id="chat-history"></div>
40
+ <div id="chat-input-container">
41
+ <input type="text" id="chat-input" placeholder="Ask follow-up...">
42
+ <button id="chat-send">Send</button>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <script src="sidepanel.js"></script>
48
+ </body>
49
+ </html>
@@ -0,0 +1,127 @@
1
+ let apiKey = '';
2
+ let chatHistory = [];
3
+
4
+ document.addEventListener('DOMContentLoaded', async () => {
5
+ const data = await chrome.storage.local.get('apiKey');
6
+ if (data.apiKey) {
7
+ apiKey = data.apiKey;
8
+ document.getElementById('setup-section').classList.add('hidden');
9
+ document.getElementById('main-section').classList.remove('hidden');
10
+ }
11
+
12
+ document.getElementById('save-key-btn').addEventListener('click', async () => {
13
+ const key = document.getElementById('api-key').value.trim();
14
+ if (key) {
15
+ await chrome.storage.local.set({ apiKey: key });
16
+ apiKey = key;
17
+ document.getElementById('setup-section').classList.add('hidden');
18
+ document.getElementById('main-section').classList.remove('hidden');
19
+ }
20
+ });
21
+
22
+ document.getElementById('analyze-btn').addEventListener('click', analyzePage);
23
+ document.getElementById('chat-send').addEventListener('click', sendChatMessage);
24
+ document.getElementById('chat-input').addEventListener('keypress', (e) => {
25
+ if (e.key === 'Enter') sendChatMessage();
26
+ });
27
+ });
28
+
29
+ async function analyzePage() {
30
+ const btn = document.getElementById('analyze-btn');
31
+ btn.textContent = "Analyzing...";
32
+ btn.disabled = true;
33
+
34
+ try {
35
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
36
+
37
+ // Execute script to extract text from the active tab
38
+ const [{ result }] = await chrome.scripting.executeScript({
39
+ target: { tabId: tab.id },
40
+ func: () => document.body.innerText
41
+ });
42
+
43
+ const pageText = result;
44
+
45
+ const prompt = `
46
+ You are an expert acting as an educational assistant.
47
+ Here is the text extracted from the webpage:
48
+ ${pageText.substring(0, 5000)}
49
+
50
+ Based on your knowledge, identify if there is a quiz question present. If so, determine the most correct option.
51
+ Return a JSON object with the following schema:
52
+ {"selectedOption": "A", "confidenceScore": 100, "reasoning": "brief explanation"}
53
+ If no clear question is found, return "N/A" for selectedOption and explain why in reasoning.
54
+ `;
55
+
56
+ chatHistory = [{ role: "user", parts: [{ text: prompt }] }];
57
+
58
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`, {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify({
62
+ contents: chatHistory,
63
+ generationConfig: { responseMimeType: "application/json" }
64
+ })
65
+ });
66
+
67
+ const data = await response.json();
68
+
69
+ if (data.error) throw new Error(data.error.message);
70
+
71
+ const rawText = data.candidates[0].content.parts[0].text;
72
+ chatHistory.push(data.candidates[0].content); // save to history
73
+
74
+ const parsed = JSON.parse(rawText);
75
+
76
+ document.getElementById('result-card').classList.remove('hidden');
77
+ document.getElementById('res-option').textContent = parsed.selectedOption;
78
+ document.getElementById('res-confidence').textContent = parsed.confidenceScore;
79
+ document.getElementById('res-reasoning').textContent = parsed.reasoning;
80
+
81
+ const chatUI = document.getElementById('chat-history');
82
+ chatUI.innerHTML += `<div style="margin-bottom:8px; color:#4caf50;"><b>AI:</b> Analysis complete. I selected Option ${parsed.selectedOption}. Ask me any questions!</div>`;
83
+
84
+ } catch (e) {
85
+ alert("Error analyzing page: " + e.message);
86
+ } finally {
87
+ btn.textContent = "Analyze Active Page";
88
+ btn.disabled = false;
89
+ }
90
+ }
91
+
92
+ async function sendChatMessage() {
93
+ const input = document.getElementById('chat-input');
94
+ const msg = input.value.trim();
95
+ if (!msg) return;
96
+
97
+ input.value = '';
98
+ input.disabled = true;
99
+
100
+ const chatUI = document.getElementById('chat-history');
101
+ chatUI.innerHTML += `<div style="margin-bottom:8px;"><b>You:</b> ${msg}</div>`;
102
+ chatUI.scrollTop = chatUI.scrollHeight;
103
+
104
+ chatHistory.push({ role: "user", parts: [{ text: msg }] });
105
+
106
+ try {
107
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ contents: chatHistory })
111
+ });
112
+
113
+ const data = await response.json();
114
+ if (data.error) throw new Error(data.error.message);
115
+
116
+ const aiMsg = data.candidates[0].content.parts[0].text;
117
+ chatHistory.push(data.candidates[0].content);
118
+
119
+ chatUI.innerHTML += `<div style="margin-bottom:8px; color:#4caf50;"><b>AI:</b> ${aiMsg}</div>`;
120
+ } catch (e) {
121
+ chatUI.innerHTML += `<div style="margin-bottom:8px; color:red;"><b>Error:</b> ${e.message}</div>`;
122
+ } finally {
123
+ input.disabled = false;
124
+ input.focus();
125
+ chatUI.scrollTop = chatUI.scrollHeight;
126
+ }
127
+ }
@@ -0,0 +1,19 @@
1
+ # Feature Timeline: AI Quiz Assistant
2
+
3
+ ## ✅ Completed Features
4
+ - **Core Automation:** Basic Playwright autonomous loop to auto-click and auto-navigate quizzes.
5
+ - **Visual Analysis:** OCR implementation via Tesseract.js & Jimp image preprocessing.
6
+ - **Browser Portability:** Chrome Extension (Manifest V3) version for standard DOM extraction.
7
+ - **Universal Overlay:** Generic Playwright UI overlay injection for testing on any website.
8
+ - **Interactive Chat:** In-browser LLM Chatbot via Playwright function exposure.
9
+ - **Multi-Model Support:** OpenRouter API integration supporting Academic/Code/Math specific models (Owl Alpha, Qwen, Laguna).
10
+ - **Beautiful Output:** Native Markdown formatting for AI reasoning using `marked`.
11
+ - **Robust Parsing:** Heuristic fallback for non-JSON conversational models (like Owl Alpha).
12
+ - **Anti-Inception Capture:** Overlay auto-hides during DOM/Screenshot capture to prevent the AI from reading its own UI.
13
+
14
+ ## 🚀 Planned Features
15
+ - [ ] Add support for Anthropic Claude models.
16
+ - [ ] Implement local, offline LLM support via Ollama.
17
+ - [ ] Add visual bounding boxes to highlight extracted question elements on the screen.
18
+ - [ ] Export chat logs and analysis history to Markdown files.
19
+ - [ ] Add PDF and Word document parsing support.
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@ojas-sta/qalify-plus",
3
+ "version": "1.1.0",
4
+ "description": "An advanced, AI-powered educational vulnerability simulator and overlay.",
5
+ "main": "src/generic.js",
6
+ "bin": {
7
+ "qalify": "./src/generic.js",
8
+ "qalify-simulator": "./src/main.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Ojas-sta/ai-quiz-assistant.git"
13
+ },
14
+ "scripts": {
15
+ "test": "echo \"Error: no test specified\" && exit 1"
16
+ },
17
+ "keywords": [],
18
+ "author": "Ojas-sta",
19
+ "license": "ISC",
20
+ "type": "commonjs",
21
+ "dependencies": {
22
+ "@google/genai": "^2.8.0",
23
+ "dotenv": "^17.4.2",
24
+ "jimp": "^1.6.1",
25
+ "marked": "^18.0.5",
26
+ "openai": "^6.42.0",
27
+ "playwright": "^1.60.0",
28
+ "tesseract.js": "^7.0.0"
29
+ }
30
+ }
package/src/ai.js ADDED
@@ -0,0 +1,155 @@
1
+ const { GoogleGenAI, Type } = require('@google/genai');
2
+ const { OpenAI } = require('openai');
3
+
4
+ class AILayer {
5
+ constructor(modelName = 'gemini-2.5-flash') {
6
+ this.modelName = modelName;
7
+ this.isOpenRouter = this.modelName.includes('/');
8
+
9
+ if (this.isOpenRouter) {
10
+ const key = process.env.OPENROUTER_API_KEY ? process.env.OPENROUTER_API_KEY.trim().replace(/^["']|["']$/g, '') : undefined;
11
+ this.openRouter = new OpenAI({
12
+ baseURL: "https://openrouter.ai/api/v1",
13
+ apiKey: key,
14
+ defaultHeaders: {
15
+ "HTTP-Referer": "http://localhost",
16
+ "X-Title": "Qalify+",
17
+ }
18
+ });
19
+ this.chatHistory = [];
20
+ } else {
21
+ const key = process.env.GEMINI_API_KEY ? process.env.GEMINI_API_KEY.trim().replace(/^["']|["']$/g, '') : undefined;
22
+ this.ai = new GoogleGenAI({ apiKey: key });
23
+ this.chatSession = null;
24
+ }
25
+ }
26
+
27
+ async determineAnswer(questionText, optionsText) {
28
+ console.log(`[AI] Analyzing question using model: ${this.modelName}...`);
29
+
30
+ const systemPrompt = "You are an expert educational assistant taking a quiz. Apply deep reasoning and academic rigor.";
31
+ const userPrompt = `
32
+ Question: ${questionText}
33
+ Options:
34
+ ${optionsText.join('\\n')}
35
+
36
+ Based on your knowledge, please select the most correct option.
37
+ Return your answer as a structured JSON object. Use exactly these keys:
38
+ - "selectedOption": Only the letter (A, B, C, or D) corresponding to your answer.
39
+ - "confidenceScore": An integer from 0 to 100 representing your confidence.
40
+ - "reasoning": A brief explanation of why this option is correct.
41
+ `;
42
+
43
+ try {
44
+ let parsed = null;
45
+
46
+ if (this.isOpenRouter) {
47
+ // OpenRouter API
48
+ if (!process.env.OPENROUTER_API_KEY) throw new Error("OPENROUTER_API_KEY is missing from .env");
49
+
50
+ const response = await this.openRouter.chat.completions.create({
51
+ model: this.modelName,
52
+ messages: [
53
+ { role: "system", content: systemPrompt },
54
+ { role: "user", content: userPrompt + "\nEnsure you output ONLY valid JSON. No markdown formatting." }
55
+ ]
56
+ });
57
+
58
+ const resultText = response.choices[0].message.content;
59
+ let cleanText = resultText.replace(/```json/gi, '').replace(/```/gi, '').trim();
60
+
61
+ try {
62
+ // Try to isolate a JSON block if there's conversational text around it
63
+ const jsonMatch = cleanText.match(/\{[\s\S]*\}/);
64
+ if (jsonMatch) {
65
+ parsed = JSON.parse(jsonMatch[0]);
66
+ } else {
67
+ parsed = JSON.parse(cleanText);
68
+ }
69
+ } catch (e) {
70
+ // Fallback heuristics if the model completely ignored the JSON prompt
71
+ console.log(`[AI] JSON parse failed, attempting heuristic fallback on: ${cleanText.substring(0, 50)}...`);
72
+ const optMatch = cleanText.match(/Option\s*([A-D])/i) || cleanText.match(/\b([A-D])\b/);
73
+ if (!optMatch) throw new Error(`Model did not return JSON. Raw output: ${cleanText.substring(0, 100)}...`);
74
+
75
+ parsed = {
76
+ selectedOption: optMatch[1].toUpperCase(),
77
+ confidenceScore: 85,
78
+ reasoning: cleanText
79
+ };
80
+ }
81
+
82
+ // Initialize chat history for OpenRouter
83
+ this.chatHistory = [
84
+ { role: "system", content: systemPrompt },
85
+ { role: "user", content: `Question: ${questionText}\\nOptions: ${optionsText.join(', ')}` },
86
+ { role: "assistant", content: `I chose Option ${parsed.selectedOption} with ${parsed.confidenceScore}% confidence because: ${parsed.reasoning}.` }
87
+ ];
88
+
89
+ } else {
90
+ // Google Gemini API natively
91
+ const responseSchema = {
92
+ type: Type.OBJECT,
93
+ properties: {
94
+ selectedOption: { type: Type.STRING },
95
+ confidenceScore: { type: Type.INTEGER },
96
+ reasoning: { type: Type.STRING }
97
+ },
98
+ required: ["selectedOption", "confidenceScore", "reasoning"],
99
+ };
100
+
101
+ const response = await this.ai.models.generateContent({
102
+ model: this.modelName,
103
+ contents: userPrompt,
104
+ config: {
105
+ responseMimeType: "application/json",
106
+ responseSchema: responseSchema,
107
+ }
108
+ });
109
+
110
+ parsed = JSON.parse(response.text);
111
+
112
+ // Initialize chat session for Gemini
113
+ this.chatSession = this.ai.chats.create({
114
+ model: this.modelName,
115
+ config: {
116
+ systemInstruction: `You are an AI taking a quiz. You chose Option ${parsed.selectedOption} because: ${parsed.reasoning}.`
117
+ }
118
+ });
119
+ }
120
+
121
+ return parsed;
122
+ } catch (error) {
123
+ console.error("[AI] Error during reasoning:", error.message);
124
+ return { error: `API Error: ${error.message}` };
125
+ }
126
+ }
127
+
128
+ async chat(message) {
129
+ try {
130
+ console.log(`[AI Chat] Processing message with ${this.modelName}...`);
131
+ if (this.isOpenRouter) {
132
+ if (this.chatHistory.length === 0) return "I am not analyzing a question right now.";
133
+
134
+ this.chatHistory.push({ role: "user", content: message });
135
+ const response = await this.openRouter.chat.completions.create({
136
+ model: this.modelName,
137
+ messages: this.chatHistory
138
+ });
139
+
140
+ const reply = response.choices[0].message.content;
141
+ this.chatHistory.push({ role: "assistant", content: reply });
142
+ return reply;
143
+ } else {
144
+ if (!this.chatSession) return "I am not analyzing a question right now.";
145
+ const response = await this.chatSession.sendMessage({ message });
146
+ return response.text;
147
+ }
148
+ } catch (error) {
149
+ console.error("[AI Chat] Error:", error.message);
150
+ return `<span style="color:red">API Error: ${error.message}</span>`;
151
+ }
152
+ }
153
+ }
154
+
155
+ module.exports = AILayer;