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 improved styling and loading animation.

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

  1. // ==UserScript==
  2. // @name Summarize with AI
  3. // @namespace https://github.com/insign/summarize-with-ai
  4. // @version 2024.10.11.1407
  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 improved styling and loading animation.
  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: rgba(0, 123, 255, 0.9);
  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. transition: background-color 0.3s, transform 0.3s;
  96. }
  97. #summarize-button:hover {
  98. background-color: rgba(0, 123, 255, 1);
  99. transform: scale(1.1);
  100. }
  101. #summarize-overlay {
  102. position: fixed;
  103. top: 50%;
  104. left: 50%;
  105. transform: translate(-50%, -50%);
  106. background-color: white;
  107. z-index: 10001;
  108. padding: 20px;
  109. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  110. overflow: auto;
  111. font-size: 1.1em;
  112. max-width: 90%;
  113. max-height: 90%;
  114. border-radius: 8px;
  115. }
  116. #summarize-overlay h2 {
  117. margin-top: 0;
  118. font-size: 1.5em;
  119. }
  120. #summarize-close {
  121. position: absolute;
  122. top: 10px;
  123. right: 10px;
  124. cursor: pointer;
  125. font-size: 22px;
  126. }
  127. #summarize-content {
  128. margin-top: 20px;
  129. }
  130. #summarize-error {
  131. position: fixed;
  132. bottom: 20px;
  133. left: 20px;
  134. background-color: rgba(255,0,0,0.8);
  135. color: white;
  136. padding: 10px 20px;
  137. border-radius: 5px;
  138. z-index: 10002;
  139. font-size: 14px;
  140. }
  141. .glow {
  142. font-size: 1.2em;
  143. color: #fff;
  144. text-align: center;
  145. animation: glow 1.5s ease-in-out infinite alternate;
  146. }
  147. @keyframes glow {
  148. from {
  149. text-shadow: 0 0 10px #00e6e6, 0 0 20px #00e6e6, 0 0 30px #00e6e6, 0 0 40px #00e6e6, 0 0 50px #00e6e6, 0 0 60px #00e6e6;
  150. }
  151. to {
  152. text-shadow: 0 0 20px #00ffff, 0 0 30px #00ffff, 0 0 40px #00ffff, 0 0 50px #00ffff, 0 0 60px #00ffff, 0 0 70px #00ffff;
  153. }
  154. }
  155. /* Media Queries para dispositivos móveis */
  156. @media (max-width: 768px) {
  157. #summarize-button {
  158. width: 60px;
  159. height: 60px;
  160. font-size: 28px;
  161. line-height: 60px;
  162. bottom: 15px;
  163. right: 15px;
  164. }
  165. #summarize-overlay {
  166. width: 95%;
  167. height: 95%;
  168. }
  169. #summarize-error {
  170. bottom: 15px;
  171. left: 15px;
  172. font-size: 12px;
  173. }
  174. }
  175. /* Ajustes para telas muito pequenas */
  176. @media (max-width: 480px) {
  177. #summarize-button {
  178. width: 70px;
  179. height: 70px;
  180. font-size: 32px;
  181. line-height: 70px;
  182. bottom: 10px;
  183. right: 10px;
  184. }
  185. #summarize-overlay {
  186. padding: 15px;
  187. }
  188. #summarize-error {
  189. padding: 8px 16px;
  190. font-size: 11px;
  191. }
  192. }
  193. `);
  194. }
  195.  
  196. // Handler for clicking the "S" button
  197. function onSummarizeClick() {
  198. const apiKey = getApiKey();
  199. if (!apiKey) {
  200. return;
  201. }
  202.  
  203. // Capture page source
  204. const pageContent = document.documentElement.outerHTML;
  205.  
  206. // Show summary overlay with loading message
  207. showSummaryOverlay('<p class="glow">Gerando resumo...</p>');
  208.  
  209. // Send content to OpenAI API
  210. summarizeContent(apiKey, pageContent);
  211. }
  212.  
  213. // Handler for the "S" key shortcut
  214. function onSummarizeShortcut() {
  215. const apiKey = getApiKey();
  216. if (!apiKey) {
  217. return;
  218. }
  219.  
  220. if (!isArticlePage()) {
  221. // Show a quick warning
  222. alert('Esta página pode não ser um artigo. Prosseguindo para resumir de qualquer forma.');
  223. }
  224.  
  225. // Capture page source
  226. const pageContent = document.documentElement.outerHTML;
  227.  
  228. // Show summary overlay with loading message
  229. showSummaryOverlay('<p class="glow">Gerando resumo...</p>');
  230.  
  231. // Send content to OpenAI API
  232. summarizeContent(apiKey, pageContent);
  233. }
  234.  
  235. // Handler for resetting the API key
  236. function onApiKeyReset() {
  237. const newKey = prompt('Por favor, insira sua chave de API da OpenAI:', '');
  238. if (newKey) {
  239. GM_setValue('openai_api_key', newKey.trim());
  240. alert('Chave de API atualizada com sucesso.');
  241. }
  242. }
  243.  
  244. // Function to get the API key
  245. function getApiKey() {
  246. let apiKey = GM_getValue('openai_api_key');
  247. if (!apiKey) {
  248. apiKey = prompt('Por favor, insira sua chave de API da OpenAI:', '');
  249. if (apiKey) {
  250. GM_setValue('openai_api_key', apiKey.trim());
  251. } else {
  252. alert('A chave de API é necessária para gerar um resumo.');
  253. return null;
  254. }
  255. }
  256. return apiKey.trim();
  257. }
  258.  
  259. // Function to show the summary overlay
  260. function showSummaryOverlay(initialContent = '') {
  261. // Create the overlay
  262. const overlay = document.createElement('div');
  263. overlay.id = 'summarize-overlay';
  264. overlay.innerHTML = `
  265. <div id="summarize-close">&times;</div>
  266. <div id="summarize-content">${initialContent}</div>
  267. `;
  268. document.body.appendChild(overlay);
  269.  
  270. // Disable background scrolling
  271. document.body.style.overflow = 'hidden';
  272.  
  273. // Add event listeners for closing the overlay
  274. document.getElementById('summarize-close').addEventListener('click', closeOverlay);
  275. overlay.addEventListener('click', function(e) {
  276. if (e.target === overlay) {
  277. closeOverlay();
  278. }
  279. });
  280. document.addEventListener('keydown', onEscapePress);
  281.  
  282. function onEscapePress(e) {
  283. if (e.key === 'Escape') {
  284. closeOverlay();
  285. }
  286. }
  287.  
  288. function closeOverlay() {
  289. overlay.remove();
  290. document.body.style.overflow = '';
  291. document.removeEventListener('keydown', onEscapePress);
  292. }
  293. }
  294.  
  295. // Function to update the summary content
  296. function updateSummaryOverlay(content) {
  297. const contentDiv = document.getElementById('summarize-content');
  298. if (contentDiv) {
  299. contentDiv.innerHTML = content;
  300. }
  301. }
  302.  
  303. // Function to display an error notification
  304. function showErrorNotification(message) {
  305. const errorDiv = document.createElement('div');
  306. errorDiv.id = 'summarize-error';
  307. errorDiv.innerText = message;
  308. document.body.appendChild(errorDiv);
  309.  
  310. // Remove the notification after 4 seconds
  311. setTimeout(() => {
  312. errorDiv.remove();
  313. }, 4000);
  314. }
  315.  
  316. // Function to summarize the content using OpenAI API (non-streaming)
  317. function summarizeContent(apiKey, content) {
  318. const userLanguage = navigator.language || 'pt-BR'; // Ajuste para português por padrão
  319.  
  320. // Prepare the API request
  321. const apiUrl = 'https://api.openai.com/v1/chat/completions';
  322. const requestData = {
  323. model: 'gpt-4o-mini',
  324. messages: [
  325. {
  326. role: 'system', content: `Você é um assistente útil que resume artigos com base no conteúdo HTML fornecido. Você deve gerar um resumo conciso que inclua uma breve introdução, seguida por uma lista de tópicos e termine com uma breve conclusão. Para os tópicos, você deve usar emojis apropriados como marcadores, e os tópicos devem consistir em títulos descritivos resumindo o assunto do tópico.
  327.  
  328. Você deve sempre usar tags HTML para estruturar o texto do resumo. O título deve estar envolvido em tags h2, e você deve sempre usar o idioma do usuário além do idioma original do artigo. O HTML gerado deve estar pronto para ser injetado no destino final, e você nunca deve usar markdown.
  329.  
  330. Estrutura necessária:
  331. - Use h2 para o título do resumo
  332. - Use parágrafos para a introdução e conclusão
  333. - Use emojis apropriados para tópicos
  334. - Não adicione textos como "Resumo do artigo" ou "Sumário do artigo" no resumo, nem "Introdução", "Tópicos", "Conclusão", etc.
  335.  
  336. Idioma do usuário: ${userLanguage}.
  337. Adapte o texto para ser curto, conciso e informativo.
  338. `
  339. },
  340. { role: 'user', content: `Conteúdo da página: \n\n${content}` }
  341. ],
  342. max_tokens: 500,
  343. temperature: 0.5,
  344. n: 1,
  345. stream: false
  346. };
  347.  
  348. // Send the request using GM_xmlhttpRequest
  349. GM_xmlhttpRequest({
  350. method: 'POST',
  351. url: apiUrl,
  352. headers: {
  353. 'Content-Type': 'application/json',
  354. 'Authorization': `Bearer ${apiKey}`
  355. },
  356. data: JSON.stringify(requestData),
  357. onload: function(response) {
  358. if (response.status === 200) {
  359. const resData = JSON.parse(response.responseText);
  360. if (resData.choices && resData.choices.length > 0) {
  361. const summary = resData.choices[0].message.content;
  362. updateSummaryOverlay(summary.replaceAll('\n', '<br>'));
  363. } else {
  364. showErrorNotification('Erro: Resposta inválida da API.');
  365. updateSummaryOverlay('<p>Erro: Resposta inválida da API.</p>');
  366. }
  367. } else if (response.status === 401) {
  368. showErrorNotification('Erro: Chave de API inválida.');
  369. updateSummaryOverlay('<p>Erro: Chave de API inválida.</p>');
  370. } else {
  371. showErrorNotification(`Erro: Falha ao recuperar o resumo. Status: ${response.status}`);
  372. updateSummaryOverlay(`<p>Erro: Falha ao recuperar o resumo. Status: ${response.status}</p>`);
  373. }
  374. },
  375. onerror: function() {
  376. showErrorNotification('Erro: Problema de rede.');
  377. updateSummaryOverlay('<p>Erro: Problema de rede.</p>');
  378. },
  379. onabort: function() {
  380. showErrorNotification('Requisição cancelada.');
  381. updateSummaryOverlay('<p>Requisição cancelada.</p>');
  382. }
  383. });
  384. }
  385.  
  386. })();