Chat Enhancements

Adds a download button, saves the chat to local storage, and enables widescreen mode.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Chat Enhancements
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Adds a download button, saves the chat to local storage, and enables widescreen mode.
// @author       InariOkami
// @match        https://character.ai/*
// @grant        none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=character.ai
// ==/UserScript==

(async function() {
    'use strict';

    (function() {
        function WideScreen() {
            var Chat = document.getElementsByClassName("overflow-x-hidden overflow-y-scroll px-1 flex flex-col-reverse min-w-full hide-scrollbar").item(0).children;
            for (var i = 0; i < Chat.length; i++) {
                Chat.item(i).style = "min-width:100%";
                document.getElementsByClassName("flex w-full flex-col max-w-2xl").item(0).style = "min-width:100%";
            }
        }
        setTimeout(() => { setInterval(WideScreen, 100); }, 1000);
    })();

    function createSaveButton() {
        const saveChatButton = document.createElement('button');
        saveChatButton.innerHTML = 'Chat Options ▼';
        saveChatButton.style.position = 'fixed';
        saveChatButton.style.top = localStorage.getItem('buttonTop') || '10px';
        saveChatButton.style.left = localStorage.getItem('buttonLeft') || '10px';
        saveChatButton.style.backgroundColor = '#ff0000';
        saveChatButton.style.color = '#ffffff';
        saveChatButton.style.padding = '10px';
        saveChatButton.style.borderRadius = '5px';
        saveChatButton.style.cursor = 'pointer';
        saveChatButton.style.zIndex = '1000';
        saveChatButton.style.border = 'none';
        saveChatButton.style.boxShadow = '0px 2px 5px rgba(0,0,0,0.2)';
        document.body.appendChild(saveChatButton);

        const dropdown = document.createElement('div');
        dropdown.style.display = 'none';
        dropdown.style.position = 'absolute';
        dropdown.style.top = '100%';
        dropdown.style.left = '0';
        dropdown.style.backgroundColor = '#333';
        dropdown.style.border = '1px solid #ccc';
        dropdown.style.boxShadow = '0px 2px 5px rgba(0,0,0,0.2)';
        dropdown.style.zIndex = '1001';
        dropdown.style.color = '#ffffff';
        dropdown.style.fontFamily = 'sans-serif';
        dropdown.style.fontSize = '14px';
        dropdown.style.padding = '5px';
        dropdown.style.cursor = 'pointer';
        dropdown.style.maxWidth = '200px';
        dropdown.style.borderRadius = '5px';
        saveChatButton.appendChild(dropdown);

        const downloadButton = document.createElement('button');
        downloadButton.innerHTML = 'Download Chat';
        downloadButton.style.display = 'block';
        downloadButton.style.width = '100%';
        downloadButton.style.border = 'none';
        downloadButton.style.padding = '10px';
        downloadButton.style.cursor = 'pointer';
        downloadButton.style.backgroundColor = '#444';
        downloadButton.style.color = '#ffffff';
        downloadButton.style.borderRadius = '5px';
        dropdown.appendChild(downloadButton);

        const saveLocalButton = document.createElement('button');
        saveLocalButton.innerHTML = 'Save to Local Storage';
        saveLocalButton.style.display = 'block';
        saveLocalButton.style.width = '100%';
        saveLocalButton.style.border = 'none';
        saveLocalButton.style.padding = '10px';
        saveLocalButton.style.cursor = 'pointer';
        saveLocalButton.style.backgroundColor = '#444';
        saveLocalButton.style.color = '#ffffff';
        saveLocalButton.style.borderRadius = '5px';
        dropdown.appendChild(saveLocalButton);

        saveChatButton.onclick = function() {
            dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
        };

        let offsetX, offsetY;
        let isDragging = false;

        saveChatButton.addEventListener('mousedown', function(e) {
            isDragging = true;
            offsetX = e.clientX - saveChatButton.getBoundingClientRect().left;
            offsetY = e.clientY - saveChatButton.getBoundingClientRect().top;
        });

        document.addEventListener('mousemove', function(e) {
            if (isDragging) {
                saveChatButton.style.left = e.clientX - offsetX + 'px';
                saveChatButton.style.top = e.clientY - offsetY + 'px';
                localStorage.setItem('buttonTop', saveChatButton.style.top);
                localStorage.setItem('buttonLeft', saveChatButton.style.left);
            }
        });

        document.addEventListener('mouseup', function() {
            isDragging = false;
        });

        return { saveChatButton, dropdown, downloadButton, saveLocalButton };
    }

    async function fetchAndDownloadChat() {
        var token = JSON.parse(document.getElementById("__NEXT_DATA__").innerHTML).props.pageProps.token;
        var _cache;

        async function _fetchchats(charid) {
            if (!_cache) {
                let url = 'https://neo.character.ai/chats/recent/' + charid;
                let response = await fetch(url, { headers: { "Authorization": `Token ${token}` } });
                let json = await response.json();
                _cache = json['chats'];
            }
            return _cache;
        }

        async function getChats(charid) {
            let json = await _fetchchats(charid);
            return json.map(chat => chat.chat_id);
        }

        async function getMessages(chat, format) {
            let url = 'https://neo.character.ai/turns/' + chat + '/';
            let next_token = null;
            let turns = [];

            do {
                let url2 = url;
                if (next_token) url2 += "?next_token=" + encodeURIComponent(next_token);
                let response = await fetch(url2, { headers: { "Authorization": `Token ${token}` } });
                let json = await response.json();
                for (let turn of json['turns']) {
                    let o = {};
                    o.author = format === "definition" ? (turn.author.is_human ? "{{user}}" : "{{char}}") : turn.author.name;
                    o.message = turn.candidates.find(x => x.candidate_id === turn.primary_candidate_id).raw_content || "";
                    turns.push(o);
                }
                next_token = json['meta']['next_token'];
            } while (next_token);
            return turns.reverse();
        }

        async function downloadChat(format) {
            let charid = prompt('Enter character ID:');
            let chats = await getChats(charid);
            let messages = await getMessages(chats[0], format);
            let content = messages.map(msg => `${msg.author}: ${msg.message}`).join('\n');
            let blob = new Blob([content], { type: 'text/plain' });
            let link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = `chat_${charid}.txt`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }

        function saveChatToLocalStorage() {
            let charid = prompt('Enter character ID:');
            getChats(charid).then(chats => {
                if (chats.length > 0) {
                    getMessages(chats[0], "definition").then(messages => {
                        const chatData = {
                            characterID: charid,
                            messages: messages
                        };
                        localStorage.setItem(`chat_${charid}`, JSON.stringify(chatData));
                        alert(`Chat saved to local storage as "chat_${charid}".`);
                    });
                } else {
                    alert("No chats found for this character ID.");
                }
            });
        }

        const dialog = document.createElement('dialog');
        dialog.innerHTML = `
            <form method="dialog">
                <p>Select format:</p>
                <label><input type="radio" name="format" value="definition" checked> Definition ({{user}}/{{char}})</label><br>
                <label><input type="radio" name="format" value="names"> Names (You/Bot)</label><br>
                <button type="submit">Download</button>
            </form>
        `;
        dialog.addEventListener('close', () => {
            const format = dialog.querySelector('input[name="format"]:checked').value;
            downloadChat(format);
        });
        document.body.appendChild(dialog);

        let { downloadButton, saveLocalButton } = createSaveButton();
        downloadButton.onclick = function() {
            dialog.showModal();
        };

        saveLocalButton.onclick = function() {
            saveChatToLocalStorage();
        };
    }

    fetchAndDownloadChat();
})();