Replace char avatar in chats with storage, chat-specific.
// ==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
})();