2024-03-23 16:36:43 +01:00
import { saveSettings , callPopup , substituteParams , getRequestHeaders , chat _metadata , this _chid , characters , saveCharacterDebounced , menu _type , eventSource , event _types , getExtensionPromptByName , saveMetadata , getCurrentChatId , extension _prompt _roles } from '../script.js' ;
2024-07-22 21:34:53 +02:00
import { download , debounce , initScrollHeight , resetScrollHeight , parseJsonFile , extractDataFromPng , getFileBuffer , getCharaFilename , getSortableDelay , escapeRegex , PAGINATION _TEMPLATE , navigation _option , waitUntilCondition , isTrueBoolean , setValueByPath , flashHighlight , select2ModifyOptions , getSelect2OptionId , dynamicSelect2DataViaAjax , highlightRegex , select2ChoiceClickSubscribe , isFalseBoolean , getSanitizedFilename , checkOverwriteExistingData , getStringHash , parseStringArray , cancelDebounce } from './utils.js' ;
2023-12-02 19:04:51 +01:00
import { extension _settings , getContext } from './extensions.js' ;
import { NOTE _MODULE _NAME , metadata _keys , shouldWIAddPrompt } from './authors-note.js' ;
2023-12-15 00:03:10 +01:00
import { isMobile } from './RossAscends-mods.js' ;
2023-12-02 19:04:51 +01:00
import { FILTER _TYPES , FilterHelper } from './filters.js' ;
2024-04-13 20:33:19 +02:00
import { getTokenCountAsync } from './tokenizers.js' ;
2023-12-02 19:04:51 +01:00
import { power _user } from './power-user.js' ;
2024-03-07 23:48:50 +01:00
import { getTagKeyForEntity } from './tags.js' ;
2024-04-28 18:47:53 +02:00
import { debounce _timeout } from './constants.js' ;
2024-05-08 16:55:33 +02:00
import { getRegexedString , regex _placement } from './extensions/regex/engine.js' ;
2024-05-12 21:15:05 +02:00
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js' ;
import { SlashCommand } from './slash-commands/SlashCommand.js' ;
import { ARGUMENT _TYPE , SlashCommandArgument , SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js' ;
2024-06-20 20:33:45 +02:00
import { SlashCommandEnumValue , enumTypes } from './slash-commands/SlashCommandEnumValue.js' ;
2024-06-21 20:04:55 +02:00
import { commonEnumProviders , enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js' ;
2024-06-20 20:33:45 +02:00
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js' ;
2024-07-06 15:55:57 +02:00
import { callGenericPopup , Popup , POPUP _TYPE } from './popup.js' ;
2024-07-22 03:17:06 +02:00
import { StructuredCloneMap } from './util/StructuredCloneMap.js' ;
2024-08-06 05:20:19 +02:00
import { renderTemplateAsync } from './templates.js' ;
2023-07-20 19:32:15 +02:00
2024-08-02 20:42:53 +02:00
export const world _info _insertion _strategy = {
2023-07-20 19:32:15 +02:00
evenly : 0 ,
character _first : 1 ,
global _first : 2 ,
} ;
2024-08-02 20:42:53 +02:00
export const world _info _logic = {
2023-12-05 11:04:27 +01:00
AND _ANY : 0 ,
2023-12-05 10:00:26 +01:00
NOT _ALL : 1 ,
2023-12-05 11:04:27 +01:00
NOT _ANY : 2 ,
2024-01-01 20:49:54 +01:00
AND _ALL : 3 ,
2023-12-05 09:56:52 +01:00
} ;
2024-07-03 22:42:27 +02:00
/ * *
* @ enum { number } Possible states of the WI evaluation
* /
2024-08-02 20:42:53 +02:00
export const scan _state = {
2024-07-03 22:42:27 +02:00
/ * *
* The scan will be stopped .
* /
NONE : 0 ,
/ * *
2024-07-03 23:18:46 +02:00
* Initial state .
2024-07-03 22:42:27 +02:00
* /
2024-07-03 23:18:46 +02:00
INITIAL : 1 ,
/ * *
* The scan is triggered by a recursion step .
* /
RECURSION : 2 ,
2024-07-03 22:42:27 +02:00
/ * *
* The scan is triggered by a min activations depth skew .
* /
2024-07-05 00:01:55 +02:00
MIN _ACTIVATIONS : 3 ,
2024-07-03 22:42:27 +02:00
} ;
2024-04-05 12:45:28 +02:00
const WI _ENTRY _EDIT _TEMPLATE = $ ( '#entry_edit_template .world_entry' ) ;
2024-08-02 20:42:53 +02:00
export let world _info = { } ;
export let selected _world _info = [ ] ;
2024-05-22 18:19:01 +02:00
/** @type {string[]} */
2024-08-02 20:42:53 +02:00
export let world _names ;
export let world _info _depth = 2 ;
export let world _info _min _activations = 0 ; // if > 0, will continue seeking chat until minimum world infos are activated
export let world _info _min _activations _depth _max = 0 ; // used when (world_info_min_activations > 0)
export let world _info _budget = 25 ;
export let world _info _include _names = true ;
export let world _info _recursive = false ;
export let world _info _overflow _alert = false ;
export let world _info _case _sensitive = false ;
export let world _info _match _whole _words = false ;
export let world _info _use _group _scoring = false ;
export let world _info _character _strategy = world _info _insertion _strategy . character _first ;
export let world _info _budget _cap = 0 ;
2024-08-23 20:34:18 +02:00
export let world _info _max _recursion _steps = 0 ;
2024-04-28 06:21:47 +02:00
const saveWorldDebounced = debounce ( async ( name , data ) => await _save ( name , data ) , debounce _timeout . relaxed ) ;
2023-07-20 19:32:15 +02:00
const saveSettingsDebounced = debounce ( ( ) => {
2023-12-02 20:11:06 +01:00
Object . assign ( world _info , { globalSelect : selected _world _info } ) ;
saveSettings ( ) ;
2024-04-28 06:21:47 +02:00
} , debounce _timeout . relaxed ) ;
2023-07-20 19:32:15 +02:00
const sortFn = ( a , b ) => b . order - a . order ;
2024-05-14 04:51:22 +02:00
let updateEditor = ( navigation , flashOnNav = true ) => { console . debug ( 'Triggered WI navigation' , navigation , flashOnNav ) ; } ;
2023-08-21 17:16:40 +02:00
2023-08-22 00:51:31 +02:00
// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.
2024-08-02 20:42:53 +02:00
export const worldInfoFilter = new FilterHelper ( ( ) => updateEditor ( ) ) ;
export const SORT _ORDER _KEY = 'world_info_sort_order' ;
export const METADATA _KEY = 'world_info' ;
2023-08-22 00:51:31 +02:00
2024-08-02 20:42:53 +02:00
export const DEFAULT _DEPTH = 4 ;
export const DEFAULT _WEIGHT = 100 ;
export const MAX _SCAN _DEPTH = 1000 ;
2024-07-14 13:07:23 +02:00
const KNOWN _DECORATORS = [ '@@activate' , '@@dont_activate' ] ;
2024-01-23 21:44:20 +01:00
2024-06-22 02:15:13 +02:00
// Typedef area
/ * *
* @ typedef { object } WIScanEntry The entry that triggered the scan
* @ property { number } [ scanDepth ] The depth of the scan
* @ property { boolean } [ caseSensitive ] If the scan is case sensitive
* @ property { boolean } [ matchWholeWords ] If the scan should match whole words
* @ property { boolean } [ useGroupScoring ] If the scan should use group scoring
* @ property { number } [ uid ] The UID of the entry that triggered the scan
2024-06-22 02:54:54 +02:00
* @ property { string } [ world ] The world info book of origin of the entry
2024-06-22 02:15:13 +02:00
* @ property { string [ ] } [ key ] The primary keys to scan for
* @ property { string [ ] } [ keysecondary ] The secondary keys to scan for
* @ property { number } [ selectiveLogic ] The logic to use for selective activation
* @ property { number } [ sticky ] The sticky value of the entry
* @ property { number } [ cooldown ] The cooldown of the entry
2024-06-26 21:43:30 +02:00
* @ property { number } [ delay ] The delay of the entry
2024-07-14 22:13:19 +02:00
* @ property { string [ ] } [ decorators ] Array of decorators for the entry
2024-09-13 09:47:25 +02:00
* @ property { number } [ hash ] The hash of the entry
2024-06-22 02:15:13 +02:00
* /
/ * *
2024-06-23 20:18:18 +02:00
* @ typedef { object } WITimedEffect Timed effect for world info
* @ property { number } hash Hash of the entry that triggered the effect
* @ property { number } start The chat index where the effect starts
* @ property { number } end The chat index where the effect ends
2024-06-24 13:51:04 +02:00
* @ property { boolean } protected The protected effect can ' t be removed if the chat does not advance
2024-06-22 02:15:13 +02:00
* /
2024-06-22 13:56:46 +02:00
/ * *
2024-06-23 20:18:18 +02:00
* @ typedef TimedEffectType Type of timed effect
2024-06-26 21:43:30 +02:00
* @ type { 'sticky' | 'cooldown' | 'delay' }
2024-06-22 13:56:46 +02:00
* /
2024-06-22 02:15:13 +02:00
// End typedef area
2024-01-23 21:44:20 +01:00
/ * *
* Represents a scanning buffer for one evaluation of World Info .
* /
class WorldInfoBuffer {
2024-04-23 02:09:52 +02:00
/ * *
2024-10-10 14:52:54 +02:00
* @ type { Map < string , object > } Map of entries that need to be activated no matter what
2024-04-23 02:09:52 +02:00
* /
2024-10-10 14:52:54 +02:00
static externalActivations = new Map ( ) ;
2024-04-23 02:09:52 +02:00
2024-01-23 21:44:20 +01:00
/ * *
2024-01-24 12:07:56 +01:00
* @ type { string [ ] } Array of messages sorted by ascending depth
2024-01-23 21:44:20 +01:00
* /
# depthBuffer = [ ] ;
/ * *
* @ type { string [ ] } Array of strings added by recursive scanning
* /
# recurseBuffer = [ ] ;
2024-07-03 23:28:34 +02:00
/ * *
* @ type { string [ ] } Array of strings added by prompt injections that are valid for the current scan
* /
# injectBuffer = [ ] ;
2024-01-23 21:44:20 +01:00
/ * *
2024-01-24 12:07:56 +01:00
* @ type { number } The skew of the global scan depth . Used in "min activations"
2024-01-23 21:44:20 +01:00
* /
# skew = 0 ;
2024-04-04 07:56:39 +02:00
/ * *
2024-07-06 20:00:27 +02:00
* @ type { number } The starting depth of the global scan depth .
2024-04-04 07:56:39 +02:00
* /
# startDepth = 0 ;
2024-01-23 21:44:20 +01:00
/ * *
* Initialize the buffer with the given messages .
* @ param { string [ ] } messages Array of messages to add to the buffer
* /
constructor ( messages ) {
this . # initDepthBuffer ( messages ) ;
}
/ * *
* Populates the buffer with the given messages .
* @ param { string [ ] } messages Array of messages to add to the buffer
* @ returns { void } Hardly seen nothing down here
* /
# initDepthBuffer ( messages ) {
for ( let depth = 0 ; depth < MAX _SCAN _DEPTH ; depth ++ ) {
if ( messages [ depth ] ) {
this . # depthBuffer [ depth ] = messages [ depth ] . trim ( ) ;
}
2024-02-23 20:23:44 +01:00
// break if last message is reached
if ( depth === messages . length - 1 ) {
break ;
}
2024-01-23 21:44:20 +01:00
}
}
/ * *
* Gets a string that respects the case sensitivity setting
* @ param { string } str The string to transform
* @ param { WIScanEntry } entry The entry that triggered the scan
* @ returns { string } The transformed string
* /
# transformString ( str , entry ) {
const caseSensitive = entry . caseSensitive ? ? world _info _case _sensitive ;
return caseSensitive ? str : str . toLowerCase ( ) ;
}
/ * *
* Gets all messages up to the given depth + recursion buffer .
* @ param { WIScanEntry } entry The entry that triggered the scan
2024-07-03 23:28:34 +02:00
* @ param { number } scanState The state of the scan
2024-01-23 21:44:20 +01:00
* @ returns { string } A slice of buffer until the given depth ( inclusive )
* /
2024-07-03 23:28:34 +02:00
get ( entry , scanState ) {
2024-04-04 07:56:39 +02:00
let depth = entry . scanDepth ? ? this . getDepth ( ) ;
if ( depth <= this . # startDepth ) {
return '' ;
}
2024-01-23 21:44:20 +01:00
if ( depth < 0 ) {
2024-07-06 14:26:45 +02:00
console . error ( ` [WI] Invalid WI scan depth ${ depth } . Must be >= 0 ` ) ;
2024-01-23 21:44:20 +01:00
return '' ;
}
if ( depth > MAX _SCAN _DEPTH ) {
2024-07-06 14:26:45 +02:00
console . warn ( ` [WI] Invalid WI scan depth ${ depth } . Truncating to ${ MAX _SCAN _DEPTH } ` ) ;
2024-01-23 21:44:20 +01:00
depth = MAX _SCAN _DEPTH ;
}
2024-09-16 05:18:37 +02:00
const MATCHER = '\x01' ;
const JOINER = '\n' + MATCHER ;
let result = MATCHER + this . # depthBuffer . slice ( this . # startDepth , depth ) . join ( JOINER ) ;
2024-01-23 21:44:20 +01:00
2024-07-03 23:28:34 +02:00
if ( this . # injectBuffer . length > 0 ) {
2024-09-14 20:50:58 +02:00
result += JOINER + this . # injectBuffer . join ( JOINER ) ;
2024-07-03 23:28:34 +02:00
}
// Min activations should not include the recursion buffer
if ( this . # recurseBuffer . length > 0 && scanState !== scan _state . MIN _ACTIVATIONS ) {
2024-09-14 20:50:58 +02:00
result += JOINER + this . # recurseBuffer . join ( JOINER ) ;
2024-01-23 21:44:20 +01:00
}
2024-05-20 12:44:12 +02:00
return result ;
2024-01-23 21:44:20 +01:00
}
/ * *
* Matches the given string against the buffer .
* @ param { string } haystack The string to search in
* @ param { string } needle The string to search for
* @ param { WIScanEntry } entry The entry that triggered the scan
* @ returns { boolean } True if the string was found in the buffer
* /
matchKeys ( haystack , needle , entry ) {
2024-05-07 02:52:22 +02:00
// If the needle is a regex, we do regex pattern matching and override all the other options
const keyRegex = parseRegexFromString ( needle ) ;
if ( keyRegex ) {
return keyRegex . test ( haystack ) ;
}
// Otherwise we do normal matching of plaintext with the chosen entry settings
2024-05-20 12:44:12 +02:00
haystack = this . # transformString ( haystack , entry ) ;
2024-01-23 21:44:20 +01:00
const transformedString = this . # transformString ( needle , entry ) ;
const matchWholeWords = entry . matchWholeWords ? ? world _info _match _whole _words ;
if ( matchWholeWords ) {
const keyWords = transformedString . split ( /\s+/ ) ;
if ( keyWords . length > 1 ) {
return haystack . includes ( transformedString ) ;
}
else {
2024-04-30 23:45:26 +02:00
// Use custom boundaries to include punctuation and other non-alphanumeric characters
const regex = new RegExp ( ` (?:^| \\ W)( ${ escapeRegex ( transformedString ) } )(?: $ | \\ W) ` ) ;
2024-01-23 21:44:20 +01:00
if ( regex . test ( haystack ) ) {
return true ;
}
}
} else {
return haystack . includes ( transformedString ) ;
}
return false ;
}
/ * *
* Adds a message to the recursion buffer .
* @ param { string } message The message to add
* /
addRecurse ( message ) {
this . # recurseBuffer . push ( message ) ;
}
2024-07-03 23:28:34 +02:00
/ * *
* Adds an injection to the buffer .
* @ param { string } message The injection to add
* /
addInject ( message ) {
this . # injectBuffer . push ( message ) ;
}
2024-01-23 21:44:20 +01:00
/ * *
2024-07-06 02:44:26 +02:00
* Checks if the recursion buffer is not empty .
* @ returns { boolean } Returns true if the recursion buffer is not empty , otherwise false
2024-01-23 21:44:20 +01:00
* /
2024-07-06 02:44:26 +02:00
hasRecurse ( ) {
return this . # recurseBuffer . length > 0 ;
}
2024-01-23 21:44:20 +01:00
/ * *
2024-07-06 20:00:27 +02:00
* Increments skew to advance the scan range .
2024-01-23 21:44:20 +01:00
* /
2024-07-06 20:00:27 +02:00
advanceScan ( ) {
2024-01-23 21:44:20 +01:00
this . # skew ++ ;
}
2024-04-04 07:56:39 +02:00
/ * *
* @ returns { number } Settings ' depth + current skew .
* /
getDepth ( ) {
return world _info _depth + this . # skew ;
}
2024-04-23 02:09:52 +02:00
/ * *
2024-10-10 14:52:54 +02:00
* Get the externally activated version of the entry , if there is one .
2024-04-23 02:09:52 +02:00
* @ param { object } entry WI entry to check
2024-10-10 16:44:13 +02:00
* @ returns { object | undefined } the external version if the entry is forcefully activated , undefined otherwise
2024-04-23 02:09:52 +02:00
* /
2024-10-10 14:52:54 +02:00
getExternallyActivated ( entry ) {
return WorldInfoBuffer . externalActivations . get ( ` ${ entry . world } . ${ entry . uid } ` ) ;
2024-04-23 02:09:52 +02:00
}
2024-06-20 23:53:00 +02:00
/ * *
2024-06-23 00:52:10 +02:00
* Clean - up the external effects for entries .
2024-06-20 23:53:00 +02:00
* /
resetExternalEffects ( ) {
2024-10-10 14:52:54 +02:00
WorldInfoBuffer . externalActivations = new Map ( ) ;
2024-04-23 02:09:52 +02:00
}
2024-05-04 22:51:28 +02:00
/ * *
* Gets the match score for the given entry .
* @ param { WIScanEntry } entry Entry to check
2024-07-03 23:28:34 +02:00
* @ param { number } scanState The state of the scan
2024-05-04 22:51:28 +02:00
* @ returns { number } The number of key activations for the given entry
* /
2024-07-03 23:28:34 +02:00
getScore ( entry , scanState ) {
const bufferState = this . get ( entry , scanState ) ;
2024-05-04 22:51:28 +02:00
let numberOfPrimaryKeys = 0 ;
let numberOfSecondaryKeys = 0 ;
let primaryScore = 0 ;
let secondaryScore = 0 ;
// Increment score for every key found in the buffer
if ( Array . isArray ( entry . key ) ) {
numberOfPrimaryKeys = entry . key . length ;
for ( const key of entry . key ) {
if ( this . matchKeys ( bufferState , key , entry ) ) {
primaryScore ++ ;
}
}
}
// Increment score for every secondary key found in the buffer
if ( Array . isArray ( entry . keysecondary ) ) {
numberOfSecondaryKeys = entry . keysecondary . length ;
for ( const key of entry . keysecondary ) {
if ( this . matchKeys ( bufferState , key , entry ) ) {
secondaryScore ++ ;
}
}
}
// No keys == no score
if ( ! numberOfPrimaryKeys ) {
return 0 ;
}
// Only positive logic influences the score
if ( numberOfSecondaryKeys > 0 ) {
switch ( entry . selectiveLogic ) {
// AND_ANY: Add both scores
case world _info _logic . AND _ANY :
return primaryScore + secondaryScore ;
// AND_ALL: Add both scores if all secondary keys are found, otherwise only primary score
case world _info _logic . AND _ALL :
return secondaryScore === numberOfSecondaryKeys ? primaryScore + secondaryScore : primaryScore ;
}
}
return primaryScore ;
}
2024-01-23 21:44:20 +01:00
}
2023-09-24 13:45:04 +02:00
2024-06-22 13:56:46 +02:00
/ * *
2024-06-23 20:18:18 +02:00
* Represents a timed effects manager for World Info .
2024-06-22 13:56:46 +02:00
* /
2024-06-23 20:18:18 +02:00
class WorldInfoTimedEffects {
2024-06-22 02:15:13 +02:00
/ * *
2024-06-24 01:33:51 +02:00
* Array of chat messages .
2024-06-23 00:53:45 +02:00
* @ type { string [ ] }
2024-06-22 02:15:13 +02:00
* /
2024-06-22 13:56:46 +02:00
# chat = [ ] ;
2024-06-22 02:15:13 +02:00
/ * *
2024-06-24 01:33:51 +02:00
* Array of entries .
2024-06-23 00:53:45 +02:00
* @ type { WIScanEntry [ ] }
2024-06-22 02:15:13 +02:00
* /
2024-06-22 13:56:46 +02:00
# entries = [ ] ;
2024-06-22 02:15:13 +02:00
/ * *
2024-06-24 01:33:51 +02:00
* Buffer for active timed effects .
2024-06-23 20:18:18 +02:00
* @ type { Record < TimedEffectType , WIScanEntry [ ] > }
2024-06-22 02:15:13 +02:00
* /
2024-06-23 19:34:07 +02:00
# buffer = {
'sticky' : [ ] ,
'cooldown' : [ ] ,
2024-06-26 21:43:30 +02:00
'delay' : [ ] ,
2024-06-23 19:34:07 +02:00
} ;
2024-06-22 02:15:13 +02:00
2024-06-24 01:33:51 +02:00
/ * *
* Callbacks for effect types ending .
* @ type { Record < TimedEffectType , ( entry : WIScanEntry ) => void > }
* /
# onEnded = {
/ * *
* Callback for when a sticky entry ends .
* Sets an entry on cooldown immediately if it has a cooldown .
* @ param { WIScanEntry } entry Entry that ended sticky
* /
'sticky' : ( entry ) => {
if ( ! entry . cooldown ) {
return ;
}
const key = this . # getEntryKey ( entry ) ;
2024-06-24 13:51:04 +02:00
const effect = this . # getEntryTimedEffect ( 'cooldown' , entry , true ) ;
2024-06-24 01:33:51 +02:00
chat _metadata . timedWorldInfo . cooldown [ key ] = effect ;
2024-07-06 14:26:45 +02:00
console . log ( ` [WI] Adding cooldown entry ${ key } on ended sticky: start= ${ effect . start } , end= ${ effect . end } , protected= ${ effect . protected } ` ) ;
2024-06-24 01:33:51 +02:00
// Set the cooldown immediately for this evaluation
2024-06-24 13:51:04 +02:00
this . # buffer . cooldown . push ( entry ) ;
2024-06-24 01:33:51 +02:00
} ,
/ * *
* Callback for when a cooldown entry ends .
* No - op , essentially .
* @ param { WIScanEntry } entry Entry that ended cooldown
* /
'cooldown' : ( entry ) => {
2024-07-06 14:26:45 +02:00
console . debug ( '[WI] Cooldown ended for entry' , entry . uid ) ;
2024-06-24 01:33:51 +02:00
} ,
2024-06-26 21:43:30 +02:00
2024-07-05 23:42:49 +02:00
'delay' : ( ) => { } ,
2024-06-24 01:33:51 +02:00
} ;
2024-06-22 02:15:13 +02:00
/ * *
2024-06-23 20:18:18 +02:00
* Initialize the timed effects with the given messages .
2024-06-22 02:15:13 +02:00
* @ param { string [ ] } chat Array of chat messages
* @ param { WIScanEntry [ ] } entries Array of entries
* /
constructor ( chat , entries ) {
this . # chat = chat ;
this . # entries = entries ;
this . # ensureChatMetadata ( ) ;
}
/ * *
* Verify correct structure of chat metadata .
* /
# ensureChatMetadata ( ) {
if ( ! chat _metadata . timedWorldInfo ) {
2024-06-22 02:54:54 +02:00
chat _metadata . timedWorldInfo = { } ;
2024-06-22 02:15:13 +02:00
}
2024-06-22 02:54:54 +02:00
[ 'sticky' , 'cooldown' ] . forEach ( type => {
// Ensure the property exists and is an object
if ( ! chat _metadata . timedWorldInfo [ type ] || typeof chat _metadata . timedWorldInfo [ type ] !== 'object' ) {
chat _metadata . timedWorldInfo [ type ] = { } ;
}
2024-06-22 02:15:13 +02:00
2024-06-22 02:54:54 +02:00
// Clean up invalid entries
Object . entries ( chat _metadata . timedWorldInfo [ type ] ) . forEach ( ( [ key , value ] ) => {
if ( ! value || typeof value !== 'object' ) {
delete chat _metadata . timedWorldInfo [ type ] [ key ] ;
}
} ) ;
} ) ;
2024-06-22 02:15:13 +02:00
}
/ * *
* Gets a hash for a WI entry .
2024-06-22 02:54:54 +02:00
* @ param { WIScanEntry } entry WI entry
2024-06-22 02:15:13 +02:00
* @ returns { number } String hash
* /
# getEntryHash ( entry ) {
2024-09-13 09:47:25 +02:00
return entry . hash ;
2024-06-22 02:15:13 +02:00
}
2024-06-22 02:54:54 +02:00
/ * *
* Gets a unique - ish key for a WI entry .
* @ param { WIScanEntry } entry WI entry
* @ returns { string } String key for the entry
* /
# getEntryKey ( entry ) {
return ` ${ entry . world } . ${ entry . uid } ` ;
}
/ * *
2024-06-23 20:18:18 +02:00
* Gets a timed effect for a WI entry .
* @ param { TimedEffectType } type Type of timed effect
2024-06-24 13:51:04 +02:00
* @ param { WIScanEntry } entry WI entry
* @ param { boolean } isProtected If the effect should be protected
2024-06-23 20:18:18 +02:00
* @ returns { WITimedEffect } Timed effect for the entry
2024-06-22 02:54:54 +02:00
* /
2024-06-24 13:51:04 +02:00
# getEntryTimedEffect ( type , entry , isProtected ) {
2024-06-22 02:54:54 +02:00
return {
hash : this . # getEntryHash ( entry ) ,
start : this . # chat . length ,
end : this . # chat . length + Number ( entry [ type ] ) ,
2024-06-24 13:51:04 +02:00
protected : ! ! isProtected ,
2024-06-22 02:54:54 +02:00
} ;
}
2024-06-22 02:15:13 +02:00
/ * *
2024-06-23 20:18:18 +02:00
* Processes entries for a given type of timed effect .
* @ param { TimedEffectType } type Identifier for the type of timed effect
2024-06-22 02:15:13 +02:00
* @ param { WIScanEntry [ ] } buffer Buffer to store the entries
2024-06-23 20:18:18 +02:00
* @ param { ( entry : WIScanEntry ) => void } onEnded Callback for when a timed effect ends
2024-06-22 02:15:13 +02:00
* /
2024-06-23 20:18:18 +02:00
# checkTimedEffectOfType ( type , buffer , onEnded ) {
/** @type {[string, WITimedEffect][]} */
const effects = Object . entries ( chat _metadata . timedWorldInfo [ type ] ) ;
for ( const [ key , value ] of effects ) {
2024-07-06 14:26:45 +02:00
console . log ( ` [WI] Processing ${ type } entry ${ key } ` , value ) ;
2024-06-22 02:54:54 +02:00
const entry = this . # entries . find ( x => String ( this . # getEntryHash ( x ) ) === String ( value . hash ) ) ;
2024-06-24 13:51:04 +02:00
if ( this . # chat . length <= Number ( value . start ) && ! value . protected ) {
2024-07-06 14:26:45 +02:00
console . log ( ` [WI] Removing ${ type } entry ${ key } from timedWorldInfo: chat not advanced ` , value ) ;
2024-06-22 02:54:54 +02:00
delete chat _metadata . timedWorldInfo [ type ] [ key ] ;
2024-06-22 02:15:13 +02:00
continue ;
}
2024-06-22 02:54:54 +02:00
// Missing entries (they could be from another character's lorebook)
2024-06-22 02:15:13 +02:00
if ( ! entry ) {
2024-06-24 13:51:04 +02:00
if ( this . # chat . length >= Number ( value . end ) ) {
2024-07-06 14:26:45 +02:00
console . log ( ` [WI] Removing ${ type } entry from timedWorldInfo: entry not found and interval passed ` , entry ) ;
2024-06-22 02:54:54 +02:00
delete chat _metadata . timedWorldInfo [ type ] [ key ] ;
}
2024-06-22 02:15:13 +02:00
continue ;
}
// Ignore invalid entries (not configured for timed effects)
if ( ! entry [ type ] ) {
2024-07-06 14:26:45 +02:00
console . log ( ` [WI] Removing ${ type } entry from timedWorldInfo: entry not ${ type } ` , entry ) ;
2024-06-22 02:54:54 +02:00
delete chat _metadata . timedWorldInfo [ type ] [ key ] ;
2024-06-22 02:15:13 +02:00
continue ;
}
2024-06-24 13:51:04 +02:00
if ( this . # chat . length >= Number ( value . end ) ) {
2024-07-06 14:26:45 +02:00
console . log ( ` [WI] Removing ${ type } entry from timedWorldInfo: ${ type } interval passed ` , entry ) ;
2024-06-22 02:54:54 +02:00
delete chat _metadata . timedWorldInfo [ type ] [ key ] ;
2024-06-22 02:15:13 +02:00
if ( typeof onEnded === 'function' ) {
onEnded ( entry ) ;
}
continue ;
}
buffer . push ( entry ) ;
2024-07-06 14:26:45 +02:00
console . log ( ` [WI] Timed effect " ${ type } " applied to entry ` , entry ) ;
2024-06-22 02:15:13 +02:00
}
}
2024-06-26 21:43:30 +02:00
/ * *
* Processes entries for the "delay" timed effect .
* @ param { WIScanEntry [ ] } buffer Buffer to store the entries
* /
# checkDelayEffect ( buffer ) {
for ( const entry of this . # entries ) {
if ( ! entry . delay ) {
continue ;
}
if ( this . # chat . length < entry . delay ) {
buffer . push ( entry ) ;
2024-07-06 14:26:45 +02:00
console . log ( '[WI] Timed effect "delay" applied to entry' , entry ) ;
2024-06-26 21:43:30 +02:00
}
}
}
2024-06-22 02:15:13 +02:00
/ * *
2024-06-22 02:54:54 +02:00
* Checks for timed effects on chat messages .
2024-06-22 02:15:13 +02:00
* /
2024-06-23 20:18:18 +02:00
checkTimedEffects ( ) {
2024-06-24 01:33:51 +02:00
this . # checkTimedEffectOfType ( 'sticky' , this . # buffer . sticky , this . # onEnded . sticky . bind ( this ) ) ;
this . # checkTimedEffectOfType ( 'cooldown' , this . # buffer . cooldown , this . # onEnded . cooldown . bind ( this ) ) ;
2024-06-26 21:43:30 +02:00
this . # checkDelayEffect ( this . # buffer . delay ) ;
2024-06-22 02:15:13 +02:00
}
2024-06-24 00:20:39 +02:00
/ * *
* Gets raw timed effect metadatum for a WI entry .
* @ param { TimedEffectType } type Type of timed effect
* @ param { WIScanEntry } entry WI entry
* @ returns { WITimedEffect } Timed effect for the entry
* /
getEffectMetadata ( type , entry ) {
if ( ! this . isValidEffectType ( type ) ) {
return null ;
}
const key = this . # getEntryKey ( entry ) ;
return chat _metadata . timedWorldInfo [ type ] [ key ] ;
}
2024-06-22 02:15:13 +02:00
/ * *
2024-06-23 20:18:18 +02:00
* Sets a timed effect for a WI entry .
* @ param { TimedEffectType } type Type of timed effect
2024-06-22 02:54:54 +02:00
* @ param { WIScanEntry } entry WI entry to check
2024-06-22 02:15:13 +02:00
* /
2024-06-23 20:18:18 +02:00
# setTimedEffectOfType ( type , entry ) {
2024-06-22 02:54:54 +02:00
// Skip if entry does not have the type (sticky or cooldown)
if ( ! entry [ type ] ) {
return ;
}
2024-06-22 02:15:13 +02:00
2024-06-22 02:54:54 +02:00
const key = this . # getEntryKey ( entry ) ;
if ( ! chat _metadata . timedWorldInfo [ type ] [ key ] ) {
2024-06-24 13:51:04 +02:00
const effect = this . # getEntryTimedEffect ( type , entry , false ) ;
2024-06-23 20:18:18 +02:00
chat _metadata . timedWorldInfo [ type ] [ key ] = effect ;
2024-06-22 02:54:54 +02:00
2024-07-06 14:26:45 +02:00
console . log ( ` [WI] Adding ${ type } entry ${ key } : start= ${ effect . start } , end= ${ effect . end } , protected= ${ effect . protected } ` ) ;
2024-06-22 02:54:54 +02:00
}
2024-06-22 02:15:13 +02:00
}
/ * *
* Sets timed effects on chat messages .
* @ param { WIScanEntry [ ] } activatedEntries Entries that were activated
* /
2024-06-23 20:18:18 +02:00
setTimedEffects ( activatedEntries ) {
2024-06-22 02:15:13 +02:00
for ( const entry of activatedEntries ) {
2024-06-23 20:18:18 +02:00
this . # setTimedEffectOfType ( 'sticky' , entry ) ;
this . # setTimedEffectOfType ( 'cooldown' , entry ) ;
2024-06-22 02:15:13 +02:00
}
}
/ * *
2024-06-23 20:18:18 +02:00
* Force set a timed effect for a WI entry .
* @ param { TimedEffectType } type Type of timed effect
* @ param { WIScanEntry } entry WI entry
* @ param { boolean } newState The state of the effect
* /
setTimedEffect ( type , entry , newState ) {
if ( ! this . isValidEffectType ( type ) ) {
return ;
}
const key = this . # getEntryKey ( entry ) ;
delete chat _metadata . timedWorldInfo [ type ] [ key ] ;
if ( newState ) {
2024-06-24 13:51:04 +02:00
const effect = this . # getEntryTimedEffect ( type , entry , false ) ;
2024-06-23 20:18:18 +02:00
chat _metadata . timedWorldInfo [ type ] [ key ] = effect ;
2024-07-06 14:26:45 +02:00
console . log ( ` [WI] Adding ${ type } entry ${ key } : start= ${ effect . start } , end= ${ effect . end } , protected= ${ effect . protected } ` ) ;
2024-06-23 20:18:18 +02:00
}
}
/ * *
* Check if the string is a valid timed effect type .
* @ param { string } type Name of the timed effect
2024-06-23 19:34:07 +02:00
* @ returns { boolean } Is recognized type
2024-06-22 02:15:13 +02:00
* /
2024-06-23 19:34:07 +02:00
isValidEffectType ( type ) {
2024-06-26 21:43:30 +02:00
return typeof type === 'string' && [ 'sticky' , 'cooldown' , 'delay' ] . includes ( type . trim ( ) . toLowerCase ( ) ) ;
2024-06-22 02:15:13 +02:00
}
/ * *
2024-06-23 19:34:07 +02:00
* Check if the current entry is sticky activated .
2024-06-23 20:18:18 +02:00
* @ param { TimedEffectType } type Type of timed effect
2024-06-23 19:34:07 +02:00
* @ param { WIScanEntry } entry WI entry to check
* @ returns { boolean } True if the entry is active
2024-06-22 02:15:13 +02:00
* /
2024-06-23 19:34:07 +02:00
isEffectActive ( type , entry ) {
if ( ! this . isValidEffectType ( type ) ) {
return false ;
}
return this . # buffer [ type ] ? . some ( x => this . # getEntryHash ( x ) === this . # getEntryHash ( entry ) ) ? ? false ;
2024-06-22 02:15:13 +02:00
}
/ * *
2024-06-23 20:18:18 +02:00
* Clean - up previously set timed effects .
2024-06-22 02:15:13 +02:00
* /
cleanUp ( ) {
2024-06-23 19:34:07 +02:00
for ( const buffer of Object . values ( this . # buffer ) ) {
buffer . splice ( 0 , buffer . length ) ;
}
2024-06-22 02:15:13 +02:00
}
}
2023-08-10 19:45:57 +02:00
export function getWorldInfoSettings ( ) {
return {
world _info ,
world _info _depth ,
2023-11-01 18:02:38 +01:00
world _info _min _activations ,
world _info _min _activations _depth _max ,
2023-08-10 19:45:57 +02:00
world _info _budget ,
2024-07-06 03:23:02 +02:00
world _info _include _names ,
2023-08-10 19:45:57 +02:00
world _info _recursive ,
world _info _overflow _alert ,
world _info _case _sensitive ,
world _info _match _whole _words ,
world _info _character _strategy ,
world _info _budget _cap ,
2024-05-04 22:51:28 +02:00
world _info _use _group _scoring ,
2024-08-23 20:34:18 +02:00
world _info _max _recursion _steps ,
2023-12-02 20:11:06 +01:00
} ;
2023-08-10 19:45:57 +02:00
}
2024-08-02 20:42:53 +02:00
export const world _info _position = {
2023-07-20 19:32:15 +02:00
before : 0 ,
after : 1 ,
ANTop : 2 ,
ANBottom : 3 ,
2023-09-21 09:04:34 +02:00
atDepth : 4 ,
2024-05-08 03:55:26 +02:00
EMTop : 5 ,
EMBottom : 6 ,
2023-07-20 19:32:15 +02:00
} ;
2024-05-08 03:55:26 +02:00
export const wi _anchor _position = {
before : 0 ,
after : 1 ,
2023-07-20 19:32:15 +02:00
} ;
2024-08-02 20:42:53 +02:00
/ * *
* The cache of all world info data that was loaded from the backend .
*
* Calling ` loadWorldInfo ` will fill this cache and utilize this cache , so should be the preferred way to load any world info data .
* Only use the cache directly if you need synchronous access .
*
* This will return a deep clone of the data , so no way to modify the data without actually saving it .
* Should generally be only used for readonly access .
*
* @ type { StructuredCloneMap < string , object > }
* * /
export const worldInfoCache = new StructuredCloneMap ( { cloneOnGet : true , cloneOnSet : false } ) ;
2023-07-20 19:32:15 +02:00
2024-02-24 16:22:51 +01:00
/ * *
* Gets the world info based on chat messages .
2024-07-06 03:23:02 +02:00
* @ param { string [ ] } chat The chat messages to scan , in reverse order .
2024-02-24 16:22:51 +01:00
* @ param { number } maxContext The maximum context size of the generation .
* @ param { boolean } isDryRun If true , the function will not emit any events .
2024-05-08 03:55:26 +02:00
* @ typedef { { worldInfoString : string , worldInfoBefore : string , worldInfoAfter : string , worldInfoExamples : any [ ] , worldInfoDepth : any [ ] } } WIPromptResult
2024-02-24 16:22:51 +01:00
* @ returns { Promise < WIPromptResult > } The world info string and depth .
* /
2024-08-02 20:42:53 +02:00
export async function getWorldInfoPrompt ( chat , maxContext , isDryRun ) {
2023-12-02 19:04:51 +01:00
let worldInfoString = '' , worldInfoBefore = '' , worldInfoAfter = '' ;
2023-07-20 19:32:15 +02:00
2024-06-20 23:53:00 +02:00
const activatedWorldInfo = await checkWorldInfo ( chat , maxContext , isDryRun ) ;
2023-07-20 19:32:15 +02:00
worldInfoBefore = activatedWorldInfo . worldInfoBefore ;
worldInfoAfter = activatedWorldInfo . worldInfoAfter ;
worldInfoString = worldInfoBefore + worldInfoAfter ;
2024-02-24 16:22:51 +01:00
if ( ! isDryRun && activatedWorldInfo . allActivatedEntries && activatedWorldInfo . allActivatedEntries . size > 0 ) {
2024-10-10 14:52:54 +02:00
const arg = Array . from ( activatedWorldInfo . allActivatedEntries . values ( ) ) ;
2024-02-24 16:22:51 +01:00
await eventSource . emit ( event _types . WORLD _INFO _ACTIVATED , arg ) ;
}
2023-09-24 13:41:56 +02:00
return {
worldInfoString ,
worldInfoBefore ,
worldInfoAfter ,
2024-05-08 14:04:17 +02:00
worldInfoExamples : activatedWorldInfo . EMEntries ? ? [ ] ,
worldInfoDepth : activatedWorldInfo . WIDepthEntries ? ? [ ] ,
2023-09-24 13:41:56 +02:00
} ;
2023-07-20 19:32:15 +02:00
}
2024-08-02 20:42:53 +02:00
export function setWorldInfoSettings ( settings , data ) {
2023-07-20 19:32:15 +02:00
if ( settings . world _info _depth !== undefined )
world _info _depth = Number ( settings . world _info _depth ) ;
2023-11-01 18:02:38 +01:00
if ( settings . world _info _min _activations !== undefined )
world _info _min _activations = Number ( settings . world _info _min _activations ) ;
if ( settings . world _info _min _activations _depth _max !== undefined )
world _info _min _activations _depth _max = Number ( settings . world _info _min _activations _depth _max ) ;
2023-07-20 19:32:15 +02:00
if ( settings . world _info _budget !== undefined )
world _info _budget = Number ( settings . world _info _budget ) ;
2024-07-06 03:23:02 +02:00
if ( settings . world _info _include _names !== undefined )
world _info _include _names = Boolean ( settings . world _info _include _names ) ;
2023-07-20 19:32:15 +02:00
if ( settings . world _info _recursive !== undefined )
world _info _recursive = Boolean ( settings . world _info _recursive ) ;
2023-07-30 04:15:54 +02:00
if ( settings . world _info _overflow _alert !== undefined )
world _info _overflow _alert = Boolean ( settings . world _info _overflow _alert ) ;
2023-07-20 19:32:15 +02:00
if ( settings . world _info _case _sensitive !== undefined )
world _info _case _sensitive = Boolean ( settings . world _info _case _sensitive ) ;
if ( settings . world _info _match _whole _words !== undefined )
world _info _match _whole _words = Boolean ( settings . world _info _match _whole _words ) ;
if ( settings . world _info _character _strategy !== undefined )
world _info _character _strategy = Number ( settings . world _info _character _strategy ) ;
2023-08-10 19:45:57 +02:00
if ( settings . world _info _budget _cap !== undefined )
world _info _budget _cap = Number ( settings . world _info _budget _cap ) ;
2024-05-04 22:51:28 +02:00
if ( settings . world _info _use _group _scoring !== undefined )
world _info _use _group _scoring = Boolean ( settings . world _info _use _group _scoring ) ;
2024-08-23 20:34:18 +02:00
if ( settings . world _info _max _recursion _steps !== undefined )
world _info _max _recursion _steps = Number ( settings . world _info _max _recursion _steps ) ;
2023-07-20 19:32:15 +02:00
// Migrate old settings
if ( world _info _budget > 100 ) {
world _info _budget = 25 ;
}
2024-05-04 22:51:28 +02:00
if ( world _info _use _group _scoring === undefined ) {
world _info _use _group _scoring = false ;
}
2023-07-20 19:32:15 +02:00
// Reset selected world from old string and delete old keys
// TODO: Remove next release
const existingWorldInfo = settings . world _info ;
2023-12-02 19:04:51 +01:00
if ( typeof existingWorldInfo === 'string' ) {
2023-07-20 19:32:15 +02:00
delete settings . world _info ;
selected _world _info = [ existingWorldInfo ] ;
} else if ( Array . isArray ( existingWorldInfo ) ) {
delete settings . world _info ;
selected _world _info = existingWorldInfo ;
}
2023-12-02 20:11:06 +01:00
world _info = settings . world _info ? ? { } ;
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
$ ( '#world_info_depth_counter' ) . val ( world _info _depth ) ;
$ ( '#world_info_depth' ) . val ( world _info _depth ) ;
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
$ ( '#world_info_min_activations_counter' ) . val ( world _info _min _activations ) ;
$ ( '#world_info_min_activations' ) . val ( world _info _min _activations ) ;
2023-11-01 18:02:38 +01:00
2023-12-02 19:04:51 +01:00
$ ( '#world_info_min_activations_depth_max_counter' ) . val ( world _info _min _activations _depth _max ) ;
$ ( '#world_info_min_activations_depth_max' ) . val ( world _info _min _activations _depth _max ) ;
2023-11-01 18:02:38 +01:00
2023-12-02 19:04:51 +01:00
$ ( '#world_info_budget_counter' ) . val ( world _info _budget ) ;
$ ( '#world_info_budget' ) . val ( world _info _budget ) ;
2023-07-20 19:32:15 +02:00
2024-07-06 03:23:02 +02:00
$ ( '#world_info_include_names' ) . prop ( 'checked' , world _info _include _names ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info_recursive' ) . prop ( 'checked' , world _info _recursive ) ;
$ ( '#world_info_overflow_alert' ) . prop ( 'checked' , world _info _overflow _alert ) ;
$ ( '#world_info_case_sensitive' ) . prop ( 'checked' , world _info _case _sensitive ) ;
$ ( '#world_info_match_whole_words' ) . prop ( 'checked' , world _info _match _whole _words ) ;
2024-05-04 22:51:28 +02:00
$ ( '#world_info_use_group_scoring' ) . prop ( 'checked' , world _info _use _group _scoring ) ;
2023-07-20 19:32:15 +02:00
$ ( ` #world_info_character_strategy option[value=' ${ world _info _character _strategy } '] ` ) . prop ( 'selected' , true ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info_character_strategy' ) . val ( world _info _character _strategy ) ;
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
$ ( '#world_info_budget_cap' ) . val ( world _info _budget _cap ) ;
$ ( '#world_info_budget_cap_counter' ) . val ( world _info _budget _cap ) ;
2023-08-10 19:45:57 +02:00
2024-08-23 20:34:18 +02:00
$ ( '#world_info_max_recursion_steps' ) . val ( world _info _max _recursion _steps ) ;
$ ( '#world_info_max_recursion_steps_counter' ) . val ( world _info _max _recursion _steps ) ;
2023-07-20 19:32:15 +02:00
world _names = data . world _names ? . length ? data . world _names : [ ] ;
// Add to existing selected WI if it exists
selected _world _info = selected _world _info . concat ( settings . world _info ? . globalSelect ? . filter ( ( e ) => world _names . includes ( e ) ) ? ? [ ] ) ;
if ( world _names . length > 0 ) {
2023-12-02 19:04:51 +01:00
$ ( '#world_info' ) . empty ( ) ;
2023-07-20 19:32:15 +02:00
}
world _names . forEach ( ( item , i ) => {
2023-12-02 19:04:51 +01:00
$ ( '#world_info' ) . append ( ` <option value=' ${ i } ' ${ selected _world _info . includes ( item ) ? ' selected' : '' } > ${ item } </option> ` ) ;
$ ( '#world_editor_select' ) . append ( ` <option value=' ${ i } '> ${ item } </option> ` ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-10-05 22:30:18 +02:00
$ ( '#world_info_sort_order' ) . val ( localStorage . getItem ( SORT _ORDER _KEY ) || '0' ) ;
2024-05-02 20:04:24 +02:00
$ ( '#world_info' ) . trigger ( 'change' ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_editor_select' ) . trigger ( 'change' ) ;
2023-10-16 22:03:42 +02:00
2024-06-23 17:50:40 +02:00
eventSource . on ( event _types . CHAT _CHANGED , async ( ) => {
2023-10-16 22:03:42 +02:00
const hasWorldInfo = ! ! chat _metadata [ METADATA _KEY ] && world _names . includes ( chat _metadata [ METADATA _KEY ] ) ;
$ ( '.chat_lorebook_button' ) . toggleClass ( 'world_set' , hasWorldInfo ) ;
2024-06-23 17:50:40 +02:00
// Pre-cache the world info data for the chat for quicker first prompt generation
await getSortedEntries ( ) ;
2023-10-16 22:03:42 +02:00
} ) ;
2023-12-01 00:50:10 +01:00
2024-04-23 02:09:52 +02:00
eventSource . on ( event _types . WORLDINFO _FORCE _ACTIVATE , ( entries ) => {
2024-10-10 14:52:54 +02:00
for ( const entry of entries ) {
if ( ! Object . hasOwn ( entry , 'world' ) || ! Object . hasOwn ( entry , 'uid' ) ) {
2024-10-11 20:59:55 +02:00
console . error ( '[WI] WORLDINFO_FORCE_ACTIVATE requires all entries to have both world and uid fields, entry IGNORED' , entry ) ;
2024-10-10 14:52:54 +02:00
} else {
WorldInfoBuffer . externalActivations . set ( ` ${ entry . world } . ${ entry . uid } ` , entry ) ;
2024-10-11 21:10:25 +02:00
console . log ( '[WI] WORLDINFO_FORCE_ACTIVATE added entry' , entry ) ;
2024-10-10 14:52:54 +02:00
}
}
2024-04-23 02:09:52 +02:00
} ) ;
2023-12-01 00:50:10 +01:00
// Add slash commands
registerWorldInfoSlashCommands ( ) ;
}
function registerWorldInfoSlashCommands ( ) {
2024-07-22 02:11:11 +02:00
/ * *
* Reloads the editor with the specified world info file
* @ param { string } file - The file to load in the editor
* @ param { boolean } [ loadIfNotSelected = false ] - Indicates whether to load the file even if it ' s not currently selected
* /
function reloadEditor ( file , loadIfNotSelected = false ) {
2024-07-28 04:17:45 +02:00
const currentIndex = Number ( $ ( '#world_editor_select' ) . val ( ) ) ;
2023-12-01 20:51:49 +01:00
const selectedIndex = world _names . indexOf ( file ) ;
2024-07-22 02:11:11 +02:00
if ( selectedIndex !== - 1 && ( loadIfNotSelected || currentIndex === selectedIndex ) ) {
2023-12-01 20:51:49 +01:00
$ ( '#world_editor_select' ) . val ( selectedIndex ) . trigger ( 'change' ) ;
}
}
2024-06-23 20:35:31 +02:00
/ * *
* Gets a * rough * approximation of the current chat context .
* Normally , it is provided externally by the prompt builder .
* Don ' t use for anything critical !
* @ returns { string [ ] }
* /
function getScanningChat ( ) {
return getContext ( ) . chat . filter ( x => ! x . is _system ) . map ( x => x . mes ) ;
}
2023-12-01 00:50:10 +01:00
async function getEntriesFromFile ( file ) {
if ( ! file || ! world _names . includes ( file ) ) {
2023-12-01 01:25:55 +01:00
toastr . warning ( 'Valid World Info file name is required' ) ;
2023-12-01 00:50:10 +01:00
return '' ;
}
2024-08-02 20:42:53 +02:00
const data = await loadWorldInfo ( file ) ;
2023-12-01 00:50:10 +01:00
2023-12-02 19:04:51 +01:00
if ( ! data || ! ( 'entries' in data ) ) {
2023-12-01 01:25:55 +01:00
toastr . warning ( 'World Info file has an invalid format' ) ;
2023-12-01 00:50:10 +01:00
return '' ;
}
const entries = Object . values ( data . entries ) ;
if ( ! entries || entries . length === 0 ) {
2023-12-01 01:25:55 +01:00
toastr . warning ( 'World Info file has no entries' ) ;
2023-12-01 00:50:10 +01:00
return '' ;
}
return entries ;
}
async function getChatBookCallback ( ) {
const chatId = getCurrentChatId ( ) ;
if ( ! chatId ) {
2023-12-01 01:25:55 +01:00
toastr . warning ( 'Open a chat to get a name of the chat-bound lorebook' ) ;
2023-12-01 00:50:10 +01:00
return '' ;
}
if ( chat _metadata [ METADATA _KEY ] && world _names . includes ( chat _metadata [ METADATA _KEY ] ) ) {
return chat _metadata [ METADATA _KEY ] ;
}
// Replace non-alphanumeric characters with underscores, cut to 64 characters
const name = ` Chat Book ${ getCurrentChatId ( ) } ` . replace ( /[^a-z0-9]/gi , '_' ) . replace ( /_{2,}/g , '_' ) . substring ( 0 , 64 ) ;
await createNewWorldInfo ( name ) ;
chat _metadata [ METADATA _KEY ] = name ;
await saveMetadata ( ) ;
$ ( '.chat_lorebook_button' ) . addClass ( 'world_set' ) ;
return name ;
}
async function findBookEntryCallback ( args , value ) {
2024-06-24 00:07:24 +02:00
const file = args . file ;
2023-12-01 00:50:10 +01:00
const field = args . field || 'key' ;
const entries = await getEntriesFromFile ( file ) ;
if ( ! entries ) {
return '' ;
}
2024-08-02 20:42:53 +02:00
if ( typeof newWorldInfoEntryTemplate [ field ] === 'boolean' ) {
2024-05-17 13:32:57 +02:00
const isTrue = isTrueBoolean ( value ) ;
const isFalse = isFalseBoolean ( value ) ;
if ( isTrue ) {
value = String ( true ) ;
}
if ( isFalse ) {
value = String ( false ) ;
}
}
2023-12-01 00:50:10 +01:00
const fuse = new Fuse ( entries , {
keys : [ { name : field , weight : 1 } ] ,
includeScore : true ,
threshold : 0.3 ,
} ) ;
const results = fuse . search ( value ) ;
if ( ! results || results . length === 0 ) {
return '' ;
}
const result = results [ 0 ] ? . item ? . uid ;
if ( result === undefined ) {
return '' ;
}
return result ;
}
async function getEntryFieldCallback ( args , uid ) {
2024-06-24 00:07:24 +02:00
const file = args . file ;
2023-12-01 00:50:10 +01:00
const field = args . field || 'content' ;
const entries = await getEntriesFromFile ( file ) ;
if ( ! entries ) {
return '' ;
}
2023-12-07 17:45:34 +01:00
const entry = entries . find ( x => String ( x . uid ) === String ( uid ) ) ;
2023-12-01 00:50:10 +01:00
if ( ! entry ) {
2023-12-01 20:51:49 +01:00
toastr . warning ( 'Valid UID is required' ) ;
return '' ;
}
2024-08-02 20:42:53 +02:00
if ( newWorldInfoEntryTemplate [ field ] === undefined ) {
2023-12-01 20:51:49 +01:00
toastr . warning ( 'Valid field name is required' ) ;
2023-12-01 00:50:10 +01:00
return '' ;
}
const fieldValue = entry [ field ] ;
if ( fieldValue === undefined ) {
return '' ;
}
if ( Array . isArray ( fieldValue ) ) {
2024-06-23 18:43:56 +02:00
return JSON . stringify ( fieldValue . map ( x => substituteParams ( x ) ) ) ;
2023-12-01 00:50:10 +01:00
}
2023-12-01 20:51:49 +01:00
return substituteParams ( String ( fieldValue ) ) ;
2023-12-01 00:50:10 +01:00
}
async function createEntryCallback ( args , content ) {
2024-06-24 00:07:24 +02:00
const file = args . file ;
2023-12-01 00:50:10 +01:00
const key = args . key ;
2024-08-02 20:42:53 +02:00
const data = await loadWorldInfo ( file ) ;
2023-12-01 00:50:10 +01:00
2023-12-02 19:04:51 +01:00
if ( ! data || ! ( 'entries' in data ) ) {
2023-12-01 01:25:55 +01:00
toastr . warning ( 'Valid World Info file name is required' ) ;
2023-12-01 00:50:10 +01:00
return '' ;
}
2024-04-27 06:18:26 +02:00
const entry = createWorldInfoEntry ( file , data ) ;
2023-12-01 00:50:10 +01:00
if ( key ) {
entry . key . push ( key ) ;
entry . addMemo = true ;
entry . comment = key ;
}
if ( content ) {
entry . content = content ;
}
2024-07-22 03:22:20 +02:00
await saveWorldInfo ( file , data ) ;
2023-12-01 20:51:49 +01:00
reloadEditor ( file ) ;
2023-12-01 00:50:10 +01:00
2024-06-14 23:48:41 +02:00
return String ( entry . uid ) ;
2023-12-01 20:51:49 +01:00
}
async function setEntryFieldCallback ( args , value ) {
2024-06-24 00:07:24 +02:00
const file = args . file ;
const uid = args . uid ;
2023-12-01 20:51:49 +01:00
const field = args . field || 'content' ;
if ( value === undefined ) {
toastr . warning ( 'Value is required' ) ;
return '' ;
2023-12-01 00:50:10 +01:00
}
2024-01-08 18:58:17 +01:00
value = value . replace ( /\\([{}|])/g , '$1' ) ;
2024-08-02 20:42:53 +02:00
const data = await loadWorldInfo ( file ) ;
2023-12-01 20:51:49 +01:00
2023-12-02 19:04:51 +01:00
if ( ! data || ! ( 'entries' in data ) ) {
2023-12-01 20:51:49 +01:00
toastr . warning ( 'Valid World Info file name is required' ) ;
return '' ;
}
const entry = data . entries [ uid ] ;
if ( ! entry ) {
toastr . warning ( 'Valid UID is required' ) ;
return '' ;
}
2024-08-02 20:42:53 +02:00
if ( newWorldInfoEntryTemplate [ field ] === undefined ) {
2023-12-01 20:51:49 +01:00
toastr . warning ( 'Valid field name is required' ) ;
return '' ;
}
if ( Array . isArray ( entry [ field ] ) ) {
2024-06-23 18:43:56 +02:00
entry [ field ] = parseStringArray ( value ) ;
2023-12-01 20:51:49 +01:00
} else if ( typeof entry [ field ] === 'boolean' ) {
entry [ field ] = isTrueBoolean ( value ) ;
} else if ( typeof entry [ field ] === 'number' ) {
entry [ field ] = Number ( value ) ;
} else {
entry [ field ] = value ;
}
2024-08-02 20:42:53 +02:00
if ( originalWIDataKeyMap [ field ] ) {
setWIOriginalDataValue ( data , uid , originalWIDataKeyMap [ field ] , entry [ field ] ) ;
2023-12-01 20:51:49 +01:00
}
2024-07-22 03:22:20 +02:00
await saveWorldInfo ( file , data ) ;
2023-12-01 20:51:49 +01:00
reloadEditor ( file ) ;
return '' ;
2023-12-01 00:50:10 +01:00
}
2024-06-23 20:35:31 +02:00
async function getTimedEffectCallback ( args , value ) {
if ( ! getCurrentChatId ( ) ) {
throw new Error ( 'This command can only be used in chat' ) ;
}
2024-06-24 00:08:24 +02:00
const file = args . file ;
const uid = value ;
2024-06-23 20:35:31 +02:00
const effect = args . effect ;
const entries = await getEntriesFromFile ( file ) ;
if ( ! entries ) {
return '' ;
}
/** @type {WIScanEntry} */
const entry = structuredClone ( entries . find ( x => String ( x . uid ) === String ( uid ) ) ) ;
if ( ! entry ) {
toastr . warning ( 'Valid UID is required' ) ;
return '' ;
}
entry . world = file ; // Required by the timed effects manager
const chat = getScanningChat ( ) ;
const timedEffects = new WorldInfoTimedEffects ( chat , [ entry ] ) ;
if ( ! timedEffects . isValidEffectType ( effect ) ) {
toastr . warning ( 'Valid effect type is required' ) ;
return '' ;
}
2024-06-24 00:20:39 +02:00
const data = timedEffects . getEffectMetadata ( effect , entry ) ;
if ( String ( args . format ) . trim ( ) . toLowerCase ( ) === ARGUMENT _TYPE . NUMBER ) {
return String ( data ? ( data . end - chat . length ) : 0 ) ;
}
return String ( ! ! data ) ;
2024-06-23 20:35:31 +02:00
}
2024-06-23 20:18:18 +02:00
async function setTimedEffectCallback ( args , value ) {
if ( ! getCurrentChatId ( ) ) {
throw new Error ( 'This command can only be used in chat' ) ;
}
2024-06-24 00:08:24 +02:00
const file = args . file ;
const uid = args . uid ;
2024-06-23 20:18:18 +02:00
const effect = args . effect ;
if ( value === undefined ) {
toastr . warning ( 'New state is required' ) ;
return '' ;
}
const entries = await getEntriesFromFile ( file ) ;
if ( ! entries ) {
return '' ;
}
/** @type {WIScanEntry} */
const entry = structuredClone ( entries . find ( x => String ( x . uid ) === String ( uid ) ) ) ;
if ( ! entry ) {
toastr . warning ( 'Valid UID is required' ) ;
return '' ;
}
entry . world = file ; // Required by the timed effects manager
2024-06-23 20:35:31 +02:00
const chat = getScanningChat ( ) ;
const timedEffects = new WorldInfoTimedEffects ( chat , [ entry ] ) ;
2024-06-23 20:18:18 +02:00
if ( ! timedEffects . isValidEffectType ( effect ) ) {
toastr . warning ( 'Valid effect type is required' ) ;
return '' ;
}
if ( ! entry [ effect ] ) {
toastr . warning ( 'This entry does not have the selected effect. Configure it in the editor first.' ) ;
return '' ;
}
const getNewEffectState = ( ) => {
2024-06-24 00:28:37 +02:00
const currentState = ! ! timedEffects . getEffectMetadata ( effect , entry ) ;
2024-06-23 20:18:18 +02:00
if ( [ 'toggle' , 't' , '' ] . includes ( value . trim ( ) . toLowerCase ( ) ) ) {
return ! currentState ;
}
if ( isTrueBoolean ( value ) ) {
return true ;
}
if ( isFalseBoolean ( value ) ) {
return false ;
}
return currentState ;
} ;
const newEffectState = getNewEffectState ( ) ;
timedEffects . setTimedEffect ( effect , entry , newEffectState ) ;
await saveMetadata ( ) ;
toastr . success ( ` Timed effect " ${ effect } " for entry ${ entry . uid } is now ${ newEffectState ? 'active' : 'inactive' } ` ) ;
return '' ;
}
2024-06-17 03:30:52 +02:00
/** A collection of local enum providers for this context of world info */
const localEnumProviders = {
/** All possible fields that can be set in a WI entry */
2024-08-02 20:42:53 +02:00
wiEntryFields : ( ) => Object . entries ( newWorldInfoEntryDefinition ) . map ( ( [ key , value ] ) =>
2024-06-20 20:33:45 +02:00
new SlashCommandEnumValue ( key , ` [ ${ value . type } ] default: ${ ( typeof value . default === 'string' ? ` ' ${ value . default } ' ` : value . default ) } ` ,
2024-06-21 20:04:55 +02:00
enumTypes . enum , enumIcons . getDataTypeIcon ( value . type ) ) ) ,
2024-06-20 20:33:45 +02:00
/** All existing UIDs based on the file argument as world name */
2024-07-12 00:19:34 +02:00
wiUids : ( /** @type {import('./slash-commands/SlashCommandExecutor.js').SlashCommandExecutor} */ executor ) => {
2024-06-20 20:33:45 +02:00
const file = executor . namedArgumentList . find ( it => it . name == 'file' ) ? . value ;
if ( file instanceof SlashCommandClosure ) throw new Error ( 'Argument \'file\' does not support closures' ) ;
// Try find world from cache
2024-06-23 17:50:40 +02:00
if ( ! worldInfoCache . has ( file ) ) return [ ] ;
const world = worldInfoCache . get ( file ) ;
2024-06-20 20:33:45 +02:00
if ( ! world ) return [ ] ;
return Object . entries ( world . entries ) . map ( ( [ uid , data ] ) =>
new SlashCommandEnumValue ( uid , ` ${ data . comment ? ` ${ data . comment } : ` : '' } ${ data . key . join ( ', ' ) } ${ data . keysecondary ? . length ? ` [ ${ Object . entries ( world _info _logic ) . find ( ( [ _ , value ] ) => value == data . selectiveLogic ) [ 0 ] } ] ${ data . keysecondary . join ( ', ' ) } ` : '' } [ ${ getWiPositionString ( data ) } ] ` ,
enumTypes . enum , enumIcons . getWiStatusIcon ( data ) ) ) ;
} ,
2024-06-23 20:18:18 +02:00
timedEffects : ( ) => [
new SlashCommandEnumValue ( 'sticky' , 'Stays active for N messages' , enumTypes . enum , '📌' ) ,
new SlashCommandEnumValue ( 'cooldown' , 'Cooldown for N messages' , enumTypes . enum , '⌛' ) ,
] ,
2024-06-17 03:30:52 +02:00
} ;
2024-06-20 20:33:45 +02:00
function getWiPositionString ( entry ) {
switch ( entry . position ) {
case world _info _position . before : return '↑Char' ;
case world _info _position . after : return '↓Char' ;
case world _info _position . EMTop : return '↑EM' ;
case world _info _position . EMBottom : return '↓EM' ;
case world _info _position . ANTop : return '↑AT' ;
case world _info _position . ANBottom : return '↓AT' ;
case world _info _position . atDepth : return ` @D ${ enumIcons . getRoleIcon ( entry . role ) } ` ;
default : return '<Unknown>' ;
}
}
2024-06-20 23:53:00 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'world' ,
2024-05-12 21:15:05 +02:00
callback : onWorldInfoChange ,
namedArgumentList : [
new SlashCommandNamedArgument (
2024-06-20 20:33:45 +02:00
'state' , 'set world state' , [ ARGUMENT _TYPE . STRING ] , false , false , null , commonEnumProviders . boolean ( 'onOffToggle' ) ( ) ,
2024-05-12 21:15:05 +02:00
) ,
new SlashCommandNamedArgument (
'silent' , 'suppress toast messages' , [ ARGUMENT _TYPE . BOOLEAN ] , false ,
) ,
] ,
unnamedArgumentList : [
2024-06-17 03:30:52 +02:00
SlashCommandArgument . fromProps ( {
description : 'world name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
enumProvider : commonEnumProviders . worlds ,
} ) ,
2024-05-12 21:15:05 +02:00
] ,
helpString : `
< div >
Sets active World , or unsets if no args provided , use < code > state = off < / c o d e > a n d < c o d e > s t a t e = t o g g l e < / c o d e > t o d e a c t i v a t e o r t o g g l e a W o r l d , u s e < c o d e > s i l e n t = t r u e < / c o d e > t o s u p p r e s s t o a s t m e s s a g e s .
< / d i v >
` ,
aliases : [ ] ,
} ) ) ;
2024-06-20 23:53:00 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'getchatbook' ,
2024-05-12 21:15:05 +02:00
callback : getChatBookCallback ,
returns : 'lorebook name' ,
helpString : 'Get a name of the chat-bound lorebook or create a new one if was unbound, and pass it down the pipe.' ,
aliases : [ 'getchatlore' , 'getchatwi' ] ,
} ) ) ;
2024-06-17 03:30:52 +02:00
2024-06-20 23:53:00 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'findentry' ,
2024-05-12 21:15:05 +02:00
aliases : [ 'findlore' , 'findwi' ] ,
returns : 'UID' ,
callback : findBookEntryCallback ,
namedArgumentList : [
2024-06-17 03:30:52 +02:00
SlashCommandNamedArgument . fromProps ( {
name : 'file' ,
description : 'book name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
2024-06-20 20:33:45 +02:00
enumProvider : commonEnumProviders . worlds ,
2024-06-17 03:30:52 +02:00
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'field' ,
description : 'field value for fuzzy match (default: key)' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
defaultValue : 'key' ,
enumList : localEnumProviders . wiEntryFields ( ) ,
} ) ,
2024-05-12 21:15:05 +02:00
] ,
unnamedArgumentList : [
new SlashCommandArgument (
'texts' , ARGUMENT _TYPE . STRING , true , true ,
) ,
] ,
helpString : `
< div >
Find a UID of the record from the specified book using the fuzzy match of a field value ( default : key ) and pass it down the pipe .
< / d i v >
< div >
< strong > Example : < / s t r o n g >
< ul >
< li >
< pre > < code > / f i n d e n t r y f i l e = c h a t L o r e f i e l d = k e y S h a d o w f a n g < / c o d e > < / p r e >
< / l i >
< / u l >
< / d i v >
` ,
} ) ) ;
2024-06-20 23:53:00 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'getentryfield' ,
2024-05-12 21:15:05 +02:00
aliases : [ 'getlorefield' , 'getwifield' ] ,
callback : getEntryFieldCallback ,
returns : 'field value' ,
namedArgumentList : [
2024-06-17 03:30:52 +02:00
SlashCommandNamedArgument . fromProps ( {
name : 'file' ,
description : 'book name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
2024-06-20 20:33:45 +02:00
enumProvider : commonEnumProviders . worlds ,
2024-06-17 03:30:52 +02:00
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'field' ,
description : 'field to retrieve (default: content)' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
defaultValue : 'content' ,
enumList : localEnumProviders . wiEntryFields ( ) ,
} ) ,
2024-05-12 21:15:05 +02:00
] ,
unnamedArgumentList : [
2024-06-20 20:33:45 +02:00
SlashCommandArgument . fromProps ( {
description : 'record UID' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
enumProvider : localEnumProviders . wiUids ,
} ) ,
2024-05-12 21:15:05 +02:00
] ,
helpString : `
< div >
Get a field value ( default : content ) of the record with the UID from the specified book and pass it down the pipe .
< / d i v >
< div >
< strong > Example : < / s t r o n g >
< ul >
< li >
< pre > < code > / g e t e n t r y f i e l d f i l e = c h a t L o r e f i e l d = c o n t e n t 1 2 3 < / c o d e > < / p r e >
< / l i >
< / u l >
< / d i v >
` ,
} ) ) ;
2024-06-20 23:53:00 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'createentry' ,
2024-05-12 21:15:05 +02:00
callback : createEntryCallback ,
aliases : [ 'createlore' , 'createwi' ] ,
returns : 'UID of the new record' ,
namedArgumentList : [
2024-06-17 03:30:52 +02:00
SlashCommandNamedArgument . fromProps ( {
name : 'file' ,
description : 'book name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
2024-06-20 20:33:45 +02:00
enumProvider : commonEnumProviders . worlds ,
2024-06-17 03:30:52 +02:00
} ) ,
2024-05-12 21:15:05 +02:00
new SlashCommandNamedArgument (
'key' , 'record key' , [ ARGUMENT _TYPE . STRING ] , false ,
) ,
] ,
unnamedArgumentList : [
new SlashCommandArgument (
'content' , [ ARGUMENT _TYPE . STRING ] , false ,
) ,
] ,
helpString : `
< div >
Create a new record in the specified book with the key and content ( both are optional ) and pass the UID down the pipe .
< / d i v >
< div >
< strong > Example : < / s t r o n g >
< ul >
< li >
< pre > < code > / c r e a t e e n t r y f i l e = c h a t L o r e k e y = S h a d o w f a n g T h e s w o r d o f t h e k i n g < / c o d e > < / p r e >
< / l i >
< / u l >
< / d i v >
` ,
} ) ) ;
2024-06-20 23:53:00 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'setentryfield' ,
2024-05-12 21:15:05 +02:00
callback : setEntryFieldCallback ,
aliases : [ 'setlorefield' , 'setwifield' ] ,
namedArgumentList : [
2024-06-17 03:30:52 +02:00
SlashCommandNamedArgument . fromProps ( {
name : 'file' ,
description : 'book name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
2024-06-20 20:33:45 +02:00
enumProvider : commonEnumProviders . worlds ,
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'uid' ,
description : 'record UID' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
enumProvider : localEnumProviders . wiUids ,
2024-06-17 03:30:52 +02:00
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'field' ,
description : 'field name (default: content)' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
defaultValue : 'content' ,
enumList : localEnumProviders . wiEntryFields ( ) ,
} ) ,
2024-05-12 21:15:05 +02:00
] ,
unnamedArgumentList : [
new SlashCommandArgument (
'value' , [ ARGUMENT _TYPE . STRING ] , true ,
) ,
] ,
helpString : `
< div >
Set a field value ( default : content ) of the record with the UID from the specified book . To set multiple values for key fields , use comma - delimited list as a value .
< / d i v >
< div >
< strong > Example : < / s t r o n g >
< ul >
< li >
< pre > < code > / s e t e n t r y f i e l d f i l e = c h a t L o r e u i d = 1 2 3 f i e l d = k e y S h a d o w f a n g , s w o r d , w e a p o n < / c o d e > < / p r e >
< / l i >
< / u l >
< / d i v >
` ,
} ) ) ;
2024-06-23 20:18:18 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'wi-set-timed-effect' ,
callback : setTimedEffectCallback ,
namedArgumentList : [
SlashCommandNamedArgument . fromProps ( {
name : 'file' ,
description : 'book name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
enumProvider : commonEnumProviders . worlds ,
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'uid' ,
description : 'record UID' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
enumProvider : localEnumProviders . wiUids ,
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'effect' ,
description : 'effect name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
enumProvider : localEnumProviders . timedEffects ,
} ) ,
] ,
unnamedArgumentList : [
SlashCommandArgument . fromProps ( {
description : 'new state of the effect' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
acceptsMultiple : false ,
enumList : commonEnumProviders . boolean ( 'onOffToggle' ) ( ) ,
} ) ,
] ,
helpString : `
< div >
Set a timed effect for the record with the UID from the specified book . The duration must be set in the entry itself .
Will only be applied for the current chat . Enabling an effect that was already active refreshes the duration .
If the last chat message is swiped or deleted , the effect will be removed .
< / d i v >
< div >
< strong > Example : < / s t r o n g >
< ul >
< li >
< pre > < code > / w i - s e t - t i m e d - e f f e c t f i l e = c h a t L o r e u i d = 1 2 3 e f f e c t = s t i c k y o n < / c o d e > < / p r e >
< / l i >
< / u l >
< / d i v >
` ,
} ) ) ;
2024-06-23 20:35:31 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'wi-get-timed-effect' ,
callback : getTimedEffectCallback ,
2024-06-24 00:20:39 +02:00
helpString : `
< div >
Get the current state of the timed effect for the record with the UID from the specified book .
< / d i v >
< div >
< strong > Example : < / s t r o n g >
< ul >
< li >
< code > / w i - g e t - t i m e d - e f f e c t f i l e = c h a t L o r e f o r m a t = b o o l e f f e c t = s t i c k y 1 2 3 < / c o d e > - r e t u r n s t r u e o r f a l s e i f t h e e f f e c t i s a c t i v e o r n o t
< / l i >
< li >
< code > / w i - g e t - t i m e d - e f f e c t f i l e = c h a t L o r e f o r m a t = n u m b e r e f f e c t = s t i c k y 1 2 3 < / c o d e > - r e t u r n s t h e r e m a i n i n g d u r a t i o n o f t h e e f f e c t , o r 0 i f i n a c t i v e
< / l i >
< / u l >
< / d i v >
` ,
returns : 'state of the effect' ,
2024-06-23 20:35:31 +02:00
namedArgumentList : [
SlashCommandNamedArgument . fromProps ( {
name : 'file' ,
description : 'book name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
enumProvider : commonEnumProviders . worlds ,
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'effect' ,
description : 'effect name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
enumProvider : localEnumProviders . timedEffects ,
} ) ,
2024-06-24 00:20:39 +02:00
SlashCommandNamedArgument . fromProps ( {
name : 'format' ,
description : 'output format' ,
isRequired : false ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
defaultValue : ARGUMENT _TYPE . BOOLEAN ,
enumList : [ ARGUMENT _TYPE . BOOLEAN , ARGUMENT _TYPE . NUMBER ] ,
} ) ,
2024-06-23 20:35:31 +02:00
] ,
unnamedArgumentList : [
SlashCommandArgument . fromProps ( {
description : 'record UID' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
enumProvider : localEnumProviders . wiUids ,
} ) ,
] ,
} ) ) ;
2023-07-20 19:32:15 +02:00
}
2024-08-02 20:42:53 +02:00
/ * *
* Loads the given world into the World Editor .
*
* @ param { string } name - The name of the world
* @ return { Promise < void > } A promise that resolves when the world editor is loaded
* /
export async function showWorldEditor ( name ) {
2023-07-20 19:32:15 +02:00
if ( ! name ) {
hideWorldEditor ( ) ;
return ;
}
2024-08-02 20:42:53 +02:00
const wiData = await loadWorldInfo ( name ) ;
2023-07-20 19:32:15 +02:00
displayWorldEntries ( name , wiData ) ;
}
2024-08-02 20:42:53 +02:00
/ * *
* Loads world info from the backend .
*
* This function will return from ` worldInfoCache ` if it has already been loaded before .
*
* @ param { string } name - The name of the world to load
* @ return { Promise < Object | null > } A promise that resolves to the loaded world information , or null if the request fails .
* /
export async function loadWorldInfo ( name ) {
2023-07-20 19:32:15 +02:00
if ( ! name ) {
return ;
}
2024-06-20 23:53:00 +02:00
if ( worldInfoCache . has ( name ) ) {
return worldInfoCache . get ( name ) ;
2023-07-20 19:32:15 +02:00
}
2023-12-06 23:09:48 +01:00
const response = await fetch ( '/api/worldinfo/get' , {
2023-12-02 19:04:51 +01:00
method : 'POST' ,
2023-07-20 19:32:15 +02:00
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { name : name } ) ,
cache : 'no-cache' ,
} ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
2024-06-20 23:53:00 +02:00
worldInfoCache . set ( name , data ) ;
2023-07-20 19:32:15 +02:00
return data ;
}
return null ;
}
async function updateWorldInfoList ( ) {
2023-12-14 22:47:03 +01:00
const result = await fetch ( '/api/settings/get' , {
2023-12-02 19:04:51 +01:00
method : 'POST' ,
2023-07-20 19:32:15 +02:00
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { } ) ,
} ) ;
if ( result . ok ) {
var data = await result . json ( ) ;
world _names = data . world _names ? . length ? data . world _names : [ ] ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info' ) . find ( 'option[value!=""]' ) . remove ( ) ;
$ ( '#world_editor_select' ) . find ( 'option[value!=""]' ) . remove ( ) ;
2023-07-20 19:32:15 +02:00
world _names . forEach ( ( item , i ) => {
2023-12-02 19:04:51 +01:00
$ ( '#world_info' ) . append ( ` <option value=' ${ i } ' ${ selected _world _info . includes ( item ) ? ' selected' : '' } > ${ item } </option> ` ) ;
$ ( '#world_editor_select' ) . append ( ` <option value=' ${ i } '> ${ item } </option> ` ) ;
2023-07-20 19:32:15 +02:00
} ) ;
}
}
function hideWorldEditor ( ) {
displayWorldEntries ( null , null ) ;
}
function getWIElement ( name ) {
2023-12-02 19:04:51 +01:00
const wiElement = $ ( '#world_info' ) . children ( ) . filter ( function ( ) {
2023-12-02 20:11:06 +01:00
return $ ( this ) . text ( ) . toLowerCase ( ) === name . toLowerCase ( ) ;
2023-07-20 19:32:15 +02:00
} ) ;
return wiElement ;
}
2023-10-05 22:30:18 +02:00
/ * *
2024-08-02 20:42:53 +02:00
* Sorts the given data based on the selected sort option
*
2023-10-05 22:30:18 +02:00
* @ param { any [ ] } data WI entries
2024-08-02 20:57:55 +02:00
* @ param { object } [ options = { } ] - Optional arguments
* @ param { { sortField ? : string , sortOrder ? : string , sortRule ? : string } } [ options . customSort = { } ] - Custom sort options , instead of the chosen UI sort
2023-10-05 22:30:18 +02:00
* @ returns { any [ ] } Sorted data
* /
2024-08-02 20:57:55 +02:00
export function sortWorldInfoEntries ( data , { customSort = null } = { } ) {
2023-12-02 19:04:51 +01:00
const option = $ ( '#world_info_sort_order' ) . find ( ':selected' ) ;
2024-08-02 22:44:21 +02:00
const sortField = customSort ? . sortField ? ? option . data ( 'field' ) ;
const sortOrder = customSort ? . sortOrder ? ? option . data ( 'order' ) ;
const sortRule = customSort ? . sortRule ? ? option . data ( 'rule' ) ;
2023-10-05 22:30:18 +02:00
const orderSign = sortOrder === 'asc' ? 1 : - 1 ;
2024-04-30 01:39:47 +02:00
if ( ! data . length ) return data ;
2024-07-27 19:56:14 +02:00
/** @type {(a: any, b: any) => number} */
let primarySort ;
// Secondary and tertiary it will always be sorted by Order descending, and last UID ascending
// This is the most sensible approach for sorts where the primary sort has a lot of equal values
const secondarySort = ( a , b ) => b . order - a . order ;
const tertiarySort = ( a , b ) => a . uid - b . uid ;
2024-04-30 01:39:47 +02:00
// If we have a search term for WI, we are sorting by weighting scores
2024-04-30 02:27:44 +02:00
if ( sortRule === 'search' ) {
2024-07-27 19:56:14 +02:00
primarySort = ( a , b ) => {
2024-04-30 01:39:47 +02:00
const aScore = worldInfoFilter . getScore ( FILTER _TYPES . WORLD _INFO _SEARCH , a . uid ) ;
const bScore = worldInfoFilter . getScore ( FILTER _TYPES . WORLD _INFO _SEARCH , b . uid ) ;
2024-07-27 19:56:14 +02:00
return aScore - bScore ;
} ;
2024-04-30 01:39:47 +02:00
}
else if ( sortRule === 'custom' ) {
2024-07-27 19:56:14 +02:00
// First by display index
primarySort = ( a , b ) => {
2023-11-11 19:16:57 +01:00
const aValue = a . displayIndex ;
const bValue = b . displayIndex ;
2024-07-27 19:56:14 +02:00
return aValue - bValue ;
} ;
2023-11-11 19:16:57 +01:00
} else if ( sortRule === 'priority' ) {
2024-07-27 19:56:14 +02:00
// First constant, then normal, then disabled.
primarySort = ( a , b ) => {
2024-10-10 16:19:16 +02:00
const aValue = a . disable ? 2 : a . constant ? 0 : 1 ;
const bValue = b . disable ? 2 : b . constant ? 0 : 1 ;
2024-07-27 19:56:14 +02:00
return aValue - bValue ;
} ;
2023-10-05 22:30:18 +02:00
} else {
2024-07-27 19:56:14 +02:00
primarySort = ( a , b ) => {
2023-10-05 22:30:18 +02:00
const aValue = a [ sortField ] ;
const bValue = b [ sortField ] ;
// Sort strings
if ( typeof aValue === 'string' && typeof bValue === 'string' ) {
if ( sortRule === 'length' ) {
// Sort by string length
return orderSign * ( aValue . length - bValue . length ) ;
} else {
// Sort by A-Z ordinal
return orderSign * aValue . localeCompare ( bValue ) ;
}
}
// Sort numbers
return orderSign * ( Number ( aValue ) - Number ( bValue ) ) ;
2023-10-06 00:18:50 +02:00
} ;
2023-10-05 22:30:18 +02:00
}
2024-07-27 19:56:14 +02:00
data . sort ( ( a , b ) => {
return primarySort ( a , b ) || secondarySort ( a , b ) || tertiarySort ( a , b ) ;
} ) ;
2023-10-05 22:30:18 +02:00
return data ;
}
2023-07-20 19:32:15 +02:00
function nullWorldInfo ( ) {
2023-12-02 19:04:51 +01:00
toastr . info ( 'Create or import a new World Info file first.' , 'World Info is not set' , { timeOut : 10000 , preventDuplicates : true } ) ;
2023-07-20 19:32:15 +02:00
}
2024-05-08 20:34:53 +02:00
/** @type {Select2Option[]} Cache all keys as selectable dropdown option */
const worldEntryKeyOptionsCache = [ ] ;
/ * *
* Update the cache and all select options for the keys with new values to display
* @ param { string [ ] | Select2Option [ ] } keyOptions - An array of options to update
* @ param { object } options - Optional arguments
* @ param { boolean ? } [ options . remove = false ] - Whether the option was removed , so the count should be reduced - otherwise it ' ll be increased
* @ param { boolean ? } [ options . reset = false ] - Whether the cache should be reset . Reset will also not trigger update of the controls , as we expect them to be redrawn anyway
* /
function updateWorldEntryKeyOptionsCache ( keyOptions , { remove = false , reset = false } = { } ) {
if ( ! keyOptions . length ) return ;
/** @type {Select2Option[]} */
const options = keyOptions . map ( x => typeof x === 'string' ? { id : getSelect2OptionId ( x ) , text : x } : x ) ;
if ( reset ) worldEntryKeyOptionsCache . length = 0 ;
options . forEach ( option => {
// Update the cache list
let cachedEntry = worldEntryKeyOptionsCache . find ( x => x . id == option . id ) ;
if ( cachedEntry ) {
cachedEntry . count += ! remove ? 1 : - 1 ;
} else if ( ! remove ) {
worldEntryKeyOptionsCache . push ( option ) ;
cachedEntry = option ;
cachedEntry . count = 1 ;
}
} ) ;
// Sort by count DESC and then alphabetically
worldEntryKeyOptionsCache . sort ( ( a , b ) => b . count - a . count || a . text . localeCompare ( b . text ) ) ;
}
2024-05-14 04:51:22 +02:00
function displayWorldEntries ( name , data , navigation = navigation _option . none , flashOnNav = true ) {
updateEditor = ( navigation , flashOnNav = true ) => displayWorldEntries ( name , data , navigation , flashOnNav ) ;
2023-08-21 20:10:11 +02:00
2024-05-01 02:08:52 +02:00
const worldEntriesList = $ ( '#world_popup_entries_list' ) ;
// We save costly performance by removing all events before emptying. Because we know there are no relevant event handlers reacting on removing elements
// This prevents jQuery from actually going through all registered events on the controls for each entry when removing it
worldEntriesList . find ( '*' ) . off ( ) ;
worldEntriesList . empty ( ) . show ( ) ;
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
if ( ! data || ! ( 'entries' in data ) ) {
$ ( '#world_popup_new' ) . off ( 'click' ) . on ( 'click' , nullWorldInfo ) ;
$ ( '#world_popup_name_button' ) . off ( 'click' ) . on ( 'click' , nullWorldInfo ) ;
$ ( '#world_popup_export' ) . off ( 'click' ) . on ( 'click' , nullWorldInfo ) ;
$ ( '#world_popup_delete' ) . off ( 'click' ) . on ( 'click' , nullWorldInfo ) ;
2024-01-09 15:24:26 +01:00
$ ( '#world_duplicate' ) . off ( 'click' ) . on ( 'click' , nullWorldInfo ) ;
2024-05-01 02:08:52 +02:00
worldEntriesList . hide ( ) ;
2023-08-22 00:51:31 +02:00
$ ( '#world_info_pagination' ) . html ( '' ) ;
2023-07-20 19:32:15 +02:00
return ;
}
2024-05-22 13:23:51 +02:00
// Regardless of whether success is displayed or not. Make sure the delete button is available.
// Do not put this code behind.
$ ( '#world_popup_delete' ) . off ( 'click' ) . on ( 'click' , async ( ) => {
2024-06-26 21:43:30 +02:00
const confirmation = await Popup . show . confirm ( ` Delete the World/Lorebook: " ${ name } "? ` , 'This action is irreversible!' ) ;
2024-05-22 13:23:51 +02:00
if ( ! confirmation ) {
return ;
}
if ( world _info . charLore ) {
world _info . charLore . forEach ( ( charLore , index ) => {
if ( charLore . extraBooks ? . includes ( name ) ) {
const tempCharLore = charLore . extraBooks . filter ( ( e ) => e !== name ) ;
if ( tempCharLore . length === 0 ) {
world _info . charLore . splice ( index , 1 ) ;
} else {
charLore . extraBooks = tempCharLore ;
}
}
} ) ;
saveSettingsDebounced ( ) ;
}
// Selected world_info automatically refreshes
await deleteWorldInfo ( name ) ;
} ) ;
2024-04-30 01:39:47 +02:00
// Before printing the WI, we check if we should enable/disable search sorting
2024-04-30 02:27:44 +02:00
verifyWorldInfoSearchSortRule ( ) ;
2024-04-30 01:39:47 +02:00
2023-08-21 20:10:11 +02:00
function getDataArray ( callback ) {
// Convert the data.entries object into an array
2023-08-22 00:51:31 +02:00
let entriesArray = Object . keys ( data . entries ) . map ( uid => {
2023-08-21 20:10:11 +02:00
const entry = data . entries [ uid ] ;
entry . displayIndex = entry . displayIndex ? ? entry . uid ;
return entry ;
} ) ;
2024-04-30 01:39:47 +02:00
// Apply the filter and do the chosen sorting
2023-08-22 00:51:31 +02:00
entriesArray = worldInfoFilter . applyFilters ( entriesArray ) ;
2024-08-02 20:42:53 +02:00
entriesArray = sortWorldInfoEntries ( entriesArray ) ;
2024-04-30 01:39:47 +02:00
2024-05-08 20:34:53 +02:00
// Cache keys
const keys = entriesArray . flatMap ( entry => [ ... entry . key , ... entry . keysecondary ] ) ;
updateWorldEntryKeyOptionsCache ( keys , { reset : true } ) ;
2024-04-30 01:39:47 +02:00
// Run the callback for printing this
2023-10-05 22:30:18 +02:00
typeof callback === 'function' && callback ( entriesArray ) ;
2023-08-22 00:51:31 +02:00
return entriesArray ;
2023-08-21 20:10:11 +02:00
}
2024-06-27 12:04:49 +02:00
const storageKey = 'WI_PerPage' ;
const perPageDefault = 25 ;
2023-08-21 20:10:11 +02:00
let startPage = 1 ;
if ( navigation === navigation _option . previous ) {
2023-12-02 19:04:51 +01:00
startPage = $ ( '#world_info_pagination' ) . pagination ( 'getCurrentPageNum' ) ;
2023-08-21 20:10:11 +02:00
}
2023-07-20 19:32:15 +02:00
2024-06-27 12:04:49 +02:00
if ( typeof navigation === 'number' && Number ( navigation ) >= 0 ) {
const data = getDataArray ( ) ;
const uidIndex = data . findIndex ( x => x . uid === navigation ) ;
const perPage = Number ( localStorage . getItem ( storageKey ) ) || perPageDefault ;
startPage = Math . floor ( uidIndex / perPage ) + 1 ;
}
2023-12-02 19:04:51 +01:00
$ ( '#world_info_pagination' ) . pagination ( {
2023-08-21 20:10:11 +02:00
dataSource : getDataArray ,
2023-10-05 22:30:18 +02:00
pageSize : Number ( localStorage . getItem ( storageKey ) ) || perPageDefault ,
2024-05-13 11:51:42 +02:00
sizeChangerOptions : [ 10 , 25 , 50 , 100 , 500 , 1000 ] ,
2023-09-18 00:52:41 +02:00
showSizeChanger : true ,
2023-08-21 20:10:11 +02:00
pageRange : 1 ,
pageNumber : startPage ,
position : 'top' ,
showPageNumbers : false ,
prevText : '<' ,
nextText : '>' ,
formatNavigator : PAGINATION _TEMPLATE ,
showNavigator : true ,
2024-07-22 04:23:05 +02:00
callback : async function ( /** @type {object[]} */ page ) {
2024-05-01 02:08:52 +02:00
// We save costly performance by removing all events before emptying. Because we know there are no relevant event handlers reacting on removing elements
// This prevents jQuery from actually going through all registered events on the controls for each entry when removing it
worldEntriesList . find ( '*' ) . off ( ) ;
worldEntriesList . empty ( ) ;
2024-08-06 11:30:17 +02:00
const keywordHeaders = await renderTemplateAsync ( 'worldInfoKeywordHeaders' ) ;
2024-08-05 02:05:27 +02:00
const blocksPromises = page . map ( async ( entry ) => await getWorldEntry ( name , data , entry ) ) . filter ( x => x ) ;
const blocks = await Promise . all ( blocksPromises ) ;
2023-11-11 19:16:57 +01:00
const isCustomOrder = $ ( '#world_info_sort_order' ) . find ( ':selected' ) . data ( 'rule' ) === 'custom' ;
if ( ! isCustomOrder ) {
blocks . forEach ( block => {
block . find ( '.drag-handle' ) . remove ( ) ;
} ) ;
}
2024-05-01 02:08:52 +02:00
worldEntriesList . append ( keywordHeaders ) ;
worldEntriesList . append ( blocks ) ;
2023-08-21 20:10:11 +02:00
} ,
afterSizeSelectorChange : function ( e ) {
localStorage . setItem ( storageKey , e . target . value ) ;
2023-12-02 21:06:57 +01:00
} ,
2024-04-05 12:45:28 +02:00
afterPaging : function ( ) {
$ ( '#world_popup_entries_list textarea[name="comment"]' ) . each ( function ( ) {
initScrollHeight ( $ ( this ) ) ;
} ) ;
} ,
2023-08-21 20:10:11 +02:00
} ) ;
2023-07-20 19:32:15 +02:00
2023-10-05 22:30:18 +02:00
if ( typeof navigation === 'number' && Number ( navigation ) >= 0 ) {
const selector = ` #world_popup_entries_list [uid=" ${ navigation } "] ` ;
waitUntilCondition ( ( ) => document . querySelector ( selector ) !== null ) . finally ( ( ) => {
const element = $ ( selector ) ;
if ( element . length === 0 ) {
console . log ( ` Could not find element for uid ${ navigation } ` ) ;
return ;
}
const elementOffset = element . offset ( ) ;
const parentOffset = element . parent ( ) . offset ( ) ;
const scrollOffset = elementOffset . top - parentOffset . top ;
$ ( '#WorldInfo' ) . scrollTop ( scrollOffset ) ;
2024-05-14 04:51:22 +02:00
if ( flashOnNav ) flashHighlight ( element ) ;
2023-10-05 22:30:18 +02:00
} ) ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 19:04:51 +01:00
$ ( '#world_popup_new' ) . off ( 'click' ) . on ( 'click' , ( ) => {
2024-04-27 06:18:26 +02:00
const entry = createWorldInfoEntry ( name , data ) ;
if ( entry ) updateEditor ( entry . uid ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_popup_name_button' ) . off ( 'click' ) . on ( 'click' , async ( ) => {
2023-07-20 19:32:15 +02:00
await renameWorldInfo ( name , data ) ;
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_backfill_memos' ) . off ( 'click' ) . on ( 'click' , async ( ) => {
2023-10-05 22:56:31 +02:00
let counter = 0 ;
for ( const entry of Object . values ( data . entries ) ) {
if ( ! entry . comment && Array . isArray ( entry . key ) && entry . key . length > 0 ) {
entry . comment = entry . key [ 0 ] ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , entry . uid , 'comment' , entry . comment ) ;
2023-10-05 22:56:31 +02:00
counter ++ ;
}
}
if ( counter > 0 ) {
toastr . info ( ` Backfilled ${ counter } titles ` ) ;
2024-07-22 03:22:20 +02:00
await saveWorldInfo ( name , data ) ;
2023-10-05 22:56:31 +02:00
updateEditor ( navigation _option . previous ) ;
}
} ) ;
2024-07-27 19:27:55 +02:00
$ ( '#world_apply_current_sorting' ) . off ( 'click' ) . on ( 'click' , async ( ) => {
2024-07-22 01:30:08 +02:00
const entryCount = Object . keys ( data . entries ) . length ;
const moreThan100 = entryCount > 100 ;
2024-07-27 19:27:55 +02:00
let content = '<span>Apply your current sorting to the "Order" field. The Order values will go down from the chosen number.</span>' ;
2024-07-22 01:30:08 +02:00
if ( moreThan100 ) {
2024-07-22 01:42:54 +02:00
content += ` <div class="m-t-1"><i class="fa-solid fa-triangle-exclamation" style="color: #FFD43B;"></i> More than 100 entries in this world. If you don't choose a number higher than that, the lower entries will default to 0.<br />(Usual default: 100)<br />Minimum: ${ entryCount } </div> ` ;
2024-07-22 01:30:08 +02:00
}
2024-07-27 19:27:55 +02:00
const result = await Popup . show . input ( 'Apply Current Sorting' , content , '100' , { okButton : 'Apply' , cancelButton : 'Cancel' } ) ;
2024-07-22 01:30:08 +02:00
if ( ! result ) return ;
const start = Number ( result ) ;
if ( isNaN ( start ) || start < 0 ) {
2024-07-27 19:27:55 +02:00
toastr . error ( 'Invalid number: ' + result , 'Apply Current Sorting' ) ;
2024-07-22 01:30:08 +02:00
return ;
}
if ( start < entryCount ) {
2024-07-27 19:27:55 +02:00
toastr . warning ( 'A number lower than the entry count has been chosen. All entries below that will default to 0.' , 'Apply Current Sorting' ) ;
2024-07-22 01:30:08 +02:00
}
2024-07-27 19:27:55 +02:00
// We need to sort the entries here, as the data source isn't sorted
const entries = Object . values ( data . entries ) ;
2024-08-02 20:42:53 +02:00
sortWorldInfoEntries ( entries ) ;
2024-07-27 19:27:55 +02:00
let updated = 0 , current = start ;
for ( const entry of entries ) {
const newOrder = Math . max ( current -- , 0 ) ;
2024-07-22 01:30:08 +02:00
if ( entry . order === newOrder ) continue ;
entry . order = newOrder ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , entry . order , 'order' , entry . order ) ;
2024-07-27 19:27:55 +02:00
updated ++ ;
2024-07-22 01:30:08 +02:00
}
2024-07-27 19:27:55 +02:00
if ( updated > 0 ) {
toastr . info ( ` Updated ${ updated } Order values ` , 'Apply Custom Sorting' ) ;
2024-07-22 01:30:08 +02:00
await saveWorldInfo ( name , data , true ) ;
updateEditor ( navigation _option . previous ) ;
} else {
toastr . info ( 'All values up to date' , 'Apply Custom Sorting' ) ;
}
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_popup_export' ) . off ( 'click' ) . on ( 'click' , ( ) => {
2023-07-20 19:32:15 +02:00
if ( name && data ) {
const jsonValue = JSON . stringify ( data ) ;
const fileName = ` ${ name } .json ` ;
2023-12-02 19:04:51 +01:00
download ( jsonValue , fileName , 'application/json' ) ;
2023-07-20 19:32:15 +02:00
}
} ) ;
2024-01-09 15:24:26 +01:00
$ ( '#world_duplicate' ) . off ( 'click' ) . on ( 'click' , async ( ) => {
const tempName = getFreeWorldName ( ) ;
2024-06-26 05:29:08 +02:00
const finalName = await Popup . show . input ( 'Create a new World Info?' , 'Enter a name for the new file:' , tempName ) ;
2024-01-09 15:24:26 +01:00
if ( finalName ) {
await saveWorldInfo ( finalName , data , true ) ;
await updateWorldInfoList ( ) ;
const selectedIndex = world _names . indexOf ( finalName ) ;
if ( selectedIndex !== - 1 ) {
$ ( '#world_editor_select' ) . val ( selectedIndex ) . trigger ( 'change' ) ;
} else {
hideWorldEditor ( ) ;
}
}
} ) ;
2023-07-20 19:32:15 +02:00
// Check if a sortable instance exists
2024-05-01 02:08:52 +02:00
if ( worldEntriesList . sortable ( 'instance' ) !== undefined ) {
2023-07-20 19:32:15 +02:00
// Destroy the instance
2024-05-01 02:08:52 +02:00
worldEntriesList . sortable ( 'destroy' ) ;
2023-07-20 19:32:15 +02:00
}
2024-05-01 02:08:52 +02:00
worldEntriesList . sortable ( {
2024-05-18 12:27:22 +02:00
items : '.world_entry' ,
2023-08-18 12:41:46 +02:00
delay : getSortableDelay ( ) ,
2023-12-02 19:04:51 +01:00
handle : '.drag-handle' ,
2024-05-07 02:01:54 +02:00
stop : async function ( _event , _ui ) {
2023-11-11 19:16:57 +01:00
const firstEntryUid = $ ( '#world_popup_entries_list .world_entry' ) . first ( ) . data ( 'uid' ) ;
const minDisplayIndex = data ? . entries [ firstEntryUid ] ? . displayIndex ? ? 0 ;
2023-07-20 19:32:15 +02:00
$ ( '#world_popup_entries_list .world_entry' ) . each ( function ( index ) {
const uid = $ ( this ) . data ( 'uid' ) ;
// Update the display index in the data array
const item = data . entries [ uid ] ;
if ( ! item ) {
console . debug ( ` Could not find entry with uid ${ uid } ` ) ;
return ;
}
2023-11-11 19:16:57 +01:00
item . displayIndex = minDisplayIndex + index ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.display_index' , item . displayIndex ) ;
2023-07-20 19:32:15 +02:00
} ) ;
console . table ( Object . keys ( data . entries ) . map ( uid => data . entries [ uid ] ) . map ( x => ( { uid : x . uid , key : x . key . join ( ',' ) , displayIndex : x . displayIndex } ) ) ) ;
2024-07-22 03:22:20 +02:00
await saveWorldInfo ( name , data ) ;
2023-12-02 21:06:57 +01:00
} ,
2023-07-20 19:32:15 +02:00
} ) ;
//$("#world_popup_entries_list").disableSelection();
}
2024-08-02 20:42:53 +02:00
export const originalWIDataKeyMap = {
2023-12-01 20:51:49 +01:00
'displayIndex' : 'extensions.display_index' ,
'excludeRecursion' : 'extensions.exclude_recursion' ,
2024-01-07 11:51:13 +01:00
'preventRecursion' : 'extensions.prevent_recursion' ,
2024-05-13 22:33:25 +02:00
'delayUntilRecursion' : 'extensions.delay_until_recursion' ,
2023-12-01 20:51:49 +01:00
'selectiveLogic' : 'selectiveLogic' ,
'comment' : 'comment' ,
'constant' : 'constant' ,
'order' : 'insertion_order' ,
'depth' : 'extensions.depth' ,
'probability' : 'extensions.probability' ,
'position' : 'extensions.position' ,
2024-03-23 16:36:43 +01:00
'role' : 'extensions.role' ,
2023-12-01 20:51:49 +01:00
'content' : 'content' ,
'enabled' : 'enabled' ,
'key' : 'keys' ,
'keysecondary' : 'secondary_keys' ,
'selective' : 'selective' ,
2024-01-23 21:44:20 +01:00
'matchWholeWords' : 'extensions.match_whole_words' ,
2024-05-04 23:42:33 +02:00
'useGroupScoring' : 'extensions.use_group_scoring' ,
2024-01-23 21:44:20 +01:00
'caseSensitive' : 'extensions.case_sensitive' ,
'scanDepth' : 'extensions.scan_depth' ,
2024-02-24 16:22:51 +01:00
'automationId' : 'extensions.automation_id' ,
2024-04-23 02:09:52 +02:00
'vectorized' : 'extensions.vectorized' ,
2024-04-27 20:03:55 +02:00
'groupOverride' : 'extensions.group_override' ,
2024-05-06 16:00:42 +02:00
'groupWeight' : 'extensions.group_weight' ,
2024-06-20 23:53:00 +02:00
'sticky' : 'extensions.sticky' ,
'cooldown' : 'extensions.cooldown' ,
2024-06-26 21:43:30 +02:00
'delay' : 'extensions.delay' ,
2023-12-02 20:11:06 +01:00
} ;
2023-12-01 20:51:49 +01:00
2024-04-30 01:39:47 +02:00
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
2024-04-30 02:27:44 +02:00
function verifyWorldInfoSearchSortRule ( ) {
2024-04-30 01:39:47 +02:00
const searchTerm = worldInfoFilter . getFilterData ( FILTER _TYPES . WORLD _INFO _SEARCH ) ;
const searchOption = $ ( '#world_info_sort_order option[data-rule="search"]' ) ;
const selector = $ ( '#world_info_sort_order' ) ;
const isHidden = searchOption . attr ( 'hidden' ) !== undefined ;
2024-04-30 02:27:44 +02:00
// If we have a search term, we are displaying the sorting option for it
2024-04-30 01:39:47 +02:00
if ( searchTerm && isHidden ) {
searchOption . removeAttr ( 'hidden' ) ;
selector . val ( searchOption . attr ( 'value' ) || '0' ) ;
flashHighlight ( selector ) ;
}
// If search got cleared, we make sure to hide the option and go back to the one before
if ( ! searchTerm && ! isHidden ) {
searchOption . attr ( 'hidden' , '' ) ;
selector . val ( localStorage . getItem ( SORT _ORDER _KEY ) || '0' ) ;
}
}
2024-08-02 20:42:53 +02:00
/ * *
* Sets the value of a specific key in the original data entry corresponding to the given uid
* This needs to be called whenever you update JSON data fields .
* Use ` originalWIDataKeyMap ` to find the correct value to be set .
*
* @ param { object } data - The data object containing the original data entries .
* @ param { string } uid - The unique identifier of the data entry .
* @ param { string } key - The key of the value to be set .
* @ param { any } value - The value to be set .
* /
export function setWIOriginalDataValue ( data , uid , key , value ) {
2023-07-20 19:32:15 +02:00
if ( data . originalData && Array . isArray ( data . originalData . entries ) ) {
let originalEntry = data . originalData . entries . find ( x => x . uid === uid ) ;
if ( ! originalEntry ) {
return ;
}
2024-03-12 00:49:05 +01:00
setValueByPath ( originalEntry , key , value ) ;
2023-07-20 19:32:15 +02:00
}
}
2024-08-02 20:51:12 +02:00
/ * *
* Deletes the original data entry corresponding to the given uid from the provided data object
*
* @ param { object } data - The data object containing the original data entries
* @ param { string } uid - The unique identifier of the data entry to be deleted
* /
export function deleteWIOriginalDataValue ( data , uid ) {
2023-07-20 19:32:15 +02:00
if ( data . originalData && Array . isArray ( data . originalData . entries ) ) {
const originalIndex = data . originalData . entries . findIndex ( x => x . uid === uid ) ;
if ( originalIndex >= 0 ) {
data . originalData . entries . splice ( originalIndex , 1 ) ;
}
}
}
2024-05-08 20:34:53 +02:00
/** @typedef {import('./utils.js').Select2Option} Select2Option */
2024-05-07 05:44:18 +02:00
2024-05-07 02:01:54 +02:00
/ * *
* Splits a given input string that contains one or more keywords or regexes , separated by commas .
*
* Each part can be a valid regex following the pattern ` /myregex/flags ` with optional flags . Commmas inside the regex are allowed , slashes have to be escaped like this : ` \/ `
* If a regex doesn ' t stand alone , it is not treated as a regex .
*
* @ param { string } input - One or multiple keywords or regexes , separated by commas
* @ returns { string [ ] } An array of keywords and regexes
* /
2024-08-02 20:42:53 +02:00
export function splitKeywordsAndRegexes ( input ) {
2024-05-07 02:01:54 +02:00
/** @type {string[]} */
let keywordsAndRegexes = [ ] ;
// We can make this easy. Instead of writing another function to find and parse regexes,
// we gonna utilize the custom tokenizer that also handles the input.
// No need for validation here
2024-05-07 05:44:18 +02:00
const addFindCallback = ( /** @type {Select2Option} */ item ) => {
keywordsAndRegexes . push ( item . text ) ;
2024-05-15 00:21:45 +02:00
} ;
2024-05-07 02:01:54 +02:00
const { term } = customTokenizer ( { _type : 'custom_call' , term : input } , undefined , addFindCallback ) ;
2024-05-14 04:51:22 +02:00
const finalTerm = term . trim ( ) ;
if ( finalTerm ) {
addFindCallback ( { id : getSelect2OptionId ( finalTerm ) , text : finalTerm } ) ;
}
2024-05-07 02:01:54 +02:00
return keywordsAndRegexes ;
}
/ * *
* Tokenizer parsing input and splitting it into keywords and regexes
2024-05-09 23:30:18 +02:00
*
2024-05-07 02:01:54 +02:00
* @ param { { _type : string , term : string } } input - The typed input
* @ param { { options : object } } _selection - The selection even object ( ? )
2024-05-07 05:44:18 +02:00
* @ param { function ( Select2Option ) : void } callback - The original callback function to call if an item should be inserted
2024-05-07 02:01:54 +02:00
* @ returns { { term : string } } - The remaining part that is untokenized in the textbox
* /
function customTokenizer ( input , _selection , callback ) {
let current = input . term ;
2024-06-14 01:02:59 +02:00
let insideRegex = false , regexClosed = false ;
2024-05-07 02:01:54 +02:00
// Go over the input and check the current state, if we can get a token
for ( let i = 0 ; i < current . length ; i ++ ) {
let char = current [ i ] ;
2024-06-14 01:02:59 +02:00
// If we find an unascaped slash, set the current regex state
if ( char === '/' && ( i === 0 || current [ i - 1 ] !== '\\' ) ) {
if ( ! insideRegex ) insideRegex = true ;
else if ( ! regexClosed ) regexClosed = true ;
}
2024-05-07 02:01:54 +02:00
// If a comma is typed, we tokenize the input.
// unless we are inside a possible regex, which would allow commas inside
if ( char === ',' ) {
// We take everything up till now and consider this a token
const token = current . slice ( 0 , i ) . trim ( ) ;
2024-06-14 01:02:59 +02:00
// Now how we test if this is a regex? And not a finished one, but a half-finished one?
// We use the state remembered from above to check whether the delimiter was opened but not closed yet.
// We don't check validity here if we are inside a regex, because it might only get valid after its finished. (Closing brackets, etc)
// Validity will be finally checked when the next comma is typed.
if ( insideRegex && ! regexClosed ) {
2024-05-07 02:01:54 +02:00
continue ;
}
// So now the comma really means the token is done.
// We take the token up till now, and insert it. Empty will be skipped.
if ( token ) {
2024-05-07 05:44:18 +02:00
const isRegex = isValidRegex ( token ) ;
2024-05-07 02:01:54 +02:00
// Last chance to check for valid regex again. Because it might have been valid while typing, but now is not valid anymore and contains commas we need to split.
2024-05-07 05:44:18 +02:00
if ( token . startsWith ( '/' ) && ! isRegex ) {
2024-05-07 02:01:54 +02:00
const tokens = token . split ( ',' ) . map ( x => x . trim ( ) ) ;
2024-05-14 04:51:22 +02:00
tokens . forEach ( x => callback ( { id : getSelect2OptionId ( x ) , text : x } ) ) ;
2024-05-07 02:01:54 +02:00
} else {
2024-05-07 05:44:18 +02:00
callback ( { id : getSelect2OptionId ( token ) , text : token } ) ;
2024-05-07 02:01:54 +02:00
}
}
// Now remove the token from the current input, and the comma too
current = current . slice ( i + 1 ) ;
2024-06-23 17:41:49 +02:00
insideRegex = false ;
regexClosed = false ;
2024-05-14 04:51:22 +02:00
i = 0 ;
2024-05-07 02:01:54 +02:00
}
}
// At the end, just return the left-over input
return { term : current } ;
}
/ * *
* Validates if a string is a valid slash - delimited regex , that can be parsed and executed
*
* This is a wrapper around ` parseRegexFromString `
*
* @ param { string } input - A delimited regex string
* @ returns { boolean } Whether this would be a valid regex that can be parsed and executed
* /
function isValidRegex ( input ) {
return parseRegexFromString ( input ) !== null ;
}
/ * *
* Gets a real regex object from a slash - delimited regex string
*
* This function works with ` / ` as delimiter , and each occurance of it inside the regex has to be escaped .
* Flags are optional , but can only be valid flags supported by JavaScript ' s ` RegExp ` ( ` g ` , ` i ` , ` m ` , ` s ` , ` u ` , ` y ` ) .
*
* @ param { string } input - A delimited regex string
* @ returns { RegExp | null } The regex object , or null if not a valid regex
* /
2024-08-02 20:42:53 +02:00
export function parseRegexFromString ( input ) {
2024-05-07 02:01:54 +02:00
// Extracting the regex pattern and flags
let match = input . match ( /^\/([\w\W]+?)\/([gimsuy]*)$/ ) ;
if ( ! match ) {
return null ; // Not a valid regex format
}
let [ , pattern , flags ] = match ;
// If we find any unescaped slash delimiter, we also exit out.
// JS doesn't care about delimiters inside regex patterns, but for this to be a valid regex outside of our implementation,
// we have to make sure that our delimiter is correctly escaped. Or every other engine would fail.
if ( pattern . match ( /(^|[^\\])\// ) ) {
return null ;
}
// Now we need to actually unescape the slash delimiters, because JS doesn't care about delimiters
pattern = pattern . replace ( '\\/' , '/' ) ;
// Then we return the regex. If it fails, it was invalid syntax.
try {
return new RegExp ( pattern , flags ) ;
} catch ( e ) {
return null ;
}
}
2024-10-13 23:16:30 +02:00
export async function getWorldEntry ( name , data , entry ) {
2023-11-04 19:02:38 +01:00
if ( ! data . entries [ entry . uid ] ) {
return ;
}
2024-04-05 12:45:28 +02:00
const template = WI _ENTRY _EDIT _TEMPLATE . clone ( ) ;
2023-12-02 19:04:51 +01:00
template . data ( 'uid' , entry . uid ) ;
template . attr ( 'uid' , entry . uid ) ;
2023-07-20 19:32:15 +02:00
2024-05-15 00:19:09 +02:00
// Init default state of WI Key toggle (=> true)
if ( typeof power _user . wi _key _input _plaintext === 'undefined' ) power _user . wi _key _input _plaintext = true ;
2024-05-07 02:01:54 +02:00
/** Function to build the keys input controls @param {string} entryPropName @param {string} originalDataValueName */
function enableKeysInput ( entryPropName , originalDataValueName ) {
2024-05-14 04:51:22 +02:00
const isFancyInput = ! isMobile ( ) && ! power _user . wi _key _input _plaintext ;
const input = isFancyInput ? template . find ( ` select[name=" ${ entryPropName } "] ` ) : template . find ( ` textarea[name=" ${ entryPropName } "] ` ) ;
2024-05-07 02:01:54 +02:00
input . data ( 'uid' , entry . uid ) ;
input . on ( 'click' , function ( event ) {
// Prevent closing the drawer on clicking the input
event . stopPropagation ( ) ;
} ) ;
2023-07-20 19:32:15 +02:00
2024-05-08 20:34:53 +02:00
function templateStyling ( /** @type {Select2Option} */ item , { searchStyle = false } = { } ) {
2024-05-10 00:42:35 +02:00
const content = $ ( '<span>' ) . addClass ( 'item' ) . text ( item . text ) . attr ( 'title' , ` ${ item . text } \n \n Click to edit ` ) ;
2024-05-07 05:44:18 +02:00
const isRegex = isValidRegex ( item . text ) ;
2024-05-08 20:34:53 +02:00
if ( isRegex ) {
2024-05-09 03:31:41 +02:00
content . html ( highlightRegex ( item . text ) ) ;
2024-05-15 00:21:45 +02:00
content . addClass ( 'regex_item' ) . prepend ( $ ( '<span>' ) . addClass ( 'regex_icon' ) . text ( '•*' ) . attr ( 'title' , 'Regex' ) ) ;
2024-05-08 20:34:53 +02:00
}
if ( searchStyle && item . count ) {
// Build a wrapping element
const wrapper = $ ( '<span>' ) . addClass ( 'result_block' )
. append ( content ) ;
wrapper . append ( $ ( '<span>' ) . addClass ( 'item_count' ) . text ( item . count ) . attr ( 'title' , ` Used as a key ${ item . count } ${ item . count != 1 ? 'times' : 'time' } in this lorebook ` ) ) ;
return wrapper ;
}
return content ;
2024-05-07 05:44:18 +02:00
}
2024-05-14 04:51:22 +02:00
if ( isFancyInput ) {
2024-08-05 01:06:37 +02:00
// First initialize existing values as options, before initializing select2, to speed up performance
select2ModifyOptions ( input , entry [ entryPropName ] , { select : true , changeEventArgs : { skipReset : true , noSave : true } } ) ;
2024-05-07 02:01:54 +02:00
input . select2 ( {
2024-05-09 03:31:41 +02:00
ajax : dynamicSelect2DataViaAjax ( ( ) => worldEntryKeyOptionsCache ) ,
2024-05-07 02:01:54 +02:00
tags : true ,
tokenSeparators : [ ',' ] ,
tokenizer : customTokenizer ,
placeholder : input . attr ( 'placeholder' ) ,
2024-05-08 20:34:53 +02:00
templateResult : item => templateStyling ( item , { searchStyle : true } ) ,
templateSelection : item => templateStyling ( item ) ,
2024-05-07 02:01:54 +02:00
} ) ;
2024-07-22 04:23:05 +02:00
input . on ( 'change' , async function ( _ , { skipReset , noSave } = { } ) {
2024-05-07 02:01:54 +02:00
const uid = $ ( this ) . data ( 'uid' ) ;
/** @type {string[]} */
2024-05-07 05:44:18 +02:00
const keys = ( $ ( this ) . select2 ( 'data' ) ) . map ( x => x . text ) ;
2024-05-07 02:01:54 +02:00
2024-08-05 05:03:46 +02:00
! skipReset && await resetScrollHeight ( this ) ;
2024-05-07 02:01:54 +02:00
if ( ! noSave ) {
data . entries [ uid ] [ entryPropName ] = keys ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , originalDataValueName , data . entries [ uid ] [ entryPropName ] ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-05-07 02:01:54 +02:00
}
} ) ;
2024-05-08 20:34:53 +02:00
input . on ( 'select2:select' , /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache ( [ event . params . data ] ) ) ;
input . on ( 'select2:unselect' , /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache ( [ event . params . data ] , { remove : true } ) ) ;
2023-07-20 19:32:15 +02:00
2024-05-10 00:42:35 +02:00
select2ChoiceClickSubscribe ( input , target => {
2024-09-05 03:44:37 +02:00
const key = $ ( target . closest ( '.regex-highlight, .item' ) ) . text ( ) ;
2024-05-10 00:42:35 +02:00
console . debug ( 'Editing WI key' , key ) ;
// Remove the current key from the actual selection
const selected = input . val ( ) ;
if ( ! Array . isArray ( selected ) ) return ;
var index = selected . indexOf ( getSelect2OptionId ( key ) ) ;
if ( index > - 1 ) selected . splice ( index , 1 ) ;
input . val ( selected ) . trigger ( 'change' ) ;
// Manually update the cache, that change event is not gonna trigger it
2024-05-15 00:21:45 +02:00
updateWorldEntryKeyOptionsCache ( [ key ] , { remove : true } ) ;
2024-05-10 00:42:35 +02:00
// We need to "hack" the actual text input into the currently open textarea
input . next ( 'span.select2-container' ) . find ( 'textarea' )
. val ( key ) . trigger ( 'input' ) ;
} , { openDrawer : true } ) ;
2024-05-07 02:01:54 +02:00
}
else {
// Compatibility with mobile devices. On mobile we need a text input field, not a select option control, so we need its own event handlers
template . find ( ` select[name=" ${ entryPropName } "] ` ) . hide ( ) ;
input . show ( ) ;
2024-07-22 04:23:05 +02:00
input . on ( 'input' , async function ( _ , { skipReset , noSave } = { } ) {
2024-05-07 02:01:54 +02:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = String ( $ ( this ) . val ( ) ) ;
2024-08-05 05:03:46 +02:00
! skipReset && await resetScrollHeight ( this ) ;
2024-05-14 04:51:22 +02:00
if ( ! noSave ) {
data . entries [ uid ] [ entryPropName ] = splitKeywordsAndRegexes ( value ) ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , originalDataValueName , data . entries [ uid ] [ entryPropName ] ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-05-14 04:51:22 +02:00
}
2024-05-07 02:01:54 +02:00
} ) ;
input . val ( entry [ entryPropName ] . join ( ', ' ) ) . trigger ( 'input' , { skipReset : true } ) ;
}
2024-05-15 23:47:31 +02:00
return { isFancy : isFancyInput , control : input } ;
2024-05-07 02:01:54 +02:00
}
// key
2024-05-15 23:47:31 +02:00
const keyInput = enableKeysInput ( 'key' , 'keys' ) ;
2024-05-07 02:01:54 +02:00
// keysecondary
2024-05-15 23:47:31 +02:00
const keySecondaryInput = enableKeysInput ( 'keysecondary' , 'secondary_keys' ) ;
2023-07-20 19:32:15 +02:00
2024-05-14 04:51:22 +02:00
// draw key input switch button
template . find ( '.switch_input_type_icon' ) . on ( 'click' , function ( ) {
power _user . wi _key _input _plaintext = ! power _user . wi _key _input _plaintext ;
saveSettingsDebounced ( ) ;
// Just redraw the panel
const uid = ( $ ( this ) . parents ( '.world_entry' ) ) . data ( 'uid' ) ;
updateEditor ( uid , false ) ;
$ ( ` .world_entry[uid=" ${ uid } "] .inline-drawer-icon ` ) . trigger ( 'click' ) ;
// setTimeout(() => {
// }, debounce_timeout.standard);
} ) . each ( ( _ , icon ) => {
$ ( icon ) . attr ( 'title' , $ ( icon ) . data ( power _user . wi _key _input _plaintext ? 'tooltip-on' : 'tooltip-off' ) ) ;
$ ( icon ) . text ( $ ( icon ) . data ( power _user . wi _key _input _plaintext ? 'icon-on' : 'icon-off' ) ) ;
} ) ;
2023-07-20 19:32:15 +02:00
// logic AND/NOT
const selectiveLogicDropdown = template . find ( 'select[name="entryLogicType"]' ) ;
2023-12-02 19:04:51 +01:00
selectiveLogicDropdown . data ( 'uid' , entry . uid ) ;
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
selectiveLogicDropdown . on ( 'click' , function ( event ) {
2023-10-01 03:21:19 +02:00
event . stopPropagation ( ) ;
2023-12-02 20:11:06 +01:00
} ) ;
2023-10-01 03:21:19 +02:00
2024-07-22 04:23:05 +02:00
selectiveLogicDropdown . on ( 'input' , async function ( ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
2023-07-20 19:32:15 +02:00
const value = Number ( $ ( this ) . val ( ) ) ;
2023-12-05 11:04:27 +01:00
data . entries [ uid ] . selectiveLogic = ! isNaN ( value ) ? value : world _info _logic . AND _ANY ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'selectiveLogic' , data . entries [ uid ] . selectiveLogic ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-10-12 05:44:52 +02:00
2023-07-20 19:32:15 +02:00
template
. find ( ` select[name="entryLogicType"] option[value= ${ entry . selectiveLogic } ] ` )
2023-12-02 19:04:51 +01:00
. prop ( 'selected' , true )
. trigger ( 'input' ) ;
2023-07-20 19:32:15 +02:00
2023-09-23 21:35:51 +02:00
// Character filter
2023-12-02 19:04:51 +01:00
const characterFilterLabel = template . find ( 'label[for="characterFilter"] > small' ) ;
characterFilterLabel . text ( entry . characterFilter ? . isExclude ? 'Exclude Character(s)' : 'Filter to Character(s)' ) ;
2023-09-23 21:35:51 +02:00
// exclude characters checkbox
2023-12-02 19:04:51 +01:00
const characterExclusionInput = template . find ( 'input[name="character_exclusion"]' ) ;
characterExclusionInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
characterExclusionInput . on ( 'input' , async function ( ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . prop ( 'checked' ) ;
characterFilterLabel . text ( value ? 'Exclude Character(s)' : 'Filter to Character(s)' ) ;
2023-09-23 21:35:51 +02:00
if ( data . entries [ uid ] . characterFilter ) {
2023-11-14 22:54:08 +01:00
if ( ! value && data . entries [ uid ] . characterFilter . names . length === 0 && data . entries [ uid ] . characterFilter . tags . length === 0 ) {
2023-09-23 21:35:51 +02:00
delete data . entries [ uid ] . characterFilter ;
} else {
2023-12-02 20:11:06 +01:00
data . entries [ uid ] . characterFilter . isExclude = value ;
2023-09-23 21:35:51 +02:00
}
} else if ( value ) {
Object . assign (
data . entries [ uid ] ,
2023-09-24 13:45:04 +02:00
{
2023-09-23 21:35:51 +02:00
characterFilter : {
isExclude : true ,
2023-11-14 22:54:08 +01:00
names : [ ] ,
tags : [ ] ,
2023-12-02 21:06:57 +01:00
} ,
} ,
2023-09-23 21:35:51 +02:00
) ;
}
2024-04-23 02:09:52 +02:00
// Verify names to exist in the system
if ( data . entries [ uid ] ? . characterFilter ? . names ? . length > 0 ) {
for ( const name of [ ... data . entries [ uid ] . characterFilter . names ] ) {
if ( ! getContext ( ) . characters . find ( x => x . avatar . replace ( /\.[^/.]+$/ , '' ) === name ) ) {
console . warn ( ` World Info: Character ${ name } not found. Removing from the entry filter. ` , entry ) ;
data . entries [ uid ] . characterFilter . names = data . entries [ uid ] . characterFilter . names . filter ( x => x !== name ) ;
}
}
}
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'character_filter' , data . entries [ uid ] . characterFilter ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-09-23 21:35:51 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
characterExclusionInput . prop ( 'checked' , entry . characterFilter ? . isExclude ? ? false ) . trigger ( 'input' ) ;
2023-09-23 21:35:51 +02:00
2023-12-02 19:04:51 +01:00
const characterFilter = template . find ( 'select[name="characterFilter"]' ) ;
2023-12-02 20:11:06 +01:00
characterFilter . data ( 'uid' , entry . uid ) ;
2023-12-15 00:03:10 +01:00
if ( ! isMobile ( ) ) {
2023-09-23 21:35:51 +02:00
$ ( characterFilter ) . select2 ( {
width : '100%' ,
2024-08-19 23:37:10 +02:00
placeholder : 'Tie this entry to specific characters or characters with specific tags' ,
2023-09-23 21:35:51 +02:00
allowClear : true ,
closeOnSelect : false ,
} ) ;
}
2023-11-14 22:54:08 +01:00
2023-09-23 21:35:51 +02:00
const characters = getContext ( ) . characters ;
characters . forEach ( ( character ) => {
const option = document . createElement ( 'option' ) ;
2023-12-02 19:04:51 +01:00
const name = character . avatar . replace ( /\.[^/.]+$/ , '' ) ? ? character . name ;
2023-11-14 22:54:08 +01:00
option . innerText = name ;
option . selected = entry . characterFilter ? . names ? . includes ( name ) ;
option . setAttribute ( 'data-type' , 'character' ) ;
characterFilter . append ( option ) ;
} ) ;
const tags = getContext ( ) . tags ;
tags . forEach ( ( tag ) => {
const option = document . createElement ( 'option' ) ;
option . innerText = ` [Tag] ${ tag . name } ` ;
option . selected = entry . characterFilter ? . tags ? . includes ( tag . id ) ;
option . value = tag . id ;
option . setAttribute ( 'data-type' , 'tag' ) ;
characterFilter . append ( option ) ;
2023-09-23 21:35:51 +02:00
} ) ;
characterFilter . on ( 'mousedown change' , async function ( e ) {
// If there's no world names, don't do anything
if ( world _names . length === 0 ) {
e . preventDefault ( ) ;
return ;
}
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
2023-11-14 22:54:08 +01:00
const selected = $ ( this ) . find ( ':selected' ) ;
if ( ( ! selected || selected ? . length === 0 ) && ! data . entries [ uid ] . characterFilter ? . isExclude ) {
2023-09-23 21:35:51 +02:00
delete data . entries [ uid ] . characterFilter ;
} else {
2023-11-14 22:54:08 +01:00
const names = selected . filter ( '[data-type="character"]' ) . map ( ( _ , e ) => e instanceof HTMLOptionElement && e . innerText ) . toArray ( ) ;
const tags = selected . filter ( '[data-type="tag"]' ) . map ( ( _ , e ) => e instanceof HTMLOptionElement && e . value ) . toArray ( ) ;
2023-09-23 21:35:51 +02:00
Object . assign (
data . entries [ uid ] ,
2023-09-24 13:45:04 +02:00
{
2023-09-23 21:35:51 +02:00
characterFilter : {
isExclude : data . entries [ uid ] . characterFilter ? . isExclude ? ? false ,
2023-11-14 22:54:08 +01:00
names : names ,
tags : tags ,
2023-12-02 21:06:57 +01:00
} ,
} ,
2023-09-23 21:35:51 +02:00
) ;
}
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'character_filter' , data . entries [ uid ] . characterFilter ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-09-23 21:35:51 +02:00
} ) ;
2023-07-20 19:32:15 +02:00
// comment
const commentInput = template . find ( 'textarea[name="comment"]' ) ;
const commentToggle = template . find ( 'input[name="addMemo"]' ) ;
2023-12-02 19:04:51 +01:00
commentInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
commentInput . on ( 'input' , async function ( _ , { skipReset } = { } ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
2023-07-20 19:32:15 +02:00
const value = $ ( this ) . val ( ) ;
2024-08-05 05:03:46 +02:00
! skipReset && await resetScrollHeight ( this ) ;
2023-07-20 19:32:15 +02:00
data . entries [ uid ] . comment = value ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'comment' , data . entries [ uid ] . comment ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
commentToggle . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
commentToggle . on ( 'input' , async function ( ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . prop ( 'checked' ) ;
2023-07-20 19:32:15 +02:00
//console.log(value)
const commentContainer = $ ( this )
2023-12-02 19:04:51 +01:00
. closest ( '.world_entry' )
. find ( '.commentContainer' ) ;
2023-07-20 19:32:15 +02:00
data . entries [ uid ] . addMemo = value ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
value ? commentContainer . show ( ) : commentContainer . hide ( ) ;
} ) ;
2024-04-05 12:45:28 +02:00
commentInput . val ( entry . comment ) . trigger ( 'input' , { skipReset : true } ) ;
//initScrollHeight(commentInput);
2023-12-02 19:04:51 +01:00
commentToggle . prop ( 'checked' , true /* entry.addMemo */ ) . trigger ( 'input' ) ;
2023-12-02 20:11:06 +01:00
commentToggle . parent ( ) . hide ( ) ;
2023-07-20 19:32:15 +02:00
// content
2023-12-02 19:04:51 +01:00
const counter = template . find ( '.world_entry_form_token_counter' ) ;
2024-04-13 20:33:19 +02:00
const countTokensDebounced = debounce ( async function ( counter , value ) {
const numberOfTokens = await getTokenCountAsync ( value ) ;
2023-08-21 20:10:11 +02:00
$ ( counter ) . text ( numberOfTokens ) ;
2024-04-28 06:21:47 +02:00
} , debounce _timeout . relaxed ) ;
2023-07-20 19:32:15 +02:00
const contentInput = template . find ( 'textarea[name="content"]' ) ;
2023-12-02 19:04:51 +01:00
contentInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
contentInput . on ( 'input' , async function ( _ , { skipCount } = { } ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
2023-07-20 19:32:15 +02:00
const value = $ ( this ) . val ( ) ;
data . entries [ uid ] . content = value ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'content' , data . entries [ uid ] . content ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
2023-08-20 12:15:02 +02:00
if ( skipCount ) {
return ;
}
2023-07-20 19:32:15 +02:00
// count tokens
2023-08-21 17:16:40 +02:00
countTokensDebounced ( counter , value ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
contentInput . val ( entry . content ) . trigger ( 'input' , { skipCount : true } ) ;
2023-07-20 19:32:15 +02:00
//initScrollHeight(contentInput);
2023-08-20 12:15:02 +02:00
template . find ( '.inline-drawer-toggle' ) . on ( 'click' , function ( ) {
if ( counter . data ( 'first-run' ) ) {
counter . data ( 'first-run' , false ) ;
2023-08-21 17:16:40 +02:00
countTokensDebounced ( counter , contentInput . val ( ) ) ;
2024-05-15 23:47:31 +02:00
if ( ! keyInput . isFancy ) initScrollHeight ( keyInput . control ) ;
2024-05-16 23:03:41 +02:00
if ( ! keySecondaryInput . isFancy ) initScrollHeight ( keySecondaryInput . control ) ;
2023-08-20 12:15:02 +02:00
}
} ) ;
2023-07-20 19:32:15 +02:00
// selective
const selectiveInput = template . find ( 'input[name="selective"]' ) ;
2023-12-02 19:04:51 +01:00
selectiveInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
selectiveInput . on ( 'input' , async function ( ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . prop ( 'checked' ) ;
2023-07-20 19:32:15 +02:00
data . entries [ uid ] . selective = value ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'selective' , data . entries [ uid ] . selective ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
const keysecondary = $ ( this )
2023-12-02 19:04:51 +01:00
. closest ( '.world_entry' )
. find ( '.keysecondary' ) ;
2023-07-20 19:32:15 +02:00
const keysecondarytextpole = $ ( this )
2023-12-02 19:04:51 +01:00
. closest ( '.world_entry' )
. find ( '.keysecondarytextpole' ) ;
2023-07-20 19:32:15 +02:00
2024-05-07 02:01:54 +02:00
const keyprimaryselect = $ ( this )
2023-12-02 19:04:51 +01:00
. closest ( '.world_entry' )
2024-05-07 02:01:54 +02:00
. find ( '.keyprimaryselect' ) ;
2023-07-20 19:32:15 +02:00
2024-05-07 02:01:54 +02:00
const keyprimaryHeight = keyprimaryselect . outerHeight ( ) ;
2023-07-20 19:32:15 +02:00
keysecondarytextpole . css ( 'height' , keyprimaryHeight + 'px' ) ;
value ? keysecondary . show ( ) : keysecondary . hide ( ) ;
} ) ;
//forced on, ignored if empty
2023-12-02 19:04:51 +01:00
selectiveInput . prop ( 'checked' , true /* entry.selective */ ) . trigger ( 'input' ) ;
2023-07-20 19:32:15 +02:00
selectiveInput . parent ( ) . hide ( ) ;
// constant
2023-10-01 03:21:19 +02:00
/ *
2023-07-20 19:32:15 +02:00
const constantInput = template . find ( 'input[name="constant"]' ) ;
constantInput . data ( "uid" , entry . uid ) ;
2024-07-22 04:23:05 +02:00
constantInput . on ( "input" , async function ( ) {
2023-07-20 19:32:15 +02:00
const uid = $ ( this ) . data ( "uid" ) ;
const value = $ ( this ) . prop ( "checked" ) ;
data . entries [ uid ] . constant = value ;
setOriginalDataValue ( data , uid , "constant" , data . entries [ uid ] . constant ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
} ) ;
constantInput . prop ( "checked" , entry . constant ) . trigger ( "input" ) ;
2023-10-01 03:21:19 +02:00
* /
2023-07-20 19:32:15 +02:00
// order
const orderInput = template . find ( 'input[name="order"]' ) ;
2023-12-02 19:04:51 +01:00
orderInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
orderInput . on ( 'input' , async function ( ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
2023-07-20 19:32:15 +02:00
const value = Number ( $ ( this ) . val ( ) ) ;
data . entries [ uid ] . order = ! isNaN ( value ) ? value : 0 ;
2023-12-02 20:11:06 +01:00
updatePosOrdDisplay ( uid ) ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'insertion_order' , data . entries [ uid ] . order ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
orderInput . val ( entry . order ) . trigger ( 'input' ) ;
2023-11-04 16:44:43 +01:00
orderInput . css ( 'width' , 'calc(3em + 15px)' ) ;
2023-07-20 19:32:15 +02:00
2023-12-07 19:06:06 +01:00
// group
const groupInput = template . find ( 'input[name="group"]' ) ;
groupInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
groupInput . on ( 'input' , async function ( ) {
2023-12-07 19:06:06 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = String ( $ ( this ) . val ( ) ) . trim ( ) ;
data . entries [ uid ] . group = value ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.group' , data . entries [ uid ] . group ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-12-07 19:06:06 +01:00
} ) ;
groupInput . val ( entry . group ? ? '' ) . trigger ( 'input' ) ;
2024-05-14 00:16:41 +02:00
setTimeout ( ( ) => createEntryInputAutocomplete ( groupInput , getInclusionGroupCallback ( data ) , { allowMultiple : true } ) , 1 ) ;
2023-12-07 19:06:06 +01:00
2024-04-27 02:23:37 +02:00
// inclusion priority
2024-04-27 20:03:55 +02:00
const groupOverrideInput = template . find ( 'input[name="groupOverride"]' ) ;
2024-04-27 04:40:35 +02:00
groupOverrideInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
groupOverrideInput . on ( 'input' , async function ( ) {
2024-04-27 02:23:37 +02:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . prop ( 'checked' ) ;
2024-04-27 04:40:35 +02:00
data . entries [ uid ] . groupOverride = value ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.group_override' , data . entries [ uid ] . groupOverride ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-04-27 02:23:37 +02:00
} ) ;
2024-04-27 04:40:35 +02:00
groupOverrideInput . prop ( 'checked' , entry . groupOverride ) . trigger ( 'input' ) ;
2024-04-27 02:23:37 +02:00
2024-05-06 16:00:42 +02:00
// group weight
const groupWeightInput = template . find ( 'input[name="groupWeight"]' ) ;
groupWeightInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
groupWeightInput . on ( 'input' , async function ( ) {
2024-05-06 16:00:42 +02:00
const uid = $ ( this ) . data ( 'uid' ) ;
2024-05-06 21:55:31 +02:00
let value = Number ( $ ( this ) . val ( ) ) ;
const min = Number ( $ ( this ) . attr ( 'min' ) ) ;
const max = Number ( $ ( this ) . attr ( 'max' ) ) ;
// Clamp the value
if ( value < min ) {
value = min ;
$ ( this ) . val ( min ) ;
} else if ( value > max ) {
value = max ;
$ ( this ) . val ( max ) ;
}
2024-05-06 16:00:42 +02:00
2024-05-06 21:55:31 +02:00
data . entries [ uid ] . groupWeight = ! isNaN ( value ) ? Math . abs ( value ) : 1 ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.group_weight' , data . entries [ uid ] . groupWeight ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-05-06 16:00:42 +02:00
} ) ;
2024-05-06 21:55:31 +02:00
groupWeightInput . val ( entry . groupWeight ? ? DEFAULT _WEIGHT ) . trigger ( 'input' ) ;
2024-05-06 16:00:42 +02:00
2024-06-20 23:53:00 +02:00
// sticky
const sticky = template . find ( 'input[name="sticky"]' ) ;
sticky . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
sticky . on ( 'input' , async function ( ) {
2024-06-20 23:53:00 +02:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = Number ( $ ( this ) . val ( ) ) ;
data . entries [ uid ] . sticky = ! isNaN ( value ) ? value : null ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.sticky' , data . entries [ uid ] . sticky ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-06-20 23:53:00 +02:00
} ) ;
sticky . val ( entry . sticky > 0 ? entry . sticky : '' ) . trigger ( 'input' ) ;
// cooldown
const cooldown = template . find ( 'input[name="cooldown"]' ) ;
cooldown . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
cooldown . on ( 'input' , async function ( ) {
2024-06-20 23:53:00 +02:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = Number ( $ ( this ) . val ( ) ) ;
data . entries [ uid ] . cooldown = ! isNaN ( value ) ? value : null ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.cooldown' , data . entries [ uid ] . cooldown ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-06-20 23:53:00 +02:00
} ) ;
cooldown . val ( entry . cooldown > 0 ? entry . cooldown : '' ) . trigger ( 'input' ) ;
2024-06-26 21:43:30 +02:00
// delay
const delay = template . find ( 'input[name="delay"]' ) ;
delay . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
delay . on ( 'input' , async function ( ) {
2024-06-26 21:43:30 +02:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = Number ( $ ( this ) . val ( ) ) ;
data . entries [ uid ] . delay = ! isNaN ( value ) ? value : null ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.delay' , data . entries [ uid ] . delay ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-06-26 21:43:30 +02:00
} ) ;
delay . val ( entry . delay > 0 ? entry . delay : '' ) . trigger ( 'input' ) ;
2023-07-20 19:32:15 +02:00
// probability
if ( entry . probability === undefined ) {
entry . probability = null ;
}
2023-09-24 13:41:56 +02:00
// depth
const depthInput = template . find ( 'input[name="depth"]' ) ;
2023-12-02 19:04:51 +01:00
depthInput . data ( 'uid' , entry . uid ) ;
2023-10-05 20:02:43 +02:00
2024-07-22 04:23:05 +02:00
depthInput . on ( 'input' , async function ( ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
2023-09-24 13:41:56 +02:00
const value = Number ( $ ( this ) . val ( ) ) ;
data . entries [ uid ] . depth = ! isNaN ( value ) ? value : 0 ;
2023-12-02 20:11:06 +01:00
updatePosOrdDisplay ( uid ) ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.depth' , data . entries [ uid ] . depth ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-09-24 13:41:56 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
depthInput . val ( entry . depth ? ? DEFAULT _DEPTH ) . trigger ( 'input' ) ;
2023-11-04 16:44:43 +01:00
depthInput . css ( 'width' , 'calc(3em + 15px)' ) ;
2023-09-24 13:41:56 +02:00
// Hide by default unless depth is specified
2023-09-24 13:45:04 +02:00
if ( entry . position === world _info _position . atDepth ) {
2023-10-05 16:40:48 +02:00
//depthInput.parent().hide();
2023-09-24 13:41:56 +02:00
}
2023-07-20 19:32:15 +02:00
const probabilityInput = template . find ( 'input[name="probability"]' ) ;
2023-12-02 19:04:51 +01:00
probabilityInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
probabilityInput . on ( 'input' , async function ( ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
2024-01-23 21:44:20 +01:00
const value = Number ( $ ( this ) . val ( ) ) ;
2023-07-20 19:32:15 +02:00
data . entries [ uid ] . probability = ! isNaN ( value ) ? value : null ;
// Clamp probability to 0-100
if ( data . entries [ uid ] . probability !== null ) {
data . entries [ uid ] . probability = Math . min ( 100 , Math . max ( 0 , data . entries [ uid ] . probability ) ) ;
if ( data . entries [ uid ] . probability !== value ) {
$ ( this ) . val ( data . entries [ uid ] . probability ) ;
}
}
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.probability' , data . entries [ uid ] . probability ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
probabilityInput . val ( entry . probability ) . trigger ( 'input' ) ;
2023-11-04 16:44:43 +01:00
probabilityInput . css ( 'width' , 'calc(3em + 15px)' ) ;
2023-07-20 19:32:15 +02:00
// probability toggle
if ( entry . useProbability === undefined ) {
entry . useProbability = false ;
}
const probabilityToggle = template . find ( 'input[name="useProbability"]' ) ;
2023-12-02 19:04:51 +01:00
probabilityToggle . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
probabilityToggle . on ( 'input' , async function ( ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . prop ( 'checked' ) ;
2023-07-20 19:32:15 +02:00
data . entries [ uid ] . useProbability = value ;
const probabilityContainer = $ ( this )
2023-12-02 19:04:51 +01:00
. closest ( '.world_entry' )
. find ( '.probabilityContainer' ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
value ? probabilityContainer . show ( ) : probabilityContainer . hide ( ) ;
if ( value && data . entries [ uid ] . probability === null ) {
data . entries [ uid ] . probability = 100 ;
}
if ( ! value ) {
data . entries [ uid ] . probability = null ;
}
2023-12-02 19:04:51 +01:00
probabilityInput . val ( data . entries [ uid ] . probability ) . trigger ( 'input' ) ;
2023-07-20 19:32:15 +02:00
} ) ;
//forced on, 100% by default
2023-12-02 19:04:51 +01:00
probabilityToggle . prop ( 'checked' , true /* entry.useProbability */ ) . trigger ( 'input' ) ;
2023-07-20 19:32:15 +02:00
probabilityToggle . parent ( ) . hide ( ) ;
// position
if ( entry . position === undefined ) {
entry . position = 0 ;
}
const positionInput = template . find ( 'select[name="position"]' ) ;
2024-04-05 12:45:28 +02:00
//initScrollHeight(positionInput);
2023-12-02 19:04:51 +01:00
positionInput . data ( 'uid' , entry . uid ) ;
positionInput . on ( 'click' , function ( event ) {
2023-10-05 20:02:43 +02:00
// Prevent closing the drawer on clicking the input
event . stopPropagation ( ) ;
} ) ;
2024-07-22 04:23:05 +02:00
positionInput . on ( 'input' , async function ( ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
2023-07-20 19:32:15 +02:00
const value = Number ( $ ( this ) . val ( ) ) ;
data . entries [ uid ] . position = ! isNaN ( value ) ? value : 0 ;
2023-09-24 13:45:04 +02:00
if ( value === world _info _position . atDepth ) {
2023-10-05 16:40:48 +02:00
depthInput . prop ( 'disabled' , false ) ;
2023-12-02 20:11:06 +01:00
depthInput . css ( 'visibility' , 'visible' ) ;
2023-10-05 16:40:48 +02:00
//depthInput.parent().show();
2024-03-23 16:36:43 +01:00
const role = Number ( $ ( this ) . find ( ':selected' ) . data ( 'role' ) ) ;
data . entries [ uid ] . role = role ;
2023-09-24 13:41:56 +02:00
} else {
2023-10-05 16:40:48 +02:00
depthInput . prop ( 'disabled' , true ) ;
2023-12-02 20:11:06 +01:00
depthInput . css ( 'visibility' , 'hidden' ) ;
2024-03-23 16:36:43 +01:00
data . entries [ uid ] . role = null ;
2023-10-05 16:40:48 +02:00
//depthInput.parent().hide();
2023-09-24 13:41:56 +02:00
}
2023-12-02 20:11:06 +01:00
updatePosOrdDisplay ( uid ) ;
2023-07-20 19:32:15 +02:00
// Spec v2 only supports before_char and after_char
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'position' , data . entries [ uid ] . position == 0 ? 'before_char' : 'after_char' ) ;
2023-07-20 19:32:15 +02:00
// Write the original value as extensions field
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.position' , data . entries [ uid ] . position ) ;
setWIOriginalDataValue ( data , uid , 'extensions.role' , data . entries [ uid ] . role ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2024-03-23 16:36:43 +01:00
const roleValue = entry . position === world _info _position . atDepth ? String ( entry . role ? ? extension _prompt _roles . SYSTEM ) : '' ;
2023-07-20 19:32:15 +02:00
template
2024-03-23 16:36:43 +01:00
. find ( ` select[name="position"] option[value= ${ entry . position } ][data-role=" ${ roleValue } "] ` )
2023-12-02 19:04:51 +01:00
. prop ( 'selected' , true )
. trigger ( 'input' ) ;
2023-07-20 19:32:15 +02:00
2023-09-25 11:03:10 +02:00
//add UID above content box (less important doesn't need to be always visible)
2023-12-02 19:04:51 +01:00
template . find ( '.world_entry_form_uid_value' ) . text ( ` (UID: ${ entry . uid } ) ` ) ;
2023-07-20 19:32:15 +02:00
2024-08-10 05:48:47 +02:00
//new tri-state selector for constant/normal/vectorized
2023-10-01 03:21:19 +02:00
const entryStateSelector = template . find ( 'select[name="entryStateSelector"]' ) ;
2023-12-02 19:04:51 +01:00
entryStateSelector . data ( 'uid' , entry . uid ) ;
entryStateSelector . on ( 'click' , function ( event ) {
2023-10-01 03:21:19 +02:00
// Prevent closing the drawer on clicking the input
event . stopPropagation ( ) ;
} ) ;
2024-07-22 04:23:05 +02:00
entryStateSelector . on ( 'input' , async function ( ) {
2023-10-01 03:21:19 +02:00
const uid = entry . uid ;
const value = $ ( this ) . val ( ) ;
switch ( value ) {
2023-12-02 19:04:51 +01:00
case 'constant' :
2023-10-01 03:21:19 +02:00
data . entries [ uid ] . constant = true ;
2024-04-23 02:09:52 +02:00
data . entries [ uid ] . vectorized = false ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'constant' , true ) ;
setWIOriginalDataValue ( data , uid , 'extensions.vectorized' , false ) ;
2023-12-02 20:11:06 +01:00
break ;
2023-12-02 19:04:51 +01:00
case 'normal' :
2023-10-01 03:21:19 +02:00
data . entries [ uid ] . constant = false ;
2024-04-23 02:09:52 +02:00
data . entries [ uid ] . vectorized = false ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'constant' , false ) ;
setWIOriginalDataValue ( data , uid , 'extensions.vectorized' , false ) ;
2024-04-23 02:09:52 +02:00
break ;
case 'vectorized' :
data . entries [ uid ] . constant = false ;
data . entries [ uid ] . vectorized = true ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'constant' , false ) ;
setWIOriginalDataValue ( data , uid , 'extensions.vectorized' , true ) ;
2023-12-02 20:11:06 +01:00
break ;
2024-08-10 05:48:47 +02:00
}
await saveWorldInfo ( name , data ) ;
} ) ;
const entryKillSwitch = template . find ( 'div[name="entryKillSwitch"]' ) ;
entryKillSwitch . data ( 'uid' , entry . uid ) ;
entryKillSwitch . on ( 'click' , async function ( event ) {
const uid = entry . uid ;
data . entries [ uid ] . disable = ! data . entries [ uid ] . disable ;
2024-08-10 23:10:17 +02:00
const isActive = ! data . entries [ uid ] . disable ;
2024-08-10 05:48:47 +02:00
setWIOriginalDataValue ( data , uid , 'enabled' , isActive ) ;
2024-08-10 10:53:02 +02:00
template . toggleClass ( 'disabledWIEntry' , ! isActive ) ;
entryKillSwitch . toggleClass ( 'fa-toggle-off' , ! isActive ) ;
entryKillSwitch . toggleClass ( 'fa-toggle-on' , isActive ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-10-01 03:21:19 +02:00
2023-12-02 20:11:06 +01:00
} ) ;
2023-10-01 03:21:19 +02:00
const entryState = function ( ) {
2024-08-10 05:48:47 +02:00
if ( entry . constant === true ) {
2023-12-02 20:11:06 +01:00
return 'constant' ;
2024-04-23 02:09:52 +02:00
} else if ( entry . vectorized === true ) {
return 'vectorized' ;
2023-10-01 03:21:19 +02:00
} else {
2023-12-02 20:11:06 +01:00
return 'normal' ;
2023-10-01 03:21:19 +02:00
}
2023-12-02 20:11:06 +01:00
} ;
2024-08-10 05:48:47 +02:00
const isActive = ! entry . disable ;
2024-08-10 10:53:02 +02:00
template . toggleClass ( 'disabledWIEntry' , ! isActive ) ;
entryKillSwitch . toggleClass ( 'fa-toggle-off' , ! isActive ) ;
entryKillSwitch . toggleClass ( 'fa-toggle-on' , isActive ) ;
2024-08-10 05:48:47 +02:00
2023-10-01 03:21:19 +02:00
template
. find ( ` select[name="entryStateSelector"] option[value= ${ entryState ( ) } ] ` )
2023-12-02 19:04:51 +01:00
. prop ( 'selected' , true )
. trigger ( 'input' ) ;
2023-10-01 03:21:19 +02:00
2023-09-23 21:35:51 +02:00
// exclude recursion
2023-07-20 19:32:15 +02:00
const excludeRecursionInput = template . find ( 'input[name="exclude_recursion"]' ) ;
2023-12-02 19:04:51 +01:00
excludeRecursionInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
excludeRecursionInput . on ( 'input' , async function ( ) {
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . prop ( 'checked' ) ;
2023-07-20 19:32:15 +02:00
data . entries [ uid ] . excludeRecursion = value ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.exclude_recursion' , data . entries [ uid ] . excludeRecursion ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
excludeRecursionInput . prop ( 'checked' , entry . excludeRecursion ) . trigger ( 'input' ) ;
2023-07-20 19:32:15 +02:00
2024-01-07 11:51:13 +01:00
// prevent recursion
const preventRecursionInput = template . find ( 'input[name="prevent_recursion"]' ) ;
preventRecursionInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
preventRecursionInput . on ( 'input' , async function ( ) {
2024-01-07 11:51:13 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . prop ( 'checked' ) ;
data . entries [ uid ] . preventRecursion = value ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.prevent_recursion' , data . entries [ uid ] . preventRecursion ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-01-07 11:51:13 +01:00
} ) ;
preventRecursionInput . prop ( 'checked' , entry . preventRecursion ) . trigger ( 'input' ) ;
2024-05-13 22:33:25 +02:00
// delay until recursion
2024-09-15 22:31:29 +02:00
// delay until recursion level
2024-05-13 22:33:25 +02:00
const delayUntilRecursionInput = template . find ( 'input[name="delay_until_recursion"]' ) ;
delayUntilRecursionInput . data ( 'uid' , entry . uid ) ;
2024-09-15 22:31:29 +02:00
const delayUntilRecursionLevelInput = template . find ( 'input[name="delayUntilRecursionLevel"]' ) ;
delayUntilRecursionLevelInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
delayUntilRecursionInput . on ( 'input' , async function ( ) {
2024-05-13 22:33:25 +02:00
const uid = $ ( this ) . data ( 'uid' ) ;
2024-09-15 22:15:24 +02:00
const toggled = $ ( this ) . prop ( 'checked' ) ;
// If the value contains a number, we'll take that one (set by the level input), otherwise we can use true/false switch
const value = toggled ? data . entries [ uid ] . delayUntilRecursion || true : false ;
2024-09-15 22:31:29 +02:00
if ( ! toggled ) delayUntilRecursionLevelInput . val ( '' ) ;
2024-05-13 22:33:25 +02:00
data . entries [ uid ] . delayUntilRecursion = value ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.delay_until_recursion' , data . entries [ uid ] . delayUntilRecursion ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-05-13 22:33:25 +02:00
} ) ;
delayUntilRecursionInput . prop ( 'checked' , entry . delayUntilRecursion ) . trigger ( 'input' ) ;
2024-09-15 22:15:24 +02:00
delayUntilRecursionLevelInput . on ( 'input' , async function ( ) {
const uid = $ ( this ) . data ( 'uid' ) ;
const content = $ ( this ) . val ( ) ;
const value = content === '' ? ( typeof data . entries [ uid ] . delayUntilRecursion === 'boolean' ? data . entries [ uid ] . delayUntilRecursion : true )
: content === 1 ? true
: ! isNaN ( Number ( content ) ) ? Number ( content )
: false ;
data . entries [ uid ] . delayUntilRecursion = value ;
setWIOriginalDataValue ( data , uid , 'extensions.delay_until_recursion' , data . entries [ uid ] . delayUntilRecursion ) ;
await saveWorldInfo ( name , data ) ;
} ) ;
// No need to retrigger inpout event, we'll just set the curret current value. It was edited/saved above already
delayUntilRecursionLevelInput . val ( [ 'number' , 'string' ] . includes ( typeof entry . delayUntilRecursion ) ? entry . delayUntilRecursion : '' ) . trigger ( 'input' ) ;
2024-04-27 06:18:26 +02:00
// duplicate button
const duplicateButton = template . find ( '.duplicate_entry_button' ) ;
duplicateButton . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
duplicateButton . on ( 'click' , async function ( ) {
2024-04-27 06:18:26 +02:00
const uid = $ ( this ) . data ( 'uid' ) ;
const entry = duplicateWorldInfoEntry ( data , uid ) ;
if ( entry ) {
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-04-27 06:18:26 +02:00
updateEditor ( entry . uid ) ;
}
} ) ;
2023-07-20 19:32:15 +02:00
// delete button
2023-12-02 19:04:51 +01:00
const deleteButton = template . find ( '.delete_entry_button' ) ;
deleteButton . data ( 'uid' , entry . uid ) ;
2024-08-02 23:00:15 +02:00
deleteButton . on ( 'click' , async function ( e ) {
e . stopPropagation ( ) ;
2023-12-02 19:04:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
2024-08-02 20:51:12 +02:00
const deleted = await deleteWorldInfoEntry ( data , uid ) ;
if ( ! deleted ) return ;
2024-08-02 20:42:53 +02:00
deleteWIOriginalDataValue ( data , uid ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2023-08-21 20:10:11 +02:00
updateEditor ( navigation _option . previous ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2024-01-23 21:44:20 +01:00
// scan depth
const scanDepthInput = template . find ( 'input[name="scanDepth"]' ) ;
scanDepthInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
scanDepthInput . on ( 'input' , async function ( ) {
2024-01-23 21:44:20 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const isEmpty = $ ( this ) . val ( ) === '' ;
const value = Number ( $ ( this ) . val ( ) ) ;
// Clamp if necessary
if ( value < 0 ) {
$ ( this ) . val ( 0 ) . trigger ( 'input' ) ;
2024-02-23 20:18:40 +01:00
toastr . warning ( 'Scan depth cannot be negative' ) ;
2024-01-23 21:44:20 +01:00
return ;
}
if ( value > MAX _SCAN _DEPTH ) {
$ ( this ) . val ( MAX _SCAN _DEPTH ) . trigger ( 'input' ) ;
2024-02-23 20:18:40 +01:00
toastr . warning ( ` Scan depth cannot exceed ${ MAX _SCAN _DEPTH } ` ) ;
2024-01-23 21:44:20 +01:00
return ;
}
2024-02-27 22:34:07 +01:00
data . entries [ uid ] . scanDepth = ! isEmpty && ! isNaN ( value ) && value >= 0 && value <= MAX _SCAN _DEPTH ? Math . floor ( value ) : null ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.scan_depth' , data . entries [ uid ] . scanDepth ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-01-23 21:44:20 +01:00
} ) ;
scanDepthInput . val ( entry . scanDepth ? ? null ) . trigger ( 'input' ) ;
// case sensitive select
const caseSensitiveSelect = template . find ( 'select[name="caseSensitive"]' ) ;
caseSensitiveSelect . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
caseSensitiveSelect . on ( 'input' , async function ( ) {
2024-01-23 21:44:20 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . val ( ) ;
data . entries [ uid ] . caseSensitive = value === 'null' ? null : value === 'true' ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.case_sensitive' , data . entries [ uid ] . caseSensitive ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-01-23 21:44:20 +01:00
} ) ;
2024-01-24 12:00:43 +01:00
caseSensitiveSelect . val ( ( entry . caseSensitive === null || entry . caseSensitive === undefined ) ? 'null' : entry . caseSensitive ? 'true' : 'false' ) . trigger ( 'input' ) ;
2024-01-23 21:44:20 +01:00
// match whole words select
const matchWholeWordsSelect = template . find ( 'select[name="matchWholeWords"]' ) ;
matchWholeWordsSelect . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
matchWholeWordsSelect . on ( 'input' , async function ( ) {
2024-01-23 21:44:20 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . val ( ) ;
data . entries [ uid ] . matchWholeWords = value === 'null' ? null : value === 'true' ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.match_whole_words' , data . entries [ uid ] . matchWholeWords ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-01-23 21:44:20 +01:00
} ) ;
2024-01-24 12:00:43 +01:00
matchWholeWordsSelect . val ( ( entry . matchWholeWords === null || entry . matchWholeWords === undefined ) ? 'null' : entry . matchWholeWords ? 'true' : 'false' ) . trigger ( 'input' ) ;
2024-01-23 21:44:20 +01:00
2024-05-04 23:42:33 +02:00
// use group scoring select
const useGroupScoringSelect = template . find ( 'select[name="useGroupScoring"]' ) ;
useGroupScoringSelect . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
useGroupScoringSelect . on ( 'input' , async function ( ) {
2024-05-04 23:42:33 +02:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . val ( ) ;
data . entries [ uid ] . useGroupScoring = value === 'null' ? null : value === 'true' ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.use_group_scoring' , data . entries [ uid ] . useGroupScoring ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-05-04 23:42:33 +02:00
} ) ;
useGroupScoringSelect . val ( ( entry . useGroupScoring === null || entry . useGroupScoring === undefined ) ? 'null' : entry . useGroupScoring ? 'true' : 'false' ) . trigger ( 'input' ) ;
2024-02-24 16:22:51 +01:00
// automation id
const automationIdInput = template . find ( 'input[name="automationId"]' ) ;
automationIdInput . data ( 'uid' , entry . uid ) ;
2024-07-22 04:23:05 +02:00
automationIdInput . on ( 'input' , async function ( ) {
2024-02-24 16:22:51 +01:00
const uid = $ ( this ) . data ( 'uid' ) ;
const value = $ ( this ) . val ( ) ;
data . entries [ uid ] . automationId = value ;
2024-08-02 20:42:53 +02:00
setWIOriginalDataValue ( data , uid , 'extensions.automation_id' , data . entries [ uid ] . automationId ) ;
2024-07-22 04:23:05 +02:00
await saveWorldInfo ( name , data ) ;
2024-02-24 16:22:51 +01:00
} ) ;
automationIdInput . val ( entry . automationId ? ? '' ) . trigger ( 'input' ) ;
2024-02-25 02:54:40 +01:00
setTimeout ( ( ) => createEntryInputAutocomplete ( automationIdInput , getAutomationIdCallback ( data ) ) , 1 ) ;
2024-02-24 16:22:51 +01:00
2023-07-20 19:32:15 +02:00
template . find ( '.inline-drawer-content' ) . css ( 'display' , 'none' ) ; //entries start collapsed
2023-09-28 17:27:12 +02:00
function updatePosOrdDisplay ( uid ) {
// display position/order info left of keyword box
2023-12-02 20:11:06 +01:00
let entry = data . entries [ uid ] ;
let posText = entry . position ;
2023-09-28 17:27:12 +02:00
switch ( entry . position ) {
case 0 :
posText = '↑CD' ;
2023-12-02 20:11:06 +01:00
break ;
2023-09-28 17:27:12 +02:00
case 1 :
posText = 'CD↓' ;
2023-12-02 20:11:06 +01:00
break ;
2023-09-28 17:27:12 +02:00
case 2 :
posText = '↑AN' ;
2023-12-02 20:11:06 +01:00
break ;
2023-09-28 17:27:12 +02:00
case 3 :
posText = 'AN↓' ;
2023-12-02 20:11:06 +01:00
break ;
2023-09-28 17:27:12 +02:00
case 4 :
posText = ` @D ${ entry . depth } ` ;
2023-12-02 20:11:06 +01:00
break ;
2023-09-28 17:27:12 +02:00
}
2023-12-02 19:04:51 +01:00
template . find ( '.world_entry_form_position_value' ) . text ( ` ( ${ posText } ${ entry . order } ) ` ) ;
2023-09-28 17:27:12 +02:00
}
2023-07-20 19:32:15 +02:00
return template ;
}
2024-02-24 18:04:44 +01:00
/ * *
2024-02-25 02:54:40 +01:00
* Get the inclusion groups for the autocomplete .
2024-02-24 18:04:44 +01:00
* @ param { any } data WI data
2024-02-25 02:54:40 +01:00
* @ returns { ( input : any , output : any ) => any } Callback function for the autocomplete
2024-02-24 18:04:44 +01:00
* /
2024-02-25 02:54:40 +01:00
function getInclusionGroupCallback ( data ) {
2024-05-14 00:54:43 +02:00
return function ( control , input , output ) {
2024-05-15 00:21:45 +02:00
const uid = $ ( control ) . data ( 'uid' ) ;
2024-05-14 00:54:43 +02:00
const thisGroups = String ( $ ( control ) . val ( ) ) . split ( /,\s*/ ) . filter ( x => x ) . map ( x => x . toLowerCase ( ) ) ;
2024-02-24 18:04:44 +01:00
const groups = new Set ( ) ;
for ( const entry of Object . values ( data . entries ) ) {
2024-05-14 00:54:43 +02:00
// Skip the groups of this entry, because auto-complete should only suggest the ones that are already available on other entries
if ( entry . uid == uid ) continue ;
2024-02-24 18:04:44 +01:00
if ( entry . group ) {
2024-05-14 00:16:41 +02:00
entry . group . split ( /,\s*/ ) . filter ( x => x ) . forEach ( x => groups . add ( x ) ) ;
2024-02-24 18:04:44 +01:00
}
}
const haystack = Array . from ( groups ) ;
haystack . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
const needle = input . term . toLowerCase ( ) ;
const hasExactMatch = haystack . findIndex ( x => x . toLowerCase ( ) == needle ) !== - 1 ;
2024-05-14 00:54:43 +02:00
const result = haystack . filter ( x => x . toLowerCase ( ) . includes ( needle ) && ( ! thisGroups . includes ( x ) || hasExactMatch && thisGroups . filter ( g => g == x ) . length == 1 ) ) ;
2024-02-24 18:04:44 +01:00
output ( result ) ;
2024-02-25 02:54:40 +01:00
} ;
}
2024-02-24 18:04:44 +01:00
2024-02-25 02:54:40 +01:00
function getAutomationIdCallback ( data ) {
2024-05-14 00:54:43 +02:00
return function ( control , input , output ) {
2024-05-15 00:21:45 +02:00
const uid = $ ( control ) . data ( 'uid' ) ;
2024-02-25 02:54:40 +01:00
const ids = new Set ( ) ;
for ( const entry of Object . values ( data . entries ) ) {
2024-05-14 00:54:43 +02:00
// Skip automation id of this entry, because auto-complete should only suggest the ones that are already available on other entries
if ( entry . uid == uid ) continue ;
2024-02-25 02:54:40 +01:00
if ( entry . automationId ) {
ids . add ( String ( entry . automationId ) ) ;
}
}
if ( 'quickReplyApi' in window ) {
// @ts-ignore
for ( const automationId of window [ 'quickReplyApi' ] . listAutomationIds ( ) ) {
ids . add ( String ( automationId ) ) ;
}
}
const haystack = Array . from ( ids ) ;
haystack . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
const needle = input . term . toLowerCase ( ) ;
const result = haystack . filter ( x => x . toLowerCase ( ) . includes ( needle ) ) ;
output ( result ) ;
} ;
}
/ * *
* Create an autocomplete for the inclusion group .
2024-05-14 00:16:41 +02:00
* @ param { JQuery < HTMLElement > } input - Input element to attach the autocomplete to
2024-05-14 00:54:43 +02:00
* @ param { ( control : JQuery < HTMLElement > , input : any , output : any ) => any } callback - Source data callbacks
2024-05-14 00:16:41 +02:00
* @ param { object } [ options = { } ] - Optional arguments
* @ param { boolean } [ options . allowMultiple = false ] - Whether to allow multiple comma - separated values
2024-02-25 02:54:40 +01:00
* /
2024-05-14 00:16:41 +02:00
function createEntryInputAutocomplete ( input , callback , { allowMultiple = false } = { } ) {
const handleSelect = ( event , ui ) => {
// Prevent default autocomplete select, so we can manually set the value
event . preventDefault ( ) ;
if ( ! allowMultiple ) {
$ ( input ) . val ( ui . item . value ) . trigger ( 'input' ) . trigger ( 'blur' ) ;
} else {
var terms = String ( $ ( input ) . val ( ) ) . split ( /,\s*/ ) ;
terms . pop ( ) ; // remove the current input
terms . push ( ui . item . value ) ; // add the selected item
2024-05-15 00:21:45 +02:00
$ ( input ) . val ( terms . filter ( x => x ) . join ( ', ' ) ) . trigger ( 'input' ) . trigger ( 'blur' ) ;
2024-05-14 00:16:41 +02:00
}
} ;
2024-02-24 18:04:44 +01:00
$ ( input ) . autocomplete ( {
minLength : 0 ,
2024-05-14 00:16:41 +02:00
source : function ( request , response ) {
if ( ! allowMultiple ) {
2024-05-14 00:54:43 +02:00
callback ( input , request , response ) ;
2024-05-14 00:16:41 +02:00
} else {
const term = request . term . split ( /,\s*/ ) . pop ( ) ;
request . term = term ;
2024-05-14 00:54:43 +02:00
callback ( input , request , response ) ;
2024-05-14 00:16:41 +02:00
}
2024-02-24 18:04:44 +01:00
} ,
2024-05-14 00:16:41 +02:00
select : handleSelect ,
2024-02-24 18:04:44 +01:00
} ) ;
2024-02-25 02:54:40 +01:00
$ ( input ) . on ( 'focus click' , function ( ) {
2024-05-14 00:16:41 +02:00
$ ( input ) . autocomplete ( 'search' , allowMultiple ? String ( $ ( input ) . val ( ) ) . split ( /,\s*/ ) . pop ( ) : $ ( input ) . val ( ) ) ;
2024-02-24 18:04:44 +01:00
} ) ;
}
2024-05-14 00:16:41 +02:00
2024-04-27 06:18:26 +02:00
/ * *
2024-08-02 20:42:53 +02:00
* Duplicate a WI entry by copying all of its properties and assigning a new uid
2024-04-27 06:18:26 +02:00
* @ param { * } data - The data of the book
* @ param { number } uid - The uid of the entry to copy in this book
* @ returns { * } The new WI duplicated entry
* /
2024-08-02 20:42:53 +02:00
export function duplicateWorldInfoEntry ( data , uid ) {
2024-04-27 06:18:26 +02:00
if ( ! data || ! ( 'entries' in data ) || ! data . entries [ uid ] ) {
return ;
}
// Exclude uid and gather the rest of the properties
2024-07-11 23:23:26 +02:00
const originalData = Object . assign ( { } , data . entries [ uid ] ) ;
delete originalData . uid ;
2024-04-27 06:18:26 +02:00
// Create new entry and copy over data
const entry = createWorldInfoEntry ( data . name , data ) ;
Object . assign ( entry , originalData ) ;
return entry ;
}
/ * *
* Deletes a WI entry , with a user confirmation dialog
* @ param { * [ ] } data - The data of the book
* @ param { number } uid - The uid of the entry to copy in this book
2024-08-02 20:51:12 +02:00
* @ param { object } [ options = { } ] - Optional arguments
* @ param { boolean } [ options . silent = false ] - Whether to prompt the user for deletion or just do it
* @ returns { Promise < boolean > } Whether the entry deletion was successful
2024-04-27 06:18:26 +02:00
* /
2024-08-02 20:51:12 +02:00
export async function deleteWorldInfoEntry ( data , uid , { silent = false } = { } ) {
2023-12-02 19:04:51 +01:00
if ( ! data || ! ( 'entries' in data ) ) {
2023-07-20 19:32:15 +02:00
return ;
}
2024-08-02 20:51:12 +02:00
const confirmation = silent || await Popup . show . confirm ( ` Delete the entry with UID: ${ uid } ? ` , 'This action is irreversible!' ) ;
if ( ! confirmation ) {
return false ;
2023-09-05 11:05:20 +02:00
}
2023-07-20 19:32:15 +02:00
delete data . entries [ uid ] ;
2024-08-02 20:51:12 +02:00
return true ;
2023-07-20 19:32:15 +02:00
}
2024-06-17 03:30:52 +02:00
/ * *
* Definitions of types for new WI entries
*
* Use ` newEntryTemplate ` if you just need the template that contains default values
*
* @ type { { [ key : string ] : { default : any , type : string } } }
* /
2024-08-02 20:42:53 +02:00
export const newWorldInfoEntryDefinition = {
2024-06-17 03:30:52 +02:00
key : { default : [ ] , type : 'array' } ,
keysecondary : { default : [ ] , type : 'array' } ,
comment : { default : '' , type : 'string' } ,
content : { default : '' , type : 'string' } ,
constant : { default : false , type : 'boolean' } ,
vectorized : { default : false , type : 'boolean' } ,
selective : { default : true , type : 'boolean' } ,
selectiveLogic : { default : world _info _logic . AND _ANY , type : 'enum' } ,
addMemo : { default : false , type : 'boolean' } ,
order : { default : 100 , type : 'number' } ,
position : { default : 0 , type : 'number' } ,
disable : { default : false , type : 'boolean' } ,
excludeRecursion : { default : false , type : 'boolean' } ,
preventRecursion : { default : false , type : 'boolean' } ,
2024-09-15 22:15:24 +02:00
delayUntilRecursion : { default : 0 , type : 'number' } ,
2024-06-17 03:30:52 +02:00
probability : { default : 100 , type : 'number' } ,
useProbability : { default : true , type : 'boolean' } ,
depth : { default : DEFAULT _DEPTH , type : 'number' } ,
group : { default : '' , type : 'string' } ,
groupOverride : { default : false , type : 'boolean' } ,
groupWeight : { default : DEFAULT _WEIGHT , type : 'number' } ,
scanDepth : { default : null , type : 'number?' } ,
caseSensitive : { default : null , type : 'boolean?' } ,
matchWholeWords : { default : null , type : 'boolean?' } ,
useGroupScoring : { default : null , type : 'boolean?' } ,
automationId : { default : '' , type : 'string' } ,
role : { default : 0 , type : 'enum' } ,
2024-06-23 17:31:40 +02:00
sticky : { default : null , type : 'number?' } ,
cooldown : { default : null , type : 'number?' } ,
2024-06-26 21:43:30 +02:00
delay : { default : null , type : 'number?' } ,
2023-12-01 20:51:49 +01:00
} ;
2024-08-02 20:42:53 +02:00
export const newWorldInfoEntryTemplate = Object . fromEntries (
Object . entries ( newWorldInfoEntryDefinition ) . map ( ( [ key , value ] ) => [ key , value . default ] ) ,
2024-06-17 03:30:52 +02:00
) ;
2024-06-23 17:41:49 +02:00
/ * *
* Creates a new world info entry from template .
* @ param { string } _name Name of the WI ( unused )
* @ param { any } data WI data
* @ returns { object | undefined } New entry object or undefined if failed
* /
2024-08-02 20:42:53 +02:00
export function createWorldInfoEntry ( _name , data ) {
2023-07-20 19:32:15 +02:00
const newUid = getFreeWorldEntryUid ( data ) ;
if ( ! Number . isInteger ( newUid ) ) {
2023-12-02 19:04:51 +01:00
console . error ( 'Couldn\'t assign UID to a new entry' ) ;
2023-07-20 19:32:15 +02:00
return ;
}
2024-08-02 20:42:53 +02:00
const newEntry = { uid : newUid , ... structuredClone ( newWorldInfoEntryTemplate ) } ;
2023-07-20 19:32:15 +02:00
data . entries [ newUid ] = newEntry ;
2023-12-01 00:50:10 +01:00
return newEntry ;
2023-07-20 19:32:15 +02:00
}
async function _save ( name , data ) {
2024-07-22 21:34:53 +02:00
// Prevent double saving if both immediate and debounced save are called
cancelDebounce ( saveWorldDebounced ) ;
2023-12-06 23:09:48 +01:00
await fetch ( '/api/worldinfo/edit' , {
2023-12-02 19:04:51 +01:00
method : 'POST' ,
2023-07-20 19:32:15 +02:00
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { name : name , data : data } ) ,
} ) ;
2024-01-01 15:34:09 +01:00
eventSource . emit ( event _types . WORLDINFO _UPDATED , name , data ) ;
2023-07-20 19:32:15 +02:00
}
2024-08-02 20:42:53 +02:00
/ * *
* Saves the world info
*
* This will also refresh the ` worldInfoCache ` .
* Note , for performance reasons the saved cache will not make a deep clone of the data .
* It is your responsibility to not modify the saved data object after calling this function , or there will be data inconsistencies .
* Call ` loadWorldInfoData ` or query directly from cache if you need the object again .
*
* @ param { string } name - The name of the world info
* @ param { any } data - The data to be saved
* @ param { boolean } [ immediately = false ] - Whether to save immediately or use debouncing
* @ return { Promise < void > } A promise that resolves when the world info is saved
* /
export async function saveWorldInfo ( name , data , immediately = false ) {
2023-07-20 19:32:15 +02:00
if ( ! name || ! data ) {
return ;
}
2024-07-22 03:17:06 +02:00
// Update cache immediately, so any future call can pull from this
2024-07-22 09:11:37 +02:00
worldInfoCache . set ( name , data ) ;
2023-07-20 19:32:15 +02:00
if ( immediately ) {
return await _save ( name , data ) ;
}
saveWorldDebounced ( name , data ) ;
}
async function renameWorldInfo ( name , data ) {
const oldName = name ;
2024-06-26 05:29:08 +02:00
const newName = await Popup . show . input ( 'Rename World Info' , 'Enter a new name:' , oldName ) ;
2023-07-20 19:32:15 +02:00
if ( oldName === newName || ! newName ) {
2023-12-02 19:04:51 +01:00
console . debug ( 'World info rename cancelled' ) ;
2023-07-20 19:32:15 +02:00
return ;
}
const entryPreviouslySelected = selected _world _info . findIndex ( ( e ) => e === oldName ) ;
await saveWorldInfo ( newName , data , true ) ;
await deleteWorldInfo ( oldName ) ;
const existingCharLores = world _info . charLore ? . filter ( ( e ) => e . extraBooks . includes ( oldName ) ) ;
if ( existingCharLores && existingCharLores . length > 0 ) {
existingCharLores . forEach ( ( charLore ) => {
const tempCharLore = charLore . extraBooks . filter ( ( e ) => e !== oldName ) ;
tempCharLore . push ( newName ) ;
charLore . extraBooks = tempCharLore ;
} ) ;
saveSettingsDebounced ( ) ;
}
if ( entryPreviouslySelected !== - 1 ) {
const wiElement = getWIElement ( newName ) ;
2023-12-02 19:04:51 +01:00
wiElement . prop ( 'selected' , true ) ;
$ ( '#world_info' ) . trigger ( 'change' ) ;
2023-07-20 19:32:15 +02:00
}
const selectedIndex = world _names . indexOf ( newName ) ;
if ( selectedIndex !== - 1 ) {
$ ( '#world_editor_select' ) . val ( selectedIndex ) . trigger ( 'change' ) ;
}
}
2024-05-22 18:19:01 +02:00
/ * *
* Deletes a world info with the given name
*
* @ param { string } worldInfoName - The name of the world info to delete
* @ returns { Promise < boolean > } A promise that resolves to true if the world info was successfully deleted , false otherwise
* /
2024-08-02 20:42:53 +02:00
export async function deleteWorldInfo ( worldInfoName ) {
2023-07-20 19:32:15 +02:00
if ( ! world _names . includes ( worldInfoName ) ) {
2024-05-22 18:19:01 +02:00
return false ;
2023-07-20 19:32:15 +02:00
}
2023-12-06 23:09:48 +01:00
const response = await fetch ( '/api/worldinfo/delete' , {
2023-12-02 19:04:51 +01:00
method : 'POST' ,
2023-07-20 19:32:15 +02:00
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { name : worldInfoName } ) ,
} ) ;
2024-05-22 18:19:01 +02:00
if ( ! response . ok ) {
return false ;
}
2023-07-20 19:32:15 +02:00
2024-05-22 18:19:01 +02:00
const existingWorldIndex = selected _world _info . findIndex ( ( e ) => e === worldInfoName ) ;
if ( existingWorldIndex !== - 1 ) {
selected _world _info . splice ( existingWorldIndex , 1 ) ;
saveSettingsDebounced ( ) ;
}
2023-07-20 19:32:15 +02:00
2024-05-22 18:19:01 +02:00
await updateWorldInfoList ( ) ;
$ ( '#world_editor_select' ) . trigger ( 'change' ) ;
if ( $ ( '#character_world' ) . val ( ) === worldInfoName ) {
$ ( '#character_world' ) . val ( '' ) . trigger ( 'change' ) ;
setWorldInfoButtonClass ( undefined , false ) ;
if ( menu _type != 'create' ) {
saveCharacterDebounced ( ) ;
2023-07-20 19:32:15 +02:00
}
}
2024-05-22 18:19:01 +02:00
return true ;
2023-07-20 19:32:15 +02:00
}
2024-08-02 20:42:53 +02:00
export function getFreeWorldEntryUid ( data ) {
2023-12-02 19:04:51 +01:00
if ( ! data || ! ( 'entries' in data ) ) {
2023-07-20 19:32:15 +02:00
return null ;
}
const MAX _UID = 1_000_000 ; // <- should be safe enough :)
for ( let uid = 0 ; uid < MAX _UID ; uid ++ ) {
if ( uid in data . entries ) {
continue ;
}
return uid ;
}
return null ;
}
2024-08-02 20:42:53 +02:00
export function getFreeWorldName ( ) {
2023-07-20 19:32:15 +02:00
const MAX _FREE _NAME = 100_000 ;
for ( let index = 1 ; index < MAX _FREE _NAME ; index ++ ) {
const newName = ` New World ( ${ index } ) ` ;
if ( world _names . includes ( newName ) ) {
continue ;
}
return newName ;
}
return undefined ;
}
2024-05-22 18:19:01 +02:00
/ * *
* Creates a new world info / lorebook with the given name .
* Checks if a world with the same name already exists , providing a warning or optionally a user confirmation dialog .
*
2024-05-22 23:52:35 +02:00
* @ param { string } worldName - The name of the new world info
2024-05-22 18:19:01 +02:00
* @ param { Object } options - Optional parameters
* @ param { boolean } [ options . interactive = false ] - Whether to show a confirmation dialog when overwriting an existing world
* @ returns { Promise < boolean > } - True if the world info was successfully created , false otherwise
* /
2024-08-02 20:42:53 +02:00
export async function createNewWorldInfo ( worldName , { interactive = false } = { } ) {
2023-07-20 19:32:15 +02:00
const worldInfoTemplate = { entries : { } } ;
2024-05-22 23:52:35 +02:00
if ( ! worldName ) {
2024-05-22 18:19:01 +02:00
return false ;
}
2024-05-23 00:39:49 +02:00
const sanitizedWorldName = await getSanitizedFilename ( worldName ) ;
const allowed = await checkOverwriteExistingData ( 'World Info' , world _names , sanitizedWorldName , { interactive : interactive , actionName : 'Create' , deleteAction : ( existingName ) => deleteWorldInfo ( existingName ) } ) ;
2024-05-22 21:11:39 +02:00
if ( ! allowed ) {
return false ;
2023-07-20 19:32:15 +02:00
}
2024-05-22 23:52:35 +02:00
await saveWorldInfo ( worldName , worldInfoTemplate , true ) ;
2023-07-20 19:32:15 +02:00
await updateWorldInfoList ( ) ;
2024-05-22 23:52:35 +02:00
const selectedIndex = world _names . indexOf ( worldName ) ;
2023-07-20 19:32:15 +02:00
if ( selectedIndex !== - 1 ) {
$ ( '#world_editor_select' ) . val ( selectedIndex ) . trigger ( 'change' ) ;
} else {
hideWorldEditor ( ) ;
}
2024-05-22 18:19:01 +02:00
return true ;
2023-07-20 19:32:15 +02:00
}
async function getCharacterLore ( ) {
const character = characters [ this _chid ] ;
const name = character ? . name ;
2024-07-05 20:49:17 +02:00
/** @type {Set<string>} */
2023-07-20 19:32:15 +02:00
let worldsToSearch = new Set ( ) ;
const baseWorldName = character ? . data ? . extensions ? . world ;
if ( baseWorldName ) {
worldsToSearch . add ( baseWorldName ) ;
}
// TODO: Maybe make the utility function not use the window context?
const fileName = getCharaFilename ( this _chid ) ;
const extraCharLore = world _info . charLore ? . find ( ( e ) => e . name === fileName ) ;
if ( extraCharLore ) {
worldsToSearch = new Set ( [ ... worldsToSearch , ... extraCharLore . extraBooks ] ) ;
}
2024-07-05 20:49:17 +02:00
if ( ! worldsToSearch . size ) {
return [ ] ;
}
2023-07-20 19:32:15 +02:00
let entries = [ ] ;
for ( const worldName of worldsToSearch ) {
if ( selected _world _info . includes ( worldName ) ) {
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Character ${ name } 's world ${ worldName } is already activated in global world info! Skipping... ` ) ;
2023-07-20 19:32:15 +02:00
continue ;
}
2023-10-16 22:13:32 +02:00
if ( chat _metadata [ METADATA _KEY ] === worldName ) {
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Character ${ name } 's world ${ worldName } is already activated in chat lore! Skipping... ` ) ;
2023-10-16 22:13:32 +02:00
continue ;
}
2024-08-02 20:42:53 +02:00
const data = await loadWorldInfo ( worldName ) ;
2024-07-05 20:50:30 +02:00
const newEntries = data ? Object . keys ( data . entries ) . map ( ( x ) => data . entries [ x ] ) . map ( ( { uid , ... rest } ) => ( { uid , world : worldName , ... rest } ) ) : [ ] ;
2023-07-20 19:32:15 +02:00
entries = entries . concat ( newEntries ) ;
2024-07-05 20:49:17 +02:00
if ( ! newEntries . length ) {
console . debug ( ` [WI] Character ${ name } 's world ${ worldName } could not be found or is empty ` ) ;
}
2023-07-20 19:32:15 +02:00
}
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Character ${ name } 's lore has ${ entries . length } world info entries ` , [ ... worldsToSearch ] ) ;
2023-07-20 19:32:15 +02:00
return entries ;
}
async function getGlobalLore ( ) {
2024-07-05 20:49:17 +02:00
if ( ! selected _world _info ? . length ) {
2023-07-20 19:32:15 +02:00
return [ ] ;
}
let entries = [ ] ;
for ( const worldName of selected _world _info ) {
2024-08-02 20:42:53 +02:00
const data = await loadWorldInfo ( worldName ) ;
2024-07-05 20:50:30 +02:00
const newEntries = data ? Object . keys ( data . entries ) . map ( ( x ) => data . entries [ x ] ) . map ( ( { uid , ... rest } ) => ( { uid , world : worldName , ... rest } ) ) : [ ] ;
2023-07-20 19:32:15 +02:00
entries = entries . concat ( newEntries ) ;
}
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Global world info has ${ entries . length } entries ` , selected _world _info ) ;
2023-07-20 19:32:15 +02:00
return entries ;
}
2023-10-16 22:03:42 +02:00
async function getChatLore ( ) {
const chatWorld = chat _metadata [ METADATA _KEY ] ;
if ( ! chatWorld ) {
return [ ] ;
}
2023-10-16 22:13:32 +02:00
if ( selected _world _info . includes ( chatWorld ) ) {
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Chat world ${ chatWorld } is already activated in global world info! Skipping... ` ) ;
2023-10-16 22:13:32 +02:00
return [ ] ;
}
2024-08-02 20:42:53 +02:00
const data = await loadWorldInfo ( chatWorld ) ;
2024-07-05 20:50:30 +02:00
const entries = data ? Object . keys ( data . entries ) . map ( ( x ) => data . entries [ x ] ) . map ( ( { uid , ... rest } ) => ( { uid , world : chatWorld , ... rest } ) ) : [ ] ;
2023-10-16 22:03:42 +02:00
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Chat lore has ${ entries . length } entries ` , [ chatWorld ] ) ;
2023-10-16 22:03:42 +02:00
return entries ;
}
2024-04-23 02:09:52 +02:00
export async function getSortedEntries ( ) {
2023-07-20 19:32:15 +02:00
try {
const globalLore = await getGlobalLore ( ) ;
const characterLore = await getCharacterLore ( ) ;
2023-10-16 22:03:42 +02:00
const chatLore = await getChatLore ( ) ;
2023-07-20 19:32:15 +02:00
let entries ;
switch ( Number ( world _info _character _strategy ) ) {
case world _info _insertion _strategy . evenly :
entries = [ ... globalLore , ... characterLore ] . sort ( sortFn ) ;
break ;
case world _info _insertion _strategy . character _first :
entries = [ ... characterLore . sort ( sortFn ) , ... globalLore . sort ( sortFn ) ] ;
break ;
case world _info _insertion _strategy . global _first :
entries = [ ... globalLore . sort ( sortFn ) , ... characterLore . sort ( sortFn ) ] ;
break ;
default :
2024-07-05 20:49:17 +02:00
console . error ( '[WI] Unknown WI insertion strategy:' , world _info _character _strategy , 'defaulting to evenly' ) ;
2023-07-20 19:32:15 +02:00
entries = [ ... globalLore , ... characterLore ] . sort ( sortFn ) ;
break ;
}
2023-10-16 22:03:42 +02:00
// Chat lore always goes first
entries = [ ... chatLore . sort ( sortFn ) , ... entries ] ;
2024-09-13 09:52:35 +02:00
// Calculate hash and parse decorators. Split maps to preserve old hashes.
2024-07-14 22:04:54 +02:00
entries = entries . map ( ( entry ) => {
2024-07-30 16:20:10 +02:00
const [ decorators , content ] = parseDecorators ( entry . content || '' ) ;
2024-09-13 09:52:35 +02:00
return { ... entry , decorators , content } ;
} ) . map ( ( entry ) => {
const hash = getStringHash ( JSON . stringify ( entry ) ) ;
return { ... entry , hash } ;
2024-07-14 22:04:54 +02:00
} ) ;
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Found ${ entries . length } world lore entries. Sorted by strategy ` , Object . entries ( world _info _insertion _strategy ) . find ( ( x ) => x [ 1 ] === world _info _character _strategy ) ) ;
2023-07-20 19:32:15 +02:00
// Need to deep clone the entries to avoid modifying the cached data
2023-09-26 08:53:04 +02:00
return structuredClone ( entries ) ;
2023-07-20 19:32:15 +02:00
}
catch ( e ) {
console . error ( e ) ;
return [ ] ;
}
}
2024-07-11 08:11:35 +02:00
/ * *
* Parse decorators from worldinfo content
* @ param { string } content The content to parse
* @ returns { [ string [ ] , string ] } The decorators found in the content and the content without decorators
* /
2024-07-14 13:07:23 +02:00
function parseDecorators ( content ) {
2024-07-11 08:11:35 +02:00
/ * *
* Check if the decorator is known
* @ param { string } data string to check
* @ returns { boolean } true if the decorator is known
* /
const isKnownDecorator = ( data ) => {
2024-07-14 13:07:23 +02:00
if ( data . startsWith ( '@@@' ) ) {
data = data . substring ( 1 ) ;
2024-07-11 08:11:35 +02:00
}
2024-07-14 13:07:23 +02:00
for ( let i = 0 ; i < KNOWN _DECORATORS . length ; i ++ ) {
if ( data . startsWith ( KNOWN _DECORATORS [ i ] ) ) {
return true ;
2024-07-11 08:11:35 +02:00
}
}
2024-07-14 13:07:23 +02:00
return false ;
} ;
2024-07-11 08:11:35 +02:00
2024-07-14 13:07:23 +02:00
if ( content . startsWith ( '@@' ) ) {
2024-07-11 08:11:35 +02:00
let newContent = content ;
const splited = content . split ( '\n' ) ;
2024-07-14 13:07:23 +02:00
let decorators = [ ] ;
2024-07-11 08:11:35 +02:00
let fallbacked = false ;
for ( let i = 0 ; i < splited . length ; i ++ ) {
2024-07-14 13:07:23 +02:00
if ( splited [ i ] . startsWith ( '@@' ) ) {
if ( splited [ i ] . startsWith ( '@@@' ) && ! fallbacked ) {
continue ;
2024-07-11 08:11:35 +02:00
}
2024-07-14 13:07:23 +02:00
if ( isKnownDecorator ( splited [ i ] ) ) {
decorators . push ( splited [ i ] . startsWith ( '@@@' ) ? splited [ i ] . substring ( 1 ) : splited [ i ] ) ;
fallbacked = false ;
2024-07-11 08:11:35 +02:00
}
2024-07-14 13:07:23 +02:00
else {
fallbacked = true ;
2024-07-11 08:11:35 +02:00
}
} else {
newContent = splited . slice ( i ) . join ( '\n' ) ;
break ;
}
}
2024-07-14 13:07:23 +02:00
return [ decorators , newContent ] ;
2024-07-11 08:11:35 +02:00
}
2024-07-14 13:07:23 +02:00
return [ [ ] , content ] ;
2024-07-11 08:11:35 +02:00
}
2024-02-24 16:22:51 +01:00
/ * *
* Performs a scan on the chat and returns the world info activated .
2024-07-06 03:23:02 +02:00
* @ param { string [ ] } chat The chat messages to scan , in reverse order .
2024-02-24 16:22:51 +01:00
* @ param { number } maxContext The maximum context size of the generation .
2024-06-20 23:53:00 +02:00
* @ param { boolean } isDryRun Whether to perform a dry run .
2024-10-10 15:12:19 +02:00
* @ typedef { { worldInfoBefore : string , worldInfoAfter : string , EMEntries : any [ ] , WIDepthEntries : any [ ] , allActivatedEntries : Set < any > } } WIActivated
2024-02-24 16:22:51 +01:00
* @ returns { Promise < WIActivated > } The world info activated .
* /
2024-08-02 20:42:53 +02:00
export async function checkWorldInfo ( chat , maxContext , isDryRun ) {
2023-07-20 19:32:15 +02:00
const context = getContext ( ) ;
2024-01-23 21:44:20 +01:00
const buffer = new WorldInfoBuffer ( chat ) ;
2023-10-12 05:44:52 +02:00
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] --- START WI SCAN (on ${ chat . length } messages) --- ` ) ;
2023-10-12 05:44:52 +02:00
// Combine the chat
// Add the depth or AN if enabled
// Put this code here since otherwise, the chat reference is modified
2023-12-11 21:47:26 +01:00
for ( const key of Object . keys ( context . extensionPrompts ) ) {
if ( context . extensionPrompts [ key ] ? . scan ) {
const prompt = getExtensionPromptByName ( key ) ;
if ( prompt ) {
2024-07-03 23:28:34 +02:00
buffer . addInject ( prompt ) ;
2023-10-25 23:39:11 +02:00
}
2023-10-12 05:44:52 +02:00
}
}
2024-09-15 16:56:21 +02:00
/** @type {scan_state} */
2024-07-03 23:18:46 +02:00
let scanState = scan _state . INITIAL ;
2023-11-01 18:02:38 +01:00
let token _budget _overflowed = false ;
2023-07-20 19:32:15 +02:00
let count = 0 ;
2024-10-10 14:52:54 +02:00
let allActivatedEntries = new Map ( ) ;
2023-07-20 19:32:15 +02:00
let failedProbabilityChecks = new Set ( ) ;
let allActivatedText = '' ;
2023-08-10 19:45:57 +02:00
let budget = Math . round ( world _info _budget * maxContext / 100 ) || 1 ;
if ( world _info _budget _cap > 0 && budget > world _info _budget _cap ) {
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Budget ${ budget } exceeds cap ${ world _info _budget _cap } , using cap ` ) ;
2023-08-10 19:45:57 +02:00
budget = world _info _budget _cap ;
}
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Context size: ${ maxContext } ; WI budget: ${ budget } (max% = ${ world _info _budget } %, cap = ${ world _info _budget _cap } ) ` ) ;
2023-07-20 19:32:15 +02:00
const sortedEntries = await getSortedEntries ( ) ;
2024-06-23 20:18:18 +02:00
const timedEffects = new WorldInfoTimedEffects ( chat , sortedEntries ) ;
2023-07-20 19:32:15 +02:00
2024-06-23 20:18:18 +02:00
! isDryRun && timedEffects . checkTimedEffects ( ) ;
2024-06-20 23:53:00 +02:00
2023-07-20 19:32:15 +02:00
if ( sortedEntries . length === 0 ) {
2024-10-10 15:12:19 +02:00
return { worldInfoBefore : '' , worldInfoAfter : '' , WIDepthEntries : [ ] , EMEntries : [ ] , allActivatedEntries : new Set ( ) } ;
2023-07-20 19:32:15 +02:00
}
2024-09-15 16:56:21 +02:00
/** @type {number[]} Represents the delay levels for entries that are delayed until recursion */
const availableRecursionDelayLevels = [ ... new Set ( sortedEntries
. filter ( entry => entry . delayUntilRecursion )
2024-09-15 22:31:29 +02:00
. map ( entry => entry . delayUntilRecursion === true ? 1 : entry . delayUntilRecursion ) ,
2024-09-15 16:56:21 +02:00
) ] . sort ( ( a , b ) => a - b ) ;
// Already preset with the first level
let currentRecursionDelayLevel = availableRecursionDelayLevels . shift ( ) ? ? 0 ;
2024-09-15 20:28:03 +02:00
if ( currentRecursionDelayLevel > 0 && availableRecursionDelayLevels . length ) {
console . debug ( '[WI] Preparing first delayed recursion level' , currentRecursionDelayLevel , '. Still delayed:' , availableRecursionDelayLevels ) ;
}
2024-09-15 16:56:21 +02:00
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] --- SEARCHING ENTRIES (on ${ sortedEntries . length } entries) --- ` ) ;
2024-07-03 22:42:27 +02:00
while ( scanState ) {
2024-08-23 20:34:18 +02:00
//if world_info_max_recursion_steps is non-zero min activations are disabled, and vice versa
if ( world _info _max _recursion _steps && world _info _max _recursion _steps <= count ) {
console . debug ( '[WI] Search stopped by reaching max recursion steps' , world _info _max _recursion _steps ) ;
break ;
}
2024-07-03 23:18:46 +02:00
// Track how many times the loop has run. May be useful for debugging.
2023-07-20 19:32:15 +02:00
count ++ ;
2024-09-15 20:28:03 +02:00
console . debug ( ` [WI] --- LOOP # ${ count } START --- ` ) ;
2024-09-15 22:31:29 +02:00
console . debug ( '[WI] Scan state' , Object . entries ( scan _state ) . find ( x => x [ 1 ] === scanState ) ) ;
2024-07-05 20:49:17 +02:00
2024-07-06 02:35:41 +02:00
// Until decided otherwise, we set the loop to stop scanning after this
let nextScanState = scan _state . NONE ;
// Loop and find all entries that can activate here
2023-07-20 19:32:15 +02:00
let activatedNow = new Set ( ) ;
for ( let entry of sortedEntries ) {
2024-07-05 22:50:20 +02:00
// Logging preparation
let headerLogged = false ;
function log ( ... args ) {
if ( ! headerLogged ) {
console . debug ( ` [WI] Entry ${ entry . uid } ` , ` from ' ${ entry . world } ' processing ` , entry ) ;
headerLogged = true ;
}
console . debug ( ` [WI] Entry ${ entry . uid } ` , ... args ) ;
}
2024-07-11 08:11:35 +02:00
2024-07-05 22:50:20 +02:00
// Already processed, considered and then skipped entries should still be skipped
2024-10-10 14:52:54 +02:00
if ( failedProbabilityChecks . has ( entry ) || allActivatedEntries . has ( ` ${ entry . world } . ${ entry . uid } ` ) ) {
2024-07-05 22:50:20 +02:00
continue ;
}
2024-07-05 20:49:17 +02:00
if ( entry . disable == true ) {
2024-07-05 23:42:49 +02:00
log ( 'disabled' ) ;
2024-07-05 20:49:17 +02:00
continue ;
}
2024-07-11 23:42:06 +02:00
2023-09-23 21:35:51 +02:00
// Check if this entry applies to the character or if it's excluded
2023-11-14 22:54:08 +01:00
if ( entry . characterFilter && entry . characterFilter ? . names ? . length > 0 ) {
2023-09-23 21:35:51 +02:00
const nameIncluded = entry . characterFilter . names . includes ( getCharaFilename ( ) ) ;
2023-12-02 20:11:06 +01:00
const filtered = entry . characterFilter . isExclude ? nameIncluded : ! nameIncluded ;
2023-09-23 21:35:51 +02:00
if ( filtered ) {
2024-07-05 23:42:49 +02:00
log ( 'filtered out by character' ) ;
2023-09-23 21:35:51 +02:00
continue ;
}
}
2023-11-14 22:54:08 +01:00
if ( entry . characterFilter && entry . characterFilter ? . tags ? . length > 0 ) {
2024-03-07 23:48:50 +01:00
const tagKey = getTagKeyForEntity ( this _chid ) ;
2023-11-14 22:54:08 +01:00
if ( tagKey ) {
const tagMapEntry = context . tagMap [ tagKey ] ;
if ( Array . isArray ( tagMapEntry ) ) {
// If tag map intersects with the tag exclusion list, skip
const includesTag = tagMapEntry . some ( ( tag ) => entry . characterFilter . tags . includes ( tag ) ) ;
const filtered = entry . characterFilter . isExclude ? includesTag : ! includesTag ;
if ( filtered ) {
2024-07-05 23:42:49 +02:00
log ( 'filtered out by tag' ) ;
2023-11-14 22:54:08 +01:00
continue ;
}
}
}
}
2024-06-23 20:18:18 +02:00
const isSticky = timedEffects . isEffectActive ( 'sticky' , entry ) ;
const isCooldown = timedEffects . isEffectActive ( 'cooldown' , entry ) ;
2024-06-26 21:43:30 +02:00
const isDelay = timedEffects . isEffectActive ( 'delay' , entry ) ;
if ( isDelay ) {
2024-07-05 23:42:49 +02:00
log ( 'suppressed by delay' ) ;
2024-06-26 21:43:30 +02:00
continue ;
}
2024-06-21 01:42:15 +02:00
if ( isCooldown && ! isSticky ) {
2024-07-05 23:42:49 +02:00
log ( 'suppressed by cooldown' ) ;
2024-06-20 23:53:00 +02:00
continue ;
}
2024-07-03 22:42:27 +02:00
// Only use checks for recursion flags if the scan step was activated by recursion
2024-09-04 22:39:32 +02:00
if ( scanState !== scan _state . RECURSION && entry . delayUntilRecursion && ! isSticky ) {
2024-07-05 23:42:49 +02:00
log ( 'suppressed by delay until recursion' ) ;
2023-07-20 19:32:15 +02:00
continue ;
}
2024-09-15 16:56:21 +02:00
if ( scanState === scan _state . RECURSION && entry . delayUntilRecursion && entry . delayUntilRecursion > currentRecursionDelayLevel && ! isSticky ) {
2024-09-15 20:28:03 +02:00
log ( 'suppressed by delay until recursion level' , entry . delayUntilRecursion , '. Currently' , currentRecursionDelayLevel ) ;
2024-09-15 16:56:21 +02:00
continue ;
}
2024-09-04 22:39:32 +02:00
if ( scanState === scan _state . RECURSION && world _info _recursive && entry . excludeRecursion && ! isSticky ) {
2024-07-05 23:42:49 +02:00
log ( 'suppressed by exclude recursion' ) ;
2024-07-03 22:42:27 +02:00
continue ;
}
2024-07-14 22:04:54 +02:00
if ( entry . decorators . includes ( '@@activate' ) ) {
log ( 'activated by @@activate decorator' ) ;
activatedNow . add ( entry ) ;
2024-07-03 23:18:46 +02:00
continue ;
}
2024-07-14 22:04:54 +02:00
if ( entry . decorators . includes ( '@@dont_activate' ) ) {
log ( 'suppressed by @@dont_activate decorator' ) ;
2023-07-20 19:32:15 +02:00
continue ;
}
2024-10-10 17:03:26 +02:00
if ( buffer . getExternallyActivated ( entry ) ) {
log ( 'externally activated' ) ;
activatedNow . add ( buffer . getExternallyActivated ( entry ) ) ;
continue ;
}
2024-07-05 20:49:17 +02:00
// Now do checks for immediate activations
if ( entry . constant ) {
2024-07-05 23:42:49 +02:00
log ( 'activated because of constant' ) ;
2024-07-11 23:43:19 +02:00
activatedNow . add ( entry ) ;
continue ;
}
2024-07-05 20:49:17 +02:00
if ( isSticky ) {
2024-07-05 23:42:49 +02:00
log ( 'activated because active sticky' ) ;
2023-07-20 19:32:15 +02:00
activatedNow . add ( entry ) ;
continue ;
}
2024-07-05 20:49:17 +02:00
if ( ! Array . isArray ( entry . key ) || ! entry . key . length ) {
2024-07-05 23:42:49 +02:00
log ( 'has no keys defined, skipped' ) ;
2024-07-05 20:49:17 +02:00
continue ;
}
2024-07-11 08:11:35 +02:00
2024-07-05 20:52:24 +02:00
// Cache the text to scan before the loop, it won't change its content
const textToScan = buffer . get ( entry , scanState ) ;
2023-12-05 09:56:52 +01:00
2024-07-06 01:53:26 +02:00
// PRIMARY KEYWORDS
let primaryKeyMatch = entry . key . find ( key => {
2024-07-05 20:49:17 +02:00
const substituted = substituteParams ( key ) ;
2024-07-06 01:53:26 +02:00
return substituted && buffer . matchKeys ( textToScan , substituted . trim ( ) , entry ) ;
} ) ;
2024-01-01 20:49:54 +01:00
2024-07-06 01:53:26 +02:00
if ( ! primaryKeyMatch ) {
// Don't write logs for simple no-matches
continue ;
}
2024-07-05 20:49:17 +02:00
2024-07-06 01:53:26 +02:00
const hasSecondaryKeywords = (
entry . selective && //all entries are selective now
Array . isArray ( entry . keysecondary ) && //always true
entry . keysecondary . length //ignore empties
) ;
2024-07-05 20:49:17 +02:00
2024-07-06 01:53:26 +02:00
if ( ! hasSecondaryKeywords ) {
// Handle cases where secondary is empty
log ( 'activated by primary key match' , primaryKeyMatch ) ;
activatedNow . add ( entry ) ;
continue ;
}
2024-07-05 20:49:17 +02:00
2024-07-06 01:53:26 +02:00
// SECONDARY KEYWORDS
const selectiveLogic = entry . selectiveLogic ? ? 0 ; // If selectiveLogic isn't found, assume it's AND, only do this once per entry
log ( 'Entry with primary key match' , primaryKeyMatch , 'has secondary keywords. Checking with logic logic' , Object . entries ( world _info _logic ) . find ( x => x [ 1 ] === entry . selectiveLogic ) ) ;
/** @type {() => boolean} */
function matchSecondaryKeys ( ) {
let hasAnyMatch = false ;
let hasAllMatch = true ;
for ( let keysecondary of entry . keysecondary ) {
const secondarySubstituted = substituteParams ( keysecondary ) ;
const hasSecondaryMatch = secondarySubstituted && buffer . matchKeys ( textToScan , secondarySubstituted . trim ( ) , entry ) ;
if ( hasSecondaryMatch ) hasAnyMatch = true ;
if ( ! hasSecondaryMatch ) hasAllMatch = false ;
// Simplified AND ANY / NOT ALL if statement. (Proper fix for PR#1356 by Bronya)
// If AND ANY logic and the main checks pass OR if NOT ALL logic and the main checks do not pass
if ( selectiveLogic === world _info _logic . AND _ANY && hasSecondaryMatch ) {
log ( 'activated. (AND ANY) Found match secondary keyword' , secondarySubstituted ) ;
return true ;
}
if ( selectiveLogic === world _info _logic . NOT _ALL && ! hasSecondaryMatch ) {
log ( 'activated. (NOT ALL) Found not matching secondary keyword' , secondarySubstituted ) ;
return true ;
2024-04-04 08:08:17 +02:00
}
2023-11-15 12:11:09 +01:00
}
2024-07-06 01:53:26 +02:00
// Handle NOT ANY logic
if ( selectiveLogic === world _info _logic . NOT _ANY && ! hasAnyMatch ) {
log ( 'activated. (NOT ANY) No secondary keywords found' , entry . keysecondary ) ;
return true ;
}
// Handle AND ALL logic
if ( selectiveLogic === world _info _logic . AND _ALL && hasAllMatch ) {
log ( 'activated. (AND ALL) All secondary keywords found' , entry . keysecondary ) ;
return true ;
}
return false ;
2023-07-20 19:32:15 +02:00
}
2024-07-06 01:53:26 +02:00
const matched = matchSecondaryKeys ( ) ;
if ( ! matched ) {
log ( 'skipped. Secondary keywords not satisfied' , entry . keysecondary ) ;
continue ;
}
// Success logging was already done inside the function, so just add the entry
activatedNow . add ( entry ) ;
continue ;
2023-07-20 19:32:15 +02:00
}
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Search done. Found ${ activatedNow . size } possible entries. ` ) ;
2024-09-03 21:14:44 +02:00
// Sort the entries for the probability and the budget limit checks
2023-07-20 19:32:15 +02:00
const newEntries = [ ... activatedNow ]
2024-09-03 21:14:44 +02:00
. sort ( ( a , b ) => {
const isASticky = timedEffects . isEffectActive ( 'sticky' , a ) ? 1 : 0 ;
const isBSticky = timedEffects . isEffectActive ( 'sticky' , b ) ? 1 : 0 ;
return isBSticky - isASticky || sortedEntries . indexOf ( a ) - sortedEntries . indexOf ( b ) ;
} ) ;
2023-12-02 19:04:51 +01:00
let newContent = '' ;
2024-04-13 20:33:19 +02:00
const textToScanTokens = await getTokenCountAsync ( allActivatedText ) ;
2023-12-07 19:06:06 +01:00
2024-09-05 20:15:45 +02:00
filterByInclusionGroups ( newEntries , allActivatedEntries , buffer , scanState , timedEffects ) ;
2023-12-07 19:06:06 +01:00
2024-07-05 20:49:17 +02:00
console . debug ( '[WI] --- PROBABILITY CHECKS ---' ) ;
2024-09-15 20:28:03 +02:00
! newEntries . length && console . debug ( '[WI] No probability checks to do' ) ;
2023-07-20 19:32:15 +02:00
for ( const entry of newEntries ) {
2024-07-05 21:59:31 +02:00
function verifyProbability ( ) {
// If we don't need to roll, it's always true
if ( ! entry . useProbability || entry . probability === 100 ) {
console . debug ( ` WI entry ${ entry . uid } does not use probability ` ) ;
return true ;
}
2023-07-20 19:32:15 +02:00
2024-06-23 20:18:18 +02:00
const isSticky = timedEffects . isEffectActive ( 'sticky' , entry ) ;
2024-07-05 21:59:31 +02:00
if ( isSticky ) {
console . debug ( ` WI entry ${ entry . uid } is sticky, does not need to re-roll probability ` ) ;
return true ;
2024-06-22 14:04:49 +02:00
}
2024-07-05 21:59:31 +02:00
const rollValue = Math . random ( ) * 100 ;
if ( rollValue <= entry . probability ) {
console . debug ( ` WI entry ${ entry . uid } passed probability check of ${ entry . probability } % ` ) ;
return true ;
2024-06-22 14:04:49 +02:00
}
2024-07-05 21:59:31 +02:00
failedProbabilityChecks . add ( entry ) ;
return false ;
}
const success = verifyProbability ( ) ;
if ( ! success ) {
console . debug ( ` WI entry ${ entry . uid } failed probability check, removing from activated entries ` , entry ) ;
continue ;
}
2023-07-20 19:32:15 +02:00
2024-05-09 06:37:14 +02:00
// Substitute macros inline, for both this checking and also future processing
2024-07-11 23:42:06 +02:00
entry . content = substituteParams ( entry . content ) ;
2024-05-09 06:37:14 +02:00
newContent += ` ${ entry . content } \n ` ;
2023-07-20 19:32:15 +02:00
2024-04-13 20:33:19 +02:00
if ( ( textToScanTokens + ( await getTokenCountAsync ( newContent ) ) ) >= budget ) {
2024-09-15 20:28:03 +02:00
console . debug ( '[WI] --- BUDGET OVERFLOW CHECK ---' ) ;
2023-07-30 04:15:54 +02:00
if ( world _info _overflow _alert ) {
2024-07-05 21:59:31 +02:00
console . warn ( ` [WI] budget of ${ budget } reached, stopping after ${ allActivatedEntries . size } entries ` ) ;
2023-09-01 22:14:01 +02:00
toastr . warning ( ` World info budget reached after ${ allActivatedEntries . size } entries. ` , 'World Info' ) ;
2024-07-05 21:59:31 +02:00
} else {
console . debug ( ` [WI] budget of ${ budget } reached, stopping after ${ allActivatedEntries . size } entries ` ) ;
2023-07-30 04:15:54 +02:00
}
2023-11-01 18:02:38 +01:00
token _budget _overflowed = true ;
2023-07-20 19:32:15 +02:00
break ;
}
2024-10-10 14:52:54 +02:00
allActivatedEntries . set ( ` ${ entry . world } . ${ entry . uid } ` , entry ) ;
2024-07-06 14:26:45 +02:00
console . debug ( ` [WI] Entry ${ entry . uid } activation successful, adding to prompt ` , entry ) ;
2023-07-20 19:32:15 +02:00
}
2024-07-05 21:59:31 +02:00
const successfulNewEntries = newEntries . filter ( x => ! failedProbabilityChecks . has ( x ) ) ;
2024-07-06 02:35:41 +02:00
const successfulNewEntriesForRecursion = successfulNewEntries . filter ( x => ! x . preventRecursion ) ;
2023-07-20 19:32:15 +02:00
2024-09-15 20:28:03 +02:00
console . debug ( ` [WI] --- LOOP # ${ count } RESULT --- ` ) ;
2024-07-05 21:59:31 +02:00
if ( ! newEntries . length ) {
2024-09-15 20:28:03 +02:00
console . debug ( '[WI] No new entries activated.' ) ;
2024-07-06 02:35:41 +02:00
} else if ( ! successfulNewEntries . length ) {
2024-09-15 20:28:03 +02:00
console . debug ( '[WI] Probability checks failed for all activated entries. No new entries activated.' ) ;
2024-07-06 02:35:41 +02:00
} else {
2024-07-06 14:26:45 +02:00
console . debug ( ` [WI] Successfully activated ${ successfulNewEntries . length } new entries to prompt. ${ allActivatedEntries . size } total entries activated. ` , successfulNewEntries ) ;
2023-07-20 19:32:15 +02:00
}
2024-09-15 20:28:03 +02:00
function logNextState ( ... args ) {
args . length && console . debug ( args . shift ( ) , ... args ) ;
console . debug ( '[WI] Setting scan state' , Object . entries ( scan _state ) . find ( x => x [ 1 ] === scanState ) ) ;
}
2024-07-06 02:44:26 +02:00
// After processing and rolling entries is done, see if we should continue with normal recursion
2024-07-06 02:35:41 +02:00
if ( world _info _recursive && ! token _budget _overflowed && successfulNewEntriesForRecursion . length ) {
nextScanState = scan _state . RECURSION ;
2024-09-15 20:28:03 +02:00
logNextState ( '[WI] Found' , successfulNewEntriesForRecursion . length , 'new entries for recursion' ) ;
2024-01-09 01:34:43 +01:00
}
2024-07-06 02:44:26 +02:00
// If we are inside min activations scan, and we have recursive buffer, we should do a recursive scan before increasing the buffer again
// There might be recurse-trigger-able entries that match the buffer, so we need to check that
if ( world _info _recursive && ! token _budget _overflowed && scanState === scan _state . MIN _ACTIVATIONS && buffer . hasRecurse ( ) ) {
nextScanState = scan _state . RECURSION ;
2024-09-15 20:28:03 +02:00
logNextState ( '[WI] Min Activations run done, whill will always be followed by a recursive scan' ) ;
2023-07-20 19:32:15 +02:00
}
2023-10-30 22:55:32 +01:00
2024-07-06 02:35:41 +02:00
// If scanning is planned to stop, but min activations is set and not satisfied, check if we should continue
const minActivationsNotSatisfied = world _info _min _activations > 0 && ( allActivatedEntries . size < world _info _min _activations ) ;
if ( ! nextScanState && ! token _budget _overflowed && minActivationsNotSatisfied ) {
console . debug ( '[WI] --- MIN ACTIVATIONS CHECK ---' ) ;
2024-04-04 07:56:39 +02:00
2024-07-06 02:35:41 +02:00
let over _max = (
world _info _min _activations _depth _max > 0 &&
buffer . getDepth ( ) > world _info _min _activations _depth _max
) || ( buffer . getDepth ( ) > chat . length ) ;
2024-04-04 07:56:39 +02:00
2024-07-06 02:35:41 +02:00
if ( ! over _max ) {
nextScanState = scan _state . MIN _ACTIVATIONS ; // loop
2024-09-15 20:28:03 +02:00
logNextState ( ` [WI] Min activations not reached ( ${ allActivatedEntries . size } / ${ world _info _min _activations } ), advancing depth to ${ buffer . getDepth ( ) + 1 } , starting another scan ` ) ;
2024-07-06 20:00:27 +02:00
buffer . advanceScan ( ) ;
2024-07-06 02:35:41 +02:00
} else {
console . debug ( ` [WI] Min activations not reached ( ${ allActivatedEntries . size } / ${ world _info _min _activations } ), but reached on of depth. Stopping ` ) ;
2023-10-30 22:55:32 +01:00
}
}
2024-07-06 02:35:41 +02:00
2024-09-15 16:56:21 +02:00
// If the scan is done, but we still have open "delay until recursion" levels, we should continue with the next one
if ( nextScanState === scan _state . NONE && availableRecursionDelayLevels . length ) {
2024-09-15 17:02:48 +02:00
nextScanState = scan _state . RECURSION ;
2024-09-15 16:56:21 +02:00
currentRecursionDelayLevel = availableRecursionDelayLevels . shift ( ) ;
2024-09-15 20:28:03 +02:00
logNextState ( '[WI] Open delayed recursion levels left. Preparing next delayed recursion level' , currentRecursionDelayLevel , '. Still delayed:' , availableRecursionDelayLevels ) ;
2024-09-15 16:56:21 +02:00
}
2024-07-06 02:35:41 +02:00
// Final check if we should really continue scan, and extend the current WI recurse buffer
scanState = nextScanState ;
if ( scanState ) {
const text = successfulNewEntriesForRecursion
. map ( x => x . content ) . join ( '\n' ) ;
buffer . addRecurse ( text ) ;
allActivatedText = ( text + '\n' + allActivatedText ) ;
2024-09-15 20:28:03 +02:00
} else {
logNextState ( '[WI] Scan done. No new entries to prompt. Stopping.' ) ;
2024-07-06 02:35:41 +02:00
}
2023-07-20 19:32:15 +02:00
}
2024-07-05 20:49:17 +02:00
console . debug ( '[WI] --- BUILDING PROMPT ---' ) ;
2023-07-20 19:32:15 +02:00
// Forward-sorted list of entries for joining
const WIBeforeEntries = [ ] ;
const WIAfterEntries = [ ] ;
2024-05-08 03:55:26 +02:00
const EMEntries = [ ] ;
2023-07-20 19:32:15 +02:00
const ANTopEntries = [ ] ;
const ANBottomEntries = [ ] ;
2023-09-24 13:41:56 +02:00
const WIDepthEntries = [ ] ;
2023-07-20 19:32:15 +02:00
// Appends from insertion order 999 to 1. Use unshift for this purpose
2024-05-08 03:55:26 +02:00
// TODO (kingbri): Change to use WI Anchor positioning instead of separate top/bottom arrays
2024-10-10 14:52:54 +02:00
[ ... allActivatedEntries . values ( ) ] . sort ( sortFn ) . forEach ( ( entry ) => {
2024-05-08 19:10:52 +02:00
const regexDepth = entry . position === world _info _position . atDepth ? ( entry . depth ? ? DEFAULT _DEPTH ) : null ;
2024-09-19 17:17:11 +02:00
const content = getRegexedString ( entry . content , regex _placement . WORLD _INFO , { depth : regexDepth , isMarkdown : false , isPrompt : true } ) ;
2024-05-08 19:10:52 +02:00
2024-05-09 13:53:17 +02:00
if ( ! content ) {
2024-07-05 22:50:20 +02:00
console . debug ( ` [WI] Entry ${ entry . uid } ` , 'skipped adding to prompt due to empty content' , entry ) ;
2024-05-09 13:53:17 +02:00
return ;
}
2023-07-20 19:32:15 +02:00
switch ( entry . position ) {
case world _info _position . before :
2024-09-19 04:16:42 +02:00
WIBeforeEntries . unshift ( content ) ;
2023-07-20 19:32:15 +02:00
break ;
case world _info _position . after :
2024-09-19 04:16:42 +02:00
WIAfterEntries . unshift ( content ) ;
2023-07-20 19:32:15 +02:00
break ;
2024-05-08 03:55:26 +02:00
case world _info _position . EMTop :
EMEntries . unshift (
2024-05-08 19:10:52 +02:00
{ position : wi _anchor _position . before , content : content } ,
2024-05-08 03:55:26 +02:00
) ;
break ;
case world _info _position . EMBottom :
EMEntries . unshift (
2024-05-08 19:10:52 +02:00
{ position : wi _anchor _position . after , content : content } ,
2024-05-08 03:55:26 +02:00
) ;
2023-07-20 19:32:15 +02:00
break ;
case world _info _position . ANTop :
2024-05-08 19:10:52 +02:00
ANTopEntries . unshift ( content ) ;
2023-07-20 19:32:15 +02:00
break ;
case world _info _position . ANBottom :
2024-05-08 19:10:52 +02:00
ANBottomEntries . unshift ( content ) ;
2023-07-20 19:32:15 +02:00
break ;
2023-12-02 16:14:06 +01:00
case world _info _position . atDepth : {
2024-03-23 16:36:43 +01:00
const existingDepthIndex = WIDepthEntries . findIndex ( ( e ) => e . depth === ( entry . depth ? ? DEFAULT _DEPTH ) && e . role === ( entry . role ? ? extension _prompt _roles . SYSTEM ) ) ;
2023-09-24 13:41:56 +02:00
if ( existingDepthIndex !== - 1 ) {
2024-05-08 19:10:52 +02:00
WIDepthEntries [ existingDepthIndex ] . entries . unshift ( content ) ;
2023-09-24 13:41:56 +02:00
} else {
WIDepthEntries . push ( {
depth : entry . depth ,
2024-05-08 19:10:52 +02:00
entries : [ content ] ,
2024-03-23 16:36:43 +01:00
role : entry . role ? ? extension _prompt _roles . SYSTEM ,
2023-09-24 13:41:56 +02:00
} ) ;
}
2023-12-02 15:07:08 +01:00
break ;
2023-12-02 16:14:06 +01:00
}
2023-07-20 19:32:15 +02:00
default :
break ;
}
} ) ;
2023-12-02 19:04:51 +01:00
const worldInfoBefore = WIBeforeEntries . length ? WIBeforeEntries . join ( '\n' ) : '' ;
const worldInfoAfter = WIAfterEntries . length ? WIAfterEntries . join ( '\n' ) : '' ;
2023-07-20 19:32:15 +02:00
if ( shouldWIAddPrompt ) {
const originalAN = context . extensionPrompts [ NOTE _MODULE _NAME ] . value ;
2024-05-30 20:23:13 +02:00
const ANWithWI = ` ${ ANTopEntries . join ( '\n' ) } \n ${ originalAN } \n ${ ANBottomEntries . join ( '\n' ) } ` . replace ( /(^\n)|(\n$)/g , '' ) ;
2024-03-23 17:45:37 +01:00
context . setExtensionPrompt ( NOTE _MODULE _NAME , ANWithWI , chat _metadata [ metadata _keys . position ] , chat _metadata [ metadata _keys . depth ] , extension _settings . note . allowWIScan , chat _metadata [ metadata _keys . role ] ) ;
2023-07-20 19:32:15 +02:00
}
2024-10-10 14:52:54 +02:00
! isDryRun && timedEffects . setTimedEffects ( Array . from ( allActivatedEntries . values ( ) ) ) ;
2024-06-20 23:53:00 +02:00
buffer . resetExternalEffects ( ) ;
2024-06-23 20:18:18 +02:00
timedEffects . cleanUp ( ) ;
2024-04-23 02:09:52 +02:00
2024-10-10 14:52:54 +02:00
console . log ( ` [WI] Adding ${ allActivatedEntries . size } entries to prompt ` , Array . from ( allActivatedEntries . values ( ) ) ) ;
2024-07-05 20:49:17 +02:00
console . debug ( '[WI] --- DONE ---' ) ;
2024-10-10 15:12:19 +02:00
return { worldInfoBefore , worldInfoAfter , EMEntries , WIDepthEntries , allActivatedEntries : new Set ( allActivatedEntries . values ( ) ) } ;
2023-07-20 19:32:15 +02:00
}
2024-05-04 22:51:28 +02:00
/ * *
* Only leaves entries with the highest key matching score in each group .
* @ param { Record < string , WIScanEntry [ ] > } groups The groups to filter
* @ param { WorldInfoBuffer } buffer The buffer to use for scoring
2024-05-04 23:42:33 +02:00
* @ param { ( entry : WIScanEntry ) => void } removeEntry The function to remove an entry
2024-07-03 23:28:34 +02:00
* @ param { number } scanState The current scan state
2024-09-05 20:15:45 +02:00
* @ param { Map < string , boolean > } hasStickyMap The sticky entries map
2024-05-04 22:51:28 +02:00
* /
2024-09-05 20:15:45 +02:00
function filterGroupsByScoring ( groups , buffer , removeEntry , scanState , hasStickyMap ) {
2024-05-04 22:51:28 +02:00
for ( const [ key , group ] of Object . entries ( groups ) ) {
2024-05-04 23:42:33 +02:00
// Group scoring is disabled both globally and for the group entries
if ( ! world _info _use _group _scoring && ! group . some ( x => x . useGroupScoring ) ) {
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Skipping group scoring for group ' ${ key } ' ` ) ;
2024-05-04 23:42:33 +02:00
continue ;
}
2024-09-05 20:15:45 +02:00
// If the group has any sticky entries, the rest are already removed by the timed effects filter
const hasAnySticky = hasStickyMap . get ( key ) ;
if ( hasAnySticky ) {
console . debug ( ` [WI] Skipping group scoring check, group ' ${ key } ' has sticky entries ` ) ;
continue ;
}
2024-07-03 23:28:34 +02:00
const scores = group . map ( entry => buffer . getScore ( entry , scanState ) ) ;
2024-05-04 22:51:28 +02:00
const maxScore = Math . max ( ... scores ) ;
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Group ' ${ key } ' max score: ` , maxScore ) ;
2024-05-04 22:51:28 +02:00
//console.table(group.map((entry, i) => ({ uid: entry.uid, key: JSON.stringify(entry.key), score: scores[i] })));
for ( let i = 0 ; i < group . length ; i ++ ) {
2024-05-04 23:42:33 +02:00
const isScored = group [ i ] . useGroupScoring ? ? world _info _use _group _scoring ;
if ( ! isScored ) {
continue ;
}
2024-05-04 22:51:28 +02:00
if ( scores [ i ] < maxScore ) {
2024-07-05 22:50:20 +02:00
console . debug ( ` [WI] Entry ${ group [ i ] . uid } ` , ` removed as score loser from inclusion group ' ${ key } ' ` , group [ i ] ) ;
2024-05-05 00:08:49 +02:00
removeEntry ( group [ i ] ) ;
2024-05-04 22:51:28 +02:00
group . splice ( i , 1 ) ;
scores . splice ( i , 1 ) ;
i -- ;
}
}
}
}
2024-09-05 20:15:45 +02:00
/ * *
* Removes entries on cooldown and forces sticky entries as winners .
* @ param { Record < string , WIScanEntry [ ] > } groups The groups to filter
* @ param { WorldInfoTimedEffects } timedEffects The timed effects to use
* @ param { ( entry : WIScanEntry ) => void } removeEntry The function to remove an entry
* @ returns { Map < string , boolean > } If any sticky entries were found
* /
function filterGroupsByTimedEffects ( groups , timedEffects , removeEntry ) {
/** @type {Map<string, boolean>} */
const hasStickyMap = new Map ( ) ;
for ( const [ key , group ] of Object . entries ( groups ) ) {
hasStickyMap . set ( key , false ) ;
// If the group has any sticky entries, leave only the sticky entries
const stickyEntries = group . filter ( x => timedEffects . isEffectActive ( 'sticky' , x ) ) ;
if ( stickyEntries . length ) {
for ( const entry of group ) {
if ( stickyEntries . includes ( entry ) ) {
continue ;
}
console . debug ( ` [WI] Entry ${ entry . uid } ` , ` removed as a non-sticky loser from inclusion group ' ${ key } ' ` , entry ) ;
removeEntry ( entry ) ;
}
hasStickyMap . set ( key , true ) ;
}
// It should not be possible for an entry on cooldown/delay to event get into the grouping phase but @Wolfsblvt told me to leave it here.
const cooldownEntries = group . filter ( x => timedEffects . isEffectActive ( 'cooldown' , x ) ) ;
if ( cooldownEntries . length ) {
console . debug ( ` [WI] Inclusion group ' ${ key } ' has entries on cooldown. They will be removed. ` , cooldownEntries ) ;
for ( const entry of cooldownEntries ) {
removeEntry ( entry ) ;
}
}
const delayEntries = group . filter ( x => timedEffects . isEffectActive ( 'delay' , x ) ) ;
if ( delayEntries . length ) {
console . debug ( ` [WI] Inclusion group ' ${ key } ' has entries with delay. They will be removed. ` , delayEntries ) ;
for ( const entry of delayEntries ) {
removeEntry ( entry ) ;
}
}
}
return hasStickyMap ;
}
2023-12-07 19:06:06 +01:00
/ * *
* Filters entries by inclusion groups .
* @ param { object [ ] } newEntries Entries activated on current recursion level
2024-10-10 14:52:54 +02:00
* @ param { Map < string , object > } allActivatedEntries Map of all activated entries
2024-05-04 22:51:28 +02:00
* @ param { WorldInfoBuffer } buffer The buffer to use for scanning
2024-07-03 23:28:34 +02:00
* @ param { number } scanState The current scan state
2024-09-05 20:15:45 +02:00
* @ param { WorldInfoTimedEffects } timedEffects The timed effects currently active
2023-12-07 19:06:06 +01:00
* /
2024-09-05 20:15:45 +02:00
function filterByInclusionGroups ( newEntries , allActivatedEntries , buffer , scanState , timedEffects ) {
2024-07-05 20:49:17 +02:00
console . debug ( '[WI] --- INCLUSION GROUP CHECKS ---' ) ;
2023-12-07 19:06:06 +01:00
const grouped = newEntries . filter ( x => x . group ) . reduce ( ( acc , item ) => {
2024-05-14 00:16:41 +02:00
item . group . split ( /,\s*/ ) . filter ( x => x ) . forEach ( group => {
if ( ! acc [ group ] ) {
acc [ group ] = [ ] ;
}
acc [ group ] . push ( item ) ;
} ) ;
2023-12-07 19:06:06 +01:00
return acc ;
} , { } ) ;
if ( Object . keys ( grouped ) . length === 0 ) {
2024-07-05 20:49:17 +02:00
console . debug ( '[WI] No inclusion groups found' ) ;
2023-12-07 19:06:06 +01:00
return ;
}
2024-04-27 02:23:37 +02:00
const removeEntry = ( entry ) => newEntries . splice ( newEntries . indexOf ( entry ) , 1 ) ;
function removeAllBut ( group , chosen , logging = true ) {
for ( const entry of group ) {
if ( entry === chosen ) {
continue ;
}
2024-07-05 22:50:20 +02:00
if ( logging ) console . debug ( ` [WI] Entry ${ entry . uid } ` , ` removed as loser from inclusion group ' ${ entry . group } ' ` , entry ) ;
2024-04-27 20:03:55 +02:00
removeEntry ( entry ) ;
2024-04-27 02:23:37 +02:00
}
}
2024-09-05 20:15:45 +02:00
const hasStickyMap = filterGroupsByTimedEffects ( grouped , timedEffects , removeEntry ) ;
filterGroupsByScoring ( grouped , buffer , removeEntry , scanState , hasStickyMap ) ;
2024-05-04 23:42:33 +02:00
2023-12-07 19:06:06 +01:00
for ( const [ key , group ] of Object . entries ( grouped ) ) {
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Checking inclusion group ' ${ key } ' with ${ group . length } entries ` , group ) ;
2023-12-07 19:06:06 +01:00
2024-09-05 20:15:45 +02:00
// If the group has any sticky entries, the rest are already removed by the timed effects filter
const hasAnySticky = hasStickyMap . get ( key ) ;
if ( hasAnySticky ) {
console . debug ( ` [WI] Skipping inclusion group check, group ' ${ key } ' has sticky entries ` ) ;
continue ;
}
2024-10-10 14:52:54 +02:00
if ( Array . from ( allActivatedEntries . values ( ) ) . some ( x => x . group === key ) ) {
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Skipping inclusion group check, group ' ${ key } ' was already activated ` ) ;
2024-01-09 01:34:43 +01:00
// We need to forcefully deactivate all other entries in the group
2024-04-27 02:23:37 +02:00
removeAllBut ( group , null , false ) ;
2023-12-07 19:06:06 +01:00
continue ;
}
2024-01-09 10:48:51 +01:00
if ( ! Array . isArray ( group ) || group . length <= 1 ) {
2024-07-05 20:49:17 +02:00
console . debug ( '[WI] Skipping inclusion group check, only one entry' ) ;
2023-12-07 19:06:06 +01:00
continue ;
}
2024-04-27 02:23:37 +02:00
// Check for group prio
2024-04-27 04:40:35 +02:00
const prios = group . filter ( x => x . groupOverride ) . sort ( sortFn ) ;
2024-04-27 02:23:37 +02:00
if ( prios . length ) {
2024-07-05 22:50:20 +02:00
console . debug ( ` [WI] Entry ${ prios [ 0 ] . uid } ` , ` activated as prio winner from inclusion group ' ${ key } ' ` , prios [ 0 ] ) ;
2024-04-27 02:23:37 +02:00
removeAllBut ( group , prios [ 0 ] ) ;
continue ;
}
2024-05-06 16:00:42 +02:00
// Do weighted random using entry's weight
const totalWeight = group . reduce ( ( acc , item ) => acc + ( item . groupWeight ? ? DEFAULT _WEIGHT ) , 0 ) ;
2023-12-07 19:06:06 +01:00
const rollValue = Math . random ( ) * totalWeight ;
let currentWeight = 0 ;
let winner = null ;
for ( const entry of group ) {
2024-05-06 16:27:43 +02:00
currentWeight += ( entry . groupWeight ? ? DEFAULT _WEIGHT ) ;
2023-12-07 19:06:06 +01:00
if ( rollValue <= currentWeight ) {
2024-07-05 22:50:20 +02:00
console . debug ( ` [WI] Entry ${ entry . uid } ` , ` activated as roll winner from inclusion group ' ${ key } ' ` , entry ) ;
2023-12-07 19:06:06 +01:00
winner = entry ;
break ;
}
}
if ( ! winner ) {
2024-07-05 20:49:17 +02:00
console . debug ( ` [WI] Failed to activate inclusion group ' ${ key } ', no winner found ` ) ;
2023-12-07 19:06:06 +01:00
continue ;
}
// Remove every group item from newEntries but the winner
2024-04-27 02:23:37 +02:00
removeAllBut ( group , winner ) ;
2023-12-07 19:06:06 +01:00
}
}
2023-07-20 19:32:15 +02:00
function convertAgnaiMemoryBook ( inputObj ) {
const outputObj = { entries : { } } ;
inputObj . entries . forEach ( ( entry , index ) => {
outputObj . entries [ index ] = {
2024-08-02 20:42:53 +02:00
... newWorldInfoEntryTemplate ,
2023-07-20 19:32:15 +02:00
uid : index ,
key : entry . keywords ,
keysecondary : [ ] ,
comment : entry . name ,
content : entry . entry ,
constant : false ,
selective : false ,
2024-04-23 02:09:52 +02:00
vectorized : false ,
2023-12-05 11:04:27 +01:00
selectiveLogic : world _info _logic . AND _ANY ,
2023-07-20 19:32:15 +02:00
order : entry . weight ,
position : 0 ,
disable : ! entry . enabled ,
addMemo : ! ! entry . name ,
excludeRecursion : false ,
2024-05-13 22:33:25 +02:00
delayUntilRecursion : false ,
2023-07-20 19:32:15 +02:00
displayIndex : index ,
2024-05-13 14:24:16 +02:00
probability : 100 ,
useProbability : true ,
2023-12-04 17:36:05 +01:00
group : '' ,
2024-04-27 04:40:35 +02:00
groupOverride : false ,
2024-05-06 16:00:42 +02:00
groupWeight : DEFAULT _WEIGHT ,
2024-04-29 14:33:56 +02:00
scanDepth : null ,
caseSensitive : null ,
matchWholeWords : null ,
2024-05-04 23:42:33 +02:00
useGroupScoring : null ,
2024-04-29 14:33:56 +02:00
automationId : '' ,
role : extension _prompt _roles . SYSTEM ,
2024-06-20 23:53:00 +02:00
sticky : null ,
cooldown : null ,
2024-06-26 21:43:30 +02:00
delay : null ,
2023-07-20 19:32:15 +02:00
} ;
} ) ;
return outputObj ;
}
function convertRisuLorebook ( inputObj ) {
const outputObj = { entries : { } } ;
inputObj . data . forEach ( ( entry , index ) => {
outputObj . entries [ index ] = {
2024-08-02 20:42:53 +02:00
... newWorldInfoEntryTemplate ,
2023-07-20 19:32:15 +02:00
uid : index ,
key : entry . key . split ( ',' ) . map ( x => x . trim ( ) ) ,
keysecondary : entry . secondkey ? entry . secondkey . split ( ',' ) . map ( x => x . trim ( ) ) : [ ] ,
comment : entry . comment ,
content : entry . content ,
constant : entry . alwaysActive ,
selective : entry . selective ,
2024-04-23 02:09:52 +02:00
vectorized : false ,
2023-12-05 11:04:27 +01:00
selectiveLogic : world _info _logic . AND _ANY ,
2023-07-20 19:32:15 +02:00
order : entry . insertorder ,
position : world _info _position . before ,
disable : false ,
addMemo : true ,
excludeRecursion : false ,
2024-05-13 22:33:25 +02:00
delayUntilRecursion : false ,
2023-07-20 19:32:15 +02:00
displayIndex : index ,
2024-05-13 14:24:16 +02:00
probability : entry . activationPercent ? ? 100 ,
useProbability : entry . activationPercent ? ? true ,
2023-12-04 17:36:05 +01:00
group : '' ,
2024-04-27 04:40:35 +02:00
groupOverride : false ,
2024-05-06 16:00:42 +02:00
groupWeight : DEFAULT _WEIGHT ,
2024-04-29 14:33:56 +02:00
scanDepth : null ,
caseSensitive : null ,
matchWholeWords : null ,
2024-05-04 23:42:33 +02:00
useGroupScoring : null ,
2024-04-29 14:33:56 +02:00
automationId : '' ,
role : extension _prompt _roles . SYSTEM ,
2024-06-20 23:53:00 +02:00
sticky : null ,
cooldown : null ,
2024-06-26 21:43:30 +02:00
delay : null ,
2023-07-20 19:32:15 +02:00
} ;
} ) ;
return outputObj ;
}
function convertNovelLorebook ( inputObj ) {
const outputObj = {
2023-12-02 21:06:57 +01:00
entries : { } ,
2023-07-20 19:32:15 +02:00
} ;
inputObj . entries . forEach ( ( entry , index ) => {
const displayName = entry . displayName ;
const addMemo = displayName !== undefined && displayName . trim ( ) !== '' ;
outputObj . entries [ index ] = {
2024-08-02 20:42:53 +02:00
... newWorldInfoEntryTemplate ,
2023-07-20 19:32:15 +02:00
uid : index ,
key : entry . keys ,
keysecondary : [ ] ,
comment : displayName || '' ,
content : entry . text ,
constant : false ,
selective : false ,
2024-04-23 02:09:52 +02:00
vectorized : false ,
2023-12-05 11:04:27 +01:00
selectiveLogic : world _info _logic . AND _ANY ,
2023-07-20 19:32:15 +02:00
order : entry . contextConfig ? . budgetPriority ? ? 0 ,
position : 0 ,
disable : ! entry . enabled ,
addMemo : addMemo ,
excludeRecursion : false ,
2024-05-13 22:33:25 +02:00
delayUntilRecursion : false ,
2023-07-20 19:32:15 +02:00
displayIndex : index ,
2024-05-13 14:24:16 +02:00
probability : 100 ,
useProbability : true ,
2023-12-04 17:36:05 +01:00
group : '' ,
2024-04-27 04:40:35 +02:00
groupOverride : false ,
2024-05-06 16:00:42 +02:00
groupWeight : DEFAULT _WEIGHT ,
2024-04-29 14:33:56 +02:00
scanDepth : null ,
caseSensitive : null ,
matchWholeWords : null ,
2024-05-04 23:42:33 +02:00
useGroupScoring : null ,
2024-04-29 14:33:56 +02:00
automationId : '' ,
role : extension _prompt _roles . SYSTEM ,
2024-06-20 23:53:00 +02:00
sticky : null ,
cooldown : null ,
2024-06-26 21:43:30 +02:00
delay : null ,
2023-07-20 19:32:15 +02:00
} ;
} ) ;
return outputObj ;
}
function convertCharacterBook ( characterBook ) {
const result = { entries : { } , originalData : characterBook } ;
characterBook . entries . forEach ( ( entry , index ) => {
// Not in the spec, but this is needed to find the entry in the original data
if ( entry . id === undefined ) {
entry . id = index ;
}
result . entries [ entry . id ] = {
2024-08-02 20:42:53 +02:00
... newWorldInfoEntryTemplate ,
2023-07-20 19:32:15 +02:00
uid : entry . id ,
key : entry . keys ,
keysecondary : entry . secondary _keys || [ ] ,
2023-12-02 19:04:51 +01:00
comment : entry . comment || '' ,
2023-07-20 19:32:15 +02:00
content : entry . content ,
constant : entry . constant || false ,
selective : entry . selective || false ,
order : entry . insertion _order ,
2023-12-02 19:04:51 +01:00
position : entry . extensions ? . position ? ? ( entry . position === 'before_char' ? world _info _position . before : world _info _position . after ) ,
2023-07-20 19:32:15 +02:00
excludeRecursion : entry . extensions ? . exclude _recursion ? ? false ,
2024-01-07 11:51:13 +01:00
preventRecursion : entry . extensions ? . prevent _recursion ? ? false ,
2024-05-13 22:33:25 +02:00
delayUntilRecursion : entry . extensions ? . delay _until _recursion ? ? false ,
2023-07-20 19:32:15 +02:00
disable : ! entry . enabled ,
addMemo : entry . comment ? true : false ,
displayIndex : entry . extensions ? . display _index ? ? index ,
2024-05-13 14:24:16 +02:00
probability : entry . extensions ? . probability ? ? 100 ,
useProbability : entry . extensions ? . useProbability ? ? true ,
2023-09-25 19:11:16 +02:00
depth : entry . extensions ? . depth ? ? DEFAULT _DEPTH ,
2023-12-05 11:04:27 +01:00
selectiveLogic : entry . extensions ? . selectiveLogic ? ? world _info _logic . AND _ANY ,
2023-12-04 17:36:05 +01:00
group : entry . extensions ? . group ? ? '' ,
2024-04-27 20:03:55 +02:00
groupOverride : entry . extensions ? . group _override ? ? false ,
2024-05-06 16:00:42 +02:00
groupWeight : entry . extensions ? . group _weight ? ? DEFAULT _WEIGHT ,
2024-01-23 21:44:20 +01:00
scanDepth : entry . extensions ? . scan _depth ? ? null ,
caseSensitive : entry . extensions ? . case _sensitive ? ? null ,
matchWholeWords : entry . extensions ? . match _whole _words ? ? null ,
2024-05-04 23:42:33 +02:00
useGroupScoring : entry . extensions ? . use _group _scoring ? ? null ,
2024-02-24 16:22:51 +01:00
automationId : entry . extensions ? . automation _id ? ? '' ,
2024-03-23 16:36:43 +01:00
role : entry . extensions ? . role ? ? extension _prompt _roles . SYSTEM ,
2024-04-23 02:09:52 +02:00
vectorized : entry . extensions ? . vectorized ? ? false ,
2024-06-20 23:53:00 +02:00
sticky : entry . extensions ? . sticky ? ? null ,
cooldown : entry . extensions ? . cooldown ? ? null ,
2024-06-26 21:43:30 +02:00
delay : entry . extensions ? . delay ? ? null ,
2024-10-12 17:30:15 +02:00
extensions : entry . extensions ? ? { } ,
2023-07-20 19:32:15 +02:00
} ;
} ) ;
return result ;
}
export function setWorldInfoButtonClass ( chid , forceValue = undefined ) {
if ( forceValue !== undefined ) {
$ ( '#set_character_world, #world_button' ) . toggleClass ( 'world_set' , forceValue ) ;
return ;
}
if ( ! chid ) {
return ;
}
const world = characters [ chid ] ? . data ? . extensions ? . world ;
const worldSet = Boolean ( world && world _names . includes ( world ) ) ;
$ ( '#set_character_world, #world_button' ) . toggleClass ( 'world_set' , worldSet ) ;
}
export function checkEmbeddedWorld ( chid ) {
$ ( '#import_character_info' ) . hide ( ) ;
if ( chid === undefined ) {
return false ;
}
if ( characters [ chid ] ? . data ? . character _book ) {
$ ( '#import_character_info' ) . data ( 'chid' , chid ) . show ( ) ;
// Only show the alert once per character
const checkKey = ` AlertWI_ ${ characters [ chid ] . avatar } ` ;
const worldName = characters [ chid ] ? . data ? . extensions ? . world ;
if ( ! localStorage . getItem ( checkKey ) && ( ! worldName || ! world _names . includes ( worldName ) ) ) {
2024-01-23 21:44:20 +01:00
localStorage . setItem ( checkKey , 'true' ) ;
2023-10-04 21:41:10 +02:00
2023-10-10 19:47:54 +02:00
if ( power _user . world _import _dialog ) {
2023-12-02 20:56:16 +01:00
const html = ` <h3>This character has an embedded World/Lorebook.</h3>
< h3 > Would you like to import it now ? < / h 3 >
< div class = "m-b-1" > If you want to import it later , select "Import Card Lore" in the "More..." dropdown menu on the character panel . < / d i v > ` ;
const checkResult = ( result ) => {
if ( result ) {
importEmbeddedWorldInfo ( true ) ;
}
} ;
2024-07-06 15:55:57 +02:00
callGenericPopup ( html , POPUP _TYPE . CONFIRM , '' , { okButton : 'Yes' } ) . then ( checkResult ) ;
2023-10-10 17:08:08 +02:00
}
else {
toastr . info (
'To import and use it, select "Import Card Lore" in the "More..." dropdown menu on the character panel.' ,
` ${ characters [ chid ] . name } has an embedded World/Lorebook ` ,
{ timeOut : 5000 , extendedTimeOut : 10000 , positionClass : 'toast-top-center' } ,
) ;
}
2023-07-20 19:32:15 +02:00
}
return true ;
}
return false ;
}
2023-10-04 21:41:10 +02:00
export async function importEmbeddedWorldInfo ( skipPopup = false ) {
2023-07-20 19:32:15 +02:00
const chid = $ ( '#import_character_info' ) . data ( 'chid' ) ;
if ( chid === undefined ) {
return ;
}
const bookName = characters [ chid ] ? . data ? . character _book ? . name || ` ${ characters [ chid ] ? . name } 's Lorebook ` ;
2023-10-04 21:41:10 +02:00
if ( ! skipPopup ) {
2024-06-26 05:29:08 +02:00
const confirmation = await Popup . show . confirm ( ` Are you sure you want to import " ${ bookName } "? ` , world _names . includes ( bookName ) ? 'It will overwrite the World/Lorebook with the same name.' : '' ) ;
2023-10-04 21:41:10 +02:00
if ( ! confirmation ) {
return ;
}
2023-07-20 19:32:15 +02:00
}
const convertedBook = convertCharacterBook ( characters [ chid ] . data . character _book ) ;
await saveWorldInfo ( bookName , convertedBook , true ) ;
await updateWorldInfoList ( ) ;
$ ( '#character_world' ) . val ( bookName ) . trigger ( 'change' ) ;
toastr . success ( ` The world " ${ bookName } " has been imported and linked to the character successfully. ` , 'World/Lorebook imported' ) ;
const newIndex = world _names . indexOf ( bookName ) ;
if ( newIndex >= 0 ) {
2023-10-08 16:04:53 +02:00
//show&draw the WI panel before..
2023-12-02 19:04:51 +01:00
$ ( '#WIDrawerIcon' ) . trigger ( 'click' ) ;
2023-10-08 16:04:53 +02:00
//..auto-opening the new imported WI
2023-12-02 19:04:51 +01:00
$ ( '#world_editor_select' ) . val ( newIndex ) . trigger ( 'change' ) ;
2023-07-20 19:32:15 +02:00
}
setWorldInfoButtonClass ( chid , true ) ;
}
2024-08-02 20:42:53 +02:00
export function onWorldInfoChange ( args , text ) {
2024-01-07 12:36:44 +01:00
if ( args !== '__notSlashCommand__' ) { // if it's a slash command
2024-01-07 17:58:30 +01:00
const silent = isTrueBoolean ( args . silent ) ;
2023-11-25 21:26:41 +01:00
if ( text . trim ( ) !== '' ) { // and args are provided
2023-12-02 19:04:51 +01:00
const slashInputSplitText = text . trim ( ) . toLowerCase ( ) . split ( ',' ) ;
2023-07-20 19:32:15 +02:00
slashInputSplitText . forEach ( ( worldName ) => {
const wiElement = getWIElement ( worldName ) ;
if ( wiElement . length > 0 ) {
2024-01-07 12:36:44 +01:00
const name = wiElement . text ( ) ;
switch ( args . state ) {
case 'off' : {
if ( selected _world _info . includes ( name ) ) {
selected _world _info . splice ( selected _world _info . indexOf ( name ) , 1 ) ;
wiElement . prop ( 'selected' , false ) ;
if ( ! silent ) toastr . success ( ` Deactivated world: ${ name } ` ) ;
} else {
if ( ! silent ) toastr . error ( ` World was not active: ${ name } ` ) ;
}
break ;
}
case 'toggle' : {
if ( selected _world _info . includes ( name ) ) {
selected _world _info . splice ( selected _world _info . indexOf ( name ) , 1 ) ;
wiElement . prop ( 'selected' , false ) ;
2024-01-07 20:19:46 +01:00
if ( ! silent ) toastr . success ( ` Deactivated world: ${ name } ` ) ;
2024-01-07 12:36:44 +01:00
} else {
selected _world _info . push ( name ) ;
wiElement . prop ( 'selected' , true ) ;
2024-01-07 19:34:16 +01:00
if ( ! silent ) toastr . success ( ` Activated world: ${ name } ` ) ;
2024-01-07 12:36:44 +01:00
}
break ;
}
2024-06-20 20:33:45 +02:00
case 'on' :
2024-01-07 12:36:44 +01:00
default : {
selected _world _info . push ( name ) ;
wiElement . prop ( 'selected' , true ) ;
if ( ! silent ) toastr . success ( ` Activated world: ${ name } ` ) ;
}
}
2023-07-20 19:32:15 +02:00
} else {
2024-01-07 12:36:44 +01:00
if ( ! silent ) toastr . error ( ` No world found named: ${ worldName } ` ) ;
2023-07-20 19:32:15 +02:00
}
2023-08-13 18:20:00 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info' ) . trigger ( 'change' ) ;
2023-07-20 19:32:15 +02:00
} else { // if no args, unset all worlds
2024-01-07 18:00:16 +01:00
if ( ! silent ) toastr . success ( 'Deactivated all worlds' ) ;
2023-07-20 19:32:15 +02:00
selected _world _info = [ ] ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info' ) . val ( null ) . trigger ( 'change' ) ;
2023-07-20 19:32:15 +02:00
}
} else { //if it's a pointer selection
2024-07-11 23:23:26 +02:00
const tempWorldInfo = [ ] ;
const val = $ ( '#world_info' ) . val ( ) ;
const selectedWorlds = ( Array . isArray ( val ) ? val : [ val ] ) . map ( ( e ) => Number ( e ) ) . filter ( ( e ) => ! isNaN ( e ) ) ;
2023-07-20 19:32:15 +02:00
if ( selectedWorlds . length > 0 ) {
selectedWorlds . forEach ( ( worldIndex ) => {
const existingWorldName = world _names [ worldIndex ] ;
if ( existingWorldName ) {
tempWorldInfo . push ( existingWorldName ) ;
} else {
const wiElement = getWIElement ( existingWorldName ) ;
2023-12-02 19:04:51 +01:00
wiElement . prop ( 'selected' , false ) ;
2023-07-20 19:32:15 +02:00
toastr . error ( ` The world with ${ existingWorldName } is invalid or corrupted. ` ) ;
}
} ) ;
}
selected _world _info = tempWorldInfo ;
}
saveSettingsDebounced ( ) ;
2023-07-03 19:06:18 +02:00
eventSource . emit ( event _types . WORLDINFO _SETTINGS _UPDATED ) ;
2024-06-14 23:48:41 +02:00
return '' ;
2023-07-20 19:32:15 +02:00
}
export async function importWorldInfo ( file ) {
if ( ! file ) {
return ;
}
const formData = new FormData ( ) ;
formData . append ( 'avatar' , file ) ;
try {
let jsonData ;
if ( file . name . endsWith ( '.png' ) ) {
const buffer = new Uint8Array ( await getFileBuffer ( file ) ) ;
jsonData = extractDataFromPng ( buffer , 'naidata' ) ;
} else {
// File should be a JSON file
jsonData = await parseJsonFile ( file ) ;
}
if ( jsonData === undefined || jsonData === null ) {
toastr . error ( ` File is not valid: ${ file . name } ` ) ;
return ;
}
// Convert Novel Lorebook
if ( jsonData . lorebookVersion !== undefined ) {
console . log ( 'Converting Novel Lorebook' ) ;
formData . append ( 'convertedData' , JSON . stringify ( convertNovelLorebook ( jsonData ) ) ) ;
}
// Convert Agnai Memory Book
if ( jsonData . kind === 'memory' ) {
console . log ( 'Converting Agnai Memory Book' ) ;
formData . append ( 'convertedData' , JSON . stringify ( convertAgnaiMemoryBook ( jsonData ) ) ) ;
}
// Convert Risu Lorebook
if ( jsonData . type === 'risu' ) {
console . log ( 'Converting Risu Lorebook' ) ;
formData . append ( 'convertedData' , JSON . stringify ( convertRisuLorebook ( jsonData ) ) ) ;
}
} catch ( error ) {
toastr . error ( ` Error parsing file: ${ error } ` ) ;
return ;
}
2024-05-26 16:19:00 +02:00
const worldName = file . name . substr ( 0 , file . name . lastIndexOf ( '.' ) ) ;
2024-05-22 21:11:39 +02:00
const sanitizedWorldName = await getSanitizedFilename ( worldName ) ;
2024-05-22 23:52:35 +02:00
const allowed = await checkOverwriteExistingData ( 'World Info' , world _names , sanitizedWorldName , { interactive : true , actionName : 'Import' , deleteAction : ( existingName ) => deleteWorldInfo ( existingName ) } ) ;
2024-05-22 21:11:39 +02:00
if ( ! allowed ) {
return false ;
}
2023-07-20 19:32:15 +02:00
jQuery . ajax ( {
2023-12-02 19:04:51 +01:00
type : 'POST' ,
2023-12-06 23:09:48 +01:00
url : '/api/worldinfo/import' ,
2023-07-20 19:32:15 +02:00
data : formData ,
beforeSend : ( ) => { } ,
cache : false ,
contentType : false ,
processData : false ,
success : async function ( data ) {
if ( data . name ) {
await updateWorldInfoList ( ) ;
const newIndex = world _names . indexOf ( data . name ) ;
if ( newIndex >= 0 ) {
2023-12-02 19:04:51 +01:00
$ ( '#world_editor_select' ) . val ( newIndex ) . trigger ( 'change' ) ;
2023-07-20 19:32:15 +02:00
}
2024-05-22 21:11:39 +02:00
toastr . success ( ` World Info " ${ data . name } " imported successfully! ` ) ;
2023-07-20 19:32:15 +02:00
}
} ,
2024-05-07 02:01:54 +02:00
error : ( _jqXHR , _exception ) => { } ,
2023-07-20 19:32:15 +02:00
} ) ;
}
2024-08-02 20:42:53 +02:00
export function assignLorebookToChat ( ) {
2023-10-16 22:03:42 +02:00
const selectedName = chat _metadata [ METADATA _KEY ] ;
const template = $ ( '#chat_world_template .chat_world' ) . clone ( ) ;
const worldSelect = template . find ( 'select' ) ;
const chatName = template . find ( '.chat_name' ) ;
chatName . text ( getCurrentChatId ( ) ) ;
for ( const worldName of world _names ) {
const option = document . createElement ( 'option' ) ;
option . value = worldName ;
option . innerText = worldName ;
option . selected = selectedName === worldName ;
worldSelect . append ( option ) ;
}
worldSelect . on ( 'change' , function ( ) {
const worldName = $ ( this ) . val ( ) ;
if ( worldName ) {
chat _metadata [ METADATA _KEY ] = worldName ;
$ ( '.chat_lorebook_button' ) . addClass ( 'world_set' ) ;
} else {
delete chat _metadata [ METADATA _KEY ] ;
$ ( '.chat_lorebook_button' ) . removeClass ( 'world_set' ) ;
}
saveMetadata ( ) ;
} ) ;
callPopup ( template , 'text' ) ;
}
2023-07-20 19:32:15 +02:00
jQuery ( ( ) => {
2023-12-02 19:04:51 +01:00
$ ( '#world_info' ) . on ( 'mousedown change' , async function ( e ) {
2023-07-20 19:32:15 +02:00
// If there's no world names, don't do anything
if ( world _names . length === 0 ) {
e . preventDefault ( ) ;
return ;
}
onWorldInfoChange ( '__notSlashCommand__' ) ;
} ) ;
//**************************WORLD INFO IMPORT EXPORT*************************//
2023-12-02 19:04:51 +01:00
$ ( '#world_import_button' ) . on ( 'click' , function ( ) {
$ ( '#world_import_file' ) . trigger ( 'click' ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_import_file' ) . on ( 'change' , async function ( e ) {
2024-05-08 14:04:17 +02:00
if ( ! ( e . target instanceof HTMLInputElement ) ) {
return ;
}
2023-07-20 19:32:15 +02:00
const file = e . target . files [ 0 ] ;
await importWorldInfo ( file ) ;
// Will allow to select the same file twice in a row
2023-08-22 00:51:31 +02:00
e . target . value = '' ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_create_button' ) . on ( 'click' , async ( ) => {
2023-07-20 19:32:15 +02:00
const tempName = getFreeWorldName ( ) ;
2024-06-26 05:29:08 +02:00
const finalName = await Popup . show . input ( 'Create a new World Info' , 'Enter a name for the new file:' , tempName ) ;
2023-07-20 19:32:15 +02:00
if ( finalName ) {
2024-05-22 18:19:01 +02:00
await createNewWorldInfo ( finalName , { interactive : true } ) ;
2023-07-20 19:32:15 +02:00
}
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_editor_select' ) . on ( 'change' , async ( ) => {
$ ( '#world_info_search' ) . val ( '' ) ;
2023-08-22 00:51:31 +02:00
worldInfoFilter . setFilterData ( FILTER _TYPES . WORLD _INFO _SEARCH , '' , true ) ;
2023-12-02 19:04:51 +01:00
const selectedIndex = String ( $ ( '#world_editor_select' ) . find ( ':selected' ) . val ( ) ) ;
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
if ( selectedIndex === '' ) {
2023-07-20 19:32:15 +02:00
hideWorldEditor ( ) ;
} else {
const worldName = world _names [ selectedIndex ] ;
showWorldEditor ( worldName ) ;
}
} ) ;
2023-07-03 19:06:18 +02:00
const saveSettings = ( ) => {
2023-12-02 20:11:06 +01:00
saveSettingsDebounced ( ) ;
2023-07-03 19:06:18 +02:00
eventSource . emit ( event _types . WORLDINFO _SETTINGS _UPDATED ) ;
2023-12-02 20:11:06 +01:00
} ;
2023-07-03 19:06:18 +02:00
2023-12-02 19:04:51 +01:00
$ ( '#world_info_depth' ) . on ( 'input' , function ( ) {
2023-07-20 19:32:15 +02:00
world _info _depth = Number ( $ ( this ) . val ( ) ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info_depth_counter' ) . val ( $ ( this ) . val ( ) ) ;
2023-07-03 19:06:18 +02:00
saveSettings ( ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info_min_activations' ) . on ( 'input' , function ( ) {
2023-11-01 18:02:38 +01:00
world _info _min _activations = Number ( $ ( this ) . val ( ) ) ;
2024-08-23 20:34:18 +02:00
$ ( '#world_info_min_activations_counter' ) . val ( world _info _min _activations ) ;
2024-08-31 22:20:35 +02:00
if ( world _info _min _activations !== 0 && world _info _max _recursion _steps !== 0 ) {
2024-08-23 20:34:18 +02:00
$ ( '#world_info_max_recursion_steps' ) . val ( 0 ) . trigger ( 'input' ) ;
2024-08-31 22:20:35 +02:00
flashHighlight ( $ ( '#world_info_max_recursion_steps' ) . parent ( ) ) ; // flash the other control to show it has changed
console . info ( '[WI] Max recursion steps set to 0, as min activations is set to' , world _info _min _activations ) ;
2024-08-23 20:34:18 +02:00
} else {
saveSettings ( ) ;
}
2023-11-01 18:02:38 +01:00
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info_min_activations_depth_max' ) . on ( 'input' , function ( ) {
2023-11-01 18:02:38 +01:00
world _info _min _activations _depth _max = Number ( $ ( this ) . val ( ) ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info_min_activations_depth_max_counter' ) . val ( $ ( this ) . val ( ) ) ;
2023-11-01 18:02:38 +01:00
saveSettings ( ) ;
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info_budget' ) . on ( 'input' , function ( ) {
2023-07-20 19:32:15 +02:00
world _info _budget = Number ( $ ( this ) . val ( ) ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info_budget_counter' ) . val ( $ ( this ) . val ( ) ) ;
2023-07-03 19:06:18 +02:00
saveSettings ( ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2024-07-06 03:23:02 +02:00
$ ( '#world_info_include_names' ) . on ( 'input' , function ( ) {
world _info _include _names = ! ! $ ( this ) . prop ( 'checked' ) ;
saveSettings ( ) ;
} ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info_recursive' ) . on ( 'input' , function ( ) {
2023-07-20 19:32:15 +02:00
world _info _recursive = ! ! $ ( this ) . prop ( 'checked' ) ;
2023-07-03 19:06:18 +02:00
saveSettings ( ) ;
2023-11-04 16:44:43 +01:00
} ) ;
2023-07-20 19:32:15 +02:00
$ ( '#world_info_case_sensitive' ) . on ( 'input' , function ( ) {
world _info _case _sensitive = ! ! $ ( this ) . prop ( 'checked' ) ;
2023-07-03 19:06:18 +02:00
saveSettings ( ) ;
2023-11-04 16:44:43 +01:00
} ) ;
2023-07-20 19:32:15 +02:00
$ ( '#world_info_match_whole_words' ) . on ( 'input' , function ( ) {
world _info _match _whole _words = ! ! $ ( this ) . prop ( 'checked' ) ;
2023-07-03 19:06:18 +02:00
saveSettings ( ) ;
2023-07-20 19:32:15 +02:00
} ) ;
$ ( '#world_info_character_strategy' ) . on ( 'change' , function ( ) {
2023-10-17 12:55:02 +02:00
world _info _character _strategy = Number ( $ ( this ) . val ( ) ) ;
2023-07-03 19:06:18 +02:00
saveSettings ( ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-07-30 04:15:54 +02:00
$ ( '#world_info_overflow_alert' ) . on ( 'change' , function ( ) {
2023-08-07 21:12:50 +02:00
world _info _overflow _alert = ! ! $ ( this ) . prop ( 'checked' ) ;
2023-07-30 04:15:54 +02:00
saveSettingsDebounced ( ) ;
} ) ;
2023-08-10 19:45:57 +02:00
2024-05-04 22:51:28 +02:00
$ ( '#world_info_use_group_scoring' ) . on ( 'change' , function ( ) {
world _info _use _group _scoring = ! ! $ ( this ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
} ) ;
2023-08-10 19:45:57 +02:00
$ ( '#world_info_budget_cap' ) . on ( 'input' , function ( ) {
world _info _budget _cap = Number ( $ ( this ) . val ( ) ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_info_budget_cap_counter' ) . val ( world _info _budget _cap ) ;
2023-08-13 18:50:12 +02:00
saveSettings ( ) ;
2023-08-10 19:45:57 +02:00
} ) ;
2023-07-20 19:32:15 +02:00
2024-08-23 20:34:18 +02:00
$ ( '#world_info_max_recursion_steps' ) . on ( 'input' , function ( ) {
world _info _max _recursion _steps = Number ( $ ( this ) . val ( ) ) ;
$ ( '#world_info_max_recursion_steps_counter' ) . val ( world _info _max _recursion _steps ) ;
2024-08-31 22:20:35 +02:00
if ( world _info _max _recursion _steps !== 0 && world _info _min _activations !== 0 ) {
2024-08-23 20:34:18 +02:00
$ ( '#world_info_min_activations' ) . val ( 0 ) . trigger ( 'input' ) ;
2024-08-31 22:20:35 +02:00
flashHighlight ( $ ( '#world_info_min_activations' ) . parent ( ) ) ; // flash the other control to show it has changed
console . info ( '[WI] Min activations set to 0, as max recursion steps is set to' , world _info _max _recursion _steps ) ;
2024-08-23 20:34:18 +02:00
} else {
saveSettings ( ) ;
}
} ) ;
2023-10-17 12:55:02 +02:00
$ ( '#world_button' ) . on ( 'click' , async function ( event ) {
2023-07-20 19:32:15 +02:00
const chid = $ ( '#set_character_world' ) . data ( 'chid' ) ;
if ( chid ) {
const worldName = characters [ chid ] ? . data ? . extensions ? . world ;
const hasEmbed = checkEmbeddedWorld ( chid ) ;
2023-10-17 02:10:57 +02:00
if ( worldName && world _names . includes ( worldName ) && ! event . shiftKey ) {
2023-07-20 19:32:15 +02:00
if ( ! $ ( '#WorldInfo' ) . is ( ':visible' ) ) {
$ ( '#WIDrawerIcon' ) . trigger ( 'click' ) ;
}
const index = world _names . indexOf ( worldName ) ;
2023-12-02 19:04:51 +01:00
$ ( '#world_editor_select' ) . val ( index ) . trigger ( 'change' ) ;
2023-10-17 02:10:57 +02:00
} else if ( hasEmbed && ! event . shiftKey ) {
2023-07-20 19:32:15 +02:00
await importEmbeddedWorldInfo ( ) ;
saveCharacterDebounced ( ) ;
}
else {
$ ( '#char-management-dropdown' ) . val ( $ ( '#set_character_world' ) . val ( ) ) . trigger ( 'change' ) ;
}
}
} ) ;
2024-04-28 06:27:55 +02:00
const debouncedWorldInfoSearch = debounce ( ( searchQuery ) => {
2024-04-28 05:42:15 +02:00
worldInfoFilter . setFilterData ( FILTER _TYPES . WORLD _INFO _SEARCH , searchQuery ) ;
2024-04-28 06:21:47 +02:00
} ) ;
2023-08-22 00:51:31 +02:00
$ ( '#world_info_search' ) . on ( 'input' , function ( ) {
2024-04-28 05:42:15 +02:00
const searchQuery = $ ( this ) . val ( ) ;
2024-04-28 06:27:55 +02:00
debouncedWorldInfoSearch ( searchQuery ) ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-10-08 11:30:12 +02:00
$ ( '#world_refresh' ) . on ( 'click' , ( ) => {
updateEditor ( navigation _option . previous ) ;
} ) ;
2023-11-11 19:16:57 +01:00
$ ( '#world_info_sort_order' ) . on ( 'change' , function ( ) {
2023-12-02 19:04:51 +01:00
const value = String ( $ ( this ) . find ( ':selected' ) . val ( ) ) ;
2024-04-30 01:39:47 +02:00
// Save sort order, but do not save search sorting, as this is a temporary sorting option
2024-04-30 02:27:44 +02:00
if ( value !== 'search' ) localStorage . setItem ( SORT _ORDER _KEY , value ) ;
2023-10-05 22:30:18 +02:00
updateEditor ( navigation _option . none ) ;
2023-12-02 20:11:06 +01:00
} ) ;
2023-10-05 22:30:18 +02:00
2023-10-16 22:03:42 +02:00
$ ( document ) . on ( 'click' , '.chat_lorebook_button' , assignLorebookToChat ) ;
2023-07-20 19:32:15 +02:00
// Not needed on mobile
2023-12-15 00:03:10 +01:00
if ( ! isMobile ( ) ) {
2023-07-20 19:32:15 +02:00
$ ( '#world_info' ) . select2 ( {
width : '100%' ,
placeholder : 'No Worlds active. Click here to select.' ,
allowClear : true ,
closeOnSelect : false ,
} ) ;
2024-05-02 20:04:24 +02:00
// Subscribe world loading to the select2 multiselect items (We need to target the specific select2 control)
2024-05-10 00:42:35 +02:00
select2ChoiceClickSubscribe ( $ ( '#world_info' ) , target => {
2024-05-09 23:30:18 +02:00
const name = $ ( target ) . text ( ) ;
const selectedIndex = world _names . indexOf ( name ) ;
if ( selectedIndex !== - 1 ) {
$ ( '#world_editor_select' ) . val ( selectedIndex ) . trigger ( 'change' ) ;
console . log ( 'Quick selection of world' , name ) ;
2024-05-02 20:04:24 +02:00
}
2024-05-10 00:42:35 +02:00
} , { buttonStyle : true , closeDrawer : true } ) ;
2023-07-20 19:32:15 +02:00
}
2024-02-24 18:04:44 +01:00
$ ( '#WorldInfo' ) . on ( 'scroll' , ( ) => {
2024-02-25 02:54:40 +01:00
$ ( '.world_entry input[name="group"], .world_entry input[name="automationId"]' ) . each ( ( _ , el ) => {
2024-02-24 18:04:44 +01:00
const instance = $ ( el ) . autocomplete ( 'instance' ) ;
if ( instance !== undefined ) {
$ ( el ) . autocomplete ( 'close' ) ;
}
} ) ;
} ) ;
2023-12-05 11:04:27 +01:00
} ) ;