2023-07-20 19:32:15 +02:00
import {
addOneMessage ,
characters ,
chat ,
chat _metadata ,
default _avatar ,
eventSource ,
event _types ,
extractMessageBias ,
getThumbnailUrl ,
replaceBiasMarkup ,
saveChatConditional ,
sendSystemMessage ,
setUserName ,
substituteParams ,
comment _avatar ,
system _avatar ,
system _message _types ,
setCharacterId ,
generateQuietPrompt ,
reloadCurrentChat ,
2023-07-26 20:00:36 +02:00
sendMessageAsUser ,
2023-07-20 19:32:15 +02:00
} from "../script.js" ;
2023-08-22 17:13:03 +02:00
import { getMessageTimeStamp } from "./RossAscends-mods.js" ;
2023-07-20 19:32:15 +02:00
import { resetSelectedGroup } from "./group-chats.js" ;
import { getRegexedString , regex _placement } from "./extensions/regex/engine.js" ;
import { chat _styles , power _user } from "./power-user.js" ;
2023-08-29 00:54:11 +02:00
import { autoSelectPersona } from "./personas.js" ;
2023-07-20 19:32:15 +02:00
export {
executeSlashCommands ,
registerSlashCommand ,
getSlashCommandsHelp ,
}
class SlashCommandParser {
constructor ( ) {
this . commands = { } ;
this . helpStrings = [ ] ;
}
addCommand ( command , callback , aliases , helpString = '' , interruptsGeneration = false , purgeFromMessage = true ) {
const fnObj = { callback , helpString , interruptsGeneration , purgeFromMessage } ;
if ( [ command , ... aliases ] . some ( x => this . commands . hasOwnProperty ( x ) ) ) {
console . trace ( 'WARN: Duplicate slash command registered!' ) ;
}
this . commands [ command ] = fnObj ;
if ( Array . isArray ( aliases ) ) {
aliases . forEach ( ( alias ) => {
this . commands [ alias ] = fnObj ;
} ) ;
}
let stringBuilder = ` <span class="monospace">/ ${ command } </span> ${ helpString } ` ;
if ( Array . isArray ( aliases ) && aliases . length ) {
let aliasesString = ` (aliases: ${ aliases . map ( x => ` <span class="monospace">/ ${ x } </span> ` ) . join ( ', ' ) } ) ` ;
stringBuilder += aliasesString ;
}
this . helpStrings . push ( stringBuilder ) ;
}
parse ( text ) {
const excludedFromRegex = [ "sendas" ]
const firstSpace = text . indexOf ( ' ' ) ;
const command = firstSpace !== - 1 ? text . substring ( 1 , firstSpace ) : text . substring ( 1 ) ;
const args = firstSpace !== - 1 ? text . substring ( firstSpace + 1 ) : '' ;
const argObj = { } ;
let unnamedArg ;
if ( args . length > 0 ) {
const argsArray = args . split ( ' ' ) ;
for ( let arg of argsArray ) {
const equalsIndex = arg . indexOf ( '=' ) ;
if ( equalsIndex !== - 1 ) {
const key = arg . substring ( 0 , equalsIndex ) ;
const value = arg . substring ( equalsIndex + 1 ) ;
argObj [ key ] = value ;
}
else {
break ;
}
}
unnamedArg = argsArray . slice ( Object . keys ( argObj ) . length ) . join ( ' ' ) ;
// Excluded commands format in their own function
if ( ! excludedFromRegex . includes ( command ) ) {
unnamedArg = getRegexedString (
unnamedArg ,
regex _placement . SLASH _COMMAND
) ;
}
}
if ( this . commands [ command ] ) {
return { command : this . commands [ command ] , args : argObj , value : unnamedArg } ;
}
return false ;
}
getHelpString ( ) {
const listItems = this . helpStrings . map ( x => ` <li> ${ x } </li> ` ) . join ( '\n' ) ;
return ` <p>Slash commands:</p><ol> ${ listItems } </ol> ` ;
}
}
const parser = new SlashCommandParser ( ) ;
const registerSlashCommand = parser . addCommand . bind ( parser ) ;
const getSlashCommandsHelp = parser . getHelpString . bind ( parser ) ;
parser . addCommand ( 'help' , helpCommandCallback , [ '?' ] , ' – displays this help message' , true , true ) ;
parser . addCommand ( 'name' , setNameCallback , [ 'persona' ] , '<span class="monospace">(name)</span> – sets user name and persona avatar (if set)' , true , true ) ;
parser . addCommand ( 'sync' , syncCallback , [ ] , ' – syncs user name in user-attributed messages in the current chat' , true , true ) ;
parser . addCommand ( 'lock' , bindCallback , [ 'bind' ] , ' – locks/unlocks a persona (name and avatar) to the current chat' , true , true ) ;
parser . addCommand ( 'bg' , setBackgroundCallback , [ 'background' ] , '<span class="monospace">(filename)</span> – sets a background according to filename, partial names allowed, will set the first one alphabetically if multiple files begin with the provided argument string' , false , true ) ;
parser . addCommand ( 'sendas' , sendMessageAs , [ ] , ` – sends message as a specific character.<br>Example:<br><pre><code>/sendas Chloe \n Hello, guys!</code></pre>will send "Hello, guys!" from "Chloe".<br>Uses character avatar if it exists in the characters list. ` , true , true ) ;
parser . addCommand ( 'sys' , sendNarratorMessage , [ ] , '<span class="monospace">(text)</span> – sends message as a system narrator' , false , true ) ;
parser . addCommand ( 'sysname' , setNarratorName , [ ] , '<span class="monospace">(name)</span> – sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.' , true , true ) ;
parser . addCommand ( 'comment' , sendCommentMessage , [ ] , '<span class="monospace">(text)</span> – adds a note/comment message not part of the chat' , false , true ) ;
parser . addCommand ( 'single' , setStoryModeCallback , [ 'story' ] , ' – sets the message style to single document mode without names or avatars visible' , true , true ) ;
parser . addCommand ( 'bubble' , setBubbleModeCallback , [ 'bubbles' ] , ' – sets the message style to bubble chat mode' , true , true ) ;
parser . addCommand ( 'flat' , setFlatModeCallback , [ 'default' ] , ' – sets the message style to flat chat mode' , true , true ) ;
parser . addCommand ( 'continue' , continueChatCallback , [ 'cont' ] , ' – continues the last message in the chat' , true , true ) ;
parser . addCommand ( 'go' , goToCharacterCallback , [ 'char' ] , '<span class="monospace">(name)</span> – opens up a chat with the character by its name' , true , true ) ;
parser . addCommand ( 'sysgen' , generateSystemMessage , [ ] , '<span class="monospace">(prompt)</span> – generates a system message using a specified prompt' , true , true ) ;
parser . addCommand ( 'delname' , deleteMessagesByNameCallback , [ 'cancel' ] , '<span class="monospace">(name)</span> – deletes all messages attributed to a specified name' , true , true ) ;
2023-07-26 20:00:36 +02:00
parser . addCommand ( 'send' , sendUserMessageCallback , [ 'add' ] , '<span class="monospace">(text)</span> – adds a user message to the chat log without triggering a generation' , true , true ) ;
2023-07-20 19:32:15 +02:00
const NARRATOR _NAME _KEY = 'narrator_name' ;
const NARRATOR _NAME _DEFAULT = 'System' ;
2023-08-26 19:26:23 +02:00
export const COMMENT _NAME _DEFAULT = 'Note' ;
2023-07-20 19:32:15 +02:00
2023-07-26 20:00:36 +02:00
async function sendUserMessageCallback ( _ , text ) {
if ( ! text ) {
console . warn ( 'WARN: No text provided for /send command' ) ;
return ;
}
text = text . trim ( ) ;
const bias = extractMessageBias ( text ) ;
sendMessageAsUser ( text , bias ) ;
}
2023-07-20 19:32:15 +02:00
async function deleteMessagesByNameCallback ( _ , name ) {
if ( ! name ) {
console . warn ( 'WARN: No name provided for /delname command' ) ;
return ;
}
name = name . trim ( ) ;
const messagesToDelete = [ ] ;
chat . forEach ( ( value ) => {
if ( value . name === name ) {
messagesToDelete . push ( value ) ;
}
} ) ;
if ( ! messagesToDelete . length ) {
console . debug ( '/delname: Nothing to delete' ) ;
return ;
}
for ( const message of messagesToDelete ) {
const index = chat . indexOf ( message ) ;
if ( index !== - 1 ) {
console . debug ( ` /delname: Deleting message # ${ index } ` , message ) ;
chat . splice ( index , 1 ) ;
}
}
await saveChatConditional ( ) ;
await reloadCurrentChat ( ) ;
toastr . info ( ` Deleted ${ messagesToDelete . length } messages from ${ name } ` ) ;
}
function findCharacterIndex ( name ) {
const matchTypes = [
( a , b ) => a === b ,
( a , b ) => a . startsWith ( b ) ,
( a , b ) => a . includes ( b ) ,
] ;
for ( const matchType of matchTypes ) {
const index = characters . findIndex ( x => matchType ( x . name . toLowerCase ( ) , name . toLowerCase ( ) ) ) ;
if ( index !== - 1 ) {
return index ;
}
}
return - 1 ;
}
function goToCharacterCallback ( _ , name ) {
if ( ! name ) {
console . warn ( 'WARN: No character name provided for /go command' ) ;
return ;
}
name = name . trim ( ) ;
const characterIndex = findCharacterIndex ( name ) ;
if ( characterIndex !== - 1 ) {
2023-07-24 16:22:51 +02:00
openChat ( new String ( characterIndex ) ) ;
2023-07-20 19:32:15 +02:00
} else {
console . warn ( ` No matches found for name " ${ name } " ` ) ;
}
}
function openChat ( id ) {
resetSelectedGroup ( ) ;
setCharacterId ( id ) ;
setTimeout ( ( ) => {
reloadCurrentChat ( ) ;
} , 1 ) ;
}
function continueChatCallback ( ) {
// Prevent infinite recursion
$ ( '#send_textarea' ) . val ( '' ) ;
$ ( '#option_continue' ) . trigger ( 'click' , { fromSlashCommand : true } ) ;
}
async function generateSystemMessage ( _ , prompt ) {
$ ( '#send_textarea' ) . val ( '' ) ;
if ( ! prompt ) {
console . warn ( 'WARN: No prompt provided for /sysgen command' ) ;
toastr . warning ( 'You must provide a prompt for the system message' ) ;
return ;
}
// Generate and regex the output if applicable
toastr . info ( 'Please wait' , 'Generating...' ) ;
let message = await generateQuietPrompt ( prompt ) ;
message = getRegexedString ( message , regex _placement . SLASH _COMMAND ) ;
sendNarratorMessage ( _ , message ) ;
}
function syncCallback ( ) {
$ ( '#sync_name_button' ) . trigger ( 'click' ) ;
}
function bindCallback ( ) {
$ ( '#lock_user_name' ) . trigger ( 'click' ) ;
}
function setStoryModeCallback ( ) {
$ ( '#chat_display' ) . val ( chat _styles . DOCUMENT ) . trigger ( 'change' ) ;
}
function setBubbleModeCallback ( ) {
$ ( '#chat_display' ) . val ( chat _styles . BUBBLES ) . trigger ( 'change' ) ;
}
function setFlatModeCallback ( ) {
$ ( '#chat_display' ) . val ( chat _styles . DEFAULT ) . trigger ( 'change' ) ;
}
function setNameCallback ( _ , name ) {
if ( ! name ) {
toastr . warning ( 'you must specify a name to change to' )
return ;
}
name = name . trim ( ) ;
// If the name is a persona, auto-select it
for ( let persona of Object . values ( power _user . personas ) ) {
if ( persona . toLowerCase ( ) === name . toLowerCase ( ) ) {
autoSelectPersona ( name ) ;
return ;
}
}
// Otherwise, set just the name
setUserName ( name ) ; //this prevented quickReply usage
}
2023-08-23 09:32:48 +02:00
async function setNarratorName ( _ , text ) {
2023-07-20 19:32:15 +02:00
const name = text || NARRATOR _NAME _DEFAULT ;
chat _metadata [ NARRATOR _NAME _KEY ] = name ;
toastr . info ( ` System narrator name set to ${ name } ` ) ;
2023-08-23 09:32:48 +02:00
await saveChatConditional ( ) ;
2023-07-20 19:32:15 +02:00
}
async function sendMessageAs ( _ , text ) {
if ( ! text ) {
return ;
}
const parts = text . split ( '\n' ) ;
if ( parts . length <= 1 ) {
toastr . warning ( 'Both character name and message are required. Separate them with a new line.' ) ;
return ;
}
const name = parts . shift ( ) . trim ( ) ;
let mesText = parts . join ( '\n' ) . trim ( ) ;
// Requires a regex check after the slash command is pushed to output
mesText = getRegexedString ( mesText , regex _placement . SLASH _COMMAND , { characterOverride : name } ) ;
// Messages that do nothing but set bias will be hidden from the context
const bias = extractMessageBias ( mesText ) ;
const isSystem = replaceBiasMarkup ( mesText ) . trim ( ) . length === 0 ;
const character = characters . find ( x => x . name === name ) ;
let force _avatar , original _avatar ;
if ( character && character . avatar !== 'none' ) {
force _avatar = getThumbnailUrl ( 'avatar' , character . avatar ) ;
original _avatar = character . avatar ;
}
else {
force _avatar = default _avatar ;
original _avatar = default _avatar ;
}
const message = {
name : name ,
is _user : false ,
is _name : true ,
is _system : isSystem ,
2023-08-22 17:13:03 +02:00
send _date : getMessageTimeStamp ( ) ,
2023-07-20 19:32:15 +02:00
mes : substituteParams ( mesText ) ,
force _avatar : force _avatar ,
original _avatar : original _avatar ,
extra : {
bias : bias . trim ( ) . length ? bias : null ,
gen _id : Date . now ( ) ,
}
} ;
chat . push ( message ) ;
await eventSource . emit ( event _types . MESSAGE _SENT , ( chat . length - 1 ) ) ;
2023-08-22 16:46:37 +02:00
addOneMessage ( message ) ;
2023-08-22 21:45:12 +02:00
await eventSource . emit ( event _types . USER _MESSAGE _RENDERED , ( chat . length - 1 ) ) ;
2023-08-23 09:32:48 +02:00
await saveChatConditional ( ) ;
2023-07-20 19:32:15 +02:00
}
async function sendNarratorMessage ( _ , text ) {
if ( ! text ) {
return ;
}
const name = chat _metadata [ NARRATOR _NAME _KEY ] || NARRATOR _NAME _DEFAULT ;
// Messages that do nothing but set bias will be hidden from the context
const bias = extractMessageBias ( text ) ;
const isSystem = replaceBiasMarkup ( text ) . trim ( ) . length === 0 ;
const message = {
name : name ,
is _user : false ,
is _name : false ,
is _system : isSystem ,
2023-08-22 17:13:03 +02:00
send _date : getMessageTimeStamp ( ) ,
2023-07-20 19:32:15 +02:00
mes : substituteParams ( text . trim ( ) ) ,
force _avatar : system _avatar ,
extra : {
type : system _message _types . NARRATOR ,
bias : bias . trim ( ) . length ? bias : null ,
gen _id : Date . now ( ) ,
} ,
} ;
chat . push ( message ) ;
await eventSource . emit ( event _types . MESSAGE _SENT , ( chat . length - 1 ) ) ;
2023-08-22 16:46:37 +02:00
addOneMessage ( message ) ;
2023-08-22 21:45:12 +02:00
await eventSource . emit ( event _types . USER _MESSAGE _RENDERED , ( chat . length - 1 ) ) ;
2023-08-23 09:32:48 +02:00
await saveChatConditional ( ) ;
2023-07-20 19:32:15 +02:00
}
async function sendCommentMessage ( _ , text ) {
if ( ! text ) {
return ;
}
const message = {
name : COMMENT _NAME _DEFAULT ,
is _user : false ,
is _name : true ,
2023-08-26 19:26:23 +02:00
is _system : true ,
2023-08-22 17:13:03 +02:00
send _date : getMessageTimeStamp ( ) ,
2023-07-20 19:32:15 +02:00
mes : substituteParams ( text . trim ( ) ) ,
force _avatar : comment _avatar ,
extra : {
type : system _message _types . COMMENT ,
gen _id : Date . now ( ) ,
} ,
} ;
chat . push ( message ) ;
await eventSource . emit ( event _types . MESSAGE _SENT , ( chat . length - 1 ) ) ;
2023-08-22 16:46:37 +02:00
addOneMessage ( message ) ;
2023-08-22 21:45:12 +02:00
await eventSource . emit ( event _types . USER _MESSAGE _RENDERED , ( chat . length - 1 ) ) ;
2023-08-23 09:32:48 +02:00
await saveChatConditional ( ) ;
2023-07-20 19:32:15 +02:00
}
function helpCommandCallback ( _ , type ) {
switch ( type ? . trim ( ) ) {
case 'slash' :
case '1' :
sendSystemMessage ( system _message _types . SLASH _COMMANDS ) ;
break ;
case 'format' :
case '2' :
sendSystemMessage ( system _message _types . FORMATTING ) ;
break ;
case 'hotkeys' :
case '3' :
sendSystemMessage ( system _message _types . HOTKEYS ) ;
break ;
case 'macros' :
case '4' :
sendSystemMessage ( system _message _types . MACROS ) ;
break ;
default :
sendSystemMessage ( system _message _types . HELP ) ;
break ;
}
}
2023-08-07 21:21:10 +02:00
$ ( document ) . on ( 'click' , '[data-displayHelp]' , function ( e ) {
e . preventDefault ( ) ;
const page = String ( $ ( this ) . data ( 'displayhelp' ) ) ;
helpCommandCallback ( null , page ) ;
} ) ;
2023-07-20 19:32:15 +02:00
function setBackgroundCallback ( _ , bg ) {
if ( ! bg ) {
return ;
}
console . log ( 'Set background to ' + bg ) ;
const bgElement = $ ( ` .bg_example[bgfile^=" ${ bg . trim ( ) } " ` ) ;
if ( bgElement . length ) {
bgElement . get ( 0 ) . click ( ) ;
}
}
function executeSlashCommands ( text ) {
if ( ! text ) {
return false ;
}
// Hack to allow multi-line slash commands
// All slash command messages should begin with a slash
const lines = text . split ( '|' ) . map ( line => line . trim ( ) ) ;
const linesToRemove = [ ] ;
let interrupt = false ;
for ( let index = 0 ; index < lines . length ; index ++ ) {
const trimmedLine = lines [ index ] . trim ( ) ;
if ( ! trimmedLine . startsWith ( '/' ) ) {
continue ;
}
const result = parser . parse ( trimmedLine ) ;
if ( ! result ) {
continue ;
}
console . debug ( 'Slash command executing:' , result ) ;
result . command . callback ( result . args , result . value ) ;
if ( result . command . interruptsGeneration ) {
interrupt = true ;
}
if ( result . command . purgeFromMessage ) {
linesToRemove . push ( lines [ index ] ) ;
}
}
const newText = lines . filter ( x => linesToRemove . indexOf ( x ) === - 1 ) . join ( '\n' ) ;
return { interrupt , newText } ;
}