ChatGPT Local

Save and read ChatGPT history, user prompts locally. It use redis as a local server, so please start redis-server and webdis locally.

目前为 2023-09-15 提交的版本,查看 最新版本

// ==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';
            }
        }
    });




})();