c.ai X Character Creation Helper

Gives visual feedback for the definition

目前為 2024-04-02 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name c.ai X Character Creation Helper
  3. // @namespace c.ai X Character Creation Helper
  4. // @version 1.3
  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. (function() {
  12. 'use strict';
  13. // Function to check for element's presence and execute a callback when found
  14. const checkElementPresence = (selector, callback, maxAttempts = 10) => {
  15. let attempts = 0;
  16. const interval = setInterval(() => {
  17. const element = document.querySelector(selector);
  18. if (element) {
  19. clearInterval(interval);
  20. callback(element);
  21. } else if (++attempts >= maxAttempts) {
  22. clearInterval(interval);
  23. console.warn(`Element ${selector} not found after ${maxAttempts} attempts.`);
  24. }
  25. }, 1000);
  26. };
  27. // Function to monitor elements on the page
  28. function monitorElements() {
  29. const initialElementIds = [
  30. //'div.flex-auto:nth-child(1) > div:nth-child(2) > div:nth-child(1)',
  31. 'div.relative:nth-child(5) > div:nth-child(1) > div:nth-child(1)', // Greeting
  32. 'div.relative:nth-child(4) > div:nth-child(1) > div:nth-child(1)' // Description
  33. ];
  34. initialElementIds.forEach(selector => {
  35. checkElementPresence(selector, (element) => {
  36. console.log(`Content of ${selector}:`, element.textContent);
  37. });
  38. });
  39. // Selector for the definition
  40. const definitionSelector = '.transition > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)';
  41. checkElementPresence(definitionSelector, (element) => {
  42. const textarea = element.querySelector('textarea');
  43. if (textarea && !document.querySelector('.custom-definition-panel')) {
  44. updatePanel(textarea); // Initial panel setup
  45. // Observer to detect changes in the textarea content
  46. const observer = new MutationObserver(() => {
  47. updatePanel(textarea);
  48. });
  49. observer.observe(textarea, {attributes: true, childList: true, subtree: true, characterData: true});
  50. }
  51. });
  52. }
  53. // Function to update or create the DefinitionFeedbackPanel based on textarea content
  54. function updatePanel(textarea) {
  55. let DefinitionFeedbackPanel = document.querySelector('.custom-definition-panel');
  56. if (!DefinitionFeedbackPanel) {
  57. DefinitionFeedbackPanel = document.createElement('div');
  58. DefinitionFeedbackPanel.classList.add('custom-definition-panel');
  59. textarea.parentNode.insertBefore(DefinitionFeedbackPanel, textarea);
  60. }
  61. DefinitionFeedbackPanel.innerHTML = ''; // Clear existing content
  62. DefinitionFeedbackPanel.style.border = '0px solid #ccc';
  63. DefinitionFeedbackPanel.style.padding = '10px';
  64. DefinitionFeedbackPanel.style.marginBottom = '10px';
  65. const cleanedContent = textarea.value.trim();
  66. console.log(`Content of Definition:`, cleanedContent);
  67. const textLines = cleanedContent.split('\n');
  68. let lastColor = '#222326';
  69. let isDialogueContinuation = false; // Track if the current line continues a dialogue
  70. let prevColor = null; // Track the previous line's color for detecting color changes
  71. // Determine line color based on content and dialogue continuation logic
  72. let consecutiveCharCount = 0;
  73. let lastCharColor = '';
  74. let lastNamedCharacterColor = '';
  75. //let isDialogueContinuation = false;
  76. function determineLineColor(line, prevLine) {
  77. const dialogueStarterRegex = /^\{\{(?:char|user|random_user_[^\}]*)\}\}:|^[A-Za-z]+:/;
  78. const isDialogueStarter = dialogueStarterRegex.test(line);
  79. const continuesDialogue = prevLine && prevLine.trim().endsWith(':') && (line.startsWith(' ') || !dialogueStarterRegex.test(line));
  80. if (isDialogueStarter) {
  81. isDialogueContinuation = true;
  82. if (line.startsWith('{{char}}:')) {
  83. consecutiveCharCount++;
  84. lastColor = consecutiveCharCount % 2 === 0 ? '#26272B' : '#292A2E';
  85. lastCharColor = lastColor;
  86. } else if (line.match(/^[A-Za-z]+:/)) {
  87. lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
  88. lastColor = lastNamedCharacterColor;
  89. }
  90. else if (line.match(/^\{\{random_user_[^\}]*\}\}:|^\{\{random_user_\}\}:|^{{random_user_}}/)) {
  91. lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
  92. lastColor = lastNamedCharacterColor;
  93. } else {
  94. consecutiveCharCount = 0;
  95. lastColor = line.startsWith('{{user}}:') ? '#37362F' : '#3B3A32';
  96. }
  97. } else if (line.startsWith('END_OF_DIALOG')) {
  98. isDialogueContinuation = false;
  99. lastColor = 'rgba(65, 65, 66, 0)';
  100. } else if (isDialogueContinuation && continuesDialogue) {
  101. // Do nothing, continuation of dialogue
  102. } else if (isDialogueContinuation && !isDialogueStarter) {
  103. // Do nothing, continuation of dialogue
  104. } else {
  105. isDialogueContinuation = false;
  106. lastColor = 'rgba(65, 65, 66, 0)';
  107. }
  108. return lastColor;
  109. }
  110. // Function to remove dialogue starters from the start of a line
  111. let trimmedParts = []; // Array to store trimmed parts
  112. let consecutiveLines = []; // Array to store consecutive lines with the same color
  113. //let prevColor = null;
  114. function trimDialogueStarters(line) {
  115. const dialogueStarterRegex = /^(\{\{char\}\}:|\{\{user\}\}:|\{\{random_user_[^\}]*\}\}:|[A-Za-z]+:)\s*/;
  116. const trimmed = line.match(dialogueStarterRegex);
  117. if (trimmed) {
  118. trimmedParts.push(trimmed[0]); // Store the trimmed part
  119. }
  120. return line.replace(dialogueStarterRegex, '');
  121. }
  122. function groupConsecutiveLines(color, lineDiv) {
  123. // Check if there are consecutive lines with the same color
  124. if (consecutiveLines.length > 0 && consecutiveLines[0].color === color) {
  125. consecutiveLines.push({ color, lineDiv });
  126. } else {
  127. // If not, append the previous group of consecutive lines and start a new group
  128. appendConsecutiveLines();
  129. consecutiveLines.push({ color, lineDiv });
  130. }
  131. }
  132. function appendConsecutiveLines() {
  133. if (consecutiveLines.length > 0) {
  134. const groupDiv = document.createElement('div');
  135. const color = consecutiveLines[0].color;
  136. // Create a container div that could potentially use flexbox
  137. const containerDiv = document.createElement('div');
  138. containerDiv.style.width = '100%';
  139. groupDiv.style.backgroundColor = color;
  140. groupDiv.style.padding = '12px';
  141. groupDiv.style.borderRadius = '16px';
  142. groupDiv.style.display = 'inline-block';
  143. groupDiv.style.maxWidth = '100%'; // You might adjust this as needed
  144. // Only apply flexbox styling if the color condition is met
  145. if (color === '#37362F' || color === '#3B3A32') {
  146. containerDiv.style.display = 'flex';
  147. containerDiv.style.justifyContent = 'flex-end'; // Aligns the child div to the right
  148. }
  149. consecutiveLines.forEach(({ lineDiv }) => {
  150. groupDiv.appendChild(lineDiv);
  151. });
  152. // Add the groupDiv to the containerDiv (flex or not based on color)
  153. containerDiv.appendChild(groupDiv);
  154. // Append the containerDiv to the DefinitionFeedbackPanel
  155. DefinitionFeedbackPanel.appendChild(containerDiv);
  156. consecutiveLines = []; // Clear the array
  157. }
  158. }
  159. function formatText(text) {
  160. // Handle headers; replace Markdown headers (# Header) with <h1>, <h2>, etc.
  161. text = text.replace(/^(######\s)(.*)$/gm, '<h6>$2</h6>'); // For h6
  162. text = text.replace(/^(#####\s)(.*)$/gm, '<h5>$2</h5>'); // For h5
  163. text = text.replace(/^(####\s)(.*)$/gm, '<h4>$2</h4>'); // For h4
  164. text = text.replace(/^(###\s)(.*)$/gm, '<h3>$2</h3>'); // For h3
  165. text = text.replace(/^(##\s)(.*)$/gm, '<h2>$2</h2>'); // For h2
  166. text = text.replace(/^(#\s)(.*)$/gm, '<h1>$2</h1>'); // For h1
  167. // Process bold italic before bold or italic to avoid nesting conflicts
  168. text = text.replace(/\*\*\*([^*]+)\*\*\*/g, '<em><strong>$1</strong></em>');
  169. // Replace text wrapped in double asterisks with <strong> tags for bold
  170. text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
  171. // Finally, replace text wrapped in single asterisks with <em> tags for italics
  172. text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
  173. return text;
  174. }
  175. textLines.forEach((line, index) => {
  176. const prevLine = index > 0 ? textLines[index - 1] : null;
  177. const currentColor = determineLineColor(line, prevLine);
  178. const trimmedLine = trimDialogueStarters(line);
  179. if (prevColor && currentColor !== prevColor) {
  180. appendConsecutiveLines(); // Append previous group of consecutive lines
  181. const spacingDiv = document.createElement('div');
  182. spacingDiv.style.marginBottom = '20px';
  183. DefinitionFeedbackPanel.appendChild(spacingDiv);
  184. }
  185. const lineDiv = document.createElement('div');
  186. lineDiv.style.wordWrap = 'break-word'; // Allow text wrapping
  187. if (trimmedLine.startsWith("END_OF_DIALOG")) {
  188. appendConsecutiveLines(); // Make sure to append any pending groups before adding the divider
  189. const separatorLine = document.createElement('hr');
  190. DefinitionFeedbackPanel.appendChild(separatorLine); // This ensures the divider is on a new line
  191. } else {
  192. if (trimmedParts.length > 0) {
  193. const headerDiv = document.createElement('div');
  194. const headerText = trimmedParts.shift();
  195. const formattedHeaderText = headerText.replace(/:/g, '');
  196. headerDiv.textContent = formattedHeaderText;
  197. if (formattedHeaderText.includes('{{user}}')) {
  198. headerDiv.style.textAlign = 'right';
  199. }
  200. DefinitionFeedbackPanel.appendChild(headerDiv);
  201. }
  202. if (trimmedLine.trim() === '') {
  203. lineDiv.appendChild(document.createElement('br'));
  204. } else {
  205. const paragraph = document.createElement('p');
  206. // Call formatTextForItalics to wrap text in asterisks with <em> tags
  207. paragraph.innerHTML = formatText(trimmedLine);
  208. lineDiv.appendChild(paragraph);
  209. }
  210. groupConsecutiveLines(currentColor, lineDiv);
  211. }
  212. prevColor = currentColor;
  213. });
  214. appendConsecutiveLines();
  215. }
  216. // Monitor for URL changes to re-initialize element monitoring
  217. let currentUrl = window.location.href;
  218. setInterval(() => {
  219. if (window.location.href !== currentUrl) {
  220. console.log("URL changed. Re-initializing element monitoring.");
  221. currentUrl = window.location.href;
  222. monitorElements();
  223. }
  224. }, 1000);
  225. monitorElements();
  226. })();