Greasy Fork is available in English.

Auto Scroll Chat Enhancer

Streamline your character.ai chat experience with the Auto Scroll Chat Enhancer! This script features a control panel with "Start" and "Stop" for key press simulations and an "Auto Scroll" toggle. Activate "Auto Scroll" to automatically view the latest messages, keeping you in the conversation flow seamlessly. Perfect for users seeking an efficient chatting experience.

// ==UserScript==
// @name         Auto Scroll Chat Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.2.1
// @description  Streamline your character.ai chat experience with the Auto Scroll Chat Enhancer! This script features a control panel with "Start" and "Stop" for key press simulations and an "Auto Scroll" toggle. Activate "Auto Scroll" to automatically view the latest messages, keeping you in the conversation flow seamlessly. Perfect for users seeking an efficient chatting experience.
// @author       anonymous
// @match        https://character.ai/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=character.ai
// @grant        none
// @license      MIT
// @note         This script's code is mostly generated by AI.
// ==/UserScript==

(function () {
    "use strict";

    let langText = navigator.language || navigator.userLanguage; //常规浏览器语言和IE浏览器
    langText = langText.substr(0, 2); //截取lang前2位字符

    // 自动滚动状态变量
    let autoScrollEnabled = true;
    const markedItems = new Set(); // 用于记录已标记的 box-item 索引

    const lang = {
        开始滚动: "Scroll",
        停止滚动: "Stop",
        自动滚动: "Auto Scroll",
        隐藏面板: "Hide",
        显示面板: "Show",
    };

    function lan(key) {
        if (langText === "zh") return key;
        return lang[key];
    }

    let isChat = window.location.href.includes("/chat/");

    // 控制面板设置
    const controlPanel = document.createElement("div");
    controlPanel.style.position = "fixed";
    controlPanel.style.top = "0";
    controlPanel.style.left = "0";
    controlPanel.style.zIndex = "10000";
    controlPanel.style.display = "flex";
    controlPanel.style.gap = "10px"; // 按钮间距
    if (!isChat) controlPanel.style.display = "none";
    document.body.appendChild(controlPanel);

    const startButton = document.createElement("button");
    startButton.textContent = lan("开始滚动");
    controlPanel.appendChild(startButton);

    const stopButton = document.createElement("button");
    stopButton.textContent = lan("停止滚动");
    controlPanel.appendChild(stopButton);

    // 创建自动滚动控制按钮
    const autoScrollButton = document.createElement("button");
    autoScrollButton.textContent = `${lan("自动滚动")}: ${autoScrollEnabled ? "on" : "off"}`;
    controlPanel.appendChild(autoScrollButton);

    function enableAutoScroll() {
        autoScrollEnabled = true;
        autoScrollButton.textContent = `${lan("自动滚动")}: on`;
        scrollToBottom();
    }

    function disableAutoScroll() {
        autoScrollEnabled = false;
        autoScrollButton.textContent = `${lan("自动滚动")}: off`;
    }

    // 创建显示/隐藏按钮
    const toggleVisibilityButton = document.createElement("button");
    toggleVisibilityButton.textContent = lan("隐藏面板");
    controlPanel.appendChild(toggleVisibilityButton);

    function showBoxPanel() {
        if (box.style.display !== "none") return;
        box.style.display = "block";
        toggleVisibilityButton.textContent = lan("隐藏面板");
        document.head.appendChild(styleDom);
    }

    function hideBoxPanel() {
        if (box.style.display === "none") return;
        box.style.display = "none";
        toggleVisibilityButton.textContent = lan("显示面板");
        document.head.removeChild(styleDom);
    }

    // 控制box的显示/隐藏状态
    function toggleBoxVisibility() {
        if (box.style.display === "none") {
            showBoxPanel();
        } else {
            hideBoxPanel();
        }
    }

    (function (history) {
        var pushState = history.pushState;
        var replaceState = history.replaceState;

        history.pushState = function (state) {
            if (typeof history.onpushstate == "function") {
                history.onpushstate({ state: state });
            }
            return pushState.apply(history, arguments);
        };

        history.replaceState = function (state) {
            if (typeof history.onreplacestate == "function") {
                history.onreplacestate({ state: state });
            }
            return replaceState.apply(history, arguments);
        };
    })(window.history);

    window.onpopstate =
        history.onpushstate =
        history.onreplacestate =
            function (event) {
                if (event?.state?.as?.startsWith?.("/chat/")) {
                    controlPanel.style.display = "flex";
                    isChat = true;
                    showBoxPanel();
                } else {
                    controlPanel.style.display = "none";
                    isChat = false;
                    hideBoxPanel();
                }
            };

    toggleVisibilityButton.addEventListener("click", toggleBoxVisibility);

    // 将自动滚动的控制抽象成单独的方法
    function toggleAutoScroll() {
        if (autoScrollEnabled) {
            disableAutoScroll();
        } else {
            enableAutoScroll();
        }
    }

    autoScrollButton.addEventListener("click", toggleAutoScroll);

    // 滚动到底部的功能
    function scrollToBottom() {
        if (!autoScrollEnabled) return;
        Array.from(box.children).pop()?.scrollIntoView(false);
    }

    // 添加样式
    const styleDom = document.createElement("style");
    styleDom.type = "text/css";
    styleDom.innerHTML = `
        .swiper-no-swiping.box-item {
            background-color: var(--surface-elevation-2);
            margin: 0 5px 15px 8px;
            border-radius: 15px;
            padding: 5px 10px;
            cursor: pointer; /* 添加手形光标表示可点击 */
            position: relative;
        }
        div[data-test-id="virtuoso-item-list"] {
            width: 50%;
            float: right;
        }
        .size-full > .items-center {
            align-items: flex-end;
        }
        aside {
            display: none;
        }
        *,
        *::before,
        *::after {
            animation-duration: .01s !important;
            transition-duration: .01s !important;
        }

        .tidot {
            animation: none;
        }

        .group {
            margin-right: 0;
        }

        .swiper-no-swiping.box-item > div:nth-child(3) {
            padding: 0;
            margin: 0;
            opacity: 0.5;
            overflow: hidden;
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: #000000;
            border-radius: 15px;
            color: transparent;
        }
        .swiper-no-swiping.box-item > div:nth-child(3) > * {
            opacity: 0;
        }

        .border-error {
            height: 0;
            overflow: hidden;
            padding: 0;
            margin: 0;
            opacity: 0.2;
        }
        .marked {
            background-color: #ffffff40 !important; /* 标记颜色 */
        }
`;
    if (isChat) document.head.appendChild(styleDom);

    // 模拟按键操作
    function simulateKeyPress(keyCode) {
        let event = document.createEvent("Event");
        event.initEvent("keydown", true, false);
        event = Object.assign(event, {
            ctrlKey: false,
            metaKey: false,
            altKey: false,
            which: keyCode,
            keyCode: keyCode,
            key: keyCode === 37 ? "ArrowLeft" : "ArrowRight",
            code: keyCode === 37 ? "ArrowLeft" : "ArrowRight",
        });
        document.dispatchEvent(event);
    }

    let intervalId;

    // 点击开始按钮
    startButton.addEventListener("click", () => {
        console.log("开始模拟按键");
        start(false);
    });

    function start(clear = true) {
        if (intervalId) clearInterval(intervalId);
        document.querySelector("textarea").focus();
        intervalId = setInterval(() => simulateKeyPress(39), 40);
        enableAutoScroll(); // 开启自动滚动
        if (clear) markedItems.clear();
    }

    // 点击停止按钮
    stopButton.addEventListener("click", () => {
        console.log("停止模拟按键");
        stop();
    });

    function stop() {
        clearInterval(intervalId);
        disableAutoScroll(); // 关闭自动滚动
    }

    // 显示内容的box
    const box = document.createElement("div");
    box.style.position = "fixed";
    box.style.top = "50px";
    box.style.left = "0";
    box.style.width = "45%";
    box.style.height = "94vh";
    box.style.overflow = "scroll";
    document.body.appendChild(box);

    // 设置一个容忍区间值,用于判定是否接近底部
    const scrollThreshold = 30;

    // 添加滚动事件监听器
    box.addEventListener("scroll", () => {
        const isNearBottom = box.scrollHeight - box.scrollTop - box.clientHeight <= scrollThreshold;

        if (!isNearBottom) {
            if (autoScrollEnabled) {
                disableAutoScroll();
            }
        } else if (!autoScrollEnabled) {
            toggleAutoScroll();
        }
    });

    let cache = "";
    // 更新box中的内容以匹配swiper-wrapper的当前状态
    function updateBoxContent() {
        const swiperWrapper = document.querySelector(".swiper-wrapper");
        if (swiperWrapper) {
            const nowCacheValue = swiperWrapper.innerText;
            if (nowCacheValue === cache) return;
            cache = nowCacheValue;
            if (swiperWrapper.children.length < 3) {
                start();
            }
            updateBoxChildren(swiperWrapper); // 调用新的方法更新内容
            // 当自动滚动开启时,自动滚动到最新内容
            if (autoScrollEnabled) {
                scrollToBottom();
            }
            if (document.querySelector(".swiper-slide-active p").textContent.startsWith("31")) {
                playBeep();
                stop();
                simulateKeyPress(37); // 按下左箭头
            }
        }
    }

    function updateBoxChildren(swiperWrapper) {
        // Create a virtual DOM
        const virtualDOM = document.createDocumentFragment();
        const elements = swiperWrapper.querySelectorAll(".swiper-no-swiping");

        elements.forEach((element, index) => {
            const clone = element.cloneNode(true);
            clone.classList.add("box-item");

            // Add event listeners for marking and double-click navigation
            clone.addEventListener("dblclick", async () => {
                stop();
                const a = Array.from(box.children).indexOf(clone);
                const b = Array.from(swiperWrapper.children).indexOf(document.querySelector(".swiper-wrapper > .swiper-slide-active"));
                const difference = a - b;
                console.error(a, b);
                if (difference < 0) {
                    for (let i = 0; i < Math.abs(difference); i++) {
                        simulateKeyPress(37); // 按下左箭头
                        await new Promise(requestAnimationFrame);
                    }
                } else {
                    for (let i = 0; i < difference; i++) {
                        simulateKeyPress(39); // 按下右箭头
                        await new Promise(requestAnimationFrame);
                    }
                }
            });

            clone.addEventListener("click", () => {
                disableAutoScroll();
                clone.classList.toggle("marked"); // 标记或取消标记
                if (markedItems.has(index)) {
                    markedItems.delete(index); // 从已标记集合中删除
                } else {
                    markedItems.add(index); // 添加到已标记集合
                }
            });

            if (markedItems.has(index)) {
                clone.classList.add("marked");
            }

            virtualDOM.appendChild(clone);
        });

        // Update only the changed parts in the box
        const boxChildren = Array.from(box.children);
        const virtualChildren = Array.from(virtualDOM.children);

        // Find the first difference between the current box and the virtual DOM
        let firstDifferenceIndex = boxChildren.findIndex((child, index) => {
            return !virtualChildren[index] || !child.isEqualNode(virtualChildren[index]);
        });

        if (firstDifferenceIndex === -1 && boxChildren.length === virtualChildren.length) {
            // No changes
            return;
        }

        // Special case: when box is empty and virtualDOM has elements
        if (firstDifferenceIndex === -1 && virtualChildren.length > 0) {
            for (let i = 0; i < virtualChildren.length; i++) {
                if (boxChildren[i]) {
                    box.replaceChild(virtualChildren[i], boxChildren[i]);
                } else {
                    box.appendChild(virtualChildren[i]);
                }
            }
            return;
        }

        // Remove excess children
        while (boxChildren.length > virtualChildren.length) {
            box.removeChild(box.lastChild);
            boxChildren.pop();
        }

        // Replace changed nodes and append new ones
        for (let i = firstDifferenceIndex; i < virtualChildren.length; i++) {
            if (boxChildren[i]) {
                box.replaceChild(virtualChildren[i], boxChildren[i]);
            } else {
                box.appendChild(virtualChildren[i]);
            }
        }
    }

    function setupObserver() {
        // 选择更高级别的元素作为观察目标,例如 body 或特定容器
        const container = document.body;

        let check = performance.now();
        const observer = new MutationObserver((mutations, obs) => {
            if (!isChat) return;
            if ([...document.body.children].find((v) => v.tagName === "DIV" && v.role === "dialog")) {
                stop();
                return;
            }
            if (Date.now() - check < 30) return;
            // 每次变动时检查.swiper-wrapper是否存在
            const swiperWrapper = document.querySelector(".swiper-wrapper");
            if (swiperWrapper) {
                check = performance.now();
                updateBoxContent();
            }
        });

        // 监视元素的子元素变化
        observer.observe(container, { childList: true, subtree: true });
    }

    setupObserver();

    // 如果不是聊天界面关闭面板
    if (!isChat) {
        box.style.display = "none";
        toggleVisibilityButton.textContent = lan("显示面板");
    }

    let beepFlow = 0;
    function playBeep() {
        if (performance.now() - beepFlow < 10000) return;
        beepFlow = performance.now();
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const oscillator = audioContext.createOscillator();
        const gainNode = audioContext.createGain();

        oscillator.type = "sine";
        oscillator.frequency.setValueAtTime(440, audioContext.currentTime);
        oscillator.connect(gainNode);
        gainNode.connect(audioContext.destination);

        oscillator.start();
        gainNode.gain.exponentialRampToValueAtTime(0.00001, audioContext.currentTime + 1);
        oscillator.stop(audioContext.currentTime + 1);
    }
})();