Merge branch 'staging' into parser-followup-2

This commit is contained in:
LenAnderson
2024-06-23 11:37:00 -04:00
47 changed files with 2334 additions and 859 deletions

View File

@ -175,6 +175,11 @@ export class SlashCommand {
}
li.append(specs);
}
const stopgap = document.createElement('span'); {
stopgap.classList.add('stopgap');
stopgap.textContent = '';
li.append(stopgap);
}
const help = document.createElement('span'); {
help.classList.add('help');
const content = document.createElement('span'); {

View File

@ -1,4 +1,5 @@
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
import { SlashCommandScope } from './SlashCommandScope.js';
@ -19,20 +20,18 @@ export const ARGUMENT_TYPE = {
'DICTIONARY': 'dictionary',
};
export class SlashCommandArgument {
/**
* Creates an unnamed argument from a properties object.
* @param {Object} props
* @param {string} props.description description of the argument
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
* @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument)
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider] function that returns auto complete options
* @param {boolean} [props.forceEnum] default: true - whether the input must match one of the enum values
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} [props.typeList=[ARGUMENT_TYPE.STRING]] default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
* @param {boolean} [props.isRequired=false] default: false - whether the argument is required (false = optional argument)
* @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values
* @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
* @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values
*/
static fromProps(props) {
return new SlashCommandArgument(
@ -43,13 +42,10 @@ export class SlashCommandArgument {
props.defaultValue ?? null,
props.enumList ?? [],
props.enumProvider ?? null,
props.forceEnum ?? true,
props.forceEnum ?? false,
);
}
/**@type {string}*/ description;
/**@type {ARGUMENT_TYPE[]}*/ typeList = [];
/**@type {boolean}*/ isRequired = false;
@ -57,8 +53,7 @@ export class SlashCommandArgument {
/**@type {string|SlashCommandClosure}*/ defaultValue;
/**@type {SlashCommandEnumValue[]}*/ enumList = [];
/**@type {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]}*/ enumProvider = null;
/**@type {boolean}*/ forceEnum = true;
/**@type {boolean}*/ forceEnum = false;
/**
* @param {string} description
@ -79,25 +74,26 @@ export class SlashCommandArgument {
});
this.enumProvider = enumProvider;
this.forceEnum = forceEnum;
// If no enums were set explictly and the type is one where we know possible enum values, we set them here
if (!this.enumList.length && this.typeList.includes(ARGUMENT_TYPE.BOOLEAN)) this.enumList = commonEnumProviders.boolean()();
}
}
export class SlashCommandNamedArgument extends SlashCommandArgument {
/**
* Creates an unnamed argument from a properties object.
* @param {Object} props
* @param {string} props.name the argument's name
* @param {string[]} [props.aliasList] list of aliases
* @param {string} props.description description of the argument
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
* @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument)
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider] function that returns auto complete options
* @param {boolean} [props.forceEnum] default: true - whether the input must match one of the enum values
* @param {string[]} [props.aliasList=[]] list of aliases
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} [props.typeList=[ARGUMENT_TYPE.STRING]] default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
* @param {boolean} [props.isRequired=false] default: false - whether the argument is required (false = optional argument)
* @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values
* @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
* @param {boolean} [props.forceEnum=true] default: true - whether the input must match one of the enum values
*/
static fromProps(props) {
return new SlashCommandNamedArgument(
@ -114,21 +110,20 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
);
}
/**@type {string}*/ name;
/**@type {string[]}*/ aliasList = [];
/**
* @param {string} name
* @param {string} description
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
* @param {string|SlashCommandClosure} defaultValue
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
* @param {boolean} forceEnum
* @param {boolean} [isRequired=false]
* @param {boolean} [acceptsMultiple=false]
* @param {string|SlashCommandClosure} [defaultValue=null]
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]]
* @param {string[]} [aliases=[]]
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options
* @param {boolean} [forceEnum=true]
*/
constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = true) {
super(description, types, isRequired, acceptsMultiple, defaultValue, enums, enumProvider, forceEnum);

View File

@ -0,0 +1,232 @@
import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types } from "../../script.js";
import { extension_settings } from "../extensions.js";
import { getGroupMembers, groups, selected_group } from "../group-chats.js";
import { power_user } from "../power-user.js";
import { searchCharByName, getTagsList, tags } from "../tags.js";
import { SlashCommandClosure } from "./SlashCommandClosure.js";
import { SlashCommandEnumValue, enumTypes } from "./SlashCommandEnumValue.js";
import { SlashCommandExecutor } from "./SlashCommandExecutor.js";
/**
* A collection of regularly used enum icons
*/
export const enumIcons = {
default: '◊',
// Variables
variable: '𝑥',
localVariable: 'L',
globalVariable: 'G',
scopeVariable: 'S',
// Common types
character: '👤',
group: '🧑‍🤝‍🧑',
persona: '🧙‍♂️',
qr: 'QR',
closure: '𝑓',
macro: '{{',
tag: '🏷️',
world: '🌐',
preset: '⚙️',
file: '📄',
message: '💬',
voice: '🎤',
true: '✔️',
false: '❌',
// Value types
boolean: '🔲',
string: '📝',
number: '1⃣',
array: '[]',
enum: '📚',
dictionary: '{}',
// Roles
system: '⚙️',
user: '👤',
assistant: '🤖',
// WI Icons
constant: '🔵',
normal: '🟢',
disabled: '❌',
vectorized: '🔗',
/**
* Returns the appropriate state icon based on a boolean
*
* @param {boolean} state - The state to determine the icon for
* @returns {string} The corresponding state icon
*/
getStateIcon: (state) => {
return state ? enumIcons.true : enumIcons.false;
},
/**
* Returns the appropriate WI icon based on the entry
*
* @param {Object} entry - WI entry
* @returns {string} The corresponding WI icon
*/
getWiStatusIcon: (entry) => {
if (entry.constant) return enumIcons.constant;
if (entry.disable) return enumIcons.disabled;
if (entry.vectorized) return enumIcons.vectorized;
return enumIcons.normal;
},
/**
* Returns the appropriate icon based on the role
*
* @param {extension_prompt_roles} role - The role to get the icon for
* @returns {string} The corresponding icon
*/
getRoleIcon: (role) => {
switch (role) {
case extension_prompt_roles.SYSTEM: return enumIcons.system;
case extension_prompt_roles.USER: return enumIcons.user;
case extension_prompt_roles.ASSISTANT: return enumIcons.assistant;
default: return enumIcons.default;
}
},
/**
* A function to get the data type icon
*
* @param {string} type - The type of the data
* @returns {string} The corresponding data type icon
*/
getDataTypeIcon: (type) => {
// Remove possible nullable types definition to match type icon
type = type.replace(/\?$/, '');
return enumIcons[type] ?? enumIcons.default;
}
}
/**
* A collection of common enum providers
*
* Can be used on `SlashCommandNamedArgument` and `SlashCommandArgument` and their `enumProvider` property.
*/
export const commonEnumProviders = {
/**
* Enum values for booleans. Either using true/false or on/off
* Optionally supports "toggle".
*
* @param {('onOff'|'onOffToggle'|'trueFalse')?} [mode='trueFalse'] - The mode to use. Default is 'trueFalse'.
* @returns {() => SlashCommandEnumValue[]}
*/
boolean: (mode = 'trueFalse') => () => {
switch (mode) {
case 'onOff': return [new SlashCommandEnumValue('on', null, 'macro', enumIcons.true), new SlashCommandEnumValue('off', null, 'macro', enumIcons.false)];
case 'onOffToggle': return [new SlashCommandEnumValue('on', null, 'macro', enumIcons.true), new SlashCommandEnumValue('off', null, 'macro', enumIcons.false), new SlashCommandEnumValue('toggle', null, 'macro', enumIcons.boolean)];
case 'trueFalse': return [new SlashCommandEnumValue('true', null, 'macro', enumIcons.true), new SlashCommandEnumValue('false', null, 'macro', enumIcons.false)];
default: throw new Error(`Invalid boolean enum provider mode: ${mode}`);
}
},
/**
* All possible variable names
*
* Can be filtered by `type` to only show global or local variables
*
* @param {...('global'|'local'|'scope'|'all')} type - The type of variables to include in the array. Can be 'all', 'global', or 'local'.
* @returns {() => SlashCommandEnumValue[]}
*/
variables: (...type) => () => {
const types = type.flat();
const isAll = types.includes('all');
return [
...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [],
...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [],
...isAll || types.includes('scope') ? [].map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], // TODO: Add scoped variables here, Lenny
]
},
/**
* All possible char entities, like characters and groups. Can be filtered down to just one type.
*
* @param {('all' | 'character' | 'group')?} [mode='all'] - Which type to return
* @returns {() => SlashCommandEnumValue[]}
*/
characters: (mode = 'all') => () => {
return [
...['all', 'character'].includes(mode) ? characters.map(char => new SlashCommandEnumValue(char.name, null, enumTypes.name, enumIcons.character)) : [],
...['all', 'group'].includes(mode) ? groups.map(group => new SlashCommandEnumValue(group.name, null, enumTypes.qr, enumIcons.group)) : [],
];
},
/**
* All group members of the given group, or default the current active one
*
* @param {string?} groupId - The id of the group - pass in `undefined` to use the current active group
* @returns {() =>SlashCommandEnumValue[]}
*/
groupMembers: (groupId = undefined) => () => getGroupMembers(groupId).map((character, index) => new SlashCommandEnumValue(String(index), character.name, enumTypes.enum, enumIcons.character)),
/**
* All possible personas
*
* @returns {SlashCommandEnumValue[]}
*/
personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
/**
* All possible tags for a given char/group entity
*
* @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show
* @returns {() => SlashCommandEnumValue[]}
*/
tagsForChar: (mode = 'all') => (/** @type {SlashCommandExecutor} */ executor) => {
// Try to see if we can find the char during execution to filter down the tags list some more. Otherwise take all tags.
const charName = executor.namedArgumentList.find(it => it.name == 'name')?.value;
if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');
const key = searchCharByName(substituteParams(charName), { suppressLogging: true });
const assigned = key ? getTagsList(key) : [];
return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
.map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
},
/**
* All messages in the current chat, returning the message id
*
* Optionally supports variable names, and/or a placeholder for the last/new message id
*
* @param {object} [options={}] - Optional arguments
* @param {boolean} [options.allowIdAfter=false] - Whether to add an enum option for the new message id after the last message
* @param {boolean} [options.allowVars=false] - Whether to add enum option for variable names
* @returns {() => SlashCommandEnumValue[]}
*/
messages: ({ allowIdAfter = false, allowVars = false } = {}) => () => {
return [
...chat.map((message, index) => new SlashCommandEnumValue(String(index), `${message.name}: ${message.mes}`, enumTypes.number, message.is_user ? enumIcons.user : message.is_system ? enumIcons.system : enumIcons.assistant)),
...allowIdAfter ? [new SlashCommandEnumValue(String(chat.length), '>> After Last Message >>', enumTypes.enum, '')] : [],
...allowVars ? commonEnumProviders.variables('all')() : [],
];
},
/**
* All existing worlds / lorebooks
*
* @returns {SlashCommandEnumValue[]}
*/
worlds: () => $('#world_info').children().toArray().map(x => new SlashCommandEnumValue(x.textContent, null, enumTypes.name, enumIcons.world)),
/**
* All existing injects for the current chat
*
* @returns {SlashCommandEnumValue[]}
*/
injects: () => {
if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) return [];
return Object.entries(chat_metadata.script_injects)
.map(([id, inject]) => {
const positionName = (Object.entries(extension_prompt_types)).find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
return new SlashCommandEnumValue(id, `${enumIcons.getRoleIcon(inject.role ?? extension_prompt_roles.SYSTEM)}[Inject](${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}) ${inject.value}`,
enumTypes.enum, '💉');
});
},
};

View File

@ -1,18 +1,68 @@
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
import { SlashCommandScope } from './SlashCommandScope.js';
/**
* @typedef {'enum' | 'command' | 'namedArgument' | 'variable' | 'qr' | 'macro' | 'number' | 'name'} EnumType
*/
/**
* Collection of the enum types that can be used with `SlashCommandEnumValue`
*
* Contains documentation on which color this will result to
*/
export const enumTypes = {
/** 'enum' - [string] - light orange @type {EnumType} */
enum: 'enum',
/** 'command' - [cmd] - light yellow @type {EnumType} */
command: 'command',
/** 'namedArgument' - [argName] - sky blue @type {EnumType} */
namedArgument: 'namedArgument',
/** 'variable' - [punctuationL1] - pink @type {EnumType} */
variable: 'variable',
/** 'qr' - [variable] - light blue @type {EnumType} */
qr: 'qr',
/** 'macro' - [variableLanguage] - blue @type {EnumType} */
macro: 'macro',
/** 'number' - [number] - light green @type {EnumType} */
number: 'number',
/** 'name' - [type] - forest green @type {EnumType} */
name: 'name',
/**
* Gets the value of the enum type based on the provided index
*
* Can be used to get differing colors or even random colors, by providing the index of a unique set
*
* @param {number?} index - The index used to retrieve the enum type
* @return {EnumType} The enum type corresponding to the index
*/
getBasedOnIndex(index) {
const keys = Object.keys(this);
return this[keys[(index ?? 0) % keys.length]];
}
}
export class SlashCommandEnumValue {
/**@type {string}*/ value;
/**@type {string}*/ description;
/**@type {string}*/ type = 'enum';
/**@type {EnumType}*/ type = 'enum';
/**@type {string}*/ typeIcon = '◊';
/**@type {(input:string)=>boolean}*/ matchProvider;
/**@type {(input:string)=>string}*/ valueProvider;
/**
* A constructor for creating a SlashCommandEnumValue instance.
*
* @param {string} value - The value
* @param {string?} description - Optional description, displayed in a second line
* @param {EnumType?} type - type of the enum (defining its color)
* @param {string} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones)
*/
constructor(value, description = null, type = 'enum', typeIcon = '◊', matchProvider, valueProvider) {
this.value = value;
this.description = description;
this.type = type;
this.type = type ?? 'enum';
this.typeIcon = typeIcon;
this.matchProvider = matchProvider;
this.valueProvider = valueProvider;

View File

@ -19,6 +19,10 @@ import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js';
import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
import { SlashCommandDebugController } from './SlashCommandDebugController.js';
import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';
/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */
/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */
/**@readonly*/
/**@enum {Number}*/
@ -34,7 +38,7 @@ export class SlashCommandParser {
/**
* @deprecated Use SlashCommandParser.addCommandObject() instead.
* @param {string} command Command name
* @param {(namedArguments:Object.<string,string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} callback The function to execute when the command is called
* @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>} callback callback The function to execute when the command is called
* @param {string[]} aliases List of alternative command names
* @param {string} helpString Help text shown in autocomplete and command browser
*/
@ -134,7 +138,7 @@ export class SlashCommandParser {
description: 'The state of the parser flag to set.',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'on',
enumList: ['on', 'off'],
enumList: commonEnumProviders.boolean('onOff')(),
}),
],
splitUnnamedArgument: true,