Merge branch 'staging' into connection-profile-omit

3b1435ba77fef03f5788b4b666276254156f61cd

Cohee <18619528+Cohee1207@users.noreply.github.com>

5 files changed, +200 -119Showing whitespace changes
public/index.html+13 -3
@@ -6090,7 +6090,10 @@
60906090 <div class="alternate_grettings flexFlowColumn flex-container">
60916091 <div class="title_restorable">
60926092 <h3><span data-i18n="Alternate Greetings" class="mdhotkey_location">Alternate Greetings</span></h3>
6093- <div title="Add" class="menu_button fa-solid fa-plus add_alternate_greeting" data-i18n="[title]Add"></div>
6093+ <div class="menu_button menu_button_icon add_alternate_greeting">
6094+ <i class="fa-solid fa-plus"></i>
6095+ <span data-i18n="Add">Add</span>
6096+ </div>
60946097 </div>
60956098 <small class="justifyLeft" data-i18n="Alternate_Greetings_desc">
60966099 These will be displayed as swipes on the first message when starting a new chat.
@@ -6106,11 +6109,18 @@
61066109 </div>
61076110 <div id="alternate_greeting_form_template" class="template_element">
61086111 <div class="alternate_greeting">
6112+ <details open>
6113+ <summary>
61096114 <div class="title_restorable">
61106115 <strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
61116116 <div class="menu_button fa-solid fa-trash-altmenu_button_icon delete_alternate_greeting"></div>
6117+ <i class="fa-solid fa-trash-alt"></i>
6118+ <span data-i18n="Delete">Delete</span>
61126119 </div>
6113- <textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="16"></textarea>
6120+ </div>
6121+ </summary>
6122+ <textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="12"></textarea>
6123+ </details>
61146124 </div>
61156125 </div>
61166126
public/script.js+29 -9
@@ -7390,7 +7390,7 @@ function onScenarioOverrideRemoveClick() {
73907390 */
73917391export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
73927392 function getOkButtonText() {
73937393 if (['text', 'alternate_greeting', 'char_not_selected'].includes(popup_type)) {
73947394 $dialoguePopupCancel.css('display', 'none');
73957395 return okButton ?? 'Ok';
73967396 } else if (['delete_extension'].includes(popup_type)) {
@@ -7827,24 +7827,42 @@ function openAlternateGreetings() {
78277827
78287828 const template = $('#alternate_greetings_template .alternate_grettings').clone();
78297829 const getArray = () => menu_type == 'create' ? create_save.alternate_greetings : characters[chid].data.alternate_greetings;
7830+ const popup = new Popup(template, POPUP_TYPE.TEXT, '', {
7831+ wide: true,
7832+ large: true,
7833+ allowVerticalScrolling: true,
7834+ onClose: async () => {
7835+ if (menu_type !== 'create') {
7836+ await createOrEditCharacter();
7837+ }
7838+ }
7839+ });
78307840
78317841 for (let index = 0; index < getArray().length; index++) {
78327842 addAlternateGreeting(template, getArray()[index], index, getArray, popup);
78337843 }
78347844
78357845 template.find('.add_alternate_greeting').on('click', function () {
78367846 const array = getArray();
78377847 const index = array.length;
78387848 array.push('');
78397849 addAlternateGreeting(template, '', index, getArray, popup);
78407850 updateAlternateGreetingsHintVisibility(template);
78417851 });
78427852
78437853 updateAlternateGreetingsHintVisibility(template);
7844- callPopup(template, 'alternate_greeting', '', { wide: true, large: true });
7854+ popup.show();
78457855}
78467856
7847-function addAlternateGreeting(template, greeting, index, getArray) {
7857+/**
7858+ * Adds an alternate greeting to the template.
7859+ * @param {JQuery<HTMLElement>} template
7860+ * @param {string} greeting
7861+ * @param {number} index
7862+ * @param {() => any[]} getArray
7863+ * @param {Popup} popup
7864+ */
7865+function addAlternateGreeting(template, greeting, index, getArray, popup) {
78487866 const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone();
78497867 greetingBlock.find('.alternate_greeting_text').on('input', async function () {
78507868 const value = $(this).val();
@@ -7852,11 +7870,16 @@ function addAlternateGreeting(template, greeting, index, getArray) {
78527870 array[index] = value;
78537871 }).val(greeting);
78547872 greetingBlock.find('.greeting_index').text(index + 1);
78557873 greetingBlock.find('.delete_alternate_greeting').on('click', async function (event) {
7874+ event.preventDefault();
7875+ event.stopPropagation();
7876+
78567877 if (confirm('Are you sure you want to delete this alternate greeting?')) {
78577878 const array = getArray();
78587879 array.splice(index, 1);
7880+
78597881 // We need to reopen the popup to update the index numbers
7882+ await popup.complete(POPUP_RESULT.AFFIRMATIVE);
78607883 openAlternateGreetings();
78617884 }
78627885 });
@@ -9574,9 +9597,6 @@ jQuery(async function () {
95749597 }, 2000);
95759598 }
95769599 }
9577- if (popup_type == 'alternate_greeting' && menu_type !== 'create') {
9578- createOrEditCharacter();
9579- }
95809600
95819601 if (dialogueResolve) {
95829602 if (popup_type == 'input') {
public/scripts/slash-commands.js+1 -1
@@ -3469,7 +3469,7 @@ function getModelOptions(quiet) {
34693469 case 'openai':
34703470 return oai_settings.chat_completion_source;
34713471 default:
34723472 return nullResultnull;
34733473 }
34743474 }
34753475
public/scripts/variables.js+137 -106
@@ -12,7 +12,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
1212import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
1313import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
1414import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
1515import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1616
1717/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */
1818/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */
@@ -488,7 +488,7 @@ function existsGlobalVariable(name) {
488488/**
489489 * Parses boolean operands from command arguments.
490490 * @param {object} args Command arguments
491491 * @returns {{a: string | number, b: string | number?, rule: string}} Boolean operands
492492 */
493493export function parseBooleanOperands(args) {
494494 // Resolution order: numeric literal, local variable, global variable, string literal
@@ -497,6 +497,9 @@ export function parseBooleanOperands(args) {
497497 */
498498 function getOperand(operand) {
499499 if (operand === undefined) {
500+ return undefined;
501+ }
502+ if (operand === '') {
500503 return '';
501504 }
502505
@@ -525,8 +528,8 @@ export function parseBooleanOperands(args) {
525528 return stringLiteral || '';
526529 }
527530
528531 const left = getOperand(args.a ||?? args.left ||?? args.first ||?? args.x);
529532 const right = getOperand(args.b ||?? args.right ||?? args.second ||?? args.y);
530533 const rule = args.rule;
531534
532535 return { a: left, b: right, rule };
@@ -534,86 +537,81 @@ export function parseBooleanOperands(args) {
534537
535538/**
536539 * Evaluates a boolean comparison rule.
537- * @param {string} rule Boolean comparison rule
540+ *
541+ * @param {string?} rule Boolean comparison rule
538542 * @param {string|number} a The left operand
539543 * @param {string|number?} b The right operand
540544 * @returns {boolean} True if the rule yields true, false otherwise
541545 */
542546export function evalBoolean(rule, a, b) {
543547 if (!rulea === undefined) {
544- toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command');
548+ throw new Error('Left operand is not provided');
545- throw new Error('Invalid command.');
549+ }
550+
551+ // If right-hand side was not provided, whe just check if the left side is truthy
552+ if (b === undefined) {
553+ switch (rule) {
554+ case undefined:
555+ case 'not': {
556+ const resultOnTruthy = rule !== 'not';
557+ if (isTrueBoolean(String(a))) return resultOnTruthy;
558+ if (isFalseBoolean(String(a))) return !resultOnTruthy;
559+ return a ? resultOnTruthy : !resultOnTruthy;
546560 }
561+ default:
562+ throw new Error(`Unknown boolean comparison rule for truthy check. If right operand is not provided, the rule must not provided or be 'not'. Provided: ${rule}`);
563+ }
564+ }
565+
566+ // If no rule was provided, we are implicitly using 'eq', as defined for the slash commands
567+ rule ??= 'eq';
547568
548- let result = false;
549569 if (typeof a === 'number' && typeof b === 'number') {
550570 // only do numeric comparison if both operands are numbers
551571 const aNumber = Number(a);
552572 const bNumber = Number(b);
553573
554574 switch (rule) {
555- case 'not':
556- result = !aNumber;
557- break;
558575 case 'gt':
559576 result =return aNumber > bNumber;
560- break;
561577 case 'gte':
562578 result =return aNumber >= bNumber;
563- break;
564579 case 'lt':
565580 result =return aNumber < bNumber;
566- break;
567581 case 'lte':
568582 result =return aNumber <= bNumber;
569- break;
570583 case 'eq':
571584 result =return aNumber === bNumber;
572- break;
573585 case 'neq':
574586 result =return aNumber !== bNumber;
587+ case 'in':
588+ case 'nin':
589+ // Fall through to string comparison. Otherwise you could not check if 12345 contains 45 for example.
590+ console.debug(`Boolean comparison rule '${rule}' is not supported for type number. Falling back to string comparison.`);
575591 break;
576592 default:
577593 toastr.errorthrow new Error('`Unknown boolean comparison rule for type number.' Accepted: gt, 'Invalidgte, command'lt, lte, eq, neq. Provided: ${rule}`);
578- throw new Error('Invalid command.');
579- }
580- } else {
581- // otherwise do case-insensitive string comparsion, stringify non-strings
582- let aString;
583- let bString;
584- if (typeof a == 'string') {
585- aString = a.toLowerCase();
586- } else {
587- aString = JSON.stringify(a).toLowerCase();
588594 }
589- if (typeof b == 'string') {
590- bString = b.toLowerCase();
591- } else {
592- bString = JSON.stringify(b).toLowerCase();
593595 }
594596
597+ // otherwise do case-insensitive string comparsion, stringify non-strings
598+ let aString = (typeof a === 'string') ? a.toLowerCase() : JSON.stringify(a).toLowerCase();
599+ let bString = (typeof b === 'string') ? b.toLowerCase() : JSON.stringify(b).toLowerCase();
600+
595601 switch (rule) {
596602 case 'in':
597603 result = return aString.includes(bString);
598- break;
599604 case 'nin':
600605 result = return !aString.includes(bString);
601- break;
602606 case 'eq':
603607 result = return aString === bString;
604- break;
605608 case 'neq':
606609 result = return aString !== bString;
607- break;
608610 default:
609- toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');
611+ throw new Error(`Unknown boolean comparison rule for type number. Accepted: in, nin, eq, neq. Provided: ${rule}`);
610- throw new Error('Invalid command.');
611612 }
612613}
613614
614- return result;
615-}
616-
617615/**
618616 * Executes a slash command from a string (may be enclosed in quotes) and returns the result.
619617 * @param {string} command Command to execute. May contain escaped macro and batch separators.
@@ -1317,32 +1315,36 @@ export function registerVariableCommands() {
13171315 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
13181316 isRequired: true,
13191317 enumProvider: commonEnumProviders.variables('all'),
1320- forceEnum: false,
13211318 }),
13221319 SlashCommandNamedArgument.fromProps({
13231320 name: 'right',
13241321 description: 'right operand',
13251322 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1326- isRequired: true,
13271323 enumProvider: commonEnumProviders.variables('all'),
1328- forceEnum: false,
13291324 }),
13301325 new SlashCommandNamedArgument.fromProps({
1331- 'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [
1326+ name: 'rule',
1332- new SlashCommandEnumValue('gt', 'a > b'),
1327+ description: 'comparison rule',
1333- new SlashCommandEnumValue('gte', 'a >= b'),
1328+ typeList: [ARGUMENT_TYPE.STRING],
1334- new SlashCommandEnumValue('lt', 'a < b'),
1329+ defaultValue: 'eq',
1335- new SlashCommandEnumValue('lte', 'a <= b'),
1330+ enumList: [
13361331 new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
13371332 new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
13381333 new SlashCommandEnumValue('notin', '!a includes b (strings & numbers as strings)'),
13391334 new SlashCommandEnumValue('innin', 'a not includes b (strings & numbers as strings)'),
13401335 new SlashCommandEnumValue('ningt', 'a not includes> b (numbers)'),
1336+ new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
1337+ new SlashCommandEnumValue('lt', 'a < b (numbers)'),
1338+ new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
1339+ new SlashCommandEnumValue('not', '!a (truthy)'),
13411340 ],
1342- ),
1341+ forceEnum: true,
1343- new SlashCommandNamedArgument(
1342+ }),
1344- 'else', 'command to execute if not true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], false,
1343+ SlashCommandNamedArgument.fromProps({
1345- ),
1344+ name: 'else',
1345+ description: 'command to execute if not true',
1346+ typeList: [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND],
1347+ }),
13461348 ],
13471349 unnamedArgumentList: [
13481350 new SlashCommandArgument(
@@ -1360,17 +1362,25 @@ export function registerVariableCommands() {
13601362 Numeric values and string literals for left and right operands supported.
13611363 </div>
13621364 <div>
1365+ If the rule is not provided, it defaults to <code>eq</code>.
1366+ </div>
1367+ <div>
1368+ If no right operand is provided, it defaults to checking the <code>left</code> value to be truthy.
1369+ A non-empty string or non-zero number is considered truthy, as is the value <code>true</code> or <code>on</code>.<br />
1370+ Only acceptable rules for no provided right operand are <code>not</code>, and no provided rule - which default to returning whether it is not or is truthy.
1371+ </div>
1372+ <div>
13631373 <strong>Available rules:</strong>
13641374 <ul>
13651375 <li>gt<code>eq</code> => a >== b <small>(strings & numbers)</small></li>
13661376 <li>gte<code>neq</code> => a >!== b <small>(strings & numbers)</small></li>
13671377 <li>lt<code>in</code> => a <includes b <small>(strings & numbers as strings)</small></li>
1368- <li>lte => a <= b</li>
1378+ <li><code>nin</code> => a not includes b <small>(strings & numbers as strings)</small></li>
13691379 <li>eq<code>gt</code> => a ==> b <small>(numbers)</small></li>
13701380 <li>neq<code>gte</code> => a !>= b <small>(numbers)</small></li>
13711381 <li>not<code>lt</code> => !a < b <small>(numbers)</small></li>
13721382 <li>in (strings)<code>lte</code> => a includes<= b <small>(numbers)</small></li>
1373- <li>nin (strings) => a not includes b</li>
1383+ <li><code>not</code> => !a <small>(truthy)</small></li>
13741384 </ul>
13751385 </div>
13761386 <div>
@@ -1380,6 +1390,17 @@ export function registerVariableCommands() {
13801390 <pre><code class="language-stscript">/if left=score right=10 rule=gte "/speak You win"</code></pre>
13811391 triggers a /speak command if the value of "score" is greater or equals 10.
13821392 </li>
1393+ <li>
1394+ <pre><code class="language-stscript">/if left={{lastMessage}} rule=in right=surprise {: /echo SURPISE! :}</code></pre>
1395+ executes a subcommand defined as a closure if the given value contains a specified word.
1396+ <li>
1397+ <pre><code class="language-stscript">/if left=myContent {: /echo My content had some content. :}</code></pre>
1398+ executes the defined subcommand, if the provided value of left is truthy (contains some kind of contant that is not empty or false)
1399+ </li>
1400+ <li>
1401+ <pre><code class="language-stscript">/if left=tree right={{getvar::object}} {: /echo The object is a tree! :}</code></pre>
1402+ executes the defined subcommand, if the left and right values are equals.
1403+ </li>
13831404 </ul>
13841405 </div>
13851406 `,
@@ -1395,32 +1416,38 @@ export function registerVariableCommands() {
13951416 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
13961417 isRequired: true,
13971418 enumProvider: commonEnumProviders.variables('all'),
1398- forceEnum: false,
13991419 }),
14001420 SlashCommandNamedArgument.fromProps({
14011421 name: 'right',
14021422 description: 'right operand',
14031423 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1404- isRequired: true,
14051424 enumProvider: commonEnumProviders.variables('all'),
1406- forceEnum: false,
14071425 }),
14081426 new SlashCommandNamedArgument.fromProps({
1409- 'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [
1427+ name: 'rule',
1410- new SlashCommandEnumValue('gt', 'a > b'),
1428+ description: 'comparison rule',
1411- new SlashCommandEnumValue('gte', 'a >= b'),
1429+ typeList: [ARGUMENT_TYPE.STRING],
1412- new SlashCommandEnumValue('lt', 'a < b'),
1430+ defaultValue: 'eq',
1413- new SlashCommandEnumValue('lte', 'a <= b'),
1431+ enumList: [
14141432 new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
14151433 new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
14161434 new SlashCommandEnumValue('notin', '!a includes b (strings & numbers as strings)'),
14171435 new SlashCommandEnumValue('innin', 'a not includes b (strings & numbers as strings)'),
14181436 new SlashCommandEnumValue('ningt', 'a not includes> b (numbers)'),
1437+ new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
1438+ new SlashCommandEnumValue('lt', 'a < b (numbers)'),
1439+ new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
1440+ new SlashCommandEnumValue('not', '!a (truthy)'),
14191441 ],
1420- ),
1442+ forceEnum: true,
1421- new SlashCommandNamedArgument(
1443+ }),
1422- 'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOff')(),
1444+ SlashCommandNamedArgument.fromProps({
1423- ),
1445+ name: 'guard',
1446+ description: 'disable loop iteration limit',
1447+ typeList: [ARGUMENT_TYPE.STRING],
1448+ defaultValue: 'off',
1449+ enumList: commonEnumProviders.boolean('onOff')(),
1450+ }),
14241451 ],
14251452 unnamedArgumentList: [
14261453 new SlashCommandArgument(
@@ -1439,15 +1466,15 @@ export function registerVariableCommands() {
14391466 <div>
14401467 <strong>Available rules:</strong>
14411468 <ul>
14421469 <li>gt<code>eq</code> => a >== b <small>(strings & numbers)</small></li>
14431470 <li>gte<code>neq</code> => a >!== b <small>(strings & numbers)</small></li>
14441471 <li>lt<code>in</code> => a <includes b <small>(strings & numbers as strings)</small></li>
1445- <li>lte => a <= b</li>
1472+ <li><code>nin</code> => a not includes b <small>(strings & numbers as strings)</small></li>
14461473 <li>eq<code>gt</code> => a ==> b <small>(numbers)</small></li>
14471474 <li>neq<code>gte</code> => a !>= b <small>(numbers)</small></li>
14481475 <li>not<code>lt</code> => !a < b <small>(numbers)</small></li>
14491476 <li>in (strings)<code>lte</code> => a includes<= b <small>(numbers)</small></li>
1450- <li>nin (strings) => a not includes b</li>
1477+ <li><code>not</code> => !a <small>(truthy)</small></li>
14511478 </ul>
14521479 </div>
14531480 <div>
@@ -1457,7 +1484,11 @@ export function registerVariableCommands() {
14571484 <pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre>
14581485 adds 1 to the value of "i" until it reaches 10.
14591486 </li>
1487+ <li>
1488+ <pre><code class="language-stscript">/while left={{getvar::currentword}} {: /setvar key=currentword {: /do-something-and-return :}() | /echo The current work is "{{getvar::currentword}}" :}</code></pre>
1489+ executes the defined subcommand as long as the "currentword" variable is truthy (has any content that is not false/empty)
14601490 </ul>
1491+ </li>
14611492 </div>
14621493 <div>
14631494 Loops are limited to 100 iterations by default, pass <code>guard=off</code> to disable.
public/style.css+20 -0
@@ -3154,6 +3154,26 @@ grammarly-extension {
31543154 align-items: center;
31553155}
31563156
3157+.alternate_greeting details {
3158+ padding: 2px;
3159+}
3160+
3161+.alternate_greeting summary {
3162+ list-style-position: outside;
3163+ margin-left: 1em;
3164+ padding-left: 1em;
3165+}
3166+
3167+.alternate_greeting textarea {
3168+ field-sizing: content;
3169+ max-height: 50dvh;
3170+}
3171+
3172+.alternate_greeting summary::marker,
3173+.alternate_greeting summary strong {
3174+ cursor: pointer;
3175+}
3176+
31573177#rm_characters_block .form_create_bottom_buttons_block {
31583178 justify-content: space-evenly !important;
31593179 flex-grow: 0;