Text Summarizer with Gemini API

Summarize selected text using Gemini 2.0 Flash API with enhanced features

  1. // ==UserScript==
  2. // @name Text Summarizer with Gemini API
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.2
  5. // @description Summarize selected text using Gemini 2.0 Flash API with enhanced features
  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 currentRequest = null
  30. let isDragging = false
  31. let isResizing = false
  32. let offsetX, offsetY, resizeOffsetX, initialWidth
  33. let isProcessing = false // Biến khóa để ngăn spam
  34.  
  35. // Hàm khởi tạo
  36. function initialize() {
  37. if (!API_KEY) {
  38. showPopup('Cài đặt', getSettingsContent())
  39. }
  40. setupEventListeners()
  41. }
  42.  
  43. // Thiết lập các sự kiện
  44. function setupEventListeners() {
  45. document.addEventListener('keydown', handleKeydown)
  46. if (typeof GM_registerMenuCommand !== 'undefined') {
  47. GM_registerMenuCommand('Cài đặt Text Summarizer', () =>
  48. showPopup('Cài đặt', getSettingsContent())
  49. )
  50. GM_registerMenuCommand('Lịch sử tóm tắt', () => {
  51. const history = JSON.parse(GM_getValue('summaryHistory', '[]'))
  52. if (history.length === 0) {
  53. showPopup('Lịch sử tóm tắt', 'Chưa có tóm tắt nào!')
  54. return
  55. }
  56. const historyContent = history
  57. .map(
  58. (item, index) => `
  59. <div class="history-item">
  60. <strong>${index + 1}. ${item.timestamp}</strong><br>
  61. <strong>Văn bn gc:</strong> ${item.text}<br>
  62. <strong>Tóm tt:</strong><br>${item.summary}<br><br>
  63. </div>
  64. `
  65. )
  66. .join('')
  67. showPopup('Lịch sử tóm tắt', historyContent)
  68. })
  69. }
  70. }
  71.  
  72. // Kiểm tra phím tắt
  73. function checkShortcut(e) {
  74. const key = e.key.toLowerCase()
  75. const modifiers = modifierKeys.map((mod) => mod.toLowerCase())
  76. const currentModifiers = []
  77. if (e.altKey) currentModifiers.push('alt')
  78. if (e.ctrlKey) currentModifiers.push('ctrl')
  79. if (e.shiftKey) currentModifiers.push('shift')
  80. return (
  81. key === shortcutKey &&
  82. currentModifiers.sort().join(',') === modifiers.sort().join(',')
  83. )
  84. }
  85.  
  86. // Xử lý phím tắt và ESC
  87. function handleKeydown(e) {
  88. if (checkShortcut(e)) {
  89. e.preventDefault()
  90. // Ngăn spam nếu đang xử lý
  91. if (isProcessing) return
  92. isProcessing = true
  93. const selectedText = window.getSelection().toString().trim()
  94. if (selectedText) {
  95. summarizeText(selectedText)
  96. } else {
  97. showPopup(
  98. 'Lỗi',
  99. 'Vui lòng chọn một đoạn văn bản để tóm tắt nhé!',
  100. 2000
  101. )
  102. }
  103. } else if (e.key === 'Escape' && currentPopup) {
  104. closeAllPopups(true) // Đóng thủ công với animation
  105. }
  106. }
  107.  
  108. // Gửi yêu cầu đến Gemini API
  109. function summarizeText(text) {
  110. const maxLength = 10000
  111. if (text.length > maxLength) {
  112. showPopup(
  113. 'Lỗi',
  114. `Văn bn quá dài (${text.length} ký tự). Vui lòng chn đon văn dưới ${maxLength} ký tự!`,
  115. 2000
  116. )
  117. return
  118. }
  119. closeAllPopups() // Xóa ngay không animation
  120. showLoader()
  121. if (currentRequest) {
  122. currentRequest.abort()
  123. }
  124. const prompt = `Tóm tt ni dung sau đây mt cách chi tiết và đầy đủ, đảm bo gi li tt c ý chính và chi tiết quan trng mà không lược b bt k thông tin nào. Kết qu cn được trình bày vi xung dòng và b cc hp lý để d đọc, rõ ràng. 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 s dng các biu tượng emoji để 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}`
  125. currentRequest = GM_xmlhttpRequest({
  126. method: 'POST',
  127. url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${API_KEY}`,
  128. headers: {'Content-Type': 'application/json'},
  129. data: JSON.stringify({
  130. contents: [{parts: [{text: prompt}]}],
  131. }),
  132. timeout: 10000,
  133. onload: function (response) {
  134. hideLoader()
  135. const data = JSON.parse(response.responseText)
  136. if (data.candidates && data.candidates.length > 0) {
  137. const summary =
  138. data.candidates[0].content.parts[0].text ||
  139. 'Không thể tóm tắt được nội dung này!'
  140. let history = JSON.parse(
  141. GM_getValue('summaryHistory', '[]')
  142. )
  143. history.unshift({
  144. text: text.substring(0, 50) + '...',
  145. summary,
  146. timestamp: new Date().toLocaleString(),
  147. })
  148. if (history.length > 5) history.pop()
  149. GM_setValue('summaryHistory', JSON.stringify(history))
  150. showPopup('Tóm tắt', summary)
  151. } else if (data.error) {
  152. showPopup(
  153. 'Lỗi',
  154. `Có li t API: ${data.error.message}`,
  155. 5000
  156. )
  157. } else {
  158. showPopup(
  159. 'Lỗi',
  160. 'Phản hồi từ API không hợp lệ. Hãy thử lại!',
  161. 5000
  162. )
  163. }
  164. currentRequest = null
  165. isProcessing = false // Mở khóa sau khi hoàn thành
  166. },
  167. onerror: function (error) {
  168. hideLoader()
  169. showPopup(
  170. 'Lỗi',
  171. `Li kết ni: ${
  172. error.statusText || 'Không xác định'
  173. }. Kim tra mng hoc API key!`,
  174. 5000
  175. )
  176. currentRequest = null
  177. isProcessing = false
  178. },
  179. ontimeout: function () {
  180. hideLoader()
  181. showPopup(
  182. 'Lỗi',
  183. 'Yêu cầu timeout. Vui lòng kiểm tra kết nối hoặc thử lại!',
  184. 5000
  185. )
  186. currentRequest = null
  187. isProcessing = false
  188. },
  189. })
  190. }
  191.  
  192. // Hiển thị popup duy nhất
  193. function showPopup(title, content, autoClose = 0) {
  194. // Xóa popup cũ ngay lập tức
  195. closeAllPopups()
  196. currentPopup = document.createElement('div')
  197. currentPopup.className = 'summarizer-popup'
  198. currentPopup.innerHTML = `
  199. <div class="popup-header">
  200. <h2>${title}</h2>
  201. <div class="header-actions">
  202. ${
  203. title === 'Tóm tắt'
  204. ? '<button class="copy-btn" title="Sao chép">📋</button>'
  205. : ''
  206. }
  207. <button class="close-btn">×</button>
  208. </div>
  209. </div>
  210. <div class="${
  211. title === 'Tóm tắt' ? 'popup-content-summary' : 'popup-content'
  212. }">${content}</div>
  213. ${title === 'Tóm tắt' ? '' : '<div class="resize-handle"></div>'}
  214. `
  215. document.body.appendChild(currentPopup)
  216.  
  217. // Hiệu ứng mở
  218. currentPopup.style.opacity = '0'
  219. currentPopup.style.transform = 'translate(-50%, -50%) scale(0.95)'
  220. requestAnimationFrame(() => {
  221. currentPopup.style.transition =
  222. 'opacity 0.15s ease-out, transform 0.15s ease-out'
  223. currentPopup.style.opacity = '1'
  224. currentPopup.style.transform = 'translate(-50%, -50%) scale(1)'
  225. })
  226.  
  227. currentPopup
  228. .querySelector('.close-btn')
  229. .addEventListener('click', () => closeAllPopups(true))
  230.  
  231. if (title === 'Tóm tắt') {
  232. const copyBtn = currentPopup.querySelector('.copy-btn')
  233. copyBtn.addEventListener('click', () => {
  234. navigator.clipboard
  235. .writeText(content)
  236. .then(() => {
  237. copyBtn.title = 'Đã sao chép!'
  238. setTimeout(() => (copyBtn.title = 'Sao chép'), 2000)
  239. })
  240. .catch((err) => {
  241. showPopup('Lỗi', 'Không thể sao chép: ' + err.message)
  242. })
  243. })
  244. }
  245.  
  246. if (title === 'Cài đặt') {
  247. const saveBtn = currentPopup.querySelector('.save-btn')
  248. if (saveBtn) saveBtn.addEventListener('click', saveSettings)
  249. const checkBtn = currentPopup.querySelector('.check-btn')
  250. checkBtn.addEventListener('click', () => {
  251. const testApiKey = document
  252. .getElementById('apiKeyInput')
  253. .value.trim()
  254. if (!testApiKey) {
  255. showPopup('Lỗi', 'Vui lòng nhập API key để kiểm tra!')
  256. return
  257. }
  258. GM_xmlhttpRequest({
  259. method: 'POST',
  260. url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${testApiKey}`,
  261. headers: {'Content-Type': 'application/json'},
  262. data: JSON.stringify({
  263. contents: [{parts: [{text: 'Test'}]}],
  264. }),
  265. onload: function (response) {
  266. const data = JSON.parse(response.responseText)
  267. if (data.candidates) {
  268. showPopup('Thành công', 'API key hợp lệ!')
  269. } else {
  270. showPopup(
  271. 'Lỗi',
  272. 'API key không hợp lệ. Vui lòng kiểm tra lại!'
  273. )
  274. }
  275. },
  276. onerror: function () {
  277. showPopup(
  278. 'Lỗi',
  279. 'Không thể kiểm tra API key. Kiểm tra mạng hoặc key!'
  280. )
  281. },
  282. })
  283. })
  284. }
  285.  
  286. const header = currentPopup.querySelector('.popup-header')
  287. header.addEventListener('mousedown', startDrag)
  288. document.addEventListener('mousemove', drag)
  289. document.addEventListener('mouseup', stopDrag)
  290.  
  291. const resizeHandle = currentPopup.querySelector('.resize-handle')
  292. if (resizeHandle) {
  293. resizeHandle.addEventListener('mousedown', startResize)
  294. document.addEventListener('mousemove', resize)
  295. document.addEventListener('mouseup', stopResize)
  296. }
  297.  
  298. document.body.style.pointerEvents = 'none'
  299. currentPopup.style.pointerEvents = 'auto'
  300.  
  301. if (autoClose > 0) {
  302. setTimeout(() => {
  303. closeAllPopups(true) // Đóng với animation
  304. isProcessing = false // Mở khóa sau khi tự động đóng
  305. }, autoClose)
  306. }
  307. }
  308.  
  309. // Đóng tất cả popup và loader
  310. function closeAllPopups(withAnimation = false) {
  311. if (currentPopup) {
  312. if (withAnimation) {
  313. // Đóng với animation (khi người dùng đóng thủ công)
  314. currentPopup.style.transition =
  315. 'opacity 0.15s ease-out, transform 0.15s ease-out'
  316. currentPopup.style.opacity = '0'
  317. currentPopup.style.transform =
  318. 'translate(-50%, -50%) scale(0.95)'
  319. setTimeout(() => {
  320. currentPopup.remove()
  321. currentPopup = null
  322. document.body.style.pointerEvents = 'auto'
  323. document.removeEventListener('mousemove', drag)
  324. document.removeEventListener('mouseup', stopDrag)
  325. document.removeEventListener('mousemove', resize)
  326. document.removeEventListener('mouseup', stopResize)
  327. }, 150)
  328. } else {
  329. // Xóa ngay không animation (khi tạo popup mới)
  330. currentPopup.remove()
  331. currentPopup = null
  332. document.body.style.pointerEvents = 'auto'
  333. document.removeEventListener('mousemove', drag)
  334. document.removeEventListener('mouseup', stopDrag)
  335. document.removeEventListener('mousemove', resize)
  336. document.removeEventListener('mouseup', stopResize)
  337. }
  338. }
  339. hideLoader()
  340. }
  341.  
  342. // Lấy nội dung cài đặt
  343. function getSettingsContent() {
  344. return `
  345. <div class="settings-container">
  346. <div class="settings-item">
  347. <label>API Key:</label>
  348. <div class="api-key-container">
  349. <input type="text" id="apiKeyInput" placeholder="Dán API key vào đây" value="${API_KEY}" />
  350. <button class="check-btn">Kim tra</button>
  351. </div>
  352. </div>
  353. <div class="settings-item instruction">
  354. <span>Ly key ti: <a href="https://aistudio.google.com/apikey" target="_blank">Google AI Studio</a></span>
  355. </div>
  356. <div class="settings-item shortcut-section">
  357. <label>Phím tt:</label>
  358. <div class="shortcut-controls">
  359. <label><input type="radio" name="modifier" value="Alt" ${
  360. modifierKeys.includes('Alt') ? 'checked' : ''
  361. }> Alt</label>
  362. <label><input type="radio" name="modifier" value="Ctrl" ${
  363. modifierKeys.includes('Ctrl') ? 'checked' : ''
  364. }> Ctrl</label>
  365. <label><input type="radio" name="modifier" value="Shift" ${
  366. modifierKeys.includes('Shift') ? 'checked' : ''
  367. }> Shift</label>
  368. <input type="text" id="shortcutKey" maxlength="1" placeholder="T" value="${shortcutKey.toUpperCase()}" />
  369. </div>
  370. </div>
  371. <div class="settings-item button-container">
  372. <button class="save-btn">
  373. <svg class="save-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
  374. <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"/>
  375. </svg>
  376. Lưu
  377. </button>
  378. </div>
  379. </div>
  380. `
  381. }
  382.  
  383. // Lưu cài đặt và làm mới trang
  384. function saveSettings() {
  385. const apiKey = document.getElementById('apiKeyInput').value.trim()
  386. const selectedModifier = document.querySelector(
  387. 'input[name="modifier"]:checked'
  388. )
  389. shortcutKey = document
  390. .getElementById('shortcutKey')
  391. .value.trim()
  392. .toLowerCase()
  393.  
  394. if (!apiKey) {
  395. showPopup('Lỗi', 'Bạn chưa nhập API key! Hãy nhập để tiếp tục.')
  396. return
  397. }
  398. if (!shortcutKey) {
  399. showPopup('Lỗi', 'Bạn chưa nhập phím tắt! Hãy nhập một ký tự.')
  400. return
  401. }
  402. if (!selectedModifier) {
  403. showPopup(
  404. 'Lỗi',
  405. 'Bạn chưa chọn phím bổ trợ! Chọn Alt, Ctrl hoặc Shift.'
  406. )
  407. return
  408. }
  409.  
  410. modifierKeys = [selectedModifier.value]
  411. GM_setValue('geminiApiKey', apiKey)
  412. GM_setValue('shortcutKey', shortcutKey)
  413. GM_setValue('modifierKeys', JSON.stringify(modifierKeys))
  414. API_KEY = apiKey
  415. closeAllPopups()
  416. showPopup(
  417. 'Thành công',
  418. 'Cài đặt đã được lưu! Trang sẽ làm mới sau 1 giây.'
  419. )
  420. setTimeout(() => location.reload(), 1000)
  421. }
  422.  
  423. // Xử lý kéo popup
  424. function startDrag(e) {
  425. isDragging = true
  426. offsetX = e.clientX - currentPopup.offsetLeft
  427. offsetY = e.clientY - currentPopup.offsetTop
  428. }
  429.  
  430. function drag(e) {
  431. if (isDragging) {
  432. e.preventDefault()
  433. currentPopup.style.left = `${e.clientX - offsetX}px`
  434. currentPopup.style.top = `${e.clientY - offsetY}px`
  435. }
  436. }
  437.  
  438. function stopDrag() {
  439. isDragging = false
  440. }
  441.  
  442. // Xử lý thay đổi kích thước (chỉ chiều ngang)
  443. function startResize(e) {
  444. isResizing = true
  445. initialWidth = currentPopup.offsetWidth
  446. resizeOffsetX = e.clientX - currentPopup.offsetLeft
  447. }
  448.  
  449. function resize(e) {
  450. if (isResizing) {
  451. const newWidth =
  452. initialWidth +
  453. (e.clientX - (currentPopup.offsetLeft + resizeOffsetX))
  454. currentPopup.style.width = `${Math.max(newWidth, 400)}px`
  455. }
  456. }
  457.  
  458. function stopResize() {
  459. isResizing = false
  460. }
  461.  
  462. // Loader
  463. function showLoader() {
  464. const loader = document.createElement('div')
  465. loader.className = 'summarizer-loader'
  466. loader.innerHTML = '<div class="spinner"></div>'
  467. document.body.appendChild(loader)
  468. }
  469.  
  470. function hideLoader() {
  471. const loader = document.querySelector('.summarizer-loader')
  472. if (loader) loader.remove()
  473. }
  474.  
  475. // CSS
  476. const style = document.createElement('style')
  477. style.innerHTML = `
  478. .summarizer-popup {
  479. position: fixed;
  480. top: 50%;
  481. left: 50%;
  482. transform: translate(-50%, -50%);
  483. width: 500px;
  484. min-width: 400px;
  485. height: 400px;
  486. background: #ffffff;
  487. border-radius: 12px;
  488. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
  489. z-index: 9999;
  490. font-family: 'Roboto', sans-serif;
  491. overflow: hidden;
  492. display: flex;
  493. flex-direction: column;
  494. will-change: opacity, transform;
  495. }
  496. .popup-header {
  497. background: linear-gradient(135deg, #4A90E2, #357ABD);
  498. color: #fff;
  499. padding: 15px 20px;
  500. display: flex;
  501. justify-content: space-between;
  502. align-items: center;
  503. cursor: move;
  504. border-top-left-radius: 12px;
  505. border-top-right-radius: 12px;
  506. flex-shrink: 0;
  507. }
  508. .popup-header h2 {
  509. margin: 0;
  510. font-size: 20px;
  511. font-weight: 600;
  512. text-align: center;
  513. line-height: 1.0;
  514. }
  515. .header-actions {
  516. display: flex;
  517. gap: 12px;
  518. }
  519. .header-actions button {
  520. background: none;
  521. border: none;
  522. color: #fff;
  523. font-size: 22px;
  524. cursor: pointer;
  525. transition: transform 0.15s ease-out, opacity 0.15s ease-out;
  526. }
  527. .header-actions button:hover {
  528. transform: scale(1.1);
  529. opacity: 0.9;
  530. }
  531. .copy-btn {
  532. font-size: 18px;
  533. padding: 2px;
  534. margin-left: 10px;
  535. }
  536. .popup-content {
  537. padding: 15px;
  538. font-size: 15px;
  539. color: #444;
  540. line-height: 1.2;
  541. flex-grow: 1;
  542. display: flex;
  543. flex-direction: column;
  544. justify-content: space-between;
  545. }
  546. .popup-content-summary {
  547. padding: 15px;
  548. font-size: 15px;
  549. color: #444;
  550. line-height: 1.6;
  551. overflow-y: auto;
  552. white-space: pre-wrap;
  553. flex-grow: 1;
  554. }
  555. .settings-container {
  556. display: flex;
  557. flex-direction: column;
  558. gap: 10px;
  559. }
  560. .settings-item {
  561. display: flex;
  562. flex-direction: column;
  563. align-items: center;
  564. gap: 5px;
  565. }
  566. .settings-item label {
  567. font-weight: 600;
  568. color: #333;
  569. line-height: 1.2;
  570. }
  571. .settings-item input[type="text"] {
  572. width: 80%;
  573. max-width: 300px;
  574. padding: 8px;
  575. border: 1px solid #ddd;
  576. border-radius: 6px;
  577. font-size: 14px;
  578. text-align: center;
  579. background: #f9f9f9;
  580. transition: border-color 0.15s ease-out;
  581. }
  582. .settings-item input[type="text"]:focus {
  583. border-color: #4A90E2;
  584. outline: none;
  585. }
  586. .api-key-container {
  587. display: flex;
  588. gap: 10px;
  589. align-items: center;
  590. }
  591. .check-btn {
  592. padding: 8px;
  593. width: 80%;
  594. max-width: 300px;
  595. background: linear-gradient(135deg, #4A90E2, #357ABD);
  596. color: #fff;
  597. border: none;
  598. border-radius: 6px;
  599. cursor: pointer;
  600. font-size: 14px;
  601. transition: transform 0.15s ease-out, box-shadow 0.15s ease-out;
  602. }
  603. .check-btn:hover {
  604. transform: translateY(-2px);
  605. box-shadow: 0 5px 15px rgba(74, 144, 226, 0.4);
  606. }
  607. .instruction {
  608. font-size: 13px;
  609. color: #666;
  610. line-height: 1.2;
  611. }
  612. .instruction a {
  613. color: #4A90E2;
  614. text-decoration: none;
  615. transition: color 0.15s ease-out;
  616. }
  617. .instruction a:hover {
  618. color: #357ABD;
  619. text-decoration: underline;
  620. }
  621. .shortcut-section {
  622. flex-direction: row;
  623. justify-content: center;
  624. align-items: center;
  625. gap: 10px;
  626. }
  627. .shortcut-controls {
  628. display: flex;
  629. align-items: center;
  630. gap: 8px;
  631. }
  632. .shortcut-controls label {
  633. display: flex;
  634. align-items: center;
  635. gap: 3px;
  636. font-size: 14px;
  637. font-weight: 400;
  638. color: #444;
  639. }
  640. .shortcut-controls input[type="radio"] {
  641. margin: 0;
  642. }
  643. .shortcut-controls input[type="text"] {
  644. width: 40px;
  645. padding: 6px;
  646. border: 1px solid #ddd;
  647. border-radius: 6px;
  648. font-size: 14px;
  649. text-align: center;
  650. background: #f9f9f9;
  651. transition: border-color 0.15s ease-out;
  652. }
  653. .shortcut-controls input[type="text"]:focus {
  654. border-color: #4A90E2;
  655. outline: none;
  656. }
  657. .button-container {
  658. margin-top: 10px;
  659. }
  660. .save-btn {
  661. padding: 8px 20px;
  662. background: linear-gradient(135deg, #4A90E2, #357ABD);
  663. color: #fff;
  664. border: none;
  665. border-radius: 6px;
  666. cursor: pointer;
  667. font-size: 15px;
  668. font-weight: 500;
  669. display: flex;
  670. align-items: center;
  671. gap: 6px;
  672. transition: transform 0.15s ease-out, box-shadow 0.15s ease-out;
  673. }
  674. .save-btn:hover {
  675. transform: translateY(-2px);
  676. box-shadow: 0 5px 15px rgba(74, 144, 226, 0.4);
  677. }
  678. .save-icon {
  679. width: 16px;
  680. height: 16px;
  681. }
  682. .history-item {
  683. border-bottom: 1px solid #ddd;
  684. padding: 10px 0;
  685. font-size: 14px;
  686. color: #444;
  687. }
  688. .history-item:last-child {
  689. border-bottom: none;
  690. }
  691. .resize-handle {
  692. position: absolute;
  693. bottom: 0;
  694. right: 0;
  695. width: 20px;
  696. height: 20px;
  697. background: #4A90E2;
  698. cursor: se-resize;
  699. border-bottom-right-radius: 12px;
  700. transition: background 0.15s ease-out;
  701. }
  702. .resize-handle:hover {
  703. background: #357ABD;
  704. }
  705. .summarizer-loader {
  706. position: fixed;
  707. top: 0;
  708. left: 0;
  709. width: 100%;
  710. height: 100%;
  711. background: rgba(0, 0, 0, 0.6);
  712. display: flex;
  713. justify-content: center;
  714. align-items: center;
  715. z-index: 10000;
  716. }
  717. .spinner {
  718. border: 5px solid rgba(255, 255, 255, 0.3);
  719. border-top: 5px solid #4A90E2;
  720. border-radius: 50%;
  721. width: 50px;
  722. height: 50px;
  723. animation: spin 0.8s linear infinite;
  724. }
  725. @keyframes spin {
  726. 0% { transform: rotate(0deg); }
  727. 100% { transform: rotate(360deg); }
  728. }
  729. `
  730. document.head.appendChild(style)
  731.  
  732. // Thêm font Roboto
  733. const fontLink = document.createElement('link')
  734. fontLink.href =
  735. 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600&display=swap'
  736. fontLink.rel = 'stylesheet'
  737. document.head.appendChild(fontLink)
  738.  
  739. // Khởi chạy script
  740. initialize()
  741. })()