CAI CharPic replacer

Replace char avatar in chats with storage, chat-specific.

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         CAI CharPic replacer
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Replace char avatar in chats with storage, chat-specific.
// @match        https://character.ai/chat/*
// @grant        none
// @author       LuxTallis
// @license      MIT
// ==/UserScript==
(function () {
    'use strict';

    const TARGET_IMAGE_BASE_URL = "https://characterai.io/i/400/static/avatars/uploaded/";
    const BROADER_SELECTOR = 'div.group img.object-cover'; // Broad selector for images in div.group
    const EXCLUDED_SELECTOR = '.flex-row-reverse img.object-cover.object-top'; // Specific selector to exclude

    // Get the current chat identifier from the URL
    function getCurrentChatId() {
        const match = window.location.pathname.match(/\/chat\/([^\/]+)/);
        return match ? match[1] : null;
    }

    // Save a custom image URL for the current chat
    function saveCustomImageUrlForChat(chatId, url) {
        const chatImages = JSON.parse(localStorage.getItem('chat_images') || '{}');
        chatImages[chatId] = url;
        localStorage.setItem('chat_images', JSON.stringify(chatImages));
        addToLibrary(url); // Add to the recent library
    }

    // Get the custom image URL for the current chat
    function getCustomImageUrlForChat(chatId) {
        const chatImages = JSON.parse(localStorage.getItem('chat_images') || '{}');
        return chatImages[chatId] || '';
    }

    // Add image to the library of recent images
    function addToLibrary(url) {
        const library = getLibrary();
        if (!url || library.includes(url)) return;
        library.push(url);
        if (library.length > 10) library.shift(); // Limit library to 10 images
        saveLibrary(library);
    }

    // Remove image from the library
    function removeFromLibrary(url) {
        let library = getLibrary();
        library = library.filter(item => item !== url);
        saveLibrary(library);
    }

    // Save the image library to local storage
    function saveLibrary(library) {
        localStorage.setItem('image_library', JSON.stringify(library));
    }

    // Get the image library from local storage
    function getLibrary() {
        return JSON.parse(localStorage.getItem('image_library') || '[]');
    }

    // Replace images with the custom URL for the current chat, excluding unwanted elements
    function replaceImagesForChat(customUrl) {
        const images = document.querySelectorAll(BROADER_SELECTOR);
        images.forEach((image) => {
            // Skip images matching the excluded selector
            if (image.closest(EXCLUDED_SELECTOR)) return;

            if (image.src.startsWith(TARGET_IMAGE_BASE_URL)) {
                // Hide the image immediately using visibility
                image.style.visibility = 'hidden';

                // Create a new image element and set its source
                const newImage = new Image();
                newImage.src = customUrl;

                // Wait for the new image to fully load
                newImage.onload = function () {
                    // Once the new image is loaded, set it to the original image
                    image.src = customUrl;
                    image.style.visibility = 'visible';  // Make the image visible
                };
            }
        });
    }

    // Function to observe and replace images on load or DOM changes
    function observeNewImages() {
        // Using setInterval to continuously check and replace images
        setInterval(() => {
            const chatId = getCurrentChatId();
            if (chatId) {
                const savedImageUrl = getCustomImageUrlForChat(chatId);
                if (savedImageUrl) {
                    replaceImagesForChat(savedImageUrl);
                }
            }
        }, 100); // Check every 100ms for new images
    }

    // Function to clean up the image library by removing unused URLs
    function cleanUpLibrary() {
        const chatImages = JSON.parse(localStorage.getItem('chat_images') || '{}');
        const usedUrls = new Set(Object.values(chatImages)); // Get all used URLs from active chats
        let library = getLibrary();

        // Filter library to only include URLs that are still used in active chats
        library = library.filter(url => usedUrls.has(url));
        saveLibrary(library); // Save the cleaned library
    }

    // Create the button
    const button = document.createElement('button');
    button.innerHTML = '👤';
    button.style.position = 'fixed';
    button.style.top = '110px'; // Positioned below the background script button
    button.style.right = '5px';
    button.style.width = '22px';
    button.style.height = '22px';
    button.style.backgroundColor = '#444';
    button.style.color = 'white';
    button.style.border = 'none';
    button.style.borderRadius = '3px';
    button.style.cursor = 'pointer';
    button.style.fontFamily = 'Montserrat, sans-serif';
    button.style.display = 'flex';
    button.style.justifyContent = 'center';
    button.style.alignItems = 'center';
    button.style.zIndex = '9999';

    // Add the button to the document
    document.body.appendChild(button);

    // Add functionality to set a new image URL for the current chat
    button.addEventListener('click', () => {
        const chatId = getCurrentChatId();
        if (!chatId) {
            alert('Could not determine the chat ID.');
            return;
        }

        // Create popup panel
        const popup = document.createElement('div');
        popup.id = 'imagePopup';
        popup.style.position = 'fixed';
        popup.style.top = '50%';
        popup.style.left = '50%';
        popup.style.transform = 'translate(-50%, -50%)';
        popup.style.backgroundColor = '#1e1e1e';
        popup.style.color = 'white';
        popup.style.borderRadius = '5px';
        popup.style.padding = '20px';
        popup.style.zIndex = '9999';
        popup.style.fontFamily = 'Montserrat, sans-serif';
        popup.style.minWidth = '300px';
        popup.style.maxWidth = '500px';

        const label = document.createElement('label');
        label.textContent = 'Enter Image URL:';
        label.style.display = 'block';
        label.style.marginBottom = '5px';

        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = 'Enter image URL';
        input.style.width = '100%';
        input.style.marginBottom = '10px';

        const applyButton = document.createElement('button');
        applyButton.textContent = 'Apply Image';
        applyButton.style.marginTop = '10px';
        applyButton.style.padding = '5px 10px';
        applyButton.style.border = 'none';
        applyButton.style.borderRadius = '3px';
        applyButton.style.backgroundColor = '#444';
        applyButton.style.color = 'white';
        applyButton.style.fontFamily = 'Montserrat, sans-serif';
        applyButton.addEventListener('click', () => {
            const url = input.value.trim();
            if (url) {
                saveCustomImageUrlForChat(chatId, url);
                replaceImagesForChat(url);
                popup.remove();
            }
        });

        // Default button (Revert to Default)
        const defaultButton = document.createElement('button');
        defaultButton.textContent = 'Default';
        defaultButton.style.marginTop = '10px';
        defaultButton.style.padding = '5px 10px';
        defaultButton.style.border = 'none';
        defaultButton.style.borderRadius = '3px';
        defaultButton.style.backgroundColor = '#444'; // Same as Apply button
        defaultButton.style.color = 'white';
        defaultButton.style.fontFamily = 'Montserrat, sans-serif';
        defaultButton.style.marginLeft = 'auto'; // Align to the right
        defaultButton.style.cursor = 'pointer';
        defaultButton.addEventListener('click', () => {
            saveCustomImageUrlForChat(chatId, '');  // Clear the custom URL
            replaceImagesForChat('');
            popup.remove();
        });

        // Library for recent images
        const libraryContainer = document.createElement('div');
        libraryContainer.style.marginTop = '10px';
        libraryContainer.style.overflowX = 'auto';
        libraryContainer.style.display = 'flex';
        libraryContainer.style.flexWrap = 'wrap'; // Allow wrapping into multiple lines
        const library = getLibrary();

        library.forEach((url) => {
            const imgContainer = document.createElement('div');
            imgContainer.style.position = 'relative';
            imgContainer.style.width = '49px';
            imgContainer.style.height = '49px';
            imgContainer.style.overflow = 'hidden';
            imgContainer.style.cursor = 'pointer';

            const img = document.createElement('img');
            img.src = url;
            img.style.width = '100%';
            img.style.height = '100%';
            img.style.objectFit = 'cover';
            img.style.borderRadius = '3px';
            img.style.cursor = 'pointer';
            img.title = url;

            img.addEventListener('click', () => {
                saveCustomImageUrlForChat(chatId, url);
                replaceImagesForChat(url);
                popup.remove();
            });

            // Add remove button for each image
            const removeButton = document.createElement('button');
            removeButton.textContent = '×';
            removeButton.style.position = 'absolute';
            removeButton.style.top = '5px';
            removeButton.style.right = '5px';
            removeButton.style.backgroundColor = 'red';
            removeButton.style.color = 'white';
            removeButton.style.border = 'none';
            removeButton.style.borderRadius = '50%';
            removeButton.style.cursor = 'pointer';
            removeButton.style.width = '20px';
            removeButton.style.height = '20px';
            removeButton.style.textAlign = 'center';
            removeButton.style.fontSize = '12px';
            removeButton.addEventListener('click', (e) => {
                e.stopPropagation();
                removeFromLibrary(url); // Remove from library
                imgContainer.remove();   // Remove from popup view
            });

            imgContainer.appendChild(img);
            imgContainer.appendChild(removeButton);
            libraryContainer.appendChild(imgContainer);
        });

        popup.appendChild(label);
        popup.appendChild(input);
        popup.appendChild(applyButton);
        popup.appendChild(libraryContainer);
        popup.appendChild(defaultButton);
        document.body.appendChild(popup);

        // Close the popup when clicking outside of it
        document.addEventListener('click', function closeOnOutsideClick(event) {
            if (!popup.contains(event.target) && event.target !== button) {
                popup.remove();
                document.removeEventListener('click', closeOnOutsideClick);
            }
        });
    });

    // Begin observing and replacing images immediately
    observeNewImages();

    // Periodic cleanup to remove unused images from library
    setInterval(cleanUpLibrary, 60000); // Clean up every minute
})();