Text Summarizer with Gemini API

Summarize selected text using Gemini 2.0 Flash API

目前为 2025-03-09 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Text Summarizer with Gemini API
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.22
  5. // @description Summarize selected text using Gemini 2.0 Flash API
  6. // @author Hà Trọng Nguyễn
  7. // @match *://*/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @connect generativelanguage.googleapis.com
  12. // @homepageURL https://github.com/htrnguyen
  13. // @supportURL https://github.com/htrnguyen/Text-Summarizer-with-Gemini-API/issues
  14. // @icon https://github.com/htrnguyen/User-Scripts/raw/main/Text-Summarizer-with-Gemini-API/text-summarizer-logo.png
  15. // @license MIT
  16. // ==/UserScript==
  17. (function () {
  18. 'use strict';
  19. let lastKeyTime = 0;
  20. let popup = null;
  21. let overlay = null;
  22. let isDragging = false;
  23. let isResizing = false;
  24. let offsetX, offsetY;
  25. let resizeOffsetX, resizeOffsetY;
  26. let initialWidth, initialHeight;
  27.  
  28. // Kiểm tra xem API key đã được lưu chưa
  29. const API_KEY = GM_getValue('geminiApiKey', '');
  30. if (!API_KEY) {
  31. showApiKeyPrompt();
  32. }
  33.  
  34. // ✅ Lắng nghe phím Alt + T (nhấn đúp)
  35. document.addEventListener('keydown', function (e) {
  36. if (e.altKey && e.key === 't') {
  37. const currentTime = new Date().getTime();
  38. if (currentTime - lastKeyTime < 500) {
  39. e.preventDefault();
  40. const selectedText = window.getSelection().toString().trim();
  41. if (selectedText) {
  42. summarizeTextWithGemini(selectedText);
  43. } else {
  44. showPopup('Lỗi', 'Vui lòng chọn văn bản để tóm tắt!');
  45. }
  46. }
  47. lastKeyTime = currentTime;
  48. }
  49. });
  50.  
  51. // ✅ Gửi văn bản đến API Gemini 2.0 Flash
  52. function summarizeTextWithGemini(text) {
  53. showLoader();
  54. const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${API_KEY}`;
  55. const prompt = `Tóm tt ni dung sau đây, đảm bo kết qu có xung dòng và b cc hp lý để d đọc. Kết qu ch nên cha thông tin cn tóm tt, loi b các phn tha như 'dưới đây là tóm tắt'. Định dng tr v là văn bn thông thường, không s dng markdown. Bn có th thêm emoji để làm du chm, s th t hoc gch đầu dòng, nhưng hãy hn chế s dng chúng. Ni dung cn tóm tt là: ${text}`;
  56. const requestBody = {
  57. contents: [{ parts: [{ text: prompt }] }]
  58. };
  59. GM_xmlhttpRequest({
  60. method: "POST",
  61. url: apiUrl,
  62. headers: { "Content-Type": "application/json" },
  63. data: JSON.stringify(requestBody),
  64. onload: function (response) {
  65. hideLoader();
  66. if (!response.responseText) {
  67. showPopup('Lỗi', 'Không có phản hồi từ API. Kiểm tra API Key hoặc thử lại sau.');
  68. return;
  69. }
  70. try {
  71. const result = JSON.parse(response.responseText);
  72. if (result.candidates && result.candidates.length > 0) {
  73. const summary = result.candidates[0]?.content?.parts[0]?.text || 'Không thể tóm tắt!';
  74. showPopup('Tóm tắt', summary);
  75. } else if (result.error) {
  76. handleApiError(result.error);
  77. } else {
  78. showPopup('Lỗi', 'Phản hồi không hợp lệ từ API.');
  79. }
  80. } catch (error) {
  81. showPopup('Lỗi', `Li x lý d liu: ${error.message}<br>Phn hi API: ${response.responseText}`);
  82. }
  83. },
  84. onerror: function (err) {
  85. hideLoader();
  86. showPopup('Lỗi', `Li kết ni API.`);
  87. },
  88. timeout: 10000,
  89. ontimeout: function () {
  90. hideLoader();
  91. showPopup('Lỗi', 'Yêu cầu đến API bị timeout. Vui lòng thử lại sau.');
  92. }
  93. });
  94. }
  95.  
  96. function handleApiError(error) {
  97. if (error.code === 403 && error.message.includes('Method doesn\'t allow unregistered callers')) {
  98. showPopup('Lỗi', 'API key không hợp lệ hoặc chưa được đăng ký. Vui lòng kiểm tra lại API key của bạn.');
  99. } else {
  100. showPopup('Lỗi', `API tr v li: ${error.message}`);
  101. }
  102. }
  103.  
  104. function showPopup(title, content) {
  105. if (popup) closePopup();
  106. // Tạo overlay
  107. overlay = document.createElement('div');
  108. overlay.className = 'overlay';
  109. document.body.appendChild(overlay);
  110. popup = document.createElement('div');
  111. popup.className = 'popup';
  112. popup.innerHTML = `
  113. <div class="popup-header" draggable="true">
  114. <h2>${title}</h2>
  115. <div class="header-actions">
  116. <button class="settings-btn" title="Cài đặt">
  117. <svg class="cog-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
  118. <path d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zm-.75-6.25l-.016-.014a.75.75 0 00-.982.114l-2.25 2.25a.75.75 0 001.06 1.06l2.25-2.25a.75.75 0 00.114-.982zM12 4.5a7.5 7.5 0 110 15 7.5 7.5 0 010-15zm0 1a6.5 6.5 0 100 13 6.5 6.5 0 000-13z"/>
  119. </svg>
  120. </button>
  121. <button class="close-btn">×</button>
  122. </div>
  123. </div>
  124. <div class="popup-content" id="popupContent">${content}</div>
  125. <div class="resize-handle"></div>
  126. `;
  127. // In ra HTML trước khi thêm vào body
  128. console.log('HTML trước khi thêm vào body:', popup.innerHTML);
  129. document.body.appendChild(popup);
  130. document.querySelector('.close-btn').onclick = closePopup;
  131. document.querySelector('.settings-btn').onclick = showApiKeyPrompt;
  132. document.addEventListener('keydown', handleEscKey);
  133. // Thêm sự kiện drag
  134. const header = document.querySelector('.popup-header');
  135. header.addEventListener('mousedown', startDrag);
  136. document.addEventListener('mousemove', drag);
  137. document.addEventListener('mouseup', stopDrag);
  138. // Thêm sự kiện resize
  139. const resizeHandle = document.querySelector('.resize-handle');
  140. resizeHandle.addEventListener('mousedown', startResize);
  141. document.addEventListener('mousemove', resize);
  142. document.addEventListener('mouseup', stopResize);
  143. // Tự động đóng popup khi nhấp bên ngoài
  144. popup.onclick = function (event) {
  145. if (event.target === popup) {
  146. closePopup();
  147. }
  148. };
  149. // In ra console khi hiển thị nút cài đặt
  150. console.log('Nút cài đặt đã được hiển thị');
  151. }
  152.  
  153. function startDrag(e) {
  154. isDragging = true;
  155. offsetX = e.clientX - popup.offsetLeft;
  156. offsetY = e.clientY - popup.offsetTop;
  157. }
  158.  
  159. function drag(e) {
  160. if (isDragging) {
  161. popup.style.left = `${e.clientX - offsetX}px`;
  162. popup.style.top = `${e.clientY - offsetY}px`;
  163. }
  164. }
  165.  
  166. function stopDrag() {
  167. isDragging = false;
  168. }
  169.  
  170. function startResize(e) {
  171. isResizing = true;
  172. initialWidth = popup.offsetWidth;
  173. initialHeight = popup.offsetHeight;
  174. resizeOffsetX = e.clientX - popup.offsetLeft;
  175. resizeOffsetY = e.clientY - popup.offsetTop;
  176. }
  177.  
  178. function resize(e) {
  179. if (isResizing) {
  180. const newWidth = initialWidth + (e.clientX - (popup.offsetLeft + resizeOffsetX));
  181. const newHeight = initialHeight + (e.clientY - (popup.offsetTop + resizeOffsetY));
  182. popup.style.width = `${newWidth}px`;
  183. popup.style.height = `${newHeight}px`;
  184. const content = document.querySelector('.popup-content');
  185. content.style.maxHeight = `${newHeight - 80}px`; // 80px là chiều cao của header và resize handle
  186. }
  187. }
  188.  
  189. function stopResize() {
  190. isResizing = false;
  191. }
  192.  
  193. function showApiKeyPrompt() {
  194. if (popup) closePopup();
  195. // Tạo overlay
  196. overlay = document.createElement('div');
  197. overlay.className = 'overlay';
  198. document.body.appendChild(overlay);
  199. // Tạo popup nhập API key
  200. popup = document.createElement('div');
  201. popup.className = 'popup';
  202. popup.innerHTML = `
  203. <div class="popup-header" draggable="true">
  204. <h2>Nhp API Key</h2>
  205. <button class="close-btn">×</button>
  206. </div>
  207. <div class="popup-content">
  208. <p>Vui lòng nhp API key ca bn để s dng dch v tóm tt văn bn.</p>
  209. <div class="api-key-input-container">
  210. <input type="text" id="apiKeyInput" placeholder="Nhập API key tại đây..." value="${API_KEY}" />
  211. </div>
  212. <div class="button-container">
  213. <button class="save-btn">Lưu</button>
  214. </div>
  215. </div>
  216. <div class="resize-handle"></div>
  217. `;
  218. document.body.appendChild(popup);
  219. document.querySelector('.close-btn').onclick = closePopup;
  220. document.querySelector('.save-btn').onclick = saveApiKey;
  221. document.getElementById('apiKeyInput').focus();
  222. document.addEventListener('keydown', handleEscKey);
  223. // Thêm sự kiện drag
  224. const header = document.querySelector('.popup-header');
  225. header.addEventListener('mousedown', startDrag);
  226. document.addEventListener('mousemove', drag);
  227. document.addEventListener('mouseup', stopDrag);
  228. // Thêm sự kiện resize
  229. const resizeHandle = document.querySelector('.resize-handle');
  230. resizeHandle.addEventListener('mousedown', startResize);
  231. document.addEventListener('mousemove', resize);
  232. document.addEventListener('mouseup', stopResize);
  233. }
  234.  
  235. function saveApiKey() {
  236. const apiKey = document.getElementById('apiKeyInput').value.trim();
  237. if (apiKey) {
  238. GM_setValue('geminiApiKey', apiKey);
  239. closePopup();
  240. showPopup('Thông Báo', 'API Key đã được lưu thành công!');
  241. } else {
  242. showPopup('Lỗi', 'API key không được để trống!');
  243. }
  244. }
  245.  
  246. function showLoader() {
  247. const loader = document.createElement('div');
  248. loader.className = 'loader';
  249. loader.innerHTML = '<div class="spinner"></div>';
  250. document.body.appendChild(loader);
  251. }
  252.  
  253. function hideLoader() {
  254. const loader = document.querySelector('.loader');
  255. if (loader) loader.remove();
  256. }
  257.  
  258. function closePopup() {
  259. if (popup) {
  260. popup.remove();
  261. popup = null;
  262. }
  263. if (overlay) {
  264. overlay.remove();
  265. overlay = null;
  266. }
  267. document.removeEventListener('keydown', handleEscKey);
  268. }
  269.  
  270. function handleEscKey(e) {
  271. if (e.key === 'Escape') {
  272. closePopup();
  273. }
  274. }
  275.  
  276. // CSS Styles
  277. const style = document.createElement('style');
  278. style.innerHTML = `
  279. .overlay {
  280. position: fixed;
  281. top: 0;
  282. left: 0;
  283. width: 100%;
  284. height: 100%;
  285. background: rgba(0, 0, 0, 0.5);
  286. z-index: 9998;
  287. }
  288. .popup {
  289. position: fixed;
  290. top: 50%;
  291. left: 50%;
  292. transform: translate(-50%, -50%);
  293. width: 500px; /* Mở rộng chiều rộng */
  294. min-width: 400px;
  295. min-height: 250px; /* Mở rộng chiều cao */
  296. background: #fff;
  297. border-radius: 10px;
  298. box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
  299. z-index: 9999;
  300. font-family: 'Roboto', sans-serif;
  301. overflow: hidden;
  302. }
  303. .popup-header {
  304. background: #4A90E2;
  305. color: #fff;
  306. padding: 15px;
  307. display: flex;
  308. justify-content: space-between;
  309. align-items: center;
  310. cursor: move;
  311. border-top-left-radius: 10px;
  312. border-top-right-radius: 10px;
  313. }
  314. .popup-header h2 {
  315. margin: 0;
  316. font-size: 18px;
  317. font-weight: bold;
  318. text-align: center;
  319. }
  320. .header-actions {
  321. display: flex;
  322. gap: 10px;
  323. }
  324. .header-actions button {
  325. background: none;
  326. border: none;
  327. color: #fff;
  328. font-size: 20px;
  329. cursor: pointer;
  330. transition: opacity 0.3s;
  331. }
  332. .header-actions button:hover {
  333. opacity: 0.7;
  334. }
  335. .popup-content {
  336. padding: 20px;
  337. font-size: 14px;
  338. color: #333;
  339. line-height: 1.6;
  340. overflow-y: auto;
  341. white-space: pre-wrap; /* Đảm bảo xuống dòng */
  342. }
  343. .popup-content p {
  344. text-align: center;
  345. }
  346. .api-key-input-container {
  347. display: flex;
  348. justify-content: center;
  349. margin-bottom: 15px;
  350. }
  351. .popup-content input[type="text"] {
  352. width: calc(100% - 40px);
  353. max-width: 400px;
  354. padding: 10px;
  355. border: 1px solid #ccc;
  356. border-radius: 5px;
  357. transition: border-color 0.3s;
  358. }
  359. .popup-content input[type="text"]:focus {
  360. border-color: #4A90E2;
  361. }
  362. .button-container {
  363. display: flex;
  364. justify-content: center;
  365. }
  366. .popup-content button {
  367. padding: 10px 20px;
  368. background: #4A90E2;
  369. color: #fff;
  370. border: none;
  371. border-radius: 5px;
  372. cursor: pointer;
  373. font-size: 14px;
  374. transition: background 0.3s;
  375. }
  376. .popup-content button:hover {
  377. background: #3A7ACB;
  378. }
  379. .close-btn {
  380. background: none;
  381. border: none;
  382. color: #fff;
  383. font-size: 20px;
  384. cursor: pointer;
  385. transition: opacity 0.3s;
  386. }
  387. .close-btn:hover {
  388. opacity: 0.7;
  389. }
  390. .resize-handle {
  391. position: absolute;
  392. bottom: 0;
  393. right: 0;
  394. width: 20px;
  395. height: 20px;
  396. background: #4A90E2;
  397. cursor: se-resize;
  398. border-bottom-right-radius: 10px;
  399. transition: background 0.3s;
  400. }
  401. .resize-handle:hover {
  402. background: #3A7ACB;
  403. }
  404. .loader {
  405. position: fixed;
  406. top: 0;
  407. left: 0;
  408. width: 100%;
  409. height: 100%;
  410. background: rgba(0, 0, 0, 0.5);
  411. display: flex;
  412. justify-content: center;
  413. align-items: center;
  414. z-index: 9999;
  415. }
  416. .spinner {
  417. border: 4px solid rgba(255, 255, 255, 0.3);
  418. border-top: 4px solid #fff;
  419. border-radius: 50%;
  420. width: 40px;
  421. height: 40px;
  422. animation: spin 1s linear infinite;
  423. }
  424. @keyframes spin {
  425. 0% { transform: rotate(0deg); }
  426. 100% { transform: rotate(360deg); }
  427. }
  428. /* SVG Icon */
  429. .cog-icon {
  430. width: 1em;
  431. height: 1em;
  432. vertical-align: middle;
  433. }
  434. /* Tooltip */
  435. .tooltip {
  436. position: relative;
  437. display: inline-block;
  438. cursor: pointer;
  439. }
  440. .tooltip .tooltiptext {
  441. visibility: hidden;
  442. width: 120px;
  443. background-color: black;
  444. color: #fff;
  445. text-align: center;
  446. border-radius: 5px;
  447. padding: 5px 0;
  448. position: absolute;
  449. z-index: 1;
  450. bottom: 125%; /* Position the tooltip above the text */
  451. left: 50%;
  452. margin-left: -60px;
  453. opacity: 0;
  454. transition: opacity 0.3s;
  455. }
  456. .tooltip:hover .tooltiptext {
  457. visibility: visible;
  458. opacity: 1;
  459. }
  460. `;
  461. document.head.appendChild(style);
  462. })();