// ==UserScript==
// @name ChatGPT Local
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Save and read ChatGPT history, user prompts locally. It use redis as a local server, so please start redis-server and webdis locally.
// @author You
// @match https://chat.openai.com/*
// @grant GM_xmlhttpRequest
// @license GPL-3.0-or-later
// ==/UserScript==
(function() {
'use strict';
const WEBREDIS_ENDPOINT = "http://127.0.0.1:7379";
var redisname = "";
var thisInHistory = false;
var HistoryPrefix = "HISTORY";
var PromptPrefix = "USERPROMPT";
/*---------------------------------History-------------------------------------*/
function load(key, callback) {
key = encodeURIComponent(key);
GM_xmlhttpRequest({
method: "GET",
url: `${WEBREDIS_ENDPOINT}/GET/${key}`,
onload: function(response) {
callback(null, JSON.parse(response.responseText));
},
onerror: function(error) {
callback(error, null);
}
});
}
function save(nameprefix, data, callback) {
var strdata;
var dname;
var currentTimestamp = "";
if (Array.isArray(data)) {// history
strdata = JSON.stringify(data.map(function(element) {
return element.innerHTML;
}));
dname = data[0].innerText.substring(0, 50).trim();
var date = new Date();
currentTimestamp = date.toLocaleString().substring(0, 19);
} else {//prompt
strdata = JSON.stringify(data);
dname = data.substring(0, 50).trim();
}
if (strdata && strdata.length < 3){
return;
}
if (redisname == ""){
redisname = encodeURIComponent(nameprefix + currentTimestamp + "\n" + dname);
console.log(redisname);
}
GM_xmlhttpRequest({
method: "GET",
url: `${WEBREDIS_ENDPOINT}/SET/${redisname}/${encodeURIComponent(strdata)}`,
onload: function(response) {
console.log(response);
callback(null, response.responseText);
},
onerror: function(error) {
console.log(error);
callback(error, null);
}
});
}
function remove(key, callback) {
key = encodeURIComponent(key);
GM_xmlhttpRequest({
method: "GET",
url: `${WEBREDIS_ENDPOINT}/DEL/${key}`,
onload: function(response) {
callback(null, JSON.parse(response.responseText));
},
onerror: function(error) {
callback(error, null);
}
});
}
function getAllRedisKeys(callback) {
GM_xmlhttpRequest({
method: "GET",
url: `${WEBREDIS_ENDPOINT}/KEYS/*`, // Update this to your actual endpoint
onload: function(response) {
if (response.responseText == undefined){
redisError();
return;
}
callback(null, JSON.parse(response.responseText));
},
onerror: function(error) {
callback(error, null);
}
});
}
function getCurrentDialogContent() {
// Get all div elements with the specified class
const divsWithSpecificClass = document.querySelectorAll('div.flex.flex-grow.flex-col.gap-3.max-w-full');
// Create an array to store the text content of each div
const divTexts = [];
// Loop through the NodeList and get the text content of each div
divsWithSpecificClass.forEach(div => {
var textContent = [];
/*div.querySelectorAll('*').forEach(child => {
if (child.children.length === 0) {
textContent.push(child.textContent);
if(child.tagName === 'P' || child.tagName === 'DIV') {
textContent.push("\n");
}
}
});
divTexts.push(textContent.join(""));*/
//divTexts.push(div.innerText);
divTexts.push(div);
});
// Return the array of text contents
return divTexts;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function showHistory(dataList) {
var targetDiv = document.querySelector('div.flex-1.overflow-hidden > div > div > div');
/*while(targetDiv.firstChild) {
targetDiv.removeChild(targetDiv.firstChild);
}*/
dataList.forEach(data => {
var newDiv = document.createElement('div');
newDiv.textContent = data;
targetDiv.appendChild(newDiv);
});
}
function makeAGroup(name, keys, elementfilter, clickcallback){
const ul = document.createElement('ul');
ul.style.overflowY = 'auto';
ul.style.maxHeight = '500px';
var eid = "myUl" + name
ul.id = eid; // Setting an ID to the ul element
var h2 = document.createElement('h2');
h2.innerText = name;
h2.style.color = 'white';
h2.style.textAlign = "center";
ul.append(h2);
for (let i = 0; i < keys.length; i++) {
const li = document.createElement('li');
if(!keys[i].startsWith(elementfilter)){
continue;
}
li.textContent = keys[i].substring(elementfilter.length, keys[i].length);
li.style.color = 'white';
// Apply CSS styles for the rounded rectangle
li.style.border = '1px solid white'; // Add a border
li.style.borderRadius = '10px'; // Adjust the horizontal and vertical radii to create rounded corners
li.style.padding = '5px'; // Add some padding to make it visually appealing
li.style.position = 'relative';
li.addEventListener('mouseenter', function() {
li.style.backgroundColor = 'grey'; // Change to your desired background color
});
li.addEventListener('mouseleave', function() {
li.style.backgroundColor = ''; // Reset to the original background color
});
li.addEventListener('click', (event) => {clickcallback(event, keys[i]);});
// add close
// Create close button
const closeButton = document.createElement('span');
closeButton.textContent = '✖';
closeButton.style.position = 'absolute';
closeButton.style.top = '5px';
closeButton.style.right = '5px';
closeButton.style.color = 'white';
closeButton.style.cursor = 'pointer'; // Set cursor to pointer to indicate it's clickable
// Add event listener for the close button
closeButton.addEventListener('click', async (event) => {
// Your callback function here
event.stopPropagation();
remove(keys[i], function (){});
await sleep(500);
InitPanel();
});
// Add close button to li
li.appendChild(closeButton);
ul.appendChild(li);
}
return ul;
}
async function InitPanel() {
getAllRedisKeys(function(error, data) {
if (error) {
redisError();
console.error('An error occurred:', error);
return;
}
const ol = document.querySelectorAll('ol')[2];
var div = document.querySelectorAll('div.flex-shrink-0.overflow-x-hidden')[0];
const ulExisting = document.getElementById('myUlHistory');
if (ulExisting) {
div.removeChild(ulExisting, function (){});
}
if (data.KEYS.length == 0){
redisError();
}
var ul = makeAGroup("History", data.KEYS.sort().reverse(), HistoryPrefix, function(event, key) {
//console.log('Item clicked:', data.KEYS[i]);
// Load data after saving
load(key, function(err, data) {
if (err) return console.error(err);
var myList = JSON.parse(data.GET);
if (Array.isArray(myList)){
for (let i = 0; i < myList.length; i++) {
if (i % 2 == 0) {
myList[i] = "👨: " + myList[i].replace(/\n/g, '<br>');
} else {
myList[i] = "🤖: " + myList[i].replace(/\n/g, '<br>');
}
}
showHistoryLog(myList.join("<br>"));
}
});
});
div.prepend(ul);
/*---Prompt---*/
var ulPrompt = document.getElementById('myUlPrompt');
if (ulPrompt) {
div.removeChild(ulPrompt);
}
var prompt = makeAGroup("Prompt", data.KEYS.sort().reverse(), PromptPrefix, function(event, key) {
//console.log('Item clicked:', data.KEYS[i]);
// Load data after saving
load(key, function(err, data) {
if (err) return console.error(err);
var prompt = JSON.parse(data.GET);
var textarea = document.getElementById('prompt-textarea');
textarea.value = prompt;
});
});
div.prepend(prompt);
if (!ulPrompt) {
var button = document.createElement('button');
button.innerText = 'Save Message As Prompt';
button.style.color = 'white';
button.style.position = 'relative';
button.style.textAlign = 'center';
button.style.border = '1px solid white';
button.style.backgroundColor = '#268BD2';
var bottomdiv = document.querySelectorAll('div.relative.pb-3.pt-2.text-center.text-xs.text-gray-600')[0];
bottomdiv.appendChild(button);
button.addEventListener('click', function() {
var textarea = document.getElementById('prompt-textarea');
save(PromptPrefix, textarea.textContent, function(err, response) {
if (err) return console.error(err);
});
InitPanel();
});
}
});
/*Remote Offical*/
await sleep(2000);
const olElements = document.querySelectorAll('ol');
olElements.forEach(ol => {
// First remove all existing children
while (ol.firstChild) {
ol.removeChild(ol.firstChild);
}
});
}
function redisError(){
var div = document.querySelectorAll('div.flex-shrink-0.overflow-x-hidden')[0];
const ul = document.createElement('ul');
div.prepend(ul);
const li = document.createElement('li');
li.textContent = "There is no record. Please verify if webdis AND redis-server has been started! Just run `webdis.sh start` to start.";
li.style.color = 'white';
ul.appendChild(li);
}
function showHistoryLog(text) {
// Check if the div with a specific id already exists
var existingDiv = document.getElementById('historyLog');
if (existingDiv) {
// If the div exists, update the messageSpan's HTML content
var messageSpan = existingDiv.querySelector('.message-span');
if (messageSpan) {
messageSpan.innerHTML = text;
}
existingDiv.style.display = '';
} else {
// If the div doesn't exist, create a new div and append it to the body
var hoverBox = document.createElement('div');
hoverBox.id = 'historyLog'; // Set a unique id for the div
hoverBox.style.position = 'fixed';
hoverBox.style.top = '50%';
hoverBox.style.left = '50%';
hoverBox.style.transform = 'translate(-50%, -50%)';
hoverBox.style.zIndex = '10000';
hoverBox.style.padding = '10px';
hoverBox.style.width = '1000px';
hoverBox.style.height = '800px';
hoverBox.style.backgroundColor = 'white';
hoverBox.style.border = '1px solid black';
hoverBox.style.borderRadius = '5px';
hoverBox.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.5)';
hoverBox.style.overflow = 'hidden'; // Hide content overflow
// Create a container div for the content and close button
var contentContainer = document.createElement('div');
contentContainer.style.overflowY = 'auto'; // Make content scrollable
//contentContainer.style.resize = 'both'; // Enable resizing
contentContainer.style.height = 'calc(100% - 40px)'; // Adjust height for close button
// Create a span element to hold the message
var messageSpan = document.createElement('span');
messageSpan.innerHTML = text;
messageSpan.className = 'message-span'; // Add a class for easy selection
messageSpan.style.display = 'block';
messageSpan.style.marginTop = '20px';
// Create a button element to close the hover box
var closeButton = document.createElement('button');
closeButton.textContent = '✖';
closeButton.style.position = 'absolute';
closeButton.style.top = '10px';
closeButton.style.right = '10px';
closeButton.addEventListener('click', function () {
hoverBox.style.display = 'none';
});
// Add the message span and close button to the content container
contentContainer.appendChild(messageSpan);
contentContainer.appendChild(closeButton);
// Add the content container to the hover box
hoverBox.appendChild(contentContainer);
document.addEventListener('click', function (event) {
if (!hoverBox.contains(event.target) && event.target !== hoverBox) {
hoverBox.style.display = 'none';
}
});
// Add the hover box to the body of the document
document.body.appendChild(hoverBox);
}
}
InitPanel();
document.addEventListener('keydown', async function(event) {
if (event.key === 'Enter' || (event.metaKey && event.key === 'r')) {
// Usage examples
while(true){
var thisdialog = getCurrentDialogContent();
save(HistoryPrefix, thisdialog, function(err, response) {
if (err) return console.error(err);
console.log('Save response:', response);
if(!thisInHistory){
InitPanel();
thisInHistory = true;
}
});
const divElement = document.querySelector('div.flex.items-center.md\\:items-end');
if (divElement == undefined || divElement.textContent == undefined || divElement.textContent != 'Stop generating'){
break;
}
await sleep(2000);
}
}
if (event.key === 'Escape') {
var existingDiv = document.getElementById('historyLog');
if (existingDiv) {
existingDiv.style.display = 'none';
}
}
});
})();