c.ai X Text Color

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

目前為 2024-08-29 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name c.ai X Text Color
  3. // @namespace c.ai X Text Color
  4. // @match https://character.ai/*
  5. // @grant none
  6. // @license MIT
  7. // @version 2.9
  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.  
  16. const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
  17. var plaintextColor = localStorage.getItem('plaintext_color');
  18. var italicColor = localStorage.getItem('italic_color');
  19. var charbubbleColor = localStorage.getItem('charbubble_color') || '#26272B';
  20. var userbubbleColor = localStorage.getItem('userbubble_color') || '#303136';
  21. // Default color if 'plaintext_color' is not set
  22. var defaultColor = '#A2A2AC';
  23. var GuideColor = localStorage.getItem('guide_color') || '#131316';
  24. var BodyColor = localStorage.getItem('body_color') || '#18181B';
  25. var InputColor = localStorage.getItem('input_color') || '#202024';
  26.  
  27. // Use the retrieved color or default color
  28. var color = plaintextColor || defaultColor;
  29.  
  30. // Create the CSS style
  31. var css = "p[node='[object Object]'] { color: " + color + " !important; font-family: '__Inter_918210','Noto Sans', sans-serif !important; } p, textarea, button, div.text-sm { font-family: '__Inter_918210','Noto Sans', sans-serif !important; } em { color: " + italicColor + " !important; }";
  32.  
  33. css += `.mt-1.bg-surface-elevation-2 { background-color: ${charbubbleColor}; } .mt-1.bg-surface-elevation-3 { background-color: ${userbubbleColor}; }`;
  34.  
  35.  
  36.  
  37. var head = document.getElementsByTagName("head")[0];
  38. var style = document.createElement("style");
  39. style.setAttribute("type", "text/css");
  40. style.innerHTML = css;
  41. head.appendChild(style);
  42.  
  43.  
  44. // Function to update CSS variables
  45. function updateCSSVariable(variableName, value) {
  46. document.documentElement.style.setProperty(variableName, value);
  47. }
  48.  
  49. if (currentTheme === 'dark') {
  50. // Update the specific CSS variables
  51. updateCSSVariable('--G850', InputColor);
  52. updateCSSVariable('--G900', BodyColor);
  53. updateCSSVariable('--G950', GuideColor);
  54.  
  55.  
  56. updateCSSVariable('--G50', '#fafafa');
  57. updateCSSVariable('--G100', '#f4f4f5');
  58. updateCSSVariable('--G150', '#ececee');
  59. }
  60. else {
  61. // Update CSS variables for light theme (or any other theme)
  62. updateCSSVariable('--G850', '#202024');
  63. updateCSSVariable('--G900', '#18181B');
  64. updateCSSVariable('--G950', '#131316');
  65. updateCSSVariable('--G50', InputColor);
  66. updateCSSVariable('--G100', BodyColor);
  67. updateCSSVariable('--G150', GuideColor);
  68. }
  69. })();
  70.  
  71.  
  72.  
  73.  
  74. function changeColors() {
  75. const pTags = document.getElementsByTagName("p");
  76. const quotationMarksColor = localStorage.getItem('quotationmarks_color') || '#FFFFFF';
  77. const customColor = localStorage.getItem('custom_color') || '#FFFFFF';
  78. const wordlistCc = JSON.parse(localStorage.getItem('wordlist_cc')) || [];
  79.  
  80. const wordRegex = wordlistCc.length > 0 ? new RegExp('\\b(' + wordlistCc.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')\\b', 'gi') : null;
  81.  
  82. Array.from(pTags).forEach((pTag) => {
  83. if (
  84. pTag.dataset.colorChanged === "true" ||
  85. pTag.querySelector("code") ||
  86. pTag.querySelector("img") ||
  87. pTag.querySelector("textarea") ||
  88. pTag.querySelector("button") ||
  89. pTag.querySelector("div")
  90. ) {
  91. return; // Skip iteration
  92. }
  93.  
  94. let text = pTag.innerHTML;
  95.  
  96. // Save .katex elements' original HTML and replace with placeholders
  97. const katexElems = Array.from(pTag.querySelectorAll(".katex"));
  98. const katexReplacements = katexElems.map((elem, index) => {
  99. const placeholder = `KATEX_PLACEHOLDER_${index}`;
  100. text = text.replace(elem.outerHTML, placeholder);
  101. return { html: elem.outerHTML, placeholder };
  102. });
  103.  
  104. // Handle <a> tags by removing them temporarily and saving their HTML for later restoration
  105. const aTags = Array.from(pTag.getElementsByTagName("a"));
  106. const aTagsReplacements = aTags.map((aTag, j) => {
  107. const placeholder = `REPLACE_ME_${j}`;
  108. text = text.replace(aTag.outerHTML, placeholder);
  109. return { tag: aTag, placeholder };
  110. });
  111.  
  112. // Change text within quotation marks and for specific words based on the regex
  113. text = text.replace(/(["“”«»].*?["“”«»])/g, `<span style="color: ${quotationMarksColor}">$1</span>`);
  114. text = text.replace(/(["“”«»][^"]*?,["“”«»])/g, `<span style="color: #E0DF7F">$1</span>`);
  115.  
  116. if (wordRegex) {
  117. text = text.replace(wordRegex, `<span style="color: ${customColor}">$1</span>`);
  118. }
  119.  
  120. // Restore .katex elements and <a> tags
  121. [...katexReplacements, ...aTagsReplacements].forEach(({ html, placeholder, tag }) => {
  122. text = text.replace(placeholder, html || tag.outerHTML);
  123. });
  124.  
  125. // Update the innerHTML and mark the <p> tag to avoid re-processing
  126. pTag.innerHTML = text;
  127. pTag.dataset.colorChanged = "true";
  128. });
  129.  
  130. console.log("Changed colors");
  131. }
  132.  
  133. const divElements = document.querySelectorAll('div');
  134.  
  135. divElements.forEach(div => {
  136. const observer = new MutationObserver(changeColors);
  137. observer.observe(div, { subtree: true, childList: true });
  138. });
  139.  
  140.  
  141.  
  142. function createButton(symbol, onClick) {
  143. const colorpalettebutton = document.createElement('button');
  144. colorpalettebutton.innerHTML = symbol;
  145. colorpalettebutton.style.position = 'relative';
  146. colorpalettebutton.style.background = 'none';
  147. colorpalettebutton.style.border = 'none';
  148. colorpalettebutton.style.fontSize = '18px';
  149. colorpalettebutton.style.top = '-5px';
  150. colorpalettebutton.style.cursor = 'pointer';
  151. colorpalettebutton.addEventListener('click', onClick);
  152. return colorpalettebutton;
  153. }
  154.  
  155. // Function to create the color selector panel
  156. function createColorPanel() {
  157. const panel = document.createElement('div');
  158. panel.id = 'colorPanel';
  159. panel.style.position = 'fixed';
  160. panel.style.top = '50%';
  161. panel.style.left = '50%';
  162. panel.style.transform = 'translate(-50%, -50%)';
  163. const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
  164. if (currentTheme === 'dark') {
  165. panel.style.backgroundColor = 'rgba(19, 19, 22, 0.95)';
  166. } else {
  167. panel.style.backgroundColor = 'rgba(214, 214, 221, 0.95)';
  168. }
  169. panel.style.border = 'none';
  170. panel.style.borderRadius = '5px';
  171. panel.style.padding = '20px';
  172. // panel.style.border = '2px solid #000';
  173. panel.style.zIndex = '9999';
  174.  
  175. const categories = ['italic', 'quotationmarks', 'plaintext', 'custom', 'charbubble', 'userbubble', 'guide', 'body', 'input'];
  176.  
  177. const colorPickers = {};
  178.  
  179. // Set a fixed width for the labels
  180. const labelWidth = '150px';
  181.  
  182. categories.forEach(category => {
  183. const colorPicker = document.createElement('input');
  184. colorPicker.type = 'color';
  185.  
  186. // Retrieve stored color from local storage
  187. const storedColor = localStorage.getItem(`${category}_color`);
  188. if (storedColor) {
  189. colorPicker.value = storedColor;
  190. }
  191.  
  192. colorPickers[category] = colorPicker;
  193.  
  194. // Create a div to hold color picker
  195. const colorDiv = document.createElement('div');
  196. colorDiv.style.position = 'relative';
  197. colorDiv.style.width = '20px';
  198. colorDiv.style.height = '20px';
  199. colorDiv.style.marginLeft = '10px';
  200. colorDiv.style.top = '5px';
  201. colorDiv.style.backgroundColor = colorPicker.value;
  202. colorDiv.style.display = 'inline-block';
  203. colorDiv.style.marginRight = '10px';
  204. colorDiv.style.cursor = 'pointer';
  205. colorDiv.style.border = '1px solid black';
  206.  
  207.  
  208. // Event listener to open color picker when the color square is clicked
  209. colorDiv.addEventListener('click', function () {
  210. colorPicker.click();
  211. });
  212.  
  213. // Event listener to update the color div when the color changes
  214. colorPicker.addEventListener('input', function () {
  215. colorDiv.style.backgroundColor = colorPicker.value;
  216. });
  217.  
  218. const label = document.createElement('label');
  219. label.style.width = labelWidth; // Set fixed width for the label
  220. label.appendChild(document.createTextNode(`${category}: `));
  221.  
  222. // Reset button for each color picker
  223. const resetButton = createButton('↺', function () {
  224. colorPicker.value = getDefaultColor(category);
  225. colorDiv.style.backgroundColor = colorPicker.value;
  226. });
  227. resetButton.style.position = 'relative';
  228. resetButton.style.top = '1px';
  229. // Create a div to hold label, color picker, and reset button
  230. const containerDiv = document.createElement('div');
  231. containerDiv.appendChild(label);
  232. containerDiv.appendChild(colorDiv);
  233. containerDiv.appendChild(resetButton);
  234.  
  235. panel.appendChild(containerDiv);
  236. panel.appendChild(document.createElement('br'));
  237. });
  238.  
  239. // Custom word list input
  240. const wordListInput = document.createElement('input');
  241. wordListInput.type = 'text';
  242. wordListInput.placeholder = 'Separate words with commas';
  243. wordListInput.style.width = '250px';
  244. wordListInput.style.height = '35px';
  245. wordListInput.style.borderRadius = '3px';
  246. wordListInput.style.marginBottom = '10px';
  247. panel.appendChild(wordListInput);
  248. panel.appendChild(document.createElement('br'));
  249.  
  250. const wordListContainer = document.createElement('div');
  251. wordListContainer.style.display = 'flex';
  252. wordListContainer.style.flexWrap = 'wrap';
  253. wordListContainer.style.maxWidth = '300px'; // Set a fixed maximum width for the container
  254.  
  255. // Display custom word list buttons
  256. const wordListArray = JSON.parse(localStorage.getItem('wordlist_cc')) || [];
  257. const wordListButtons = [];
  258.  
  259. function createWordButton(word) {
  260. const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
  261.  
  262. const removeSymbol = isMobile ? '×' : '🞮';
  263.  
  264. const wordButton = createButton(`${word} ${removeSymbol}`, function() {
  265. // Remove the word from the list and update the panel
  266. const index = wordListArray.indexOf(word);
  267. if (index !== -1) {
  268. wordListArray.splice(index, 1);
  269. updateWordListButtons();
  270. }
  271. });
  272.  
  273. // Word Buttons
  274. wordButton.style.borderRadius = '3px';
  275. wordButton.style.border = 'none';
  276. if (currentTheme === 'dark') {
  277. wordButton.style.backgroundColor = '#26272B';
  278. } else {
  279. wordButton.style.backgroundColor = '#E4E4E7';
  280. }
  281. wordButton.style.marginBottom = '5px';
  282. wordButton.style.marginRight = '5px';
  283. wordButton.style.fontSize = '16px';
  284. wordButton.classList.add('word-button');
  285. return wordButton;
  286. }
  287.  
  288. function updateWordListButtons() {
  289. wordListContainer.innerHTML = ''; // Clear the container
  290. wordListArray.forEach(word => {
  291. const wordButton = createWordButton(word);
  292. wordListContainer.appendChild(wordButton);
  293. });
  294. }
  295.  
  296. // Append wordListContainer to the panel
  297.  
  298.  
  299.  
  300. updateWordListButtons();
  301.  
  302. // Add Words button
  303. const addWordsButton = document.createElement('button');
  304. addWordsButton.textContent = 'Add';
  305. addWordsButton.style.marginTop = '-8px';
  306. addWordsButton.style.marginLeft = '5px';
  307. addWordsButton.style.borderRadius = '3px';
  308. addWordsButton.style.border = 'none';
  309. if (currentTheme === 'dark') {
  310. addWordsButton.style.backgroundColor = '#26272B';
  311. } else {
  312. addWordsButton.style.backgroundColor = '#E4E4E7';
  313. }
  314. addWordsButton.addEventListener('click', function() {
  315. // Get the input value, split into words, and add to wordListArray
  316. const wordListValue = wordListInput.value;
  317. const newWords = wordListValue.split(',').map(word => word.trim().toLowerCase()).filter(word => word !== ''); // Convert to lowercase and remove empty entries
  318. wordListArray.push(...newWords);
  319.  
  320. // Update the word list buttons in the panel
  321. updateWordListButtons();
  322. });
  323.  
  324. // Create a div to group the input and button on the same line
  325. const inputButtonContainer = document.createElement('div');
  326. inputButtonContainer.style.display = 'flex';
  327. inputButtonContainer.style.alignItems = 'center';
  328.  
  329. inputButtonContainer.appendChild(wordListInput);
  330. inputButtonContainer.appendChild(addWordsButton);
  331.  
  332. // Append the container to the panel
  333. panel.appendChild(inputButtonContainer);
  334. panel.appendChild(wordListContainer);
  335. // Create initial word list buttons
  336. updateWordListButtons();
  337.  
  338.  
  339. // OK button
  340. const okButton = document.createElement('button');
  341. okButton.textContent = 'Confirm';
  342. okButton.style.marginTop = '-20px';
  343. okButton.style.width = '75px';
  344. okButton.style.height = '35px';
  345. okButton.style.marginRight = '5px';
  346. okButton.style.borderRadius = '3px';
  347. okButton.style.border = 'none';
  348. if (currentTheme === 'dark') {
  349. okButton.style.backgroundColor = '#26272B';
  350. } else {
  351. okButton.style.backgroundColor = '#D9D9DF';
  352. }
  353.  
  354. okButton.style.position = 'relative';
  355. okButton.style.left = '24%';
  356. //okButton.style.transform = 'translateX(-50%)';
  357. okButton.addEventListener('click', function() {
  358. // Save selected colors to local storage
  359. categories.forEach(category => {
  360. const oldValue = localStorage.getItem(`${category}_color`);
  361. const newValue = colorPickers[category].value;
  362.  
  363. if (oldValue !== newValue) {
  364. localStorage.setItem(`${category}_color`, newValue);
  365.  
  366. // If 'plaintext' color is changed, auto-reload the page
  367. if (category === 'plaintext' || category === 'guide' || category === 'body' || category === 'input') {
  368. window.location.reload();
  369. }
  370. }
  371. });
  372.  
  373.  
  374. // Save custom word list to local storage
  375. const wordListValue = wordListInput.value;
  376. const newWords = wordListValue.split(',').map(word => word.trim().toLowerCase()).filter(word => word !== ''); // Convert to lowercase and remove empty entries
  377. const uniqueNewWords = Array.from(new Set(newWords)); // Remove duplicates
  378.  
  379. // Check for existing words and add only new ones
  380. uniqueNewWords.forEach(newWord => {
  381. if (!wordListArray.includes(newWord)) {
  382. wordListArray.push(newWord);
  383. }
  384. });
  385.  
  386. localStorage.setItem('wordlist_cc', JSON.stringify(wordListArray));
  387.  
  388. updateWordListButtons();
  389.  
  390. // Close the panel
  391. panel.remove();
  392. });
  393.  
  394. // Cancel button
  395. const cancelButton = document.createElement('button');
  396. cancelButton.textContent = 'Cancel';
  397. cancelButton.style.marginTop = '-20px';
  398. cancelButton.style.borderRadius = '3px';
  399. cancelButton.style.width = '75px';
  400. cancelButton.style.marginLeft = '5px';
  401. cancelButton.style.height = '35px';
  402. cancelButton.style.border = 'none';
  403. if (currentTheme === 'dark') {
  404. cancelButton.style.backgroundColor = '#5E5E5E';
  405. } else {
  406. cancelButton.style.backgroundColor = '#CBD2D4';
  407. }
  408. cancelButton.style.position = 'relative';
  409. cancelButton.style.left = '25%';
  410. cancelButton.addEventListener('click', function() {
  411. // Close the panel without saving
  412. panel.remove();
  413. });
  414.  
  415. panel.appendChild(document.createElement('br'));
  416. panel.appendChild(okButton);
  417. panel.appendChild(cancelButton);
  418.  
  419. document.body.appendChild(panel);
  420. }
  421.  
  422.  
  423.  
  424. // Function to get the default color for a category
  425. function getDefaultColor(category) {
  426. const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
  427. if (currentTheme === 'dark') {
  428. const defaultColors = {
  429. 'italic': '#E0DF7F',
  430. 'quotationmarks': '#FFFFFF',
  431. 'plaintext': '#A2A2AC',
  432. 'custom': '#E0DF7F',
  433. 'charbubble': '#26272B',
  434. 'userbubble': '#303136',
  435. 'guide': '#131316',
  436. 'input': '#202024',
  437. 'body': '#18181B'
  438. };
  439. return defaultColors[category];
  440. }
  441. else {
  442. const defaultColors = {
  443. 'italic': '#4F7AA6',
  444. 'quotationmarks': '#000000',
  445. 'plaintext': '#374151',
  446. 'custom': '#4F7AA6',
  447. 'charbubble': '#E4E4E7',
  448. 'userbubble': '#D9D9DF',
  449. 'guide': '#FAFAFA',
  450. 'input': '#F4F4F5',
  451. 'body': '#ECECEE'
  452. };
  453. return defaultColors[category];
  454. }
  455. }
  456. const mainButton = createButton('', function() {
  457. const colorPanelExists = document.getElementById('colorPanel');
  458. if (!colorPanelExists) {
  459. createColorPanel();
  460. }
  461. });
  462.  
  463. // Set the background image of the button to the provided image
  464. mainButton.style.backgroundImage = "url('https://i.imgur.com/yBgJ3za.png')";
  465. mainButton.style.backgroundSize = "cover";
  466. mainButton.style.position = "fixed"; // Use "fixed" for a position relative to the viewport
  467. mainButton.style.top = "10px"; // Adjust the top position as needed
  468. mainButton.style.right = "10px"; // Adjust the right position as needed
  469. mainButton.style.width = "22px"; // Adjust the width and height as needed
  470. mainButton.style.height = "22px"; // Adjust the width and height as needed
  471.  
  472. // Function to insert the mainButton into the body of the document
  473. function insertMainButton() {
  474. document.body.appendChild(mainButton);
  475. }
  476.  
  477. // Call the function to insert the mainButton into the body
  478. insertMainButton();
  479.  
  480. console.info('c.ai Text Color Button appended to the top right corner.');