CAI Toolbar

Adds a toolbar with auto-scroll (including auto resending after 500 errrors), copy last message (adding bold and italics markdown), and remove last message.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         CAI Toolbar
// @namespace    https://sleazyfork.org/en/users/1033554-thud-butt
// @version      1.0
// @description  Adds a toolbar with auto-scroll (including auto resending after 500 errrors), copy last message (adding bold and italics markdown), and remove last message.
// @author       Thud Butt
// @match        https://beta.character.ai/*
// @icon         https://characterai.io/static/logo512.png
// @license      MIT
// ==/UserScript==
'use strict';

const enabledColor = '#009933';
const disabledColor = '#ff9900';


// Keep consistent state between page refreshes, unless we're coming from a 500 refresh
let autoScrollEnabled = (sessionStorage.getItem('auto-scroll') !== null) ? JSON.parse(sessionStorage.getItem('auto-scroll')) : false;
let autoScrollInterval = null;

loadToolbar();

setAutoScrollState(autoScrollEnabled);


function loadToolbar()
{
    let customStyle = document.createElement('style');
    customStyle.innerHTML = `
    div.custom-toolbar {
    background-color: rgb(37, 37, 37);
    border-right: 1px solid rgb(15, 15, 15) !important;
    border-bottom: 1px solid rgb(15, 15, 15) !important;
    border-left: 1px solid rgb(15, 15, 15) !important;
    border-radius: 0px 0px 10px 10px;
    width: 200px;
    height: 50px;
    position: sticky;
    top: 0;
    margin-left: auto;
    margin-right: auto;
    z-index: 1000;
    display: flex;
    justify-content: space-around;
    }
    `;

    document.body.prepend(customStyle);

    // TOOLBAR
    let divCustomToolbar = document.createElement('div');
    divCustomToolbar.classList.add('custom-toolbar');


    // AUTO SCROLL BUTTON
    let buttonAutoScroll = document.createElement('button');
    buttonAutoScroll.id = 'button-auto-scroll';
    buttonAutoScroll.innerHTML = `
    <svg width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="${autoScrollEnabled ? enabledColor : disabledColor}" d="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192V352c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448H352c88.4 0 160-71.6 160-160z"/></svg>
    `;
    buttonAutoScroll.type = 'button';
    buttonAutoScroll.title = 'Auto-scroll'
    buttonAutoScroll.classList.add('btn');
    buttonAutoScroll.onclick = function()
    {
        autoScrollEnabled = !autoScrollEnabled;
        setAutoScrollState(autoScrollEnabled)
    };
    divCustomToolbar.append(buttonAutoScroll);
    document.body.prepend(divCustomToolbar);


    // COPY LAST MESSAGE BUTTON
    let buttonCopy = document.createElement('button');
    buttonCopy.id = 'button-copy-last';
    buttonCopy.innerHTML = `
    <svg width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#ededed" d="M224 0c-35.3 0-64 28.7-64 64V288c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H224zM64 160c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H288c35.3 0 64-28.7 64-64V384H288v64H64V224h64V160H64z"/></svg>
    `;
    buttonCopy.type = 'button';
    buttonCopy.title = 'Copy last message'
    buttonCopy.classList.add('btn');
    buttonCopy.onclick = function()
    {
        let lastMessage = document.querySelector('div.chatdisplay div.user-msg:last-of-type p').innerHTML;
        lastMessage = lastMessage.replace(/<\/?em[^>]*>/g, '*');
        lastMessage = lastMessage.replace(/<\/?strong[^>]*>/g, '**');
        navigator.clipboard.writeText(lastMessage);
    };
    divCustomToolbar.append(buttonCopy);

    // REMOVE LAST MESSAGE BUTTON
    let buttonRemoveLast = document.createElement('button');
    buttonRemoveLast.id = 'button-remove-last';
    buttonRemoveLast.innerHTML = `
    <svg width="30px" height="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#ededed" d="M258.7 57.4L25.4 290.7c-25 25-25 65.5 0 90.5l80 80c12 12 28.3 18.7 45.3 18.7H256h9.4H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H355.9L486.6 285.3c25-25 25-65.5 0-90.5L349.3 57.4c-25-25-65.5-25-90.5 0zM265.4 416H256l-105.4 0-80-80L195.3 211.3 332.7 348.7 265.4 416z"/></svg>
    `;
    buttonRemoveLast.type = 'button';
    buttonRemoveLast.title = 'Remove last message'
    buttonRemoveLast.classList.add('btn');
    buttonRemoveLast.onclick = function()
    {
        document.querySelector('div.chattop span[data-toggle="dropdown"]').click();
        document.querySelector('div.dropdown-menu.show button:nth-of-type(4n)').click();
        document.querySelector('div.chatdisplay input[type="checkbox"]:last-of-type').click();
        document.querySelector('div.chatfooter button.btn-danger').click();
    };
    divCustomToolbar.append(buttonRemoveLast);


    document.body.prepend(divCustomToolbar);
}

// Autoscrolling
function startAutoScroll()
{
    waitForElement("div[class='swiper-button-next']", 1000).then(function()
    {
        for(let step = 0; step < 5; step++)
        {
            arrowRight();
        };
    })
    .catch(() => {});

    // Clicks the Try Again button
    waitForElement('.Toastify__toast--default', 1000).then(function()
    {
        document.querySelector('.Toastify__toast--default .btn-primary').click();
    })
    .catch(() => {});

    // If there has been a 500 error, then we need to reload the page, remove our last message, and resend it.
    waitForElement('.Toastify__toast--error', 1000).then(function()
    {
        document.getElementById('user-input').closest('div').querySelector('button:nth-child(2)').click();
    })
    .catch(() => {});

    // Add message count
    waitForElement('div.msg-row', 30000).then(function()
    {
        addNumber();

        let messageNumberObserverRow = new MutationObserver(addNumber());

        messageNumberObserverRow.observe(document.querySelector('div.swiper-wrapper'), {childList: true});

        let messageNumberObserverCol = new MutationObserver(function()
        {
            messageNumberObserverRow.observe(document.querySelector('div.swiper-wrapper'), {childList: true});
        });

        messageNumberObserverCol.observe(document.querySelector('div.infinite-scroll-component'), {childList: true});
    })
    .catch(() => {});
}

function setAutoScrollState(state)
{
    // Toggle the auto retry button
    if(state)
    {
        document.querySelector('#button-auto-scroll path').setAttribute('fill', disabledColor);
        clearInterval(autoScrollInterval);
    }
    else
    {
        document.querySelector('#button-auto-scroll path').setAttribute('fill', enabledColor);
        autoScrollInterval = setInterval(startAutoScroll, 1000);
    }

    sessionStorage.setItem('auto-scroll', autoScrollEnabled);
}

// Timed MutationObserver
function waitForElement(querySelector, timeout)
{
    return new Promise((resolve, reject) =>
    {
        let timer = false;

        if(document.querySelectorAll(querySelector).length) return resolve();

        const observer = new MutationObserver(() =>
        {
            if(document.querySelectorAll(querySelector).length)
            {
                observer.disconnect();
                if(timer !== false) clearTimeout(timer);
                return resolve();
            }
        });

        observer.observe(document.body, {childList: true, subtree: true});

        if(timeout)
        {
            timer = setTimeout(function()
            {
                observer.disconnect();
                reject();
            }, timeout);
        }
    });
}

// Arrow right and arrow left keypresses for "swiping"
function arrowRight()
{
    document.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, key: 'ArrowRight'}));
}

function arrowLeft()
{
    document.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, key: 'ArrowLeft'}));
}

function addNumber()
{
    let messageNumber = document.querySelectorAll('.swiper-wrapper .swiper-slide .rounded .flex-column');

    for (let i = 0; i < messageNumber.length; i++)
    {
        messageNumber[i].innerHTML = messageNumber[i].innerHTML.replace(messageNumber[i].innerHTML, 'c.AI | ' + (i + 1));
    }
}