// ==UserScript==
// @name Text Summarizer with Gemini API
// @namespace http://tampermonkey.net/
// @version 2.1
// @description Summarize selected text using Gemini 2.0 Flash API
// @author Hà Trọng Nguyễn
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect generativelanguage.googleapis.com
// @homepageURL https://github.com/htrnguyen
// @supportURL https://github.com/htrnguyen/Text-Summarizer-with-Gemini-API/issues
// @icon https://github.com/htrnguyen/User-Scripts/raw/main/Text-Summarizer-with-Gemini-API/text-summarizer-logo.png
// @license MIT
// ==/UserScript==
;(function () {
'use strict'
// Khai báo biến toàn cục
let API_KEY = GM_getValue('geminiApiKey', '') || ''
let shortcutKey = GM_getValue('shortcutKey', 't')
let modifierKeys = JSON.parse(GM_getValue('modifierKeys', '["Alt"]')) || [
'Alt',
]
let currentPopup = null
let isDragging = false
let isResizing = false
let offsetX, offsetY, resizeOffsetX, initialWidth
// Hàm khởi tạo
function initialize() {
if (!API_KEY) {
showPopup('Cài đặt', getSettingsContent())
}
setupEventListeners()
}
// Thiết lập các sự kiện
function setupEventListeners() {
document.addEventListener('keydown', handleKeydown)
if (typeof GM_registerMenuCommand !== 'undefined') {
GM_registerMenuCommand('Cài đặt Text Summarizer', () =>
showPopup('Cài đặt', getSettingsContent())
)
}
}
// Kiểm tra phím tắt
function checkShortcut(e) {
const key = e.key.toLowerCase()
const modifiers = modifierKeys.map((mod) => mod.toLowerCase())
const currentModifiers = []
if (e.altKey) currentModifiers.push('alt')
if (e.ctrlKey) currentModifiers.push('ctrl')
if (e.shiftKey) currentModifiers.push('shift')
return (
key === shortcutKey &&
currentModifiers.sort().join(',') === modifiers.sort().join(',')
)
}
// Xử lý phím tắt và ESC
function handleKeydown(e) {
if (checkShortcut(e)) {
e.preventDefault()
const selectedText = window.getSelection().toString().trim()
if (selectedText) {
summarizeText(selectedText)
} else {
showPopup(
'Lỗi',
'Vui lòng chọn một đoạn văn bản để tóm tắt nhé!'
)
}
} else if (e.key === 'Escape' && currentPopup) {
closePopup()
}
}
// Gửi yêu cầu đến Gemini API
function summarizeText(text) {
showLoader()
GM_xmlhttpRequest({
method: 'POST',
url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${API_KEY}`,
headers: {'Content-Type': 'application/json'},
data: JSON.stringify({
contents: [
{
parts: [
{
text: `Tóm tắt nội dung sau đây, đảm bảo giữ lại các ý chính và chi tiết quan trọng, tránh lược bỏ quá nhiều. Kết quả cần có xuống dòng và bố cục hợp lý để dễ đọc. Chỉ bao gồm thông tin cần tóm tắt, không thêm phần thừa như 'dưới đây là tóm tắt' hoặc lời dẫn. Định dạng trả về là văn bản thông thường, không sử dụng markdown. Bạn có thể thêm emoji (🌟, ➡️, 1️⃣) để làm dấu chấm, số thứ tự hoặc gạch đầu dòng, nhưng hãy hạn chế và sử dụng một cách tinh tế. Nội dung cần tóm tắt là: ${text}`,
},
],
},
],
}),
onload: function (response) {
hideLoader()
const data = JSON.parse(response.responseText)
if (data.candidates && data.candidates.length > 0) {
const summary =
data.candidates[0].content.parts[0].text ||
'Không thể tóm tắt được nội dung này!'
showPopup('Tóm tắt', summary)
} else if (data.error) {
showPopup('Lỗi', `Có lỗi từ API: ${data.error.message}`)
} else {
showPopup(
'Lỗi',
'Phản hồi từ API không hợp lệ. Hãy thử lại!'
)
}
},
onerror: function (error) {
hideLoader()
showPopup(
'Lỗi',
`Lỗi kết nối: ${error.message}. Kiểm tra mạng nhé!`
)
},
})
}
// Hiển thị popup duy nhất
function showPopup(title, content) {
closePopup() // Đóng popup cũ trước khi mở mới
currentPopup = document.createElement('div')
currentPopup.className = 'summarizer-popup'
currentPopup.innerHTML = `
<div class="popup-header">
<h2>${title}</h2>
<div class="header-actions">
<button class="close-btn">×</button>
</div>
</div>
<div class="${
title === 'Tóm tắt' ? 'popup-content-summary' : 'popup-content'
}">${content}</div>
<div class="resize-handle"></div>
`
document.body.appendChild(currentPopup)
currentPopup.style.opacity = '0'
currentPopup.style.transform = 'translate(-50%, -50%) scale(0.9)'
setTimeout(() => {
currentPopup.style.opacity = '1'
currentPopup.style.transform = 'translate(-50%, -50%) scale(1)'
}, 10)
currentPopup
.querySelector('.close-btn')
.addEventListener('click', closePopup)
if (title === 'Cài đặt') {
const saveBtn = currentPopup.querySelector('.save-btn')
if (saveBtn) saveBtn.addEventListener('click', saveSettings)
}
const header = currentPopup.querySelector('.popup-header')
header.addEventListener('mousedown', startDrag)
document.addEventListener('mousemove', drag)
document.addEventListener('mouseup', stopDrag)
const resizeHandle = currentPopup.querySelector('.resize-handle')
resizeHandle.addEventListener('mousedown', startResize)
document.addEventListener('mousemove', resize)
document.addEventListener('mouseup', stopResize)
document.body.style.pointerEvents = 'none'
currentPopup.style.pointerEvents = 'auto'
}
// Lấy nội dung cài đặt
function getSettingsContent() {
return `
<div class="settings-container">
<div class="settings-item">
<label>API Key:</label>
<input type="text" id="apiKeyInput" placeholder="Dán API key vào đây" value="${API_KEY}" />
</div>
<div class="settings-item instruction">
<span>Lấy key tại: <a href="https://aistudio.google.com/apikey" target="_blank">Google AI Studio</a></span>
</div>
<div class="settings-item shortcut-section">
<label>Phím tắt:</label>
<div class="shortcut-controls">
<label><input type="radio" name="modifier" value="Alt" ${
modifierKeys.includes('Alt') ? 'checked' : ''
}> Alt</label>
<label><input type="radio" name="modifier" value="Ctrl" ${
modifierKeys.includes('Ctrl') ? 'checked' : ''
}> Ctrl</label>
<label><input type="radio" name="modifier" value="Shift" ${
modifierKeys.includes('Shift') ? 'checked' : ''
}> Shift</label>
<input type="text" id="shortcutKey" maxlength="1" placeholder="T" value="${shortcutKey.toUpperCase()}" />
</div>
</div>
<div class="settings-item button-container">
<button class="save-btn">
<svg class="save-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<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"/>
</svg>
Lưu
</button>
</div>
</div>
`
}
// Lưu cài đặt và làm mới trang
function saveSettings() {
const apiKey = document.getElementById('apiKeyInput').value.trim()
const selectedModifier = document.querySelector(
'input[name="modifier"]:checked'
)
shortcutKey = document
.getElementById('shortcutKey')
.value.trim()
.toLowerCase()
if (!apiKey) {
showPopup('Lỗi', 'Bạn chưa nhập API key! Hãy nhập để tiếp tục.')
return
}
if (!shortcutKey) {
showPopup('Lỗi', 'Bạn chưa nhập phím tắt! Hãy nhập một ký tự.')
return
}
if (!selectedModifier) {
showPopup(
'Lỗi',
'Bạn chưa chọn phím bổ trợ! Chọn Alt, Ctrl hoặc Shift.'
)
return
}
modifierKeys = [selectedModifier.value]
GM_setValue('geminiApiKey', apiKey)
GM_setValue('shortcutKey', shortcutKey)
GM_setValue('modifierKeys', JSON.stringify(modifierKeys))
API_KEY = apiKey
closePopup()
showPopup(
'Thành công',
'Cài đặt đã được lưu! Trang sẽ làm mới sau 1 giây.'
)
setTimeout(() => location.reload(), 1000) // Làm mới trang để tránh bị kẹt
}
// Xử lý kéo popup
function startDrag(e) {
isDragging = true
offsetX = e.clientX - currentPopup.offsetLeft
offsetY = e.clientY - currentPopup.offsetTop
}
function drag(e) {
if (isDragging) {
e.preventDefault()
currentPopup.style.left = `${e.clientX - offsetX}px`
currentPopup.style.top = `${e.clientY - offsetY}px`
}
}
function stopDrag() {
isDragging = false
}
// Xử lý thay đổi kích thước (chỉ chiều ngang)
function startResize(e) {
isResizing = true
initialWidth = currentPopup.offsetWidth
resizeOffsetX = e.clientX - currentPopup.offsetLeft
}
function resize(e) {
if (isResizing) {
const newWidth =
initialWidth +
(e.clientX - (currentPopup.offsetLeft + resizeOffsetX))
currentPopup.style.width = `${Math.max(newWidth, 400)}px`
}
}
function stopResize() {
isResizing = false
}
// Loader
function showLoader() {
const loader = document.createElement('div')
loader.className = 'summarizer-loader'
loader.innerHTML = '<div class="spinner"></div>'
document.body.appendChild(loader)
}
function hideLoader() {
const loader = document.querySelector('.summarizer-loader')
if (loader) loader.remove()
}
// Đóng popup
function closePopup() {
if (currentPopup) {
currentPopup.style.opacity = '1'
currentPopup.style.transform = 'translate(-50%, -50%) scale(1)'
setTimeout(() => {
currentPopup.style.opacity = '0'
currentPopup.style.transform =
'translate(-50%, -50%) scale(0.9)'
setTimeout(() => {
currentPopup.remove()
currentPopup = null
document.body.style.pointerEvents = 'auto'
document.removeEventListener('mousemove', drag)
document.removeEventListener('mouseup', stopDrag)
}, 200)
}, 10)
}
}
// CSS
const style = document.createElement('style')
style.innerHTML = `
.summarizer-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 500px;
min-width: 400px;
height: 400px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
z-index: 9999;
font-family: 'Roboto', sans-serif;
overflow: hidden;
transition: opacity 0.2s ease, transform 0.2s ease;
display: flex;
flex-direction: column;
}
.popup-header {
background: linear-gradient(135deg, #4A90E2, #357ABD);
color: #fff;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
flex-shrink: 0;
}
.popup-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
text-align: center;
line-height: 1.0;
}
.header-actions {
display: flex;
gap: 12px;
}
.header-actions button {
background: none;
border: none;
color: #fff;
font-size: 22px;
cursor: pointer;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.header-actions button:hover {
transform: scale(1.1);
opacity: 0.9;
}
.popup-content {
padding: 15px;
font-size: 15px;
color: #444;
line-height: 1.2;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.popup-content-summary {
padding: 15px;
font-size: 15px;
color: #444;
line-height: 1.6;
overflow-y: auto;
white-space: pre-wrap;
flex-grow: 1;
}
.settings-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.settings-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.settings-item label {
font-weight: 600;
color: #333;
line-height: 1.2;
}
.settings-item input[type="text"] {
width: 80%;
max-width: 300px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
text-align: center;
background: #f9f9f9;
transition: border-color 0.2s ease;
}
.settings-item input[type="text"]:focus {
border-color: #4A90E2;
outline: none;
}
.instruction {
font-size: 13px;
color: #666;
line-height: 1.2;
}
.instruction a {
color: #4A90E2;
text-decoration: none;
transition: color 0.2s ease;
}
.instruction a:hover {
color: #357ABD;
text-decoration: underline;
}
.shortcut-section {
flex-direction: row;
justify-content: center;
align-items: center;
gap: 10px;
}
.shortcut-controls {
display: flex;
align-items: center;
gap: 8px;
}
.shortcut-controls label {
display: flex;
align-items: center;
gap: 3px;
font-size: 14px;
font-weight: 400;
color: #444;
}
.shortcut-controls input[type="radio"] {
margin: 0;
}
.shortcut-controls input[type="text"] {
width: 40px;
padding: 6px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
text-align: center;
background: #f9f9f9;
transition: border-color 0.2s ease;
}
.shortcut-controls input[type="text"]:focus {
border-color: #4A90E2;
outline: none;
}
.button-container {
margin-top: 10px;
}
.save-btn {
padding: 8px 20px;
background: linear-gradient(135deg, #4A90E2, #357ABD);
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 15px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.save-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(74, 144, 226, 0.4);
}
.save-icon {
width: 16px;
height: 16px;
}
.resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
background: #4A90E2;
cursor: se-resize;
border-bottom-right-radius: 12px;
transition: background 0.2s ease;
}
.resize-handle:hover {
background: #357ABD;
}
.summarizer-loader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
}
.spinner {
border: 5px solid rgba(255, 255, 255, 0.3);
border-top: 5px solid #4A90E2;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`
document.head.appendChild(style)
// Thêm font Roboto
const fontLink = document.createElement('link')
fontLink.href =
'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600&display=swap'
fontLink.rel = 'stylesheet'
document.head.appendChild(fontLink)
// Khởi chạy script
initialize()
})()