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 2.1
  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. // @grant GM_registerMenuCommand
  12. // @connect generativelanguage.googleapis.com
  13. // @homepageURL https://github.com/htrnguyen
  14. // @supportURL https://github.com/htrnguyen/Text-Summarizer-with-Gemini-API/issues
  15. // @icon https://github.com/htrnguyen/User-Scripts/raw/main/Text-Summarizer-with-Gemini-API/text-summarizer-logo.png
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19. ;(function () {
  20. 'use strict'
  21.  
  22. // Khai báo biến toàn cục
  23. let API_KEY = GM_getValue('geminiApiKey', '') || ''
  24. let shortcutKey = GM_getValue('shortcutKey', 't')
  25. let modifierKeys = JSON.parse(GM_getValue('modifierKeys', '["Alt"]')) || [
  26. 'Alt',
  27. ]
  28. let currentPopup = null
  29. let isDragging = false
  30. let isResizing = false
  31. let offsetX, offsetY, resizeOffsetX, initialWidth
  32.  
  33. // Hàm khởi tạo
  34. function initialize() {
  35. if (!API_KEY) {
  36. showPopup('Cài đặt', getSettingsContent())
  37. }
  38. setupEventListeners()
  39. }
  40.  
  41. // Thiết lập các sự kiện
  42. function setupEventListeners() {
  43. document.addEventListener('keydown', handleKeydown)
  44. if (typeof GM_registerMenuCommand !== 'undefined') {
  45. GM_registerMenuCommand('Cài đặt Text Summarizer', () =>
  46. showPopup('Cài đặt', getSettingsContent())
  47. )
  48. }
  49. }
  50.  
  51. // Kiểm tra phím tắt
  52. function checkShortcut(e) {
  53. const key = e.key.toLowerCase()
  54. const modifiers = modifierKeys.map((mod) => mod.toLowerCase())
  55. const currentModifiers = []
  56. if (e.altKey) currentModifiers.push('alt')
  57. if (e.ctrlKey) currentModifiers.push('ctrl')
  58. if (e.shiftKey) currentModifiers.push('shift')
  59. return (
  60. key === shortcutKey &&
  61. currentModifiers.sort().join(',') === modifiers.sort().join(',')
  62. )
  63. }
  64.  
  65. // Xử lý phím tắt và ESC
  66. function handleKeydown(e) {
  67. if (checkShortcut(e)) {
  68. e.preventDefault()
  69. const selectedText = window.getSelection().toString().trim()
  70. if (selectedText) {
  71. summarizeText(selectedText)
  72. } else {
  73. showPopup(
  74. 'Lỗi',
  75. 'Vui lòng chọn một đoạn văn bản để tóm tắt nhé!'
  76. )
  77. }
  78. } else if (e.key === 'Escape' && currentPopup) {
  79. closePopup()
  80. }
  81. }
  82.  
  83. // Gửi yêu cầu đến Gemini API
  84. function summarizeText(text) {
  85. showLoader()
  86. GM_xmlhttpRequest({
  87. method: 'POST',
  88. url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${API_KEY}`,
  89. headers: {'Content-Type': 'application/json'},
  90. data: JSON.stringify({
  91. contents: [
  92. {
  93. parts: [
  94. {
  95. text: `Tóm tt ni dung sau đây, đảm bo gi li các ý chính và chi tiết quan trng, tránh lược b quá nhiu. Kết qu cn có xung dòng và b cc hp lý để d đọc. Ch bao gm thông tin cn tóm tt, không thêm phn tha như 'dưới đây là tóm tắt' hoc li dn. Định dng tr v là văn bn thông thường, không s dng markdown. Bn có th thêm emoji (🌟, ➡️, 1️⃣) để làm du chm, s th t hoc gch đầu dòng, nhưng hãy hn chế và s dng mt cách tinh tế. Ni dung cn tóm tt là: ${text}`,
  96. },
  97. ],
  98. },
  99. ],
  100. }),
  101. onload: function (response) {
  102. hideLoader()
  103. const data = JSON.parse(response.responseText)
  104. if (data.candidates && data.candidates.length > 0) {
  105. const summary =
  106. data.candidates[0].content.parts[0].text ||
  107. 'Không thể tóm tắt được nội dung này!'
  108. showPopup('Tóm tắt', summary)
  109. } else if (data.error) {
  110. showPopup('Lỗi', `Có li t API: ${data.error.message}`)
  111. } else {
  112. showPopup(
  113. 'Lỗi',
  114. 'Phản hồi từ API không hợp lệ. Hãy thử lại!'
  115. )
  116. }
  117. },
  118. onerror: function (error) {
  119. hideLoader()
  120. showPopup(
  121. 'Lỗi',
  122. `Li kết ni: ${error.message}. Kim tra mng nhé!`
  123. )
  124. },
  125. })
  126. }
  127.  
  128. // Hiển thị popup duy nhất
  129. function showPopup(title, content) {
  130. closePopup() // Đóng popup cũ trước khi mở mới
  131.  
  132. currentPopup = document.createElement('div')
  133. currentPopup.className = 'summarizer-popup'
  134. currentPopup.innerHTML = `
  135. <div class="popup-header">
  136. <h2>${title}</h2>
  137. <div class="header-actions">
  138. <button class="close-btn">×</button>
  139. </div>
  140. </div>
  141. <div class="${
  142. title === 'Tóm tắt' ? 'popup-content-summary' : 'popup-content'
  143. }">${content}</div>
  144. <div class="resize-handle"></div>
  145. `
  146. document.body.appendChild(currentPopup)
  147.  
  148. currentPopup.style.opacity = '0'
  149. currentPopup.style.transform = 'translate(-50%, -50%) scale(0.9)'
  150. setTimeout(() => {
  151. currentPopup.style.opacity = '1'
  152. currentPopup.style.transform = 'translate(-50%, -50%) scale(1)'
  153. }, 10)
  154.  
  155. currentPopup
  156. .querySelector('.close-btn')
  157. .addEventListener('click', closePopup)
  158.  
  159. if (title === 'Cài đặt') {
  160. const saveBtn = currentPopup.querySelector('.save-btn')
  161. if (saveBtn) saveBtn.addEventListener('click', saveSettings)
  162. }
  163.  
  164. const header = currentPopup.querySelector('.popup-header')
  165. header.addEventListener('mousedown', startDrag)
  166. document.addEventListener('mousemove', drag)
  167. document.addEventListener('mouseup', stopDrag)
  168.  
  169. const resizeHandle = currentPopup.querySelector('.resize-handle')
  170. resizeHandle.addEventListener('mousedown', startResize)
  171. document.addEventListener('mousemove', resize)
  172. document.addEventListener('mouseup', stopResize)
  173.  
  174. document.body.style.pointerEvents = 'none'
  175. currentPopup.style.pointerEvents = 'auto'
  176. }
  177.  
  178. // Lấy nội dung cài đặt
  179. function getSettingsContent() {
  180. return `
  181. <div class="settings-container">
  182. <div class="settings-item">
  183. <label>API Key:</label>
  184. <input type="text" id="apiKeyInput" placeholder="Dán API key vào đây" value="${API_KEY}" />
  185. </div>
  186. <div class="settings-item instruction">
  187. <span>Ly key ti: <a href="https://aistudio.google.com/apikey" target="_blank">Google AI Studio</a></span>
  188. </div>
  189. <div class="settings-item shortcut-section">
  190. <label>Phím tt:</label>
  191. <div class="shortcut-controls">
  192. <label><input type="radio" name="modifier" value="Alt" ${
  193. modifierKeys.includes('Alt') ? 'checked' : ''
  194. }> Alt</label>
  195. <label><input type="radio" name="modifier" value="Ctrl" ${
  196. modifierKeys.includes('Ctrl') ? 'checked' : ''
  197. }> Ctrl</label>
  198. <label><input type="radio" name="modifier" value="Shift" ${
  199. modifierKeys.includes('Shift') ? 'checked' : ''
  200. }> Shift</label>
  201. <input type="text" id="shortcutKey" maxlength="1" placeholder="T" value="${shortcutKey.toUpperCase()}" />
  202. </div>
  203. </div>
  204. <div class="settings-item button-container">
  205. <button class="save-btn">
  206. <svg class="save-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
  207. <path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
  208. </svg>
  209. Lưu
  210. </button>
  211. </div>
  212. </div>
  213. `
  214. }
  215.  
  216. // Lưu cài đặt và làm mới trang
  217. function saveSettings() {
  218. const apiKey = document.getElementById('apiKeyInput').value.trim()
  219. const selectedModifier = document.querySelector(
  220. 'input[name="modifier"]:checked'
  221. )
  222. shortcutKey = document
  223. .getElementById('shortcutKey')
  224. .value.trim()
  225. .toLowerCase()
  226.  
  227. if (!apiKey) {
  228. showPopup('Lỗi', 'Bạn chưa nhập API key! Hãy nhập để tiếp tục.')
  229. return
  230. }
  231. if (!shortcutKey) {
  232. showPopup('Lỗi', 'Bạn chưa nhập phím tắt! Hãy nhập một ký tự.')
  233. return
  234. }
  235. if (!selectedModifier) {
  236. showPopup(
  237. 'Lỗi',
  238. 'Bạn chưa chọn phím bổ trợ! Chọn Alt, Ctrl hoặc Shift.'
  239. )
  240. return
  241. }
  242.  
  243. modifierKeys = [selectedModifier.value]
  244. GM_setValue('geminiApiKey', apiKey)
  245. GM_setValue('shortcutKey', shortcutKey)
  246. GM_setValue('modifierKeys', JSON.stringify(modifierKeys))
  247. API_KEY = apiKey
  248. closePopup()
  249. showPopup(
  250. 'Thành công',
  251. 'Cài đặt đã được lưu! Trang sẽ làm mới sau 1 giây.'
  252. )
  253. setTimeout(() => location.reload(), 1000) // Làm mới trang để tránh bị kẹt
  254. }
  255.  
  256. // Xử lý kéo popup
  257. function startDrag(e) {
  258. isDragging = true
  259. offsetX = e.clientX - currentPopup.offsetLeft
  260. offsetY = e.clientY - currentPopup.offsetTop
  261. }
  262.  
  263. function drag(e) {
  264. if (isDragging) {
  265. e.preventDefault()
  266. currentPopup.style.left = `${e.clientX - offsetX}px`
  267. currentPopup.style.top = `${e.clientY - offsetY}px`
  268. }
  269. }
  270.  
  271. function stopDrag() {
  272. isDragging = false
  273. }
  274.  
  275. // Xử lý thay đổi kích thước (chỉ chiều ngang)
  276. function startResize(e) {
  277. isResizing = true
  278. initialWidth = currentPopup.offsetWidth
  279. resizeOffsetX = e.clientX - currentPopup.offsetLeft
  280. }
  281.  
  282. function resize(e) {
  283. if (isResizing) {
  284. const newWidth =
  285. initialWidth +
  286. (e.clientX - (currentPopup.offsetLeft + resizeOffsetX))
  287. currentPopup.style.width = `${Math.max(newWidth, 400)}px`
  288. }
  289. }
  290.  
  291. function stopResize() {
  292. isResizing = false
  293. }
  294.  
  295. // Loader
  296. function showLoader() {
  297. const loader = document.createElement('div')
  298. loader.className = 'summarizer-loader'
  299. loader.innerHTML = '<div class="spinner"></div>'
  300. document.body.appendChild(loader)
  301. }
  302.  
  303. function hideLoader() {
  304. const loader = document.querySelector('.summarizer-loader')
  305. if (loader) loader.remove()
  306. }
  307.  
  308. // Đóng popup
  309. function closePopup() {
  310. if (currentPopup) {
  311. currentPopup.style.opacity = '1'
  312. currentPopup.style.transform = 'translate(-50%, -50%) scale(1)'
  313. setTimeout(() => {
  314. currentPopup.style.opacity = '0'
  315. currentPopup.style.transform =
  316. 'translate(-50%, -50%) scale(0.9)'
  317. setTimeout(() => {
  318. currentPopup.remove()
  319. currentPopup = null
  320. document.body.style.pointerEvents = 'auto'
  321. document.removeEventListener('mousemove', drag)
  322. document.removeEventListener('mouseup', stopDrag)
  323. }, 200)
  324. }, 10)
  325. }
  326. }
  327.  
  328. // CSS
  329. const style = document.createElement('style')
  330. style.innerHTML = `
  331. .summarizer-popup {
  332. position: fixed;
  333. top: 50%;
  334. left: 50%;
  335. transform: translate(-50%, -50%);
  336. width: 500px;
  337. min-width: 400px;
  338. height: 400px;
  339. background: #ffffff;
  340. border-radius: 12px;
  341. box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
  342. z-index: 9999;
  343. font-family: 'Roboto', sans-serif;
  344. overflow: hidden;
  345. transition: opacity 0.2s ease, transform 0.2s ease;
  346. display: flex;
  347. flex-direction: column;
  348. }
  349. .popup-header {
  350. background: linear-gradient(135deg, #4A90E2, #357ABD);
  351. color: #fff;
  352. padding: 15px 20px;
  353. display: flex;
  354. justify-content: space-between;
  355. align-items: center;
  356. cursor: move;
  357. border-top-left-radius: 12px;
  358. border-top-right-radius: 12px;
  359. flex-shrink: 0;
  360. }
  361. .popup-header h2 {
  362. margin: 0;
  363. font-size: 20px;
  364. font-weight: 600;
  365. text-align: center;
  366. line-height: 1.0;
  367. }
  368. .header-actions {
  369. display: flex;
  370. gap: 12px;
  371. }
  372. .header-actions button {
  373. background: none;
  374. border: none;
  375. color: #fff;
  376. font-size: 22px;
  377. cursor: pointer;
  378. transition: transform 0.2s ease, opacity 0.2s ease;
  379. }
  380. .header-actions button:hover {
  381. transform: scale(1.1);
  382. opacity: 0.9;
  383. }
  384. .popup-content {
  385. padding: 15px;
  386. font-size: 15px;
  387. color: #444;
  388. line-height: 1.2;
  389. flex-grow: 1;
  390. display: flex;
  391. flex-direction: column;
  392. justify-content: space-between;
  393. }
  394. .popup-content-summary {
  395. padding: 15px;
  396. font-size: 15px;
  397. color: #444;
  398. line-height: 1.6;
  399. overflow-y: auto;
  400. white-space: pre-wrap;
  401. flex-grow: 1;
  402. }
  403. .settings-container {
  404. display: flex;
  405. flex-direction: column;
  406. gap: 10px;
  407. }
  408. .settings-item {
  409. display: flex;
  410. flex-direction: column;
  411. align-items: center;
  412. gap: 5px;
  413. }
  414. .settings-item label {
  415. font-weight: 600;
  416. color: #333;
  417. line-height: 1.2;
  418. }
  419. .settings-item input[type="text"] {
  420. width: 80%;
  421. max-width: 300px;
  422. padding: 8px;
  423. border: 1px solid #ddd;
  424. border-radius: 6px;
  425. font-size: 14px;
  426. text-align: center;
  427. background: #f9f9f9;
  428. transition: border-color 0.2s ease;
  429. }
  430. .settings-item input[type="text"]:focus {
  431. border-color: #4A90E2;
  432. outline: none;
  433. }
  434. .instruction {
  435. font-size: 13px;
  436. color: #666;
  437. line-height: 1.2;
  438. }
  439. .instruction a {
  440. color: #4A90E2;
  441. text-decoration: none;
  442. transition: color 0.2s ease;
  443. }
  444. .instruction a:hover {
  445. color: #357ABD;
  446. text-decoration: underline;
  447. }
  448. .shortcut-section {
  449. flex-direction: row;
  450. justify-content: center;
  451. align-items: center;
  452. gap: 10px;
  453. }
  454. .shortcut-controls {
  455. display: flex;
  456. align-items: center;
  457. gap: 8px;
  458. }
  459. .shortcut-controls label {
  460. display: flex;
  461. align-items: center;
  462. gap: 3px;
  463. font-size: 14px;
  464. font-weight: 400;
  465. color: #444;
  466. }
  467. .shortcut-controls input[type="radio"] {
  468. margin: 0;
  469. }
  470. .shortcut-controls input[type="text"] {
  471. width: 40px;
  472. padding: 6px;
  473. border: 1px solid #ddd;
  474. border-radius: 6px;
  475. font-size: 14px;
  476. text-align: center;
  477. background: #f9f9f9;
  478. transition: border-color 0.2s ease;
  479. }
  480. .shortcut-controls input[type="text"]:focus {
  481. border-color: #4A90E2;
  482. outline: none;
  483. }
  484. .button-container {
  485. margin-top: 10px;
  486. }
  487. .save-btn {
  488. padding: 8px 20px;
  489. background: linear-gradient(135deg, #4A90E2, #357ABD);
  490. color: #fff;
  491. border: none;
  492. border-radius: 6px;
  493. cursor: pointer;
  494. font-size: 15px;
  495. font-weight: 500;
  496. display: flex;
  497. align-items: center;
  498. gap: 6px;
  499. transition: transform 0.2s ease, box-shadow 0.2s ease;
  500. }
  501. .save-btn:hover {
  502. transform: translateY(-2px);
  503. box-shadow: 0 5px 15px rgba(74, 144, 226, 0.4);
  504. }
  505. .save-icon {
  506. width: 16px;
  507. height: 16px;
  508. }
  509. .resize-handle {
  510. position: absolute;
  511. bottom: 0;
  512. right: 0;
  513. width: 20px;
  514. height: 20px;
  515. background: #4A90E2;
  516. cursor: se-resize;
  517. border-bottom-right-radius: 12px;
  518. transition: background 0.2s ease;
  519. }
  520. .resize-handle:hover {
  521. background: #357ABD;
  522. }
  523. .summarizer-loader {
  524. position: fixed;
  525. top: 0;
  526. left: 0;
  527. width: 100%;
  528. height: 100%;
  529. background: rgba(0, 0, 0, 0.6);
  530. display: flex;
  531. justify-content: center;
  532. align-items: center;
  533. z-index: 10000;
  534. }
  535. .spinner {
  536. border: 5px solid rgba(255, 255, 255, 0.3);
  537. border-top: 5px solid #4A90E2;
  538. border-radius: 50%;
  539. width: 50px;
  540. height: 50px;
  541. animation: spin 1s linear infinite;
  542. }
  543. @keyframes spin {
  544. 0% { transform: rotate(0deg); }
  545. 100% { transform: rotate(360deg); }
  546. }
  547. `
  548. document.head.appendChild(style)
  549.  
  550. // Thêm font Roboto
  551. const fontLink = document.createElement('link')
  552. fontLink.href =
  553. 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600&display=swap'
  554. fontLink.rel = 'stylesheet'
  555. document.head.appendChild(fontLink)
  556.  
  557. // Khởi chạy script
  558. initialize()
  559. })()