Albert.io Question Scraper

Scrape Albert.io questions and answer choices, very basic

  1. // ==UserScript==
  2. // @name Albert.io Question Scraper
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.4
  5. // @description Scrape Albert.io questions and answer choices, very basic
  6. // @author You
  7. // @match https://*.albert.io/*
  8. // @grant GM_addStyle
  9. // @license GPL-3.0-or-later
  10. // @run-at document-idle
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. GM_addStyle(`
  17. .albert-dl-btn {
  18. background-color: #4080BD;
  19. color: white;
  20. border: none;
  21. border-radius: 4px;
  22. padding: 5px 10px;
  23. margin: 5px;
  24. cursor: pointer;
  25. font-size: 12px;
  26. transition: background-color 0.3s;
  27. z-index: 9999;
  28. }
  29. .albert-dl-btn:hover {
  30. background-color: #336699;
  31. }
  32. .albert-primary-btn {
  33. background-color: #4080BD;
  34. color: white;
  35. border: none;
  36. border-radius: 4px;
  37. padding: 8px 15px;
  38. margin: 5px;
  39. cursor: pointer;
  40. font-size: 14px;
  41. font-weight: bold;
  42. transition: background-color 0.3s;
  43. }
  44. .albert-primary-btn:hover {
  45. background-color: #336699;
  46. }
  47. .albert-danger-btn {
  48. background-color: #dc3545;
  49. color: white;
  50. }
  51. .albert-danger-btn:hover {
  52. background-color: #bd2130;
  53. }
  54. .albert-success-btn {
  55. background-color: #28a745;
  56. color: white;
  57. }
  58. .albert-success-btn:hover {
  59. background-color: #218838;
  60. }
  61. .albert-dl-all-btn {
  62. position: fixed;
  63. top: 10px;
  64. right: 10px;
  65. background-color: #4080BD;
  66. color: white;
  67. border: none;
  68. border-radius: 4px;
  69. padding: 8px 12px;
  70. font-size: 14px;
  71. z-index: 9999;
  72. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  73. }
  74. .albert-scraper-panel {
  75. position: fixed;
  76. top: 50px;
  77. right: 10px;
  78. background-color: white;
  79. border: 1px solid #ccc;
  80. border-radius: 4px;
  81. padding: 15px;
  82. z-index: 9999;
  83. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  84. display: none;
  85. max-width: 400px;
  86. font-family: Arial, sans-serif;
  87. }
  88. .albert-question-count {
  89. margin-bottom: 10px;
  90. font-weight: bold;
  91. }
  92. .albert-option-row {
  93. margin-bottom: 10px;
  94. display: flex;
  95. justify-content: space-between;
  96. align-items: center;
  97. flex-wrap: wrap;
  98. }
  99. .albert-option-label {
  100. flex-grow: 1;
  101. margin-right: 10px;
  102. font-size: 14px;
  103. }
  104. .albert-checkbox-container {
  105. display: flex;
  106. align-items: center;
  107. margin-bottom: 10px;
  108. }
  109. .albert-checkbox {
  110. margin-right: 8px;
  111. }
  112. .albert-progress {
  113. margin-top: 10px;
  114. background-color: #f0f0f0;
  115. border-radius: 4px;
  116. height: 20px;
  117. overflow: hidden;
  118. display: none;
  119. }
  120. .albert-progress-bar {
  121. background-color: #4080BD;
  122. height: 100%;
  123. width: 0%;
  124. transition: width 0.3s;
  125. }
  126. .albert-progress-text {
  127. text-align: center;
  128. margin-top: 5px;
  129. font-size: 12px;
  130. }
  131. .albert-question-checkbox {
  132. position: absolute;
  133. top: 5px;
  134. right: 5px;
  135. z-index: 9999;
  136. }
  137. .albert-checkbox-label {
  138. font-size: 12px;
  139. cursor: pointer;
  140. }
  141. .albert-tab-container {
  142. margin-bottom: 15px;
  143. }
  144. .albert-tab {
  145. display: inline-block;
  146. padding: 8px 15px;
  147. background-color: #f0f0f0;
  148. border: 1px solid #ccc;
  149. border-bottom: none;
  150. border-radius: 4px 4px 0 0;
  151. cursor: pointer;
  152. }
  153. .albert-tab.active {
  154. background-color: #4080BD;
  155. color: white;
  156. }
  157. .albert-tab-content {
  158. border: 1px solid #ccc;
  159. padding: 15px;
  160. border-radius: 0 4px 4px 4px;
  161. }
  162. .albert-input-group {
  163. margin-bottom: 10px;
  164. display: flex;
  165. align-items: center;
  166. }
  167. .albert-input-label {
  168. flex-grow: 1;
  169. margin-right: 10px;
  170. }
  171. .albert-input {
  172. width: 70px;
  173. padding: 5px;
  174. border: 1px solid #ccc;
  175. border-radius: 4px;
  176. }
  177. .albert-switch {
  178. position: relative;
  179. display: inline-block;
  180. width: 45px;
  181. height: 24px;
  182. }
  183. .albert-switch input {
  184. opacity: 0;
  185. width: 0;
  186. height: 0;
  187. }
  188. .albert-slider {
  189. position: absolute;
  190. cursor: pointer;
  191. top: 0;
  192. left: 0;
  193. right: 0;
  194. bottom: 0;
  195. background-color: #ccc;
  196. transition: .4s;
  197. border-radius: 24px;
  198. }
  199. .albert-slider:before {
  200. position: absolute;
  201. content: "";
  202. height: 18px;
  203. width: 18px;
  204. left: 3px;
  205. bottom: 3px;
  206. background-color: white;
  207. transition: .4s;
  208. border-radius: 50%;
  209. }
  210. input:checked + .albert-slider {
  211. background-color: #4080BD;
  212. }
  213. input:checked + .albert-slider:before {
  214. transform: translateX(21px);
  215. }
  216. .albert-status-badge {
  217. display: inline-block;
  218. padding: 3px 8px;
  219. border-radius: 10px;
  220. font-size: 12px;
  221. font-weight: bold;
  222. margin-left: 5px;
  223. }
  224. .albert-status-badge.active {
  225. background-color: #28a745;
  226. color: white;
  227. }
  228. .albert-status-badge.inactive {
  229. background-color: #dc3545;
  230. color: white;
  231. }
  232. .albert-status-badge.pending {
  233. background-color: #ffc107;
  234. color: black;
  235. }
  236. .albert-divider {
  237. margin: 15px 0;
  238. border-top: 1px solid #eee;
  239. }
  240. .albert-section-title {
  241. font-weight: bold;
  242. margin-bottom: 10px;
  243. font-size: 16px;
  244. }
  245. .albert-counter {
  246. font-weight: bold;
  247. color: #4080BD;
  248. }
  249. .albert-textarea {
  250. width: 100%;
  251. height: 150px;
  252. margin-top: 10px;
  253. padding: 8px;
  254. border: 1px solid #ccc;
  255. border-radius: 4px;
  256. font-family: monospace;
  257. font-size: 12px;
  258. resize: vertical;
  259. }
  260. .albert-button-container {
  261. display: flex;
  262. justify-content: center;
  263. margin-top: 10px;
  264. }
  265. .albert-debug {
  266. position: fixed;
  267. bottom: 10px;
  268. left: 10px;
  269. padding: 8px;
  270. background-color: rgba(0,0,0,0.7);
  271. color: white;
  272. font-size: 10px;
  273. border-radius: 4px;
  274. z-index: 9999;
  275. }
  276. `);
  277.  
  278. let foundQuestions = [];
  279. let selectedQuestions = [];
  280. let autoNavigationActive = false;
  281. let autoNavigationDelay = 2000;
  282. let questionsProcessed = 0;
  283. let totalQuestionsToProcess = 0;
  284. let collectedContent = '';
  285. let currentQuestionNumber = 1;
  286. let debugMode = false;
  287.  
  288. window.addEventListener('load', function() {
  289. setTimeout(initScraper, 2000);
  290. });
  291.  
  292. function initScraper() {
  293. console.log('Albert.io Question Scraper is running...');
  294.  
  295. findAndProcessQuestions();
  296.  
  297. addScanButton();
  298.  
  299. observeDOMChanges();
  300. }
  301.  
  302. function findAndProcessQuestions() {
  303.  
  304. foundQuestions = [];
  305.  
  306. const questions = document.querySelectorAll('.question-wrapper, .question-container, .question');
  307.  
  308. if (questions && questions.length > 0) {
  309. console.log(`Found ${questions.length} question(s) with standard selectors`);
  310. processQuestions(Array.from(questions));
  311. } else {
  312. console.log('No questions found with standard selectors. Trying alternative approach...');
  313. scanForQuestions();
  314. }
  315. }
  316.  
  317. function processQuestions(questions) {
  318.  
  319. foundQuestions = questions;
  320.  
  321. if (questions.length > 0) {
  322. addScraperPanel(questions);
  323. }
  324.  
  325. questions.forEach((question, index) => {
  326. addSelectionToQuestion(question, index);
  327. addDownloadButton(question, index);
  328. });
  329. }
  330.  
  331. function addSelectionToQuestion(questionElement, index) {
  332.  
  333. if (questionElement.querySelector('.albert-question-checkbox')) {
  334. return;
  335. }
  336.  
  337. const checkboxContainer = document.createElement('div');
  338. checkboxContainer.className = 'albert-question-checkbox';
  339.  
  340. const checkbox = document.createElement('input');
  341. checkbox.type = 'checkbox';
  342. checkbox.id = `albert-question-${index}`;
  343. checkbox.className = 'albert-checkbox';
  344. checkbox.dataset.index = index;
  345. checkbox.addEventListener('change', function() {
  346. if (this.checked) {
  347. if (!selectedQuestions.includes(index)) {
  348. selectedQuestions.push(index);
  349. }
  350. } else {
  351. const selectedIndex = selectedQuestions.indexOf(index);
  352. if (selectedIndex > -1) {
  353. selectedQuestions.splice(selectedIndex, 1);
  354. }
  355. }
  356. updateSelectedCount();
  357. });
  358.  
  359. const label = document.createElement('label');
  360. label.className = 'albert-checkbox-label';
  361. label.htmlFor = `albert-question-${index}`;
  362. label.textContent = 'Select';
  363.  
  364. checkboxContainer.appendChild(checkbox);
  365. checkboxContainer.appendChild(label);
  366.  
  367. const insertTarget = questionElement.querySelector('.question-wrapper__heading') ||
  368. questionElement.querySelector('.mcq-option') ||
  369. questionElement;
  370.  
  371. insertTarget.style.position = 'relative';
  372. insertTarget.appendChild(checkboxContainer);
  373. }
  374.  
  375. function downloadTextAsFile(filename, text) {
  376. const blob = new Blob([text], {type: 'text/plain'});
  377. const url = URL.createObjectURL(blob);
  378.  
  379. const element = document.createElement('a');
  380. element.href = url;
  381. element.download = filename;
  382. element.style.display = 'none';
  383. document.body.appendChild(element);
  384. element.click();
  385.  
  386. setTimeout(function() {
  387. document.body.removeChild(element);
  388. URL.revokeObjectURL(url);
  389. }, 100);
  390. }
  391.  
  392. function extractQuestionContent(questionElement = null) {
  393. let questionText = '';
  394. let questionTitle = '';
  395. let questionNumber = currentQuestionNumber;
  396. let optionsFound = false;
  397.  
  398. if (!questionElement) {
  399.  
  400. questionElement = document.querySelector('.practice-view__question-area') ||
  401. document.querySelector('.question-wrapper') ||
  402. document.querySelector('.question-container') ||
  403. document.querySelector('.question') ||
  404. document.body;
  405. }
  406.  
  407. if (debugMode) {
  408. logDebug(`Extracting from ${questionElement.className}`);
  409. }
  410.  
  411. const titleElem = document.querySelector('.student-practice-view-toolbar__title');
  412. if (titleElem) {
  413. questionTitle = titleElem.textContent.trim();
  414. questionText += "Title: " + questionTitle + "\n\n";
  415. }
  416.  
  417. const questionNumElem = document.querySelector('[data-testid="question-dropdown-navigator__toggle-button"]');
  418. if (questionNumElem) {
  419. const questionNumText = questionNumElem.textContent.trim();
  420. const match = questionNumText.match(/Question\s+(\d+)/i);
  421. if (match && match[1]) {
  422. questionNumber = parseInt(match[1]);
  423. questionText += "Question " + questionNumber + "\n\n";
  424. } else {
  425. questionText += "Question " + questionNumber + "\n\n";
  426. }
  427. } else {
  428. questionText += "Question " + questionNumber + "\n\n";
  429. }
  430.  
  431. const instructions = document.querySelector('.question-wrapper__body, .instructions-pane');
  432. if (instructions) {
  433. const markdownContent = instructions.querySelector('.markdown-renderer-v2');
  434. if (markdownContent) {
  435. questionText += markdownContent.textContent.trim() + "\n\n";
  436. } else {
  437. questionText += instructions.textContent.trim() + "\n\n";
  438. }
  439. }
  440.  
  441. let options = document.querySelectorAll('.mcq-option-eliminator');
  442. if (options && options.length > 0) {
  443. questionText += "Options:\n";
  444. optionsFound = true;
  445.  
  446. options.forEach((option) => {
  447. const letterElement = option.querySelector('.mcq-option__letter');
  448. const contentElement = option.querySelector('.mcq-option__content');
  449.  
  450. if (letterElement && contentElement) {
  451. const letter = letterElement.textContent.trim();
  452. const content = contentElement.textContent.trim();
  453. questionText += letter + ") " + content + "\n";
  454. }
  455. });
  456.  
  457. questionText += "\n";
  458.  
  459. if (debugMode) {
  460. logDebug(`Found ${options.length} options with method 1`);
  461. }
  462. }
  463.  
  464. if (!optionsFound) {
  465. options = document.querySelectorAll('.mcq-option-accessible-wrapper');
  466. if (options && options.length > 0) {
  467. questionText += "Options:\n";
  468. optionsFound = true;
  469.  
  470. options.forEach((option) => {
  471. const letterElement = option.querySelector('.mcq-option__letter');
  472. const contentElement = option.querySelector('.mcq-option__content');
  473.  
  474. if (letterElement && contentElement) {
  475. const letter = letterElement.textContent.trim();
  476. const content = contentElement.textContent.trim();
  477. questionText += letter + ") " + content + "\n";
  478. }
  479. });
  480.  
  481. questionText += "\n";
  482.  
  483. if (debugMode) {
  484. logDebug(`Found ${options.length} options with method 2`);
  485. }
  486. }
  487. }
  488.  
  489. if (!optionsFound) {
  490. options = document.querySelectorAll('label[for^="input-"]');
  491. if (options && options.length > 0) {
  492. questionText += "Options:\n";
  493. optionsFound = true;
  494.  
  495. options.forEach((option, index) => {
  496. const letterElement = option.querySelector('.mcq-option__letter');
  497. const contentElement = option.querySelector('.mcq-option__content');
  498.  
  499. const letter = letterElement ? letterElement.textContent.trim() : String.fromCharCode(65 + index);
  500.  
  501. const content = contentElement
  502. ? contentElement.textContent.trim()
  503. : option.textContent.replace(letter, '').trim();
  504.  
  505. questionText += letter + ") " + content + "\n";
  506. });
  507.  
  508. questionText += "\n";
  509.  
  510. if (debugMode) {
  511. logDebug(`Found ${options.length} options with method 3`);
  512. }
  513. }
  514. }
  515.  
  516. if (!optionsFound) {
  517. const selector = '[class*="option" i]:not(.albert-option-row):not(.albert-option-label)';
  518. options = document.querySelectorAll(selector);
  519. if (options && options.length > 0) {
  520. questionText += "Options:\n";
  521. optionsFound = true;
  522.  
  523. options.forEach((option, index) => {
  524.  
  525. const letter = String.fromCharCode(65 + index);
  526.  
  527. let content = option.textContent.trim();
  528.  
  529. content = content.replace(/^[A-E][)\]]?\s*/, '');
  530.  
  531. questionText += letter + ") " + content + "\n";
  532. });
  533.  
  534. questionText += "\n";
  535.  
  536. if (debugMode) {
  537. logDebug(`Found ${options.length} options with method 4`);
  538. }
  539. }
  540. }
  541.  
  542. if (debugMode) {
  543. logDebug(`Options found: ${optionsFound}`);
  544. }
  545.  
  546. const explanation = document.querySelector('.explanation-content, .explanation');
  547. if (explanation) {
  548. questionText += "Explanation:\n" + explanation.textContent.trim() + "\n";
  549. }
  550.  
  551. const tables = document.querySelectorAll('table');
  552. if (tables && tables.length > 0) {
  553. tables.forEach((table, idx) => {
  554. questionText += `\nTable ${idx + 1}:\n`;
  555.  
  556. const headers = table.querySelectorAll('th');
  557. if (headers.length > 0) {
  558. const headerRow = Array.from(headers).map(th => th.textContent.trim());
  559. questionText += headerRow.join(' | ') + '\n';
  560. questionText += '-'.repeat(headerRow.join(' | ').length) + '\n';
  561. }
  562.  
  563. const rows = table.querySelectorAll('tr');
  564. rows.forEach(row => {
  565.  
  566. if (row.querySelector('th')) return;
  567.  
  568. const cells = row.querySelectorAll('td');
  569. if (cells.length > 0) {
  570. questionText += Array.from(cells).map(td => td.textContent.trim()).join(' | ') + '\n';
  571. }
  572. });
  573.  
  574. questionText += '\n';
  575. });
  576. }
  577.  
  578. return {
  579. text: questionText,
  580. number: questionNumber,
  581. optionsFound: optionsFound
  582. };
  583. }
  584.  
  585. function addDownloadButton(questionElement, index) {
  586.  
  587. if (questionElement.querySelector('.albert-dl-btn')) {
  588. return;
  589. }
  590.  
  591. const downloadBtn = document.createElement('button');
  592. downloadBtn.className = 'albert-dl-btn';
  593. downloadBtn.innerText = 'Save as TXT';
  594. downloadBtn.addEventListener('click', function(e) {
  595. e.preventDefault();
  596. e.stopPropagation();
  597.  
  598. downloadCurrentQuestion(questionElement);
  599. });
  600.  
  601. const insertTarget = questionElement.querySelector('.question-wrapper__heading') ||
  602. questionElement.querySelector('.mcq-option') ||
  603. questionElement;
  604.  
  605. insertTarget.style.position = 'relative';
  606. insertTarget.appendChild(downloadBtn);
  607. }
  608.  
  609. function downloadCurrentQuestion(questionElement) {
  610. const extract = extractQuestionContent(questionElement);
  611. const content = extract.text;
  612. let filename = `albert_question_${extract.number}.txt`;
  613.  
  614. const contentLines = content.split('\n');
  615. if (contentLines.length > 0 && contentLines[0].startsWith('Title:')) {
  616. const title = contentLines[0].replace('Title:', '').trim();
  617. filename = `albert_${title.substring(0, 30).replace(/[^a-z0-9]/gi, '_')}_q${extract.number}.txt`;
  618. }
  619.  
  620. if (content.trim() !== '') {
  621. downloadTextAsFile(filename, content);
  622.  
  623. if (autoNavigationActive) {
  624. questionsProcessed++;
  625. updateAutoNavigationStatus();
  626. }
  627. } else {
  628. alert('Could not extract question content. Please try using the "Scan for Questions" button.');
  629. }
  630. }
  631.  
  632. function addScraperPanel(questions) {
  633.  
  634. if (document.querySelector('.albert-scraper-panel')) {
  635. updateScraperPanel(questions);
  636. return;
  637. }
  638.  
  639. const panel = document.createElement('div');
  640. panel.className = 'albert-scraper-panel';
  641.  
  642. const tabContainer = document.createElement('div');
  643. tabContainer.className = 'albert-tab-container';
  644.  
  645. const autoNavTab = document.createElement('div');
  646. autoNavTab.className = 'albert-tab active';
  647. autoNavTab.textContent = 'Auto Collector';
  648. autoNavTab.dataset.tab = 'autonav';
  649.  
  650. const manualTab = document.createElement('div');
  651. manualTab.className = 'albert-tab';
  652. manualTab.textContent = 'Manual Selection';
  653. manualTab.dataset.tab = 'manual';
  654.  
  655. tabContainer.appendChild(autoNavTab);
  656. tabContainer.appendChild(manualTab);
  657. panel.appendChild(tabContainer);
  658.  
  659. const tabContent = document.createElement('div');
  660. tabContent.className = 'albert-tab-content';
  661. panel.appendChild(tabContent);
  662.  
  663. const autoNavContent = document.createElement('div');
  664. autoNavContent.id = 'albert-autonav-tab';
  665.  
  666. const autoNavDescription = document.createElement('p');
  667. autoNavDescription.innerText = 'Automatically navigate through all questions, collecting them into a single file.';
  668. autoNavContent.appendChild(autoNavDescription);
  669.  
  670. const delayGroup = document.createElement('div');
  671. delayGroup.className = 'albert-input-group';
  672.  
  673. const delayLabel = document.createElement('label');
  674. delayLabel.className = 'albert-input-label';
  675. delayLabel.innerText = 'Delay between questions (ms)';
  676.  
  677. const delayInput = document.createElement('input');
  678. delayInput.type = 'number';
  679. delayInput.min = '1000';
  680. delayInput.max = '10000';
  681. delayInput.step = '500';
  682. delayInput.value = autoNavigationDelay;
  683. delayInput.className = 'albert-input';
  684. delayInput.addEventListener('change', function() {
  685. const value = parseInt(this.value);
  686. if (value >= 1000 && value <= 10000) {
  687. autoNavigationDelay = value;
  688. } else {
  689. this.value = autoNavigationDelay;
  690. alert('Please enter a value between 1000 and 10000 milliseconds');
  691. }
  692. });
  693.  
  694. delayGroup.appendChild(delayLabel);
  695. delayGroup.appendChild(delayInput);
  696.  
  697. autoNavContent.appendChild(delayGroup);
  698.  
  699. const debugGroup = document.createElement('div');
  700. debugGroup.className = 'albert-input-group';
  701.  
  702. const debugLabel = document.createElement('label');
  703. debugLabel.className = 'albert-input-label';
  704. debugLabel.innerText = 'Debug Mode';
  705.  
  706. const debugSwitch = document.createElement('label');
  707. debugSwitch.className = 'albert-switch';
  708.  
  709. const debugCheckbox = document.createElement('input');
  710. debugCheckbox.type = 'checkbox';
  711. debugCheckbox.checked = debugMode;
  712. debugCheckbox.addEventListener('change', function() {
  713. debugMode = this.checked;
  714. if (debugMode) {
  715. addDebugPanel();
  716. } else {
  717. removeDebugPanel();
  718. }
  719. });
  720.  
  721. const debugSlider = document.createElement('span');
  722. debugSlider.className = 'albert-slider';
  723.  
  724. debugSwitch.appendChild(debugCheckbox);
  725. debugSwitch.appendChild(debugSlider);
  726.  
  727. debugGroup.appendChild(debugLabel);
  728. debugGroup.appendChild(debugSwitch);
  729.  
  730. autoNavContent.appendChild(debugGroup);
  731.  
  732. const statusGroup = document.createElement('div');
  733. statusGroup.className = 'albert-input-group';
  734.  
  735. const statusLabel = document.createElement('label');
  736. statusLabel.className = 'albert-input-label';
  737. statusLabel.innerText = 'Auto-Collection Status';
  738.  
  739. const statusBadge = document.createElement('span');
  740. statusBadge.className = 'albert-status-badge inactive';
  741. statusBadge.id = 'albert-autonav-status';
  742. statusBadge.innerText = 'Inactive';
  743.  
  744. statusGroup.appendChild(statusLabel);
  745. statusGroup.appendChild(statusBadge);
  746.  
  747. autoNavContent.appendChild(statusGroup);
  748.  
  749. const counterGroup = document.createElement('div');
  750. counterGroup.className = 'albert-input-group';
  751.  
  752. const counterLabel = document.createElement('label');
  753. counterLabel.className = 'albert-input-label';
  754. counterLabel.innerText = 'Questions Collected';
  755.  
  756. const counterValue = document.createElement('span');
  757. counterValue.className = 'albert-counter';
  758. counterValue.id = 'albert-questions-processed';
  759. counterValue.innerText = '0';
  760.  
  761. counterGroup.appendChild(counterLabel);
  762. counterGroup.appendChild(counterValue);
  763.  
  764. autoNavContent.appendChild(counterGroup);
  765.  
  766. const divider = document.createElement('div');
  767. divider.className = 'albert-divider';
  768. autoNavContent.appendChild(divider);
  769.  
  770. const textareaLabel = document.createElement('div');
  771. textareaLabel.className = 'albert-section-title';
  772. textareaLabel.innerText = 'Collected Content';
  773. autoNavContent.appendChild(textareaLabel);
  774.  
  775. const contentTextarea = document.createElement('textarea');
  776. contentTextarea.className = 'albert-textarea';
  777. contentTextarea.id = 'albert-collected-content';
  778. contentTextarea.readOnly = true;
  779. contentTextarea.placeholder = 'Collected question content will appear here...';
  780. contentTextarea.value = collectedContent;
  781. autoNavContent.appendChild(contentTextarea);
  782.  
  783. const downloadCollectedBtn = document.createElement('button');
  784. downloadCollectedBtn.className = 'albert-primary-btn';
  785. downloadCollectedBtn.innerText = 'Download Collected Content';
  786. downloadCollectedBtn.id = 'albert-download-collected';
  787. downloadCollectedBtn.addEventListener('click', function() {
  788. downloadCollectedContent();
  789. });
  790. downloadCollectedBtn.style.width = '100%';
  791. downloadCollectedBtn.style.marginTop = '10px';
  792.  
  793. autoNavContent.appendChild(downloadCollectedBtn);
  794.  
  795. const navBtnGroup = document.createElement('div');
  796. navBtnGroup.className = 'albert-button-container';
  797.  
  798. const startNavBtn = document.createElement('button');
  799. startNavBtn.className = 'albert-primary-btn albert-success-btn';
  800. startNavBtn.innerText = 'Start Auto-Collection';
  801. startNavBtn.id = 'albert-start-autonav';
  802. startNavBtn.addEventListener('click', function() {
  803. startAutoNavigation();
  804. });
  805.  
  806. const stopNavBtn = document.createElement('button');
  807. stopNavBtn.className = 'albert-primary-btn albert-danger-btn';
  808. stopNavBtn.innerText = 'Stop';
  809. stopNavBtn.id = 'albert-stop-autonav';
  810. stopNavBtn.style.display = 'none';
  811. stopNavBtn.addEventListener('click', function() {
  812. stopAutoNavigation();
  813. });
  814.  
  815. navBtnGroup.appendChild(startNavBtn);
  816. navBtnGroup.appendChild(stopNavBtn);
  817.  
  818. autoNavContent.appendChild(navBtnGroup);
  819.  
  820. tabContent.appendChild(autoNavContent);
  821.  
  822. const manualContent = document.createElement('div');
  823. manualContent.id = 'albert-manual-tab';
  824. manualContent.style.display = 'none';
  825.  
  826. const countDisplay = document.createElement('div');
  827. countDisplay.className = 'albert-question-count';
  828. countDisplay.innerText = `Found ${questions.length} question(s)`;
  829. countDisplay.id = 'albert-question-count';
  830. manualContent.appendChild(countDisplay);
  831.  
  832. const selectedDisplay = document.createElement('div');
  833. selectedDisplay.className = 'albert-question-count';
  834. selectedDisplay.style.fontSize = '12px';
  835. selectedDisplay.innerText = `Selected: 0 question(s)`;
  836. selectedDisplay.id = 'albert-selected-count';
  837. manualContent.appendChild(selectedDisplay);
  838.  
  839. const selectionOptions = document.createElement('div');
  840. selectionOptions.className = 'albert-option-row';
  841.  
  842. const selectAllBtn = document.createElement('button');
  843. selectAllBtn.className = 'albert-dl-btn';
  844. selectAllBtn.innerText = 'Select All';
  845. selectAllBtn.addEventListener('click', function() {
  846. selectedQuestions = [];
  847. for (let i = 0; i < questions.length; i++) {
  848. selectedQuestions.push(i);
  849. const checkbox = document.getElementById(`albert-question-${i}`);
  850. if (checkbox) {
  851. checkbox.checked = true;
  852. }
  853. }
  854. updateSelectedCount();
  855. });
  856.  
  857. const deselectAllBtn = document.createElement('button');
  858. deselectAllBtn.className = 'albert-dl-btn';
  859. deselectAllBtn.innerText = 'Deselect All';
  860. deselectAllBtn.addEventListener('click', function() {
  861. selectedQuestions = [];
  862. for (let i = 0; i < questions.length; i++) {
  863. const checkbox = document.getElementById(`albert-question-${i}`);
  864. if (checkbox) {
  865. checkbox.checked = false;
  866. }
  867. }
  868. updateSelectedCount();
  869. });
  870.  
  871. selectionOptions.appendChild(selectAllBtn);
  872. selectionOptions.appendChild(deselectAllBtn);
  873. manualContent.appendChild(selectionOptions);
  874.  
  875. const downloadAllBtn = document.createElement('button');
  876. downloadAllBtn.className = 'albert-dl-btn';
  877. downloadAllBtn.style.width = '100%';
  878. downloadAllBtn.style.marginTop = '10px';
  879. downloadAllBtn.innerText = 'Download All as One File';
  880. downloadAllBtn.addEventListener('click', function() {
  881. downloadAllQuestionsAsOne(questions);
  882. });
  883.  
  884. manualContent.appendChild(downloadAllBtn);
  885.  
  886. const downloadSelectedBtn = document.createElement('button');
  887. downloadSelectedBtn.className = 'albert-dl-btn';
  888. downloadSelectedBtn.style.width = '100%';
  889. downloadSelectedBtn.style.marginTop = '10px';
  890. downloadSelectedBtn.innerText = 'Download Selected as One File';
  891. downloadSelectedBtn.addEventListener('click', function() {
  892. if (selectedQuestions.length === 0) {
  893. alert('Please select at least one question to download.');
  894. return;
  895. }
  896. downloadSelectedQuestionsAsOne(questions);
  897. });
  898.  
  899. manualContent.appendChild(downloadSelectedBtn);
  900.  
  901. tabContent.appendChild(manualContent);
  902.  
  903. const closeBtn = document.createElement('button');
  904. closeBtn.className = 'albert-dl-btn';
  905. closeBtn.innerText = 'Close';
  906. closeBtn.style.marginTop = '15px';
  907. closeBtn.addEventListener('click', function() {
  908. panel.style.display = 'none';
  909. toggleButton.style.display = 'block';
  910. });
  911. tabContent.appendChild(closeBtn);
  912.  
  913. document.body.appendChild(panel);
  914.  
  915. const toggleButton = document.createElement('button');
  916. toggleButton.className = 'albert-dl-all-btn';
  917. toggleButton.innerText = 'Albert.io Scraper';
  918. toggleButton.addEventListener('click', function() {
  919. panel.style.display = 'block';
  920. toggleButton.style.display = 'none';
  921. });
  922.  
  923. document.body.appendChild(toggleButton);
  924.  
  925. autoNavTab.addEventListener('click', function() {
  926. setActiveTab('autonav');
  927. });
  928.  
  929. manualTab.addEventListener('click', function() {
  930. setActiveTab('manual');
  931. });
  932. }
  933.  
  934. function addDebugPanel() {
  935. if (document.getElementById('albert-debug-panel')) {
  936. return;
  937. }
  938.  
  939. const debugPanel = document.createElement('div');
  940. debugPanel.id = 'albert-debug-panel';
  941. debugPanel.className = 'albert-debug';
  942. debugPanel.textContent = 'Debug mode active';
  943. document.body.appendChild(debugPanel);
  944. }
  945.  
  946. function removeDebugPanel() {
  947. const debugPanel = document.getElementById('albert-debug-panel');
  948. if (debugPanel) {
  949. debugPanel.remove();
  950. }
  951. }
  952.  
  953. function logDebug(message) {
  954. console.log('[Albert.io Scraper] ' + message);
  955.  
  956. const debugPanel = document.getElementById('albert-debug-panel');
  957. if (debugPanel) {
  958. debugPanel.textContent = message;
  959. }
  960. }
  961.  
  962. function startAutoNavigation() {
  963. if (autoNavigationActive) {
  964. return;
  965. }
  966.  
  967. questionsProcessed = 0;
  968. collectedContent = '';
  969.  
  970. const contentTextarea = document.getElementById('albert-collected-content');
  971. if (contentTextarea) {
  972. contentTextarea.value = '';
  973. }
  974.  
  975. const questionNavbar = document.querySelector('[data-testid="question-dropdown-navigator__toggle-button"]');
  976. if (questionNavbar) {
  977. const navbarText = questionNavbar.textContent.trim();
  978. const match = navbarText.match(/Question\s+\d+\s+\/\s+(\d+)/i);
  979. if (match && match[1]) {
  980. totalQuestionsToProcess = parseInt(match[1]);
  981. }
  982. }
  983.  
  984. if (!totalQuestionsToProcess) {
  985. totalQuestionsToProcess = 100;
  986. }
  987.  
  988. document.getElementById('albert-autonav-status').innerText = 'Active';
  989. document.getElementById('albert-autonav-status').className = 'albert-status-badge active';
  990. document.getElementById('albert-start-autonav').style.display = 'none';
  991. document.getElementById('albert-stop-autonav').style.display = 'block';
  992.  
  993. autoNavigationActive = true;
  994.  
  995. processCurrentQuestion();
  996. }
  997.  
  998. function stopAutoNavigation() {
  999. autoNavigationActive = false;
  1000.  
  1001. document.getElementById('albert-autonav-status').innerText = 'Stopped';
  1002. document.getElementById('albert-autonav-status').className = 'albert-status-badge inactive';
  1003. document.getElementById('albert-start-autonav').style.display = 'block';
  1004. document.getElementById('albert-stop-autonav').style.display = 'none';
  1005. }
  1006.  
  1007. function processCurrentQuestion() {
  1008. if (!autoNavigationActive) {
  1009. return;
  1010. }
  1011.  
  1012. setTimeout(() => {
  1013.  
  1014. const extract = extractQuestionContent();
  1015.  
  1016. if (extract.text.trim() !== '') {
  1017.  
  1018. collectedContent += `==================== QUESTION ${extract.number} ====================\n\n`;
  1019. collectedContent += extract.text;
  1020. collectedContent += '\n\n';
  1021.  
  1022. const contentTextarea = document.getElementById('albert-collected-content');
  1023. if (contentTextarea) {
  1024. contentTextarea.value = collectedContent;
  1025. contentTextarea.scrollTop = contentTextarea.scrollHeight;
  1026. }
  1027.  
  1028. questionsProcessed++;
  1029. updateAutoNavigationStatus();
  1030.  
  1031. if (debugMode) {
  1032. logDebug(`Question ${extract.number}: Options found = ${extract.optionsFound}`);
  1033. }
  1034. } else {
  1035. if (debugMode) {
  1036. logDebug('No content extracted for current question');
  1037. }
  1038. }
  1039.  
  1040. setTimeout(navigateToNextQuestion, autoNavigationDelay);
  1041. }, 1000);
  1042. }
  1043.  
  1044. function navigateToNextQuestion() {
  1045. if (!autoNavigationActive) {
  1046. return;
  1047. }
  1048.  
  1049. const nextButton = document.querySelector('[data-testid="question-dropdown-navigator__next-button"]');
  1050.  
  1051. if (nextButton && !nextButton.disabled) {
  1052.  
  1053. nextButton.click();
  1054.  
  1055. setTimeout(processCurrentQuestion, 1000);
  1056. } else {
  1057.  
  1058. console.log('Reached the end of the questions');
  1059. stopAutoNavigation();
  1060. alert(`Auto-collection complete! Collected ${questionsProcessed} questions. You can now download the content.`);
  1061. }
  1062. }
  1063.  
  1064. function downloadCollectedContent() {
  1065. if (collectedContent.trim() === '') {
  1066. alert('No content has been collected yet. Please start auto-collection first.');
  1067. return;
  1068. }
  1069.  
  1070. let assignmentName = document.querySelector('.student-practice-view-toolbar__title')?.textContent.trim() || 'questions';
  1071. assignmentName = assignmentName.substring(0, 30).replace(/[^a-z0-9]/gi, '_');
  1072.  
  1073. const timeStamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 16);
  1074.  
  1075. downloadTextAsFile(`albert_${assignmentName}_all_${questionsProcessed}_questions_${timeStamp}.txt`, collectedContent);
  1076. }
  1077.  
  1078. function updateAutoNavigationStatus() {
  1079. const counterElement = document.getElementById('albert-questions-processed');
  1080. if (counterElement) {
  1081. counterElement.innerText = `${questionsProcessed} / ${totalQuestionsToProcess}`;
  1082. }
  1083. }
  1084.  
  1085. function updateScraperPanel(questions) {
  1086.  
  1087. const countDisplay = document.getElementById('albert-question-count');
  1088. if (countDisplay) {
  1089. countDisplay.innerText = `Found ${questions.length} question(s)`;
  1090. }
  1091.  
  1092. const contentTextarea = document.getElementById('albert-collected-content');
  1093. if (contentTextarea) {
  1094. contentTextarea.value = collectedContent;
  1095. }
  1096.  
  1097. updateSelectedCount();
  1098. }
  1099.  
  1100. function setActiveTab(tabName) {
  1101.  
  1102. const tabs = document.querySelectorAll('.albert-tab');
  1103. tabs.forEach(tab => {
  1104. if (tab.dataset.tab === tabName) {
  1105. tab.classList.add('active');
  1106. } else {
  1107. tab.classList.remove('active');
  1108. }
  1109. });
  1110.  
  1111. const autoNavContent = document.getElementById('albert-autonav-tab');
  1112. const manualContent = document.getElementById('albert-manual-tab');
  1113.  
  1114. autoNavContent.style.display = tabName === 'autonav' ? 'block' : 'none';
  1115. manualContent.style.display = tabName === 'manual' ? 'block' : 'none';
  1116. }
  1117.  
  1118. function updateSelectedCount() {
  1119. const selectedDisplay = document.getElementById('albert-selected-count');
  1120. if (selectedDisplay) {
  1121. selectedDisplay.innerText = `Selected: ${selectedQuestions.length} question(s)`;
  1122. }
  1123. }
  1124.  
  1125. function downloadAllQuestionsAsOne(questions) {
  1126. let allContent = '';
  1127. let validQuestions = 0;
  1128.  
  1129. for (let i = 0; i < questions.length; i++) {
  1130. const question = questions[i];
  1131. const extract = extractQuestionContent(question);
  1132.  
  1133. if (extract.text.trim() !== '') {
  1134. validQuestions++;
  1135. allContent += `==================== QUESTION ${extract.number} ====================\n\n`;
  1136. allContent += extract.text;
  1137. allContent += '\n\n';
  1138. }
  1139. }
  1140.  
  1141. if (allContent.trim() !== '') {
  1142.  
  1143. let assignmentName = document.querySelector('.student-practice-view-toolbar__title')?.textContent.trim() || 'questions';
  1144. assignmentName = assignmentName.substring(0, 30).replace(/[^a-z0-9]/gi, '_');
  1145.  
  1146. const timeStamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 16);
  1147.  
  1148. downloadTextAsFile(`albert_${assignmentName}_all_${validQuestions}_questions_${timeStamp}.txt`, allContent);
  1149. } else {
  1150. alert('Could not extract any question content.');
  1151. }
  1152. }
  1153.  
  1154. function downloadSelectedQuestionsAsOne(questions) {
  1155. if (selectedQuestions.length === 0) {
  1156. alert('Please select at least one question to download.');
  1157. return;
  1158. }
  1159.  
  1160. let allContent = '';
  1161. let validQuestions = 0;
  1162.  
  1163. for (const index of selectedQuestions) {
  1164. if (index >= 0 && index < questions.length) {
  1165. const question = questions[index];
  1166. const extract = extractQuestionContent(question);
  1167.  
  1168. if (extract.text.trim() !== '') {
  1169. validQuestions++;
  1170. allContent += `==================== QUESTION ${extract.number} ====================\n\n`;
  1171. allContent += extract.text;
  1172. allContent += '\n\n';
  1173. }
  1174. }
  1175. }
  1176.  
  1177. if (allContent.trim() !== '') {
  1178.  
  1179. let assignmentName = document.querySelector('.student-practice-view-toolbar__title')?.textContent.trim() || 'questions';
  1180. assignmentName = assignmentName.substring(0, 30).replace(/[^a-z0-9]/gi, '_');
  1181.  
  1182. const timeStamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 16);
  1183.  
  1184. downloadTextAsFile(`albert_${assignmentName}_selected_${validQuestions}_questions_${timeStamp}.txt`, allContent);
  1185. } else {
  1186. alert('Could not extract any question content from selected questions.');
  1187. }
  1188. }
  1189.  
  1190. function scanForQuestions() {
  1191.  
  1192. const potentialQuestions = [];
  1193.  
  1194. const albertElements = document.querySelectorAll('.markdown-renderer-v2, .question-statement, .mcq-option');
  1195. if (albertElements.length > 0) {
  1196.  
  1197. let commonParents = [];
  1198. albertElements.forEach(el => {
  1199.  
  1200. let parent = el;
  1201. for (let i = 0; i < 3; i++) {
  1202. if (parent.parentElement) parent = parent.parentElement;
  1203. }
  1204. if (parent && !commonParents.includes(parent)) {
  1205. commonParents.push(parent);
  1206. }
  1207. });
  1208.  
  1209. commonParents.forEach(parent => {
  1210. if (!potentialQuestions.includes(parent)) {
  1211. potentialQuestions.push(parent);
  1212. }
  1213. });
  1214. }
  1215.  
  1216. const allElements = document.querySelectorAll('div:not([class*="albert-"]), section, article');
  1217. allElements.forEach(element => {
  1218. const text = element.innerText;
  1219.  
  1220. if (
  1221. (text.includes('Question') && text.length > 100) ||
  1222. /[A-E]\)\s/.test(text) ||
  1223. (element.querySelector('table') && text.includes('Instructions')) ||
  1224. (text.match(/p-value|t-test|confidence interval/i) && text.length > 200)
  1225. ) {
  1226.  
  1227. let isContained = false;
  1228. for (const potentialQuestion of potentialQuestions) {
  1229. if (potentialQuestion.contains(element) && potentialQuestion !== element) {
  1230. isContained = true;
  1231. break;
  1232. }
  1233. }
  1234.  
  1235. if (!isContained && element.offsetHeight > 100) {
  1236. potentialQuestions.push(element);
  1237. }
  1238. }
  1239. });
  1240.  
  1241. if (potentialQuestions.length > 0) {
  1242. console.log(`Found ${potentialQuestions.length} potential question elements`);
  1243. processQuestions(potentialQuestions);
  1244. } else {
  1245. console.log('No potential questions found');
  1246.  
  1247. const message = document.createElement('div');
  1248. message.style.position = 'fixed';
  1249. message.style.top = '10px';
  1250. message.style.left = '10px';
  1251. message.style.padding = '10px';
  1252. message.style.backgroundColor = '#f8d7da';
  1253. message.style.color = '#721c24';
  1254. message.style.borderRadius = '4px';
  1255. message.style.zIndex = '9999';
  1256. message.innerHTML = 'No questions detected. Try using the "Scan for Questions" button manually.';
  1257. document.body.appendChild(message);
  1258.  
  1259. setTimeout(() => {
  1260. message.remove();
  1261. }, 5000);
  1262. }
  1263. }
  1264.  
  1265. function addScanButton() {
  1266. const scanBtn = document.createElement('button');
  1267. scanBtn.className = 'albert-dl-btn';
  1268. scanBtn.innerText = 'Scan for Questions';
  1269. scanBtn.style.position = 'fixed';
  1270. scanBtn.style.bottom = '10px';
  1271. scanBtn.style.left = '10px';
  1272. scanBtn.style.zIndex = '9999';
  1273. scanBtn.addEventListener('click', function(e) {
  1274. e.preventDefault();
  1275. scanForQuestions();
  1276. });
  1277. document.body.appendChild(scanBtn);
  1278. }
  1279.  
  1280. function observeDOMChanges() {
  1281. const observer = new MutationObserver(function(mutations) {
  1282. let shouldRescan = false;
  1283.  
  1284. mutations.forEach(function(mutation) {
  1285. if (mutation.addedNodes && mutation.addedNodes.length > 0) {
  1286.  
  1287. for (let i = 0; i < mutation.addedNodes.length; i++) {
  1288. const node = mutation.addedNodes[i];
  1289. if (node.nodeType === 1) {
  1290. if (
  1291. (node.classList && (
  1292. node.classList.contains('question-wrapper') ||
  1293. node.classList.contains('question-container') ||
  1294. node.classList.contains('question')
  1295. )) ||
  1296. node.querySelector('.question-wrapper, .question-container, .question, .mcq-option')
  1297. ) {
  1298. shouldRescan = true;
  1299. break;
  1300. }
  1301. }
  1302. }
  1303. }
  1304. });
  1305.  
  1306. if (shouldRescan) {
  1307. console.log('New question content detected, rescanning...');
  1308.  
  1309. setTimeout(findAndProcessQuestions, 1000);
  1310. }
  1311. });
  1312.  
  1313. observer.observe(document.body, { childList: true, subtree: true });
  1314. }
  1315. })();