c.ai X Character Creation Helper

Gives visual feedback for the definition

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

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