AI Character Chat Common Code and Resources

Compiled by Wren / bu8m4n

//BLANK CHARACTER TEMPLATE (9-19-24)

//START

Name:
Species:
Physical Description: keyword1, keyword2,
Clothing: keyword1, keyword2,
Age:
Sex:
Pronouns:
Speech Style / Accent: (era, accent, class, etc.)
Abilities:
Occupation:

Personality:

  • Enneagram: core+wing+healthy/unhealthy (i.e. 7w6, 9w8)
    • Instincts: (Self-Preservation -OR- Sexual/Attraction -OR- Social/Adaptive)
  • HBDI: (A B C -OR- D)
  • MBTI: Extroversion -or- Introversion -AND- Sensing/Intuition -AND- Thinking/Feeling -AND- Judging/Perceiving #//Abbreviations fine
  • Likes: keyword1, keyword2,
  • Dislikes: keyword1, keyword2,
  • Flaws: keyword1, keyword2,
  • Goals: keyword1, keyword2,
  • Fears: keyword1, keyword2,
  • Comforts: keyword1, keyword2,
  • Discomforts: keyword1, keyword2,
  • Love Language:

Character Influences: historical figure

Backstory:

Description/Personality: (how this character is perceived by, and acts around others)

Thoughts: "thought1", "thought2",

<!--Personality Digest https:#//rentry.co/2eo5oyur -->

//END


//UTILITY BOTS

//ELLIOT (Retired author, knows much about the world and is ready to help you flesh out your characters and settings. Can check urls.)
https://perchance.org/ai-character-chat?data=Elliot_Asimov~1cf10bf892ed216b44f08be8618bf84c.gz

//CHARACTER CREATOR (Give it a description and it -should- spit out a filled character sheet in JSON format)
https://perchance.org/ai-character-chat?data=Character_Creator_JSON~a25da6bb4ce40643281b4e60135a81ea.gz

//JAVASCRIPT ASSISTANT (w/ URL CHECKING)
https://perchance.org/ai-character-chat?data=Javascript_Assistant_(with_url_checking)~5f2d200f540411dbe4f12fce4ab379d4.gz


//CSS Styling / Default Messages

Adjusted (made for large avatar @ 4x and portrait, leaving here to have settings to play with):
backdrop-filter: blur(4px); border-radius: 5px; background-color: rgb(25 87 68 / 0.75); color: rgb(243 182 110); font-size: 120%; font-family: "papyrus", sans-serif; font-weight: bolder; text-align: center; text-justify: inter-word; margin-right: 7%; padding: 20px;

Generic (white transparent bg, black text, no frills):
backdrop-filter:blur(3px); border-radius:5px; background-color:rgb(255 255 255 / 0.75); color:black; font-size: 120%;


//Character Insertables ;p

Reminder Box:

[SYSTEM]: Write messages in a literary style (adult novel, high reading level). Paragraphs should be long and full of atmospheric and immersive description. In-line dialogue is acceptable. The environment should be painted with an inspired brush. There should be vivid characterization, and detailed descriptions of thoughts, emotions, body-language, and facial expressions. Do not repeat dialogue.

Message Prompt:

Prompt: <> (Instructions: Write messages in a literary style (adult novel, high reading level). Paragraphs should be long and full of atmospheric and immersive description. In-line dialogue is acceptable. The environment should be painted with an inspired brush. There should be vivid characterization, and detailed descriptions of thoughts, emotions, body-language, and facial expressions. Do not repeat dialogue.)

End_of_Sheet Rules:

// **Plot-Point** if using Smart Narrator; Very last thing in sheet.

//USER Rules

1
2
3
4
5
6
[SYSTEM]:
#{{user}}'s RULES:
- {{user}} will only act according to their role and will not advance the narrative or story too quickly, remaining focused on present moment interactions.
- Each of {{user}}'s messages must be written in a literary style (adult novel, high reading level). Speech must be in quotation marks. Thoughts must be in single quotes.
- {{user}} must take their personality into account when writing responses. Actions, decision-making, and responses should reflect their unique personality.
- {{user}} must take other characters' personalities into account, when writing responses. Actions, decision-making, and responses should reflect their unique personality.

//CHAR Rules

1
2
3
4
5
6
[SYSTEM]:
#{{char}}'s RULES:
- {{char}} will only act according to their role and will not advance the narrative or story too quickly, remaining focused on present moment interactions.
- Each of {{char}}'s messages must be written in a literary style (adult novel, high reading level). Speech must be in quotation marks. Thoughts must be in single quotes.
- {{char}} must take their personality into account when writing responses. Actions, decision-making, and responses should reflect their unique personality.
- {{char}} must take other characters' personalities into account, when writing responses. Actions, decision-making, and responses should reflect their unique personality.

//JAVASCRIPT

//ALL-IN-ONE (change avatar based on mood, resize, cycle backgrounds, rewrite user posts with /rewrite

//START

// Background Configuration
const backgroundKeywords = {
  "market": "url_to_text_list_with_market_image_urls",
  "town": "url_to_text_list_with_town_image_urls",
  "park": "url_to_text_list_with_park_image_urls",
  // Add more background keywords and their corresponding text file URLs as needed
  // Text files should be a list of urls to images. The background will cycle through avaialable urls until a keyword changes the used list.
};

//const avatarUrls is rendered redundant by the mood-based code. Use this only if you want keywords to trigger avatar changes.
const avatarUrls = {
  "keyword1": "avatar_url_1",
  "keyword2": "avatar_url_2",
  // Add more avatar keywords as needed
};

//CHANGE AVATAR BASED ON MOOD
// Expression to avatar URL mapping
// Add multiple urls by separating them with "|" e.g. https:url.jpeg|https:url.jpeg and one will be chosen at random.
let expressions = `
neutral, annoyed, unimpressed: example_url1.jpeg|example_dropbox_url_dl=1
knowing, secretive, flirty, playful, teasing: 
Sly, cunning, clever: 
relaxed, casual: 
earnest, determined, congratulatory, encouraging, optimistic: 
joyful tears, heartfelt confession: 
crying, crushed, heartbroken: 
serious, focused, determined: 
angry, stern, deadly serious, pissed off: 
joyful, laughing, excited, smiling brightly: 
shocked, surprised, impressed: 
worried, scared, powerless, self-doubting: 
shy, smiling in embarrassment, loving: 
embarrassed, unsure, doubtful, apprehensive: 
Seductive, bedroom eyes, come-hither look: 
`.trim().split("\n").map(l => [l.trim().split(":")[0].trim(), l.trim().split(":").slice(1).join(":").trim()]).map(a => ({label:a[0], url:a[1]}));

let numMessagesInContext = 4; // Number of historical messages to consider for context

oc.thread.on("messageadded", async function() {
  let lastMessage = oc.thread.messages.at(-1);
  if(lastMessage.author !== "ai") return;

  let questionText = `I'm about to ask you to classify the facial expression of a particular message, but here's some context first:

---
${oc.thread.messages.slice(-numMessagesInContext).filter(m => m.role!=="system").map(m => (m.author=="ai" ? `[${oc.character.name}]: ` : `[Anon]: `)+m.content).join("\n\n")}
---

Okay, now that you have the context, please classify the facial expression of the following text:

---
${lastMessage.content}
---

Choose between the following categories:

${expressions.map((e, i) => `${i}) ${e.label}`).join("\n")}

Please respond with the number which corresponds to the facial expression that most accurately matches the given message. Respond with just the number - nothing else.`;

  let response = await oc.getInstructCompletion({
    instruction: questionText,
    startWith: ""
  });

  let index = parseInt(response.trim());
  if (isNaN(index) || index < 0 || index >= expressions.length) {
    console.log("Invalid response from AI:", response);
    return;
  }

  let expressionObj = expressions[index];
  console.log("Selected expression:", expressionObj.label);

  // Update the character's avatar
  oc.character.avatar.url = expressionObj.url;
  console.log("Avatar updated to:", expressionObj.url);
});

oc.thread.on("MessageAdded", async function() {
  oc.thread.messages.forEach(a => {
    a.avatar = {
      size: oc.character.avatar.size,
      shape: oc.character.avatar.shape
    };
  });
});

//BACKGROUND CHANGE TIMER
const randomBackgrounds = true;
const intervalSeconds = 190; // This can be increased for longer delays between background changes, but note changes will only occur when a message is added, edited, or deleted.

// Global variables
let currentBackgroundIndex = -1;
let nextBgURL = null;
let currentBackgroundList = [];
let backgroundKeywordsWithLinks = {};
let lastBackgroundChangeTime = Date.now();
let currentBackgroundKeyword = null;
let timerExpired = false;

function normalizeString(str) {
  return str.toLowerCase().replace(/[\s'"]/g, '');
}

async function fetchTextFile(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    const bgs = await response.text();
    const bgList = bgs.split(/\r?\n/);
    const uniqueBgList = [...new Set(bgList)];
    console.log('Background list retrieved:', uniqueBgList);
    return uniqueBgList;
  } catch (error) {
    console.error('Error fetching the text file:', error);
    return [];
  }
}

async function initializeBackgroundKeywordsWithLinks() {
  for (const [keyword, url] of Object.entries(backgroundKeywords)) {
    backgroundKeywordsWithLinks[keyword] = await fetchTextFile(url);
  }
  console.log('Initialized backgroundKeywordsWithLinks:', backgroundKeywordsWithLinks);
}

function getRandomArbitrary(min, max) {
  return Math.floor(Math.random() * (max - min) + min);
}

function queueBackground(newBgIndex) {
  console.log(`Retrieving index ${newBgIndex}`);
  const newBackgroundURL = currentBackgroundList[newBgIndex];
  console.log('Next bg url:', newBackgroundURL);

  if (newBackgroundURL) {
    currentBackgroundIndex = newBgIndex;
    nextBgURL = newBackgroundURL;
  }
}

function changeBackground() {
  const backgroundListSize = currentBackgroundList.length;

  if (randomBackgrounds && backgroundListSize > 1) {
    let randomNumber = -1;
    do {
      randomNumber = getRandomArbitrary(0, backgroundListSize);
    } while (randomNumber === backgroundListSize || randomNumber === currentBackgroundIndex);

    console.log(`Randomized background index: ${randomNumber}`);
    currentBackgroundIndex = randomNumber;
  } else {
    console.log(`Changing current background from ${currentBackgroundIndex} to next available`);
    if (currentBackgroundIndex >= backgroundListSize - 1) {
      currentBackgroundIndex = 0;
    } else {
      currentBackgroundIndex++;
    }
  }

  queueBackground(currentBackgroundIndex);
}

function handleBackgroundKeyword(keyword) {
  if (keyword !== currentBackgroundKeyword) {
    currentBackgroundList = backgroundKeywordsWithLinks[keyword];
    if (currentBackgroundList && currentBackgroundList.length > 0) {
      currentBackgroundIndex = -1;
      nextBgURL = null;
      currentBackgroundKeyword = keyword;
      changeBackground(); // Queue the next background
      return true; // Indicate that the background list has changed
    }
  }
  return false; // Indicate that the background list hasn't changed
}

function applyBackground() {
  if (nextBgURL) {
    console.log(`Applying BG ${nextBgURL}`);
    oc.thread.messages.at(-1).scene = {
      'background': {
        'url': nextBgURL
      }
    };
    nextBgURL = null;
    lastBackgroundChangeTime = Date.now();
    timerExpired = false; // Reset the timer expired flag
    setTimeout(() => {
      timerExpired = true;
    }, intervalSeconds * 1000);
  }
}

const applyKeywordBG = function ({ message }) {
  let normalizedMessage = normalizeString(message.content);

  let foundBGs = Object.keys(backgroundKeywordsWithLinks).filter(key => normalizedMessage.includes(normalizeString(key)));
  let foundAVATARs = Object.keys(avatarUrls).filter(key => normalizedMessage.includes(normalizeString(key)));

  let backgroundListChanged = false;
  if (foundBGs.length > 0) {
    // Use only the last matching background keyword
    let lastFoundBG = foundBGs[foundBGs.length - 1];
    backgroundListChanged = handleBackgroundKeyword(lastFoundBG);
  }

  if (foundAVATARs.length > 0) {
    // Use only the last matching avatar keyword
    let lastFoundAVATAR = foundAVATARs[foundAVATARs.length - 1];
    oc.character.avatar.url = avatarUrls[lastFoundAVATAR];
  }

  if (backgroundListChanged) {
    applyBackground(); // Apply immediately if the background list has changed
  } else if (timerExpired) {
    changeBackground(); // Queue the next background in the rotation
    applyBackground(); // Apply the queued background
  }
};

oc.thread.on("MessageAdded", async function() {
  let lastMessage = oc.thread.messages.at(-1);
  if (lastMessage.author !== "user" || !lastMessage.content.includes("/rewrite")) return; // only edit USER messages containing "/rewrite"

  // Remove the "/rewrite" trigger from the message content
  let messageContent = lastMessage.content.replace("/rewrite", "").trim();

  let instruction = `
Here's a message:
---
${messageContent}
---
Rewrite this message in a literary style (adult novel, high reading level). Paragraphs should be long and full of atmospheric and immersive description. Environments should be painted with an inspired brush. There should be vivid characterization, and detailed descriptions of thoughts, emotions, body-language, and facial expressions. Respond with only the rewritten message. Do not alter the meaning of the message significantly.
`.trim();

  // TODO: I should rewrite this example using `oc.getInstructCompletion({instruction:"...", startWith:"..."})`, since it'll likely give better results.
  let response = await oc.getChatCompletion({
    messages: [
      {author:"system", content:"You are a message editing assistant. You rewrite messages according the the user's instruction. You respond only with the edited message. You will not change or alter the meaning of the message significantly, nor will you progress the story too much."},
      {author:"user", content:instruction},
    ],
  });
  lastMessage.content = response;
});

// Initialize the backgroundKeywordsWithLinks object and start the timer
initializeBackgroundKeywordsWithLinks().then(() => {
  console.log('Initialization complete. Setting up event listeners and starting timer.');
  oc.thread.on("MessageAdded", applyKeywordBG);
  oc.thread.on("MessageEdited", applyKeywordBG);
  oc.thread.on("MessageInserted", applyKeywordBG);

  // Start the initial timer
  setTimeout(() => {
    timerExpired = true;
  }, intervalSeconds * 1000);
});

//END

//(OLD) RESIZE/RESHAPE AVATAR TO MATCH ACTIVE CHAR

//START

1
2
3
4
5
6
7
8
oc.thread.on("MessageAdded", async function() {
  oc.thread.messages.forEach(a => {
    a.avatar = {
      size: oc.character.avatar.size,
      shape: oc.character.avatar.shape
    };
  });
});

//END

//CHANGE AVATAR BASED ON MOOD

//START

// Expression to avatar URL mapping
let expressions = `
neutral, happy: https://i.imgur.com/gPaq8YS.jpeg
horrified, shocked: https://i.imgur.com/aoDL1QP.jpeg
drunk: https://i.imgur.com/anoE7tj.jpeg
wistful, dreamy: https://i.imgur.com/dMcGtOA.jpeg
gross, disgusted, eww: https://i.imgur.com/F7NYSk0.jpeg
confident: https://i.imgur.com/KQS54ET.jpeg
beaming, proud of self, happy and alert: https://i.imgur.com/Y3NBEr4.jpeg
sorry, apologetic: https://i.imgur.com/5d8qxBd.jpeg
angry: https://i.imgur.com/51jbvuM.jpeg
sly: https://i.imgur.com/2Tcw7DO.jpeg
sly, hint hint nudge nudge: https://i.imgur.com/Mpt4UIt.jpeg
relaxed confident grin: https://i.imgur.com/EGDfzaN.jpeg
concerned: https://i.imgur.com/rYFlBDd.jpeg
worried, scared: https://i.imgur.com/5rp01eP.jpeg
concerned: https://i.imgur.com/V4Y3jUh.jpeg
disbelief: https://i.imgur.com/D05qdJ5.jpeg
happy, optimistic: https://i.imgur.com/B6tWeLV.jpeg
very surprised, frozen, stunned: https://i.imgur.com/Ra5Pb4c.jpeg
caught red handed: https://i.imgur.com/fvfw0Lc.jpeg
cool, dismissive: https://i.imgur.com/Z38xuvY.jpeg
patronising, teacherly: https://i.imgur.com/Tq1gKKw.jpeg
charming, sexy eyes: https://i.imgur.com/ny6HoRC.jpeg
disappointed: https://i.imgur.com/vxhjb6U.jpeg
disapproving face: https://i.imgur.com/x5XiOgv.jpeg
wacky, crazy, fun: https://i.imgur.com/9Q2osAe.jpeg
woops: https://i.imgur.com/CwYTcDO.jpeg
sucking up to someone: https://i.imgur.com/FkwJs8X.jpeg
staring blankly: https://i.imgur.com/JSMx8EW.jpeg
`.trim().split("\n").map(l => [l.trim().split(":")[0].trim(), l.trim().split(":").slice(1).join(":").trim()]).map(a => ({label:a[0], url:a[1]}));

let numMessagesInContext = 4; // Number of historical messages to consider for context

oc.thread.on("messageadded", async function() {
  let lastMessage = oc.thread.messages.at(-1);
  if(lastMessage.author !== "ai") return;

  let questionText = `I'm about to ask you to classify the facial expression of a particular message, but here's some context first:

---
${oc.thread.messages.slice(-numMessagesInContext).filter(m => m.role!=="system").map(m => (m.author=="ai" ? `[${oc.character.name}]: ` : `[Anon]: `)+m.content).join("\n\n")}
---

Okay, now that you have the context, please classify the facial expression of the following text:

---
${lastMessage.content}
---

Choose between the following categories:

${expressions.map((e, i) => `${i}) ${e.label}`).join("\n")}

Please respond with the number which corresponds to the facial expression that most accurately matches the given message. Respond with just the number - nothing else.`;

  let response = await oc.getInstructCompletion({
    instruction: questionText,
    startWith: ""
  });

  let index = parseInt(response.trim());
  if (isNaN(index) || index < 0 || index >= expressions.length) {
    console.log("Invalid response from AI:", response);
    return;
  }

  let expressionObj = expressions[index];
  console.log("Selected expression:", expressionObj.label);

  // Update the character's avatar
  oc.character.avatar.url = expressionObj.url;
  console.log("Avatar updated to:", expressionObj.url);
});

END

//SMART NARRATOR

//START

// URL of the external text file containing entries
const entryFileUrl = 'https://www.dropbox.com/scl/fi/3si9noalmqoc2fyqvcuwj/this-is-a-paragraph-test3..txt?rlkey=0ub2t18exgql36orei8k88fdw&dl=1';

// Global variables
let entries = [];
let currentEntryIndex = 0;
let isPaused = false;

// Function to fetch and parse the external text file
async function fetchEntries(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
    }
    const text = await response.text();
    // Split by newline and filter out any empty lines
    return text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
  } catch (error) {
    console.error('Error fetching the entry file:', error);
    return [];
  }
}

// Function to format the entry for insertion
function formatEntry(entry) {
  const parts = entry.split(';');
  if (parts.length > 1) {
    // For entries with multiple parts (name, pronunciation, description)
    return parts.map(part => part.trim()).join('\n');
  } else {
    // For entries with just a description (like Nicnevin)
    return entry;
  }
}

// Function to update the editable paragraph in roleInstruction
function updateEditableParagraph() {
  // Split the roleInstruction into JSON part and editable part
  let [jsonPart, editablePart] = oc.character.roleInstruction.split('**Plot-Point**').map(part => part.trim());

  // Format and replace the entire editable part with the new entry
  const formattedEntry = formatEntry(entries[currentEntryIndex]);
  const newRoleInstruction = `${jsonPart}\n**Plot-Point**\n${formattedEntry}`;

  // Update the character's roleInstruction
  oc.character.roleInstruction = newRoleInstruction;

  console.log(`Updated to entry ${currentEntryIndex + 1} of ${entries.length}`);
}

// Function to check if the current plot element has completed
async function hasPlotElementCompleted() {
  const relevantMessages = oc.thread.messages.slice(-5, -1);  // Get last 5 messages, excluding the most recent
  const context = relevantMessages.map(m => `[${m.author === 'ai' ? oc.character.name : 'Anon'}]: ${m.content}`).join("\n\n");

  const questionText = `Based on the following context, has the current plot element been completed? Please respond with just 'yes' or 'no'.

Context:
${context}

Current plot element:
${entries[currentEntryIndex]}`;

  const response = await oc.getInstructCompletion({
    instruction: questionText,
    startWith: ""
  });

  return response.trim().toLowerCase() === 'yes';
}

// Function to handle user commands
function handleUserCommand(message) {
  const commandRegex = /\/(plot-advance|plot-pause|plot-rewind)/;
  const match = message.content.match(commandRegex);

  if (match) {
    const command = match[1];
    switch (command) {
      case 'plot-advance':
        if (!isPaused) {
          currentEntryIndex = (currentEntryIndex + 1) % entries.length;
          updateEditableParagraph();
        }
        break;
      case 'plot-pause':
        isPaused = !isPaused;
        console.log(isPaused ? 'Plot advancement paused' : 'Plot advancement resumed');
        break;
      case 'plot-rewind':
        if (!isPaused) {
          currentEntryIndex = (currentEntryIndex - 1 + entries.length) % entries.length;
          updateEditableParagraph();
        }
        break;
    }

    // Remove the command from the message
    message.content = message.content.replace(commandRegex, '').trim();
  }
}

// Initialize the script
(async function init() {
  entries = await fetchEntries(entryFileUrl);
  if (entries.length > 0) {
    console.log(`Loaded ${entries.length} entries`);
    updateEditableParagraph();
  } else {
    console.error('No entries loaded. Check the URL and file format.');
  }
})();

// Event handler for message addition
oc.thread.on("MessageAdded", async ({ message }) => {
  handleUserCommand(message);

  if (!isPaused && await hasPlotElementCompleted()) {
    currentEntryIndex = (currentEntryIndex + 1) % entries.length;
    updateEditableParagraph();
  }
});

//END

//(OLD) AUTO-AVVY RESIZING + KEYWORD BG CHANGE

//START

const backgroundUrls = {
  "Your bg keyword": "image-url.jpeg",
  "Your bg keyword2": "image-url2.jpeg",
  // Add more background keywords as needed
};

const avatarUrls = {
  "Your avvy keyword": "image-url.jpeg",
  "Your avvy keyword 2": "image-url.jpeg",
  // Add more avatar keywords as needed
};

oc.thread.on("MessageAdded", async function() {
  oc.thread.messages.forEach(a => {
    a.avatar = {
      size: oc.character.avatar.size,
      shape: oc.character.avatar.shape
    };
  });
});


function normalizeString(str) {
  return str.toLowerCase().replace(/[\s'"]/g, ''); // Convert to lower case and remove spaces and single quotes
}

oc.thread.on("MessageAdded", function({message}) {
  let normalizedMessage = normalizeString(message.content); // Normalize message content

  let foundBG = Object.keys(backgroundUrls).find(key => normalizedMessage.includes(normalizeString(key))); 
  let foundAVATAR = Object.keys(avatarUrls).find(key => normalizedMessage.includes(normalizeString(key)));

  if (foundBG) {
    oc.thread.messages.at(-1).scene = { 'background': { 'url': backgroundUrls[foundBG] } }; // Properly updating scene background
  }

  if (foundAVATAR) {
    oc.character.avatar.url = avatarUrls[foundAVATAR]; // Properly updating avatar URL
  }
});

//END

//(OLD) AUTO-BG + AVVY SWITCHING

//START

const backgroundUrls = {
  'keyword': 'img url',
  'keyword': 'img url',
  // Add more background keywords and URLs as needed
};

const avatarUrls = {
  'happy': 'happy_avatar_url.jpeg',
  'sad': 'sad_avatar_url.jpeg',
  // Add more avatar keywords as needed
};

oc.thread.on("MessageAdded", function({message}) {
  let words = message.content.toLowerCase() // just separates the text into words
  let foundBG = Object.keys(backgroundUrls).find((a) => words.includes(a)); // finds the keyword that is found on the message
  let foundAVATAR = Object.keys(avatarUrls).find((a) => words.includes(a));
  if (foundBG) oc.thread.messages.at(-1).scene = { 'background': {'url': backgroundUrls[foundBG]} }; // if a background keyword is found, change the bg
  if (foundAVATAR) oc.character.avatar.url = avatarUrls[foundAVATAR]; // if an avatar keyword is found, change the avatar

});

//END

Edit
Pub: 19 Sep 2024 18:03 UTC
Edit: 19 Sep 2024 19:11 UTC
Views: 118