c.AI Enhancements

Adds a save and download button with a format dropdown to character.AI, with widescreen support.

目前为 2024-09-10 提交的版本。查看 最新版本

// ==UserScript==
// @name         c.AI Enhancements
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Adds a save and download button with a format dropdown to character.AI, with widescreen support.
// @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 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 = '#ffffff';
        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 = '#000000';
        dropdown.style.fontFamily = 'sans-serif';
        dropdown.style.fontSize = '14px';
        dropdown.style.padding = '5px';
        saveChatButton.appendChild(dropdown);

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

        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.onclick = async function() {
            let format = prompt('Enter format (definition/names):', 'definition');
            if (format === 'definition' || format === 'names') {
                await saveAndDownloadChat(format);
            } else {
                alert('Invalid format. Please enter "definition" or "names".');
            }
        };
        dropdown.appendChild(downloadButton);

        return { saveChatButton, dropdown };
    }

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

    function makeDraggable(saveChatButton) {
        saveChatButton.onmousedown = function(event) {
            event.preventDefault();
            let shiftX = event.clientX - saveChatButton.getBoundingClientRect().left;
            let shiftY = event.clientY - saveChatButton.getBoundingClientRect().top;
            document.onmousemove = function(e) {
                saveChatButton.style.left = (e.clientX - shiftX) + 'px';
                saveChatButton.style.top = (e.clientY - shiftY) + 'px';
            };
            document.onmouseup = function() {
                localStorage.setItem('buttonTop', saveChatButton.style.top);
                localStorage.setItem('buttonLeft', saveChatButton.style.left);
                document.onmousemove = null;
                document.onmouseup = null;
            };
        };
    }

    function updateStyles(saveChatButton, dropdown) {
        const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
        saveChatButton.style.backgroundColor = isDarkMode ? '#333' : '#ff0000';
        saveChatButton.style.color = isDarkMode ? '#fff' : '#ffffff';
        dropdown.style.backgroundColor = isDarkMode ? '#333' : '#ffffff';
        dropdown.style.color = isDarkMode ? '#ffffff' : '#000000';
    }

    (function() {
        function WideScreen() {
            if (document.URL.startsWith("https://old.character.ai/chat")) {
                if (document.URL.includes("/chat2") || document.URL.includes("/chat")) {
                    document.body.getElementsByClassName("apppage").item(0).firstElementChild.attributes.style.value =
                        "height: 100%; display: flex; flex-direction: column; overflow-y: hidden; min-width: 300px; max-width: 7680; margin: 0px auto;";
                    document.getElementsByClassName("container-fluid chatbottom").item(0).attributes.item(1).value = "max-width: 7680;";
                }
                if (document.URL.includes("/chat")) {
                    document.getElementsByClassName("container-fluid  chattop").item(0).attributes.item(1).value = "max-width: 7680";
                }
            }

            if (document.URL.startsWith("https://character.ai/chat")) {
                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);
    })();

    var cai_version = -1;
    if(location.hostname === "old.character.ai")
        cai_version = 1;
    else if(location.pathname.startsWith("/chat/"))
        cai_version = 2;
    else
        return alert("Unsupported character.ai version");

    var token;
    if(cai_version === 1)
        token = JSON.parse(localStorage['char_token']).value;
    else if(cai_version === 2)
        token = JSON.parse(document.getElementById("__NEXT_DATA__").innerHTML).props.pageProps.token;

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

    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();

            json['turns'].forEach(turn => {
                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 getCharacterName(charid) {
        let json = await _fetchchats(charid);
        return json[0].character_name;
    }

    async function saveChat() {
        const chatElements = document.querySelectorAll('.prose.dark\\:prose-invert');
        let chatContent = '';
        chatElements.forEach(element => {
            chatContent += element.innerText + '\n';
        });
        const blob = new Blob([chatContent], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'chat.txt';
        a.click();
        URL.revokeObjectURL(url);
    }

    async function saveAndDownloadChat(format) {
        const charid = location.pathname.split("/")[2];
        let chats = await getChats(charid);
        let turns = [];
        for(let i = 0; i < chats.length; i++)
            turns = turns.concat(await getMessages(chats[i], format));
        let content = turns.map(turn => `${turn.author}: ${turn.message}`).join("\n\n");
        let filename = (await getCharacterName(charid)).replace(/ /g, "_") + ".txt";
        let blob = new Blob([content], { type: 'text/plain' });
        let url = URL.createObjectURL(blob);
        let a = document.createElement('a');
        a.href = url;
        a.download = filename;
        a.click();
        URL.revokeObjectURL(url);
    }

    function params(parameterName) {
        var result = null,
            tmp = [];
        location.search
            .substr(1)
            .split("&")
            .forEach(function (item) {
              tmp = item.split("=");
              if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
            });
        return result;
    }

    function init() {
        const { saveChatButton, dropdown } = createSaveButton();
        makeDraggable(saveChatButton);
        updateStyles(saveChatButton, dropdown);
        window.matchMedia('(prefers-color-scheme: dark)').addListener(() => updateStyles(saveChatButton, dropdown));
        saveChatButton.addEventListener('click', () => toggleDropdown(dropdown));
    }

    init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址