c.ai X Character Creation Helper

Gives visual feedback for the definition

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        c.ai X Character Creation Helper
// @namespace   c.ai X Character Creation Helper
// @version     2.1
// @license     MIT
// @description Gives visual feedback for the definition
// @author      Vishanka
// @match       https://character.ai/*
// @grant       none
// ==/UserScript==


(function() {
    'use strict';

    // Function to check for element's presence and execute a callback when found
    const checkElementPresence = (selector, callback, maxAttempts = 10) => {
        let attempts = 0;
        const interval = setInterval(() => {
            const element = document.querySelector(selector);
            if (element) {
                clearInterval(interval);
                callback(element);
            } else if (++attempts >= maxAttempts) {
                clearInterval(interval);
                console.warn(`Element ${selector} not found after ${maxAttempts} attempts.`);
            }
        }, 1000);
    };

    // Function to monitor elements on the page
    function monitorElements() {
        const initialElementIds = [
            'div.flex-auto:nth-child(1) > div:nth-child(2) > div:nth-child(1)',
            'div.relative:nth-child(5) > div:nth-child(1) > div:nth-child(1)', // Greeting
            'div.relative:nth-child(4) > div:nth-child(1) > div:nth-child(1)'  // Description
        ];


        initialElementIds.forEach(selector => {
            checkElementPresence(selector, (element) => {
                console.log(`Content of ${selector}:`, element.textContent);
            });
        });

        // Selector for the definition
        const definitionSelector = '.transition > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)';
        checkElementPresence(definitionSelector, (element) => {
            const textarea = element.querySelector('textarea');
            if (textarea && !document.querySelector('.custom-definition-panel')) {
                updatePanel(textarea); // Initial panel setup

                // Observer to detect changes in the textarea content
                const observer = new MutationObserver(() => {
                    updatePanel(textarea);
                });

                observer.observe(textarea, {attributes: true, childList: true, subtree: true, characterData: true});
            }
        });
    }

// Function to update or create the DefinitionFeedbackPanel based on textarea content
function updatePanel(textarea) {
    let DefinitionFeedbackPanel = document.querySelector('.custom-definition-panel');
    if (!DefinitionFeedbackPanel) {
        DefinitionFeedbackPanel = document.createElement('div');
        DefinitionFeedbackPanel.classList.add('custom-definition-panel');
        textarea.parentNode.insertBefore(DefinitionFeedbackPanel, textarea);
    }
    DefinitionFeedbackPanel.innerHTML = ''; // Clear existing content
    DefinitionFeedbackPanel.style.border = '0px solid #ccc';
    DefinitionFeedbackPanel.style.padding = '10px';
    DefinitionFeedbackPanel.style.marginBottom = '10px';
    DefinitionFeedbackPanel.style.marginTop = '5px';
    DefinitionFeedbackPanel.style.maxHeight = '500px'; // Adjust the max-height as needed
    DefinitionFeedbackPanel.style.overflowY = 'auto';


    var plaintextColor = localStorage.getItem('plaintext_color');
    var defaultColor = '#D1D5DB';
    var color = plaintextColor || defaultColor;
    DefinitionFeedbackPanel.style.color = color;

    const cleanedContent = textarea.value.trim();
    console.log(`Content of Definition:`, cleanedContent);
    const textLines = cleanedContent.split('\n');

let lastColor = '#222326';
let isDialogueContinuation = false; // Track if the current line continues a dialogue
let prevColor = null; // Track the previous line's color for detecting color changes

let consecutiveCharCount = 0;
let consecutiveUserCount = 0; // Track the number of consecutive {{user}} lines
let lastCharColor = '';
let lastNamedCharacterColor = '';

function determineLineColor(line, prevLine) {
    // Extract the part of the line before the first colon
    const indexFirstColon = line.indexOf(':');
    const firstPartOfLine = indexFirstColon !== -1 ? line.substring(0, indexFirstColon + 1) : line;
    // Define the dialogue starter regex with updated conditions
    const dialogueStarterRegex = /^\{\{(?:char|user|random_user_[^\}]*)\}\}:|^{{[\S\s]+}}:|^[^\s:]+:/;
    const isDialogueStarter = dialogueStarterRegex.test(firstPartOfLine);
    const continuesDialogue = prevLine && prevLine.trim().endsWith(':') && (line.startsWith(' ') || !dialogueStarterRegex.test(firstPartOfLine));

    const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';

    if (isDialogueStarter) {
        isDialogueContinuation = true;
          if (line.startsWith('{{char}}:')) {
            consecutiveCharCount++;
            if (currentTheme === 'dark') {
              lastColor = consecutiveCharCount % 2 === 0 ? '#26272B' : '#292A2E';
              lastCharColor = lastColor;
            } else {
              lastColor = consecutiveCharCount % 2 === 0 ? '#E4E4E7' : '#E3E3E6';
              lastCharColor = lastColor;
            }
        } else if (line.startsWith('{{user}}:')) {
            consecutiveUserCount++;
    if (currentTheme === 'dark') {
        lastColor = consecutiveUserCount % 2 === 0 ? '#363630' : '#383832';
    } else {
        lastColor = consecutiveUserCount % 2 === 0 ? '#D9D9DF' : '#D5D5DB'; // Light theme color
    }
            consecutiveCharCount = 0; // Reset this if you need to ensure it only affects consecutive {{char}} dialogues

        } else if (line.match(/^\{\{(?:char|user|random_user_[^\}]*)\}\}:|^{{[\S\s]+}}:|^[\S]+:/)) {
            if (currentTheme === 'dark') {
            lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
            } else {
            lastNamedCharacterColor = lastNamedCharacterColor === '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
            }
            lastColor = lastNamedCharacterColor;
        }
          else if (line.match(/^\{\{random_user_[^\}]*\}\}:|^\{\{random_user_\}\}:|^{{random_user_}}/)) {
            if (currentTheme === 'dark') {
            lastNamedCharacterColor = lastNamedCharacterColor === '#474747' ? '#4C4C4D' : '#474747';
            } else {
            lastNamedCharacterColor = lastNamedCharacterColor === '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
            }
            lastColor = lastNamedCharacterColor;
        } else {
            consecutiveCharCount = 0;
            if (currentTheme === 'dark') {
            lastColor = '#474747' ? '#4C4C4D' : '#474747'; // Default case for non-{{char}} dialogues; adjust as needed
            } else {
            lastColor = '#CFDCE8' ? '#CCDAE6' : '#CFDCE8';
            }
        }
    } else if (line.startsWith('END_OF_DIALOG')) {
        isDialogueContinuation = false;
        lastColor = 'rgba(65, 65, 66, 0)';
    } else if (isDialogueContinuation && continuesDialogue) {
        // Do nothing, continuation of dialogue
    } else if (isDialogueContinuation && !isDialogueStarter) {
        // Do nothing, continuation of dialogue
    } else {
        isDialogueContinuation = false;
        lastColor = 'rgba(65, 65, 66, 0)';
    }
    return lastColor;
}


// Function to remove dialogue starters from the start of a line
let trimmedParts = []; // Array to store trimmed parts
let consecutiveLines = []; // Array to store consecutive lines with the same color
//let prevColor = null;

function trimDialogueStarters(line) {
    // Find the index of the first colon
    const indexFirstColon = line.indexOf(':');
    // If there's no colon, return the line as is
    if (indexFirstColon === -1) return line;

    // Extract the part of the line before the first colon to check against the regex
    const firstPartOfLine = line.substring(0, indexFirstColon + 1);

    // Define the dialogue starter regex
    const dialogueStarterRegex = /^(\{\{char\}\}:|\{\{user\}\}:|\{\{random_user_[^\}]*\}\}:|^{{[\S\s]+}}:|^[^\s:]+:)\s*/;

    // Check if the first part of the line matches the dialogue starter regex
    const trimmed = firstPartOfLine.match(dialogueStarterRegex);
    if (trimmed) {
        // Store the trimmed part
        trimmedParts.push(trimmed[0]);
        // Return the line without the dialogue starter and any leading whitespace that follows it
        return line.substring(indexFirstColon + 1).trim();
    }

    // If the first part doesn't match, return the original line
    return line;
}

function groupConsecutiveLines(color, lineDiv) {
    // Check if there are consecutive lines with the same color
    if (consecutiveLines.length > 0 && consecutiveLines[0].color === color) {
        consecutiveLines.push({ color, lineDiv });
    } else {
        // If not, append the previous group of consecutive lines and start a new group
        appendConsecutiveLines();
        consecutiveLines.push({ color, lineDiv });
    }
}



function appendConsecutiveLines() {
    if (consecutiveLines.length > 0) {
        const groupDiv = document.createElement('div');
        const color = consecutiveLines[0].color;

        const containerDiv = document.createElement('div');
        containerDiv.style.width = '100%';

        groupDiv.style.backgroundColor = color;
        groupDiv.style.padding = '12px';
        groupDiv.style.paddingBottom = '15px'; // Increased bottom padding to provide space
        groupDiv.style.borderRadius = '16px';
        groupDiv.style.display = 'inline-block';
        groupDiv.style.maxWidth = '90%';
        groupDiv.style.position = 'relative'; // Set position as relative for the absolute positioning of countDiv

        if (color === '#363630' || color === '#383832' || color === '#D9D9DF' || color === '#D5D5DB') {
            containerDiv.style.display = 'flex';
            containerDiv.style.justifyContent = 'flex-end';
        }

        // Calculate total number of characters across all lines
        const totalSymbolCount = consecutiveLines.reduce((acc, { lineDiv }) => acc + lineDiv.textContent.length, 0);

        consecutiveLines.forEach(({ lineDiv }) => {
            const lineContainer = document.createElement('div');

            lineContainer.style.display = 'flex';
            lineContainer.style.justifyContent = 'space-between';
            lineContainer.style.alignItems = 'flex-end'; // Ensure items align to the bottom
            lineContainer.style.width = '100%'; // Ensure container takes full width

            lineDiv.style.flexGrow = '1'; // Allow lineDiv to grow and fill space
            // Append the lineDiv to the container
            lineContainer.appendChild(lineDiv);

            // Append the container to the groupDiv
            groupDiv.appendChild(lineContainer);
        });

        const countDiv = document.createElement('div');
        countDiv.textContent = `${totalSymbolCount}`;
        countDiv.style.position = 'absolute'; // Use absolute positioning
        countDiv.style.bottom = '3px'; // Position at the bottom
        countDiv.style.right = '12px'; // Position on the right
        countDiv.style.fontSize = '11px';
// darkmode user
        if (color === '#363630' || color === '#383832'){
            countDiv.style.color = '#5C5C52';
//lightmode user
        } else if (color === '#D9D9DF' || color === '#D5D5DB') {
            countDiv.style.color = '#B3B3B8';
//darkmode char
        } else if (color === '#26272B' || color === '#292A2E') {
          countDiv.style.color = '#44464D';
//lightmode char
        } else if (color === '#E4E4E7' || color === '#E3E3E6') {
          countDiv.style.color = '#C4C4C7';
//darkmode random
        } else if (color === '#474747' || color === '#4C4C4D') {
          countDiv.style.color = '#6E6E6E';
//lightmode random
        } else if (color === '#CFDCE8' || color === '#CCDAE6') {
          countDiv.style.color = '#B4BFC9';
        } else {
          countDiv.style.color = 'rgba(65, 65, 66, 0)';
        }

        // Append the countDiv to the groupDiv
        groupDiv.appendChild(countDiv);

        // Add the groupDiv to the containerDiv (flex or not based on color)
        containerDiv.appendChild(groupDiv);

        // Append the containerDiv to the DefinitionFeedbackPanel
        DefinitionFeedbackPanel.appendChild(containerDiv);
        consecutiveLines = []; // Clear the array
    }
}


function formatText(text) {
    // Handle headers; replace Markdown headers (# Header) with <h1>, <h2>, etc.
    text = text.replace(/^(######\s)(.*)$/gm, '<h6>$2</h6>'); // For h6
    text = text.replace(/^(#####\s)(.*)$/gm, '<h5>$2</h5>'); // For h5
    text = text.replace(/^(####\s)(.*)$/gm, '<h4>$2</h4>'); // For h4
    text = text.replace(/^(###\s)(.*)$/gm, '<h3>$2</h3>'); // For h3
    text = text.replace(/^(##\s)(.*)$/gm, '<h2>$2</h2>'); // For h2
    text = text.replace(/^(#\s)(.*)$/gm, '<h1>$2</h1>'); // For h1

    // Process bold italic before bold or italic to avoid nesting conflicts
    text = text.replace(/\*\*\*([^*]+)\*\*\*/g, '<em><strong>$1</strong></em>');
    // Replace text wrapped in double asterisks with <strong> tags for bold
    text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
    // Finally, replace text wrapped in single asterisks with <em> tags for italics
    text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');

    return text;
}



textLines.forEach((line, index) => {
    const prevLine = index > 0 ? textLines[index - 1] : null;
    const currentColor = determineLineColor(line, prevLine);
    const trimmedLine = trimDialogueStarters(line);

    if (prevColor && currentColor !== prevColor) {
        appendConsecutiveLines(); // Append previous group of consecutive lines

        const spacingDiv = document.createElement('div');
        spacingDiv.style.marginBottom = '20px';
        DefinitionFeedbackPanel.appendChild(spacingDiv);
    }

    const lineDiv = document.createElement('div');
    lineDiv.style.wordWrap = 'break-word'; // Allow text wrapping

if (trimmedLine.startsWith("END_OF_DIALOG")) {
    appendConsecutiveLines(); // Make sure to append any pending groups before adding the divider
    const separatorLine = document.createElement('hr');
    DefinitionFeedbackPanel.appendChild(separatorLine); // This ensures the divider is on a new line
} else {
        if (trimmedParts.length > 0) {
            const headerDiv = document.createElement('div');
            const headerText = trimmedParts.shift();
            const formattedHeaderText = headerText.replace(/:/g, '');
            headerDiv.textContent = formattedHeaderText;
          const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
    if (currentTheme === 'dark') {
        headerDiv.style.color = '#A2A2AC'; // Dark mode text color
    } else {
        headerDiv.style.color = '#26272B';
    }
            if (formattedHeaderText.includes('{{user}}')) {
                headerDiv.style.textAlign = 'right';
            }
            DefinitionFeedbackPanel.appendChild(headerDiv);
        }

        if (trimmedLine.trim() === '') {
            lineDiv.appendChild(document.createElement('br'));
        } else {
            const paragraph = document.createElement('p');
            // Call formatTextForItalics to wrap text in asterisks with <em> tags
            paragraph.innerHTML = formatText(trimmedLine);
            lineDiv.appendChild(paragraph);
        }

        groupConsecutiveLines(currentColor, lineDiv);
    }

    prevColor = currentColor;
});

appendConsecutiveLines();




}



    // Monitor for URL changes to re-initialize element monitoring
    let currentUrl = window.location.href;
    setInterval(() => {
        if (window.location.href !== currentUrl) {
            console.log("URL changed. Re-initializing element monitoring.");
            currentUrl = window.location.href;
            monitorElements();
        }
    }, 1000);

    monitorElements();
})();