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.11.18
  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: 22px;
  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. <div>${summaryText.replaceAll('\n', '<br>')}</div>
  244. `;
  245. document.body.appendChild(overlay);
  246.  
  247. // Add event listener for close button
  248. document.getElementById('summarize-close').addEventListener('click', () => {
  249. overlay.remove();
  250. });
  251. }
  252.  
  253. // Function to display an error notification
  254. function showErrorNotification(message) {
  255. const errorDiv = document.createElement('div');
  256. errorDiv.id = 'summarize-error';
  257. errorDiv.innerText = message;
  258. document.body.appendChild(errorDiv);
  259.  
  260. // Remove the notification after 2 seconds
  261. setTimeout(() => {
  262. errorDiv.remove();
  263. }, 2000);
  264. }
  265.  
  266. // Variable to hold the XMLHttpRequest for cancellation
  267. let xhrRequest = null;
  268.  
  269. // Function to summarize the content using OpenAI API
  270. function summarizeContent(apiKey, content) {
  271. const userLanguage = navigator.language;
  272.  
  273. // Prepare the API request
  274. const apiUrl = 'https://api.openai.com/v1/chat/completions';
  275. const requestData = {
  276. model: 'gpt-4o-mini',
  277. messages: [
  278. {
  279. 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.
  280.  
  281. 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.
  282.  
  283. Required structure:
  284. - Use h2 for the summary title
  285. - Use paragraphs for the introduction and conclusion
  286. - Use appropriate emojis for topics
  287.  
  288. User language: ${userLanguage}`
  289. },
  290. { role: 'user', content: `Page content: \n\n${content}` }
  291. ],
  292. max_tokens: 500,
  293. temperature: 0.5,
  294. n: 1,
  295. stream: false
  296. };
  297.  
  298. // Send the request using GM_xmlhttpRequest
  299. xhrRequest = GM_xmlhttpRequest({
  300. method: 'POST',
  301. url: apiUrl,
  302. headers: {
  303. 'Content-Type': 'application/json',
  304. 'Authorization': `Bearer ${apiKey}`
  305. },
  306. data: JSON.stringify(requestData),
  307. onload: function(response) {
  308. removeLoadingOverlay();
  309. if (response.status === 200) {
  310. const resData = JSON.parse(response.responseText);
  311. const summary = resData.choices[0].message.content;
  312. showSummaryOverlay(summary);
  313. } else {
  314. showErrorNotification('Error: Failed to retrieve summary.');
  315. }
  316. },
  317. onerror: function() {
  318. removeLoadingOverlay();
  319. showErrorNotification('Error: Network error.');
  320. },
  321. onabort: function() {
  322. removeLoadingOverlay();
  323. showErrorNotification('Request canceled.');
  324. }
  325. });
  326. }
  327.  
  328. })();