Files
SillyTavern/public/scripts/power-user.js
T
Wolfsblvt 6f8b6b098e Macros 2.0 (v0.3) - Replacing the existing Macro System with a new Macro Engine (#4820)
* Chevrotain lib and env setup

* First draft of the macro lexer

* fix ESLint types loading for chevrotain

* Cleaner lexer modes

* Readme link to Chevrotain & license

* Add jsconfig to tests folder

- Add jsconfig.json to tests folder, to prevent IDE errors on dynamic imports inside the page.evaluate execution.

* Slight improvements on lexer & first tests

* Add more lexer tests

* More edge cases tests

* Reorder tests

* Add macro execution modifiers + more tests

- Added macro flags (execution modifiers) to lexer
- Fixed some lexing issues
- Expanded lexer tests
- Treat lexer errors as failed test

* enable eslint for tests and run it

* Fix lexing unknown flags - treat as error

* Rewrote lexer modes/tokes to capture errors better

* Add lexing for output modifiers

* Clearer names for lexer tokens

* Increase tests default timeout

* Restructure lexer error testcases

* Allow legacy underscores in macro identifiers

* Test case for legacy single-colon syntax

* Improve lexer, removing warnings

* Basic setup for MacroParser + initial tests

* Make parser errors testable

* Add macros stuff to SillyTavern.getContext

* macros test case naming + lint

* Parser consumes basic macros

- Fix lexer mode names
- Add basic macro parsing (identifier, and arguments)
- Tests: basic macro parsing tests
- Tests: simplifyCstNode supports ignoring nodes, or flattening nodes to just plaintext

* Improve macro argument parsing to allow colons in values

Enhances separator handling by fixing separator type detection and enabling colon characters within argument values
Updates validation to require at least one argument component and adds error cases for empty arguments
Includes expanded test coverage for mixed separator scenarios and edge cases

* More nested macro tests

Add error case tests to enforce macro start position requirements
Include nested macro parsing scenarios and invalid syntax checks
Ensures parser correctly handles edge cases with embedded macros

* Unvendor chevrotain

* Add document rule

* Implement visitor, switch built-ins to new type

* Puppeteer -> playwright

* Revert "Implement visitor, switch built-ins to new type"

This reverts commit 706a94b4de62129df6bd6c25e2c6dec692d12226.

* Converted puppeteeer tests

* File rename

* chore: reduce Playwright worker count to 4 for performance/stability

* test: add comprehensive legacy macro parser test suite

- Added 13 test cases covering legacy macro formats (roll, reverse, comment, datetime, time_UTC, banned, setvar)
- Documented parser limitations with TODO comments for whitespace separators, special characters, and empty arguments
- Tests validate parsing of various separator styles (space, colon, +/-) and argument formats (quoted, numeric, empty)

* fix: handle legacy macro syntax with colon or whitespace separator

- Modified arguments rule to support both double-colon (::) and single-colon (:) separators
- Made single-colon separator optional to allow whitespace-separated legacy macros
- Removed TODO comments as parser now correctly handles legacy macro formats

* feat: support space-separated quoted arguments in macro parser

- Added parsing support for equals signs and quotes as valid argument tokens
- Removed TODO comments for legacy macro parsing with quoted arguments

* fix: improves macro argument parsing with colon handling

Enhances parser to correctly handle double colons within legacy single-colon arguments
Introduces separate parsing rules for arguments with different colon constraints
Adds test coverage for arguments containing double colons in legacy format

* fix: allow empty macro arguments after double-colon separator

- Changed argument rule from AT_LEAST_ONE to MANY to permit zero-length arguments
- Updated tests to verify empty argument parsing (e.g., `{{something::}}`)
- Enhanced simplifyCstNode helper with default flatten/ignore keys and improved null handling

* refactor: improve test documentation with inline macro display

- Added inline comments showing the actual macro syntax being tested for better readability
- Removed duplicate comment in error test case
- Cleaned up extra whitespace in legacy macro tests

* feat: add legacy macro preprocessing for time offset format

- Implemented preProcessFixLegacyMacros method to convert {{time_UTC±N}} to {{time::UTC±N}} format
- Updated tests to use new preprocessing step for legacy time macro parsing
- Added runPreProcessFix option to test helper functions for controlled legacy macro handling

* feat: add support for comment macros with double-slash syntax

- Added `DoubleSlash` token to lexer to recognize `//` as a valid macro identifier
- Updated parser to accept either `//` or standard identifiers as macro names
- Enhanced test suite with comprehensive comment macro test cases including multiline support

* feat: implement macro evaluation engine with CST walking and registry integration

(I'm tired, let's just throw this in right now)

- Added CST walker and macro registry to engine initialization
- Enhanced parseDocument to handle empty input, legacy macro preprocessing, and error collection
- Implemented async evaluate method with full macro resolution pipeline
- Added resolveMacro callback to handle unknown macros and registry execution
- Integrated lexing/parsing error handling with console warnings
- Added support for preserving unknown macro

* refactor: improve type safety and code clarity in macro evaluation system

- Simplified typedef imports to use correct Chevrotain types (CstNode, IToken)
- Added TokenRange typedef for consistent offset handling
- Enhanced error messages with context-specific prefixes
- Replaced verbose type casts with inline JSDoc annotations
- Condensed singleton pattern declarations to single lines
- Improved null safety with optional chaining and nullish coalescing
- Extracted resolveMacro logic into private root function

* fix test macro whitespace arguments onls accepting one argument

* Fix OpenRouter embeddings URL
#4736

* Fix: Prevent data loss on bulk regex move to scoped scripts (#4760)

* Fix: Prevent data loss in regex bulk move

* prevents moving to scoped scripts with group selected for bulkedit

* Refactor: make whitelist validation a bit more robust (#4757)

* refactor: extract IP whitelist validation into helper function

- Added isIPInWhitelist helper with error handling for individual whitelist entry checks
- Replaced inline whitelist matching logic with reusable function calls
- Added JSDoc type annotations and error logging for failed IP matching attempts

* refactor: simplify whitelist validation with upfront filtering

- Moved IP validation to startup time instead of per-request checking
- Extracted validateWhitelist function to filter invalid entries once at initialization
- Simplified isIPInWhitelist by removing redundant error handling after validation

* fix: correct IP whitelist matching to use parsed CIDR notation

* Feat: Improve multiline input handling in popups (#4756)

* feat: improve multiline input handling in popups

- Added Ctrl+Enter requirement for submission in multiline input popups to prevent accidental sends
- Exported PopupUtils class for external use

* refactor: remove redundant higher/different rows from input popups

- Removed rows: 2 from callGenericPopup calls where default behavior is sufficient
- Increased rows from 2 to 4 in caption extension for better multiline input experience

* Fix npm audit

* feat: add max-height and scrolling to world entry key input fields (#4769)

- Added 160px max-height to select2 multiple selection, primary key, and secondary key text areas in world entries
- Enabled vertical scrolling with hidden horizontal overflow for better UX with long content

* Bump anti-troll tags limit
Closes #4763

* OpenAI: Add Sora 2 API (#4748)

* OpenAI: Add Sora 2 API

* Add duration control

* Support client generation abort

* Reduce poll log amount

* Simplify selector

* Simplify model-specific control handling

* Gemini: Pass non-success response content to frontend

* Vertex: Add Vertex AI-specific safety setting (#4770)

Closes #4455

* Feature: allows sorting tags by most used (#4768)

* add sorting tags by most used

* Fix whitespaces

* Code review updates

* Remove commented code

* Fix capitalization in comment

* Apply review suggestion

* Simplify template init

* Reformat

* Add documentation for appendViewTagToList and printViewTagList functions

* Reprint renamed tags regardless of sorting mode

---------

Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

* Move img.swipe notice block up

* Preserve media playback state when running appendMediaToMessage (#4771)

* Preserve media playback state when running appendMediaToMessage

* Better selector specificity

* Fix local variable name

* Move typedef to global.d.ts

* Check for readyState on save/restore

* Only check for currentSrc on restoration callback

* gpt-5.1

* A 500 billion dollar startup can’t filter API payload fields

* Refactor macro argument validation to use requiredArgs and list pattern

- Replace minArgs/maxArgs/enforceArity with requiredArgs and list specification
- Add MacroListSpec typedef for flexible list argument constraints (min/max)
- Rename enforceArity to strictArgs with inverted default behavior
- Update validation logic to separately track required vs. list arguments
- Add requiredArgs and list arrays to MacroExecutionContext for easier access
- Improve error messages to clearly distinguish between required and list

* Refactor MacroRegistry.registerMacro to use options object pattern

- Move handler function into options object as required property
- Improve validation with detailed error messages for all option fields
- Add explicit type checking for requiredArgs, list, strictArgs, and description
- Consolidate name normalization and validation logic
- Simplify list option parsing with clearer conditional structure
- Update all builtin macro registrations to use new signature

* Add e2e tests for macro arity validation errors

- Test ping macro rejects calls with unexpected arguments
- Test upper macro rejects calls without required arguments
- Verify macros remain unresolved and log warnings on arity violations

* re-implement core macros as registered macros

- Create shallow frozen copy of env object before passing to macro handlers
- Add comprehensive MacroEnv typedef with nested types for names, character, system, and extra fields
- Update MacroDefinitionOptions typedef to clarify default values and mark handler as required
- Export variable manipulation functions for external use
- Fix whitespace in ifCallback JSDoc comment

* Defer slash command autocomplete initialization to firstLoadInit

- Move slash command autocomplete setup from module-level to initSlashCommandAutoComplete function
- Add null check for sendTextarea.value before accessing first character
- Import and call initSlashCommandAutoComplete in firstLoadInit sequence
- Export registerCoreMacros, registerInstructMacros, and registerVariableMacros using named export syntax
- Call registerCoreMacros from initMacros with

* Add variable manipulation functions to SillyTavern context API

- Import deleteGlobalVariable, deleteLocalVariable, addGlobalVariable, addLocalVariable, incrementGlobalVariable, incrementLocalVariable, decrementGlobalVariable, and decrementLocalVariable from variables.js
- Expose del, add, inc, and dec methods on both context.variables.local and context.variables.global objects
- Provide consistent API for variable deletion, addition, increment, and decrement operations

* Extract test setup utilities and fix trailing comma in core-macros.js

- Add testSetup utility object with goST and awaitST helper functions for Playwright tests
- Replace duplicated beforeEach setup code in MacroLexer, MacroParser, and MacroRegistry tests with testSetup.goST
- Add explanatory comments indicating tests currently run without ST context
- Fix missing trailing comma in input macro handler registration

* Refactor MacroEngine e2e tests to use real core macros instead of prototypes

- Replace prototype macros (ping, echo, upper, wrap, first) with actual core macros (newline, reverse, setvar, getvar, addvar, roll)
- Use testSetup.awaitST helper from utils.js for beforeEach setup
- Increase test timeout from 10s to 20s due to additional setup requirements
- Remove registerPrototypeMacros import and registration logic from evaluateWithEngine
- Add page.waitForTimeout(1000) to ensure macros are fully initialize
- Remove now obsolete MacroBuiltins.js

* Fix audit in tests

* Rename frontend test utils file to frontent-test-utils.js and update imports

- Rename tests/frontend/utils.js to tests/frontend/frontent-test-utils.js
- Update testSetup imports across MacroEngine, MacroLexer, MacroParser, and MacroRegistry e2e tests

* Refactor MacroRegistry for improved readability and consistency

- Reorder MacroDefinition typedef fields to match registration order (handler moved to end)
- Add inline JSDoc comments to MacroDefinitionOptions properties for clarity
- Simplify name validation logic in unregisterMacro, hasMacro, and getMacro methods using early returns
- Extract argument validation logic into dedicated isArgsValid helper function
- Refactor executeMacro to be async and use Promise.resolve for consistent promise handling

* Add returns field to MacroDefinitionOptions for documenting macro return values

- Add returns property to MacroDefinitionOptions typedef with string type and null default
- Include returns field in MacroDefinition typedef
- Store returns value in macro definition object for documentation purposes

* Add error handling to macro execution and refactor core macro handlers

- Add catch handler in MacroRegistry.executeMacro to log errors and return empty string on handler failures
- Update macro descriptions to use "index" instead of "ID" for message position macros
- Simplify reverse macro description and remove redundant null coalescing
- Add strictArgs: false to comment macro to ensure it's always removed
- Enhance time macro description with UTC offset examples
- Remove unnecessary null check

* Refactor instruct-macros.js for consistency and reduce code duplication

- Rename enabled helper to instEnabled and add sysEnabled helper for clarity
- Consolidate defaultSystemPrompt, instructSystem, and instructSystemPrompt into single registerSimple call
- Simplify systemPrompt handler by removing unnecessary null coalescing and intermediate variables
- Convert chatSeparator and chatStart macros to use registerSimple helper
- Fix JSDoc comment formatting for registerSimple helper function

* Add normalize helper to MacroExecutionContext and refactor variable macros for consistency

- Add normalizeMacroResult method to MacroEngine for converting macro results to strings
- Include normalize function in MacroExecutionContext for handler use
- Update normalizeMacroResult to handle arrays explicitly alongside objects
- Add returns field to variable macros that produce side-effects only
- Refactor variable macro handlers to use destructuring and normalize helper
- Remove redundant null checks an

* chore: Remove duplicate getglobalvar macro registration from variable-macros.js

- Remove redundant getglobalvar macro definition (already registered earlier in the file)
- Import MacroEngine in MacroRegistry for normalizeMacroResult access
- Add fallback binding for normalize in MacroExecutionContext when not provided by caller
- Update executeMacro to use executionContext.normalize instead of standalone normalizeMacroResult
- Remove normalizeMacroResult helper function from MacroRegistry (now handled by MacroEngine)

* Add core environment macros for names, character fields, system info, and deterministic pick

- Add lastGenerationType tracking with event listeners for GENERATION_STARTED and CHAT_CHANGED
- Add ensureLastGenerationTypeTracking helper to initialize event listeners once
- Register name macros: user, char, group, groupNotMuted, notChar, charIfNotGroup
- Register character card field macros: charPrompt, charInstruction, description, personality, scenario, persona, mesExamplesRaw, charDepthPrompt, cre

* Refactor core macros into separate modules by category (env, state, chat, time)

- Move name and character card field macros to new env-macros.js module
- Move system/device/runtime state macros to new state-macros.js module
- Move chat inspection macros (lastMessage, lastMessageId, etc.) to new chat-macros.js module
- Move time/date macros (time, date, weekday, isotime, etc.) to new time-macros.js module
- Remove lastGenerationType tracking logic and helper functions from core-macros.js (moved to state

* Reorganize macro system into engine and definitions directories

- Move macro engine components (MacroEngine, MacroRegistry, MacroLexer, MacroParser, MacroCstWalker) to macros/engine/ subdirectory
- Move macro definition modules (core-macros, env-macros, state-macros, chat-macros, time-macros, variable-macros, instruct-macros) to macros/definitions/ subdirectory
- Create macro-system.js as central entry point that exports engine singletons and initRegisterMacros function
- Refactor variable-macros.js to use S

* Export getGeneratingModel function and add MacroEnvBuilder to macro system exports

- Export getGeneratingModel function from script.js for external use
- Import MacroEnvBuilder in macro-system.js
- Add envBuilder singleton to macros export object alongside existing engine components

* Extract MacroEnv typedefs into separate MacroEnv.types.js file

- Create MacroEnv.types.js with MacroEnv, MacroEnvNames, MacroEnvCharacter, and MacroEnvSystem typedefs
- Remove MacroEnv typedef definitions from MacroRegistry.js
- Update MacroRegistry.js, MacroEngine.js, MacroEnvBuilder.js, and env-macros.js to import MacroEnv from MacroEnv.types.js
- Change MacroEngine.evaluate env parameter type from any to optional MacroEnv

* Add comprehensive e2e tests for MacroEnvBuilder

- Create MacroEnvBuilder.e2e.js with 13 test cases validating environment construction
- Test name override precedence (overrides vs global fallback)
- Test character field population based on replaceCharacterCard flag
- Test original value one-shot helper function behavior
- Test group override string propagation to group/group

* Add substituteParamsAsync function with experimental macro engine support

- Create substituteParamsAsync function in script.js as async alternative to substituteParams
- Use object destructuring pattern for function parameters following RO-RO convention
- Add experimental_macro_engine flag to power_user settings for feature gating
- Add MacroEnvFunctions typedef with original and postProcess function types to MacroEnv.types.js
- Add functions property to MacroEnv typedef for one-shot helpers an

* Add dynamic macro support to MacroEngine with environment-based override and postProcess execution

- Add defOverride parameter to MacroRegistry.executeMacro for temporary macro definitions
- Check env.dynamicMacros in MacroEngine#resolveMacro and create temporary macro definition when found
- Set strictArgs to true for dynamic macros to fail if called with arguments
- Execute env.functions.postProcess on macro results in MacroEngine#resolveMacro with error handling
- Update MacroEnv typedef to make

* Refactor macro handlers to assume non-null env and remove optional chaining

- Remove optional chaining (?.) from env property access in env-macros.js and instruct-macros.js
- Change MacroExecutionContext env property from optional to required in MacroRegistry.js
- Update executeMacro context parameter from optional to required
- Simplify original macro handler to call env.functions.original() directly without try-catch
- Update MacroHandler typedef to use arrow function syntax for consistency
- Remove redundant null checks an

* Refactor MacroRegistry.executeMacro to accept MacroCall object and make MacroExecutionContext properties non-optional

- Change executeMacro to accept MacroCall object instead of separate name and context parameters
- Move normalize function from context parameter to options object in executeMacro
- Construct MacroExecutionContext from MacroCall properties within executeMacro
- Set namedArgs to null in executionContext (currently unused)
- Update MacroEngine.resolveMacro to pass MacroCall directly

* Remove normalize parameter from MacroRegistry.executeMacro and bind normalizeMacroResult directly in execution context

- Remove normalize parameter from executeMacro options object in MacroRegistry.js
- Bind MacroEngine.normalizeMacroResult directly in executionContext instead of accepting override
- Remove normalize option from MacroEngine.resolveMacro call to executeMacro
- Add missing name fields (group, groupNotMuted, notChar) to MacroEnvBuilder default env object

* Make MacroEnvRawContext properties optional and change original function from required to optional in MacroEnvFunctions

- Change MacroEnvRawContext properties from required to optional with null defaults
- Change original function from required to optional in MacroEnvFunctions typedef
- Remove original function from default env object in MacroEnvBuilder (only include when provided)

* Refactor MacroEnvBuilder tests to use optional properties and add macro arity validation tests

- Remove explicit undefined/false assignments from MacroEnvRawContext test objects (now optional with defaults)
- Rename additionalMacro to dynamicMacros in MacroEnvBuilder tests and update property access from env.extra to env.dynamicMacros
- Move original function from env.extra to env.functions in test assertions
- Remove test for additionalMacro overriding original helper (no longer applicable with

* Improve macro error handling with dedicated diagnostics and runtime error propagation

- Import and use logMacroInternalError and logMacroRuntimeWarning from MacroDiagnostics in MacroEngine
- Wrap MacroRegistry.executeMacro call in try-catch to distinguish runtime vs internal errors
- Nest postProcess execution in inner try-catch with dedicated error logging
- Return raw macro syntax on execution failure instead of empty string
- Replace console.warn with logMacroRuntimeWarning for argument count

* Add macro argument type validation with positional argument definitions and runtime type checking

- Add MacroArgType and MacroPositionalArgDef typedefs for argument metadata
- Change requiredArgs option to accept number or MacroPositionalArgDef[] array
- Add requiredArgDefs property to MacroDefinition to store normalized argument definitions
- Validate requiredArgs array elements during macro registration (name, description, type fields)
- Generate default argument definitions when requiredArgs is a

* Add e2e tests for macro type validation and dynamic macro strict arity enforcement

- Add test verifying strict typed macros fail resolution when argument type is invalid
- Add test verifying non-strict typed macros execute with invalid types but log warnings
- Add test verifying dynamic macros reject arguments due to strictArgs enforcement
- Capture and assert runtime warning messages for type validation and arity violations
- Register test macros with integer type requirements and varying strict

* Add mesExamples macro with instruct mode formatting support

- Register mesExamples macro in env-macros.js to format dialogue examples
- Import parseMesExamples, main_api, power_user, and formatInstructModeExamples
- Check instruct mode enabled state and main_api to determine formatting path
- Parse raw examples using parseMesExamples with instruct mode flag
- Return empty string when raw examples are missing or parsed result is empty
- Format examples using formatInstructModeExamples when instruct mode is active

* Add URL navigation wait in test setup and increase Playwright worker count to 4

- Add waitForURL check after user selection in awaitST to ensure navigation completes before preloader check
- Increase Playwright workers from 1 to 4 for parallel test execution

* Add e2e tests for multi-line macro arguments and comment macro functionality

- Add test verifying reverse macro handles multi-line arguments with newline characters
- Add test verifying comment macro removes single-line comments with simple body
- Add test verifying comment macro accepts non-word characters immediately after //
- Add test verifying comment macro ignores additional // sequences inside comment body
- Add test verifying comment macro supports multi-line comment bodies

* Standardize JSDoc type annotations to use explicit null defaults and union types instead of nullable shorthand

* Remove individual test timeout configurations from frontend macro test files

* Add positional argument definitions with sample values and descriptions to core, env, and time macros

- Add sampleValue field to MacroPositionalArgDef typedef (optional string)
- Replace numeric requiredArgs with positional argument definition arrays in roll, banned, outlet, datetimeformat, and timeDiff macros
- Include name, sampleValue, description, and type fields for each positional argument
- Update timeDiff description to clarify absolute difference calculation
- Remove unnecessary blank lines in

* Remove async/await from macro engine evaluation and convert all macro handlers to synchronous execution

Makes me sad, but such is life

* Refactor substituteParams to use options object signature with backward compatibility for legacy positional arguments

- Rename original substituteParams to substituteParamsLegacy with unchanged positional argument signature
- Rename substituteParamsNew to substituteParams as the new primary function
- Add automatic detection and routing of legacy positional argument calls to substituteParamsLegacy
- Update substituteParamsExtended to use new options object signature and mark as deprecated

* forgor

* fix missing import, and package-lock, finally. Maybe.

* Add experimental macro engine toggle to UI settings

- Add experimental_macro_engine setting to default settings.json
- Move experimental_macro_engine property to correct position in power_user object (with other experimental settings)
- Add checkbox UI control in settings panel with flask icon and tooltip explaining nested macro resolution and logical replacement order
- Wire up checkbox event handler to save experimental_macro_engine setting
- Load experimental_macro_engine state on settings initialization

* Refactor macro pre/post-processing from parser to engine and improve error handling

- Move legacy macro pre-processing (time_UTC format) from MacroParser to MacroEngine #runPreProcessors
- Move trim macro post-processing to MacroEngine #runPostProcessors to handle cross-boundary behavior
- Remove MacroLexer import from MacroEngine (now handled by MacroParser.parseDocument)
- Update MacroParser.parseDocument to return separate lexingErrors and parserErrors arrays

* Add {{trim}} macro placeholder that defers to post-processing for cross-boundary whitespace handling

* Add eslint-plugin-playwright

* Add logMacroSyntaxWarning function for structured lexer/parser error reporting with compact human-readable payload

* Small code review fixes

- import event types from events.js
- Add chained fallback to {{input}}, just to be safe
- switched variables.js to export-per-method
- minor text adjustments in registered macro docs

* fix lexer not capturing linebreaks correctly & simplify plaintext token

- Replace alternation-based pattern with negated character class approach
- Use `(?:[^{]|\{(?!\{))+` to match non-brace chars or single braces not followed by another brace
- Add unicode flag for consistency
- Update comment to clarify intent: consume anything that is not the start of a macro '{{'

* Skip macro processing on char/group fields for env build (performance)

- Add returnRaw parameter to getCharacterCardFields to optionally return raw values without baseChatReplace
- Add returnRaw parameter to getGroupCharacterCards for consistent raw value handling
- Replace direct baseChatReplace calls with conditional transform function based on returnRaw flag
- Apply collapseNewlines when returnRaw is true and collapse_newlines setting is enabled
- Update MacroEnvBuilder to use the returnRaw as true

* Fix lexer failing to handle literal '{' before macro openers

- Add PlaintextOpenBrace token to lexer with pattern `/\{(?=\{\{)/` to match single '{' immediately before '{{'
- Update lexer mode definition to consume PlaintextOpenBrace before attempting macro start
- Update parser document rule to handle PlaintextOpenBrace as alternative to Plaintext
- Update MacroCstWalker to collect both Plaintext and Plaintext.OpenBrace tokens when building document items

* Add legacy non-curly marker pre-processing (<USER>, <BOT>, <CHAR>, <GROUP>, <CHARIFNOTGROUP>)

- Add pre-processing step in MacroEngine to rewrite legacy angle-bracket markers into their curly-brace macro equivalents
- Map <USER> → {{user}}, <BOT> → {{char}}, <CHAR> → {{char}}, <GROUP> → {{group}}, <CHARIFNOTGROUP> → {{charIfNotGroup}}
- Add e2e tests verifying legacy marker resolution through the engine pipeline
- Tests cover <USER>, <BOT>/<CHAR>, and <GROUP>/<CHARIFNOTGROUP> markers

* Add env.content to MacroEnv and use it for deterministic {{pick}} hashing

- Add content property to MacroEnv type definition and MacroEnvBuilder to expose the full original input string
- Update {{pick}} macro handler to use env.content hash instead of rawListString hash for seed generation
- Ensures deterministic behavior when the same prompt position contains different list items across evaluations
- Prevents {{pick}} from returning different values when nested macros resolve to different intermediate

* add e2e tests for env.content exposure and deterministic {{pick}} behavior

- Rename getChatIdHashCore → getChatIdHash in core-macros.js for consistency
- Add e2e test verifying env.content is exposed to macro handlers
- Add e2e test confirming {{pick}} returns stable results for same chat and content
- Tests verify deterministic behavior by comparing multiple evaluations with fixed chat_id_hash

* Rename chat macro helper functions to remove 'Core' suffix for consistency

* Add MacrosParser deprecation warnings and bridge to new macro engine when experimental flag enabled

- Import macroSystem and power_user for experimental macro engine integration
- Add @deprecated JSDoc tag to MacrosParser class
- Add #logDeprecated helper to warn about deprecated MacrosParser methods
- Add #registerMacroInNewEngine to bridge legacy macro registrations into new engine
- Add #unregisterMacroInNewEngine to bridge legacy macro unregistrations
- Log deprecation warnings in get, has, register

* Fix lint in tests

* Add comprehensive bracket handling tests and improve macro lexer/parser resilience to invalid syntax

- Add logMacroGeneralError function for non-macro-specific error logging
- Add CST validation check in MacroEngine to return original input if parser produces invalid CST
- Wrap MacroCstWalker.evaluateDocument in try-catch to gracefully handle evaluation failures
- Update MacroLexer Unknown token pattern to capture single closing braces not followed by another closing brace
- Add fallback mode exit

* Add error recovery for incomplete macros by flattening them to plaintext while preserving nested complete macros

- Enable Chevrotain error recovery in MacroParser constructor
- Add #isRecoveryToken helper to detect tokens inserted during error recovery
- Add #flattenIncompleteMacro to recursively convert incomplete macro nodes into plaintext items
- Update #collectDocumentItems to detect recovery-inserted Macro.End tokens and flatten incomplete macros
- Simplify plaintext token collection to use

* Add e2e test verifying nested macros resolve even when outer macro has invalid argument count

* Remove unused macro CST node caching mechanism from MacroCstWalker

* Add JSDoc type re-exports and register shorthand to macro-system.js for improved DX

- Re-export commonly used JSDoc types from MacroRegistry and MacroEnv modules for easier consumption by external code
- Add macros.register shorthand function bound to MacroRegistry.registerMacro for convenient macro registration
- Includes MacroDefinitionOptions, MacroHandler, MacroEnv, and related type definitions

* Migrate legacy macro registrations to new macro system API across multiple modules

- Replace MacrosParser.registerMacro calls with macros.register using object-based configuration
- Update imports from './macros.js' to './macros/macro-system.js'
- Extract macro registration into dedicated registerAuthorsNoteMacros function in authors-note.js
- Add descriptions to all macro registrations for better documentation
- Update MacrosParser iterator to yield from new registry when experimental engine is enabled

* Add MacroCategory to all macro registrations across codebase for improved organization and discoverability

- Import MacroCategory from macro-system.js in authors-note.js, memory/index.js, stable-diffusion/index.js, and macros.js
- Add category property to all macro registrations using appropriate MacroCategory values
- Assign 'legacy' category to MacrosParser auto-registered macros
- Categorize macros across chat, character, prompts, utility, random, state, time, and names categories

* fix lint

* Add MacroBrowser UI component and integrate macro help system into chat interface

- Add implementation of MacroBrowser
- Add macros.css stylesheet link to index.html
- Update core-macros.js ban macro to return empty string instead of 'Empty string' description
- Add sampleValue property to positional argument definitions in MacroRegistry
- Generate default sampleValue for numeric requiredArgs using 'arg{n}' pattern
- Set default description to '<no description>' and returns to '<empty string>' when empty

* Reorder JSDoc typedef declarations in MacroRegistry.js to have the most relevant first

* fix missing category in legacy initMacros

* Add displayOverride and exampleUsage properties to macro registration system

- Add displayOverride property to MacroDefinitionOptions for custom signature display
- Add exampleUsage property to MacroDefinitionOptions for documentation examples
- Update MacroRegistry.registerMacro to validate and process displayOverride and exampleUsage
- Add logMacroRegisterWarning function to MacroDiagnostics for registration-time warnings
- Auto-wrap displayOverride and exampleUsage in curly braces if missing

* Add error handling to MacroRegistry.registerMacro and logMacroRegisterError diagnostic function

- Add logMacroRegisterError function to MacroDiagnostics for registration failures
- Wrap MacroRegistry.registerMacro body in try-catch to handle registration errors gracefully
- Change registerMacro return type from MacroDefinition to MacroDefinition|null
- Log registration errors and return null instead of throwing, preventing macro registration failures from breaking the application

* Optimize character card field access with lazy evaluation to improve macro execution performance

* Add brace unescaping to MacroEngine post-processing to support literal curly braces in macro output

- Add regex replacement to unescape \{ and \} to { and } after macro execution
- Allows users to output literal braces by escaping them with backslashes
- Escaped sequences like \{\{ don't match MacroStart pattern and pass through as plain text

* Add alias system to macro registry with UI support for displaying and navigating macro aliases

- Add `aliases` property to MacroDefinitionOptions for defining alternative macro names
- Add `aliasOf` and `aliasVisible` properties to MacroDefinition to track alias relationships
- Update MacroRegistry.registerMacro to create alias entries pointing to primary definitions
- Add `getPrimaryMacro` method to retrieve primary definition from alias names
- Add filtering options to `getAllMacros` to exclude aliases

* Strip curly braces from MacroBrowser search query to match macro name format in search definitions

* Add enhanced macro autocomplete with argument hints and context-aware suggestions

- Add comprehensive CSS styling for enhanced macro autocomplete items with flex layout
- Add argument hint banner styling with gradient background and border
- Add current argument highlighting in details panel
- Export formatMacroSignature, createSourceIndicator, createAliasIndicator, and createTypeBadge from MacroBrowser for reuse

* Fix macro autocomplete not showing details/arguments because of trailing colons

* Change macro details CSS selectors from `.macroBrowser` to `.macro-details` for better reusability and update autocomplete to use macro enum icon

- Replace all `.macroBrowser` selectors with `.macro-details` in macros.css to allow macro details panel styling to work outside MacroBrowser context
- Change autocomplete option icon from hardcoded '{}' to `enumIcons.macro` for consistency
- Remove redundant `showCategory: false` option from renderMacroDetails call in autocomplete (now handled by default

* Add MacroArgType enum to replace string literal type union for macro argument types

- Add MacroArgType enum with STRING, INTEGER, NUMBER, and BOOLEAN values
- Replace MacroArgType typedef string literal union with enum reference
- Export MacroArgType from macro-system.js alongside MacroCategory
- Remove MacroArgType typedef re-export (now an enum, not a type)

* Add support for multiple argument types in macro definitions

- Update createTypeBadge to handle both single type and array of types, displaying as "type1 | type2" with tooltip
- Add JSDoc comments to MacroArgType enum values explaining each type
- Update MacroArgDefinition typedef to allow type property to be single MacroArgType or array
- Update MacroRegistry.registerMacro to validate array of types and default empty arrays to 'string'
- Update validateArgTypes to check if argument value matches

* Add returnType property to macro definitions with automatic type badge display in macro details panel

- Rename MacroArgType enum to MacroValueType to reflect dual use for arguments and return types
- Add returnType property to MacroDefinitionOptions (defaults to MacroValueType.STRING)
- Add returnType validation in MacroRegistry.registerMacro to ensure valid type values
- Update renderMacroDetails to always show Returns section with type badge
- Add macro-returns-content CSS class with flex layout for type

* Add detailed argument definitions, return types, and example usage to all variable macros

- Replace numeric requiredArgs with detailed argument definition objects including name, type, and description
- Add returnType property to all macros that return values (inc/dec/get variants)
- Add returns property descriptions to all macros
- Add exampleUsage arrays demonstrating typical usage patterns for each macro
- Apply changes consistently

* Add returns descriptions and return types to core macro definitions with improved documentation

* Add returns descriptions, display overrides, and example usage to time macros with improved documentation

* fix lint

* Add returns descriptions and return types to chat, environment, instruct, and state macros with improved documentation

* Add missing properties to dynamic macro definition override to match MacroDefinitionOptions structure

* Replace console logging with MacroDiagnostics logging in MacroEnvBuilder and MacroRegistry

* Add support for array-based argument types in macro autocomplete with union type display and tooltip

* Rename requiredArgs to unnamedArgs and add support for optional unnamed arguments with bracket notation in macro signatures and hints

* Add optional offset argument definition to time macro with type, sample value, and description

* Simplify example usage for random and pick macros by removing surrounding context text

* Add default value display for optional macro arguments in autocomplete and browser documentation

* Fix macro args defaultValue not being converted into the normalized values for register and display

* Allow STscript macro auto completion to still show up when typing closing braces

* Add space macro with optional count argument for inserting multiple spaces

* Add optional count argument to newline macro for inserting multiple newlines with default value of 1

* Remove MutationObserver and CTRL+F keyboard event handling from MacroBrowser

* fix lint

* Register `{{summary}}` macro for both old and new macro engines based on experimental flag

* Register `{{charPrefix}}` and `{{charNegativePrefix}}` macros for both old and new macro engines based on experimental flag

* Update hidden alias badge text to indicate deprecation status

* Simplify macro name validation and error handling in MacroRegistry

Remove redundant `macroName` variable by normalizing `name` parameter early and reusing it throughout the registration flow. Consolidate trim checks in validation condition.

* Remove exp macro engine flag checks from auto complete and help, and remove legacy macro template

Move macro registration to always use new engine regardless of experimental flag. Remove conditional logic for `experimental_macro_engine` in MacroBrowser, system messages, and slash command parser. Delete legacy `macros.html` template and associated static macro help generation. Always use MacroBrowser for macro documentation display.

* Rename macros to camelCase and add backward-compatible aliases

Rename `description`, `personality`, `scenario`, `creatorNotes` to `charDescription`, `charPersonality`, `charScenario`, `charCreatorNotes` respectively. Rename `idle_duration` to `idleDuration`. Add old names as aliases for backward compatibility. Add `comment` as visible alias for `//` macro. Mark `idle_duration` alias as hidden.

* fix `random` and `pick` macros by using list parameter directly instead of raw string

Remove `raw` parameter from `random` and `pick` macro handlers. Simplify legacy comma-separated list handling by using `list[0]` directly instead of `rawListString`. Rename `items` variable to `list` in `pick` macro for consistency.

* fix `random` and `pick` not handling all colon-separated lists as before

Extract `readSingleArgsRandomList` helper function to handle legacy comma-separated and double-colon list parsing. Reuse this helper in both `random` and `pick` macro handlers to eliminate duplicated list parsing logic.

---------

Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Co-authored-by: bmen25124 <bmen25124@gmail.com>
2025-12-15 21:29:46 +02:00

4565 lines
170 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Fuse, Handlebars } from '../lib.js';
import {
saveSettingsDebounced,
scrollChatToBottom,
characters,
reloadMarkdownProcessor,
reloadCurrentChat,
getRequestHeaders,
substituteParams,
eventSource,
event_types,
getCurrentChatId,
printCharactersDebounced,
setCharacterId,
setEditedMessageId,
chat,
getFirstDisplayedMessageId,
showMoreMessages,
saveSettings,
saveChatConditional,
setAnimationDuration,
ANIMATION_DURATION_DEFAULT,
setActiveGroup,
setActiveCharacter,
entitiesFilter,
doNewChat,
online_status,
messageFormatting,
extension_prompt_types,
extension_prompt_roles,
deleteMessage,
} from '../script.js';
import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
import {
groups,
resetSelectedGroup,
} from './group-chats.js';
import {
instruct_presets,
loadInstructMode,
names_behavior_types,
selectInstructPreset,
updateBindModelTemplatesState,
} from './instruct-mode.js';
import { getTagsList, tag_import_setting, tag_map, tag_sort_mode, tags } from './tags.js';
import { tokenizers } from './tokenizers.js';
import { BIAS_CACHE } from './logit-bias.js';
import { renderTemplateAsync } from './templates.js';
import { countOccurrences, debounce, delay, download, getFileText, getSanitizedFilename, getStringHash, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
import { FILTER_TYPES } from './filters.js';
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_STATE, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './popup.js';
import { loadSystemPrompts } from './sysprompt.js';
import { fuzzySearchCategories } from './filters.js';
import { accountStorage } from './util/AccountStorage.js';
import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';
import { bindModelTemplates } from './chat-templates.js';
import { IMAGE_OVERSWIPE, MEDIA_DISPLAY } from './constants.js';
import { t } from './i18n.js';
export const toastPositionClasses = [
'toast-top-left',
'toast-top-center',
'toast-top-right',
'toast-bottom-left',
'toast-bottom-center',
'toast-bottom-right',
];
export const MAX_CONTEXT_DEFAULT = 8192;
export const MAX_RESPONSE_DEFAULT = 2048;
const MAX_CONTEXT_UNLOCKED = 512 * 1024;
const MAX_RESPONSE_UNLOCKED = 64 * 1024;
const unlockedMaxContextStep = 512;
const maxContextMin = 512;
const maxContextStep = 64;
const defaultStoryString = '{{#if system}}{{system}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}\'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}';
const defaultExampleSeparator = '***';
const defaultChatStart = '***';
const defaultToastPosition = 'toast-top-center';
const avatar_styles = {
ROUND: 0,
RECTANGULAR: 1,
SQUARE: 2,
ROUNDED: 3,
};
export const chat_styles = {
DEFAULT: 0,
BUBBLES: 1,
DOCUMENT: 2,
};
export const send_on_enter_options = {
DISABLED: -1,
AUTO: 0,
ENABLED: 1,
};
export const persona_description_positions = {
IN_PROMPT: 0,
/**
* @deprecated Use persona_description_positions.IN_PROMPT instead.
*/
AFTER_CHAR: 1,
TOP_AN: 2,
BOTTOM_AN: 3,
AT_DEPTH: 4,
NONE: 9,
};
export const power_user = {
charListGrid: false,
tokenizer: tokenizers.BEST_MATCH,
token_padding: 64,
collapse_newlines: false,
pin_examples: false,
strip_examples: false,
trim_sentences: false,
always_force_name2: false,
user_prompt_bias: '',
show_user_prompt_bias: true,
auto_continue: {
enabled: false,
allow_chat_completions: false,
target_length: 400,
},
markdown_escape_strings: '',
chat_truncation: 100,
streaming_fps: 30,
smooth_streaming: false,
smooth_streaming_no_think: false,
smooth_streaming_speed: 50,
stream_fade_in: false,
fast_ui_mode: true,
avatar_style: avatar_styles.ROUND,
chat_display: chat_styles.DEFAULT,
toastr_position: defaultToastPosition,
chat_width: 50,
never_resize_avatars: false,
show_card_avatar_urls: false,
play_message_sound: false,
play_sound_unfocused: true,
auto_save_msg_edits: false,
confirm_message_delete: true,
sort_field: 'name',
sort_order: 'asc',
sort_rule: null,
font_scale: 1,
blur_strength: 10,
shadow_width: 2,
main_text_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBodyColor').trim()}`,
italics_text_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeEmColor').trim()}`,
underline_text_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeUnderlineColor').trim()}`,
quote_text_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeQuoteColor').trim()}`,
blur_tint_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBlurTintColor').trim()}`,
chat_tint_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeChatTintColor').trim()}`,
user_mes_blur_tint_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeUserMesBlurTintColor').trim()}`,
bot_mes_blur_tint_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBotMesBlurTintColor').trim()}`,
shadow_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeShadowColor').trim()}`,
border_color: `${getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBorderColor').trim()}`,
custom_css: '',
waifuMode: false,
movingUI: false,
movingUIState: {},
movingUIPreset: '',
noShadows: false,
theme: 'Default (Dark) 1.7.1',
gestures: true,
auto_swipe: false,
auto_swipe_minimum_length: 0,
auto_swipe_blacklist: [],
auto_swipe_blacklist_threshold: 2,
auto_scroll_chat_to_bottom: true,
auto_fix_generated_markdown: true,
send_on_enter: send_on_enter_options.AUTO,
console_log_prompts: false,
request_token_probabilities: false,
show_group_chat_queue: false,
allow_name1_display: false,
allow_name2_display: false,
hotswap_enabled: true,
timer_enabled: true,
timestamps_enabled: true,
timestamp_model_icon: false,
mesIDDisplay_enabled: false,
hideChatAvatars_enabled: false,
max_context_unlocked: false,
message_token_count_enabled: false,
expand_message_actions: false,
enableZenSliders: false,
enableLabMode: false,
prefer_character_prompt: true,
prefer_character_jailbreak: true,
quick_continue: false,
quick_impersonate: false,
continue_on_send: false,
trim_spaces: true,
relaxed_api_urls: false,
world_import_dialog: true,
enable_auto_select_input: false,
enable_md_hotkeys: false,
tag_import_setting: tag_import_setting.ASK,
tag_sort_mode: tag_sort_mode.MANUAL,
disable_group_trimming: false,
single_line: false,
instruct: {
enabled: false,
preset: 'Alpaca',
input_sequence: '### Instruction:',
input_suffix: '',
output_sequence: '### Response:',
output_suffix: '',
system_sequence: '',
system_suffix: '',
last_system_sequence: '',
first_input_sequence: '',
first_output_sequence: '',
last_input_sequence: '',
last_output_sequence: '',
story_string_prefix: '',
story_string_suffix: '',
stop_sequence: '',
wrap: true,
macro: true,
names_behavior: names_behavior_types.FORCE,
activation_regex: '',
bind_to_context: false,
user_alignment_message: '',
system_same_as_user: false,
/** @deprecated Use output_suffix instead */
separator_sequence: '',
sequences_as_stop_strings: true,
},
context: {
preset: 'Default',
story_string: defaultStoryString,
chat_start: defaultChatStart,
example_separator: defaultExampleSeparator,
use_stop_strings: true,
names_as_stop_strings: true,
story_string_position: extension_prompt_types.IN_PROMPT,
story_string_role: extension_prompt_roles.SYSTEM,
story_string_depth: 1,
},
instruct_derived: false,
context_derived: false,
context_size_derived: false,
/** User-defined model identifier / chat template hash to instruct/context template mappings */
model_templates_mappings: {},
/** The chat template hash of the currently loaded model, if any; used when deriving mappings */
chat_template_hash: '',
sysprompt: {
enabled: true,
name: 'Neutral - Chat',
content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
post_history: '',
},
reasoning: {
name: DEFAULT_REASONING_TEMPLATE,
auto_parse: false,
add_to_prompts: false,
auto_expand: false,
show_hidden: false,
prefix: '<think>\n',
suffix: '\n</think>',
separator: '\n\n',
max_additions: 1,
},
personas: {},
default_persona: null,
persona_descriptions: {},
persona_description: '',
persona_description_position: persona_description_positions.IN_PROMPT,
persona_description_role: 0,
persona_description_depth: 2,
persona_description_lorebook: '',
persona_show_notifications: true,
persona_sort_order: 'asc',
custom_stopping_strings: '',
custom_stopping_strings_macro: true,
fuzzy_search: false,
encode_tags: false,
experimental_macro_engine: false,
servers: [],
bogus_folders: false,
zoomed_avatar_magnification: false,
show_tag_filters: false,
aux_field: 'character_version',
stscript: {
matching: 'fuzzy',
autocomplete: {
state: AUTOCOMPLETE_STATE.ALWAYS,
autoHide: false,
style: 'theme',
font: {
scale: 0.8,
},
width: {
left: AUTOCOMPLETE_WIDTH.CHAT,
right: AUTOCOMPLETE_WIDTH.CHAT,
},
select: AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER,
},
parser: {
/**@type {Object.<PARSER_FLAG,boolean>} */
flags: {},
},
},
restore_user_input: true,
reduced_motion: false,
compact_input_area: true,
show_swipe_num_all_messages: false,
auto_connect: false,
auto_load_chat: false,
forbid_external_media: true,
external_media_allowed_overrides: [],
external_media_forbidden_overrides: [],
pin_styles: true,
click_to_edit: false,
media_display: MEDIA_DISPLAY.LIST,
image_overswipe: IMAGE_OVERSWIPE.GENERATE,
};
let themes = [];
let movingUIPresets = [];
export let context_presets = [];
const storage_keys = {
storyStringValidationCache: 'StoryStringValidationCache',
};
const contextControls = [
// Power user context scoped settings
{ id: 'context_story_string', property: 'story_string', isCheckbox: false, isGlobalSetting: false },
{ id: 'context_example_separator', property: 'example_separator', isCheckbox: false, isGlobalSetting: false },
{ id: 'context_chat_start', property: 'chat_start', isCheckbox: false, isGlobalSetting: false },
{ id: 'context_use_stop_strings', property: 'use_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: false },
{ id: 'context_names_as_stop_strings', property: 'names_as_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: true },
{ id: 'context_story_string_position', property: 'story_string_position', isCheckbox: false, isGlobalSetting: false, defaultValue: extension_prompt_types.IN_PROMPT, trigger: true },
{ id: 'context_story_string_depth', property: 'story_string_depth', isCheckbox: false, isGlobalSetting: false, defaultValue: 1 },
{ id: 'context_story_string_role', property: 'story_string_role', isCheckbox: false, isGlobalSetting: false, defaultValue: extension_prompt_roles.SYSTEM },
// Existing power user settings
{ id: 'always-force-name2-checkbox', property: 'always_force_name2', isCheckbox: true, isGlobalSetting: true, defaultValue: true },
{ id: 'trim_sentences_checkbox', property: 'trim_sentences', isCheckbox: true, isGlobalSetting: true, defaultValue: false },
{ id: 'single_line', property: 'single_line', isCheckbox: true, isGlobalSetting: true, defaultValue: false },
];
let browser_has_focus = true;
const debug_functions = [];
const setHotswapsDebounced = debounce(favsToHotswap);
export function playMessageSound() {
if (!power_user.play_message_sound) {
return;
}
if (power_user.play_sound_unfocused && browser_has_focus) {
return;
}
const audio = document.getElementById('audio_message_sound');
if (audio instanceof HTMLAudioElement) {
audio.volume = 0.8;
audio.pause();
audio.currentTime = 0;
audio.play();
}
}
/**
* Replaces consecutive newlines with a single newline.
* @param {string} x String to be processed.
* @returns {string} Processed string.
* @example
* collapseNewlines("\n\n\n"); // "\n"
*/
export function collapseNewlines(x) {
return x.replaceAll(/\n+/g, '\n');
}
/**
* Fix formatting problems in markdown.
* @param {string} text Text to be processed.
* @param {boolean} forDisplay Whether the text is being processed for display.
* @returns {string} Processed text.
* @example
* "^example * text*\n" // "^example *text*\n"
* "^*example * text\n"// "^*example* text\n"
* "^example *text *\n" // "^example *text*\n"
* "^* example * text\n" // "^*example* text\n"
* // take note that the side you move the asterisk depends on where its pairing is
* // i.e. both of the following strings have the same broken asterisk ' * ',
* // but you move the first to the left and the second to the right, to match the non-broken asterisk
* "^example * text*\n" // "^*example * text\n"
* // and you HAVE to handle the cases where multiple pairs of asterisks exist in the same line
* "^example * text* * harder problem *\n" // "^example *text* *harder problem*\n"
*/
export function fixMarkdown(text, forDisplay) {
// Find pairs of formatting characters and capture the text in between them
const format = /([*_]{1,2})([\s\S]*?)\1/gm;
let matches = [];
let match;
while ((match = format.exec(text)) !== null) {
matches.push(match);
}
// Iterate through the matches and replace adjacent spaces immediately beside formatting characters
let newText = text;
for (let i = matches.length - 1; i >= 0; i--) {
let matchText = matches[i][0];
let replacementText = matchText.replace(/(\*|_)([\t \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]+)|([\t \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]+)(\*|_)/g, '$1$4');
newText = newText.slice(0, matches[i].index) + replacementText + newText.slice(matches[i].index + matchText.length);
}
// Don't auto-fix asterisks if this is a message clean-up procedure.
// It botches the continue function. Apply this to display only.
if (!forDisplay) {
return newText;
}
const splitText = newText.split('\n');
// Fix asterisks, and quotes that are not paired
for (let index = 0; index < splitText.length; index++) {
const line = splitText[index];
const charsToCheck = ['*', '"'];
for (const char of charsToCheck) {
if (line.includes(char) && isOdd(countOccurrences(line, char))) {
splitText[index] = line.trimEnd() + char;
}
}
}
newText = splitText.join('\n');
return newText;
}
function switchHotswap() {
$('body').toggleClass('no-hotswap', !power_user.hotswap_enabled);
$('#hotswapEnabled').prop('checked', power_user.hotswap_enabled);
}
function switchTimer() {
$('body').toggleClass('no-timer', !power_user.timer_enabled);
$('#messageTimerEnabled').prop('checked', power_user.timer_enabled);
}
function switchTimestamps() {
$('body').toggleClass('no-timestamps', !power_user.timestamps_enabled);
$('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);
}
function switchIcons() {
$('body').toggleClass('no-modelIcons', !power_user.timestamp_model_icon);
$('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);
}
function switchTokenCount() {
$('body').toggleClass('no-tokenCount', !power_user.message_token_count_enabled);
$('#messageTokensEnabled').prop('checked', power_user.message_token_count_enabled);
}
function switchMesIDDisplay() {
$('body').toggleClass('no-mesIDDisplay', !power_user.mesIDDisplay_enabled);
$('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
}
function switchHideChatAvatars() {
$('body').toggleClass('hideChatAvatars', power_user.hideChatAvatars_enabled);
$('#hideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);
}
function switchMessageActions() {
$('body').toggleClass('expandMessageActions', power_user.expand_message_actions);
$('#expandMessageActions').prop('checked', power_user.expand_message_actions);
$('.extraMesButtons, .extraMesButtonsHint').removeAttr('style');
}
function switchReducedMotion() {
jQuery.fx.off = power_user.reduced_motion;
const overrideDuration = power_user.reduced_motion ? 0 : ANIMATION_DURATION_DEFAULT;
setAnimationDuration(overrideDuration);
$('#reduced_motion').prop('checked', power_user.reduced_motion);
$('body').toggleClass('reduced-motion', power_user.reduced_motion);
}
function switchCompactInputArea() {
$('#send_form').toggleClass('compact', power_user.compact_input_area);
$('#compact_input_area').prop('checked', power_user.compact_input_area);
}
function switchSwipeNumAllMessages() {
$('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
$('body').toggleClass('swipeAllMessages', !!power_user.show_swipe_num_all_messages);
}
var originalSliderValues = [];
async function switchLabMode({ noReset = false } = {}) {
/* if (power_user.enableZenSliders && power_user.enableLabMode) {
toastr.warning("Can't start Lab Mode while Zen Sliders are active")
return
//$("#enableZenSliders").trigger('click')
}
*/
await delay(100);
$('body').toggleClass('enableLabMode', power_user.enableLabMode);
$('#enableLabMode').prop('checked', power_user.enableLabMode);
if (power_user.enableLabMode) {
//save all original slider values into an array
$('#advanced-ai-config-block input').each(function () {
let id = $(this).attr('id');
let min = $(this).attr('min');
let max = $(this).attr('max');
let step = $(this).attr('step');
originalSliderValues.push({ id, min, max, step });
});
//console.log(originalSliderValues)
//remove limits on all inputs and hide sliders
$('#advanced-ai-config-block input')
.attr('min', '-99999')
.attr('max', '99999')
.attr('step', '0.001');
$('#labModeWarning').removeClass('displayNone');
//$("#advanced-ai-config-block input[type='range']").hide()
$('#amount_gen_counter').attr('min', '1')
.attr('max', '99999')
.attr('step', '1');
$('#amount_gen').attr('min', '1')
.attr('max', '99999')
.attr('step', '1');
} else if (!noReset) {
//re apply the original sliders values to each input
originalSliderValues.forEach(function (slider) {
$('#' + slider.id)
.attr('min', slider.min)
.attr('max', slider.max)
.attr('step', slider.step)
.trigger('input');
});
$('#advanced-ai-config-block input[type=\'range\']').show();
$('#labModeWarning').addClass('displayNone');
// To set the correct amount_gen back, we just call the function calculating it correctly
switchMaxContextSize();
}
}
async function switchZenSliders() {
await delay(100);
$('body').toggleClass('enableZenSliders', power_user.enableZenSliders);
$('#enableZenSliders').prop('checked', power_user.enableZenSliders);
if (power_user.enableZenSliders) {
$('#clickSlidersTips').hide();
$('#pro-settings-block input[type=\'number\']').hide();
//hide number inputs that are not 'seed' inputs
$(`#textgenerationwebui_api-settings :input[type='number']:not([id^='seed']):not([id^='n_']),
#kobold_api-settings :input[type='number']:not([id^='seed'])`).hide();
//hide original sliders
$(`#textgenerationwebui_api-settings input[type='range'],
#kobold_api-settings input[type='range'],
#pro-settings-block input[type='range']:not(#max_context)`) //exclude max context because its creation is handled by switchMaxContext()
.hide()
.each(function () {
//make a zen slider for each original slider
CreateZenSliders($(this));
});
//this is for when zensliders is toggled after pageload
switchMaxContextSize();
} else {
$('#clickSlidersTips').show();
revertOriginalSliders();
}
function revertOriginalSliders() {
$('#pro-settings-block input[type=\'number\']').show();
$(`#textgenerationwebui_api-settings input[type='number'],
#kobold_api-settings input[type='number']`).show();
$(`#textgenerationwebui_api-settings input[type='range'],
#kobold_api-settings input[type='range'],
#pro-settings-block input[type='range']`).each(function () {
$(this).show();
});
$('div[id$="_zenslider"]').remove();
}
}
async function CreateZenSliders(elmnt) {
var originalSlider = elmnt;
var sliderID = originalSlider.attr('id');
var sliderMin = Number(originalSlider.attr('min'));
var sliderMax = Number(originalSlider.attr('max'));
var sliderValue = originalSlider.val();
var sliderRange = sliderMax - sliderMin;
var numSteps = 20;
var decimals = 2;
var offVal, allVal;
var stepScale;
var steps;
if (sliderID == 'amount_gen') {
decimals = 0;
steps = [16, 50, 100, 150, 200, 256, 300, 400, 512, 1024];
sliderMin = 0;
sliderMax = steps.length - 1;
stepScale = 1;
numSteps = 10;
sliderValue = steps.indexOf(Number(sliderValue));
if (sliderValue === -1) { sliderValue = 4; } // default to '200' if origSlider has value we can't use
}
if (sliderID == 'rep_pen_range_textgenerationwebui') {
if (power_user.max_context_unlocked) {
steps = [0, 256, 512, 768, 1024, 2048, 4096, 8192, 16355, 24576, 32768, 49152, 65536, -1];
numSteps = 13;
allVal = 13;
} else {
steps = [0, 256, 512, 768, 1024, 2048, 4096, 8192, -1];
numSteps = 8;
allVal = 8;
}
decimals = 0;
offVal = 0;
sliderMin = 0;
sliderMax = steps.length - 1;
stepScale = 1;
sliderValue = steps.indexOf(Number(sliderValue));
if (sliderValue === -1) { sliderValue = allVal; } // default to allValue if origSlider has value we can't use
}
//customize decimals
if (sliderID == 'max_context' ||
sliderID == 'mirostat_mode_textgenerationwebui' ||
sliderID == 'mirostat_tau_textgenerationwebui' ||
sliderID == 'top_k_textgenerationwebui' ||
sliderID == 'num_beams_textgenerationwebui' ||
sliderID == 'no_repeat_ngram_size_textgenerationwebui' ||
sliderID == 'min_length_textgenerationwebui' ||
sliderID == 'top_k' ||
sliderID == 'mirostat_mode_kobold' ||
sliderID == 'rep_pen_range' ||
sliderID == 'dry_allowed_length_textgenerationwebui' ||
sliderID == 'rep_pen_decay_textgenerationwebui' ||
sliderID == 'dry_penalty_last_n_textgenerationwebui' ||
sliderID == 'max_tokens_second_textgenerationwebui') {
decimals = 0;
}
if (sliderID == 'min_temp_textgenerationwebui' ||
sliderID == 'max_temp_textgenerationwebui' ||
sliderID == 'smoothing_curve_textgenerationwebui' ||
sliderID == 'smoothing_factor_textgenerationwebui' ||
sliderID == 'dry_multiplier_textgenerationwebui' ||
sliderID == 'dry_base_textgenerationwebui') {
decimals = 2;
}
if (sliderID == 'eta_cutoff_textgenerationwebui' ||
sliderID == 'epsilon_cutoff_textgenerationwebui') {
numSteps = 50;
decimals = 1;
}
if (sliderID == 'nsigma') {
numSteps = 50;
decimals = 1;
}
//customize steps
if (sliderID == 'mirostat_mode_textgenerationwebui' ||
sliderID == 'mirostat_mode_kobold') {
numSteps = 2;
}
if (sliderID == 'encoder_rep_pen_textgenerationwebui') {
numSteps = 14;
}
if (sliderID == 'max_context') {
numSteps = 15;
}
if (sliderID == 'mirostat_tau_textgenerationwebui' ||
sliderID == 'top_k_textgenerationwebui' ||
sliderID == 'num_beams_textgenerationwebui' ||
sliderID == 'no_repeat_ngram_size_textgenerationwebui' ||
sliderID == 'epsilon_cutoff_textgenerationwebui' ||
sliderID == 'tfs_textgenerationwebui' ||
sliderID == 'min_p_textgenerationwebui' ||
sliderID == 'temp_textgenerationwebui' ||
sliderID == 'temp') {
numSteps = 20;
}
if (sliderID == 'mirostat_eta_textgenerationwebui' ||
sliderID == 'penalty_alpha_textgenerationwebui' ||
sliderID == 'length_penalty_textgenerationwebui' ||
sliderID == 'min_temp_textgenerationwebui' ||
sliderID == 'max_temp_textgenerationwebui') {
numSteps = 50;
}
//customize off values
if (sliderID == 'presence_pen_textgenerationwebui' ||
sliderID == 'freq_pen_textgenerationwebui' ||
sliderID == 'mirostat_mode_textgenerationwebui' ||
sliderID == 'mirostat_mode_kobold' ||
sliderID == 'mirostat_tau_textgenerationwebui' ||
sliderID == 'mirostat_tau_kobold' ||
sliderID == 'mirostat_eta_textgenerationwebui' ||
sliderID == 'mirostat_eta_kobold' ||
sliderID == 'min_p_textgenerationwebui' ||
sliderID == 'min_p' ||
sliderID == 'no_repeat_ngram_size_textgenerationwebui' ||
sliderID == 'penalty_alpha_textgenerationwebui' ||
sliderID == 'length_penalty_textgenerationwebui' ||
sliderID == 'epsilon_cutoff_textgenerationwebui' ||
sliderID == 'nsigma' ||
sliderID == 'rep_pen_range' ||
sliderID == 'eta_cutoff_textgenerationwebui' ||
sliderID == 'top_a_textgenerationwebui' ||
sliderID == 'top_a' ||
sliderID == 'top_k_textgenerationwebui' ||
sliderID == 'top_k' ||
sliderID == 'rep_pen_slope' ||
sliderID == 'smoothing_factor_textgenerationwebui' ||
sliderID == 'smoothing_curve_textgenerationwebui' ||
sliderID == 'skew_textgenerationwebui' ||
sliderID == 'dry_multiplier_textgenerationwebui' ||
sliderID == 'min_length_textgenerationwebui') {
offVal = 0;
}
if (sliderID == 'rep_pen_textgenerationwebui' ||
sliderID == 'rep_pen' ||
sliderID == 'tfs_textgenerationwebui' ||
sliderID == 'tfs' ||
sliderID == 'top_p_textgenerationwebui' ||
sliderID == 'top_p' ||
sliderID == 'typical_p_textgenerationwebui' ||
sliderID == 'typical_p' ||
sliderID == 'encoder_rep_pen_textgenerationwebui' ||
sliderID == 'temp_textgenerationwebui' ||
sliderID == 'temp' ||
sliderID == 'min_temp_textgenerationwebui' ||
sliderID == 'max_temp_textgenerationwebui' ||
sliderID == 'dynatemp_exponent_textgenerationwebui' ||
sliderID == 'guidance_scale_textgenerationwebui' ||
sliderID == 'rep_pen_slope_textgenerationwebui' ||
sliderID == 'guidance_scale') {
offVal = 1;
}
if (sliderID == 'guidance_scale_textgenerationwebui') {
numSteps = 78;
}
if (sliderID == 'top_k_textgenerationwebui') {
sliderMin = 0;
}
//customize amt gen steps
if (sliderID !== 'amount_gen' && sliderID !== 'rep_pen_range_textgenerationwebui') {
stepScale = sliderRange / numSteps;
}
var newSlider = $('<div>')
.attr('id', `${sliderID}_zenslider`)
.css('width', '100%')
.insertBefore(originalSlider);
newSlider.slider({
value: sliderValue,
step: stepScale,
min: sliderMin,
max: sliderMax,
create: async function () {
await delay(100);
var handle = $(this).find('.ui-slider-handle');
var handleText, stepNumber, leftMargin;
//handling creation of amt_gen
if (newSlider.attr('id') == 'amount_gen_zenslider') {
handleText = steps[sliderValue];
stepNumber = sliderValue;
leftMargin = ((stepNumber) / numSteps) * 50 * -1;
handle.text(handleText)
.css('margin-left', `${leftMargin}px`);
//console.log(`${newSlider.attr('id')} initial value:${handleText}, stepNum:${stepNumber}, numSteps:${numSteps}, left-margin:${leftMargin}`)
}
//handling creation of rep_pen_range for ooba
else if (newSlider.attr('id') == 'rep_pen_range_textgenerationwebui_zenslider') {
if ($('#rep_pen_range_textgenerationwebui_zensliders').length !== 0) {
$('#rep_pen_range_textgenerationwebui_zensliders').remove();
}
handleText = steps[sliderValue];
stepNumber = sliderValue;
leftMargin = ((stepNumber) / numSteps) * 50 * -1;
if (sliderValue === offVal) {
handleText = 'Off';
handle.css('color', 'rgba(128,128,128,0.5');
}
else if (sliderValue === allVal) { handleText = 'All'; }
else { handle.css('color', ''); }
handle.text(handleText)
.css('margin-left', `${leftMargin}px`);
//console.log(sliderValue, handleText, offVal, allVal)
//console.log(`${newSlider.attr('id')} sliderValue = ${sliderValue}, handleText:${handleText}, stepNum:${stepNumber}, numSteps:${numSteps}, left-margin:${leftMargin}`)
originalSlider.val(steps[sliderValue]);
}
//create all other sliders
else {
var numVal = Number(sliderValue).toFixed(decimals);
offVal = Number(offVal).toFixed(decimals);
if (numVal === offVal) {
handle.text('Off').css('color', 'rgba(128,128,128,0.5');
} else {
handle.text(numVal).css('color', '');
}
stepNumber = ((sliderValue - sliderMin) / stepScale);
leftMargin = (stepNumber / numSteps) * 50 * -1;
originalSlider.val(numVal)
.data('newSlider', newSlider);
//console.log(`${newSlider.attr('id')} sliderValue = ${sliderValue}, handleText:${handleText, numVal}, stepNum:${stepNumber}, numSteps:${numSteps}, left-margin:${leftMargin}`)
var isManualInput = false;
var valueBeforeManualInput;
handle.css('margin-left', `${leftMargin}px`)
.attr('contenteditable', 'true')
//these sliders need listeners for manual inputs
.on('click', function () {
//this just selects all the text in the handle so user can overwrite easily
//needed because JQUery UI uses left/right arrow keys as well as home/end to move the slider..
valueBeforeManualInput = newSlider.val();
console.log(valueBeforeManualInput);
let handleElement = handle.get(0);
let range = document.createRange();
range.selectNodeContents(handleElement);
let selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
})
.on('keyup', function (e) {
valueBeforeManualInput = numVal;
//console.log(valueBeforeManualInput, numVal, handleText);
isManualInput = true;
//allow enter to trigger slider update
if (e.key === 'Enter') {
e.preventDefault();
handle.trigger('blur');
}
})
//trigger slider changes when user clicks away
.on('mouseup blur', function () {
let manualInput = parseFloat(parseFloat(handle.text()).toFixed(decimals));
if (isManualInput) {
//disallow manual inputs outside acceptable range
if (manualInput >= sliderMin && manualInput <= sliderMax) {
//if value is ok, assign to slider and update handle text and position
newSlider.val(manualInput);
handleSlideEvent.call(newSlider, null, { value: manualInput }, 'manual');
valueBeforeManualInput = manualInput;
} else {
//if value not ok, warn and reset to last known valid value
toastr.warning(`Invalid value. Must be between ${sliderMin} and ${sliderMax}`);
console.log(valueBeforeManualInput);
newSlider.val(valueBeforeManualInput);
handle.text(valueBeforeManualInput);
handleSlideEvent.call(newSlider, null, { value: parseFloat(valueBeforeManualInput) }, 'manual');
}
}
isManualInput = false;
});
}
//zenSlider creation done, hide the original
originalSlider.hide();
},
slide: handleSlideEvent,
});
function handleSlideEvent(event, ui, type) {
var handle = $(this).find('.ui-slider-handle');
var numVal = parseFloat(Number(ui.value).toFixed(decimals));
offVal = parseFloat(Number(offVal).toFixed(decimals));
allVal = parseFloat(Number(allVal).toFixed(decimals));
console.log(numVal, sliderMin, sliderMax, numVal > sliderMax, numVal < sliderMin);
if (numVal > sliderMax) { numVal = sliderMax; }
if (numVal < sliderMin) { numVal = sliderMin; }
var stepNumber = parseFloat(((ui.value - sliderMin) / stepScale).toFixed(0));
var handleText = (ui.value);
var leftMargin = (stepNumber / numSteps) * 50 * -1;
var perStepPercent = 1 / numSteps; //how far in % each step should be on the slider
var leftPos = newSlider.width() * (stepNumber * perStepPercent); //how big of a left margin to give the slider for manual inputs
/* console.log(`
numVal: ${numVal},
sliderMax: ${sliderMax}
sliderMin: ${sliderMin}
sliderValRange: ${sliderValRange}
stepScale: ${stepScale}
Step: ${stepNumber} of ${numSteps}
offVal: ${offVal}
allVal = ${allVal}
initial value: ${handleText}
left-margin: ${leftMargin}
width: ${newSlider.width()}
percent of max: ${percentOfMax}
left: ${leftPos}`) */
//special handling for response length slider, pulls text aliases for step values from an array
if (newSlider.attr('id') == 'amount_gen_zenslider') {
handleText = steps[stepNumber];
handle.text(handleText);
newSlider.val(stepNumber);
numVal = steps[stepNumber];
}
//special handling for TextCompletion rep pen range slider, pulls text aliases for step values from an array
else if (newSlider.attr('id') == 'rep_pen_range_textgenerationwebui_zenslider') {
handleText = steps[stepNumber];
handle.text(handleText);
newSlider.val(stepNumber);
if (numVal === offVal) { handle.text('Off').css('color', 'rgba(128,128,128,0.5'); }
else if (numVal === allVal) { handle.text('All'); }
else { handle.css('color', ''); }
numVal = steps[stepNumber];
}
//everything else uses the flat slider value
//also note: the above sliders are not custom inputtable due to the array aliasing
else {
//show 'off' if disabled value is set
if (numVal === offVal) { handle.text('Off').css('color', 'rgba(128,128,128,0.5'); }
else { handle.text(ui.value.toFixed(decimals)).css('color', ''); }
newSlider.val(handleText);
}
//for manually typed-in values we must adjust left position because JQUI doesn't do it for us
handle.css('left', leftPos);
//adjust a negative left margin to avoid overflowing right side of slider body
handle.css('margin-left', `${leftMargin}px`);
originalSlider.val(numVal);
originalSlider.trigger('input');
originalSlider.trigger('change');
}
}
function switchUiMode() {
$('body').toggleClass('no-blur', power_user.fast_ui_mode);
$('#fast_ui_mode').prop('checked', power_user.fast_ui_mode);
if (power_user.fast_ui_mode) {
$('#blur-strength-block').css('opacity', '0.2');
$('#blur_strength').prop('disabled', true);
} else {
$('#blur-strength-block').css('opacity', '1');
$('#blur_strength').prop('disabled', false);
}
}
function toggleWaifu() {
$('#waifuMode').trigger('click');
return '';
}
function switchWaifuMode() {
$('body').toggleClass('waifuMode', power_user.waifuMode);
$('#waifuMode').prop('checked', power_user.waifuMode);
scrollChatToBottom();
}
function switchSpoilerMode() {
if (power_user.spoiler_free_mode) {
$('#descriptionWrapper').hide();
$('#firstMessageWrapper').hide();
$('#spoiler_free_desc').addClass('flex1');
$('#creators_note_desc_hidden').show();
}
else {
$('#descriptionWrapper').show();
$('#firstMessageWrapper').show();
$('#spoiler_free_desc').removeClass('flex1');
$('#creators_note_desc_hidden').hide();
}
}
function peekSpoilerMode() {
$('#descriptionWrapper').toggle();
$('#firstMessageWrapper').toggle();
$('#spoiler_free_desc').toggleClass('flex1');
$('#creators_note_desc_hidden').toggle();
}
function switchMovingUI() {
$('.drawer-content.maximized').each(function () {
$(this).find('.inline-drawer-maximize').trigger('click');
});
$('body').toggleClass('movingUI', power_user.movingUI);
if (power_user.movingUI === true) {
initMovingUI();
if (power_user.movingUIState) {
loadMovingUIState();
}
} else {
if (Object.keys(power_user.movingUIState).length !== 0) {
power_user.movingUIState = {};
resetMovablePanels();
saveSettingsDebounced();
}
}
}
function applyNoShadows() {
$('body').toggleClass('noShadows', power_user.noShadows);
$('#noShadowsmode').prop('checked', power_user.noShadows);
if (power_user.noShadows) {
$('#shadow-width-block').css('opacity', '0.2');
$('#shadow_width').prop('disabled', true);
} else {
$('#shadow-width-block').css('opacity', '1');
$('#shadow_width').prop('disabled', false);
}
scrollChatToBottom();
}
function applyAvatarStyle() {
$('body').toggleClass('big-avatars', power_user.avatar_style === avatar_styles.RECTANGULAR);
$('body').toggleClass('square-avatars', power_user.avatar_style === avatar_styles.SQUARE);
$('body').toggleClass('rounded-avatars', power_user.avatar_style === avatar_styles.ROUNDED);
$('#avatar_style').val(power_user.avatar_style).prop('selected', true);
}
function applyChatDisplay() {
if ([null, undefined].includes(power_user.chat_display)) {
console.debug('applyChatDisplay: saw no chat display type defined');
power_user.chat_display = chat_styles.DEFAULT;
}
console.debug(`poweruser.chat_display ${power_user.chat_display}`);
$('#chat_display').val(power_user.chat_display).prop('selected', true);
switch (power_user.chat_display) {
case 0: {
console.debug('applying default chat');
$('body').removeClass('bubblechat');
$('body').removeClass('documentstyle');
break;
}
case 1: {
console.debug('applying bubblechat');
$('body').addClass('bubblechat');
$('body').removeClass('documentstyle');
break;
}
case 2: {
console.debug('applying document style');
$('body').removeClass('bubblechat');
$('body').addClass('documentstyle');
break;
}
}
}
function applyToastrPosition() {
if (!toastPositionClasses.includes(power_user.toastr_position)) {
power_user.toastr_position = defaultToastPosition;
console.warn(`applyToastrPosition: invalid toastr position, defaulting to ${defaultToastPosition}`);
}
toastr.options.positionClass = power_user.toastr_position;
fixToastrForDialogs();
$('#toastr_position').val(power_user.toastr_position);
$(`#toastr_position option[value="${power_user.toastr_position}"]`).prop('selected', true);
}
function applyChatWidth(type) {
if (type === 'forced') {
let r = document.documentElement;
r.style.setProperty('--sheldWidth', `${power_user.chat_width}vw`);
$('#chat_width_slider').val(power_user.chat_width);
//document.documentElement.style.setProperty('--sheldWidth', power_user.chat_width);
} else {
//this is to prevent the slider from updating page in real time
$('#chat_width_slider').off('mouseup touchend').on('mouseup touchend', async () => {
// This is a hack for Firefox to let it render before applying the block width.
// Otherwise it takes the incorrect slider position with the new value AFTER the resizing.
await delay(1);
document.documentElement.style.setProperty('--sheldWidth', `${power_user.chat_width}vw`);
await delay(1);
});
}
$('#chat_width_slider_counter').val(power_user.chat_width);
}
function applyThemeColor(type) {
if (type === 'main') {
document.documentElement.style.setProperty('--SmartThemeBodyColor', power_user.main_text_color);
const color = power_user.main_text_color.split('(')[1].split(')')[0].split(',');
document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorR', color[0]);
document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorG', color[1]);
document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorB', color[2]);
document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorA', color[3]);
}
if (type === 'italics') {
document.documentElement.style.setProperty('--SmartThemeEmColor', power_user.italics_text_color);
}
if (type === 'underline') {
document.documentElement.style.setProperty('--SmartThemeUnderlineColor', power_user.underline_text_color);
}
if (type === 'quote') {
document.documentElement.style.setProperty('--SmartThemeQuoteColor', power_user.quote_text_color);
}
/* if (type === 'fastUIBG') {
document.documentElement.style.setProperty('--SmartThemeFastUIBGColor', power_user.fastui_bg_color);
} */
if (type === 'blurTint') {
let metaThemeColor = document.querySelector('meta[name=theme-color]');
document.documentElement.style.setProperty('--SmartThemeBlurTintColor', power_user.blur_tint_color);
metaThemeColor.setAttribute('content', power_user.blur_tint_color);
}
if (type === 'chatTint') {
document.documentElement.style.setProperty('--SmartThemeChatTintColor', power_user.chat_tint_color);
}
if (type === 'userMesBlurTint') {
document.documentElement.style.setProperty('--SmartThemeUserMesBlurTintColor', power_user.user_mes_blur_tint_color);
}
if (type === 'botMesBlurTint') {
document.documentElement.style.setProperty('--SmartThemeBotMesBlurTintColor', power_user.bot_mes_blur_tint_color);
}
if (type === 'shadow') {
document.documentElement.style.setProperty('--SmartThemeShadowColor', power_user.shadow_color);
}
if (type === 'border') {
document.documentElement.style.setProperty('--SmartThemeBorderColor', power_user.border_color);
}
}
function applyCustomCSS() {
$('#customCSS').val(power_user.custom_css);
var styleId = 'custom-style';
var style = document.getElementById(styleId);
if (!style) {
style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.setAttribute('id', styleId);
document.head.appendChild(style);
}
style.innerHTML = power_user.custom_css;
}
function applyBlurStrength() {
document.documentElement.style.setProperty('--blurStrength', String(power_user.blur_strength));
$('#blur_strength_counter').val(power_user.blur_strength);
$('#blur_strength').val(power_user.blur_strength);
}
function applyShadowWidth() {
document.documentElement.style.setProperty('--shadowWidth', String(power_user.shadow_width));
$('#shadow_width_counter').val(power_user.shadow_width);
$('#shadow_width').val(power_user.shadow_width);
}
function applyFontScale(type) {
//this is to allow forced setting on page load, theme swap, etc
if (type === 'forced') {
document.documentElement.style.setProperty('--fontScale', String(power_user.font_scale));
} else {
//this is to prevent the slider from updating page in real time
$('#font_scale').off('mouseup touchend').on('mouseup touchend', () => {
document.documentElement.style.setProperty('--fontScale', String(power_user.font_scale));
});
}
$('#font_scale_counter').val(power_user.font_scale);
$('#font_scale').val(power_user.font_scale);
}
/**
* Checks if the chat needs to be reloaded to apply media display settings.
* @returns {boolean} True if the chat needs reload to apply media display settings
*/
function isMediaDisplayReloadNeeded() {
// A user is not currently in a chat.
const chatId = getCurrentChatId();
if (!chatId) {
return false;
}
const firstDisplayedIndex = getFirstDisplayedMessageId();
const hasUnprocessedMediaMessages = chat.some((message, index) => {
// Skip messages that are not currently displayed
if (index < firstDisplayedIndex) {
return false;
}
const hasMediaAttachments = Array.isArray(message?.extra?.media) && message.extra.media.length > 0;
const lacksMediaDisplay = !message?.extra?.media_display;
return hasMediaAttachments && lacksMediaDisplay;
});
return hasUnprocessedMediaMessages;
}
/**
* Shows a toast notification prompting the user to reload the chat if media display settings have changed
* and there are messages with media attachments that haven't been processed with the new display format.
*/
function showMediaDisplayReloadPrompt() {
if (!isMediaDisplayReloadNeeded()) {
return;
}
toastr.info(
t`Reload the chat to apply the changes. Click here to reload.`,
t`Media Style changed`,
{ onclick: () => void reloadCurrentChat() },
);
}
function applyTheme(name) {
const theme = themes.find(x => x.name == name);
if (!theme) {
return;
}
const themeProperties = [
{ key: 'main_text_color', selector: '#main-text-color-picker', type: 'main' },
{ key: 'italics_text_color', selector: '#italics-color-picker', type: 'italics' },
{ key: 'underline_text_color', selector: '#underline-color-picker', type: 'underline' },
{ key: 'quote_text_color', selector: '#quote-color-picker', type: 'quote' },
{ key: 'blur_tint_color', selector: '#blur-tint-color-picker', type: 'blurTint' },
{ key: 'chat_tint_color', selector: '#chat-tint-color-picker', type: 'chatTint' },
{ key: 'user_mes_blur_tint_color', selector: '#user-mes-blur-tint-color-picker', type: 'userMesBlurTint' },
{ key: 'bot_mes_blur_tint_color', selector: '#bot-mes-blur-tint-color-picker', type: 'botMesBlurTint' },
{ key: 'shadow_color', selector: '#shadow-color-picker', type: 'shadow' },
{ key: 'border_color', selector: '#border-color-picker', type: 'border' },
{
key: 'blur_strength',
action: () => {
applyBlurStrength();
},
},
{
key: 'custom_css',
action: () => {
applyCustomCSS();
},
},
{
key: 'shadow_width',
action: () => {
applyShadowWidth();
},
},
{
key: 'font_scale',
action: () => {
applyFontScale('forced');
},
},
{
key: 'fast_ui_mode',
action: () => {
switchUiMode();
},
},
{
key: 'waifuMode',
action: () => {
switchWaifuMode();
},
},
{
key: 'chat_display',
action: () => {
applyChatDisplay();
},
},
{
key: 'toastr_position',
action: () => {
applyToastrPosition();
},
},
{
key: 'avatar_style',
action: () => {
applyAvatarStyle();
},
},
{
key: 'noShadows',
action: () => {
applyNoShadows();
},
},
{
key: 'chat_width',
action: () => {
// If chat width is not set, set it to 50
if (!power_user.chat_width) {
power_user.chat_width = 50;
}
applyChatWidth('forced');
},
},
{
key: 'timer_enabled',
action: () => {
switchTimer();
},
},
{
key: 'timestamps_enabled',
action: () => {
switchTimestamps();
},
},
{
key: 'timestamp_model_icon',
action: () => {
switchIcons();
},
},
{
key: 'message_token_count_enabled',
action: () => {
switchTokenCount();
},
},
{
key: 'mesIDDisplay_enabled',
action: () => {
switchMesIDDisplay();
},
},
{
key: 'hideChatAvatars_enabled',
action: () => {
switchHideChatAvatars();
},
},
{
key: 'expand_message_actions',
action: () => {
switchMessageActions();
},
},
{
key: 'enableZenSliders',
action: () => {
switchMessageActions();
},
},
{
key: 'enableLabMode',
action: () => {
switchMessageActions();
},
},
{
key: 'hotswap_enabled',
action: () => {
switchHotswap();
},
},
{
key: 'bogus_folders',
action: () => {
$('#bogus_folders').prop('checked', power_user.bogus_folders);
printCharactersDebounced();
},
},
{
key: 'zoomed_avatar_magnification',
action: () => {
$('#zoomed_avatar_magnification').prop('checked', power_user.zoomed_avatar_magnification);
printCharactersDebounced();
},
},
{
key: 'reduced_motion',
action: () => {
$('#reduced_motion').prop('checked', power_user.reduced_motion);
switchReducedMotion();
},
},
{
key: 'compact_input_area',
action: () => {
$('#compact_input_area').prop('checked', power_user.compact_input_area);
switchCompactInputArea();
},
},
{
key: 'show_swipe_num_all_messages',
action: () => {
$('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
switchSwipeNumAllMessages();
},
},
{
key: 'click_to_edit',
action: () => {
$('#click_to_edit').prop('checked', power_user.click_to_edit);
},
},
{
key: 'media_display',
action: (oldValue, newValue) => {
$('#media_display').val(power_user.media_display);
if (oldValue !== newValue) {
showMediaDisplayReloadPrompt();
}
},
},
];
for (const { key, selector, type, action } of themeProperties) {
if (theme[key] !== undefined) {
const oldValue = power_user[key];
const newValue = theme[key];
power_user[key] = newValue;
if (selector) $(selector).attr('color', newValue);
if (type) applyThemeColor(type);
if (action) action(oldValue, newValue);
} else {
console.debug(`Empty theme key: ${key}`);
}
}
console.log('theme applied: ' + name);
}
async function applyMovingUIPreset(name) {
await resetMovablePanels('quiet');
const movingUIPreset = movingUIPresets.find(x => x.name == name);
if (!movingUIPreset) {
return;
}
power_user.movingUIState = movingUIPreset.movingUIState;
console.log('MovingUI Preset applied: ' + name);
loadMovingUIState();
saveSettingsDebounced();
}
/**
* Register a function to be executed when the debug menu is opened.
* @param {string} functionId Unique ID for the function.
* @param {string} name Name of the function.
* @param {string} description Description of the function.
* @param {function} func Function to be executed.
*/
export function registerDebugFunction(functionId, name, description, func) {
debug_functions.push({ functionId, name, description, func });
}
async function showDebugMenu() {
const template = await renderTemplateAsync('debug', { functions: debug_functions });
callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, allowVerticalScrolling: true });
}
export function applyPowerUserSettings() {
switchUiMode();
applyFontScale('forced');
applyThemeColor();
applyChatWidth('forced');
applyAvatarStyle();
applyBlurStrength();
applyShadowWidth();
applyCustomCSS();
switchMovingUI();
applyNoShadows();
switchHotswap();
switchTimer();
switchTimestamps();
switchIcons();
switchMesIDDisplay();
switchHideChatAvatars();
switchTokenCount();
switchMessageActions();
switchSwipeNumAllMessages();
}
export function applyStylePins() {
try {
const existingPins = document.querySelector('#chat > .style-pins');
if (existingPins) {
existingPins.remove();
}
if (!power_user.pin_styles) {
return;
}
const firstDisplayed = getFirstDisplayedMessageId();
if (firstDisplayed === 0 || !isFinite(firstDisplayed)) {
return;
}
const chatElement = document.getElementById('chat');
if (!chatElement) {
return;
}
const firstMessage = chat[0];
if (!firstMessage) {
return;
}
const formattedMessage = messageFormatting(firstMessage.mes, firstMessage.name, firstMessage.is_system, firstMessage.is_user, 0, {}, false);
const htmlElement = document.createElement('div');
htmlElement.innerHTML = formattedMessage;
const styleTags = htmlElement.querySelectorAll('style');
if (styleTags.length === 0) {
return;
}
const pinsElement = document.createElement('div');
pinsElement.classList.add('style-pins');
pinsElement.append(...Array.from(styleTags));
chatElement.prepend(pinsElement);
} catch (error) {
console.error('Error applying style pins:', error);
}
}
function getExampleMessagesBehavior() {
if (power_user.strip_examples) {
return 'strip';
}
if (power_user.pin_examples) {
return 'keep';
}
return 'normal';
}
//MARK: loadPowerUser
export async function loadPowerUserSettings(settings, data) {
const defaultStscript = JSON.parse(JSON.stringify(power_user.stscript));
// Load from settings.json
if (settings.power_user !== undefined) {
// Migrate old preference to a new setting
if (settings.power_user.click_to_edit === undefined && settings.power_user.chat_display === chat_styles.DOCUMENT) {
settings.power_user.click_to_edit = true;
}
if (Object.hasOwn(settings.power_user, 'auto_sort_tags') && !Object.hasOwn(settings.power_user, 'tag_sort_mode')) {
settings.power_user.tag_sort_mode = settings.power_user.auto_sort_tags ? tag_sort_mode.ALPHABETICAL : tag_sort_mode.MANUAL;
delete settings.power_user.auto_sort_tags;
}
Object.assign(power_user, settings.power_user);
}
if (power_user.stscript === undefined) {
power_user.stscript = defaultStscript;
} else {
if (power_user.stscript.autocomplete === undefined) {
power_user.stscript.autocomplete = defaultStscript.autocomplete;
} else {
if (power_user.stscript.autocomplete.state === undefined) {
power_user.stscript.autocomplete.state = defaultStscript.autocomplete.state;
}
if (power_user.stscript.autocomplete.width === undefined) {
power_user.stscript.autocomplete.width = defaultStscript.autocomplete.width;
}
if (power_user.stscript.autocomplete.font === undefined) {
power_user.stscript.autocomplete.font = defaultStscript.autocomplete.font;
}
if (power_user.stscript.autocomplete.style === undefined) {
power_user.stscript.autocomplete.style = power_user.stscript.autocomplete_style || defaultStscript.autocomplete.style;
}
if (power_user.stscript.autocomplete.select === undefined) {
power_user.stscript.autocomplete.select = defaultStscript.autocomplete.select;
}
}
if (power_user.stscript.parser === undefined) {
power_user.stscript.parser = defaultStscript.parser;
} else if (power_user.stscript.parser.flags === undefined) {
power_user.stscript.parser.flags = defaultStscript.parser.flags;
}
// Cleanup old flags
delete power_user.stscript.autocomplete_style;
}
if (data.themes !== undefined) {
themes = data.themes;
}
if (data.movingUIPresets !== undefined) {
movingUIPresets = data.movingUIPresets;
}
if (data.context !== undefined) {
context_presets = data.context;
}
if (typeof power_user.chat_display !== 'number') {
power_user.chat_display = chat_styles.DEFAULT;
}
if (typeof power_user.waifuMode !== 'boolean') {
power_user.waifuMode = false;
}
if (typeof power_user.chat_width !== 'number') {
power_user.chat_width = 50;
}
if (power_user.tokenizer === tokenizers.LEGACY) {
power_user.tokenizer = tokenizers.GPT2;
}
// Clean up old/legacy settings
if (power_user.import_card_tags !== undefined) {
power_user.tag_import_setting = power_user.import_card_tags ? tag_import_setting.ASK : tag_import_setting.NONE;
delete power_user.import_card_tags;
}
if (power_user?.instruct?.derived === true) {
power_user.instruct_derived = true;
delete power_user.instruct.derived;
}
// Reset the saved chat template hash
power_user.chat_template_hash = '';
$('#single_line').prop('checked', power_user.single_line);
$('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
$('#world_import_dialog').prop('checked', power_user.world_import_dialog);
$('#enable_auto_select_input').prop('checked', power_user.enable_auto_select_input);
$('#enable_md_hotkeys').prop('checked', power_user.enable_md_hotkeys);
$('#trim_spaces').prop('checked', power_user.trim_spaces);
$('#continue_on_send').prop('checked', power_user.continue_on_send);
$('#quick_continue').prop('checked', power_user.quick_continue);
$('#quick_impersonate').prop('checked', power_user.quick_continue);
$('#mes_continue').css('display', power_user.quick_continue ? '' : 'none');
$('#mes_impersonate').css('display', power_user.quick_impersonate ? '' : 'none');
$('#gestures-checkbox').prop('checked', power_user.gestures);
$('#auto_swipe').prop('checked', power_user.auto_swipe);
$('#auto_swipe_minimum_length').val(power_user.auto_swipe_minimum_length);
$('#auto_swipe_blacklist').val(power_user.auto_swipe_blacklist.join(', '));
$('#auto_swipe_blacklist_threshold').val(power_user.auto_swipe_blacklist_threshold);
$('#custom_stopping_strings').text(power_user.custom_stopping_strings);
$('#custom_stopping_strings_macro').prop('checked', power_user.custom_stopping_strings_macro);
$('#fuzzy_search_checkbox').prop('checked', power_user.fuzzy_search);
$('#persona_show_notifications').prop('checked', power_user.persona_show_notifications);
$('#persona_allow_multi_connections').prop('checked', power_user.persona_allow_multi_connections);
$('#persona_auto_lock').prop('checked', power_user.persona_auto_lock);
$('#encode_tags').prop('checked', power_user.encode_tags);
$('#experimental_macro_engine').prop('checked', power_user.experimental_macro_engine);
$('#example_messages_behavior').val(getExampleMessagesBehavior());
$(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true);
$('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived);
$('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);
$('#context_size_derived').prop('checked', !!power_user.context_size_derived);
$('#console_log_prompts').prop('checked', power_user.console_log_prompts);
$('#request_token_probabilities').prop('checked', power_user.request_token_probabilities);
$('#show_group_chat_queue').prop('checked', power_user.show_group_chat_queue);
$('#auto_fix_generated_markdown').prop('checked', power_user.auto_fix_generated_markdown);
$('#auto_scroll_chat_to_bottom').prop('checked', power_user.auto_scroll_chat_to_bottom);
$('#bogus_folders').prop('checked', power_user.bogus_folders);
$('#zoomed_avatar_magnification').prop('checked', power_user.zoomed_avatar_magnification);
$(`#tokenizer option[value="${power_user.tokenizer}"]`).prop('selected', true);
$(`#send_on_enter option[value=${power_user.send_on_enter}]`).prop('selected', true);
$('#confirm_message_delete').prop('checked', power_user.confirm_message_delete !== undefined ? !!power_user.confirm_message_delete : true);
$('#spoiler_free_mode').prop('checked', power_user.spoiler_free_mode);
$('#collapse-newlines-checkbox').prop('checked', power_user.collapse_newlines);
$('#always-force-name2-checkbox').prop('checked', power_user.always_force_name2);
$('#trim_sentences_checkbox').prop('checked', power_user.trim_sentences);
$('#disable_group_trimming').prop('checked', power_user.disable_group_trimming);
$('#markdown_escape_strings').val(power_user.markdown_escape_strings);
$('#fast_ui_mode').prop('checked', power_user.fast_ui_mode);
$('#waifuMode').prop('checked', power_user.waifuMode);
$('#movingUImode').prop('checked', power_user.movingUI);
$('#noShadowsmode').prop('checked', power_user.noShadows);
$('#start_reply_with').text(power_user.user_prompt_bias);
$('#chat-show-reply-prefix-checkbox').prop('checked', power_user.show_user_prompt_bias);
$('#auto_continue_enabled').prop('checked', power_user.auto_continue.enabled);
$('#auto_continue_allow_chat_completions').prop('checked', power_user.auto_continue.allow_chat_completions);
$('#auto_continue_target_length').val(power_user.auto_continue.target_length);
$('#play_message_sound').prop('checked', power_user.play_message_sound);
$('#play_sound_unfocused').prop('checked', power_user.play_sound_unfocused);
$('#never_resize_avatars').prop('checked', power_user.never_resize_avatars);
$('#show_card_avatar_urls').prop('checked', power_user.show_card_avatar_urls);
$('#auto_save_msg_edits').prop('checked', power_user.auto_save_msg_edits);
$('#allow_name1_display').prop('checked', power_user.allow_name1_display);
$('#allow_name2_display').prop('checked', power_user.allow_name2_display);
//$("#removeXML").prop("checked", power_user.removeXML);
$('#hotswapEnabled').prop('checked', power_user.hotswap_enabled);
$('#messageTimerEnabled').prop('checked', power_user.timer_enabled);
$('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);
$('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);
$('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
$('#hideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);
$('#prefer_character_prompt').prop('checked', power_user.prefer_character_prompt);
$('#prefer_character_jailbreak').prop('checked', power_user.prefer_character_jailbreak);
$('#enableZenSliders').prop('checked', power_user.enableZenSliders).trigger('input');
$('#enableLabMode').prop('checked', power_user.enableLabMode).trigger('input', { fromInit: true });
$(`input[name="avatar_style"][value="${power_user.avatar_style}"]`).prop('checked', true);
$(`#chat_display option[value=${power_user.chat_display}]`).prop('selected', true).trigger('change');
$(`#toastr_position option[value=${power_user.toastr_position}]`).prop('selected', true).trigger('change');
$('#chat_width_slider').val(power_user.chat_width);
$('#token_padding').val(power_user.token_padding);
$('#aux_field').val(power_user.aux_field);
$('#tag_import_setting').val(power_user.tag_import_setting);
$('#stscript_autocomplete_state').val(power_user.stscript.autocomplete.state).trigger('input');
$('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
$('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
$('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme');
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style);
$('#stscript_autocomplete_select').val(power_user.stscript.autocomplete.select ?? (AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER));
$('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false);
$('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false);
$('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
$('#stscript_autocomplete_font_scale_counter').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
document.body.style.setProperty('--ac-font-scale', power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale.toString());
$('#stscript_autocomplete_width_left').val(power_user.stscript.autocomplete.width.left ?? AUTOCOMPLETE_WIDTH.CHAT);
document.querySelector('#stscript_autocomplete_width_left')?.dispatchEvent(new Event('input', { bubbles: true }));
$('#stscript_autocomplete_width_right').val(power_user.stscript.autocomplete.width.right ?? AUTOCOMPLETE_WIDTH.CHAT);
document.querySelector('#stscript_autocomplete_width_right')?.dispatchEvent(new Event('input', { bubbles: true }));
$('#restore_user_input').prop('checked', power_user.restore_user_input);
$('#chat_truncation').val(power_user.chat_truncation);
$('#chat_truncation_counter').val(power_user.chat_truncation);
$('#streaming_fps').val(power_user.streaming_fps);
$('#streaming_fps_counter').val(power_user.streaming_fps);
$('#smooth_streaming').prop('checked', power_user.smooth_streaming);
$('#smooth_streaming_no_think').prop('checked', power_user.smooth_streaming_no_think);
$('#smooth_streaming_speed').val(power_user.smooth_streaming_speed);
$('#stream_fade_in').prop('checked', power_user.stream_fade_in);
$('#font_scale').val(power_user.font_scale);
$('#font_scale_counter').val(power_user.font_scale);
$('#blur_strength').val(power_user.blur_strength);
$('#blur_strength_counter').val(power_user.blur_strength);
$('#shadow_width').val(power_user.shadow_width);
$('#shadow_width_counter').val(power_user.shadow_width);
$('#main-text-color-picker').attr('color', power_user.main_text_color);
$('#italics-color-picker').attr('color', power_user.italics_text_color);
$('#underline-color-picker').attr('color', power_user.underline_text_color);
$('#quote-color-picker').attr('color', power_user.quote_text_color);
$('#blur-tint-color-picker').attr('color', power_user.blur_tint_color);
$('#chat-tint-color-picker').attr('color', power_user.chat_tint_color);
$('#user-mes-blur-tint-color-picker').attr('color', power_user.user_mes_blur_tint_color);
$('#bot-mes-blur-tint-color-picker').attr('color', power_user.bot_mes_blur_tint_color);
$('#shadow-color-picker').attr('color', power_user.shadow_color);
$('#border-color-picker').attr('color', power_user.border_color);
$('#reduced_motion').prop('checked', power_user.reduced_motion);
$('#auto-connect-checkbox').prop('checked', power_user.auto_connect);
$('#auto-load-chat-checkbox').prop('checked', power_user.auto_load_chat);
$('#forbid_external_media').prop('checked', power_user.forbid_external_media);
$('#pin_styles').prop('checked', power_user.pin_styles);
$('#click_to_edit').prop('checked', power_user.click_to_edit);
$('#media_display').val(power_user.media_display);
$('#image_overswipe').val(power_user.image_overswipe);
for (const theme of themes) {
const option = document.createElement('option');
option.value = theme.name;
option.innerText = theme.name;
option.selected = theme.name == power_user.theme;
$('#themes').append(option);
}
for (const movingUIPreset of movingUIPresets) {
const option = document.createElement('option');
option.value = movingUIPreset.name;
option.innerText = movingUIPreset.name;
option.selected = movingUIPreset.name == power_user.movingUIPreset;
$('#movingUIPresets').append(option);
}
$(`#character_sort_order option[data-order="${power_user.sort_order}"][data-field="${power_user.sort_field}"]`).prop('selected', true);
switchReducedMotion();
switchCompactInputArea();
reloadMarkdownProcessor();
await loadInstructMode(data);
await loadContextSettings();
await loadSystemPrompts(data);
await loadReasoningTemplates(data);
loadMaxContextUnlocked();
switchWaifuMode();
switchSpoilerMode();
loadMovingUIState();
loadCharListState();
toggleMDHotkeyIconDisplay();
applyToastrPosition();
}
function toggleMDHotkeyIconDisplay() {
if (power_user.enable_md_hotkeys) {
$('.mdhotkey_location').each(function () {
$(this).parent().append('<i class="fa-brands fa-markdown mdhotkey_icon"></i>');
});
} else {
$('.mdhotkey_icon').remove();
}
}
function loadCharListState() {
document.body.classList.toggle('charListGrid', power_user.charListGrid);
}
export function loadMovingUIState() {
if (!isMobile()
&& power_user.movingUIState
&& power_user.movingUI === true) {
console.debug('loading movingUI state');
for (var elmntName of Object.keys(power_user.movingUIState)) {
var elmntState = power_user.movingUIState[elmntName];
try {
var elmnt = $('#' + $.escapeSelector(elmntName));
if (elmnt.length) {
console.debug(`loading state for ${elmntName}`);
elmnt.css(elmntState);
} else {
console.debug(`skipping ${elmntName} because it doesn't exist in the DOM`);
}
} catch (err) {
console.debug(`error occurred while processing ${elmntName}: ${err}`);
}
}
} else {
console.debug('skipping movingUI state load');
return;
}
}
function loadMaxContextUnlocked() {
$('#max_context_unlocked').prop('checked', power_user.max_context_unlocked);
$('#max_context_unlocked').on('change', function () {
power_user.max_context_unlocked = !!$(this).prop('checked');
switchMaxContextSize();
saveSettingsDebounced();
});
switchMaxContextSize();
}
function switchMaxContextSize() {
const elements = [
$('#max_context'),
$('#max_context_counter'),
$('#rep_pen_range'),
$('#rep_pen_range_counter'),
$('#rep_pen_range_textgenerationwebui'),
$('#rep_pen_range_counter_textgenerationwebui'),
$('#dry_penalty_last_n_textgenerationwebui'),
$('#dry_penalty_last_n_counter_textgenerationwebui'),
$('#rep_pen_decay_textgenerationwebui'),
$('#rep_pen_decay_counter_textgenerationwebui'),
];
const maxValue = power_user.max_context_unlocked ? MAX_CONTEXT_UNLOCKED : MAX_CONTEXT_DEFAULT;
const minValue = power_user.max_context_unlocked ? maxContextMin : maxContextMin;
const steps = power_user.max_context_unlocked ? unlockedMaxContextStep : maxContextStep;
$('#rep_pen_range_textgenerationwebui_zenslider').remove(); //unsure why, but this is necessary.
$('#dry_penalty_last_n_textgenerationwebui_zenslider').remove();
$('#rep_pen_decay_textgenerationwebui_zenslider').remove();
for (const element of elements) {
const id = element.attr('id');
element.attr('max', maxValue);
if (typeof id === 'string' && id?.indexOf('max_context') !== -1) {
element.attr('min', minValue);
element.attr('step', steps); //only change setps for max context, because rep pen range needs step of 1 due to important values of -1 and 0
}
const value = Number(element.val());
if (value >= maxValue) {
element.val(maxValue).trigger('input');
}
}
const maxAmountGen = power_user.max_context_unlocked ? MAX_RESPONSE_UNLOCKED : MAX_RESPONSE_DEFAULT;
$('#amount_gen').attr('max', maxAmountGen);
$('#amount_gen_counter').attr('max', maxAmountGen);
if (Number($('#amount_gen').val()) >= maxAmountGen) {
$('#amount_gen').val(maxAmountGen).trigger('input');
}
if (power_user.enableZenSliders) {
$('#max_context_zenslider').remove();
CreateZenSliders($('#max_context'));
$('#rep_pen_range_textgenerationwebui_zenslider').remove();
CreateZenSliders($('#rep_pen_range_textgenerationwebui'));
$('#dry_penalty_last_n_textgenerationwebui_zenslider').remove();
CreateZenSliders($('#dry_penalty_last_n_textgenerationwebui'));
$('#rep_pen_decay_textgenerationwebui_zenslider').remove();
CreateZenSliders($('#rep_pen_decay_textgenerationwebui'));
}
}
// Fetch a compiled object of all preset settings
export function getContextSettings() {
let compiledSettings = {};
contextControls.forEach((control) => {
let value = control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property];
// Force to a boolean if the setting is a checkbox
if (control.isCheckbox) {
value = !!value;
}
compiledSettings[control.property] = value;
});
return compiledSettings;
}
// TODO: Maybe add a refresh button to reset settings to preset
// TODO: Add "global state" if a preset doesn't set the power_user checkboxes
async function loadContextSettings() {
/**
* Auto-fix missing fields in the story string
* @param {ContextSettings} contextSettings Context settings instance
*/
function autoFixStoryString(contextSettings) {
// Already migrated, no need to fix
if (!contextSettings || Object.hasOwn(contextSettings, 'story_string_position')) {
return;
}
let storyString = contextSettings.story_string || '';
/**
* @param {string} field Missing field name
* @param {'start'|'end'} position Position of auto-fix
*/
function autoFixMissingField(field, position) {
if (storyString.includes(`{{${field}}}`)) {
return;
}
console.warn(`[Story String Validation] Story String is missing a field: ${field}. Adding it at the ${position}.`);
const fieldTemplate = `{{#if ${field}}}{{${field}}}\n{{/if}}`;
const firstCurlyPosition = storyString.includes('{{') ? storyString.indexOf('{{') : 0;
const lastCurlyPosition = storyString.includes('}}') ? storyString.lastIndexOf('}}') + '}}'.length : storyString.length;
const lastTrimPosition = storyString.includes('{{trim}}') ? storyString.lastIndexOf('{{trim}}') : storyString.length;
const endPosition = Math.min(lastTrimPosition, lastCurlyPosition);
storyString = position === 'start'
? storyString.substring(0, firstCurlyPosition) + fieldTemplate + storyString.substring(firstCurlyPosition)
: storyString.substring(0, endPosition) + fieldTemplate + storyString.substring(endPosition);
}
autoFixMissingField('anchorBefore', 'start');
autoFixMissingField('anchorAfter', 'end');
contextSettings.story_string = storyString;
}
// Migrate story string to add missing fields
autoFixStoryString(power_user.context);
contextControls.forEach(control => {
const $element = $(`#${control.id}`);
if (control.isGlobalSetting) {
return;
}
if (control.defaultValue !== undefined && power_user.context[control.property] === undefined) {
power_user.context[control.property] = control.defaultValue;
}
if (control.isCheckbox) {
$element.prop('checked', power_user.context[control.property]);
} else {
$element.val(power_user.context[control.property]);
}
console.debug(`Setting ${$element.prop('id')} to ${power_user.context[control.property]}`);
// If the setting already exists, no need to duplicate it
// TODO: Maybe check the power_user object for the setting instead of a flag?
$element.on('input', async function () {
let value = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
if (typeof control.defaultValue === 'number') {
value = Number(value);
}
if (control.isGlobalSetting) {
power_user[control.property] = value;
} else {
power_user.context[control.property] = value;
}
console.debug(`Setting ${$element.prop('id')} to ${value}`);
if (!CSS.supports('field-sizing', 'content') && $(this).is('textarea')) {
await resetScrollHeight($(this));
}
saveSettingsDebounced();
});
if (control.trigger) {
$element.trigger('input');
}
});
context_presets.forEach((preset) => {
const name = preset.name;
const option = document.createElement('option');
option.value = name;
option.innerText = name;
option.selected = name === power_user.context.preset;
$('#context_presets').append(option);
});
$('#context_presets').on('change', function () {
const name = String($(this).find(':selected').text());
const preset = context_presets.find(x => x.name === name);
if (!preset) {
return;
}
// Migrate story string to add missing fields
autoFixStoryString(preset);
power_user.context.preset = name;
contextControls.forEach(control => {
const presetValue = preset[control.property] ?? control.defaultValue;
if (presetValue !== undefined) {
if (control.isGlobalSetting) {
power_user[control.property] = presetValue;
} else {
power_user.context[control.property] = presetValue;
}
const $element = $(`#${control.id}`);
if (control.isCheckbox) {
$element
.prop('checked', control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property])
.trigger('input');
} else {
$element.val(control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property]);
$element.trigger('input');
}
}
});
if (power_user.instruct.bind_to_context) {
// Select matching instruct preset
for (const instruct_preset of instruct_presets) {
// If instruct preset matches the context template
if (instruct_preset.name === name) {
selectInstructPreset(instruct_preset.name, { isAuto: true });
break;
}
}
}
updateBindModelTemplatesState();
saveSettingsDebounced();
});
}
/**
* Common function to perform fuzzy search with optional caching
* @template T
* @param {string} type - Type of search from fuzzySearchCategories
* @param {T[]} data - Data array to search in
* @param {Array<{name: string, weight: number, getFn?: (obj: T) => string}>} keys - Fuse.js keys configuration
* @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<T>[]} Results as items with their score
*/
export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
// Check cache if provided
if (fuzzySearchCaches) {
const cache = fuzzySearchCaches[type];
if (cache?.resultMap.has(searchValue)) {
return cache.resultMap.get(searchValue);
}
}
const fuse = new Fuse(data, {
keys: keys,
includeScore: true,
ignoreLocation: true,
useExtendedSearch: true,
threshold: 0.2,
});
const results = fuse.search(searchValue);
// Store in cache if provided
if (fuzzySearchCaches) {
fuzzySearchCaches[type].resultMap.set(searchValue, results);
}
return results;
}
/**
* Fuzzy search characters by a search term
* @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/
export function fuzzySearchCharacters(searchValue, fuzzySearchCaches = null) {
const keys = [
{ name: 'data.name', weight: 20 },
{ name: '#tags', weight: 10, getFn: (character) => getTagsList(character.avatar).map(x => x.name).join('||') },
{ name: 'data.description', weight: 3 },
{ name: 'data.mes_example', weight: 3 },
{ name: 'data.scenario', weight: 2 },
{ name: 'data.personality', weight: 2 },
{ name: 'data.first_mes', weight: 2 },
{ name: 'data.creator_notes', weight: 2 },
{ name: 'data.creator', weight: 1 },
{ name: 'data.tags', weight: 1 },
{ name: 'data.alternate_greetings', weight: 1 },
];
return performFuzzySearch(fuzzySearchCategories.characters, characters, keys, searchValue, fuzzySearchCaches);
}
/**
* Fuzzy search world info entries by a search term
* @param {*[]} data - WI items data array
* @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/
export function fuzzySearchWorldInfo(data, searchValue, fuzzySearchCaches = null) {
const keys = [
{ name: 'key', weight: 20 },
{ name: 'group', weight: 15 },
{ name: 'comment', weight: 10 },
{ name: 'keysecondary', weight: 10 },
{ name: 'content', weight: 3 },
{ name: 'uid', weight: 1 },
{ name: 'automationId', weight: 1 },
];
return performFuzzySearch(fuzzySearchCategories.worldInfo, data, keys, searchValue, fuzzySearchCaches);
}
/**
* Fuzzy search persona entries by a search term
* @param {*[]} data - persona data array
* @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/
export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null) {
const mappedData = data.map(x => ({
key: x,
name: power_user.personas[x] ?? '',
description: power_user.persona_descriptions[x]?.description ?? '',
}));
const keys = [
{ name: 'name', weight: 20 },
{ name: 'description', weight: 3 },
];
return performFuzzySearch(fuzzySearchCategories.personas, mappedData, keys, searchValue, fuzzySearchCaches);
}
/**
* Fuzzy search tags by a search term
* @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/
export function fuzzySearchTags(searchValue, fuzzySearchCaches = null) {
const keys = [
{ name: 'name', weight: 1 },
];
return performFuzzySearch(fuzzySearchCategories.tags, tags, keys, searchValue, fuzzySearchCaches);
}
/**
* Fuzzy search groups by a search term
* @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
*/
export function fuzzySearchGroups(searchValue, fuzzySearchCaches = null) {
const keys = [
{ name: 'name', weight: 20 },
{ name: 'members', weight: 15 },
{ name: '#tags', weight: 10, getFn: (group) => getTagsList(group.id).map(x => x.name).join('||') },
{ name: 'id', weight: 1 },
];
return performFuzzySearch(fuzzySearchCategories.groups, groups, keys, searchValue, fuzzySearchCaches);
}
/**
* Renders a story string template with the given parameters.
* @param {object} params Template parameters.
* @param {object} [options] Additional options.
* @param {string} [options.customStoryString] Custom story string template.
* @param {InstructSettings} [options.customInstructSettings] Custom instruct settings.
* @param {ContextSettings} [options.customContextSettings] Custom context settings.
* @returns {string} The rendered story string.
*/
export function renderStoryString(params, { customStoryString = null, customInstructSettings = null, customContextSettings = null } = {}) {
try {
const instructSettings = structuredClone(customInstructSettings ?? power_user.instruct);
const contextSettings = structuredClone(customContextSettings ?? power_user.context);
const storyString = customStoryString ?? contextSettings.story_string;
const storyStringPosition = contextSettings.story_string_position ?? extension_prompt_types.IN_PROMPT;
// Validate and log possible warnings/errors
validateStoryString(storyString, params);
// compile the story string template into a function, with no HTML escaping
const compiledTemplate = Handlebars.compile(storyString, { noEscape: true });
// render the story string template with the given params
let output = compiledTemplate(params);
// substitute {{macro}} params that are not defined in the story string
output = substituteParams(output, params.user, params.char);
// remove leading newlines
output = output.replace(/^\n+/, '');
// add a newline to the end of the story string if it doesn't have one
if (output.length > 0 && !output.endsWith('\n') && storyStringPosition !== extension_prompt_types.IN_CHAT) {
if (!instructSettings.enabled || (instructSettings.wrap && !instructSettings.story_string_suffix)) {
output += '\n';
}
}
return output;
} catch (e) {
toastr.error('Check the story string template for validity', 'Error rendering story string');
console.error('Error rendering story string', e);
throw e; // rethrow the error
}
}
/**
* Validate the story string for possible warnings or issues
*
* @param {string} storyString - The story string
* @param {Object} params - The story string parameters
*/
function validateStoryString(storyString, params) {
/** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
const cache = JSON.parse(accountStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
const hash = getStringHash(storyString);
// Initialize the cache for the current hash if it doesn't exist
if (!cache.hashCache[hash]) {
cache.hashCache[hash] = { fieldsWarned: {} };
}
const currentCache = cache.hashCache[hash];
const fieldsToWarn = [];
function validateMissingField(field, fallbackLegacyField = null) {
const contains = storyString.includes(`{{${field}}}`) || (!!fallbackLegacyField && storyString.includes(`{{${fallbackLegacyField}}}`));
if (!contains && params[field]) {
const wasLogged = currentCache.fieldsWarned[field];
if (!wasLogged) {
fieldsToWarn.push(field);
currentCache.fieldsWarned[field] = true;
}
console.warn(`The story string does not contain {{${field}}}, but it would contain content:\n`, params[field]);
}
}
validateMissingField('description');
validateMissingField('personality');
validateMissingField('persona');
validateMissingField('scenario');
// validateMissingField('system');
validateMissingField('wiBefore', 'loreBefore');
validateMissingField('wiAfter', 'loreAfter');
if (fieldsToWarn.length > 0) {
const fieldsList = fieldsToWarn.map(field => `{{${field}}}`).join(', ');
toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
}
accountStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
}
const sortFunc = (a, b) => power_user.sort_order == 'asc' ? compareFunc(a, b) : compareFunc(b, a);
const compareFunc = (first, second) => {
const a = first[power_user.sort_field];
const b = second[power_user.sort_field];
if (power_user.sort_field === 'create_date') {
return sortMoments(timestampToMoment(b), timestampToMoment(a));
}
switch (power_user.sort_rule) {
case 'boolean':
if (a === true || a === 'true') return 1; // Prioritize 'true' or true
if (b === true || b === 'true') return -1; // Prioritize 'true' or true
if (a && !b) return -1; // Move truthy values to the end
if (!a && b) return 1; // Move falsy values to the beginning
if (a === b) return 0; // Sort equal values normally
return a < b ? -1 : 1; // Sort non-boolean values normally
default:
return typeof a == 'string'
? a.localeCompare(b)
: a - b;
}
};
/**
* Sorts an array of entities based on the current sort settings
* @param {any[]} entities An array of objects with an `item` property
* @param {boolean} forceSearch Whether to force search sorting
* @param {import('./filters.js').FilterHelper} [filterHelper=null] Filter helper to use
*/
export function sortEntitiesList(entities, forceSearch, filterHelper = null) {
filterHelper = filterHelper ?? entitiesFilter;
if (power_user.sort_field == undefined || entities.length === 0) {
return;
}
const isSearch = forceSearch || $('#character_sort_order option[data-field="search"]').is(':selected');
if (!isSearch && power_user.sort_order === 'random') {
shuffle(entities);
return;
}
entities.sort((a, b) => {
// Sort tags/folders will always be at the top. Their original sorting will be kept, to respect manual tag sorting.
if (a.type === 'tag' || b.type === 'tag') {
// The one that is a tag will be at the top
return (a.type === 'tag' ? -1 : 1) - (b.type === 'tag' ? -1 : 1);
}
// If we have search sorting, we take scores and use those
if (isSearch) {
const aScore = filterHelper.getScore(FILTER_TYPES.SEARCH, `${a.type}.${a.id}`);
const bScore = filterHelper.getScore(FILTER_TYPES.SEARCH, `${b.type}.${b.id}`);
return (aScore - bScore);
}
return sortFunc(a.item, b.item);
});
}
/**
* Updates the current UI theme file.
*/
async function updateTheme() {
await saveTheme(power_user.theme);
toastr.success('Theme saved.');
}
async function deleteTheme() {
const themeName = power_user.theme;
if (!themeName) {
toastr.info('No theme selected.');
return;
}
const template = $(await renderTemplateAsync('themeDelete', { themeName }));
const confirm = await callGenericPopup(template, POPUP_TYPE.CONFIRM);
if (!confirm) {
return;
}
const response = await fetch('/api/themes/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ name: themeName }),
});
if (!response.ok) {
toastr.error('Failed to delete theme. Check the console for more information.');
return;
}
const themeIndex = themes.findIndex(x => x.name == themeName);
if (themeIndex !== -1) {
themes.splice(themeIndex, 1);
$(`#themes option[value="${themeName}"]`).remove();
power_user.theme = themes[0]?.name;
saveSettingsDebounced();
if (power_user.theme) {
applyTheme(power_user.theme);
}
toastr.success('Theme deleted.');
}
}
/**
* Exports the current theme to a file.
*/
async function exportTheme() {
const themeFile = await saveTheme(power_user.theme);
const fileName = `${themeFile.name}.json`;
download(JSON.stringify(themeFile, null, 4), fileName, 'application/json');
}
/**
* Imports a theme from a file.
* @param {File} file File to import.
* @returns {Promise<void>} A promise that resolves when the theme is imported.
*/
async function importTheme(file) {
if (!file) {
return;
}
const fileText = await getFileText(file);
const parsed = JSON.parse(fileText);
if (!parsed.name) {
throw new Error('Missing name');
}
if (themes.some(t => t.name === parsed.name)) {
throw new Error('Theme with that name already exists');
}
if (typeof parsed.custom_css === 'string' && parsed.custom_css.includes('@import')) {
const template = $(await renderTemplateAsync('themeImportWarning'));
const confirm = await callGenericPopup(template, POPUP_TYPE.CONFIRM);
if (!confirm) {
throw new Error('Theme contains @import lines');
}
}
themes.push(parsed);
await saveTheme(parsed.name, getNewTheme(parsed));
const option = document.createElement('option');
option.selected = false;
option.value = parsed.name;
option.innerText = parsed.name;
$('#themes').append(option);
saveSettingsDebounced();
toastr.success(parsed.name, 'Theme imported');
}
/**
* Saves the current theme to the server.
* @param {string|undefined} name Theme name. If undefined, a popup will be shown to enter a name.
* @param {object|undefined} theme Theme object. If undefined, the current theme will be saved.
* @returns {Promise<object>} A promise that resolves when the theme is saved.
*/
async function saveTheme(name = undefined, theme = undefined) {
if (typeof name !== 'string') {
const newName = await callGenericPopup('Enter a theme preset name:', POPUP_TYPE.INPUT, power_user.theme);
if (!newName) {
return;
}
name = await getSanitizedFilename(String(newName));
}
if (typeof theme !== 'object') {
theme = getThemeObject(name);
}
const response = await fetch('/api/themes/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(theme),
});
if (!response.ok) {
toastr.error('Check the server connection and reload the page to prevent data loss.', 'Theme could not be saved');
console.error('Theme could not be saved', response);
throw new Error('Theme could not be saved');
}
const themeIndex = themes.findIndex(x => x.name == name);
if (themeIndex == -1) {
themes.push(theme);
const option = document.createElement('option');
option.selected = true;
option.value = name;
option.innerText = name;
$('#themes').append(option);
}
else {
themes[themeIndex] = theme;
$(`#themes option[value="${name}"]`).prop('selected', true);
}
power_user.theme = name;
saveSettingsDebounced();
return theme;
}
/**
* Gets a snapshot of the current theme settings.
* @param {string} name Name of the theme
* @returns {object} Theme object
*/
function getThemeObject(name) {
return {
name,
blur_strength: power_user.blur_strength,
main_text_color: power_user.main_text_color,
italics_text_color: power_user.italics_text_color,
underline_text_color: power_user.underline_text_color,
quote_text_color: power_user.quote_text_color,
blur_tint_color: power_user.blur_tint_color,
chat_tint_color: power_user.chat_tint_color,
user_mes_blur_tint_color: power_user.user_mes_blur_tint_color,
bot_mes_blur_tint_color: power_user.bot_mes_blur_tint_color,
shadow_color: power_user.shadow_color,
shadow_width: power_user.shadow_width,
border_color: power_user.border_color,
font_scale: power_user.font_scale,
fast_ui_mode: power_user.fast_ui_mode,
waifuMode: power_user.waifuMode,
avatar_style: power_user.avatar_style,
chat_display: power_user.chat_display,
toastr_position: power_user.toastr_position,
noShadows: power_user.noShadows,
chat_width: power_user.chat_width,
timer_enabled: power_user.timer_enabled,
timestamps_enabled: power_user.timestamps_enabled,
timestamp_model_icon: power_user.timestamp_model_icon,
mesIDDisplay_enabled: power_user.mesIDDisplay_enabled,
hideChatAvatars_enabled: power_user.hideChatAvatars_enabled,
message_token_count_enabled: power_user.message_token_count_enabled,
expand_message_actions: power_user.expand_message_actions,
enableZenSliders: power_user.enableZenSliders,
enableLabMode: power_user.enableLabMode,
hotswap_enabled: power_user.hotswap_enabled,
custom_css: power_user.custom_css,
bogus_folders: power_user.bogus_folders,
zoomed_avatar_magnification: power_user.zoomed_avatar_magnification,
reduced_motion: power_user.reduced_motion,
compact_input_area: power_user.compact_input_area,
show_swipe_num_all_messages: power_user.show_swipe_num_all_messages,
click_to_edit: power_user.click_to_edit,
media_display: power_user.media_display,
};
}
/**
* Applies imported theme properties to the theme object.
* @param {object} parsed Parsed object to get the theme from.
* @returns {object} Theme assigned to the parsed object.
*/
function getNewTheme(parsed) {
const theme = getThemeObject(parsed.name);
for (const key in parsed) {
if (Object.hasOwn(theme, key)) {
theme[key] = parsed[key];
}
}
return theme;
}
async function saveMovingUI() {
const popupResult = await callGenericPopup('Enter a name for the MovingUI Preset:', POPUP_TYPE.INPUT);
if (!popupResult) {
return;
}
const name = await getSanitizedFilename(String(popupResult));
const movingUIPreset = {
name,
movingUIState: power_user.movingUIState,
};
console.log(movingUIPreset);
const response = await fetch('/api/moving-ui/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(movingUIPreset),
});
if (response.ok) {
const movingUIPresetIndex = movingUIPresets.findIndex(x => x.name == name);
if (movingUIPresetIndex == -1) {
movingUIPresets.push(movingUIPreset);
const option = document.createElement('option');
option.selected = true;
option.value = name;
option.innerText = name;
$('#movingUIPresets').append(option);
}
else {
movingUIPresets[movingUIPresetIndex] = movingUIPreset;
$(`#movingUIPresets option[value="${name}"]`).prop('selected', true);
}
power_user.movingUIPreset = name;
saveSettingsDebounced();
} else {
toastr.error('Failed to save MovingUI state.');
console.error('MovingUI could not be saved', response);
}
}
/**
* Resets the movable styles of the given element to their unset values.
* @param {string} id Element ID
*/
export function resetMovableStyles(id) {
const panelStyles = ['top', 'left', 'right', 'bottom', 'height', 'width', 'margin'];
const panel = document.getElementById(id);
if (panel) {
panelStyles.forEach((style) => {
panel.style[style] = '';
});
}
}
async function resetMovablePanels(type) {
const panelIds = [
'sheld',
'left-nav-panel',
'right-nav-panel',
'WorldInfo',
'floatingPrompt',
'expression-holder',
'groupMemberListPopout',
'summaryExtensionPopout',
'gallery',
'logprobsViewer',
'cfgConfig',
];
/**
* @type {HTMLElement[]} Generic panels that don't have a known ID
*/
const draggedElements = Array.from(document.querySelectorAll('[data-dragged]'));
const allDraggable = panelIds.map(id => document.getElementById(id)).concat(draggedElements).filter(onlyUnique);
const panelStyles = ['top', 'left', 'right', 'bottom', 'height', 'width', 'margin'];
allDraggable.forEach((panel) => {
if (panel) {
$(panel).addClass('resizing');
panelStyles.forEach((style) => {
panel.style[style] = '';
});
}
});
/**
* @type {HTMLElement[]} Zoomed avatars that are currently being resized
*/
const zoomedAvatars = Array.from(document.querySelectorAll('.zoomed_avatar'));
if (zoomedAvatars.length > 0) {
zoomedAvatars.forEach((avatar) => {
avatar.classList.add('resizing');
panelStyles.forEach((style) => {
avatar.style[style] = '';
});
});
}
$('[data-dragged="true"]').removeAttr('data-dragged');
await delay(50);
power_user.movingUIState = {};
//if user manually resets panels, deselect the current preset
if (type !== 'quiet' && type !== 'resize') {
power_user.movingUIPreset = 'Default';
$('#movingUIPresets option[value="Default"]').prop('selected', true);
}
saveSettingsDebounced();
await eventSource.emit(event_types.MOVABLE_PANELS_RESET);
eventSource.once(event_types.SETTINGS_UPDATED, () => {
$('.resizing').removeClass('resizing');
//if happening as part of preset application, do it quietly.
if (type === 'quiet') {
return;
//if happening due to resize, tell user.
} else if (type === 'resize') {
toastr.warning('Panel positions reset due to zoom/resize');
//if happening due to manual button press
} else {
toastr.success('Panel positions reset');
}
});
}
/**
* Finds the ID of the tag with the given name.
* @param {string} name
* @returns {string} The ID of the tag with the given name.
*/
function findTagIdByName(name) {
const matchTypes = [
(a, b) => a === b,
(a, b) => a.startsWith(b),
(a, b) => a.includes(b),
];
// Only get tags that contain at least one record in the tag_map
const liveTagIds = new Set(Object.values(tag_map).flat());
const liveTags = tags.filter(x => liveTagIds.has(x.id));
const exactNameMatchIndex = liveTags.map(x => x.name.toLowerCase()).indexOf(name.toLowerCase());
if (exactNameMatchIndex !== -1) {
return liveTags[exactNameMatchIndex].id;
}
for (const matchType of matchTypes) {
const index = liveTags.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
if (index !== -1) {
return liveTags[index].id;
}
}
}
async function doRandomChat(_, tagName) {
/**
* Gets the ID of a random character.
* @returns {string} The order index of the randomly selected character.
*/
function getRandomCharacterId() {
if (!tagName) {
return Math.floor(Math.random() * characters.length).toString();
}
const tagId = findTagIdByName(tagName);
const taggedCharacters = Object.entries(tag_map)
.filter(x => x[1].includes(tagId)) // Get only records that include the tag
.map(x => x[0]) // Map the character avatar
.filter(x => characters.find(y => y.avatar === x)); // Filter out characters that don't exist
const randomCharacter = taggedCharacters[Math.floor(Math.random() * taggedCharacters.length)];
const randomIndex = characters.findIndex(x => x.avatar === randomCharacter);
if (randomIndex === -1) {
return;
}
return randomIndex.toString();
}
resetSelectedGroup();
const characterId = getRandomCharacterId();
if (!characterId) {
toastr.error('No characters found');
return;
}
setCharacterId(characterId);
setActiveCharacter(characters[characterId]?.avatar);
setActiveGroup(null);
await delay(1);
await reloadCurrentChat();
return characters[characterId]?.name;
}
/**
* Loads the chat until the given message ID is displayed.
* @param {number} mesId
* @returns JQuery<HTMLElement>
*/
async function loadUntilMesId(mesId) {
let target;
while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
await showMoreMessages();
await delay(1);
target = $('#chat').find(`.mes[mesid="${mesId}"]`);
if (target.length) {
break;
}
}
if (!target.length) {
toastr.error(`Could not find message with ID: ${mesId}`);
return target;
}
return target;
}
async function doMesCut(_, text) {
console.debug(`was asked to cut message id #${text}`);
const range = stringToRange(text, 0, chat.length - 1);
//reject invalid args or no args
if (!range) {
toastr.warning('Must provide a Message ID or a range to cut.');
return;
}
let totalMesToCut = (range.end - range.start) + 1;
let mesIDToCut = range.start;
let cutText = '';
for (let i = 0; i < totalMesToCut; i++) {
cutText += (chat[mesIDToCut]?.mes || '') + '\n';
let mesToCut = $('#chat').find(`.mes[mesid=${mesIDToCut}]`);
if (!mesToCut.length) {
mesToCut = await loadUntilMesId(mesIDToCut);
if (!mesToCut || !mesToCut.length) {
return;
}
}
setEditedMessageId(mesIDToCut);
await deleteMessage(mesIDToCut, null, false);
}
await saveChatConditional();
return cutText;
}
async function doDelMode(_, text) {
//reject invalid args
if (text && isNaN(text)) {
toastr.warning('Must enter a number or nothing.');
return '';
}
// Just enter the delete mode.
if (!text) {
$('#option_delete_mes').trigger('click', { fromSlashCommand: true });
return '';
}
const count = Number(text);
// Nothing to delete.
if (count < 1) {
return '';
}
if (count > chat.length) {
toastr.warning(`Cannot delete more than ${chat.length} messages.`);
return '';
}
const range = `${chat.length - count}-${chat.length - 1}`;
return doMesCut(_, range);
}
function doResetPanels() {
$('#movingUIreset').trigger('click');
return '';
}
function setAvgBG() {
const bgimg = new Image();
bgimg.src = $('#bg1')
.css('background-image')
.replace(/^url\(['"]?/, '')
.replace(/['"]?\)$/, '');
/* const charAvatar = new Image()
charAvatar.src = $("#avatar_load_preview")
.attr('src')
.replace(/^url\(['"]?/, '')
.replace(/['"]?\)$/, '');
const userAvatar = new Image()
userAvatar.src = $("#user_avatar_block .avatar.selected img")
.attr('src')
.replace(/^url\(['"]?/, '')
.replace(/['"]?\)$/, ''); */
bgimg.onload = function () {
var rgb = getAverageRGB(bgimg);
//console.log(`average color of the bg is:`)
//console.log(rgb);
$('#blur-tint-color-picker').attr('color', 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')');
const backgroundColorString = $('#blur-tint-color-picker').attr('color')
.replace('rgba', '')
.replace('rgb', '')
.replace('(', '[')
.replace(')', ']'); //[50, 120, 200, 1]; // Example background color
const backgroundColorArray = JSON.parse(backgroundColorString); //[200, 200, 200, 1]
console.log(backgroundColorArray);
$('#main-text-color-picker').attr('color', getReadableTextColor(backgroundColorArray));
console.log($('#main-text-color-picker').attr('color')); // Output: 'rgba(0, 47, 126, 1)'
};
/* charAvatar.onload = function () {
var rgb = getAverageRGB(charAvatar);
//console.log(`average color of the AI avatar is:`);
//console.log(rgb);
$("#bot-mes-blur-tint-color-picker").attr('color', 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')');
}
userAvatar.onload = function () {
var rgb = getAverageRGB(userAvatar);
//console.log(`average color of the user avatar is:`);
//console.log(rgb);
$("#user-mes-blur-tint-color-picker").attr('color', 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')');
} */
function getAverageRGB(imgEl) {
var blockSize = 5, // only visit every 5 pixels
defaultRGB = { r: 0, g: 0, b: 0 }, // for non-supporting envs
canvas = document.createElement('canvas'),
context = canvas.getContext && canvas.getContext('2d'),
data, width, height,
i = -4,
length,
rgb = { r: 0, g: 0, b: 0 },
count = 0;
if (!context) {
return defaultRGB;
}
height = canvas.height = imgEl.naturalHeight || imgEl.offsetHeight || imgEl.height;
width = canvas.width = imgEl.naturalWidth || imgEl.offsetWidth || imgEl.width;
context.drawImage(imgEl, 0, 0);
try {
data = context.getImageData(0, 0, width, height);
} catch (e) {
/* security error, img on diff domain */alert('x');
return defaultRGB;
}
length = data.data.length;
while ((i += blockSize * 4) < length) {
++count;
rgb.r += data.data[i];
rgb.g += data.data[i + 1];
rgb.b += data.data[i + 2];
}
// ~~ used to floor values
rgb.r = ~~(rgb.r / count);
rgb.g = ~~(rgb.g / count);
rgb.b = ~~(rgb.b / count);
return rgb;
}
/**
* Converts an HSL color value to RGB.
* @param {number} h Hue value
* @param {number} s Saturation value
* @param {number} l Luminance value
* @return {Array} The RGB representation
*/
function hslToRgb(h, s, l) {
const hueToRgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
if (s === 0) {
return [l, l, l];
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
const r = hueToRgb(p, q, h + 1 / 3);
const g = hueToRgb(p, q, h);
const b = hueToRgb(p, q, h - 1 / 3);
return [r * 255, g * 255, b * 255];
}
//this version keeps BG and main text in same hue
/* function getReadableTextColor(rgb) {
const [r, g, b] = rgb;
// Convert RGB to HSL
const rgbToHsl = (r, g, b) => {
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const l = (max + min) / 2;
if (d === 0) return [0, 0, l];
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
const h = (() => {
switch (max) {
case r:
return (g - b) / d + (g < b ? 6 : 0);
case g:
return (b - r) / d + 2;
case b:
return (r - g) / d + 4;
}
})() / 6;
return [h, s, l];
};
const [h, s, l] = rgbToHsl(r / 255, g / 255, b / 255);
// Calculate appropriate text color based on background color
const targetLuminance = l > 0.5 ? 0.2 : 0.8;
const targetSaturation = s > 0.5 ? s - 0.2 : s + 0.2;
const [rNew, gNew, bNew] = hslToRgb(h, targetSaturation, targetLuminance);
// Return the text color in RGBA format
return `rgba(${rNew.toFixed(0)}, ${gNew.toFixed(0)}, ${bNew.toFixed(0)}, 1)`;
}*/
//this version makes main text complimentary color to BG color
function getReadableTextColor(rgb) {
const [r, g, b] = rgb;
// Convert RGB to HSL
const rgbToHsl = (r, g, b) => {
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const l = (max + min) / 2;
if (d === 0) return [0, 0, l];
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
const h = (() => {
switch (max) {
case r:
return (g - b) / d + (g < b ? 6 : 0);
case g:
return (b - r) / d + 2;
case b:
return (r - g) / d + 4;
}
})() / 6;
return [h, s, l];
};
const [h, s, l] = rgbToHsl(r / 255, g / 255, b / 255);
// Calculate complementary color based on background color
const complementaryHue = (h + 0.5) % 1;
const complementarySaturation = s > 0.5 ? s - 0.6 : s + 0.6;
const complementaryLuminance = l > 0.5 ? 0.2 : 0.8;
// Convert complementary color back to RGB
const [rNew, gNew, bNew] = hslToRgb(complementaryHue, complementarySaturation, complementaryLuminance);
// Return the text color in RGBA format
return `rgba(${rNew.toFixed(0)}, ${gNew.toFixed(0)}, ${bNew.toFixed(0)}, 1)`;
}
return '';
}
async function setThemeCallback(_, themeName) {
if (!themeName) {
// allow reporting of the theme name if called without args
// for use in ST Scripts via pipe
return power_user.theme;
}
// @ts-ignore
const fuse = new Fuse(themes, {
keys: [
{ name: 'name', weight: 1 },
],
});
const results = fuse.search(themeName);
console.debug('Theme fuzzy search results for ' + themeName, results);
const theme = results[0]?.item;
if (!theme) {
toastr.warning(`Could not find theme with name: ${themeName}`);
return;
}
power_user.theme = theme.name;
applyTheme(theme.name);
$('#themes').val(theme.name);
saveSettingsDebounced();
return '';
}
async function setmovingUIPreset(_, text) {
// @ts-ignore
const fuse = new Fuse(movingUIPresets, {
keys: [
{ name: 'name', weight: 1 },
],
});
const results = fuse.search(text);
console.debug('movingUI preset fuzzy search results for ' + text, results);
const preset = results[0]?.item;
if (!preset) {
toastr.warning(`Could not find preset with name: ${text}`);
return;
}
power_user.movingUIPreset = preset.name;
applyMovingUIPreset(preset.name);
$('#movingUIPresets').val(preset.name);
saveSettingsDebounced();
return '';
}
const EPHEMERAL_STOPPING_STRINGS = [];
/**
* Adds a stopping string to the list of stopping strings that are only used for the next generation.
* @param {string} value The stopping string to add
*/
export function addEphemeralStoppingString(value) {
if (!EPHEMERAL_STOPPING_STRINGS.includes(value)) {
console.debug('Adding ephemeral stopping string:', value);
EPHEMERAL_STOPPING_STRINGS.push(value);
}
}
export function flushEphemeralStoppingStrings() {
if (EPHEMERAL_STOPPING_STRINGS.length === 0) {
return;
}
console.debug('Flushing ephemeral stopping strings:', EPHEMERAL_STOPPING_STRINGS);
EPHEMERAL_STOPPING_STRINGS.splice(0, EPHEMERAL_STOPPING_STRINGS.length);
}
/**
* Checks if the generated text should be filtered based on the auto-swipe settings.
* @param {string} text The text to check
* @returns {boolean} If the generated text should be filtered
*/
export function generatedTextFiltered(text) {
/**
* Checks if the given text contains any of the blacklisted words.
* @param {string} text The text to check
* @param {string[]} blacklist The list of blacklisted words
* @param {number} threshold The number of blacklisted words that need to be present to trigger the check
* @returns {boolean} Whether the text contains blacklisted words
*/
function containsBlacklistedWords(text, blacklist, threshold) {
const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
const matches = text.match(regex) || [];
return matches.length >= threshold;
}
// Make sure a generated text is non-empty
// Otherwise we might get in a loop with a broken API
text = text.trim();
if (text.length > 0) {
if (power_user.auto_swipe_minimum_length) {
if (text.length < power_user.auto_swipe_minimum_length) {
console.log('Generated text size too small');
return true;
}
}
if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
console.log('Generated text has blacklisted words');
return true;
}
}
}
return false;
}
/**
* Gets the custom stopping strings from the power user settings.
* @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
* @returns {string[]} An array of custom stopping strings
*/
export function getCustomStoppingStrings(limit = undefined) {
function getPermanent() {
try {
// If there's no custom stopping strings, return an empty array
if (!power_user.custom_stopping_strings) {
return [];
}
// Parse the JSON string
let strings = JSON.parse(power_user.custom_stopping_strings);
// Make sure it's an array
if (!Array.isArray(strings)) {
return [];
}
// Make sure all the elements are strings and non-empty.
strings = strings.filter(s => typeof s === 'string' && s.length > 0);
// Substitute params if necessary
if (power_user.custom_stopping_strings_macro) {
strings = strings.map(x => substituteParams(x));
}
return strings;
} catch (error) {
// If there's an error, return an empty array
console.warn('Error parsing custom stopping strings:', error);
return [];
}
}
const permanent = getPermanent();
const ephemeral = EPHEMERAL_STOPPING_STRINGS;
const strings = [...permanent, ...ephemeral];
// Apply the limit. If limit is 0, return all strings.
if (limit > 0) {
return strings.slice(0, limit);
}
return strings;
}
export function forceCharacterEditorTokenize() {
$('[data-token-counter]').each(function () {
$(document.getElementById($(this).data('token-counter'))).data('last-value-hash', '');
});
$('#rm_ch_create_block').trigger('input');
$('#character_popup').trigger('input');
}
jQuery(() => {
const adjustAutocompleteDebounced = debounce(() => {
$('.ui-autocomplete-input').each(function () {
const isOpen = $(this).autocomplete('widget')[0].style.display !== 'none';
if (isOpen) {
$(this).autocomplete('search');
}
});
});
const reportZoomLevelDebounced = debounce(() => {
const zoomLevel = parseFloat(Number(window.devicePixelRatio).toFixed(2)) || 1;
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
const originalWidth = winWidth * zoomLevel;
const originalHeight = winHeight * zoomLevel;
console.debug(`Window resize: ${coreTruthWinWidth}x${coreTruthWinHeight} -> ${window.innerWidth}x${window.innerHeight}`);
console.debug(`Zoom: ${zoomLevel}, X:${winWidth}, Y:${winHeight}, original: ${originalWidth}x${originalHeight} `);
return zoomLevel;
});
var coreTruthWinWidth = window.innerWidth;
var coreTruthWinHeight = window.innerHeight;
$(window).on('resize', async () => {
adjustAutocompleteDebounced();
setHotswapsDebounced();
if (isMobile()) {
return;
}
reportZoomLevelDebounced();
//attempt to scale movingUI elements naturally across window resizing/zooms
//this will still break if the zoom level causes mobile styles to come into play.
const scaleY = parseFloat(Number(window.innerHeight / coreTruthWinHeight).toFixed(4));
const scaleX = parseFloat(Number(window.innerWidth / coreTruthWinWidth).toFixed(4));
if (Object.keys(power_user.movingUIState).length > 0) {
for (var elmntName of Object.keys(power_user.movingUIState)) {
var elmntState = power_user.movingUIState[elmntName];
var oldHeight = elmntState.height;
var oldWidth = elmntState.width;
var oldLeft = elmntState.left;
var oldTop = elmntState.top;
var oldBottom = elmntState.bottom;
var oldRight = elmntState.right;
var newHeight, newWidth, newTop, newBottom, newLeft, newRight;
newHeight = Number(oldHeight * scaleY).toFixed(0);
newWidth = Number(oldWidth * scaleX).toFixed(0);
newLeft = Number(oldLeft * scaleX).toFixed(0);
newTop = Number(oldTop * scaleY).toFixed(0);
newBottom = Number(oldBottom * scaleY).toFixed(0);
newRight = Number(oldRight * scaleX).toFixed(0);
try {
var elmnt = $('#' + $.escapeSelector(elmntName));
if (elmnt.length) {
console.log(`scaling ${elmntName} by ${scaleX}x${scaleY} to ${newWidth}x${newHeight}`);
elmnt.css('height', newHeight);
elmnt.css('width', newWidth);
elmnt.css('inset', `${newTop}px ${newRight}px ${newBottom}px ${newLeft}px`);
power_user.movingUIState[elmntName].height = newHeight;
power_user.movingUIState[elmntName].width = newWidth;
power_user.movingUIState[elmntName].top = newTop;
power_user.movingUIState[elmntName].bottom = newBottom;
power_user.movingUIState[elmntName].left = newLeft;
power_user.movingUIState[elmntName].right = newRight;
} else {
console.log(`skipping ${elmntName} because it doesn't exist in the DOM`);
}
} catch (err) {
console.log(`error occurred while processing ${elmntName}: ${err}`);
}
}
} else {
console.debug('aborting MUI reset', Object.keys(power_user.movingUIState).length);
}
saveSettingsDebounced();
coreTruthWinWidth = window.innerWidth;
coreTruthWinHeight = window.innerHeight;
});
// Settings that go to settings.json
$('#collapse-newlines-checkbox').on('change', function () {
power_user.collapse_newlines = !!$(this).prop('checked');
saveSettingsDebounced();
});
// include newline is the child of trim sentences
// if include newline is checked, trim sentences must be checked
// if trim sentences is unchecked, include newline must be unchecked
$('#trim_sentences_checkbox').on('change', function () {
power_user.trim_sentences = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#single_line').on('input', function () {
const value = !!$(this).prop('checked');
power_user.single_line = value;
saveSettingsDebounced();
});
$('#context_derived').on('input', function () {
const value = !!$(this).prop('checked');
power_user.context_derived = value;
saveSettingsDebounced();
});
$('#context_derived').on('change', function () {
$('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);
});
$('#instruct_derived').on('input', function () {
const value = !!$(this).prop('checked');
power_user.instruct_derived = value;
saveSettingsDebounced();
});
$('#instruct_derived').on('change', function () {
$('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived);
});
$('#context_size_derived').on('input', function () {
const value = !!$(this).prop('checked');
power_user.context_size_derived = value;
saveSettingsDebounced();
});
$('#context_size_derived').on('change', function () {
$('#context_size_derived').prop('checked', !!power_user.context_size_derived);
});
$('#context_story_string_position').on('input', function () {
const value = Number($(this).val());
$('#context_story_string_inject_settings').toggle(value === extension_prompt_types.IN_CHAT);
});
$('#bind_model_templates').on('input', function () {
if (bindModelTemplates(power_user, online_status)) {
saveSettingsDebounced();
}
});
$('#bind_model_templates').on('change', updateBindModelTemplatesState);
$('#always-force-name2-checkbox').on('change', function () {
power_user.always_force_name2 = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#markdown_escape_strings').on('input', function () {
power_user.markdown_escape_strings = String($(this).val());
saveSettingsDebounced();
reloadMarkdownProcessor();
});
$('#start_reply_with').on('input', function () {
power_user.user_prompt_bias = String($(this).val());
saveSettingsDebounced();
});
$('#chat-show-reply-prefix-checkbox').on('change', function () {
power_user.show_user_prompt_bias = !!$(this).prop('checked');
reloadCurrentChat();
saveSettingsDebounced();
});
$('#auto_continue_enabled').on('change', function () {
power_user.auto_continue.enabled = $(this).prop('checked');
saveSettingsDebounced();
});
$('#auto_continue_allow_chat_completions').on('change', function () {
power_user.auto_continue.allow_chat_completions = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#auto_continue_target_length').on('input', function () {
power_user.auto_continue.target_length = Number($(this).val());
saveSettingsDebounced();
});
$('#example_messages_behavior').on('change', function () {
const selectedOption = String($(this).find(':selected').val());
console.log('Setting example messages behavior to', selectedOption);
switch (selectedOption) {
case 'normal':
power_user.pin_examples = false;
power_user.strip_examples = false;
break;
case 'keep':
power_user.pin_examples = true;
power_user.strip_examples = false;
break;
case 'strip':
power_user.pin_examples = false;
power_user.strip_examples = true;
break;
}
console.debug('power_user.pin_examples', power_user.pin_examples);
console.debug('power_user.strip_examples', power_user.strip_examples);
saveSettingsDebounced();
});
$('#fast_ui_mode').on('change', function () {
power_user.fast_ui_mode = $(this).prop('checked');
switchUiMode();
saveSettingsDebounced();
});
$('#waifuMode').on('change', () => {
power_user.waifuMode = !!$('#waifuMode').prop('checked');
switchWaifuMode();
saveSettingsDebounced();
});
$('#customCSS').on('input', () => {
power_user.custom_css = String($('#customCSS').val());
saveSettingsDebounced();
applyCustomCSS();
});
$('#movingUImode').on('change', function () {
power_user.movingUI = $(this).prop('checked');
switchMovingUI();
saveSettingsDebounced();
});
$('#noShadowsmode').on('change', function () {
power_user.noShadows = $(this).prop('checked');
applyNoShadows();
saveSettingsDebounced();
});
$('#movingUIreset').on('click', resetMovablePanels);
$('#avatar_style').on('change', function () {
const value = $(this).find(':selected').val();
power_user.avatar_style = Number(value);
applyAvatarStyle();
saveSettingsDebounced();
});
$('#chat_display').on('change', function () {
const value = $(this).find(':selected').val();
power_user.chat_display = Number(value);
applyChatDisplay();
saveSettingsDebounced();
});
$('#toastr_position').on('change', function () {
const value = $(this).find(':selected').val();
power_user.toastr_position = String(value);
applyToastrPosition();
saveSettingsDebounced();
});
$('#chat_width_slider').on('input', function (e, data) {
const applyMode = data?.forced ? 'forced' : 'normal';
power_user.chat_width = Number($(this).val());
applyChatWidth(applyMode);
saveSettingsDebounced();
setHotswapsDebounced();
});
$('#chat_truncation').on('input', function () {
power_user.chat_truncation = Number($('#chat_truncation').val());
$('#chat_truncation_counter').val(power_user.chat_truncation);
saveSettingsDebounced();
});
$('#streaming_fps').on('input', function () {
power_user.streaming_fps = Number($('#streaming_fps').val());
$('#streaming_fps_counter').val(power_user.streaming_fps);
saveSettingsDebounced();
});
$('#smooth_streaming').on('input', function () {
power_user.smooth_streaming = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#smooth_streaming_no_think').on('input', function () {
power_user.smooth_streaming_no_think = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#smooth_streaming_speed').on('input', function () {
power_user.smooth_streaming_speed = Number($('#smooth_streaming_speed').val());
saveSettingsDebounced();
});
$('#stream_fade_in').on('input', function () {
power_user.stream_fade_in = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('input[name="font_scale"]').on('input', async function (e, data) {
const applyMode = data?.forced ? 'forced' : 'normal';
power_user.font_scale = Number($(this).val());
$('#font_scale_counter').val(power_user.font_scale);
applyFontScale(applyMode);
saveSettingsDebounced();
});
$('input[name="blur_strength"]').on('input', async function (e) {
power_user.blur_strength = Number($(this).val());
$('#blur_strength_counter').val(power_user.blur_strength);
applyBlurStrength();
saveSettingsDebounced();
});
$('input[name="shadow_width"]').on('input', async function (e) {
power_user.shadow_width = Number($(this).val());
$('#shadow_width_counter').val(power_user.shadow_width);
applyShadowWidth();
saveSettingsDebounced();
});
$('#main-text-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
power_user.main_text_color = evt.detail.rgba;
applyThemeColor('main');
saveSettingsDebounced();
});
$('#italics-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
power_user.italics_text_color = evt.detail.rgba;
applyThemeColor('italics');
saveSettingsDebounced();
});
$('#underline-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
power_user.underline_text_color = evt.detail.rgba;
applyThemeColor('underline');
saveSettingsDebounced();
});
$('#quote-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
power_user.quote_text_color = evt.detail.rgba;
applyThemeColor('quote');
saveSettingsDebounced();
});
$('#blur-tint-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
power_user.blur_tint_color = evt.detail.rgba;
applyThemeColor('blurTint');
saveSettingsDebounced();
});
$('#chat-tint-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
power_user.chat_tint_color = evt.detail.rgba;
applyThemeColor('chatTint');
saveSettingsDebounced();
});
$('#user-mes-blur-tint-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
power_user.user_mes_blur_tint_color = evt.detail.rgba;
applyThemeColor('userMesBlurTint');
saveSettingsDebounced();
});
$('#bot-mes-blur-tint-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
power_user.bot_mes_blur_tint_color = evt.detail.rgba;
applyThemeColor('botMesBlurTint');
saveSettingsDebounced();
});
$('#shadow-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
power_user.shadow_color = evt.detail.rgba;
applyThemeColor('shadow');
saveSettingsDebounced();
});
$('#border-color-picker').on('change', (/** @type {ColorPickerEvent} */ evt) => {
power_user.border_color = evt.detail.rgba;
applyThemeColor('border');
saveSettingsDebounced();
});
$('#themes').on('change', function () {
const themeSelected = String($(this).find(':selected').val());
power_user.theme = themeSelected;
applyTheme(themeSelected);
saveSettingsDebounced();
});
$('#movingUIPresets').on('change', async function () {
console.log('saw MUI preset change');
const movingUIPresetSelected = String($(this).find(':selected').val());
power_user.movingUIPreset = movingUIPresetSelected;
applyMovingUIPreset(movingUIPresetSelected);
saveSettingsDebounced();
});
$('#ui-preset-save-button').on('click', () => saveTheme());
$('#ui-preset-update-button').on('click', () => updateTheme());
$('#ui-preset-delete-button').on('click', () => deleteTheme());
$('#movingui-preset-save-button').on('click', saveMovingUI);
$('#never_resize_avatars').on('input', function () {
power_user.never_resize_avatars = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#show_card_avatar_urls').on('input', function () {
power_user.show_card_avatar_urls = !!$(this).prop('checked');
printCharactersDebounced();
saveSettingsDebounced();
});
$('#play_message_sound').on('input', function () {
power_user.play_message_sound = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#play_sound_unfocused').on('input', function () {
power_user.play_sound_unfocused = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#auto_save_msg_edits').on('input', function () {
power_user.auto_save_msg_edits = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#character_sort_order').on('change', function () {
const field = String($(this).find(':selected').data('field'));
// Save sort order, but do not save search sorting, as this is a temporary sorting option
if (field !== 'search') {
power_user.sort_field = field;
power_user.sort_order = $(this).find(':selected').data('order');
power_user.sort_rule = $(this).find(':selected').data('rule');
}
printCharactersDebounced();
saveSettingsDebounced();
});
$('#gestures-checkbox').on('change', function () {
power_user.gestures = !!$('#gestures-checkbox').prop('checked');
saveSettingsDebounced();
});
$('#auto_swipe').on('input', function () {
power_user.auto_swipe = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#auto_swipe_blacklist').on('input', function () {
power_user.auto_swipe_blacklist = String($(this).val())
.split(',')
.map(str => str.trim())
.filter(str => str);
console.log('power_user.auto_swipe_blacklist', power_user.auto_swipe_blacklist);
saveSettingsDebounced();
});
$('#auto_swipe_minimum_length').on('input', function () {
const number = Number($(this).val());
if (!isNaN(number)) {
power_user.auto_swipe_minimum_length = number;
saveSettingsDebounced();
}
});
$('#auto_swipe_blacklist_threshold').on('input', function () {
const number = Number($(this).val());
if (!isNaN(number)) {
power_user.auto_swipe_blacklist_threshold = number;
saveSettingsDebounced();
}
});
$('#auto_fix_generated_markdown').on('input', function () {
power_user.auto_fix_generated_markdown = !!$(this).prop('checked');
reloadCurrentChat();
saveSettingsDebounced();
});
$('#console_log_prompts').on('input', function () {
power_user.console_log_prompts = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#request_token_probabilities').on('input', function () {
power_user.request_token_probabilities = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#show_group_chat_queue').on('input', function () {
power_user.show_group_chat_queue = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#auto_scroll_chat_to_bottom').on('input', function () {
power_user.auto_scroll_chat_to_bottom = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#tokenizer').on('change', function () {
const value = $(this).find(':selected').val();
power_user.tokenizer = Number(value);
BIAS_CACHE.clear();
saveSettingsDebounced();
// Trigger character editor re-tokenize
forceCharacterEditorTokenize();
});
$('#send_on_enter').on('change', function () {
const value = $(this).find(':selected').val();
power_user.send_on_enter = Number(value);
saveSettingsDebounced();
});
$('#confirm_message_delete').on('input', function () {
power_user.confirm_message_delete = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#reload_chat').on('click', async function () {
const currentChatId = getCurrentChatId();
if (currentChatId !== undefined && currentChatId !== null) {
await saveSettings();
await saveChatConditional();
await reloadCurrentChat();
}
});
$('#allow_name1_display').on('input', function () {
power_user.allow_name1_display = !!$(this).prop('checked');
reloadCurrentChat();
saveSettingsDebounced();
});
$('#allow_name2_display').on('input', function () {
power_user.allow_name2_display = !!$(this).prop('checked');
reloadCurrentChat();
saveSettingsDebounced();
});
$('#token_padding').on('input', function () {
power_user.token_padding = Number($(this).val());
saveSettingsDebounced();
});
$('#messageTimerEnabled').on('input', function () {
const value = !!$(this).prop('checked');
power_user.timer_enabled = value;
switchTimer();
saveSettingsDebounced();
});
$('#messageTimestampsEnabled').on('input', function () {
const value = !!$(this).prop('checked');
power_user.timestamps_enabled = value;
switchTimestamps();
saveSettingsDebounced();
});
$('#messageModelIconEnabled').on('input', function () {
const value = !!$(this).prop('checked');
power_user.timestamp_model_icon = value;
switchIcons();
saveSettingsDebounced();
});
$('#messageTokensEnabled').on('input', function () {
const value = !!$(this).prop('checked');
power_user.message_token_count_enabled = value;
switchTokenCount();
saveSettingsDebounced();
});
$('#expandMessageActions').on('input', function () {
const value = !!$(this).prop('checked');
power_user.expand_message_actions = value;
switchMessageActions();
saveSettingsDebounced();
});
$('#enableZenSliders').on('input', function () {
const value = !!$(this).prop('checked');
if (power_user.enableLabMode === true && value === true) {
//disallow zenSliders while Lab Mode is active
toastr.warning('Disable Mad Lab Mode before enabling Zen Sliders');
$(this).prop('checked', false).trigger('input');
return;
}
power_user.enableZenSliders = value;
switchZenSliders();
saveSettingsDebounced();
});
$('#enableLabMode').on('input', function (event, { fromInit = false } = {}) {
const value = !!$(this).prop('checked');
if (power_user.enableZenSliders === true && value === true) {
//disallow Lab Mode if ZenSliders are active
toastr.warning('Disable Zen Sliders before enabling Mad Lab Mode');
$(this).prop('checked', false).trigger('input');
return;
}
power_user.enableLabMode = value;
switchLabMode({ noReset: fromInit });
saveSettingsDebounced();
});
$('#mesIDDisplayEnabled').on('input', function () {
const value = !!$(this).prop('checked');
power_user.mesIDDisplay_enabled = value;
switchMesIDDisplay();
saveSettingsDebounced();
});
$('#hideChatAvatarsEnabled').on('input', function () {
const value = !!$(this).prop('checked');
power_user.hideChatAvatars_enabled = value;
switchHideChatAvatars();
saveSettingsDebounced();
});
$('#hotswapEnabled').on('input', function () {
const value = !!$(this).prop('checked');
power_user.hotswap_enabled = value;
switchHotswap();
saveSettingsDebounced();
});
$('#prefer_character_prompt').on('input', function () {
const value = !!$(this).prop('checked');
power_user.prefer_character_prompt = value;
saveSettingsDebounced();
});
$('#prefer_character_jailbreak').on('input', function () {
const value = !!$(this).prop('checked');
power_user.prefer_character_jailbreak = value;
saveSettingsDebounced();
});
$('#continue_on_send').on('input', function () {
const value = !!$(this).prop('checked');
power_user.continue_on_send = value;
saveSettingsDebounced();
});
$('#quick_continue').on('input', function () {
const value = !!$(this).prop('checked');
power_user.quick_continue = value;
$('#mes_continue').css('display', value ? '' : 'none');
saveSettingsDebounced();
});
$('#quick_impersonate').on('input', function () {
const value = !!$(this).prop('checked');
power_user.quick_impersonate = value;
$('#mes_impersonate').css('display', value ? '' : 'none');
saveSettingsDebounced();
});
$('#trim_spaces').on('input', function () {
const value = !!$(this).prop('checked');
power_user.trim_spaces = value;
saveSettingsDebounced();
});
$('#relaxed_api_urls').on('input', function () {
const value = !!$(this).prop('checked');
power_user.relaxed_api_urls = value;
saveSettingsDebounced();
});
$('#world_import_dialog').on('input', function () {
const value = !!$(this).prop('checked');
power_user.world_import_dialog = value;
saveSettingsDebounced();
});
$('#enable_auto_select_input').on('input', function () {
const value = !!$(this).prop('checked');
power_user.enable_auto_select_input = value;
saveSettingsDebounced();
});
$('#enable_md_hotkeys').on('input', function () {
const value = !!$(this).prop('checked');
power_user.enable_md_hotkeys = value;
toggleMDHotkeyIconDisplay();
saveSettingsDebounced();
});
$('#spoiler_free_mode').on('input', function () {
power_user.spoiler_free_mode = !!$(this).prop('checked');
switchSpoilerMode();
saveSettingsDebounced();
});
$('#spoiler_free_desc_button').on('click', function (e) {
e.stopPropagation();
peekSpoilerMode();
$(this).toggleClass('fa-eye fa-eye-slash');
});
$('#custom_stopping_strings').on('input', function () {
power_user.custom_stopping_strings = String($(this).val()).trim();
saveSettingsDebounced();
});
$('#custom_stopping_strings_macro').on('change', function () {
power_user.custom_stopping_strings_macro = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#fuzzy_search_checkbox').on('input', function () {
power_user.fuzzy_search = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#persona_show_notifications').on('input', function () {
power_user.persona_show_notifications = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#persona_allow_multi_connections').on('input', function () {
power_user.persona_allow_multi_connections = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#persona_auto_lock').on('input', function () {
power_user.persona_auto_lock = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#encode_tags').on('input', async function () {
power_user.encode_tags = !!$(this).prop('checked');
await reloadCurrentChat();
saveSettingsDebounced();
});
$('#experimental_macro_engine').on('input', function () {
power_user.experimental_macro_engine = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#disable_group_trimming').on('input', function () {
power_user.disable_group_trimming = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#debug_menu').on('click', function () {
showDebugMenu();
});
$('#bogus_folders').on('input', function () {
power_user.bogus_folders = !!$(this).prop('checked');
printCharactersDebounced();
saveSettingsDebounced();
});
$('#zoomed_avatar_magnification').on('input', function () {
power_user.zoomed_avatar_magnification = !!$(this).prop('checked');
printCharactersDebounced();
saveSettingsDebounced();
});
$('#aux_field').on('change', function () {
const value = $(this).find(':selected').val();
power_user.aux_field = String(value);
printCharactersDebounced();
saveSettingsDebounced();
});
$('#tag_import_setting').on('change', function () {
const value = $(this).find(':selected').val();
power_user.tag_import_setting = Number(value);
saveSettingsDebounced();
});
$('#stscript_autocomplete_state').on('input', function () {
power_user.stscript.autocomplete.state = Number($(this).val());
saveSettingsDebounced();
});
$('#stscript_autocomplete_autoHide').on('input', function () {
power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#stscript_matching').on('change', function () {
const value = $(this).find(':selected').val();
power_user.stscript.matching = String(value);
saveSettingsDebounced();
});
$('#stscript_autocomplete_style').on('change', function () {
const value = $(this).find(':selected').val();
power_user.stscript.autocomplete.style = String(value);
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style);
saveSettingsDebounced();
});
$('#stscript_autocomplete_select').on('change', function () {
const value = $(this).find(':selected').val();
power_user.stscript.autocomplete.select = parseInt(String(value));
saveSettingsDebounced();
});
$('#stscript_autocomplete_font_scale').on('input', function () {
const value = $(this).val();
$('#stscript_autocomplete_font_scale_counter').val(value);
power_user.stscript.autocomplete.font.scale = Number(value);
document.body.style.setProperty('--ac-font-scale', value.toString());
window.dispatchEvent(new Event('resize', { bubbles: true }));
saveSettingsDebounced();
});
$('#stscript_autocomplete_font_scale_counter').on('input', function () {
const value = $(this).val();
$('#stscript_autocomplete_font_scale').val(value);
power_user.stscript.autocomplete.font.scale = Number(value);
document.body.style.setProperty('--ac-font-scale', value.toString());
window.dispatchEvent(new Event('resize', { bubbles: true }));
saveSettingsDebounced();
});
$('#stscript_autocomplete_width_left').on('input', function () {
const value = $(this).val();
power_user.stscript.autocomplete.width.left = Number(value);
/**@type {HTMLElement}*/(this.closest('.doubleRangeInputContainer')).style.setProperty('--value', value.toString());
window.dispatchEvent(new Event('resize', { bubbles: true }));
saveSettingsDebounced();
});
$('#stscript_autocomplete_width_right').on('input', function () {
const value = $(this).val();
power_user.stscript.autocomplete.width.right = Number(value);
/**@type {HTMLElement}*/(this.closest('.doubleRangeInputContainer')).style.setProperty('--value', value.toString());
window.dispatchEvent(new Event('resize', { bubbles: true }));
saveSettingsDebounced();
});
$('#stscript_parser_flag_strict_escaping').on('click', function () {
const value = $(this).prop('checked');
power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] = value;
saveSettingsDebounced();
});
$('#stscript_parser_flag_replace_getvar').on('click', function () {
const value = $(this).prop('checked');
power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] = value;
saveSettingsDebounced();
});
$('#restore_user_input').on('input', function () {
power_user.restore_user_input = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#reduced_motion').on('input', function () {
power_user.reduced_motion = !!$(this).prop('checked');
switchReducedMotion();
saveSettingsDebounced();
});
$('#compact_input_area').on('input', function () {
power_user.compact_input_area = !!$(this).prop('checked');
switchCompactInputArea();
saveSettingsDebounced();
});
$('#show_swipe_num_all_messages').on('input', function () {
power_user.show_swipe_num_all_messages = !!$(this).prop('checked');
switchSwipeNumAllMessages();
saveSettingsDebounced();
});
$('#auto-connect-checkbox').on('input', function () {
power_user.auto_connect = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#auto-load-chat-checkbox').on('input', function () {
power_user.auto_load_chat = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#forbid_external_media').on('input', function () {
power_user.forbid_external_media = !!$(this).prop('checked');
saveSettingsDebounced();
reloadCurrentChat();
});
$('#pin_styles').on('input', function () {
power_user.pin_styles = !!$(this).prop('checked');
saveSettingsDebounced();
applyStylePins();
});
$('#click_to_edit').on('input', function () {
power_user.click_to_edit = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#ui_preset_import_button').on('click', function () {
$('#ui_preset_import_file').trigger('click');
});
$('#ui_preset_import_file').on('change', async function () {
const inputElement = this instanceof HTMLInputElement && this;
try {
const file = inputElement?.files?.[0];
await importTheme(file);
} catch (error) {
console.error('Error importing UI theme', error);
toastr.error(String(error), 'Failed to import UI theme');
} finally {
if (inputElement) {
inputElement.value = null;
}
}
});
$('#ui_preset_export_button').on('click', async function () {
await exportTheme();
});
$('#media_display').on('input', async function () {
power_user.media_display = $(this).val().toString();
saveSettingsDebounced();
if (isMediaDisplayReloadNeeded()) {
await reloadCurrentChat();
}
});
$('#image_overswipe').on('input', function () {
power_user.image_overswipe = $(this).val().toString();
saveSettingsDebounced();
});
$(document).on('click', '#debug_table [data-debug-function]', function () {
const functionId = $(this).data('debug-function');
const functionRecord = debug_functions.find(f => f.functionId === functionId);
if (functionRecord) {
functionRecord.func();
} else {
console.warn(`Debug function ${functionId} not found`);
}
});
$(window).on('focus', function () {
browser_has_focus = true;
});
$(window).on('blur', function () {
browser_has_focus = false;
});
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'vn',
callback: toggleWaifu,
helpString: 'Swaps Visual Novel Mode On/Off',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'newchat',
/** @type {(args: { delete: string?}, string) => Promise<''>} */
callback: async (args, _) => {
await doNewChat({ deleteCurrentChat: isTrueBoolean(args.delete) });
return '';
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'delete',
description: 'delete the current chat',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'false',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
helpString: 'Start a new chat with the current character',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'random',
callback: doRandomChat,
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'optional tag name',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: () => tags.filter(tag => Object.values(tag_map).some(x => x.includes(tag.id))).map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.enum, enumIcons.tag)),
}),
],
helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'del',
callback: doDelMode,
aliases: ['delete', 'delmode'],
unnamedArgumentList: [
new SlashCommandArgument(
'optional number', [ARGUMENT_TYPE.NUMBER], false,
),
],
helpString: 'Enter message deletion mode, and auto-deletes last N messages if numeric argument is provided.',
returns: 'The text of the deleted messages.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'cut',
callback: doMesCut,
returns: 'the text of cut messages separated by a newline',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'number or range',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
isRequired: true,
acceptsMultiple: true,
enumProvider: commonEnumProviders.messages(),
}),
],
helpString: `
<div>
Cuts the specified message or continuous chunk from the chat.
</div>
<div>
Ranges are inclusive!
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/cut 0-10</code></pre>
</li>
</ul>
</div>
`,
aliases: [],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'resetpanels',
callback: doResetPanels,
helpString: 'resets UI panels to original state',
aliases: ['resetui'],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'bgcol',
callback: setAvgBG,
helpString: ' WIP test of auto-bg avg coloring',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'theme',
callback: setThemeCallback,
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'theme name',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: () => themes.map(theme => new SlashCommandEnumValue(theme.name)),
}),
],
helpString: `
<div>
Sets a UI theme by name.
</div>
<div>
If no theme name is is provided, this will return the currently active theme.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/theme Cappuccino</code></pre>
</li>
<li>
<pre><code>/theme</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'css-var',
/** @param {{to: string, varname: string }} args @param {string} value @returns {string} */
callback: (args, value) => {
// Map enum to target selector
const targetSelector = {
chat: '#chat',
background: '#bg1',
gallery: '#gallery',
zoomedAvatar: 'div.zoomed_avatar',
}[args.to || 'chat'];
if (!targetSelector) {
toastr.error(`Invalid target: ${args.to}`);
return;
}
if (!args.varname) {
toastr.error('CSS variable name is required');
return;
}
if (!args.varname.startsWith('--')) {
toastr.error('CSS variable names must start with "--"');
return;
}
const elements = document.querySelectorAll(targetSelector);
if (elements.length === 0) {
toastr.error(`No elements found for ${args.to ?? 'chat'} with selector "${targetSelector}"`);
return;
}
elements.forEach(element => {
element.style.setProperty(args.varname, value);
});
console.info(`Set CSS variable "${args.varname}" to "${value}" on "${targetSelector}"`);
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'varname',
description: 'CSS variable name (starting with double dashes)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
}),
SlashCommandNamedArgument.fromProps({
name: 'to',
description: 'The target element to which the CSS variable will be applied',
typeList: [ARGUMENT_TYPE.STRING],
enumList: [
new SlashCommandEnumValue('chat', null, enumTypes.enum, enumIcons.message),
new SlashCommandEnumValue('background', null, enumTypes.enum, enumIcons.image),
new SlashCommandEnumValue('zoomedAvatar', null, enumTypes.enum, enumIcons.character),
new SlashCommandEnumValue('gallery', null, enumTypes.enum, enumIcons.image),
],
defaultValue: 'chat',
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'CSS variable value',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
}),
],
helpString: `
<div>
Sets a CSS variable to a specified value on a target element.
<br />
Only setting of variable names is supported. They have to be prefixed with double dashes ("--exampleVar").
Setting actual CSS properties is not supported. Custom CSS in the theme settings can be used for that.
<br /><br />
<b>This value will be gone after a page reload!</b>
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/css-var varname="--SmartThemeBodyColor" #ff0000</code></pre>
Sets the text color of the chat to red
</li>
<li>
<pre><code>/css-var to=zoomedAvatar varname="--SmartThemeBlurStrength" 0</code></pre>
Remove the blur from the zoomed avatar
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'movingui',
callback: setmovingUIPreset,
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => movingUIPresets.map(preset => new SlashCommandEnumValue(preset.name)),
}),
],
helpString: 'activates a movingUI preset by name',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'stop-strings',
aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
helpString: `
<div>
Sets a list of custom stopping strings. Gets the list if no value is provided.
Use a "force" argument to force set an empty value.
</div>
<div>
<strong>Examples:</strong>
</div>
<ul>
<li>Force set an empty value: <pre><code class="language-stscript">/stop-strings force="true" {{noop}}</code></pre></li>
<li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
<li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
</ul>
`,
returns: ARGUMENT_TYPE.LIST,
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'force',
description: 'force set a value if empty',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'false',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'list of strings',
typeList: [ARGUMENT_TYPE.LIST],
acceptsMultiple: false,
isRequired: false,
}),
],
callback: (args, value) => {
const force = isTrueBoolean(String(args?.force ?? false));
value = String(value ?? '').trim();
// Skip processing if no value and not forced
if (!force && !value) {
return power_user.custom_stopping_strings;
}
// Use empty array for forced empty value
if (force && !value) {
value = JSON.stringify([]);
}
const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
if (!parsedValue || !Array.isArray(parsedValue)) {
throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
}
parsedValue.forEach((item, index) => {
parsedValue[index] = String(item);
});
power_user.custom_stopping_strings = JSON.stringify(parsedValue);
$('#custom_stopping_strings').val(power_user.custom_stopping_strings);
saveSettingsDebounced();
return power_user.custom_stopping_strings;
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'start-reply-with',
helpString: `
<div>
Sets a "Start Reply With". Gets the current value if no value is provided.
Use a "force" argument to force set an empty value.
</div>
<div>
<strong>Examples:</strong>
</div>
<ul>
<li>Set the field value: <pre><code class="language-stscript">/start-reply-with Sure!</code></pre></li>
<li>Force set an empty value: <pre><code class="language-stscript">/start-reply-with force="true" {{noop}}</code></pre></li>
</ul>
`,
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'force',
description: 'force set a value if empty',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'false',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'value',
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
isRequired: false,
}),
],
callback: (args, value) => {
const force = isTrueBoolean(String(args?.force ?? false));
// Skip processing if no value and not forced
if (!force && !value) {
return power_user.user_prompt_bias;
}
power_user.user_prompt_bias = String(value ?? '');
$('#start_reply_with').val(power_user.user_prompt_bias);
saveSettingsDebounced();
return power_user.user_prompt_bias;
},
}));
});