Character.AI Text Color

Lets you change the text colors as you wish and highlight chosen words

  1. // ==UserScript==
  2. // @name Character.AI Text Color
  3. // @namespace Character.AI Text Color by Vishanka
  4. // @match https://*.character.ai/*
  5. // @grant none
  6. // @license MIT
  7. // @version 2.3
  8. // @author Vishanka via chatGPT
  9. // @description Lets you change the text colors as you wish and highlight chosen words
  10. // @icon https://i.imgur.com/ynjBqKW.png
  11. // ==/UserScript==
  12.  
  13.  
  14. (function () {
  15. var plaintextColor = localStorage.getItem('plaintext_color');
  16.  
  17. // Default color if 'plaintext_color' is not set
  18. var defaultColor = '#958C7F';
  19.  
  20. // Use the retrieved color or default color
  21. var color = plaintextColor || defaultColor;
  22.  
  23. // Create the CSS style
  24. var css =
  25. "p, .swiper-no-swiping div { color: " + color + " !important; font-family: 'Noto Sans', sans-serif !important; }";
  26.  
  27. var head = document.getElementsByTagName("head")[0];
  28. var style = document.createElement("style");
  29. style.setAttribute("type", "text/css");
  30. style.innerHTML = css;
  31. head.appendChild(style);
  32.  
  33.  
  34.  
  35. })();
  36.  
  37. function changeColors() {
  38. const pTags = document.getElementsByTagName("p");
  39. for (let i = 0; i < pTags.length; i++) {
  40. const pTag = pTags[i];
  41. if (
  42. pTag.dataset.colorChanged === "true" ||
  43. pTag.querySelector("code") ||
  44. pTag.querySelector("img")
  45. ) {
  46. continue;
  47. }
  48. let text = pTag.innerHTML;
  49.  
  50. const aTags = pTag.getElementsByTagName("a"); // Get all <a> tags within the <p> tag
  51.  
  52. // Remove the <a> tags temporarily
  53. for (let j = 0; j < aTags.length; j++) {
  54. const aTag = aTags[j];
  55. text = text.replace(aTag.outerHTML, "REPLACE_ME_" + j); // Use a placeholder to be able to restore the links later
  56. }
  57.  
  58. //Changes Text within Quotation Marks to white
  59. text = text.replace(/(["“”«»].*?["“”«»])/g, `<span style="color: ${localStorage.getItem('quotationmarks_color') || '#FFFFFF'}">$1</span>`);
  60. //Changes Text within Quotation Marks and a comma at the end to yellow
  61. // text = text.replace(/(["“”«»][^"]*?,["“”«»])/g, '<span style="color: #E0DF7F">$1</span>');
  62. // Changes Italic Text Color
  63. text = text.replace(/<em>(.*?)<\/em>/g,`<span style="color: ${localStorage.getItem('italic_color') || '#E0DF7F'}; font-style: italic;">$1</span>`);
  64. // Changes Textcolor within Percentages to blue
  65. // text = text.replace(/([%][^"]*?[%])/g, '<span style="color: #0000FF">$1</span>');
  66.  
  67. var wordlist_cc = JSON.parse(localStorage.getItem('wordlist_cc')) || [];
  68.  
  69. // Check if the wordlist is not empty before creating the regex
  70. if (wordlist_cc.length > 0) {
  71. // Escape special characters in each word and join them with the "|" operator
  72. var wordRegex = new RegExp('\\b(' + wordlist_cc.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')\\b', 'gi');
  73.  
  74. // Replace matching words in the text
  75. text = text.replace(wordRegex, `<span style="color: ${localStorage.getItem('custom_color') || '#FFFFFF'}">$1</span>`);
  76. }
  77.  
  78.  
  79.  
  80. // Restore the <a> tags
  81. for (let j = 0; j < aTags.length; j++) {
  82. const aTag = aTags[j];
  83. text = text.replace("REPLACE_ME_" + j, aTag.outerHTML);
  84. }
  85.  
  86. pTag.innerHTML = text;
  87. pTag.dataset.colorChanged = "true";
  88. }
  89.  
  90. console.log("Changed colors");
  91. }
  92.  
  93. const observer = new MutationObserver(changeColors);
  94. observer.observe(document, { subtree: true, childList: true });
  95. changeColors();
  96.  
  97.  
  98.  
  99. function createButton(symbol, onClick) {
  100. const colorpalettebutton = document.createElement('button');
  101. colorpalettebutton.innerHTML = symbol;
  102. colorpalettebutton.style.position = 'relative';
  103. colorpalettebutton.style.background = 'none';
  104. colorpalettebutton.style.border = 'none';
  105. colorpalettebutton.style.fontSize = '18px';
  106. colorpalettebutton.style.top = '-5px';
  107. colorpalettebutton.style.cursor = 'pointer';
  108. colorpalettebutton.addEventListener('click', onClick);
  109. return colorpalettebutton;
  110. }
  111.  
  112. // Function to create the color selector panel
  113. function createColorPanel() {
  114. const panel = document.createElement('div');
  115. panel.id = 'colorPanel';
  116. panel.style.position = 'fixed';
  117. panel.style.top = '50%';
  118. panel.style.left = '50%';
  119. panel.style.transform = 'translate(-50%, -50%)';
  120. panel.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';
  121. panel.style.border = 'none';
  122. panel.style.borderRadius = '5px';
  123. panel.style.padding = '20px';
  124. // panel.style.border = '2px solid #000';
  125. panel.style.zIndex = '9999';
  126.  
  127. const categories = ['italic', 'quotationmarks', 'plaintext', 'custom'];
  128.  
  129. const colorPickers = {};
  130.  
  131. // Set a fixed width for the labels
  132. const labelWidth = '150px';
  133.  
  134. categories.forEach(category => {
  135. const colorPicker = document.createElement('input');
  136. colorPicker.type = 'color';
  137.  
  138. // Retrieve stored color from local storage
  139. const storedColor = localStorage.getItem(`${category}_color`);
  140. if (storedColor) {
  141. colorPicker.value = storedColor;
  142. }
  143.  
  144. colorPickers[category] = colorPicker;
  145.  
  146. // Create a div to hold color picker
  147. const colorDiv = document.createElement('div');
  148. colorDiv.style.position = 'relative';
  149. colorDiv.style.width = '20px';
  150. colorDiv.style.height = '20px';
  151. colorDiv.style.marginLeft = '10px';
  152. colorDiv.style.top = '5px';
  153. colorDiv.style.backgroundColor = colorPicker.value;
  154. colorDiv.style.display = 'inline-block';
  155. colorDiv.style.marginRight = '10px';
  156. colorDiv.style.cursor = 'pointer';
  157. colorDiv.style.border = '1px solid black';
  158.  
  159.  
  160. // Event listener to open color picker when the color square is clicked
  161. colorDiv.addEventListener('click', function () {
  162. colorPicker.click();
  163. });
  164.  
  165. // Event listener to update the color div when the color changes
  166. colorPicker.addEventListener('input', function () {
  167. colorDiv.style.backgroundColor = colorPicker.value;
  168. });
  169.  
  170. const label = document.createElement('label');
  171. label.style.width = labelWidth; // Set fixed width for the label
  172. label.appendChild(document.createTextNode(`${category}: `));
  173.  
  174. // Reset button for each color picker
  175. const resetButton = createButton('↺', function () {
  176. colorPicker.value = getDefaultColor(category);
  177. colorDiv.style.backgroundColor = colorPicker.value;
  178. });
  179. resetButton.style.position = 'relative';
  180. resetButton.style.top = '1px';
  181. // Create a div to hold label, color picker, and reset button
  182. const containerDiv = document.createElement('div');
  183. containerDiv.appendChild(label);
  184. containerDiv.appendChild(colorDiv);
  185. containerDiv.appendChild(resetButton);
  186.  
  187. panel.appendChild(containerDiv);
  188. panel.appendChild(document.createElement('br'));
  189. });
  190.  
  191. // Custom word list input
  192. const wordListInput = document.createElement('input');
  193. wordListInput.type = 'text';
  194. wordListInput.placeholder = 'Separate words with commas';
  195. wordListInput.style.width = '250px';
  196. wordListInput.style.height = '35px';
  197. wordListInput.style.borderRadius = '3px';
  198. wordListInput.style.marginBottom = '10px';
  199. panel.appendChild(wordListInput);
  200. panel.appendChild(document.createElement('br'));
  201.  
  202. const wordListContainer = document.createElement('div');
  203. wordListContainer.style.display = 'flex';
  204. wordListContainer.style.flexWrap = 'wrap';
  205. wordListContainer.style.maxWidth = '300px'; // Set a fixed maximum width for the container
  206.  
  207. // Display custom word list buttons
  208. const wordListArray = JSON.parse(localStorage.getItem('wordlist_cc')) || [];
  209. const wordListButtons = [];
  210.  
  211. function createWordButton(word) {
  212. const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
  213.  
  214. const removeSymbol = isMobile ? '×' : '🞮';
  215.  
  216. const wordButton = createButton(`${word} ${removeSymbol}`, function() {
  217. // Remove the word from the list and update the panel
  218. const index = wordListArray.indexOf(word);
  219. if (index !== -1) {
  220. wordListArray.splice(index, 1);
  221. updateWordListButtons();
  222. }
  223. });
  224.  
  225. // Word Buttons
  226. wordButton.style.borderRadius = '3px';
  227. wordButton.style.border = 'none';
  228. wordButton.style.backgroundColor = 'lightsteelblue';
  229. wordButton.style.marginBottom = '5px';
  230. wordButton.style.marginRight = '5px';
  231. wordButton.style.fontSize = '16px';
  232. wordButton.classList.add('word-button');
  233. return wordButton;
  234. }
  235.  
  236. function updateWordListButtons() {
  237. wordListContainer.innerHTML = ''; // Clear the container
  238. wordListArray.forEach(word => {
  239. const wordButton = createWordButton(word);
  240. wordListContainer.appendChild(wordButton);
  241. });
  242. }
  243.  
  244. // Append wordListContainer to the panel
  245.  
  246.  
  247.  
  248. updateWordListButtons();
  249.  
  250. // Add Words button
  251. const addWordsButton = document.createElement('button');
  252. addWordsButton.textContent = 'Add';
  253. addWordsButton.style.marginTop = '-8px';
  254. addWordsButton.style.marginLeft = '5px';
  255. addWordsButton.style.borderRadius = '3px';
  256. addWordsButton.style.border = 'none';
  257. addWordsButton.style.backgroundColor = 'lightsteelblue';
  258. addWordsButton.addEventListener('click', function() {
  259. // Get the input value, split into words, and add to wordListArray
  260. const wordListValue = wordListInput.value;
  261. const newWords = wordListValue.split(',').map(word => word.trim().toLowerCase()).filter(word => word !== ''); // Convert to lowercase and remove empty entries
  262. wordListArray.push(...newWords);
  263.  
  264. // Update the word list buttons in the panel
  265. updateWordListButtons();
  266. });
  267.  
  268. // Create a div to group the input and button on the same line
  269. const inputButtonContainer = document.createElement('div');
  270. inputButtonContainer.style.display = 'flex';
  271. inputButtonContainer.style.alignItems = 'center';
  272.  
  273. inputButtonContainer.appendChild(wordListInput);
  274. inputButtonContainer.appendChild(addWordsButton);
  275.  
  276. // Append the container to the panel
  277. panel.appendChild(inputButtonContainer);
  278. panel.appendChild(wordListContainer);
  279. // Create initial word list buttons
  280. updateWordListButtons();
  281.  
  282.  
  283. // OK button
  284. const okButton = document.createElement('button');
  285. okButton.textContent = 'Confirm';
  286. okButton.style.marginTop = '-20px';
  287. okButton.style.width = '75px';
  288. okButton.style.height = '35px';
  289. okButton.style.marginRight = '5px';
  290. okButton.style.borderRadius = '3px';
  291. okButton.style.border = 'none';
  292. okButton.style.backgroundColor = 'lightsteelblue';
  293. okButton.style.position = 'relative';
  294. okButton.style.left = '24%';
  295. //okButton.style.transform = 'translateX(-50%)';
  296. okButton.addEventListener('click', function() {
  297. // Save selected colors to local storage
  298. categories.forEach(category => {
  299. const oldValue = localStorage.getItem(`${category}_color`);
  300. const newValue = colorPickers[category].value;
  301.  
  302. if (oldValue !== newValue) {
  303. localStorage.setItem(`${category}_color`, newValue);
  304.  
  305. // If 'plaintext' color is changed, auto-reload the page
  306. if (category === 'plaintext') {
  307. window.location.reload();
  308. }
  309. }
  310. });
  311.  
  312. // Save custom word list to local storage
  313. const wordListValue = wordListInput.value;
  314. const newWords = wordListValue.split(',').map(word => word.trim().toLowerCase()).filter(word => word !== ''); // Convert to lowercase and remove empty entries
  315. const uniqueNewWords = Array.from(new Set(newWords)); // Remove duplicates
  316.  
  317. // Check for existing words and add only new ones
  318. uniqueNewWords.forEach(newWord => {
  319. if (!wordListArray.includes(newWord)) {
  320. wordListArray.push(newWord);
  321. }
  322. });
  323.  
  324. localStorage.setItem('wordlist_cc', JSON.stringify(wordListArray));
  325.  
  326. updateWordListButtons();
  327.  
  328. // Close the panel
  329. panel.remove();
  330. });
  331.  
  332. // Cancel button
  333. const cancelButton = document.createElement('button');
  334. cancelButton.textContent = 'Cancel';
  335. cancelButton.style.marginTop = '-20px';
  336. cancelButton.style.borderRadius = '3px';
  337. cancelButton.style.width = '75px';
  338. cancelButton.style.marginLeft = '5px';
  339. cancelButton.style.height = '35px';
  340. cancelButton.style.border = 'none';
  341. cancelButton.style.backgroundColor = 'darkgrey';
  342. cancelButton.style.position = 'relative';
  343. cancelButton.style.left = '25%';
  344. cancelButton.addEventListener('click', function() {
  345. // Close the panel without saving
  346. panel.remove();
  347. });
  348.  
  349. panel.appendChild(document.createElement('br'));
  350. panel.appendChild(okButton);
  351. panel.appendChild(cancelButton);
  352.  
  353. document.body.appendChild(panel);
  354. }
  355.  
  356.  
  357.  
  358. // Function to get the default color for a category
  359. function getDefaultColor(category) {
  360. const defaultColors = {
  361. 'italic': '#E0DF7F',
  362. 'quotationmarks': '#FFFFFF',
  363. 'plaintext': '#958C7F',
  364. 'custom': '#E0DF7F'
  365. };
  366. return defaultColors[category];
  367. }
  368.  
  369. const mainButton = createButton('', function() {
  370. const colorPanelExists = document.getElementById('colorPanel');
  371. if (!colorPanelExists) {
  372. createColorPanel();
  373. }
  374. });
  375.  
  376. // Set the background image of the button to the provided image
  377. mainButton.style.backgroundImage = "url('https://i.imgur.com/yBgJ3za.png')";
  378. mainButton.style.backgroundSize = "cover";
  379. mainButton.style.top = "0px";
  380. mainButton.style.width = "22px"; // Adjust the width and height as needed
  381. mainButton.style.height = "22px"; // Adjust the width and height as needed
  382.  
  383.  
  384. const targetSelector = '.chat2 > div:nth-child(1) > div:nth-child(1) > div:nth-child(3) > div:nth-child(1)';
  385. const targetPanel1 = document.querySelector(targetSelector);
  386.  
  387. if (targetPanel1) {
  388. targetPanel1.insertBefore(mainButton, targetPanel1.firstChild);
  389. } else {
  390. // If the target panel is not found, set up a MutationObserver to keep checking for it.
  391. const observer = new MutationObserver(() => {
  392. const updatedTargetPanel = document.querySelector(targetSelector);
  393. if (updatedTargetPanel) {
  394. // Once the target panel is available, insert the mainButton and disconnect the observer.
  395. updatedTargetPanel.insertBefore(mainButton, updatedTargetPanel.firstChild);
  396. observer.disconnect();
  397. }
  398. });
  399.  
  400. // Set up the observer to watch for changes in the parent of the target panel or higher up in the DOM.
  401. observer.observe(document.body, { subtree: true, childList: true });
  402.  
  403. console.error('Target panel not found. Waiting for changes...');
  404. }