ScholarQA Enhanced Copy Tools

Adds copy button and double-click functionality to extract formatted text from scholarqa.allen.ai

  1. // ==UserScript==
  2. // @name ScholarQA Enhanced Copy Tools
  3. // @namespace https://violentmonkey.github.io/
  4. // @version 1.4
  5. // @description Adds copy button and double-click functionality to extract formatted text from scholarqa.allen.ai
  6. // @author Bui Quoc Dung
  7. // @match *://scholarqa.allen.ai/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // Define CSS selector variables for easier maintenance
  15. const ACCORDION_SUMMARY_SELECTOR = '.MuiButtonBase-root.MuiAccordionSummary-root';
  16. const ACCORDION_ROOT_SELECTOR = '.MuiAccordion-root';
  17. const ACCORDION_HEADING_SELECTOR = '.MuiTypography-h5';
  18. const ACCORDION_BODY_SELECTOR = '.MuiTypography-body1';
  19. const CITATION_SELECTOR = '.MuiChip-root';
  20. const PARAGRAPH_SELECTOR = '.MuiCollapse-root';
  21. const COPY_BUTTON_CLASS = 'MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeSmall';
  22.  
  23. // Find the "Disclaimer" button dynamically based on its text content
  24. function findDisclaimerButton() {
  25. const buttons = document.querySelectorAll('button');
  26. for (let button of buttons) {
  27. if (button.textContent.includes('Disclaimer')) {
  28. return button;
  29. }
  30. }
  31. return null;
  32. }
  33.  
  34. // Expands all collapsed sections for complete content extraction
  35. function expandSections() {
  36. console.log("Expanding all sections...");
  37. document.querySelectorAll(ACCORDION_SUMMARY_SELECTOR).forEach(btn => btn.click());
  38. }
  39.  
  40. // Shows notification when content is copied
  41. function showNotification(x, y) {
  42. let notification = document.createElement('div');
  43. notification.innerText = "Copied!";
  44. Object.assign(notification.style, {
  45. position: 'absolute',
  46. left: `${x}px`,
  47. top: `${y}px`,
  48. background: 'rgba(0, 0, 0, 0.8)',
  49. color: 'white',
  50. padding: '8px 12px',
  51. borderRadius: '5px',
  52. fontSize: '14px',
  53. zIndex: '1000',
  54. transition: 'opacity 0.5s'
  55. });
  56. document.body.appendChild(notification);
  57. setTimeout(() => {
  58. notification.style.opacity = '0';
  59. setTimeout(() => document.body.removeChild(notification), 500);
  60. }, 1500);
  61. }
  62.  
  63. // Copies formatted HTML content to clipboard
  64. function copyToClipboard(text) {
  65. let tempContainer = document.createElement('div');
  66. tempContainer.innerHTML = text;
  67. document.body.appendChild(tempContainer);
  68. let range = document.createRange();
  69. range.selectNode(tempContainer);
  70. let selection = window.getSelection();
  71. selection.removeAllRanges();
  72. selection.addRange(range);
  73.  
  74. try {
  75. document.execCommand('copy');
  76. console.log("Copied formatted text.");
  77. } catch (err) {
  78. console.error("Copy failed", err);
  79. }
  80.  
  81. selection.removeAllRanges();
  82. document.body.removeChild(tempContainer);
  83. }
  84.  
  85. // Processes text from a single paragraph element
  86. function processText(paragraphElement, includeHeading = true) {
  87. let tempDiv = paragraphElement.cloneNode(true);
  88.  
  89. let paragraphText = tempDiv.innerHTML;
  90.  
  91. tempDiv.querySelectorAll(CITATION_SELECTOR).forEach(citation => {
  92. let linkElement = citation.closest('a');
  93. if (linkElement) {
  94. let hyperlink = linkElement.href;
  95. let citationText = citation.innerText;
  96. let formattedCitation = `<a href="${hyperlink}" target="_blank">${citationText}</a>`;
  97. paragraphText = paragraphText.replace(citation.outerHTML, formattedCitation);
  98. }
  99. });
  100.  
  101. paragraphText = paragraphText.replace(/<div[^>]*>/g, ' ')
  102. .replace(/<\/div>/g, '')
  103. .replace(/\s+/g, ' ');
  104.  
  105. paragraphText = paragraphText.replace(/Is this section helpful\? /g, ""); // Remove "Is this section helpful? "
  106.  
  107. if (includeHeading) {
  108. let headingText = "";
  109. let intermediateText = "";
  110.  
  111. let accordion = paragraphElement.closest(ACCORDION_ROOT_SELECTOR);
  112. if (accordion) {
  113. let headingElement = accordion.querySelector(ACCORDION_HEADING_SELECTOR);
  114. if (headingElement) {
  115. headingText = `<strong><u>${headingElement.innerText.trim()}</u></strong>`.trim();
  116. }
  117.  
  118. let intermediateElement = accordion.querySelector(ACCORDION_BODY_SELECTOR);
  119. if (intermediateElement) {
  120. intermediateText = intermediateElement.innerText.trim();
  121. }
  122.  
  123. if (headingText) {
  124. let prefix = headingText + ':\n';
  125. if (intermediateText) {
  126. prefix += intermediateText + '\n';
  127. }
  128. paragraphText = prefix + paragraphText;
  129. }
  130. }
  131. }
  132.  
  133. return paragraphText;
  134. }
  135.  
  136.  
  137. // Adds a copy button next to the "Disclaimer" button
  138. function addCopyButton() {
  139. let targetButton = findDisclaimerButton();
  140. if (!targetButton) {
  141. return;
  142. }
  143.  
  144. if (document.querySelector('#custom-copy-button')) {
  145. return;
  146. }
  147. let copyButton = document.createElement('button');
  148. copyButton.id = 'custom-copy-button';
  149. copyButton.className = COPY_BUTTON_CLASS;
  150. copyButton.style.marginLeft = '10px';
  151. copyButton.style.color = 'black';
  152. copyButton.title = "Copy Text";
  153. copyButton.textContent = "Copy";
  154.  
  155. targetButton.parentNode.insertBefore(copyButton, targetButton.nextSibling);
  156.  
  157. copyButton.addEventListener('click', (event) => {
  158. console.log("Copy button clicked!");
  159. copyButton.textContent = "Coping";
  160.  
  161. expandSections();
  162.  
  163. setTimeout(() => {
  164. let copiedText = extractAllText();
  165. copyToClipboard(copiedText);
  166. copyButton.textContent = "Copy";
  167. }, 1500);
  168. });
  169. }
  170.  
  171. // Extracts formatted text from all paragraphs
  172. function extractAllText() {
  173. let paragraphs = document.querySelectorAll(PARAGRAPH_SELECTOR);
  174. let copiedText = "";
  175.  
  176. paragraphs = Array.from(paragraphs).slice(0, -1); // Loại bỏ phần tử cuối
  177.  
  178. paragraphs.forEach(paragraph => {
  179. let processedText = processText(paragraph, true);
  180. copiedText += processedText + '\n\n';
  181. });
  182.  
  183. return copiedText;
  184. }
  185.  
  186. // Handles double-click event to copy individual paragraph
  187. function handleDoubleClick(event) {
  188. const targetDiv = event.target.closest(PARAGRAPH_SELECTOR);
  189. if (!targetDiv) return;
  190.  
  191. let accordion = targetDiv.closest(ACCORDION_ROOT_SELECTOR);
  192. if (accordion) {
  193. let button = accordion.querySelector(ACCORDION_SUMMARY_SELECTOR);
  194. if (button) {
  195. button.click();
  196. }
  197. }
  198.  
  199. setTimeout(() => {
  200. let textContent = processText(targetDiv, true);
  201. copyToClipboard(textContent);
  202. showNotification(event.pageX, event.pageY);
  203. }, 500);
  204. }
  205.  
  206. // Initialize both features
  207. function initialize() {
  208. const observer = new MutationObserver(() => {
  209. addCopyButton();
  210. });
  211.  
  212. observer.observe(document.body, { childList: true, subtree: true });
  213.  
  214. document.body.addEventListener('dblclick', handleDoubleClick);
  215.  
  216. setTimeout(addCopyButton, 2000);
  217. }
  218.  
  219. initialize();
  220. })();