Summarize with AI

Adiciona um botão ou atalho de teclado para resumir artigos, notícias e conteúdos similares usando a API da OpenAI (modelo gpt-4o-mini). O resumo é exibido em uma sobreposição com estilos aprimorados e animação de carregamento.

目前為 2024-09-29 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Summarize with AI
  3. // @namespace https://github.com/insign/summarize-with-ai
  4. // @version 2024.10.11.1453
  5. // @description Adiciona um botão ou atalho de teclado para resumir artigos, notícias e conteúdos similares usando a API da OpenAI (modelo gpt-4o-mini). O resumo é exibido em uma sobreposição com estilos aprimorados e animação de carregamento.
  6. // @author Hélio
  7. // @license GPL-3.0
  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. /*** Inicialização ***/
  20.  
  21. // Adicionar evento de teclado para a tecla 'S' para acionar a sumarização
  22. document.addEventListener('keydown', function(e) {
  23. const activeElement = document.activeElement;
  24. const isInput = activeElement && (['INPUT', 'TEXTAREA'].includes(activeElement.tagName) || activeElement.isContentEditable);
  25. if (!isInput && (e.key === 's' || e.key === 'S')) {
  26. onSummarizeShortcut();
  27. }
  28. });
  29.  
  30. // Verificar se a página é um artigo e adicionar o botão se for
  31. isArticlePage().then(function(isArticle) {
  32. if (isArticle) {
  33. addSummarizeButton();
  34. }
  35. }).catch(function(error) {
  36. console.error('Erro ao verificar se a página é um artigo:', error);
  37. });
  38.  
  39. /*** Definições de Funções ***/
  40.  
  41. // Função para determinar se a página é um artigo
  42. function isArticlePage() {
  43. return new Promise(function(resolve, reject) {
  44. try {
  45. // Verificar se existe um elemento <article>
  46. if (document.querySelector('article')) {
  47. resolve(true);
  48. return;
  49. }
  50.  
  51. // Verificar a meta tag Open Graph
  52. const ogType = document.querySelector('meta[property="og:type"]');
  53. if (ogType && ogType.content === 'article') {
  54. resolve(true);
  55. return;
  56. }
  57.  
  58. // Verificar se a URL contém termos relacionados a notícias ou artigos
  59. const url = window.location.href;
  60. if (/news|article|story|post/i.test(url)) {
  61. resolve(true);
  62. return;
  63. }
  64.  
  65. // Verificar o conteúdo textual significativo (mais de 500 palavras)
  66. const bodyText = document.body.innerText || "";
  67. const wordCount = bodyText.split(/\s+/).length;
  68. if (wordCount > 500) {
  69. resolve(true);
  70. return;
  71. }
  72.  
  73. resolve(false);
  74. } catch (error) {
  75. reject(error);
  76. }
  77. });
  78. }
  79.  
  80. // Função para adicionar o botão de sumarização
  81. function addSummarizeButton() {
  82. // Criar o elemento do botão
  83. const button = document.createElement('div');
  84. button.id = 'summarize-button';
  85. button.innerText = 'S';
  86. document.body.appendChild(button);
  87.  
  88. // Adicionar listeners de eventos
  89. button.addEventListener('click', onSummarizeClick);
  90. button.addEventListener('dblclick', onApiKeyReset);
  91.  
  92. // Adicionar estilos via GM.addStyle
  93. GM.addStyle(`
  94. #summarize-button {
  95. position: fixed;
  96. bottom: 20px;
  97. right: 20px;
  98. width: 50px;
  99. height: 50px;
  100. background-color: rgba(0, 123, 255, 0.9);
  101. color: white;
  102. font-size: 24px;
  103. font-weight: bold;
  104. text-align: center;
  105. line-height: 50px;
  106. border-radius: 50%;
  107. cursor: pointer;
  108. z-index: 10000;
  109. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  110. transition: background-color 0.3s, transform 0.3s;
  111. }
  112. #summarize-button:hover {
  113. background-color: rgba(0, 123, 255, 1);
  114. transform: scale(1.1);
  115. }
  116. #summarize-overlay {
  117. position: fixed;
  118. top: 50%;
  119. left: 50%;
  120. transform: translate(-50%, -50%);
  121. background-color: white;
  122. z-index: 10001;
  123. padding: 20px;
  124. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  125. overflow: auto;
  126. font-size: 1.1em;
  127. max-width: 90%;
  128. max-height: 90%;
  129. border-radius: 8px;
  130. }
  131. #summarize-overlay h2 {
  132. margin-top: 0;
  133. font-size: 1.5em;
  134. }
  135. #summarize-close {
  136. position: absolute;
  137. top: 10px;
  138. right: 10px;
  139. cursor: pointer;
  140. font-size: 22px;
  141. }
  142. #summarize-content {
  143. margin-top: 20px;
  144. }
  145. #summarize-error {
  146. position: fixed;
  147. bottom: 20px;
  148. left: 20px;
  149. background-color: rgba(255,0,0,0.8);
  150. color: white;
  151. padding: 10px 20px;
  152. border-radius: 5px;
  153. z-index: 10002;
  154. font-size: 14px;
  155. }
  156. .glow {
  157. font-size: 1.2em;
  158. color: #fff;
  159. text-align: center;
  160. animation: glow 1.5s ease-in-out infinite alternate;
  161. }
  162. @keyframes glow {
  163. from {
  164. 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;
  165. }
  166. to {
  167. 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;
  168. }
  169. }
  170. /* Media Queries para dispositivos móveis */
  171. @media (max-width: 768px) {
  172. #summarize-button {
  173. width: 60px;
  174. height: 60px;
  175. font-size: 28px;
  176. line-height: 60px;
  177. bottom: 15px;
  178. right: 15px;
  179. }
  180. #summarize-overlay {
  181. width: 95%;
  182. height: 95%;
  183. }
  184. #summarize-error {
  185. bottom: 15px;
  186. left: 15px;
  187. font-size: 12px;
  188. }
  189. }
  190. /* Ajustes para telas muito pequenas */
  191. @media (max-width: 480px) {
  192. #summarize-button {
  193. width: 70px;
  194. height: 70px;
  195. font-size: 32px;
  196. line-height: 70px;
  197. bottom: 10px;
  198. right: 10px;
  199. }
  200. #summarize-overlay {
  201. padding: 15px;
  202. }
  203. #summarize-error {
  204. padding: 8px 16px;
  205. font-size: 11px;
  206. }
  207. }
  208. `);
  209. }
  210.  
  211. // Handler para o clique no botão "S"
  212. function onSummarizeClick() {
  213. processSummarization();
  214. }
  215.  
  216. // Handler para o atalho de teclado "S"
  217. function onSummarizeShortcut() {
  218. isArticlePage().then(function(isArticle) {
  219. if (!isArticle) {
  220. alert('Esta página pode não ser um artigo. Prosseguindo para resumir de qualquer forma.');
  221. }
  222. processSummarization();
  223. }).catch(function(error) {
  224. console.error('Erro ao verificar se a página é um artigo no atalho:', error);
  225. processSummarization();
  226. });
  227. }
  228.  
  229. // Função para processar a sumarização
  230. function processSummarization() {
  231. getApiKey().then(function(apiKey) {
  232. if (!apiKey) {
  233. return;
  234. }
  235.  
  236. // Capturar o conteúdo da página
  237. const pageContent = document.documentElement.outerHTML;
  238.  
  239. // Mostrar sobreposição de resumo com mensagem de carregamento
  240. showSummaryOverlay('<p class="glow">Gerando resumo...</p>');
  241.  
  242. // Enviar conteúdo para a API da OpenAI
  243. summarizeContent(apiKey, pageContent);
  244. }).catch(function(error) {
  245. showErrorNotification('Erro: Falha ao obter a chave da API.');
  246. updateSummaryOverlay('<p>Erro: Falha ao obter a chave da API.</p>');
  247. console.error('Erro ao obter a chave da API:', error);
  248. });
  249. }
  250.  
  251. // Handler para resetar a chave da API
  252. function onApiKeyReset() {
  253. const newKey = prompt('Por favor, insira sua chave de API da OpenAI:', '');
  254. if (newKey) {
  255. GM.setValue('openai_api_key', newKey.trim()).then(function() {
  256. alert('Chave de API atualizada com sucesso.');
  257. }).catch(function(error) {
  258. alert('Erro ao atualizar a chave de API.');
  259. console.error('Erro ao atualizar a chave de API:', error);
  260. });
  261. }
  262. }
  263.  
  264. // Função para obter a chave da API
  265. function getApiKey() {
  266. return new Promise(function(resolve, reject) {
  267. GM.getValue('openai_api_key').then(function(apiKey) {
  268. if (apiKey) {
  269. resolve(apiKey.trim());
  270. } else {
  271. const userInput = prompt('Por favor, insira sua chave de API da OpenAI:', '');
  272. if (userInput) {
  273. GM.setValue('openai_api_key', userInput.trim()).then(function() {
  274. resolve(userInput.trim());
  275. }).catch(function(error) {
  276. reject(error);
  277. });
  278. } else {
  279. alert('A chave de API é necessária para gerar um resumo.');
  280. resolve(null);
  281. }
  282. }
  283. }).catch(function(error) {
  284. reject(error);
  285. });
  286. });
  287. }
  288.  
  289. // Função para exibir a sobreposição de resumo
  290. function showSummaryOverlay(content) {
  291. // Verificar se a sobreposição já existe
  292. if (document.getElementById('summarize-overlay')) {
  293. updateSummaryOverlay(content);
  294. return;
  295. }
  296.  
  297. // Criar a sobreposição
  298. const overlay = document.createElement('div');
  299. overlay.id = 'summarize-overlay';
  300. overlay.innerHTML = `
  301. <div id="summarize-close">&times;</div>
  302. <div id="summarize-content">${content}</div>
  303. `;
  304. document.body.appendChild(overlay);
  305.  
  306. // Desabilitar a rolagem de fundo
  307. document.body.style.overflow = 'hidden';
  308.  
  309. // Adicionar listeners para fechar a sobreposição
  310. document.getElementById('summarize-close').addEventListener('click', closeOverlay);
  311. overlay.addEventListener('click', function(e) {
  312. if (e.target === overlay) {
  313. closeOverlay();
  314. }
  315. });
  316. document.addEventListener('keydown', onEscapePress);
  317.  
  318. function onEscapePress(e) {
  319. if (e.key === 'Escape') {
  320. closeOverlay();
  321. }
  322. }
  323.  
  324. function closeOverlay() {
  325. if (document.getElementById('summarize-overlay')) {
  326. document.getElementById('summarize-overlay').remove();
  327. document.body.style.overflow = '';
  328. document.removeEventListener('keydown', onEscapePress);
  329. }
  330. }
  331. }
  332.  
  333. // Função para atualizar o conteúdo da sobreposição de resumo
  334. function updateSummaryOverlay(content) {
  335. const contentDiv = document.getElementById('summarize-content');
  336. if (contentDiv) {
  337. contentDiv.innerHTML = content;
  338. }
  339. }
  340.  
  341. // Função para exibir uma notificação de erro
  342. function showErrorNotification(message) {
  343. // Verificar se a notificação já existe
  344. if (document.getElementById('summarize-error')) {
  345. document.getElementById('summarize-error').innerText = message;
  346. return;
  347. }
  348.  
  349. const errorDiv = document.createElement('div');
  350. errorDiv.id = 'summarize-error';
  351. errorDiv.innerText = message;
  352. document.body.appendChild(errorDiv);
  353.  
  354. // Remover a notificação após 4 segundos
  355. setTimeout(function() {
  356. if (document.getElementById('summarize-error')) {
  357. document.getElementById('summarize-error').remove();
  358. }
  359. }, 4000);
  360. }
  361.  
  362. // Função para resumir o conteúdo usando a API da OpenAI (não streaming)
  363. function summarizeContent(apiKey, content) {
  364. const userLanguage = navigator.language || 'pt-BR'; // Ajuste para português por padrão
  365.  
  366. // Preparar a requisição para a API
  367. const apiUrl = 'https://api.openai.com/v1/chat/completions';
  368. const requestData = {
  369. model: 'gpt-4o-mini',
  370. messages: [
  371. {
  372. role: 'system',
  373. 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.
  374.  
  375. 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.
  376.  
  377. Estrutura necessária:
  378. - Use h2 para o título do resumo
  379. - Use parágrafos para a introdução e conclusão
  380. - Use emojis apropriados para tópicos
  381. - Não adicione textos como "Resumo do artigo" ou "Sumário do artigo" no resumo, nem "Introdução", "Tópicos", "Conclusão", etc.
  382.  
  383. Idioma do usuário: ${userLanguage}.
  384. Adapte o texto para ser curto, conciso e informativo.
  385. `
  386. },
  387. { role: 'user', content: `Conteúdo da página:\n\n${content}` }
  388. ],
  389. max_tokens: 500,
  390. temperature: 0.5,
  391. n: 1,
  392. stream: false
  393. };
  394.  
  395. GM.xmlHttpRequest({
  396. method: 'POST',
  397. url: apiUrl,
  398. headers: {
  399. 'Content-Type': 'application/json',
  400. 'Authorization': `Bearer ${apiKey}`
  401. },
  402. data: JSON.stringify(requestData),
  403. onload: function(response) {
  404. if (response && response.status === 200) {
  405. try {
  406. const resData = JSON.parse(response.responseText);
  407. if (resData.choices && resData.choices.length > 0) {
  408. const summary = resData.choices[0].message.content;
  409. updateSummaryOverlay(summary.replace(/\n/g, '<br>'));
  410. } else {
  411. showErrorNotification('Erro: Resposta inválida da API.');
  412. updateSummaryOverlay('<p>Erro: Resposta inválida da API.</p>');
  413. }
  414. } catch (parseError) {
  415. showErrorNotification('Erro: Falha ao processar a resposta da API.');
  416. updateSummaryOverlay('<p>Erro: Falha ao processar a resposta da API.</p>');
  417. console.error('Erro ao analisar a resposta da API:', parseError);
  418. }
  419. } else if (response && response.status === undefined) {
  420. // Tratamento para caso o status esteja indefinido
  421. showErrorNotification('Erro: Resposta inesperada da API.');
  422. console.error('Resposta da API sem status:', response);
  423. updateSummaryOverlay('<p>Erro: Resposta inesperada da API.</p>');
  424. } else if (response && response.status === 401) {
  425. showErrorNotification('Erro: Chave de API inválida.');
  426. updateSummaryOverlay('<p>Erro: Chave de API inválida.</p>');
  427. } else {
  428. showErrorNotification(`Erro: Falha ao recuperar o resumo. Status: ${response.status || 'N/A'}`);
  429. updateSummaryOverlay(`<p>Erro: Falha ao recuperar o resumo. Status: ${response.status || 'N/A'}</p>`);
  430. }
  431. },
  432. onerror: function() {
  433. showErrorNotification('Erro: Problema de rede.');
  434. updateSummaryOverlay('<p>Erro: Problema de rede.</p>');
  435. },
  436. onabort: function() {
  437. showErrorNotification('Requisição cancelada.');
  438. updateSummaryOverlay('<p>Requisição cancelada.</p>');
  439. }
  440. });
  441. }
  442.  
  443. })();