0% found this document useful (0 votes)
315 views5 pages

Perchance Code

The document explains how to use custom JavaScript code in character creation to enhance character functionality, including self-modification, internet access, and custom responses. It details the structure of messages and provides examples of manipulating message content, accessing character properties, and handling events. Additionally, it covers rendering messages differently for users and AI, and using AI APIs for tasks like text generation and image creation.

Uploaded by

el.sr.otaku
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
315 views5 pages

Perchance Code

The document explains how to use custom JavaScript code in character creation to enhance character functionality, including self-modification, internet access, and custom responses. It details the structure of messages and provides examples of manipulating message content, accessing character properties, and handling events. Additionally, it covers rendering messages differently for users and AI, and using AI APIs for tasks like text generation and image creation.

Uploaded by

el.sr.otaku
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 5

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.

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:

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 await ed 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/[email protected]?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/[email protected]/+esm").then(m => m.default);
pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.jsdelivr.net/npm/[email protected]/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.

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:

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

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

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:

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):

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):

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 :

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:

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.

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:

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:
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:

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:

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:

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:

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.

You might also like