c.ai X Character Creation Helper

Gives visual feedback for the definition

Ajankohdalta 3.4.2024. Katso uusin versio.

  1. // ==UserScript==
  2. // @name c.ai X Character Creation Helper
  3. // @namespace c.ai X Character Creation Helper
  4. // @version 2.1
  5. // @license MIT
  6. // @description Gives visual feedback for the definition
  7. // @author Vishanka
  8. // @match https://character.ai/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Function to check for element's presence and execute a callback when found
  17. const checkElementPresence = (selector, callback, maxAttempts = 10) => {
  18. let attempts = 0;
  19. const interval = setInterval(() => {
  20. const element = document.querySelector(selector);
  21. if (element) {
  22. clearInterval(interval);
  23. callback(element);
  24. } else if (++attempts >= maxAttempts) {
  25. clearInterval(interval);
  26. console.warn(`Element ${selector} not found after ${maxAttempts} attempts.`);
  27. }
  28. }, 1000);
  29. };
  30.  
  31. // Function to monitor elements on the page
  32. function monitorElements() {
  33. const initialElementIds = [
  34. 'div.flex-auto:nth-child(1) > div:nth-child(2) > div:nth-child(1)',
  35. 'div.relative:nth-child(5) > div:nth-child(1) > div:nth-child(1)', // Greeting
  36. 'div.relative:nth-child(4) > div:nth-child(1) > div:nth-child(1)' // Description
  37. ];
  38.  
  39.  
  40. initialElementIds.forEach(selector => {
  41. checkElementPresence(selector, (element) => {
  42. console.log(`Content of ${selector}:`, element.textContent);
  43. });
  44. });
  45.  
  46. // Selector for the definition
  47. const definitionSelector = '.transition > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)';
  48. checkElementPresence(definitionSelector, (element) => {
  49. const textarea = element.querySelector('textarea');
  50. if (textarea && !document.querySelector('.custom-definition-panel')) {
  51. updatePanel(textarea); // Initial panel setup
  52.  
  53. // Observer to detect changes in the textarea content
  54. const observer = new MutationObserver(() => {
  55. updatePanel(textarea);
  56. });
  57.  
  58. observer.observe(textarea, {attributes: true, childList: true, subtree: true, characterData: true});
  59. }
  60. });
  61. }
  62.  
  63. // Function to update or create the DefinitionFeedbackPanel based on textarea content
  64. function updatePanel(textarea) {
  65. let DefinitionFeedbackPanel = document.querySelector('.custom-definition-panel');
  66. if (!DefinitionFeedbackPanel) {
  67. DefinitionFeedbackPanel = document.createElement('div');
  68. DefinitionFeedbackPanel.classList.add('custom-definition-panel');
  69. textarea.parentNode.insertBefore(DefinitionFeedbackPanel, textarea);
  70. }
  71. DefinitionFeedbackPanel.innerHTML = ''; // Clear existing content
  72. DefinitionFeedbackPanel.style.border = '0px solid #ccc';
  73. DefinitionFeedbackPanel.style.padding = '10px';
  74. DefinitionFeedbackPanel.style.marginBottom = '10px';
  75. DefinitionFeedbackPanel.style.marginTop = '5px';
  76. DefinitionFeedbackPanel.style.maxHeight = '500px'; // Adjust the max-height as needed
  77. DefinitionFeedbackPanel.style.overflowY = 'auto';
  78.  
  79.  
  80. var plaintextColor = localStorage.getItem('plaintext_color');
  81. var defaultColor = '#D1D5DB';
  82. var color = plaintextColor || defaultColor;
  83. DefinitionFeedbackPanel.style.color = color;
  84.  
  85. const cleanedContent = textarea.value.trim();
  86. console.log(`Content of Definition:`, cleanedContent);
  87. const textLines = cleanedContent.split('\n');
  88.  
  89. let lastColor = '#222326';
  90. let isDialogueContinuation = false; // Track if the current line continues a dialogue
  91. let prevColor = null; // Track the previous line's color for detecting color changes
  92.  
  93. let consecutiveCharCount = 0;
  94. let consecutiveUserCount = 0; // Track the number of consecutive {{user}} lines
  95. let lastCharColor = '';
  96. let lastNamedCharacterColor = '';
  97.  
  98. function determineLineColor(line, prevLine) {
  99. // Extract the part of the line before the first colon
  100. const indexFirstColon = line.indexOf(':');
  101. const firstPartOfLine = indexFirstColon !== -1 ? line.substring(0, indexFirstColon + 1) : line;
  102. // Define the dialogue starter regex with updated conditions
  103. const dialogueStarterRegex = /^\{\{(?:char|user|random_user_[^\}]*)\}\}:|^{{[\S\s]+}}:|^[^\s:]+:/;
  104. const isDialogueStarter = dialogueStarterRegex.test(firstPartOfLine);
  105. const continuesDialogue = prevLine && prevLine.trim().endsWith(':') && (line.startsWith(' ') || !dialogueStarterRegex.test(firstPartOfLine));
  106.  
  107. const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
  108.  
  109. if (isDialogueStarter) {
  110. isDialogueContinuation = true;
  111. if (line.startsWith('{{char}}:')) {
  112. consecutiveCharCount++;
  113. if (currentTheme === 'dark') {
  114. lastColor = consecutiveCharCount % 2 === 0 ? '#26272B' : '#292A2E';
  115. lastCharColor = lastColor;
  116. } else {
  117. lastColor = consecutiveCharCount % 2 === 0 ? '#E4E4E7' : '#E3E3E6';
  118. lastCharColor = lastColor;
  119. }
  120. } else if (line.startsWith('{{user}}:')) {
  121. consecutiveUserCount++;
  122. if (currentTheme === 'dark') {
  123. lastColor = consecutiveUserCount % 2 === 0 ? '#363630' : '#383832';
  124. } else {
  125. lastColor = consecutiveUserCount % 2 === 0 ? '#D9D9DF' : '#D5D5DB'; // Light theme color
  126. }
  127. consecutiveCharCount = 0; // Reset this if you need to ensure it only affects consecutive {{char}} dialogues
  128.  
  129. } else if (line.match(/^\{\{(?:char|user|random_user_[^\}]*)\}\}:|^{{[\S\s]+}}:|^[\S]+:/)) {
  130. if (currentTheme === 'dark') {
  131. lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
  132. } else {
  133. lastNamedCharacterColor = lastNamedCharacterColor === '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
  134. }
  135. lastColor = lastNamedCharacterColor;
  136. }
  137. else if (line.match(/^\{\{random_user_[^\}]*\}\}:|^\{\{random_user_\}\}:|^{{random_user_}}/)) {
  138. if (currentTheme === 'dark') {
  139. lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
  140. } else {
  141. lastNamedCharacterColor = lastNamedCharacterColor === '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
  142. }
  143. lastColor = lastNamedCharacterColor;
  144. } else {
  145. consecutiveCharCount = 0;
  146. if (currentTheme === 'dark') {
  147. lastColor = '#474747' ? '#4C4C4D' : '#474747'; // Default case for non-{{char}} dialogues; adjust as needed
  148. } else {
  149. lastColor = '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
  150. }
  151. }
  152. } else if (line.startsWith('END_OF_DIALOG')) {
  153. isDialogueContinuation = false;
  154. lastColor = 'rgba(65, 65, 66, 0)';
  155. } else if (isDialogueContinuation && continuesDialogue) {
  156. // Do nothing, continuation of dialogue
  157. } else if (isDialogueContinuation && !isDialogueStarter) {
  158. // Do nothing, continuation of dialogue
  159. } else {
  160. isDialogueContinuation = false;
  161. lastColor = 'rgba(65, 65, 66, 0)';
  162. }
  163. return lastColor;
  164. }
  165.  
  166.  
  167. // Function to remove dialogue starters from the start of a line
  168. let trimmedParts = []; // Array to store trimmed parts
  169. let consecutiveLines = []; // Array to store consecutive lines with the same color
  170. //let prevColor = null;
  171.  
  172. function trimDialogueStarters(line) {
  173. // Find the index of the first colon
  174. const indexFirstColon = line.indexOf(':');
  175. // If there's no colon, return the line as is
  176. if (indexFirstColon === -1) return line;
  177.  
  178. // Extract the part of the line before the first colon to check against the regex
  179. const firstPartOfLine = line.substring(0, indexFirstColon + 1);
  180.  
  181. // Define the dialogue starter regex
  182. const dialogueStarterRegex = /^(\{\{char\}\}:|\{\{user\}\}:|\{\{random_user_[^\}]*\}\}:|^{{[\S\s]+}}:|^[^\s:]+:)\s*/;
  183.  
  184. // Check if the first part of the line matches the dialogue starter regex
  185. const trimmed = firstPartOfLine.match(dialogueStarterRegex);
  186. if (trimmed) {
  187. // Store the trimmed part
  188. trimmedParts.push(trimmed[0]);
  189. // Return the line without the dialogue starter and any leading whitespace that follows it
  190. return line.substring(indexFirstColon + 1).trim();
  191. }
  192.  
  193. // If the first part doesn't match, return the original line
  194. return line;
  195. }
  196.  
  197. function groupConsecutiveLines(color, lineDiv) {
  198. // Check if there are consecutive lines with the same color
  199. if (consecutiveLines.length > 0 && consecutiveLines[0].color === color) {
  200. consecutiveLines.push({ color, lineDiv });
  201. } else {
  202. // If not, append the previous group of consecutive lines and start a new group
  203. appendConsecutiveLines();
  204. consecutiveLines.push({ color, lineDiv });
  205. }
  206. }
  207.  
  208.  
  209.  
  210. function appendConsecutiveLines() {
  211. if (consecutiveLines.length > 0) {
  212. const groupDiv = document.createElement('div');
  213. const color = consecutiveLines[0].color;
  214.  
  215. const containerDiv = document.createElement('div');
  216. containerDiv.style.width = '100%';
  217.  
  218. groupDiv.style.backgroundColor = color;
  219. groupDiv.style.padding = '12px';
  220. groupDiv.style.paddingBottom = '15px'; // Increased bottom padding to provide space
  221. groupDiv.style.borderRadius = '16px';
  222. groupDiv.style.display = 'inline-block';
  223. groupDiv.style.maxWidth = '90%';
  224. groupDiv.style.position = 'relative'; // Set position as relative for the absolute positioning of countDiv
  225.  
  226. if (color === '#363630' || color === '#383832' || color === '#D9D9DF' || color === '#D5D5DB') {
  227. containerDiv.style.display = 'flex';
  228. containerDiv.style.justifyContent = 'flex-end';
  229. }
  230.  
  231. // Calculate total number of characters across all lines
  232. const totalSymbolCount = consecutiveLines.reduce((acc, { lineDiv }) => acc + lineDiv.textContent.length, 0);
  233.  
  234. consecutiveLines.forEach(({ lineDiv }) => {
  235. const lineContainer = document.createElement('div');
  236.  
  237. lineContainer.style.display = 'flex';
  238. lineContainer.style.justifyContent = 'space-between';
  239. lineContainer.style.alignItems = 'flex-end'; // Ensure items align to the bottom
  240. lineContainer.style.width = '100%'; // Ensure container takes full width
  241.  
  242. lineDiv.style.flexGrow = '1'; // Allow lineDiv to grow and fill space
  243. // Append the lineDiv to the container
  244. lineContainer.appendChild(lineDiv);
  245.  
  246. // Append the container to the groupDiv
  247. groupDiv.appendChild(lineContainer);
  248. });
  249.  
  250. const countDiv = document.createElement('div');
  251. countDiv.textContent = `${totalSymbolCount}`;
  252. countDiv.style.position = 'absolute'; // Use absolute positioning
  253. countDiv.style.bottom = '3px'; // Position at the bottom
  254. countDiv.style.right = '12px'; // Position on the right
  255. countDiv.style.fontSize = '11px';
  256. // darkmode user
  257. if (color === '#363630' || color === '#383832'){
  258. countDiv.style.color = '#5C5C52';
  259. //lightmode user
  260. } else if (color === '#D9D9DF' || color === '#D5D5DB') {
  261. countDiv.style.color = '#B3B3B8';
  262. //darkmode char
  263. } else if (color === '#26272B' || color === '#292A2E') {
  264. countDiv.style.color = '#44464D';
  265. //lightmode char
  266. } else if (color === '#E4E4E7' || color === '#E3E3E6') {
  267. countDiv.style.color = '#C4C4C7';
  268. //darkmode random
  269. } else if (color === '#474747' || color === '#4C4C4D') {
  270. countDiv.style.color = '#6E6E6E';
  271. //lightmode random
  272. } else if (color === '#CFDCE8' || color === '#CCDAE6') {
  273. countDiv.style.color = '#B4BFC9';
  274. } else {
  275. countDiv.style.color = 'rgba(65, 65, 66, 0)';
  276. }
  277.  
  278. // Append the countDiv to the groupDiv
  279. groupDiv.appendChild(countDiv);
  280.  
  281. // Add the groupDiv to the containerDiv (flex or not based on color)
  282. containerDiv.appendChild(groupDiv);
  283.  
  284. // Append the containerDiv to the DefinitionFeedbackPanel
  285. DefinitionFeedbackPanel.appendChild(containerDiv);
  286. consecutiveLines = []; // Clear the array
  287. }
  288. }
  289.  
  290.  
  291. function formatText(text) {
  292. // Handle headers; replace Markdown headers (# Header) with <h1>, <h2>, etc.
  293. text = text.replace(/^(######\s)(.*)$/gm, '<h6>$2</h6>'); // For h6
  294. text = text.replace(/^(#####\s)(.*)$/gm, '<h5>$2</h5>'); // For h5
  295. text = text.replace(/^(####\s)(.*)$/gm, '<h4>$2</h4>'); // For h4
  296. text = text.replace(/^(###\s)(.*)$/gm, '<h3>$2</h3>'); // For h3
  297. text = text.replace(/^(##\s)(.*)$/gm, '<h2>$2</h2>'); // For h2
  298. text = text.replace(/^(#\s)(.*)$/gm, '<h1>$2</h1>'); // For h1
  299.  
  300. // Process bold italic before bold or italic to avoid nesting conflicts
  301. text = text.replace(/\*\*\*([^*]+)\*\*\*/g, '<em><strong>$1</strong></em>');
  302. // Replace text wrapped in double asterisks with <strong> tags for bold
  303. text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
  304. // Finally, replace text wrapped in single asterisks with <em> tags for italics
  305. text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
  306.  
  307. return text;
  308. }
  309.  
  310.  
  311.  
  312. textLines.forEach((line, index) => {
  313. const prevLine = index > 0 ? textLines[index - 1] : null;
  314. const currentColor = determineLineColor(line, prevLine);
  315. const trimmedLine = trimDialogueStarters(line);
  316.  
  317. if (prevColor && currentColor !== prevColor) {
  318. appendConsecutiveLines(); // Append previous group of consecutive lines
  319.  
  320. const spacingDiv = document.createElement('div');
  321. spacingDiv.style.marginBottom = '20px';
  322. DefinitionFeedbackPanel.appendChild(spacingDiv);
  323. }
  324.  
  325. const lineDiv = document.createElement('div');
  326. lineDiv.style.wordWrap = 'break-word'; // Allow text wrapping
  327.  
  328. if (trimmedLine.startsWith("END_OF_DIALOG")) {
  329. appendConsecutiveLines(); // Make sure to append any pending groups before adding the divider
  330. const separatorLine = document.createElement('hr');
  331. DefinitionFeedbackPanel.appendChild(separatorLine); // This ensures the divider is on a new line
  332. } else {
  333. if (trimmedParts.length > 0) {
  334. const headerDiv = document.createElement('div');
  335. const headerText = trimmedParts.shift();
  336. const formattedHeaderText = headerText.replace(/:/g, '');
  337. headerDiv.textContent = formattedHeaderText;
  338. const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
  339. if (currentTheme === 'dark') {
  340. headerDiv.style.color = '#A2A2AC'; // Dark mode text color
  341. } else {
  342. headerDiv.style.color = '#26272B';
  343. }
  344. if (formattedHeaderText.includes('{{user}}')) {
  345. headerDiv.style.textAlign = 'right';
  346. }
  347. DefinitionFeedbackPanel.appendChild(headerDiv);
  348. }
  349.  
  350. if (trimmedLine.trim() === '') {
  351. lineDiv.appendChild(document.createElement('br'));
  352. } else {
  353. const paragraph = document.createElement('p');
  354. // Call formatTextForItalics to wrap text in asterisks with <em> tags
  355. paragraph.innerHTML = formatText(trimmedLine);
  356. lineDiv.appendChild(paragraph);
  357. }
  358.  
  359. groupConsecutiveLines(currentColor, lineDiv);
  360. }
  361.  
  362. prevColor = currentColor;
  363. });
  364.  
  365. appendConsecutiveLines();
  366.  
  367.  
  368.  
  369.  
  370. }
  371.  
  372.  
  373.  
  374. // Monitor for URL changes to re-initialize element monitoring
  375. let currentUrl = window.location.href;
  376. setInterval(() => {
  377. if (window.location.href !== currentUrl) {
  378. console.log("URL changed. Re-initializing element monitoring.");
  379. currentUrl = window.location.href;
  380. monitorElements();
  381. }
  382. }, 1000);
  383.  
  384. monitorElements();
  385. })();