c.ai X Character Creation Helper

Gives visual feedback for the definition

  1. // ==UserScript==
  2. // @name c.ai X Character Creation Helper
  3. // @namespace c.ai X Character Creation Helper
  4. // @version 2.6
  5. // @license MIT
  6. // @description Gives visual feedback for the definition
  7. // @author Vishanka
  8. // @match https://character.ai/*
  9. // @icon https://i.imgur.com/iH2r80g.png
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // Define the new CSS rule
  18. var newCss = ".custom-char-color { color: #ff4500 !important; }";
  19.  
  20. // Create a new <style> tag
  21. var style = document.createElement('style');
  22. style.type = 'text/css';
  23. style.innerHTML = newCss;
  24.  
  25. // Append the <style> tag to the <head> of the document
  26. document.head.appendChild(style);
  27.  
  28. const checkElementPresence = (selector, callback, fastIntervalTime = 1000, slowIntervalTime = 5000) => {
  29. let elementFoundPreviously = false;
  30. let fastCheck = true;
  31. let attempts = 0;
  32. let maxAttempts = 10;
  33.  
  34. const checkElement = () => {
  35. const element = document.querySelector(selector);
  36. // When element is found for the first time or reappears after being absent
  37. if (element) {
  38. if (!elementFoundPreviously) {
  39. callback(element);
  40. console.log(`Element ${selector} found and callback applied.`);
  41. elementFoundPreviously = true;
  42. // Reset fastCheck to quickly detect if it disappears again
  43. fastCheck = true;
  44. attempts = 0;
  45. }
  46. } else {
  47. if (elementFoundPreviously) {
  48. // Element was found before but now is missing, might have been removed
  49. console.warn(`Element ${selector} disappeared.`);
  50. elementFoundPreviously = false;
  51. // Speed up the checks temporarily to catch the re-appearance
  52. fastCheck = true;
  53. attempts = 0;
  54. }
  55. }
  56.  
  57. // Adjust checking interval based on state
  58. if (fastCheck && ++attempts >= maxAttempts) {
  59. // Switch to slower checking after a number of fast attempts
  60. console.warn(`Switching to slower check for ${selector}.`);
  61. fastCheck = false;
  62. }
  63.  
  64. // Continuously adjust timeout interval for checking based on fastCheck flag
  65. setTimeout(checkElement, fastCheck ? fastIntervalTime : slowIntervalTime);
  66. };
  67.  
  68. // Start checking
  69. checkElement();
  70. };
  71.  
  72. // Usage example
  73. checkElementPresence('#definitionSelector', (element) => {
  74. // Apply your action here
  75. console.log('Action applied to:', element);
  76. });
  77.  
  78.  
  79.  
  80. // Function to monitor elements on the page
  81. function monitorElements() {
  82. const containerSelectors = [
  83. 'div.flex-auto:nth-child(1) > div:nth-child(2)', // Container potentially holding the input
  84. 'div.relative:nth-child(5) > div:nth-child(1) > div:nth-child(1)', // Greeting
  85. 'div.relative:nth-child(4) > div:nth-child(1) > div:nth-child(1)' // Description
  86. ];
  87.  
  88. function updateInputCurrentName(newValue) {
  89. // Trim leading and trailing spaces and replace internal spaces with hyphens
  90. const processedValue = newValue.trim().replace(/\s+/g, '-');
  91. sessionStorage.setItem('inputCurrentName', processedValue);
  92. console.log(`Updated session storage with new input value: ${processedValue}`);
  93.  
  94. // Dispatch a custom event to indicate that the input value has changed
  95. window.dispatchEvent(new CustomEvent('inputCurrentNameChanged', { detail: { newValue: processedValue } }));
  96. }
  97.  
  98. containerSelectors.forEach(selector => {
  99. checkElementPresence(selector, (element) => {
  100. const inputElement = element.querySelector('input');
  101. if (inputElement) {
  102. inputElement.addEventListener('input', () => {
  103. updateInputCurrentName(inputElement.value);
  104. });
  105.  
  106. // Store initial value in session storage
  107. updateInputCurrentName(inputElement.value);
  108. } else {
  109. console.log(`Content of ${selector}:`, element.textContent);
  110. }
  111. });
  112. });
  113.  
  114.  
  115.  
  116.  
  117. // Selector for the definition
  118. const definitionSelector = '.transition > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)';
  119. checkElementPresence(definitionSelector, (element) => {
  120. const textarea = element.querySelector('textarea');
  121. if (textarea && !document.querySelector('.custom-definition-panel')) {
  122. // Initial panel setup
  123. updatePanel(textarea);
  124.  
  125. // Listen for the custom event to update the panel
  126. window.addEventListener('inputCurrentNameChanged', () => {
  127. updatePanel(textarea);
  128. });
  129.  
  130. // Observer to detect changes in the textarea content
  131. const observer = new MutationObserver(() => {
  132. updatePanel(textarea);
  133. });
  134.  
  135. observer.observe(textarea, { attributes: true, childList: true, subtree: true, characterData: true });
  136. }
  137. });
  138. }
  139.  
  140.  
  141.  
  142.  
  143. // Function to update or create the DefinitionFeedbackPanel based on textarea content
  144. function updatePanel(textarea) {
  145. let DefinitionFeedbackPanel = document.querySelector('.custom-definition-panel');
  146. if (!DefinitionFeedbackPanel) {
  147. DefinitionFeedbackPanel = document.createElement('div');
  148. DefinitionFeedbackPanel.classList.add('custom-definition-panel');
  149. textarea.parentNode.insertBefore(DefinitionFeedbackPanel, textarea);
  150. }
  151. DefinitionFeedbackPanel.innerHTML = ''; // Clear existing content
  152. DefinitionFeedbackPanel.style.border = '0px solid #ccc';
  153. DefinitionFeedbackPanel.style.padding = '10px';
  154. DefinitionFeedbackPanel.style.marginBottom = '10px';
  155. DefinitionFeedbackPanel.style.marginTop = '5px';
  156. DefinitionFeedbackPanel.style.maxHeight = '500px'; // Adjust the max-height as needed
  157. DefinitionFeedbackPanel.style.overflowY = 'auto';
  158.  
  159.  
  160. var plaintextColor = localStorage.getItem('plaintext_color');
  161. var defaultColor = '#D1D5DB';
  162. var color = plaintextColor || defaultColor;
  163. DefinitionFeedbackPanel.style.color = color;
  164.  
  165. const cleanedContent = textarea.value.trim();
  166. console.log(`Content of Definition:`, cleanedContent);
  167. const textLines = cleanedContent.split('\n');
  168.  
  169. let lastColor = '#222326';
  170. let isDialogueContinuation = false; // Track if the current line continues a dialogue
  171. let prevColor = null; // Track the previous line's color for detecting color changes
  172.  
  173. let consecutiveCharCount = 0;
  174. let consecutiveUserCount = 0; // Track the number of consecutive {{user}} lines
  175. let lastCharColor = '';
  176. let lastNamedCharacterColor = '';
  177.  
  178. function determineLineColor(line, prevLine) {
  179. // Extract the part of the line before the first colon
  180. const indexFirstColon = line.indexOf(':');
  181. const firstPartOfLine = indexFirstColon !== -1 ? line.substring(0, indexFirstColon + 1) : line;
  182. // Define the dialogue starter regex with updated conditions
  183. const dialogueStarterRegex = /^\{\{(?:char|user|random_user_[^\}]*)\}\}:|^{{[\S\s]+}}:|^[^\s:]+:/;
  184. const isDialogueStarter = dialogueStarterRegex.test(firstPartOfLine);
  185. const continuesDialogue = prevLine && prevLine.trim().endsWith(':') && (line.startsWith(' ') || !dialogueStarterRegex.test(firstPartOfLine));
  186.  
  187. const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
  188.  
  189. if (isDialogueStarter) {
  190. isDialogueContinuation = true;
  191. if (line.startsWith('{{char}}:')) {
  192. consecutiveCharCount++;
  193. if (currentTheme === 'dark') {
  194. lastColor = consecutiveCharCount % 2 === 0 ? '#26272B' : '#292A2E';
  195. lastCharColor = lastColor;
  196. } else {
  197. lastColor = consecutiveCharCount % 2 === 0 ? '#E4E4E7' : '#E3E3E6';
  198. lastCharColor = lastColor;
  199. }
  200. } else if (line.startsWith('{{user}}:')) {
  201. consecutiveUserCount++;
  202. if (currentTheme === 'dark') {
  203. lastColor = consecutiveUserCount % 2 === 0 ? '#363630' : '#383832';
  204. } else {
  205. lastColor = consecutiveUserCount % 2 === 0 ? '#D9D9DF' : '#D5D5DB'; // Light theme color
  206. }
  207. consecutiveCharCount = 0; // Reset this if you need to ensure it only affects consecutive {{char}} dialogues
  208.  
  209. } else if (line.match(/^\{\{(?:char|user|random_user_[^\}]*)\}\}:|^{{[\S\s]+}}:|^[\S]+:/)) {
  210. if (currentTheme === 'dark') {
  211. lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
  212. } else {
  213. lastNamedCharacterColor = lastNamedCharacterColor === '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
  214. }
  215. lastColor = lastNamedCharacterColor;
  216. }
  217. else if (line.match(/^\{\{random_user_[^\}]*\}\}:|^\{\{random_user_\}\}:|^{{random_user_}}/)) {
  218. if (currentTheme === 'dark') {
  219. lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
  220. } else {
  221. lastNamedCharacterColor = lastNamedCharacterColor === '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
  222. }
  223. lastColor = lastNamedCharacterColor;
  224. } else {
  225. consecutiveCharCount = 0;
  226. if (currentTheme === 'dark') {
  227. lastColor = '#474747' ? '#4C4C4D' : '#474747'; // Default case for non-{{char}} dialogues; adjust as needed
  228. } else {
  229. lastColor = '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
  230. }
  231. }
  232. } else if (line.startsWith('END_OF_DIALOG')) {
  233. isDialogueContinuation = false;
  234. lastColor = 'rgba(65, 65, 66, 0)';
  235. } else if (isDialogueContinuation && continuesDialogue) {
  236. // Do nothing, continuation of dialogue
  237. } else if (isDialogueContinuation && !isDialogueStarter) {
  238. // Do nothing, continuation of dialogue
  239. } else {
  240. isDialogueContinuation = false;
  241. lastColor = 'rgba(65, 65, 66, 0)';
  242. }
  243. return lastColor;
  244. }
  245.  
  246.  
  247. // Function to remove dialogue starters from the start of a line
  248. let trimmedParts = []; // Array to store trimmed parts
  249. let consecutiveLines = []; // Array to store consecutive lines with the same color
  250. //let prevColor = null;
  251.  
  252. function trimDialogueStarters(line) {
  253. // Find the index of the first colon
  254. const indexFirstColon = line.indexOf(':');
  255. // If there's no colon, return the line as is
  256. if (indexFirstColon === -1) return line;
  257.  
  258. // Extract the part of the line before the first colon to check against the regex
  259. const firstPartOfLine = line.substring(0, indexFirstColon + 1);
  260.  
  261. // Define the dialogue starter regex
  262. const dialogueStarterRegex = /^(\{\{char\}\}:|\{\{user\}\}:|\{\{random_user_[^\}]*\}\}:|^{{[\S\s]+}}:|^[^\s:]+:)\s*/;
  263.  
  264. // Check if the first part of the line matches the dialogue starter regex
  265. const trimmed = firstPartOfLine.match(dialogueStarterRegex);
  266. if (trimmed) {
  267. // Store the trimmed part
  268. trimmedParts.push(trimmed[0]);
  269. // Return the line without the dialogue starter and any leading whitespace that follows it
  270. return line.substring(indexFirstColon + 1).trim();
  271. }
  272.  
  273. // If the first part doesn't match, return the original line
  274. return line;
  275. }
  276.  
  277. function groupConsecutiveLines(color, lineDiv) {
  278. // Check if there are consecutive lines with the same color
  279. if (consecutiveLines.length > 0 && consecutiveLines[0].color === color) {
  280. consecutiveLines.push({ color, lineDiv });
  281. } else {
  282. // If not, append the previous group of consecutive lines and start a new group
  283. appendConsecutiveLines();
  284. consecutiveLines.push({ color, lineDiv });
  285. }
  286. }
  287.  
  288.  
  289.  
  290. function appendConsecutiveLines() {
  291. if (consecutiveLines.length > 0) {
  292. const groupDiv = document.createElement('div');
  293. const color = consecutiveLines[0].color;
  294.  
  295. const containerDiv = document.createElement('div');
  296. containerDiv.style.width = '100%';
  297.  
  298. groupDiv.style.backgroundColor = color;
  299. groupDiv.style.padding = '12px';
  300. groupDiv.style.paddingBottom = '15px'; // Increased bottom padding to provide space
  301. groupDiv.style.borderRadius = '16px';
  302. groupDiv.style.display = 'inline-block';
  303. groupDiv.style.maxWidth = '90%';
  304. groupDiv.style.position = 'relative'; // Set position as relative for the absolute positioning of countDiv
  305.  
  306. if (color === '#363630' || color === '#383832' || color === '#D9D9DF' || color === '#D5D5DB') {
  307. containerDiv.style.display = 'flex';
  308. containerDiv.style.justifyContent = 'flex-end';
  309. }
  310.  
  311. // Calculate total number of characters across all lines
  312. const totalSymbolCount = consecutiveLines.reduce((acc, { lineDiv }) => acc + lineDiv.textContent.length, 0);
  313.  
  314. consecutiveLines.forEach(({ lineDiv }) => {
  315. const lineContainer = document.createElement('div');
  316.  
  317. lineContainer.style.display = 'flex';
  318. lineContainer.style.justifyContent = 'space-between';
  319. lineContainer.style.alignItems = 'flex-end'; // Ensure items align to the bottom
  320. lineContainer.style.width = '100%'; // Ensure container takes full width
  321.  
  322. lineDiv.style.flexGrow = '1'; // Allow lineDiv to grow and fill space
  323. // Append the lineDiv to the container
  324. lineContainer.appendChild(lineDiv);
  325.  
  326. // Append the container to the groupDiv
  327. groupDiv.appendChild(lineContainer);
  328. });
  329.  
  330. const countDiv = document.createElement('div');
  331. countDiv.textContent = `${totalSymbolCount}`;
  332. countDiv.style.position = 'absolute'; // Use absolute positioning
  333. countDiv.style.bottom = '3px'; // Position at the bottom
  334. countDiv.style.right = '12px'; // Position on the right
  335. countDiv.style.fontSize = '11px';
  336. // darkmode user
  337. if (color === '#363630' || color === '#383832'){
  338. countDiv.style.color = '#5C5C52';
  339. //lightmode user
  340. } else if (color === '#D9D9DF' || color === '#D5D5DB') {
  341. countDiv.style.color = '#B3B3B8';
  342. //darkmode char
  343. } else if (color === '#26272B' || color === '#292A2E') {
  344. countDiv.style.color = '#44464D';
  345. //lightmode char
  346. } else if (color === '#E4E4E7' || color === '#E3E3E6') {
  347. countDiv.style.color = '#C4C4C7';
  348. //darkmode random
  349. } else if (color === '#474747' || color === '#4C4C4D') {
  350. countDiv.style.color = '#6E6E6E';
  351. //lightmode random
  352. } else if (color === '#CFDCE8' || color === '#CCDAE6') {
  353. countDiv.style.color = '#B4BFC9';
  354. } else {
  355. countDiv.style.color = 'rgba(65, 65, 66, 0)';
  356. }
  357.  
  358. // Append the countDiv to the groupDiv
  359. groupDiv.appendChild(countDiv);
  360.  
  361. // Add the groupDiv to the containerDiv (flex or not based on color)
  362. containerDiv.appendChild(groupDiv);
  363.  
  364. // Append the containerDiv to the DefinitionFeedbackPanel
  365. DefinitionFeedbackPanel.appendChild(containerDiv);
  366. consecutiveLines = []; // Clear the array
  367. }
  368. }
  369.  
  370.  
  371.  
  372.  
  373. function formatText(text) {
  374. // Retrieve the value of 'inputCurrentName' from session storage
  375. const inputCurrentName = sessionStorage.getItem('inputCurrentName') || '';
  376.  
  377. text = text.replace(/(?:{{)+char(?:}})+|{{((?!{{char}}|{{user}}|random_user_)[^}]+)}}/g, function(match, p1) {
  378. if (match.includes('char')) {
  379. return match.replace(/{{char}}/g, inputCurrentName);
  380. } else if (p1 === 'user') {
  381. return match;
  382. } else {
  383. return p1.replace(/ /g, '-');
  384. }
  385. });
  386.  
  387.  
  388. // Process bold italic before bold or italic to avoid nesting conflicts
  389. text = text.replace(/\*\*\*([^*]+)\*\*\*/g, '<em><strong>$1</strong></em>');
  390. // Replace text wrapped in double asterisks with <strong> tags for bold
  391. text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
  392. // Replace text wrapped in single asterisks with <em> tags for italics
  393. text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
  394.  
  395. return text;
  396. }
  397.  
  398.  
  399. textLines.forEach((line, index) => {
  400. const prevLine = index > 0 ? textLines[index - 1] : null;
  401. const currentColor = determineLineColor(line, prevLine);
  402. const trimmedLine = trimDialogueStarters(line);
  403.  
  404. if (prevColor && currentColor !== prevColor) {
  405. appendConsecutiveLines(); // Append previous group of consecutive lines
  406.  
  407. const spacingDiv = document.createElement('div');
  408. spacingDiv.style.marginBottom = '20px';
  409. DefinitionFeedbackPanel.appendChild(spacingDiv);
  410. }
  411.  
  412. const lineDiv = document.createElement('div');
  413. lineDiv.style.wordWrap = 'break-word'; // Allow text wrapping
  414.  
  415. if (trimmedLine.startsWith("END_OF_DIALOG")) {
  416. appendConsecutiveLines(); // Make sure to append any pending groups before adding the divider
  417. const separatorLine = document.createElement('hr');
  418. DefinitionFeedbackPanel.appendChild(separatorLine); // This ensures the divider is on a new line
  419. } else {
  420. if (trimmedParts.length > 0) {
  421. const headerDiv = document.createElement('div');
  422. let headerText = trimmedParts.shift();
  423.  
  424. // Apply replacements for '{{char}}' and other placeholders within double curly brackets
  425. headerText = headerText.replace(/{{char}}/g, sessionStorage.getItem('inputCurrentName') || '');
  426.  
  427. // Apply logic to handle text within curly brackets, excluding '{{user}}'
  428. headerText = headerText.replace(/{{((?!random_user_)[^}]+)}}/g, function(match, p1) {
  429. if (p1 === 'user') {
  430. return match;
  431. }
  432. return p1.replace(/ /g, '-');
  433. });
  434.  
  435.  
  436. headerText = headerText.replace(/:/g, ''); // Remove colons
  437. headerDiv.textContent = headerText;
  438.  
  439. const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
  440. headerDiv.style.color = currentTheme === 'dark' ? '#A2A2AC' : '#26272B';
  441. if (headerText.includes('{{user}}')) {
  442. headerDiv.style.textAlign = 'right';
  443. }
  444. DefinitionFeedbackPanel.appendChild(headerDiv);
  445. }
  446.  
  447. if (trimmedLine.trim() === '') {
  448. lineDiv.appendChild(document.createElement('br'));
  449. } else {
  450. const paragraph = document.createElement('p');
  451. // Call formatTextForItalics to wrap text in asterisks with <em> tags
  452. paragraph.innerHTML = formatText(trimmedLine);
  453. lineDiv.appendChild(paragraph);
  454. }
  455.  
  456. groupConsecutiveLines(currentColor, lineDiv);
  457. }
  458.  
  459. prevColor = currentColor;
  460. });
  461.  
  462. appendConsecutiveLines();
  463.  
  464.  
  465.  
  466. }
  467.  
  468.  
  469. // Monitor for URL changes to re-initialize element monitoring
  470. let currentUrl = window.location.href;
  471. setInterval(() => {
  472. if (window.location.href !== currentUrl) {
  473. console.log("URL changed. Re-initializing element monitoring.");
  474. currentUrl = window.location.href;
  475. monitorElements();
  476. }
  477. }, 1000);
  478.  
  479. monitorElements();
  480. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址