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