c.ai X Character Creation Helper

Gives visual feedback for the definition

当前为 2024-04-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name c.ai X Character Creation Helper
  3. // @namespace c.ai X Character Creation Helper
  4. // @version 1.8
  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. initialElementIds.forEach(selector => {
  40. checkElementPresence(selector, (element) => {
  41. console.log(`Content of ${selector}:`, element.textContent);
  42. });
  43. });
  44.  
  45. // Selector for the definition
  46. const definitionSelector = '.transition > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)';
  47. checkElementPresence(definitionSelector, (element) => {
  48. const textarea = element.querySelector('textarea');
  49. if (textarea && !document.querySelector('.custom-definition-panel')) {
  50. updatePanel(textarea); // Initial panel setup
  51.  
  52. // Observer to detect changes in the textarea content
  53. const observer = new MutationObserver(() => {
  54. updatePanel(textarea);
  55. });
  56.  
  57. observer.observe(textarea, {attributes: true, childList: true, subtree: true, characterData: true});
  58. }
  59. });
  60. }
  61.  
  62. // Function to update or create the DefinitionFeedbackPanel based on textarea content
  63. function updatePanel(textarea) {
  64. let DefinitionFeedbackPanel = document.querySelector('.custom-definition-panel');
  65. if (!DefinitionFeedbackPanel) {
  66. DefinitionFeedbackPanel = document.createElement('div');
  67. DefinitionFeedbackPanel.classList.add('custom-definition-panel');
  68. textarea.parentNode.insertBefore(DefinitionFeedbackPanel, textarea);
  69. }
  70. DefinitionFeedbackPanel.innerHTML = ''; // Clear existing content
  71. DefinitionFeedbackPanel.style.border = '0px solid #ccc';
  72. DefinitionFeedbackPanel.style.padding = '10px';
  73. DefinitionFeedbackPanel.style.marginBottom = '10px';
  74. var plaintextColor = localStorage.getItem('plaintext_color');
  75. var defaultColor = '#D1D5DB';
  76. var color = plaintextColor || defaultColor;
  77. DefinitionFeedbackPanel.style.color = color;
  78.  
  79. const cleanedContent = textarea.value.trim();
  80. console.log(`Content of Definition:`, cleanedContent);
  81. const textLines = cleanedContent.split('\n');
  82.  
  83. let lastColor = '#222326';
  84. let isDialogueContinuation = false; // Track if the current line continues a dialogue
  85. let prevColor = null; // Track the previous line's color for detecting color changes
  86.  
  87. let consecutiveCharCount = 0;
  88. let consecutiveUserCount = 0; // Track the number of consecutive {{user}} lines
  89. let lastCharColor = '';
  90. let lastNamedCharacterColor = '';
  91.  
  92. function determineLineColor(line, prevLine) {
  93. // Extract the part of the line before the first colon
  94. const indexFirstColon = line.indexOf(':');
  95. const firstPartOfLine = indexFirstColon !== -1 ? line.substring(0, indexFirstColon + 1) : line;
  96. // Define the dialogue starter regex with updated conditions
  97. const dialogueStarterRegex = /^\{\{(?:char|user|random_user_[^\}]*)\}\}:|^{{[\S\s]+}}:|^[^\s:]+:/;
  98. const isDialogueStarter = dialogueStarterRegex.test(firstPartOfLine);
  99. const continuesDialogue = prevLine && prevLine.trim().endsWith(':') && (line.startsWith(' ') || !dialogueStarterRegex.test(firstPartOfLine));
  100.  
  101. const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
  102.  
  103. if (isDialogueStarter) {
  104. isDialogueContinuation = true;
  105. if (line.startsWith('{{char}}:')) {
  106. consecutiveCharCount++;
  107. if (currentTheme === 'dark') {
  108. lastColor = consecutiveCharCount % 2 === 0 ? '#26272B' : '#292A2E';
  109. lastCharColor = lastColor;
  110. } else {
  111. lastColor = consecutiveCharCount % 2 === 0 ? '#E4E4E7' : '#E3E3E6';
  112. lastCharColor = lastColor;
  113. }
  114. } else if (line.startsWith('{{user}}:')) {
  115. consecutiveUserCount++;
  116. if (currentTheme === 'dark') {
  117. lastColor = consecutiveUserCount % 2 === 0 ? '#363630' : '#383832';
  118. } else {
  119. lastColor = consecutiveUserCount % 2 === 0 ? '#D9D9DF' : '#D5D5DB'; // Light theme color
  120. }
  121. consecutiveCharCount = 0; // Reset this if you need to ensure it only affects consecutive {{char}} dialogues
  122.  
  123. } else if (line.match(/^\{\{(?:char|user|random_user_[^\}]*)\}\}:|^{{[\S\s]+}}:|^[\S]+:/)) {
  124. if (currentTheme === 'dark') {
  125. lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
  126. } else {
  127. lastNamedCharacterColor = lastNamedCharacterColor === '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
  128. }
  129. lastColor = lastNamedCharacterColor;
  130. }
  131. else if (line.match(/^\{\{random_user_[^\}]*\}\}:|^\{\{random_user_\}\}:|^{{random_user_}}/)) {
  132. if (currentTheme === 'dark') {
  133. lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
  134. } else {
  135. lastNamedCharacterColor = lastNamedCharacterColor === '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
  136. }
  137. lastColor = lastNamedCharacterColor;
  138. } else {
  139. consecutiveCharCount = 0;
  140. if (currentTheme === 'dark') {
  141. lastColor = '#474747' ? '#4C4C4D' : '#474747'; // Default case for non-{{char}} dialogues; adjust as needed
  142. } else {
  143. lastColor = '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
  144. }
  145. }
  146. } else if (line.startsWith('END_OF_DIALOG')) {
  147. isDialogueContinuation = false;
  148. lastColor = 'rgba(65, 65, 66, 0)';
  149. } else if (isDialogueContinuation && continuesDialogue) {
  150. // Do nothing, continuation of dialogue
  151. } else if (isDialogueContinuation && !isDialogueStarter) {
  152. // Do nothing, continuation of dialogue
  153. } else {
  154. isDialogueContinuation = false;
  155. lastColor = 'rgba(65, 65, 66, 0)';
  156. }
  157. return lastColor;
  158. }
  159.  
  160.  
  161. // Function to remove dialogue starters from the start of a line
  162. let trimmedParts = []; // Array to store trimmed parts
  163. let consecutiveLines = []; // Array to store consecutive lines with the same color
  164. //let prevColor = null;
  165.  
  166. function trimDialogueStarters(line) {
  167. // Find the index of the first colon
  168. const indexFirstColon = line.indexOf(':');
  169. // If there's no colon, return the line as is
  170. if (indexFirstColon === -1) return line;
  171.  
  172. // Extract the part of the line before the first colon to check against the regex
  173. const firstPartOfLine = line.substring(0, indexFirstColon + 1);
  174.  
  175. // Define the dialogue starter regex
  176. const dialogueStarterRegex = /^(\{\{char\}\}:|\{\{user\}\}:|\{\{random_user_[^\}]*\}\}:|^{{[\S\s]+}}:|^[^\s:]+:)\s*/;
  177.  
  178. // Check if the first part of the line matches the dialogue starter regex
  179. const trimmed = firstPartOfLine.match(dialogueStarterRegex);
  180. if (trimmed) {
  181. // Store the trimmed part
  182. trimmedParts.push(trimmed[0]);
  183. // Return the line without the dialogue starter and any leading whitespace that follows it
  184. return line.substring(indexFirstColon + 1).trim();
  185. }
  186.  
  187. // If the first part doesn't match, return the original line
  188. return line;
  189. }
  190.  
  191. function groupConsecutiveLines(color, lineDiv) {
  192. // Check if there are consecutive lines with the same color
  193. if (consecutiveLines.length > 0 && consecutiveLines[0].color === color) {
  194. consecutiveLines.push({ color, lineDiv });
  195. } else {
  196. // If not, append the previous group of consecutive lines and start a new group
  197. appendConsecutiveLines();
  198. consecutiveLines.push({ color, lineDiv });
  199. }
  200. }
  201.  
  202. function appendConsecutiveLines() {
  203. if (consecutiveLines.length > 0) {
  204. const groupDiv = document.createElement('div');
  205. const color = consecutiveLines[0].color;
  206.  
  207. // Create a container div that could potentially use flexbox
  208. const containerDiv = document.createElement('div');
  209. containerDiv.style.width = '100%';
  210.  
  211. groupDiv.style.backgroundColor = color;
  212. groupDiv.style.padding = '12px';
  213. groupDiv.style.borderRadius = '16px';
  214. groupDiv.style.display = 'inline-block';
  215. groupDiv.style.maxWidth = '90%'; // You might adjust this as needed
  216.  
  217. // Only apply flexbox styling if the color condition is met
  218. if (color === '#363630' || color === '#383832' || color === '#D9D9DF' || color === '#D5D5DB') {
  219. containerDiv.style.display = 'flex';
  220. containerDiv.style.justifyContent = 'flex-end'; // Aligns the child div to the right
  221. }
  222.  
  223. consecutiveLines.forEach(({ lineDiv }) => {
  224. groupDiv.appendChild(lineDiv);
  225. });
  226.  
  227. // Add the groupDiv to the containerDiv (flex or not based on color)
  228. containerDiv.appendChild(groupDiv);
  229.  
  230. // Append the containerDiv to the DefinitionFeedbackPanel
  231. DefinitionFeedbackPanel.appendChild(containerDiv);
  232. consecutiveLines = []; // Clear the array
  233. }
  234. }
  235.  
  236.  
  237.  
  238. function formatText(text) {
  239. // Handle headers; replace Markdown headers (# Header) with <h1>, <h2>, etc.
  240. text = text.replace(/^(######\s)(.*)$/gm, '<h6>$2</h6>'); // For h6
  241. text = text.replace(/^(#####\s)(.*)$/gm, '<h5>$2</h5>'); // For h5
  242. text = text.replace(/^(####\s)(.*)$/gm, '<h4>$2</h4>'); // For h4
  243. text = text.replace(/^(###\s)(.*)$/gm, '<h3>$2</h3>'); // For h3
  244. text = text.replace(/^(##\s)(.*)$/gm, '<h2>$2</h2>'); // For h2
  245. text = text.replace(/^(#\s)(.*)$/gm, '<h1>$2</h1>'); // For h1
  246.  
  247. // Process bold italic before bold or italic to avoid nesting conflicts
  248. text = text.replace(/\*\*\*([^*]+)\*\*\*/g, '<em><strong>$1</strong></em>');
  249. // Replace text wrapped in double asterisks with <strong> tags for bold
  250. text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
  251. // Finally, replace text wrapped in single asterisks with <em> tags for italics
  252. text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
  253.  
  254. return text;
  255. }
  256.  
  257.  
  258.  
  259. textLines.forEach((line, index) => {
  260. const prevLine = index > 0 ? textLines[index - 1] : null;
  261. const currentColor = determineLineColor(line, prevLine);
  262. const trimmedLine = trimDialogueStarters(line);
  263.  
  264. if (prevColor && currentColor !== prevColor) {
  265. appendConsecutiveLines(); // Append previous group of consecutive lines
  266.  
  267. const spacingDiv = document.createElement('div');
  268. spacingDiv.style.marginBottom = '20px';
  269. DefinitionFeedbackPanel.appendChild(spacingDiv);
  270. }
  271.  
  272. const lineDiv = document.createElement('div');
  273. lineDiv.style.wordWrap = 'break-word'; // Allow text wrapping
  274.  
  275. if (trimmedLine.startsWith("END_OF_DIALOG")) {
  276. appendConsecutiveLines(); // Make sure to append any pending groups before adding the divider
  277. const separatorLine = document.createElement('hr');
  278. DefinitionFeedbackPanel.appendChild(separatorLine); // This ensures the divider is on a new line
  279. } else {
  280. if (trimmedParts.length > 0) {
  281. const headerDiv = document.createElement('div');
  282. const headerText = trimmedParts.shift();
  283. const formattedHeaderText = headerText.replace(/:/g, '');
  284. headerDiv.textContent = formattedHeaderText;
  285. // Determine the current theme based on the class of the <html> element
  286. const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
  287.  
  288. // Set text color based on the current theme
  289. if (currentTheme === 'dark') {
  290. headerDiv.style.color = '#A2A2AC'; // Dark mode text color
  291. } else {
  292. headerDiv.style.color = '#26272B'; // Light mode text color (black)
  293. }
  294. if (formattedHeaderText.includes('{{user}}')) {
  295. headerDiv.style.textAlign = 'right';
  296. }
  297. DefinitionFeedbackPanel.appendChild(headerDiv);
  298. }
  299.  
  300. if (trimmedLine.trim() === '') {
  301. lineDiv.appendChild(document.createElement('br'));
  302. } else {
  303. const paragraph = document.createElement('p');
  304. // Call formatTextForItalics to wrap text in asterisks with <em> tags
  305. paragraph.innerHTML = formatText(trimmedLine);
  306. lineDiv.appendChild(paragraph);
  307. }
  308.  
  309. groupConsecutiveLines(currentColor, lineDiv);
  310. }
  311.  
  312. prevColor = currentColor;
  313. });
  314.  
  315. appendConsecutiveLines();
  316.  
  317.  
  318.  
  319.  
  320. }
  321.  
  322.  
  323.  
  324. // Monitor for URL changes to re-initialize element monitoring
  325. let currentUrl = window.location.href;
  326. setInterval(() => {
  327. if (window.location.href !== currentUrl) {
  328. console.log("URL changed. Re-initializing element monitoring.");
  329. currentUrl = window.location.href;
  330. monitorElements();
  331. }
  332. }, 1000);
  333.  
  334. monitorElements();
  335. })();