Summarize with AI

Adds a little button to summarize articles, news, and similar content using the OpenAI API (gpt-4o-mini model). The button only appears on pages detected as articles or news. The summary is displayed in a responsive overlay with a loading effect and error handling.

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

  1. // ==UserScript==
  2. // @name Summarize with AI
  3. // @namespace https://github.com/insign/summarize-with-ai
  4. // @version 2024.09.19.09.58
  5. // @description Adds a little button to summarize articles, news, and similar content using the OpenAI API (gpt-4o-mini model). The button only appears on pages detected as articles or news. The summary is displayed in a responsive overlay with a loading effect and error handling.
  6. // @author Hélio <open@helio.me>
  7. // @license WTFPL
  8. // @match *://*/*
  9. // @grant GM_addStyle
  10. // @grant GM_xmlhttpRequest
  11. // @connect api.openai.com
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // Check if the current page is an article or news content
  18. if (!isArticlePage()) {
  19. return;
  20. }
  21.  
  22. // Add the "S" button to the page
  23. addSummarizeButton();
  24.  
  25. /*** Function Definitions ***/
  26.  
  27. // Function to determine if the page is an article
  28. function isArticlePage() {
  29. // Check for <article> element
  30. if (document.querySelector('article')) {
  31. return true;
  32. }
  33.  
  34. // Check for Open Graph meta tag
  35. const ogType = document.querySelector('meta[property="og:type"]');
  36. if (ogType && ogType.content === 'article') {
  37. return true;
  38. }
  39.  
  40. // Check for news content in the URL
  41. const url = window.location.href;
  42. if (/news|article|story|post/i.test(url)) {
  43. return true;
  44. }
  45.  
  46. // Check for significant text content (e.g., more than 500 words)
  47. const bodyText = document.body.innerText || "";
  48. const wordCount = bodyText.split(/\s+/).length;
  49. if (wordCount > 500) {
  50. return true;
  51. }
  52.  
  53. return false;
  54. }
  55.  
  56. // Function to add the summarize button
  57. function addSummarizeButton() {
  58. // Create the button element
  59. const button = document.createElement('div');
  60. button.id = 'summarize-button';
  61. button.innerText = 'S';
  62. document.body.appendChild(button);
  63.  
  64. // Add event listeners
  65. button.addEventListener('click', onSummarizeClick);
  66. button.addEventListener('dblclick', onApiKeyReset);
  67.  
  68. // Add styles
  69. GM_addStyle(`
  70. #summarize-button {
  71. position: fixed;
  72. bottom: 20px;
  73. right: 20px;
  74. width: 50px;
  75. height: 50px;
  76. background-color: #007bff;
  77. color: white;
  78. font-size: 24px;
  79. font-weight: bold;
  80. text-align: center;
  81. line-height: 50px;
  82. border-radius: 50%;
  83. cursor: pointer;
  84. z-index: 10000;
  85. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  86. }
  87. #summarize-overlay {
  88. position: fixed;
  89. top: 50%;
  90. left: 50%;
  91. transform: translate(-50%, -50%);
  92. background-color: white;
  93. z-index: 10001;
  94. padding: 20px;
  95. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  96. overflow: auto;
  97. }
  98. #summarize-overlay h2 {
  99. margin-top: 0;
  100. }
  101. #summarize-close {
  102. position: absolute;
  103. top: 10px;
  104. right: 10px;
  105. cursor: pointer;
  106. font-size: 18px;
  107. }
  108. #summarize-loading {
  109. position: fixed;
  110. top: 0;
  111. left: 0;
  112. width: 100%;
  113. height: 100%;
  114. background: linear-gradient(45deg, #007bff, #00ff6a, #007bff);
  115. background-size: 600% 600%;
  116. animation: GradientAnimation 3s ease infinite;
  117. z-index: 10000;
  118. display: flex;
  119. align-items: center;
  120. justify-content: center;
  121. flex-direction: column;
  122. color: white;
  123. font-size: 24px;
  124. }
  125. @keyframes GradientAnimation {
  126. 0%{background-position:0% 50%}
  127. 50%{background-position:100% 50%}
  128. 100%{background-position:0% 50%}
  129. }
  130. #summarize-cancel {
  131. margin-top: 20px;
  132. padding: 10px 20px;
  133. background-color: rgba(0,0,0,0.3);
  134. border: none;
  135. color: white;
  136. font-size: 18px;
  137. cursor: pointer;
  138. }
  139. #summarize-error {
  140. position: fixed;
  141. bottom: 20px;
  142. left: 20px;
  143. background-color: rgba(255,0,0,0.8);
  144. color: white;
  145. padding: 10px 20px;
  146. border-radius: 5px;
  147. z-index: 10002;
  148. }
  149. @media (max-width: 768px) {
  150. #summarize-overlay {
  151. width: 90%;
  152. height: 90%;
  153. }
  154. }
  155. @media (min-width: 769px) {
  156. #summarize-overlay {
  157. width: 60%;
  158. height: 85%;
  159. }
  160. }
  161. `);
  162. }
  163.  
  164. // Handler for clicking the "S" button
  165. function onSummarizeClick() {
  166. const apiKey = getApiKey();
  167. if (!apiKey) {
  168. return;
  169. }
  170.  
  171. // Capture page source
  172. const pageContent = document.documentElement.outerHTML;
  173.  
  174. // Show loading overlay
  175. showLoadingOverlay();
  176.  
  177. // Send content to OpenAI API
  178. summarizeContent(apiKey, pageContent);
  179. }
  180.  
  181. // Handler for resetting the API key
  182. function onApiKeyReset() {
  183. const newKey = prompt('Please enter your OpenAI API key:', '');
  184. if (newKey) {
  185. localStorage.setItem('openai_api_key', newKey.trim());
  186. alert('API key updated successfully.');
  187. }
  188. }
  189.  
  190. // Function to get the API key
  191. function getApiKey() {
  192. let apiKey = localStorage.getItem('openai_api_key');
  193. if (!apiKey) {
  194. apiKey = prompt('Please enter your OpenAI API key:', '');
  195. if (apiKey) {
  196. localStorage.setItem('openai_api_key', apiKey.trim());
  197. } else {
  198. alert('API key is required to generate a summary.');
  199. return null;
  200. }
  201. }
  202. return apiKey.trim();
  203. }
  204.  
  205. // Function to show the loading overlay with animation
  206. function showLoadingOverlay() {
  207. // Create the loading overlay
  208. const loadingDiv = document.createElement('div');
  209. loadingDiv.id = 'summarize-loading';
  210. loadingDiv.innerHTML = `
  211. <div>Generating summary...</div>
  212. <button id="summarize-cancel">Cancel</button>
  213. `;
  214. document.body.appendChild(loadingDiv);
  215.  
  216. // Add event listener for cancel button
  217. document.getElementById('summarize-cancel').addEventListener('click', onCancelRequest);
  218. }
  219.  
  220. // Handler to cancel the API request
  221. function onCancelRequest() {
  222. if (xhrRequest) {
  223. xhrRequest.abort();
  224. removeLoadingOverlay();
  225. }
  226. }
  227.  
  228. // Function to remove the loading overlay
  229. function removeLoadingOverlay() {
  230. const loadingDiv = document.getElementById('summarize-loading');
  231. if (loadingDiv) {
  232. loadingDiv.remove();
  233. }
  234. }
  235.  
  236. // Function to display the summary in an overlay
  237. function showSummaryOverlay(summaryText) {
  238. // Create the overlay
  239. const overlay = document.createElement('div');
  240. overlay.id = 'summarize-overlay';
  241. overlay.innerHTML = `
  242. <div id="summarize-close">&times;</div>
  243. <h2>Summary</h2>
  244. <div>${summaryText.replace(/\n/g, '<br>')}</div>
  245. `;
  246. document.body.appendChild(overlay);
  247.  
  248. // Add event listener for close button
  249. document.getElementById('summarize-close').addEventListener('click', () => {
  250. overlay.remove();
  251. });
  252. }
  253.  
  254. // Function to display an error notification
  255. function showErrorNotification(message) {
  256. const errorDiv = document.createElement('div');
  257. errorDiv.id = 'summarize-error';
  258. errorDiv.innerText = message;
  259. document.body.appendChild(errorDiv);
  260.  
  261. // Remove the notification after 2 seconds
  262. setTimeout(() => {
  263. errorDiv.remove();
  264. }, 2000);
  265. }
  266.  
  267. // Variable to hold the XMLHttpRequest for cancellation
  268. let xhrRequest = null;
  269.  
  270. // Function to summarize the content using OpenAI API
  271. function summarizeContent(apiKey, content) {
  272. // Prepare the API request
  273. const apiUrl = 'https://api.openai.com/v1/chat/completions';
  274. const requestData = {
  275. model: 'gpt-4o-mini',
  276. messages: [
  277. { role: 'system', content: 'You are a helpful assistant that summarizes articles.' },
  278. { role: 'user', content: `Please provide a concise summary of the following article, add a small introduction and conclusion, in the middle list topics but instead of bullet points use the most appropriate emoji to indicate the topic: \n\n${content}` }
  279. ],
  280. max_tokens: 500,
  281. temperature: 0.5,
  282. n: 1,
  283. stream: false
  284. };
  285.  
  286. // Send the request using GM_xmlhttpRequest
  287. xhrRequest = GM_xmlhttpRequest({
  288. method: 'POST',
  289. url: apiUrl,
  290. headers: {
  291. 'Content-Type': 'application/json',
  292. 'Authorization': `Bearer ${apiKey}`
  293. },
  294. data: JSON.stringify(requestData),
  295. onload: function(response) {
  296. removeLoadingOverlay();
  297. if (response.status === 200) {
  298. const resData = JSON.parse(response.responseText);
  299. const summary = resData.choices[0].message.content;
  300. showSummaryOverlay(summary);
  301. } else {
  302. showErrorNotification('Error: Failed to retrieve summary.');
  303. }
  304. },
  305. onerror: function() {
  306. removeLoadingOverlay();
  307. showErrorNotification('Error: Network error.');
  308. },
  309. onabort: function() {
  310. removeLoadingOverlay();
  311. showErrorNotification('Request canceled.');
  312. }
  313. });
  314. }
  315.  
  316. })();