您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Single-button AI summarization with model selection dropdown
当前为
// ==UserScript== // @name Summarize with AI (Unified) // @namespace https://github.com/insign/summarize-with-ai // @version 2025.02.14.19.42 // @description Single-button AI summarization with model selection dropdown // @author Hélio <[email protected]> // @license WTFPL // @match *://*/* // @grant GM.addStyle // @grant GM.xmlHttpRequest // @grant GM.setValue // @grant GM.getValue // @connect api.openai.com // @connect generativelanguage.googleapis.com // @require https://cdnjs.cloudflare.com/ajax/libs/readability/0.5.0/Readability.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/readability/0.5.0/Readability-readerable.min.js // ==/UserScript== (function() { 'use strict' const BUTTON_ID = 'summarize-button' const DROPDOWN_ID = 'model-dropdown' const OVERLAY_ID = 'summarize-overlay' const CLOSE_BUTTON_ID = 'summarize-close' const CONTENT_ID = 'summarize-content' const ERROR_ID = 'summarize-error' const MODEL_GROUPS = { openai: { name : 'OpenAI', models : [ 'gpt-4o-mini' ], baseUrl: 'https://api.openai.com/v1/chat/completions', }, gemini: { name : 'Gemini', models : [ 'gemini-2.0-flash-exp', 'gemini-2.0-pro-exp-02-05', 'gemini-2.0-flash-thinking-exp-01-21', 'learnlm-1.5-pro-experimental', 'gemini-2.0-flash-lite-preview-02-05', ], baseUrl: 'https://generativelanguage.googleapis.com/v1beta/models/', }, } const PROMPT_TEMPLATE = (title, content, lang) => `You are a helpful assistant that provides clear and affirmative explanations of content. Generate a concise summary that includes: - 2-sentence introduction - Bullet points with relevant emojis - No section headers - Use HTML formatting, but send withouy \`\`\`html markdown syntax since it well be injected into the page to the browser evaluate correctly - After the last bullet point add a 2-sentence conclusion using opinionated language your general knowledge - Language: ${lang} Article Title: ${title} Article Content: ${content}` let activeModel = 'gpt-4o-mini' let articleData = null function initialize() { document.addEventListener('keydown', handleKeyPress) setupFocusListeners() articleData = getArticleData() if (articleData) { addSummarizeButton() showElement(BUTTON_ID) } } function getArticleData() { try { const docClone = document.cloneNode(true) docClone.querySelectorAll('script, style').forEach(el => el.remove()) if (!isProbablyReaderable(docClone)) return null const reader = new Readability(docClone) const article = reader.parse() return article?.content ? { title: article.title, content: article.textContent } : null } catch (error) { console.error('Article parsing failed:', error) return null } } function addSummarizeButton() { if (document.getElementById(BUTTON_ID)) return const button = document.createElement('div') button.id = BUTTON_ID button.textContent = 'S' document.body.appendChild(button) const dropdown = createDropdown() document.body.appendChild(dropdown) button.addEventListener('click', toggleDropdown) button.addEventListener('dblclick', handleApiKeyReset) injectStyles() } function createDropdown() { const dropdown = document.createElement('div') dropdown.id = DROPDOWN_ID dropdown.style.display = 'none' Object.entries(MODEL_GROUPS).forEach(([ service, group ]) => { const groupDiv = document.createElement('div') groupDiv.className = 'model-group' groupDiv.appendChild(createHeader(group.name)) group.models.forEach(model => groupDiv.appendChild(createModelItem(model))) dropdown.appendChild(groupDiv) }) return dropdown } function createHeader(text) { const header = document.createElement('div') header.className = 'group-header' header.textContent = text return header } function createModelItem(model) { const item = document.createElement('div') item.className = 'model-item' item.textContent = model item.addEventListener('click', () => { activeModel = model hideElement(DROPDOWN_ID) processSummarization() }) return item } async function processSummarization() { try { const service = getCurrentService() const apiKey = await getApiKey(service) if (!apiKey) return showSummaryOverlay('<p class="glow">Summarizing...</p>') const payload = { title: articleData.title, content: articleData.content, lang: navigator.language || 'en-US' } const response = await sendApiRequest(service, apiKey, payload) handleApiResponse(response, service) } catch (error) { showErrorNotification(`Error: ${error.message}`) } } async function sendApiRequest(service, apiKey, payload) { const url = service === 'openai' ? MODEL_GROUPS.openai.baseUrl : `${MODEL_GROUPS.gemini.baseUrl}${activeModel}:generateContent?key=${apiKey}` return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method : 'POST', url, headers: getHeaders(service, apiKey), data : JSON.stringify(buildRequestBody(service, payload)), onload : resolve, onerror: reject, onabort: () => reject(new Error('Request aborted')), }) }) } function handleApiResponse(response, service) { if (response.status !== 200) { throw new Error(`API Error (${response.status}): ${response.statusText}`) } const data = JSON.parse(response.responseText) const summary = service === 'openai' ? data.choices[0].message.content : data.candidates[0].content.parts[0].text updateSummaryOverlay(summary.replace(/\n/g, '<br>')) } function buildRequestBody(service, { title, content, lang }) { return service === 'openai' ? { model : activeModel, messages : [ { role : 'system', content: PROMPT_TEMPLATE(title, content, lang), }, { role : 'user', content: 'Generate summary', }, ], temperature: 0.5, max_tokens : 500, } : { contents: [ { parts: [ { text: PROMPT_TEMPLATE(title, content, lang), }, ], }, ], } } function getHeaders(service, apiKey) { return service === 'openai' ? { 'Content-Type' : 'application/json', 'Authorization': `Bearer ${apiKey}`, } : { 'Content-Type': 'application/json' } } function getCurrentService() { return Object.keys(MODEL_GROUPS).find(service => MODEL_GROUPS[service].models.includes(activeModel), ) } function toggleDropdown(e) { e.stopPropagation() const dropdown = document.getElementById(DROPDOWN_ID) dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none' } function handleKeyPress(e) { if (e.altKey && e.code === 'KeyS') { e.preventDefault() document.getElementById(BUTTON_ID)?.click() } } async function getApiKey(service) { const storageKey = `${service}_api_key` let apiKey = await GM.getValue(storageKey) if (!apiKey) { apiKey = prompt(`Enter ${service.toUpperCase()} API key:`) if (apiKey) await GM.setValue(storageKey, apiKey.trim()) } return apiKey?.trim() } function handleApiKeyReset() { const service = prompt('Reset API key for (openai/gemini):').toLowerCase() if (MODEL_GROUPS[service]) { const newKey = prompt(`Enter new ${service} API key:`) if (newKey) GM.setValue(`${service}_api_key`, newKey.trim()) } } function injectStyles() { GM.addStyle(` #${BUTTON_ID} { position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; background: #2563eb; color: white; font-size: 28px; font-family: sans-serif; border-radius: 50%; cursor: pointer; z-index: 99999; box-shadow: 0 2px 10px rgba(0,0,0,0.2); display: flex !important; align-items: center !important; justify-content: center !important; transition: transform 0.2s; line-height: 1; } #${DROPDOWN_ID} { position: fixed; bottom: 90px; right: 20px; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 100000; max-height: 60vh; overflow-y: auto; padding: 12px; width: 280px; font-family: sans-serif; } .model-group { margin: 8px 0; } .group-header { padding: 8px 12px; font-weight: 600; color: #4b5563; background: #f3f4f6; border-radius: 4px; margin-bottom: 6px; font-family: sans-serif; } .model-item { padding: 10px 16px; margin: 4px 0; border-radius: 6px; transition: background 0.2s; font-size: 14px; font-family: sans-serif; cursor: pointer; } .model-item:hover { background: #1143b2; } #${OVERLAY_ID} { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 100000; display: flex; align-items: center; justify-content: center; overflow: auto; font-family: sans-serif; } #${CONTENT_ID} { background-color: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 0 15px rgba(0,0,0,0.5); max-width: 700px; max-height: 90%; overflow: auto; position: relative; font-size: 1.2em; color: #000; font-family: sans-serif; } #${ERROR_ID} { position: fixed; bottom: 20px; left: 20px; background-color: rgba(255,0,0,0.8); color: white; padding: 10px 20px; border-radius: 5px; z-index: 100001; font-size: 14px; font-family: sans-serif; } .glow { font-size: 1.5em; color: #333; text-align: center; animation: glow 2s ease-in-out infinite alternate; font-family: sans-serif; } @keyframes glow { from { color: #4b6cb7; text-shadow: 0 0 10px #4b6cb7; } to { color: #182848; text-shadow: 0 0 20px #8e2de2; } } `) } function showSummaryOverlay(content) { if (document.getElementById(OVERLAY_ID)) { updateSummaryOverlay(content) return } const overlay = document.createElement('div') overlay.id = OVERLAY_ID overlay.innerHTML = ` <div id="${CONTENT_ID}"> <div id="${CLOSE_BUTTON_ID}">×</div> ${content} </div> ` document.body.appendChild(overlay) document.body.style.overflow = 'hidden' document.getElementById(CLOSE_BUTTON_ID).addEventListener('click', closeOverlay) overlay.addEventListener('click', (e) => { if (!e.target.closest(`#${CONTENT_ID}`)) closeOverlay() }) document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeOverlay() }) function closeOverlay() { document.getElementById(OVERLAY_ID)?.remove() document.body.style.overflow = '' } } function updateSummaryOverlay(content) { const contentDiv = document.getElementById(CONTENT_ID) if (contentDiv) { contentDiv.innerHTML = `<div id="${CLOSE_BUTTON_ID}">×</div>${content}` document.getElementById(CLOSE_BUTTON_ID).addEventListener('click', closeOverlay) } function closeOverlay() { document.getElementById(OVERLAY_ID)?.remove() document.body.style.overflow = '' } } function showErrorNotification(message) { if (document.getElementById(ERROR_ID)) { document.getElementById(ERROR_ID).innerText = message return } const errorDiv = document.createElement('div') errorDiv.id = ERROR_ID errorDiv.innerText = message document.body.appendChild(errorDiv) setTimeout(() => errorDiv.remove(), 4000) } function hideElement(id) { const el = document.getElementById(id) if (el) el.style.display = 'none' } function showElement(id) { const el = document.getElementById(id) if (el) el.style.display = 'block' } function setupFocusListeners() { document.addEventListener('focusin', toggleButtonVisibility) document.addEventListener('focusout', toggleButtonVisibility) } function toggleButtonVisibility() { const active = document.activeElement const isInput = active?.matches('input, textarea, select, [contenteditable]') document.getElementById(BUTTON_ID).style.display = isInput ? 'none' : 'block' } initialize() })()