Custom Code

If you open the advanced options in the character creation area then you'll see the "custom code" input. This allows you to add some JavaScript code that extend the functionality of your character.

Some examples of what you can do with this:

  • Allow a character to transform/edit itself (like the "Unknown" starter character)
  • Give your character access to the internet (e.g. so you can ask it to summarise webpages)
  • Improve your character's memory by setting up your own embedding/retrieval system (see "Storing Data" section below)
  • Give your character a custom voice using an API like ElevenLabs
  • Allow your character to run custom JS or Python code
  • Give your character the ability to create pictures using Stable Diffusion
  • Auto-delete/retry messages from your character that contain certain keywords
  • Change the background image of the chat, or the chat bubble style, or the avatar images, or the music, depending on what's happening in your story

Examples

After reading this doc to get a sense of the basics, visit this page for more complex, "real-world" examples: Custom Code Examples

The oc Object

Within your custom code, you can access and update oc.thread.messages. It's an array that looks like this:

[
  {
    author: "user",
    content: "Hello",
  },
  {
    author: "ai",
    content: "Hi.",
  },
  {
    author: "system",
    hiddenFrom: ["user"], // can contain "user" and/or "ai"
    expectsReply: false, // this means the AI won't automatically reply to this message
    content: "Here's an example system message that's hidden from the user and which the AI won't automatically reply to.",
  },
]

The most recent message is at the bottom/end of the array. The author field can be user, ai, or system. Use "system" for guiding the AI's behavior, and including context/info where it wouldn't make sense to have that context/info come from the user or the AI.

Below is an example that replaces :) with ૮ ˶ᵔ ᵕ ᵔ˶ ა in every message that is added to the thread. Just paste it into the custom code box to try it out.

1
2
3
oc.thread.on("MessageAdded", function({message}) {
  message.content = message.content.replaceAll(":)", "૮ ˶ᵔ ᵕ ᵔ˶ ა");
});

You can edit existing messages like in this example, and you can also delete them by just removing them from the oc.thread.messages array (with pop, shift, splice, or however else), and you can of course add new ones - e.g. with push/unshift.

Messages have a bunch of other properties which are mentioned futher down on this page. For example, here's how to randomize the text color of each message that is added to the chat thread using the wrapperStyle property:

1
2
3
4
5
6
oc.thread.on("MessageAdded", function({message}) {
  let red = Math.round(Math.random()*255);
  let green = Math.round(Math.random()*255);
  let blue = Math.round(Math.random()*255);
  message.wrapperStyle = `color:rgb(${red}, ${green}, ${blue});`;
});

Note that your MessageAdded handler can be async, and it'll be awaited so that you can be sure your code has finished running before the AI responds.

You can also access and edit character data via oc.character.propertyName. Here's a full list of all the property names that you can access and edit on the oc object:

  • character
    • name - text/string
    • avatar
      • url - url to an image
      • size - multiple of default size (default value is 1)
      • shape - "circle" or "square" or "portrait"
    • roleInstruction - text/string describing the character and their role in the chat
    • reminderMessage - text/string reminding the character of things it tends to forget
    • initialMessages - an array of message objects (see thread.messages below for valid message properties)
    • customCode - yep, a character can edit its own custom code
    • imagePromptPrefix - text added before the prompt for all images generated by the AI in chats with this character
    • imagePromptSuffix - text added after the prompt for all images generated by the AI in chats with this character
    • imagePromptTriggers - each line is of the form trigger phrase: description of the thing - see character editor for examples
    • shortcutButtons - an array of objects like {autoSend:false, insertionType:"replace", message:"/ai be silly", name: "silly response", clearAfterSend:true}. When a new chat thread is created, a snapshot of these shortcutButtons is copied over to the thread, so if you want to change the current buttons in the thread, you should edit oc.thread.shortcutButtons instead. Only change oc.character.shortcutButtons if you want to change the buttons that will be available for all future chat threads created with this character.
      • insertionType can be replace, or prepend (put before existing text), or append (put after existing text)
      • clearAfterSend and autoSend can both be either true or false
      • name is just the label used for the button
      • message is the content that you want to send or insert into the reply box
    • streamingResponse - true or false (default is true)
    • customData - an object/dict where you can store arbitrary data
      • PUBLIC - a special sub-property of customData that will be shared within character sharing URLs
  • thread
    • name - text/string
    • messages - an array of messages, where each message has:
      • content - required - the message text - it can include HTML, and is rendered as markdown by default (see oc.messageRenderingPipeline)
      • author - required - "user" or "ai"
      • name - if this is not undefined, then it overrides the default ai/user/system name as which is oc.thread.character.name or if that's undefined, then oc.character.name is used as the final fallback/default. If name is not defined for an author=="user" message, then oc.thread.userCharacter.name is the first fallback, and then oc.character.userCharacter.name, and then oc.userCharacter.name (which is read-only). And for the system character the first fallback is oc.thread.systemCharacter.name and then oc.character.systemCharacter.name.
      • hiddenFrom - array that can contain "user" or "ai" or both or neither
      • expectsReply - true (bot will reply to this message) or false (bot will not reply), or undefined (use default behavior - i.e. reply to user messages, but not own messages)
      • customData - message-specific custom data storage
      • avatar - this will override the user's/ai's default avatar for this particular message. See the above name property for info on fallbacks.
        • url - url to an image
        • size - multiple of default size (default value is 1)
        • shape - "circle" or "square" or "portrait"
      • wrapperStyle - css for the "message bubble" - e.g. "background:white; border-radius:10px; color:grey;"
        • note that you can include HTML within the content of message (but you should use oc.messageRenderingPipeline for visuals where possible - see below)
      • instruction - the instruction that was written in /ai <instruction> or /user <instruction> - used when the regenerate button is clicked
      • scene - the most recent message that has a scene is the scene that is "active"
        • background
          • url - image or video url
          • filter - css filter - e.g. hue-rotate(90deg); blur(5px)
        • music
          • url - audio url (also supports video urls)
          • volume - between 0 and 1
    • character - thread-specific character overrides
      • name - text/string
      • avatar
        • url
        • size
        • shape
      • reminderMessage
      • roleInstruction
    • userCharacter - thread-specific user character overrides
      • name
      • avatar
        • url
        • size
        • shape
    • systemCharacter - thread-specific system character overrides
      • name
      • avatar
        • url
        • size
        • shape
    • customData - thread-specific custom data storage
    • messageWrapperStyle - CSS applied to all messages in the thread, except those with message.wrapperStyle defined
    • shortcutButtons - see notes on oc.character.shortcutButtons, above.
  • messageRenderingPipeline - an array of processing functions that get applied to messages before they are seen by the user and/or the ai (see "Message Rendering" section below)

Note that many character properties aren't available in the character editor UI, so if you e.g. wanted to add a stop sequence for your character so it stops whenever it writes ":)", then you could do it by adding this text to the custom code text box in the character editor:

oc.character.stopSequences = [":)"];

Here's some custom code which allows the AI to see the contents of webpages/PDFs if you put URLs in your messages:

async function getPdfText(data) {
  let doc = await window.pdfjsLib.getDocument({data}).promise;
  let pageTexts = Array.from({length: doc.numPages}, async (v,i) => {
    return (await (await doc.getPage(i+1)).getTextContent()).items.map(token => token.str).join('');
  });
  return (await Promise.all(pageTexts)).join(' ');
}

oc.thread.on("MessageAdded", async function ({message}) {
  if(message.author === "user") {
    let urlsInLastMessage = [...message.content.matchAll(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g)].map(m => m[0]);
    if(urlsInLastMessage.length === 0) return;
    if(!window.Readability) window.Readability = await import("https://esm.sh/@mozilla/readability@0.4.4?no-check").then(m => m.Readability);
    let url = urlsInLastMessage.at(-1); // we use the last URL in the message, if there are multiple
    let blob = await fetch(url).then(r => r.blob());
    let output;
    if(blob.type === "application/pdf") {
      if(!window.pdfjsLib) {
        window.pdfjsLib = await import("https://cdn.jsdelivr.net/npm/pdfjs-dist@3.6.172/+esm").then(m => m.default);
        pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.6.172/build/pdf.worker.min.js";
      }
      let text = await getPdfText(await blob.arrayBuffer());
      output = text.slice(0, 5000); // <-- grab only the first 5000 characters (you can change this)
    } else {
      let html = await blob.text();
      let doc = new DOMParser().parseFromString(html, "text/html");
      let article = new Readability(doc).parse();
      output = `# ${article.title || "(no page title)"}\n\n${article.textContent}`;
      output = output.slice(0, 5000); // <-- grab only the first 5000 characters (you can change this)
    }
    oc.thread.messages.push({
      author: "system",
      hiddenFrom: ["user"], // hide the message from user so it doesn't get in the way of the conversation
      content: "Here's the content of the webpage that was linked in the previous message: \n\n"+output,
    });
  }
});

Custom code is executed securely (i.e. in a sandboxed iframe), so if you're using a character that was created by someone else (and that has some custom code), then their code won't be able to access your user settings or your messages with other characters, for example. The custom code only has access to the character data and the messages for your current conversation.

Here's some custom code that adds a /charname command that changes the name of the character. It intercepts the user messages, and if it begins with /charname, then it changes oc.character.name to whatever comes after /charname, and then deletes the message.

1
2
3
4
5
6
7
oc.thread.on("MessageAdded", async function ({message}) {
  let m = message; // the message that was just added
  if(m.author === "user" && m.content.startsWith("/charname ")) {
    oc.character.name = m.content.replace(/^\/charname /, "");
    oc.thread.messages.pop(); // remove the message
  }
});

Events

Each of these events has a message object, and MessageDeleted has originalIndex for the index of the deleted message:

  • oc.thread.on("MessageAdded", function({message}) { ... }) - a message was added to the end of the thread (note: this event triggers after the message has finished generating completely)
  • oc.thread.on("MessageEdited", function({message}) { ... }) - message was edited or regenerated
  • oc.thread.on("MessageInserted", function({message}) { ... }) - message was inserted (see message editing popup)
  • oc.thread.on("MessageDeleted", function({message, originalIndex}) { ... }) - user deleted a message (trash button)
  • oc.thread.on("MessageStreaming", function(data) { ... }) - see 'Streaming Messages' section below

The message object is an actual reference to the object, so you can edit it directly like this:

1
2
3
oc.thread.on("MessageAdded", function({message}) {
  message.content += "blah";
})

Here's an example of how you can get the index of edited messages:

1
2
3
4
oc.thread.on("MessageEdited", function({message}) {
  let editedMessageIndex = oc.thread.messages.findIndex(m => m === message);
  // ...
});

Message Rendering

Sometimes you may want to display different text to the user than what the AI sees. For that, you can use oc.messageRenderingPipeline. It's an array that you .push() a function into, and that function is used to process messages. Your function should use the reader parameter to determine who is "reading" the message (either user or ai), and then "render" the message content accordingly. Here's an example to get you started:

1
2
3
4
oc.messageRenderingPipeline.push(function({message, reader}) {
  if(reader === "user") message.content += "🌸"; // user will see all messages with a flower emoji appended
  if(reader === "ai") message.content = message.content.replaceAll("wow", "WOW"); // ai will see a version of the message with all instances of "wow" capitalized
});

Visual Display and User Inputs

Your custom code runs inside an iframe. You can visually display the iframe using oc.window.show() (and hide with oc.window.hide()). The user can drag the embed around on the page and resize it. All your custom code is running within the iframe embed whether it's currently displayed or not. You can display content in the embed by just executing custom code like document.body.innerHTML = "hello world".

You can use the embed to e.g. display a dynamic video/gif avatar for your character that changes depending on the emotion that is evident in the characters messages (example). Or to e.g. display the result of the p5.js code that the character is helping you write. And so on.

Using the AI in Your Custom Code

You may want to use GPT/LLM APIs in your message processing code. For example, you may want to classify the sentiment of a message in order to display the correct avatar (see "Visual Display ..." section), or you may want to implement your own custom chat-summarization system, for example. In this case, you can use oc.getInstructCompletion or oc.textToImage.

Here's how to use oc.getInstructCompletion (see the ai-text-plugin page for details on the parameters):

1
2
3
4
5
6
let result = await oc.getInstructCompletion({
  instruction: "Write the first paragraph of a story about fantasy world.",
  startWith: "Once upon a", // this is optional - to force the AI's to start its response with some specific text
  stopSequences: ["\n"], // this is optional - tells the AI to stop generating when it generates a newline
  ...
});

That gives you result.text, which is the whole text, including the startWith text that you specified, and result.generatedText, which is only the text that came after the startWith text - i.e. only the text that the AI actually generated.

Here's how to use oc.textToImage (see the text-to-image-plugin page for details on some other parameters you can use):

1
2
3
4
5
let result = await oc.textToImage({
  prompt: "anime style digital art of a sentient robot, forest background, painterly textures",
  negativePrompt: "night time, blurry", // this is optional - tells the AI what *not* to generate
  ...
});

And now you can use result.dataUrl, which will look something like data:image/jpeg;base64,s8G58o8ujR4...... A data URL is like a normal URL, except the data is stored in the URL itself instead of being stored on a server somewhere. But you can just treat it as if it were something like https://example.com/foo.jpeg.

You should use oc.getInstructCompletion for most tasks, but sometimes a chat-style API may be useful. Here's how to use oc.getChatCompletion:

1
2
3
4
5
let result = await oc.getChatCompletion({
  messages: [{author:"system", content:"..."}, {author:"user", content:"..."}, {author:"ai", content:"..."}, ...],
  stopSequences: ["\n"],
  ...
});

The messages parameter is the only required one.

Here's an example of some custom code that edits all messages to include more emojis:

1
2
3
4
5
6
oc.thread.on("MessageAdded", async function({message}) {
  let result = await oc.getChatCompletion({
    messages: [{author:"user", content:`Please edit the following message to have more emojis:\n\n---\n${message.content}\n---\n\nReply with only the above message (the content between ---), but with more (relevant) emojis.`}],
  });
  message.content = result.trim().replace(/^---|---$/g, "").trim();
});

Storing Custom Data

If you'd like to save some data that is generated by your custom code, then you can do that by using oc.thread.customData - e.g. oc.thread.customData.foo = 10. You can also store custom data on individual messages like this: message.customData.foo = 10. If you want to store data in the character itself, then use oc.character.customData.foo = 10, but note that this data will not be shared within character share links. If you do want to save the data to the character in a way that's preserved in character share links, then you should store data under oc.character.customData.PUBLIC - e.g. oc.character.customData.PUBLIC = {foo:10}.

Streaming Messages

See the text-to-speech plugin code for a "real-world" example of this.

1
2
3
4
5
oc.thread.on("StreamingMessage", async function (data) {
  for await (let chunk of data.chunks) {
    console.log(chunk.text); // `chunk.text` is a small fragment of text
  }
});

Interactive Messages

You can use button onclick handlers in message so that e.g. the user can click a button to take an action instead of typing:

1
2
3
What would you like to do?
1. <button onclick="oc.thread.messages.push({author:'user', content:'Fight'});">Fight</button>
2. <button onclick="oc.thread.messages.push({author:'user', content:'Run'});">Run</button>

I recommend that you use oc.messageRenderingPipeline to turn a custom format into HTML, rather than actually having HTML in your messages (the HTML would use more tokens, and might confuse the AI). So your format might look like this:

1
2
3
What would you like to do?
1. [[Fight]]
2. [[Run]]

You could prompt/instruct/remind your character to reply in that format with an instruction message that's something similar to this:

1
2
3
4
5
6
7
You are a game master. You creatively and engagingly simulate a world for the user. The user takes actions, and you describe the consequences.

Your messages should end with a list of possible actions, and each action should be wrapped in double-square brackets like this:

Actions:
1. [[Say sorry]]
2. [[Turn and run]]

And then you'd add this to your custom code:

1
2
3
4
5
6
7
8
oc.messageRenderingPipeline.push(function({message, reader}) {
  if(reader === "user") {
    message.content = message.content.replace(/\[\[(.+?)\]\]/g, (match, text) => {
      let encodedText = encodeURIComponent(text); // this is a 'hacky' but simple way to prevent special characters like quotes from breaking the onclick attribute
      return `<button onclick="oc.thread.messages.push({author:'user', content:decodeURIComponent('${encodedText}')});">${text}</button>`;
    });
  }
});

If you want to change something about the way this works (e.g. change the double-square-bracket format to something else), but don't know JavaScript, the "Custom Code Helper" starter character might be able to help you make some adjustments.

Note that you can't use the this keyword within the button onclick handler - it actually just sends the code in the onclick to your custom code iframe and executes it there, so there's no actual element that's firing the onclick from the iframe's perspective, and thus no this or event, etc.

Gotchas

"<function> is not defined" in click/event handlers

The following code won't work:

1
2
3
4
5
function hello() {
  console.log("hi");
}
document.body.innerHTML = `<div onclick="hello()">click me</div>`;
oc.window.show();

This is because all custom code is executed inside a <script type=module> so you need to make functions global if you want to access them from outside the module (e.g. in click handlers). So if you want to the above code to work, you should define the hello function like this instead:

1
2
3
window.hello = function() {
  console.log("hi");
}

FAQ

  • Is it possible to run a custom function before the AI tries to respond? I.e., after the user message lands, but before the AI responds? And then kick off the AI response process after the async call returns?
    • Answer: Yep, the MessageAdded event runs every time a message is added - user or ai. So you can check if(oc.thread.messages.at(-1).author === "user") { ... } (i.e. if latest message is from user) and the ... code will run right after the user responds, and before the ai responds.
Edit
Pub: 12 Nov 2023 22:00 UTC
Edit: 18 Sep 2024 01:29 UTC
Views: 61577