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.0
  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 newPanel based on textarea content
  63. function updatePanel(textarea) {
  64. let newPanel = document.querySelector('.custom-definition-panel');
  65. if (!newPanel) {
  66. newPanel = document.createElement('div');
  67. newPanel.classList.add('custom-definition-panel');
  68. textarea.parentNode.insertBefore(newPanel, textarea);
  69. }
  70. newPanel.innerHTML = ''; // Clear existing content
  71. newPanel.style.border = '0px solid #ccc';
  72. newPanel.style.padding = '10px';
  73. newPanel.style.marginBottom = '10px';
  74.  
  75. const cleanedContent = textarea.value.trim();
  76. console.log(`Content of Definition:`, cleanedContent);
  77. const textLines = cleanedContent.split('\n');
  78. let lastColor = '#222326';
  79. let isDialogueContinuation = false; // Track if the current line continues a dialogue
  80. let prevColor = null; // Track the previous line's color for detecting color changes
  81.  
  82.  
  83. // Determine line color based on content and dialogue continuation logic
  84. let consecutiveCharCount = 0;
  85. let lastCharColor = '';
  86. let lastNamedCharacterColor = '';
  87. //let isDialogueContinuation = false;
  88.  
  89. function determineLineColor(line, prevLine) {
  90. const dialogueStarterRegex = /^\{\{(?:char|user|random_user_[^\}]*)\}\}:|^[A-Za-z]+:/;
  91. const isDialogueStarter = dialogueStarterRegex.test(line);
  92. const continuesDialogue = prevLine && prevLine.trim().endsWith(':') && (line.startsWith(' ') || !dialogueStarterRegex.test(line));
  93.  
  94. if (isDialogueStarter) {
  95. isDialogueContinuation = true;
  96. if (line.startsWith('{{char}}:')) {
  97. consecutiveCharCount++;
  98. lastColor = consecutiveCharCount % 2 === 0 ? '#26272B' : '#292A2E';
  99. lastCharColor = lastColor;
  100. } else if (line.match(/^[A-Za-z]+:/)) {
  101. lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
  102. lastColor = lastNamedCharacterColor;
  103. }
  104. else if (line.match(/^\{\{random_user_[^\}]*\}\}:|^\{\{random_user_\}\}:|^{{random_user_}}/)) {
  105. lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
  106. lastColor = lastNamedCharacterColor;
  107. } else {
  108. consecutiveCharCount = 0;
  109. lastColor = line.startsWith('{{user}}:') ? '#37362F' : '#3B3A32';
  110. }
  111. } else if (line.startsWith('END_OF_DIALOG')) {
  112. isDialogueContinuation = false;
  113. lastColor = 'rgba(65, 65, 66, 0)';
  114. } else if (isDialogueContinuation && continuesDialogue) {
  115. // Do nothing, continuation of dialogue
  116. } else if (isDialogueContinuation && !isDialogueStarter) {
  117. // Do nothing, continuation of dialogue
  118. } else {
  119. isDialogueContinuation = false;
  120. lastColor = 'rgba(65, 65, 66, 0)';
  121. }
  122. return lastColor;
  123. }
  124.  
  125. // Function to remove dialogue starters from the start of a line
  126. let trimmedParts = []; // Array to store trimmed parts
  127. let consecutiveLines = []; // Array to store consecutive lines with the same color
  128. //let prevColor = null;
  129.  
  130. function trimDialogueStarters(line) {
  131. const dialogueStarterRegex = /^(\{\{char\}\}:|\{\{user\}\}:|\{\{random_user_[^\}]*\}\}:|[A-Za-z]+:)\s*/;
  132.  
  133. const trimmed = line.match(dialogueStarterRegex);
  134. if (trimmed) {
  135. trimmedParts.push(trimmed[0]); // Store the trimmed part
  136. }
  137. return line.replace(dialogueStarterRegex, '');
  138. }
  139.  
  140. function groupConsecutiveLines(color, lineDiv) {
  141. // Check if there are consecutive lines with the same color
  142. if (consecutiveLines.length > 0 && consecutiveLines[0].color === color) {
  143. consecutiveLines.push({ color, lineDiv });
  144. } else {
  145. // If not, append the previous group of consecutive lines and start a new group
  146. appendConsecutiveLines();
  147. consecutiveLines.push({ color, lineDiv });
  148. }
  149. }
  150.  
  151. function appendConsecutiveLines() {
  152. if (consecutiveLines.length > 0) {
  153. const groupDiv = document.createElement('div');
  154. const color = consecutiveLines[0].color;
  155.  
  156. // Create a container div that could potentially use flexbox
  157. const containerDiv = document.createElement('div');
  158. containerDiv.style.width = '100%';
  159.  
  160. groupDiv.style.backgroundColor = color;
  161. groupDiv.style.padding = '12px';
  162. groupDiv.style.borderRadius = '16px';
  163. groupDiv.style.display = 'inline-block';
  164. groupDiv.style.maxWidth = '100%'; // You might adjust this as needed
  165.  
  166. // Only apply flexbox styling if the color condition is met
  167. if (color === '#37362F' || color === '#3B3A32') {
  168. containerDiv.style.display = 'flex';
  169. containerDiv.style.justifyContent = 'flex-end'; // Aligns the child div to the right
  170. }
  171.  
  172. consecutiveLines.forEach(({ lineDiv }) => {
  173. groupDiv.appendChild(lineDiv);
  174. });
  175.  
  176. // Add the groupDiv to the containerDiv (flex or not based on color)
  177. containerDiv.appendChild(groupDiv);
  178.  
  179. // Append the containerDiv to the newPanel
  180. newPanel.appendChild(containerDiv);
  181. consecutiveLines = []; // Clear the array
  182. }
  183. }
  184.  
  185.  
  186.  
  187. function formatText(text) {
  188. // Handle headers; replace Markdown headers (# Header) with <h1>, <h2>, etc.
  189. text = text.replace(/^(######\s)(.*)$/gm, '<h6>$2</h6>'); // For h6
  190. text = text.replace(/^(#####\s)(.*)$/gm, '<h5>$2</h5>'); // For h5
  191. text = text.replace(/^(####\s)(.*)$/gm, '<h4>$2</h4>'); // For h4
  192. text = text.replace(/^(###\s)(.*)$/gm, '<h3>$2</h3>'); // For h3
  193. text = text.replace(/^(##\s)(.*)$/gm, '<h2>$2</h2>'); // For h2
  194. text = text.replace(/^(#\s)(.*)$/gm, '<h1>$2</h1>'); // For h1
  195.  
  196. // Process bold italic before bold or italic to avoid nesting conflicts
  197. text = text.replace(/\*\*\*([^*]+)\*\*\*/g, '<em><strong>$1</strong></em>');
  198. // Replace text wrapped in double asterisks with <strong> tags for bold
  199. text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
  200. // Finally, replace text wrapped in single asterisks with <em> tags for italics
  201. text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
  202.  
  203. return text;
  204. }
  205.  
  206.  
  207.  
  208. textLines.forEach((line, index) => {
  209. const prevLine = index > 0 ? textLines[index - 1] : null;
  210. const currentColor = determineLineColor(line, prevLine);
  211. const trimmedLine = trimDialogueStarters(line);
  212.  
  213. if (prevColor && currentColor !== prevColor) {
  214. appendConsecutiveLines(); // Append previous group of consecutive lines
  215.  
  216. const spacingDiv = document.createElement('div');
  217. spacingDiv.style.marginBottom = '20px';
  218. newPanel.appendChild(spacingDiv);
  219. }
  220.  
  221. const lineDiv = document.createElement('div');
  222. lineDiv.style.wordWrap = 'break-word'; // Allow text wrapping
  223.  
  224. if (trimmedLine.startsWith("END_OF_DIALOG")) {
  225. appendConsecutiveLines(); // Make sure to append any pending groups before adding the divider
  226. const separatorLine = document.createElement('hr');
  227. newPanel.appendChild(separatorLine); // This ensures the divider is on a new line
  228. } else {
  229. if (trimmedParts.length > 0) {
  230. const headerDiv = document.createElement('div');
  231. const headerText = trimmedParts.shift();
  232. const formattedHeaderText = headerText.replace(/:/g, '');
  233. headerDiv.textContent = formattedHeaderText;
  234. if (formattedHeaderText.includes('{{user}}')) {
  235. headerDiv.style.textAlign = 'right';
  236. }
  237. newPanel.appendChild(headerDiv);
  238. }
  239.  
  240. if (trimmedLine.trim() === '') {
  241. lineDiv.appendChild(document.createElement('br'));
  242. } else {
  243. const paragraph = document.createElement('p');
  244. // Call formatTextForItalics to wrap text in asterisks with <em> tags
  245. paragraph.innerHTML = formatText(trimmedLine);
  246. lineDiv.appendChild(paragraph);
  247. }
  248.  
  249. groupConsecutiveLines(currentColor, lineDiv);
  250. }
  251.  
  252. prevColor = currentColor;
  253. });
  254.  
  255. appendConsecutiveLines();
  256.  
  257.  
  258.  
  259.  
  260. }
  261.  
  262.  
  263.  
  264. // Monitor for URL changes to re-initialize element monitoring
  265. let currentUrl = window.location.href;
  266. setInterval(() => {
  267. if (window.location.href !== currentUrl) {
  268. console.log("URL changed. Re-initializing element monitoring.");
  269. currentUrl = window.location.href;
  270. monitorElements();
  271. }
  272. }, 1000);
  273.  
  274. monitorElements();
  275. })();
  276.