Summarize with AI

Adds a button or key shortcut to summarize articles, news, and similar content using the OpenAI API (gpt-4o-mini model). The summary is displayed in an overlay with streaming support and improved styling.

当前为 2024-09-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Summarize with AI
  3. // @namespace https://github.com/insign/summarize-with-ai
  4. // @version 2024.10.10.1212
  5. // @description Adds a button or key shortcut to summarize articles, news, and similar content using the OpenAI API (gpt-4o-mini model). The summary is displayed in an overlay with streaming support and improved styling.
  6. // @author Hélio
  7. // @license WTFPL
  8. // @match *://*/*
  9. // @grant GM_addStyle
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @connect api.openai.com
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // Add keydown event listener for 'S' key to trigger summarization
  20. document.addEventListener('keydown', function(e) {
  21. const activeElement = document.activeElement;
  22. const isInput = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable);
  23. if (!isInput && (e.key === 's' || e.key === 'S')) {
  24. onSummarizeShortcut();
  25. }
  26. });
  27.  
  28. // Add summarize button if the page is an article
  29. addSummarizeButton();
  30.  
  31. /*** Function Definitions ***/
  32.  
  33. // Function to determine if the page is an article
  34. function isArticlePage() {
  35. // Check for <article> element
  36. if (document.querySelector('article')) {
  37. return true;
  38. }
  39.  
  40. // Check for Open Graph meta tag
  41. const ogType = document.querySelector('meta[property="og:type"]');
  42. if (ogType && ogType.content === 'article') {
  43. return true;
  44. }
  45.  
  46. // Check for news content in the URL
  47. const url = window.location.href;
  48. if (/news|article|story|post/i.test(url)) {
  49. return true;
  50. }
  51.  
  52. // Check for significant text content (e.g., more than 500 words)
  53. const bodyText = document.body.innerText || "";
  54. const wordCount = bodyText.split(/\s+/).length;
  55. if (wordCount > 500) {
  56. return true;
  57. }
  58.  
  59. return false;
  60. }
  61.  
  62. // Function to add the summarize button
  63. function addSummarizeButton() {
  64. if (!isArticlePage()) {
  65. return; // Do not add the button if not an article
  66. }
  67. // Create the button element
  68. const button = document.createElement('div');
  69. button.id = 'summarize-button';
  70. button.innerText = 'S';
  71. document.body.appendChild(button);
  72.  
  73. // Add event listeners
  74. button.addEventListener('click', onSummarizeClick);
  75. button.addEventListener('dblclick', onApiKeyReset);
  76.  
  77. // Add styles
  78. GM_addStyle(`
  79. #summarize-button {
  80. position: fixed;
  81. bottom: 20px;
  82. right: 20px;
  83. width: 50px;
  84. height: 50px;
  85. background-color: #007bff;
  86. color: white;
  87. font-size: 24px;
  88. font-weight: bold;
  89. text-align: center;
  90. line-height: 50px;
  91. border-radius: 50%;
  92. cursor: pointer;
  93. z-index: 10000;
  94. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  95. }
  96. #summarize-overlay {
  97. position: fixed;
  98. top: 50%;
  99. left: 50%;
  100. transform: translate(-50%, -50%);
  101. background-color: white;
  102. z-index: 10001;
  103. padding: 20px;
  104. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  105. overflow: auto;
  106. font-size: 1.1em;
  107. max-width: 90%;
  108. max-height: 90%;
  109. }
  110. #summarize-overlay h2 {
  111. margin-top: 0;
  112. font-size: 1.5em;
  113. }
  114. #summarize-close {
  115. position: absolute;
  116. top: 10px;
  117. right: 10px;
  118. cursor: pointer;
  119. font-size: 22px;
  120. }
  121. #summarize-content {
  122. margin-top: 20px;
  123. }
  124. #summarize-error {
  125. position: fixed;
  126. bottom: 20px;
  127. left: 20px;
  128. background-color: rgba(255,0,0,0.8);
  129. color: white;
  130. padding: 10px 20px;
  131. border-radius: 5px;
  132. z-index: 10002;
  133. }
  134. @media (max-width: 768px) {
  135. #summarize-overlay {
  136. width: 90%;
  137. height: 90%;
  138. }
  139. }
  140. @media (min-width: 769px) {
  141. #summarize-overlay {
  142. width: 60%;
  143. height: 85%;
  144. }
  145. }
  146. `);
  147. }
  148.  
  149. // Handler for clicking the "S" button
  150. function onSummarizeClick() {
  151. const apiKey = getApiKey();
  152. if (!apiKey) {
  153. return;
  154. }
  155.  
  156. // Capture page source
  157. const pageContent = document.documentElement.outerHTML;
  158.  
  159. // Show summary overlay with loading message
  160. showSummaryOverlay('<p>Generating summary...</p>');
  161.  
  162. // Send content to OpenAI API
  163. summarizeContent(apiKey, pageContent);
  164. }
  165.  
  166. // Handler for the "S" key shortcut
  167. function onSummarizeShortcut() {
  168. const apiKey = getApiKey();
  169. if (!apiKey) {
  170. return;
  171. }
  172.  
  173. if (!isArticlePage()) {
  174. // Show a quick warning
  175. alert('This page may not be an article. Proceeding to summarize anyway.');
  176. }
  177.  
  178. // Capture page source
  179. const pageContent = document.documentElement.outerHTML;
  180.  
  181. // Show summary overlay with loading message
  182. showSummaryOverlay('<p>Generating summary...</p>');
  183.  
  184. // Send content to OpenAI API
  185. summarizeContent(apiKey, pageContent);
  186. }
  187.  
  188. // Handler for resetting the API key
  189. function onApiKeyReset() {
  190. const newKey = prompt('Please enter your OpenAI API key:', '');
  191. if (newKey) {
  192. GM_setValue('openai_api_key', newKey.trim());
  193. alert('API key updated successfully.');
  194. }
  195. }
  196.  
  197. // Function to get the API key
  198. function getApiKey() {
  199. let apiKey = GM_getValue('openai_api_key');
  200. if (!apiKey) {
  201. apiKey = prompt('Please enter your OpenAI API key:', '');
  202. if (apiKey) {
  203. GM_setValue('openai_api_key', apiKey.trim());
  204. } else {
  205. alert('API key is required to generate a summary.');
  206. return null;
  207. }
  208. }
  209. return apiKey.trim();
  210. }
  211.  
  212. // Function to show the summary overlay
  213. function showSummaryOverlay(initialContent = '') {
  214. // Create the overlay
  215. const overlay = document.createElement('div');
  216. overlay.id = 'summarize-overlay';
  217. overlay.innerHTML = `
  218. <div id="summarize-close">&times;</div>
  219. <div id="summarize-content">${initialContent}</div>
  220. `;
  221. document.body.appendChild(overlay);
  222.  
  223. // Add event listener for close button
  224. document.getElementById('summarize-close').addEventListener('click', () => {
  225. overlay.remove();
  226. document.removeEventListener('keydown', onEscapePress);
  227. });
  228.  
  229. // Add event listener for 'Escape' key to close the overlay
  230. document.addEventListener('keydown', onEscapePress);
  231.  
  232. function onEscapePress(e) {
  233. if (e.key === 'Escape') {
  234. overlay.remove();
  235. document.removeEventListener('keydown', onEscapePress);
  236. }
  237. }
  238. }
  239.  
  240. // Function to update the summary content incrementally
  241. function updateSummaryOverlay(content) {
  242. const contentDiv = document.getElementById('summarize-content');
  243. if (contentDiv) {
  244. contentDiv.innerHTML += content.replaceAll('\n', '<br>');
  245. }
  246. }
  247.  
  248. // Function to display an error notification
  249. function showErrorNotification(message) {
  250. const errorDiv = document.createElement('div');
  251. errorDiv.id = 'summarize-error';
  252. errorDiv.innerText = message;
  253. document.body.appendChild(errorDiv);
  254.  
  255. // Remove the notification after 4 seconds
  256. setTimeout(() => {
  257. errorDiv.remove();
  258. }, 4000);
  259. }
  260.  
  261. // Variable to hold the XMLHttpRequest for cancellation (if needed)
  262. let xhrRequest = null;
  263.  
  264. // Function to summarize the content using OpenAI API with streaming
  265. function summarizeContent(apiKey, content) {
  266. const userLanguage = navigator.language || 'en';
  267.  
  268. // Prepare the API request
  269. const apiUrl = 'https://api.openai.com/v1/chat/completions';
  270. const requestData = {
  271. model: 'gpt-4o-mini',
  272. messages: [
  273. {
  274. role: 'system', content: `You are a helpful assistant that summarizes articles based on the HTML content provided. You must generate a concise summary that includes a short introduction, followed by a list of topics, and ends with a short conclusion. For the topics, you must use appropriate emojis as bullet points, and the topics must consist of descriptive titles with no detailed descriptions.
  275.  
  276. You must always use HTML tags to structure the summary text. The title must be wrapped in h2 tags, and you must always use the user's language besides the article's original language. The generated HTML must be ready to be injected into the final target, and you must never use markdown.
  277.  
  278. Required structure:
  279. - Use h2 for the summary title
  280. - Use paragraphs for the introduction and conclusion
  281. - Use appropriate emojis for topics
  282.  
  283. User language: ${userLanguage}`
  284. },
  285. { role: 'user', content: `Page content: \n\n${content}` }
  286. ],
  287. max_tokens: 500,
  288. temperature: 0.5,
  289. n: 1,
  290. stream: true
  291. };
  292.  
  293. // Initialize variables for processing the streaming response
  294. let buffer = '';
  295. let lastPosition = 0;
  296.  
  297. // Send the request using GM_xmlhttpRequest with streaming support
  298. xhrRequest = GM_xmlhttpRequest({
  299. method: 'POST',
  300. url: apiUrl,
  301. headers: {
  302. 'Content-Type': 'application/json',
  303. 'Authorization': `Bearer ${apiKey}`
  304. },
  305. data: JSON.stringify(requestData),
  306. responseType: 'text',
  307. onprogress: function(response) {
  308. const newText = response.responseText.substring(lastPosition);
  309. lastPosition = response.responseText.length;
  310.  
  311. buffer += newText;
  312. processStreamData();
  313. },
  314. onload: function(response) {
  315. // Streaming complete
  316. },
  317. onerror: function() {
  318. showErrorNotification('Error: Network error.');
  319. },
  320. onabort: function() {
  321. showErrorNotification('Request canceled.');
  322. }
  323. });
  324.  
  325. // Function to process the streaming data
  326. function processStreamData() {
  327. let lines = buffer.split('\n');
  328. let incompleteLine = '';
  329.  
  330. // If the last line is not empty, it's an incomplete line
  331. if (lines[lines.length - 1] && lines[lines.length - 1].trim() !== '') {
  332. incompleteLine = lines.pop();
  333. }
  334.  
  335. for (let line of lines) {
  336. line = line.trim();
  337. if (line.startsWith('data: ')) {
  338. let jsonStr = line.replace('data: ', '').trim();
  339.  
  340. if (jsonStr === '[DONE]') {
  341. // Streaming complete
  342. return;
  343. }
  344.  
  345. try {
  346. let json = JSON.parse(jsonStr);
  347. let content = json.choices[0].delta.content;
  348. if (content) {
  349. // Update the summary overlay with new content
  350. updateSummaryOverlay(content);
  351. }
  352. } catch (e) {
  353. console.error('Failed to parse JSON chunk:', jsonStr);
  354. }
  355. }
  356. }
  357.  
  358. // Keep the incomplete line in buffer
  359. buffer = incompleteLine;
  360. }
  361. }
  362.  
  363. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址