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