c.ai X Character Creation Helper

Gives visual feedback for the definition

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

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