// ==UserScript==
// @name GPT4Free Page Summarizer (Local Free API)
// @version 1.4.1
// @description Generate a summary of any webpage, selected text, YouTube video transcript using a local instance of g4f
// @author SH3LL
// @match *://*/*
// @grant GM.xmlHttpRequest
// @run-at document-end
// @namespace http://tampermonkey.net/
// ==/UserScript==
function getPageText() {
return document.body.innerText;
}
function getSelectedText() {
return window.getSelection().toString();
}
function summarizePage(textToSummarize, language, maxLines, callback) {
const apiUrl = 'http://localhost:1337/v1/chat/completions';
const prompt = `Summarize the following text in ${language} in max ${maxLines} lines, in a clear, concise and text organised in paragraphs.
Don't add any other sentence like "Here is the summary", write directly the summary itself.
Here is the text: ${textToSummarize}`;
const data = {
messages: [{
role: 'user',
content: prompt
}],
model: 'DeepSeek-V3',
provider: 'Blackbox'
};
GM.xmlHttpRequest({
method: 'POST',
url: apiUrl,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(data),
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.choices && jsonResponse.choices[0].message && jsonResponse.choices[0].message.content) {
callback(null, jsonResponse.choices[0].message.content, response.status);
} else {
callback('Unexpected format of the API response.', null, response.status);
}
} catch (error) {
callback('Error parsing the API response: ' + error, null, response.status);
}
} else {
callback('Error in the API call: ' + response.status + ' ' + response.statusText, null, response.status);
}
},
onerror: function(error) {
callback('Network error during the API call: ' + error, null, null);
}
});
}
function getYouTubeTranscript() {
return new Promise((resolve, reject) => {
const TRANSCRIPT_TARGET_ID = "engagement-panel-searchable-transcript";
const VISIBILITY_EXPANDED = "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED";
const VISIBILITY_HIDDEN = "ENGAGEMENT_PANEL_VISIBILITY_HIDDEN";
const TRANSCRIPT_CONTENT_SELECTOR = `ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_TARGET_ID}"] #content`;
const SEGMENT_SELECTOR = '.segment-text';
let transcriptPanelElement = null;
function hideTranscriptPanel() {
if (transcriptPanelElement) {
transcriptPanelElement.setAttribute("visibility", VISIBILITY_HIDDEN);
transcriptPanelElement = null; // Release the reference
}
}
function extractTranscript() {
const transcriptPanelContent = document.querySelector(TRANSCRIPT_CONTENT_SELECTOR);
if (transcriptPanelContent) {
let transcriptText = "";
transcriptPanelContent.querySelectorAll(SEGMENT_SELECTOR).forEach(segment => {
transcriptText += segment.textContent.trim() + " ";
});
hideTranscriptPanel();
resolve(transcriptText.trim());
} else {
reject("Transcript panel content not found.");
}
}
function onTranscriptPanelVisible() {
const transcriptContentObserver = new MutationObserver((mutationsList, observer) => {
const transcriptPanelContent = document.querySelector(TRANSCRIPT_CONTENT_SELECTOR);
if (transcriptPanelContent && transcriptPanelContent.children.length > 0) {
extractTranscript();
observer.disconnect(); // Stop observing once extracted
}
});
const transcriptPanel = document.querySelector(`ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_TARGET_ID}"]`);
const contentTarget = transcriptPanel ? transcriptPanel.querySelector('#content') : null;
const config = { childList: true, subtree: true }; // Observe for addition of child nodes
if (contentTarget) {
transcriptContentObserver.observe(contentTarget, config);
} else {
reject("Could not find the #content element within the transcript panel.");
}
}
function showTranscriptPanelAndObserveContent() {
const transcripts = document.querySelectorAll(`[target-id="${TRANSCRIPT_TARGET_ID}"]`);
if (transcripts.length === 1) {
transcriptPanelElement = transcripts[0]; // Store panel element
transcriptPanelElement.setAttribute("visibility", VISIBILITY_EXPANDED);
onTranscriptPanelVisible();
} else {
reject('Transcript panel element not found.');
}
}
const panelObserver = new MutationObserver((mutationsList, observer) => {
const transcriptPanelContainer = document.querySelector(`ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_TARGET_ID}"]`);
if (transcriptPanelContainer) {
showTranscriptPanelAndObserveContent();
observer.disconnect(); // Stop observing once found
}
});
// Start observing for the transcript panel container
const targetNode = document.body;
const config = { childList: true, subtree: true };
panelObserver.observe(targetNode, config);
// Set a timeout in case the transcript panel doesn't load
setTimeout(() => {
if (!transcriptPanelElement && !document.querySelector(TRANSCRIPT_CONTENT_SELECTOR)) {
panelObserver.disconnect(); // Ensure observer is stopped
reject("Timeout: Could not find transcript panel.");
}
}, 10000); // Adjust timeout as needed
});
}
(function() {
'use strict';
// GET LANGUAGE PAGE
const browserLanguage = navigator.language;
let selectedLanguage;
try {
const languageNames = new Intl.DisplayNames([browserLanguage], { type: 'language' });
selectedLanguage = languageNames.of(browserLanguage);
// Fallback to the raw language code if Intl.DisplayNames fails or returns undefined
if (!selectedLanguage) {
selectedLanguage = browserLanguage;
}
} catch (error) {
console.error("Error initializing Intl.DisplayNames:", error);
selectedLanguage = browserLanguage; // Fallback to raw language code
}
// GET YOUTUBE TRANSCRIPT
let ytTranscript = null;
const isYouTubeVideoPage = window.location.hostname.includes("youtube.com") && window.location.pathname === "/watch";
if (isYouTubeVideoPage) {
getYouTubeTranscript()
.then(transcript => {
ytTranscript = transcript;
console.log("YOUTUBE Transcript:", transcript);
})
.catch(error => {
console.error("Error getting YOUTUBE transcript:", error);
});
}
// CREATE DOM CONTAINER
const shadowHost = document.createElement('div');
document.body.appendChild(shadowHost);
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
// CREATE SIDEBAR DOM
const sidebar = document.createElement('div');
sidebar.style.position = 'fixed';
sidebar.style.right = '-300px';
sidebar.style.top = '0';
sidebar.style.width = '300px';
sidebar.style.height = '100vh';
sidebar.style.backgroundColor = '#000000';
sidebar.style.color = '#ffffff';
sidebar.style.padding = '20px';
sidebar.style.zIndex = '999999'; // Increased to ensure it's above all elements
sidebar.style.fontFamily = 'Arial, sans-serif';
sidebar.style.boxSizing = 'border-box';
sidebar.style.display = 'flex';
sidebar.style.flexDirection = 'column';
sidebar.style.gap = '10px';
sidebar.style.transition = 'right 0.3s ease';
shadowRoot.appendChild(sidebar);
// CREATE BUTTON HIDE/SHOW SIDEBAR
const toggleButton = document.createElement('button');
toggleButton.textContent = '>';
toggleButton.style.position = 'fixed';
toggleButton.style.right = '0';
toggleButton.style.top = '20px';
toggleButton.style.backgroundColor = '#FFC107'; // Mustard yellow
toggleButton.style.color = '#000000'; // White text
toggleButton.style.border = 'none';
toggleButton.style.padding = '10px';
toggleButton.style.cursor = 'pointer';
toggleButton.style.zIndex = '1000000'; // Higher than sidebar
toggleButton.style.fontSize = '14px';
toggleButton.style.transition = 'right 0.3s ease';
shadowRoot.appendChild(toggleButton);
// CREATE CONTAINER FOR BUTTON AND SELECTOR
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '10px';
buttonContainer.style.alignItems = 'center';
sidebar.appendChild(buttonContainer);
// CREATE SUMMARIZE BUTTON
const summarizeButton = document.createElement('button');
if (isYouTubeVideoPage && ytTranscript!==null) {
summarizeButton.textContent = 'Summarize YT';
}else{
summarizeButton.textContent = 'Summarize';
}
summarizeButton.style.backgroundColor = '#333333';
summarizeButton.style.color = '#ffffff';
summarizeButton.style.border = 'none';
summarizeButton.style.padding = '10px 20px';
summarizeButton.style.cursor = 'pointer';
summarizeButton.style.fontSize = '14px';
summarizeButton.style.flex = '1';
summarizeButton.style.transition = 'background-color 0.3s';
summarizeButton.onmouseover = () => summarizeButton.style.backgroundColor = '#4d4d4d';
summarizeButton.onmouseout = () => summarizeButton.style.backgroundColor = '#333333';
buttonContainer.appendChild(summarizeButton);
// Create the line number selector
const linesSelector = document.createElement('select');
linesSelector.style.backgroundColor = '#333333';
linesSelector.style.color = '#ffffff';
linesSelector.style.border = 'none';
linesSelector.style.padding = '10px';
linesSelector.style.fontSize = '14px';
linesSelector.style.cursor = 'pointer';
const lineOptions = [5, 10, 15, 20, 25, 30, 50, 100];
lineOptions.forEach(lines => {
const option = document.createElement('option');
option.value = lines;
option.textContent = `${lines} lines`;
if (lines === 5) option.selected = true;
linesSelector.appendChild(option);
});
buttonContainer.appendChild(linesSelector);
// Create the API status display with detected language
const statusDisplay = document.createElement('div');
statusDisplay.style.fontSize = '12px';
statusDisplay.style.color = '#888888';
statusDisplay.textContent = `Status: Idle | Lang: ${selectedLanguage}`;
sidebar.appendChild(statusDisplay);
// Create the summary container
const summaryContainer = document.createElement('div');
summaryContainer.style.fontSize = '14px';
summaryContainer.style.lineHeight = '1.5';
summaryContainer.style.display = 'none';
summaryContainer.style.overflowY = 'auto';
summaryContainer.style.maxHeight = 'calc(100vh - 130px)';
sidebar.appendChild(summaryContainer);
// Variable to track if text is selected
let isTextSelected = false;
// Function to update the button text based on text selection
function updateButtonText() {
const selectedText = getSelectedText();
if (selectedText) {
summarizeButton.textContent = `Summarize [${selectedText.substring(0, 2).trim().replace(/^\n+/, '').trim()}..]`;
summarizeButton.style.fontSize = "14px";
isTextSelected = true;
} else if (isYouTubeVideoPage && ytTranscript!==null) {
summarizeButton.textContent = 'Summarize YT';
summarizeButton.style.fontSize = "14px";
isTextSelected = false;
} else {
summarizeButton.textContent = 'Summarize';
summarizeButton.style.fontSize = "14px";
isTextSelected = false;
}
}
// Listen for text selection changes on the document
document.addEventListener('mouseup', updateButtonText);
document.addEventListener('mousedown', () => {
setTimeout(updateButtonText, 100);
});
// Handle sidebar visibility
let isSidebarVisible = false;
toggleButton.addEventListener('click', function() {
if (isSidebarVisible) {
sidebar.style.right = '-300px';
toggleButton.style.right = '0';
toggleButton.textContent = '>';
} else {
sidebar.style.right = '0';
toggleButton.style.right = '300px';
toggleButton.textContent = '<';
}
isSidebarVisible = !isSidebarVisible;
});
// Listener for the summarize button
summarizeButton.addEventListener('click', function() {
summarizeButton.disabled = true;
summarizeButton.textContent = 'Loading...';
statusDisplay.style.color = '#888888';
statusDisplay.textContent = `Status: Requesting... | Lang: ${selectedLanguage}`;
summaryContainer.style.display = 'none';
let textToSummarize = '';
if (isYouTubeVideoPage && !isTextSelected && ytTranscript) {
textToSummarize = ytTranscript;
console.log("Summarizing YouTube transcript...");
} else if (isTextSelected) {
textToSummarize = getSelectedText();
console.log("Summarizing selected text...");
} else {
textToSummarize = getPageText();
console.log("Summarizing page text...");
}
const maxLines = parseInt(linesSelector.value, 10);
summarizePage(textToSummarize, selectedLanguage, maxLines, function(error, summary, statusCode) {
summarizeButton.disabled = false;
updateButtonText();
if (error) {
summaryContainer.textContent = 'Error: ' + error;
summaryContainer.style.color = '#ff4444';
statusDisplay.textContent = `Status: Failed${statusCode ? ` (${statusCode})` : ''} | Lang: ${selectedLanguage}`;
statusDisplay.style.color = '#ff4444';
} else {
summaryContainer.textContent = summary;
summaryContainer.style.color = '#ffffff';
statusDisplay.textContent = `Status: Success (${statusCode}) | Lang: ${selectedLanguage}`;
statusDisplay.style.color = '#00ff00';
}
summaryContainer.style.display = 'block';
});
});
})();